#!/usr/bin/env node import type { CasRef, ThreadId, ThreadStatus } from "@united-workforce/protocol"; import { Command } from "commander"; import { cmdConfigGet, cmdConfigList, cmdConfigSet } from "./commands/config.js"; import { cmdLogClean, cmdLogList, cmdLogShow } from "./commands/log.js"; import { cmdPromptAdapter, cmdPromptAuthor, cmdPromptBootstrap, cmdPromptDeveloper, cmdPromptList, cmdPromptSetup, cmdPromptUsage, cmdPromptUser, } from "./commands/prompt.js"; import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js"; import { cmdStepFork, cmdStepList, cmdStepRead, cmdStepShow } from "./commands/step.js"; import { cmdThreadCancel, cmdThreadExec, cmdThreadList, cmdThreadRead, cmdThreadResume, cmdThreadShow, cmdThreadStart, cmdThreadStop, THREAD_READ_DEFAULT_QUOTA, } from "./commands/thread.js"; import { parseTimeInput } from "./commands/thread-time-parser.js"; import { cmdWorkflowAdd, cmdWorkflowList, cmdWorkflowShow } from "./commands/workflow.js"; import { formatOutput, type OutputFormat } from "./format.js"; import { resolveStorageRoot } from "./store.js"; function writeOutput(data: unknown): void { const fmt = program.opts().format as OutputFormat; process.stdout.write(`${formatOutput(data, fmt)}\n`); } function runAction(action: () => Promise): void { action().catch((e: unknown) => { const message = e instanceof Error ? e.message : String(e); process.stderr.write(`${message}\n`); process.exit(1); }); } const program = new Command(); // eslint-disable-next-line -- dynamic import for version const pkg = await import("../package.json", { with: { type: "json" } }); program .name("uwf") .description( "Stateless workflow CLI\n\n" + "Four-layer architecture:\n" + " workflow → thread → step → turn", ) .version(pkg.default.version, "-V, --version"); program.option("--format ", "Output format: json or yaml", "json"); const workflow = program .command("workflow") .description("Workflow definitions (layer 1: templates)"); workflow .command("add") .description("Register a workflow from YAML") .argument("", "Workflow YAML file") .action((file: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdWorkflowAdd(storageRoot, file); writeOutput(result); }); }); workflow .command("show") .description("Show a workflow by name or CAS hash") .argument("", "Workflow name or hash") .action((id: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdWorkflowShow(storageRoot, id); writeOutput(result); }); }); workflow .command("list") .description("List registered workflows") .action(() => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdWorkflowList(storageRoot, process.cwd()); writeOutput(result); }); }); const thread = program.command("thread").description("Thread execution (layer 2: instances)"); thread .command("start") .description("Create a thread without executing") .argument("", "Workflow name or hash") .requiredOption("-p, --prompt ", "User prompt") .option("--cwd ", "Working directory for thread execution (default: process.cwd())") .action((workflow: string, opts: { prompt: string; cwd: string | undefined }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdThreadStart( storageRoot, workflow, opts.prompt, process.cwd(), opts.cwd ?? process.cwd(), ); writeOutput(result); }); }); thread .command("exec") .description("Execute one or more steps") .argument("", "Thread ULID") .option("--agent ", "Override agent command") .option("-c, --count ", "Number of steps to run (default: 1)") .option("--background", "Run in background and return immediately") .option("--_background-worker", "Internal flag for background worker process", false) .action( ( threadId: string, opts: { agent: string | undefined; count: string | undefined; background: boolean; _backgroundWorker: boolean; }, ) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const agentOverride = opts.agent ?? null; const count = opts.count !== undefined ? Number(opts.count) : 1; const background = opts.background ?? false; const backgroundWorker = opts._backgroundWorker ?? false; const results = await cmdThreadExec( storageRoot, threadId, agentOverride, count, background, backgroundWorker, ); if (results.length === 1) { writeOutput(results[0]); } else { writeOutput(results); } }); }, ); thread .command("show") .description("Show thread head pointer") .argument("", "Thread ULID") .action((threadId: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdThreadShow(storageRoot, threadId); writeOutput(result); }); }); // Helper functions for thread list command parsing function parseStatusFilter(status: string | undefined): ThreadStatus[] | null { if (status === undefined) return null; const raw = status.trim(); if (raw === "active") return ["idle", "running"]; const parts = raw.split(",").map((s) => s.trim()); const validStatuses: ThreadStatus[] = ["idle", "running", "suspended", "completed", "cancelled"]; for (const part of parts) { if (!validStatuses.includes(part as ThreadStatus)) { process.stderr.write( `Invalid status: ${part}. Must be one of: idle, running, suspended, completed, cancelled, active\n`, ); process.exit(1); } } return parts as ThreadStatus[]; } function parseTimeFilters( after: string | undefined, before: string | undefined, nowMs: number, ): { afterMs: number | null; beforeMs: number | null } { try { const afterMs = after !== undefined ? parseTimeInput(after, nowMs) : null; const beforeMs = before !== undefined ? parseTimeInput(before, nowMs) : null; return { afterMs, beforeMs }; } catch (e) { const message = e instanceof Error ? e.message : String(e); process.stderr.write(`${message}\n`); process.exit(1); } } function parsePaginationOptions( skip: string | undefined, take: string | undefined, ): { skip: number | null; take: number | null } { let skipVal: number | null = null; let takeVal: number | null = null; if (skip !== undefined) { skipVal = Number.parseInt(skip, 10); if (!Number.isInteger(skipVal) || skipVal < 0) { process.stderr.write("--skip must be a non-negative integer\n"); process.exit(1); } } if (take !== undefined) { takeVal = Number.parseInt(take, 10); if (!Number.isInteger(takeVal) || takeVal < 1) { process.stderr.write("--take must be a positive integer\n"); process.exit(1); } } return { skip: skipVal, take: takeVal }; } thread .command("list") .description("List threads") .option( "--status ", "Filter by status: idle, running, completed, cancelled, active (idle+running), or comma-separated values", ) .option("--after ", "Filter threads created after this date (ISO or relative like '7d')") .option("--before ", "Filter threads created before this date (ISO or relative like '7d')") .option("--skip ", "Skip first n threads") .option("--take ", "Return at most n threads") .action( (opts: { status: string | undefined; after: string | undefined; before: string | undefined; skip: string | undefined; take: string | undefined; }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const statusFilter = parseStatusFilter(opts.status); const nowMs = Date.now(); const { afterMs, beforeMs } = parseTimeFilters(opts.after, opts.before, nowMs); const { skip, take } = parsePaginationOptions(opts.skip, opts.take); const result = await cmdThreadList( storageRoot, statusFilter, afterMs, beforeMs, skip, take, ); writeOutput(result); }); }, ); thread .command("resume") .description("Resume a suspended thread and re-run the suspended role") .argument("", "Thread ULID") .option("-p, --prompt ", "Supplementary info to append to the resume prompt") .option("--agent ", "Override agent command") .action((threadId: string, opts: { prompt: string | undefined; agent: string | undefined }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const supplement = opts.prompt ?? null; const agentOverride = opts.agent ?? null; const result = await cmdThreadResume( storageRoot, threadId as ThreadId, supplement, agentOverride, ); writeOutput(result); }); }); thread .command("stop") .description("Stop background execution of a thread (keep thread active)") .argument("", "Thread ULID") .action((threadId: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdThreadStop(storageRoot, threadId); writeOutput(result); }); }); thread .command("cancel") .description("Cancel a thread (stop execution and move to history)") .argument("", "Thread ULID") .action((threadId: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdThreadCancel(storageRoot, threadId); writeOutput(result); }); }); thread .command("read") .description("Read thread context as human-readable markdown") .argument("", "Thread ULID") .option("--quota ", "Max output characters", String(THREAD_READ_DEFAULT_QUOTA)) .option("--before ", "Load steps before this hash (exclusive)") .option("--start", "Include start step in output") .action( (threadId: string, opts: { quota: string; before: string | undefined; start: boolean }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const quota = Number.parseInt(opts.quota, 10); if (!Number.isFinite(quota) || quota < 1) { process.stderr.write("invalid --quota: must be a positive integer\n"); process.exit(1); } const before = opts.before ?? null; const markdown = await cmdThreadRead( storageRoot, threadId as ThreadId, quota, before, opts.start ?? false, ); process.stdout.write(markdown.endsWith("\n") ? markdown : `${markdown}\n`); }); }, ); const step = program.command("step").description("Step results (layer 3: single cycle)"); step .command("list") .description("List all steps in a thread") .argument("", "Thread ULID") .action((threadId: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdStepList(storageRoot, threadId); writeOutput(result); }); }); step .command("show") .description("Show details of a specific step") .argument("", "CAS hash of the StepNode") .action((stepHash: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const detail = await cmdStepShow(storageRoot, stepHash as CasRef); writeOutput(detail); }); }); step .command("read") .description("Read a step's turns as human-readable markdown") .argument("", "CAS hash of the StepNode") .option("--quota ", "Max output characters", "4000") .option("--prompt", "Show the assembled prompt sent to the agent instead of turns") .action((stepHash: string, opts: { quota: string; prompt: boolean }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const quota = Number.parseInt(opts.quota, 10); if (!Number.isFinite(quota) || quota < 1) { process.stderr.write("invalid --quota: must be a positive integer\n"); process.exit(1); } const markdown = await cmdStepRead( storageRoot, stepHash as CasRef, quota, opts.prompt === true, ); process.stdout.write(markdown.endsWith("\n") ? markdown : `${markdown}\n`); }); }); step .command("fork") .description("Fork a thread from a specific step") .argument("", "CAS hash of the StartNode or StepNode to fork from") .action((stepHash: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdStepFork(storageRoot, stepHash as CasRef); writeOutput(result); }); }); // ── Deprecation Handlers ────────────────────────────────────────────────────── // These commands have been removed. Show helpful error messages. workflow .command("put") .description("[DEPRECATED] Use 'workflow add' instead") .argument("", "Workflow YAML file") .action(() => { process.stderr.write(`Error: Command 'workflow put' has been removed. Use 'workflow add' instead. For more information, see: uwf help workflow add `); process.exit(1); }); thread .command("step") .description("[DEPRECATED] Use 'thread exec' instead") .argument("", "Thread ULID") .allowUnknownOption() .action(() => { process.stderr.write(`Error: Command 'thread step' has been removed. Use 'thread exec' instead. For more information, see: uwf help thread exec `); process.exit(1); }); thread .command("steps") .description("[DEPRECATED] Use 'step list' instead") .argument("", "Thread ULID") .action(() => { process.stderr.write(`Error: Command 'thread steps' has been removed. Use 'step list' instead. For more information, see: uwf help step list `); process.exit(1); }); thread .command("step-details") .description("[DEPRECATED] Use 'step show' instead") .argument("", "Step hash") .action(() => { process.stderr.write(`Error: Command 'thread step-details' has been removed. Use 'step show' instead. For more information, see: uwf help step show `); process.exit(1); }); thread .command("fork") .description("[DEPRECATED] Use 'step fork' instead") .argument("", "Step hash") .action(() => { process.stderr.write(`Error: Command 'thread fork' has been removed. Use 'step fork' instead. For more information, see: uwf help step fork `); process.exit(1); }); thread .command("kill") .description("[DEPRECATED] Use 'thread stop' or 'thread cancel' instead") .argument("", "Thread ULID") .action(() => { process.stderr.write(`Error: Command 'thread kill' has been removed. Use 'thread stop' to stop background execution (keep thread active), or 'thread cancel' to cancel and archive the thread. For more information, see: uwf help thread stop uwf help thread cancel `); process.exit(1); }); thread .command("running") .description("[DEPRECATED] Use 'thread list --status running' instead") .action(() => { process.stderr.write(`Error: Command 'thread running' has been removed. Use 'thread list --status running' instead. For more information, see: uwf help thread list `); process.exit(1); }); const prompt = program.command("prompt").description("Built-in prompt references for agents"); prompt.addHelpCommand(false); prompt .command("usage") .description("Print the complete skill content (all references combined)") .action(() => { console.log(cmdPromptUsage()); }); prompt .command("setup") .description("Print setup instructions for installing the uwf skill") .action(() => { console.log(cmdPromptSetup()); }); prompt .command("adapter") .description("Print the adapter reference (building agent adapters)") .action(() => { console.log(cmdPromptAdapter()); }); prompt .command("author") .description("Print the author reference (workflow YAML design guide)") .action(() => { console.log(cmdPromptAuthor()); }); prompt .command("developer") .description("Print the developer reference (coding conventions + architecture)") .action(() => { console.log(cmdPromptDeveloper()); }); prompt .command("user") .description("Print the user reference (CLI guide + typical workflows)") .action(() => { console.log(cmdPromptUser()); }); prompt .command("bootstrap") .description("Print the bootstrap skill YAML for Hermes agents") .action(() => { console.log(cmdPromptBootstrap()); }); prompt .command("list") .description("List all available prompt names") .action(() => { console.log(cmdPromptList().join("\n")); }); program .command("setup") .description("Configure provider, model, and agent") .option("--provider ", "Provider name") .option("--base-url ", "OpenAI-compatible API base URL") .option("--api-key ", "API key") .option("--model ", "Default model name") .option("--agent ", "Default agent adapter (e.g. hermes → uwf-hermes)") .action( (opts: { provider?: string; baseUrl?: string; apiKey?: string; model?: string; agent?: string; }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { if (opts.provider && opts.baseUrl && opts.apiKey && opts.model) { const result = await cmdSetup({ provider: opts.provider, baseUrl: opts.baseUrl, apiKey: opts.apiKey, model: opts.model, agent: opts.agent ?? undefined, storageRoot, }); writeOutput(result); } else if (!opts.provider && !opts.baseUrl && !opts.apiKey && !opts.model) { await cmdSetupInteractive(storageRoot); } else { throw new Error( "Non-interactive setup requires all of: --provider, --base-url, --api-key, --model", ); } }); }, ); const log = program.command("log").description("Process-level debug logs"); log .command("list") .description("List log files with sizes") .action(() => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdLogList(storageRoot); writeOutput(result); }); }); log .command("show") .description("Show and filter log entries") .option("--thread ", "Filter by thread ID") .option("--process ", "Filter by process ID") .option("--date ", "Filter by date (YYYY-MM-DD)") .action( (opts: { thread: string | undefined; process: string | undefined; date: string | undefined; }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdLogShow(storageRoot, { thread: opts.thread ?? null, process: opts.process ?? null, date: opts.date ?? null, }); writeOutput(result); }); }, ); log .command("clean") .description("Delete log files older than given date") .requiredOption("--before ", "Delete files before this date (YYYY-MM-DD)") .action((opts: { before: string }) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdLogClean(storageRoot, opts.before); writeOutput(result); }); }); const config = program.command("config").description("Configuration management"); config .command("list") .description("Display all configuration values (masks API keys)") .action(() => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdConfigList(storageRoot); writeOutput(result); }); }); config .command("get") .description("Get a specific configuration value") .argument( "", "Dot-notation path to config value (e.g., defaultAgent, providers.dashscope.baseUrl)", ) .action((key: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdConfigGet(storageRoot, key); writeOutput({ value: result }); }); }); config .command("set") .description("Set a specific configuration value") .argument("", "Dot-notation path to config value") .argument("", "New value (use JSON array for 'args' key, e.g., '[\"--flag\"]')") .action((key: string, value: string) => { const storageRoot = resolveStorageRoot(); runAction(async () => { const result = await cmdConfigSet(storageRoot, key, value); writeOutput(result); }); }); program.parseAsync(process.argv).catch((e: unknown) => { const message = e instanceof Error ? e.message : String(e); process.stderr.write(`${message}\n`); process.exit(1); });