Files
pikasTech-unidesk/scripts/src/decision-center.ts
T
AgentRun Artificer fc3a095858 fix: preserve decision diary summary
Keep diary summary as an independent Decision Center field when body content is supplied from --body-file, and cover the CLI/backend contract.
2026-06-11 17:35:35 +08:00

624 lines
36 KiB
TypeScript

import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { type UniDeskConfig, repoRoot } from "./config";
import { coreInternalFetch } from "./microservices";
type DecisionRecordType = "meeting" | "decision" | "goal" | "external_goal" | "internal_goal" | "blocker" | "debt" | "experiment";
type RequirementRecordType = Exclude<DecisionRecordType, "meeting">;
type DecisionRecordLevel = "G0" | "G1" | "G2" | "G3" | "P0" | "P1" | "P2" | "P3" | "none";
type DecisionRecordStatus = "active" | "blocked" | "parked" | "done";
type DecisionDocumentType = "DCSN" | "GOAL" | "PLAN" | "RPRT" | "ACTN" | "ISSU" | "RETR" | "RQST" | "RESP" | "MINS";
type DecisionDocumentPriority = "P0" | "P1" | "P2" | "P3";
const serviceId = "decision-center";
const typeValues = new Set<DecisionRecordType>(["meeting", "decision", "goal", "external_goal", "internal_goal", "blocker", "debt", "experiment"]);
const requirementTypeValues = new Set<RequirementRecordType>(["decision", "goal", "external_goal", "internal_goal", "blocker", "debt", "experiment"]);
const levelValues = new Set<DecisionRecordLevel>(["G0", "G1", "G2", "G3", "P0", "P1", "P2", "P3", "none"]);
const statusValues = new Set<DecisionRecordStatus>(["active", "blocked", "parked", "done"]);
const documentTypeValues = new Set<DecisionDocumentType>(["DCSN", "GOAL", "PLAN", "RPRT", "ACTN", "ISSU", "RETR", "RQST", "RESP", "MINS"]);
const documentPriorityValues = new Set<DecisionDocumentPriority>(["P0", "P1", "P2", "P3"]);
function optionValue(args: string[], names: string[]): string | undefined {
for (const name of names) {
const index = args.indexOf(name);
if (index === -1) continue;
const raw = args[index + 1];
if (raw === undefined || raw.length === 0 || raw.startsWith("--")) throw new Error(`${name} requires a non-empty value`);
return raw;
}
return undefined;
}
function optionValues(args: string[], names: string[]): string[] {
const values: string[] = [];
for (let index = 0; index < args.length; index += 1) {
if (!names.includes(args[index] ?? "")) continue;
const raw = args[index + 1];
if (raw === undefined || raw.length === 0 || raw.startsWith("--")) throw new Error(`${args[index]} requires a non-empty value`);
values.push(raw);
index += 1;
}
return values;
}
function positionalArgs(args: string[]): string[] {
const positions: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const value = args[index] ?? "";
if (value.startsWith("--")) {
index += 1;
continue;
}
positions.push(value);
}
return positions;
}
function parseType(raw: string | undefined, fallback: DecisionRecordType): DecisionRecordType {
const value = raw || fallback;
if (!typeValues.has(value as DecisionRecordType)) throw new Error(`--type must be one of: ${Array.from(typeValues).join(", ")}`);
return value as DecisionRecordType;
}
function parseRequirementType(raw: string | undefined, fallback: RequirementRecordType): RequirementRecordType {
const value = raw || fallback;
if (!requirementTypeValues.has(value as RequirementRecordType)) throw new Error(`--type must be one of: ${Array.from(requirementTypeValues).join(", ")}`);
return value as RequirementRecordType;
}
function parseLevel(raw: string | undefined, fallback: DecisionRecordLevel): DecisionRecordLevel {
const value = raw || fallback;
if (!levelValues.has(value as DecisionRecordLevel)) throw new Error(`--level must be one of: ${Array.from(levelValues).join(", ")}`);
return value as DecisionRecordLevel;
}
function parseStatus(raw: string | undefined, fallback: DecisionRecordStatus): DecisionRecordStatus {
const value = raw || fallback;
if (!statusValues.has(value as DecisionRecordStatus)) throw new Error(`--status must be one of: ${Array.from(statusValues).join(", ")}`);
return value as DecisionRecordStatus;
}
function parseDocumentNo(raw: string | undefined): string | undefined {
if (raw === undefined) return undefined;
const value = raw.trim().toUpperCase();
if (!/^DC-(DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS)-P[0-3]-\d{4}-\d{1,9}$/u.test(value)) {
throw new Error("--doc-no must match DC-<TYPE>-<PRIORITY>-<YEAR>-<SEQ>, for example DC-GOAL-P0-2026-001");
}
const [prefix, docType, docPriority, docYear, docSeq] = value.split("-");
return `${prefix}-${docType}-${docPriority}-${docYear}-${String(Number(docSeq)).padStart(3, "0")}`;
}
function parseDocumentType(raw: string | undefined): DecisionDocumentType | undefined {
if (raw === undefined) return undefined;
const value = raw.toUpperCase();
if (!documentTypeValues.has(value as DecisionDocumentType)) throw new Error(`--doc-type must be one of: ${Array.from(documentTypeValues).join(", ")}`);
return value as DecisionDocumentType;
}
function parseDocumentPriority(raw: string | undefined): DecisionDocumentPriority | undefined {
if (raw === undefined) return undefined;
const value = raw.toUpperCase();
if (!documentPriorityValues.has(value as DecisionDocumentPriority)) throw new Error(`--doc-priority must be one of: ${Array.from(documentPriorityValues).join(", ")}`);
return value as DecisionDocumentPriority;
}
function parseDocumentYear(raw: string | undefined): number | undefined {
if (raw === undefined) return undefined;
const value = Number(raw);
if (!/^\d{4}$/u.test(raw) || !Number.isInteger(value) || value < 1970 || value > 2100) throw new Error("--doc-year must be a four-digit year between 1970 and 2100");
return value;
}
function parseDocumentSeq(raw: string | undefined): number | undefined {
if (raw === undefined) return undefined;
const value = Number(raw);
if (!/^\d+$/u.test(raw) || !Number.isInteger(value) || value <= 0) throw new Error("--doc-seq must be a positive integer");
return value;
}
function addDocumentPayloadFields(payload: Record<string, unknown>, args: string[]): void {
const docNo = parseDocumentNo(optionValue(args, ["--doc-no", "--docNo", "--document-no", "--documentNo"]));
const docType = parseDocumentType(optionValue(args, ["--doc-type", "--docType"]));
const docPriority = parseDocumentPriority(optionValue(args, ["--doc-priority", "--docPriority"]));
const docYear = parseDocumentYear(optionValue(args, ["--doc-year", "--docYear", "--year"]));
const docSeq = parseDocumentSeq(optionValue(args, ["--doc-seq", "--docSeq"]));
const signer = optionValue(args, ["--signer"]);
const issuedAt = optionValue(args, ["--issued-at", "--issuedAt"]);
const effectiveScope = optionValue(args, ["--effective-scope", "--effectiveScope"]);
const supersedes = splitList(optionValues(args, ["--supersedes"]));
const supersededBy = splitList(optionValues(args, ["--superseded-by", "--supersededBy"]));
if (docNo !== undefined) payload.docNo = docNo;
if (docType !== undefined) payload.docType = docType;
if (docPriority !== undefined) payload.docPriority = docPriority;
if (docYear !== undefined) payload.docYear = docYear;
if (docSeq !== undefined) payload.docSeq = docSeq;
if (signer !== undefined) payload.signer = signer;
if (issuedAt !== undefined) payload.issuedAt = issuedAt;
if (effectiveScope !== undefined) payload.effectiveScope = effectiveScope;
if (supersedes.length > 0) payload.supersedes = supersedes;
if (supersededBy.length > 0) payload.supersededBy = supersededBy;
}
function splitList(values: string[]): string[] {
return [...new Set(values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean))];
}
function readMarkdownFile(path: string): { absolutePath: string; markdown: string } {
const absolutePath = resolve(repoRoot, path);
const markdown = readFileSync(absolutePath, "utf8");
if (markdown.trim().length === 0) throw new Error(`markdown file is empty: ${absolutePath}`);
if (markdown.length > 4_500_000) throw new Error(`markdown file is too large: ${absolutePath}`);
return { absolutePath, markdown };
}
function optionalBodyFromArgs(args: string[], command: string): { body: string | undefined; bodySource: Record<string, string> } {
const body = optionValue(args, ["--body"]);
const bodyFile = optionValue(args, ["--body-file", "--markdown-file"]);
const markdownFile = optionValue(args, ["--file"]);
const sources = [body !== undefined, bodyFile !== undefined, markdownFile !== undefined].filter(Boolean).length;
if (sources > 1) throw new Error(`${command} accepts only one of --body, --body-file, or --file`);
if (body !== undefined) return { body, bodySource: { kind: "inline" } };
const file = bodyFile ?? markdownFile;
if (file !== undefined) {
const { absolutePath, markdown } = readMarkdownFile(file);
return { body: markdown, bodySource: { kind: "file", path: absolutePath } };
}
return { body: undefined, bodySource: { kind: "none" } };
}
function bodyFromArgs(args: string[], command: string): { body: string; bodySource: Record<string, string> } {
const { body, bodySource } = optionalBodyFromArgs(args, command);
if (body !== undefined) return { body, bodySource };
throw new Error(`${command} requires --body text or --body-file path`);
}
function decisionProxy(path: string, init?: { method?: string; body?: unknown }): unknown {
return coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/proxy${path}`, init);
}
async function decisionProxyAsync(
fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>,
path: string,
init?: { method?: string; body?: unknown },
): Promise<unknown> {
return await fetcher(`/api/microservices/${encodeURIComponent(serviceId)}/proxy${path}`, init);
}
function unwrapProxyResponse(response: unknown): unknown {
const record = typeof response === "object" && response !== null && !Array.isArray(response) ? response as Record<string, unknown> : {};
if (record.ok !== true) return response;
const body = record.body;
return { upstream: { ok: record.ok, status: record.status }, body };
}
function uploadMeeting(args: string[]): unknown {
const file = positionalArgs(args)[0];
if (!file) throw new Error("decision upload requires markdown file");
const { absolutePath, markdown } = readMarkdownFile(file);
const type = parseType(optionValue(args, ["--type"]), "meeting");
const payload = {
markdown,
title: optionValue(args, ["--title"]),
type,
level: parseLevel(optionValue(args, ["--level", "--priority"]), "none"),
status: parseStatus(optionValue(args, ["--status"]), "active"),
linkedGoalId: optionValue(args, ["--linked-goal-id", "--linkedGoalId"]),
tags: splitList(optionValues(args, ["--tag", "--tags"])),
evidenceLinks: splitList(optionValues(args, ["--evidence", "--evidence-link", "--evidenceLinks"])),
source: optionValue(args, ["--source"]),
sourceSession: optionValue(args, ["--source-session", "--sourceSession"]),
issueId: optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]),
taskId: optionValue(args, ["--task-id", "--taskId"]),
commitId: optionValue(args, ["--commit-id", "--commitId"]),
};
addDocumentPayloadFields(payload, args);
const endpoint = type === "meeting" ? "/api/meetings/import" : "/api/records";
const body = type === "meeting" ? payload : { ...payload, body: markdown };
return { file: absolutePath, result: unwrapProxyResponse(decisionProxy(endpoint, { method: "POST", body })) };
}
async function uploadMeetingAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
const file = positionalArgs(args)[0];
if (!file) throw new Error("decision upload requires markdown file");
const { absolutePath, markdown } = readMarkdownFile(file);
const type = parseType(optionValue(args, ["--type"]), "meeting");
const payload = {
markdown,
title: optionValue(args, ["--title"]),
type,
level: parseLevel(optionValue(args, ["--level", "--priority"]), "none"),
status: parseStatus(optionValue(args, ["--status"]), "active"),
linkedGoalId: optionValue(args, ["--linked-goal-id", "--linkedGoalId"]),
tags: splitList(optionValues(args, ["--tag", "--tags"])),
evidenceLinks: splitList(optionValues(args, ["--evidence", "--evidence-link", "--evidenceLinks"])),
source: optionValue(args, ["--source"]),
sourceSession: optionValue(args, ["--source-session", "--sourceSession"]),
issueId: optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]),
taskId: optionValue(args, ["--task-id", "--taskId"]),
commitId: optionValue(args, ["--commit-id", "--commitId"]),
};
addDocumentPayloadFields(payload, args);
const endpoint = type === "meeting" ? "/api/meetings/import" : "/api/records";
const body = type === "meeting" ? payload : { ...payload, body: markdown };
return { file: absolutePath, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, endpoint, { method: "POST", body })) };
}
function summarizeDiaryImportResult(result: unknown, includeEntries: boolean): unknown {
const body = typeof result === "object" && result !== null && !Array.isArray(result) ? result as Record<string, unknown> : {};
const entries = Array.isArray(body.entries) ? body.entries : [];
const summaryEntries = entries.map((item) => {
const record = typeof item === "object" && item !== null && !Array.isArray(item) ? item as Record<string, unknown> : {};
return {
id: record.id,
date: record.date,
month: record.month,
title: record.title,
markdownPath: record.markdownPath,
summary: record.summary,
sourceFile: record.sourceFile,
updatedAt: record.updatedAt,
};
});
return {
...body,
entries: includeEntries ? summaryEntries : summaryEntries.slice(0, 20),
entriesOmitted: includeEntries ? 0 : Math.max(0, summaryEntries.length - 20),
outputPolicy: includeEntries
? { bounded: false, entries: summaryEntries.length }
: { bounded: true, entriesShown: Math.min(summaryEntries.length, 20), fullCommand: "Re-run with --include-entries to show every imported day summary." },
};
}
function diaryImportPayload(args: string[]): { absolutePath: string; payload: Record<string, unknown> } {
const file = positionalArgs(args)[0];
if (!file) throw new Error("decision diary import requires markdown file");
const { absolutePath, markdown } = readMarkdownFile(file);
return {
absolutePath,
payload: {
markdown,
sourceFile: optionValue(args, ["--source-file", "--source-path", "--source"]) ?? absolutePath,
tags: splitList(optionValues(args, ["--tag", "--tags"])),
},
};
}
function importDiary(args: string[]): unknown {
const { absolutePath, payload } = diaryImportPayload(args);
const result = unwrapProxyResponse(decisionProxy("/api/diary/import", { method: "POST", body: payload }));
const body = typeof result === "object" && result !== null && !Array.isArray(result) && "body" in result
? (result as { body?: unknown }).body
: result;
return { file: absolutePath, result: { ...(typeof result === "object" && result !== null && !Array.isArray(result) && "upstream" in result ? { upstream: (result as { upstream?: unknown }).upstream } : {}), body: summarizeDiaryImportResult(body, args.includes("--include-entries")) } };
}
async function importDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
const { absolutePath, payload } = diaryImportPayload(args);
const result = unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/import", { method: "POST", body: payload }));
const body = typeof result === "object" && result !== null && !Array.isArray(result) && "body" in result
? (result as { body?: unknown }).body
: result;
return { file: absolutePath, result: { ...(typeof result === "object" && result !== null && !Array.isArray(result) && "upstream" in result ? { upstream: (result as { upstream?: unknown }).upstream } : {}), body: summarizeDiaryImportResult(body, args.includes("--include-entries")) } };
}
function listRecords(args: string[]): unknown {
const query = recordQuery(args);
return unwrapProxyResponse(decisionProxy(`/api/records${query ? `?${query}` : ""}`));
}
async function listRecordsAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
const query = recordQuery(args);
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records${query ? `?${query}` : ""}`));
}
function recordQuery(args: string[], options: { requirementOnly?: boolean } = {}): string {
const params = new URLSearchParams();
const type = optionValue(args, ["--type"]);
const status = optionValue(args, ["--status"]);
const level = optionValue(args, ["--level", "--priority"]);
const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]);
const source = optionValue(args, ["--source"]);
const issueId = optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]);
const tag = optionValue(args, ["--tag", "--tags"]);
const queryText = optionValue(args, ["--query", "--q"]);
const limit = optionValue(args, ["--limit"]);
const docNo = parseDocumentNo(optionValue(args, ["--doc-no", "--docNo", "--document-no", "--documentNo"]));
const docType = parseDocumentType(optionValue(args, ["--doc-type", "--docType"]));
const docPriority = parseDocumentPriority(optionValue(args, ["--doc-priority", "--docPriority"]));
const docYear = parseDocumentYear(optionValue(args, ["--doc-year", "--docYear", "--year"]));
if (docNo !== undefined) params.set("docNo", docNo);
if (docType !== undefined) params.set("docType", docType);
if (docPriority !== undefined) params.set("docPriority", docPriority);
if (docYear !== undefined) params.set("docYear", String(docYear));
if (type !== undefined) params.set("type", options.requirementOnly === true ? parseRequirementType(type, "goal") : parseType(type, "meeting"));
if (status !== undefined) params.set("status", parseStatus(status, "active"));
if (level !== undefined) params.set("level", parseLevel(level, "none"));
if (linkedGoalId !== undefined) params.set("linkedGoalId", linkedGoalId);
if (source !== undefined) params.set("source", source);
if (issueId !== undefined) params.set("issueId", issueId);
if (tag !== undefined) params.set("tag", tag);
if (queryText !== undefined) params.set("q", queryText);
if (limit !== undefined) params.set("limit", limit);
if (args.includes("--include-body")) params.set("includeBody", "true");
return params.toString();
}
function diaryQuery(args: string[]): string {
const params = new URLSearchParams();
const month = optionValue(args, ["--month"]);
const from = optionValue(args, ["--from"]);
const to = optionValue(args, ["--to"]);
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
const limit = optionValue(args, ["--limit"]);
if (month !== undefined) params.set("month", month);
if (from !== undefined) params.set("from", from);
if (to !== undefined) params.set("to", to);
if (sourceFile !== undefined) params.set("sourceFile", sourceFile);
if (limit !== undefined) params.set("limit", limit);
if (args.includes("--include-body")) params.set("includeBody", "true");
const query = params.toString();
return query ? `?${query}` : "";
}
function listDiary(args: string[]): unknown {
return unwrapProxyResponse(decisionProxy(`/api/diary/entries${diaryQuery(args)}`));
}
async function listDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries${diaryQuery(args)}`));
}
function diaryHistory(args: string[]): unknown {
return unwrapProxyResponse(decisionProxy(`/api/diary/history${diaryQuery(args)}`));
}
async function diaryHistoryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/history${diaryQuery(args)}`));
}
function listDiaryMonths(): unknown {
return unwrapProxyResponse(decisionProxy("/api/diary/months"));
}
async function listDiaryMonthsAsync(fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/months"));
}
function diaryShowQuery(args: string[]): string {
const params = new URLSearchParams();
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
if (sourceFile !== undefined) params.set("sourceFile", sourceFile);
const query = params.toString();
return query ? `?${query}` : "";
}
function showDiary(key: string | undefined, args: string[] = []): unknown {
if (!key) throw new Error("decision diary show requires entry id or YYYY-MM-DD date");
return unwrapProxyResponse(decisionProxy(`/api/diary/entries/${encodeURIComponent(key)}${diaryShowQuery(args)}`));
}
async function showDiaryAsync(key: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>, args: string[] = []): Promise<unknown> {
if (!key) throw new Error("decision diary show requires entry id or YYYY-MM-DD date");
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries/${encodeURIComponent(key)}${diaryShowQuery(args)}`));
}
function todayDiary(): unknown {
return unwrapProxyResponse(decisionProxy("/api/diary/today"));
}
async function todayDiaryAsync(fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/today"));
}
function diaryEditPayload(args: string[], command: string): { key: string; payload: Record<string, unknown>; bodySource: Record<string, string> } {
const key = positionalArgs(args)[0];
if (!key) throw new Error(`${command} requires entry id or YYYY-MM-DD date`);
const { body, bodySource } = optionalBodyFromArgs(args, command);
const payload: Record<string, unknown> = {};
const title = optionValue(args, ["--title"]);
const summary = optionValue(args, ["--summary"]);
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
if (body !== undefined) payload.body = body;
if (title !== undefined) payload.title = title;
if (summary !== undefined) payload.summary = summary;
if (sourceFile !== undefined) payload.sourceFile = sourceFile;
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
if (tags.length > 0) payload.tags = tags;
if (Object.keys(payload).length === 0) throw new Error(`${command} requires --body text, --body-file path, --summary, --title, --source-file, or --tag`);
return { key, payload, bodySource };
}
function editDiary(args: string[]): unknown {
const { key, payload, bodySource } = diaryEditPayload(args, "decision diary edit");
return { key, bodySource, result: unwrapProxyResponse(decisionProxy(`/api/diary/entries/${encodeURIComponent(key)}`, { method: "PUT", body: payload })) };
}
async function editDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
const { key, payload, bodySource } = diaryEditPayload(args, "decision diary edit");
return { key, bodySource, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries/${encodeURIComponent(key)}`, { method: "PUT", body: payload })) };
}
function editTodayDiary(args: string[]): unknown {
const { body, bodySource } = optionalBodyFromArgs(args, "decision diary today --edit");
const payload: Record<string, unknown> = {};
const title = optionValue(args, ["--title"]);
const summary = optionValue(args, ["--summary"]);
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
if (body !== undefined) payload.body = body;
if (title !== undefined) payload.title = title;
if (summary !== undefined) payload.summary = summary;
if (sourceFile !== undefined) payload.sourceFile = sourceFile;
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
if (tags.length > 0) payload.tags = tags;
if (Object.keys(payload).length === 0) throw new Error("decision diary today --edit requires --body text, --body-file path, --summary, --title, --source-file, or --tag");
return { bodySource, result: unwrapProxyResponse(decisionProxy("/api/diary/today", { method: "PUT", body: payload })) };
}
async function editTodayDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
const { body, bodySource } = optionalBodyFromArgs(args, "decision diary today --edit");
const payload: Record<string, unknown> = {};
const title = optionValue(args, ["--title"]);
const summary = optionValue(args, ["--summary"]);
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
if (body !== undefined) payload.body = body;
if (title !== undefined) payload.title = title;
if (summary !== undefined) payload.summary = summary;
if (sourceFile !== undefined) payload.sourceFile = sourceFile;
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
if (tags.length > 0) payload.tags = tags;
if (Object.keys(payload).length === 0) throw new Error("decision diary today --edit requires --body text, --body-file path, --summary, --title, --source-file, or --tag");
return { bodySource, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/today", { method: "PUT", body: payload })) };
}
function showRecord(id: string | undefined): unknown {
if (!id) throw new Error("decision show requires record id");
return unwrapProxyResponse(decisionProxy(`/api/records/${encodeURIComponent(id)}`));
}
async function showRecordAsync(id: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
if (!id) throw new Error("decision show requires record id");
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/records/${encodeURIComponent(id)}`));
}
function requirementPayload(args: string[], command: string, options: { partial?: boolean; includeId?: boolean } = {}): Record<string, unknown> {
const title = optionValue(args, ["--title"]);
const bodyArg = optionValue(args, ["--body"]);
const bodyFile = optionValue(args, ["--body-file", "--markdown-file", "--file"]);
const body = bodyArg !== undefined ? bodyArg : bodyFile !== undefined ? readMarkdownFile(bodyFile).markdown : undefined;
const payload: Record<string, unknown> = {};
const id = optionValue(args, ["--id"]);
const type = optionValue(args, ["--type"]);
const level = optionValue(args, ["--level", "--priority"]);
const status = optionValue(args, ["--status"]);
const linkedGoalId = optionValue(args, ["--linked-goal-id", "--linkedGoalId"]);
const source = optionValue(args, ["--source"]);
const sourceSession = optionValue(args, ["--source-session", "--sourceSession"]);
const issueId = optionValue(args, ["--issue", "--issue-id", "--issueId", "--linked-issue", "--linked-task"]);
const taskId = optionValue(args, ["--task-id", "--taskId"]);
const commitId = optionValue(args, ["--commit-id", "--commitId"]);
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
const evidenceLinks = splitList(optionValues(args, ["--evidence", "--evidence-link", "--evidenceLinks"]));
if (options.includeId !== false && id !== undefined) payload.id = id;
if (title !== undefined) payload.title = title;
if (body !== undefined) payload.body = body;
if (type !== undefined || options.partial !== true) payload.type = parseRequirementType(type, "external_goal");
if (level !== undefined || options.partial !== true) payload.level = parseLevel(level, "none");
if (status !== undefined || options.partial !== true) payload.status = parseStatus(status, "active");
if (linkedGoalId !== undefined) payload.linkedGoalId = linkedGoalId;
if (tags.length > 0 || options.partial !== true) payload.tags = tags;
if (evidenceLinks.length > 0 || options.partial !== true) payload.evidenceLinks = evidenceLinks;
if (source !== undefined) payload.source = source;
if (sourceSession !== undefined) payload.sourceSession = sourceSession;
if (issueId !== undefined) payload.issueId = issueId;
if (taskId !== undefined) payload.taskId = taskId;
if (commitId !== undefined) payload.commitId = commitId;
addDocumentPayloadFields(payload, args);
if (Object.keys(payload).length === 0) throw new Error(`${command} requires at least one field to write`);
if (options.partial !== true && title === undefined && body === undefined) throw new Error(`${command} requires --title or --body-file/--body`);
return payload;
}
function createRequirement(args: string[]): unknown {
return unwrapProxyResponse(decisionProxy("/api/requirements", { method: "POST", body: requirementPayload(args, "decision requirement create") }));
}
async function createRequirementAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/requirements", { method: "POST", body: requirementPayload(args, "decision requirement create") }));
}
function upsertRequirement(args: string[]): unknown {
return unwrapProxyResponse(decisionProxy("/api/requirements", { method: "PUT", body: requirementPayload(args, "decision requirement upsert") }));
}
async function upsertRequirementAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/requirements", { method: "PUT", body: requirementPayload(args, "decision requirement upsert") }));
}
function showRequirement(id: string | undefined): unknown {
if (!id) throw new Error("decision requirement show requires record id or docNo");
return unwrapProxyResponse(decisionProxy(`/api/requirements/${encodeURIComponent(id)}`));
}
async function showRequirementAsync(id: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
if (!id) throw new Error("decision requirement show requires record id or docNo");
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/requirements/${encodeURIComponent(id)}`));
}
function updateRequirement(id: string | undefined, args: string[]): unknown {
if (!id) throw new Error("decision requirement update requires record id or docNo");
return unwrapProxyResponse(decisionProxy(`/api/requirements/${encodeURIComponent(id)}`, { method: "PUT", body: requirementPayload(args, "decision requirement update", { partial: true, includeId: false }) }));
}
async function updateRequirementAsync(id: string | undefined, args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
if (!id) throw new Error("decision requirement update requires record id or docNo");
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/requirements/${encodeURIComponent(id)}`, { method: "PUT", body: requirementPayload(args, "decision requirement update", { partial: true, includeId: false }) }));
}
export async function runDecisionCenterCommand(_config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "list", id] = args;
if (action === "diary") {
const [diaryAction = "list", diaryId] = args.slice(1);
if (diaryAction === "import") return importDiary(args.slice(2));
if (diaryAction === "list") return listDiary(args.slice(2));
if (diaryAction === "history") return diaryHistory(args.slice(2));
if (diaryAction === "months") return listDiaryMonths();
if (diaryAction === "today") return args.includes("--edit") ? editTodayDiary(args.slice(2).filter((arg) => arg !== "--edit")) : todayDiary();
if (diaryAction === "show") return showDiary(diaryId, args.slice(3));
if (diaryAction === "edit" || diaryAction === "upsert") return editDiary(args.slice(2));
throw new Error("decision diary command must be one of: import, list, history, months, today, show, edit, upsert");
}
if (action === "requirement" || action === "requirements") {
const [requirementAction = "list", requirementId] = args.slice(1);
if (requirementAction === "list") {
const query = recordQuery(args.slice(2), { requirementOnly: true });
return unwrapProxyResponse(decisionProxy(`/api/requirements${query ? `?${query}` : ""}`));
}
if (requirementAction === "create") return createRequirement(args.slice(2));
if (requirementAction === "upsert") return upsertRequirement(args.slice(2));
if (requirementAction === "show") return showRequirement(requirementId);
if (requirementAction === "update") return updateRequirement(requirementId, args.slice(3));
throw new Error("decision requirement command must be one of: list, create, show, update, upsert");
}
if (action === "upload") return uploadMeeting(args.slice(1));
if (action === "list") return listRecords(args.slice(1));
if (action === "show") return showRecord(id);
if (action === "health") return unwrapProxyResponse(coreInternalFetch(`/api/microservices/${encodeURIComponent(serviceId)}/health`));
throw new Error("decision command must be one of: upload, list, show, health, requirement, diary");
}
export async function runDecisionCenterCommandAsync(
_config: UniDeskConfig,
args: string[],
fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>,
): Promise<unknown> {
const [action = "list", id] = args;
if (action === "diary") {
const [diaryAction = "list", diaryId] = args.slice(1);
if (diaryAction === "import") return importDiaryAsync(args.slice(2), fetcher);
if (diaryAction === "list") return listDiaryAsync(args.slice(2), fetcher);
if (diaryAction === "history") return diaryHistoryAsync(args.slice(2), fetcher);
if (diaryAction === "months") return listDiaryMonthsAsync(fetcher);
if (diaryAction === "today") return args.includes("--edit") ? editTodayDiaryAsync(args.slice(2).filter((arg) => arg !== "--edit"), fetcher) : todayDiaryAsync(fetcher);
if (diaryAction === "show") return showDiaryAsync(diaryId, fetcher, args.slice(3));
if (diaryAction === "edit" || diaryAction === "upsert") return editDiaryAsync(args.slice(2), fetcher);
throw new Error("decision diary command must be one of: import, list, history, months, today, show, edit, upsert");
}
if (action === "requirement" || action === "requirements") {
const [requirementAction = "list", requirementId] = args.slice(1);
if (requirementAction === "list") {
const query = recordQuery(args.slice(2), { requirementOnly: true });
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/requirements${query ? `?${query}` : ""}`));
}
if (requirementAction === "create") return createRequirementAsync(args.slice(2), fetcher);
if (requirementAction === "upsert") return upsertRequirementAsync(args.slice(2), fetcher);
if (requirementAction === "show") return showRequirementAsync(requirementId, fetcher);
if (requirementAction === "update") return updateRequirementAsync(requirementId, args.slice(3), fetcher);
throw new Error("decision requirement command must be one of: list, create, show, update, upsert");
}
if (action === "upload") return uploadMeetingAsync(args.slice(1), fetcher);
if (action === "list") return listRecordsAsync(args.slice(1), fetcher);
if (action === "show") return showRecordAsync(id, fetcher);
if (action === "health") return unwrapProxyResponse(await fetcher(`/api/microservices/${encodeURIComponent(serviceId)}/health`));
throw new Error("decision command must be one of: upload, list, show, health, requirement, diary");
}