Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5c09adae6 | |||
| f81e2a8aac | |||
| 2b38e583be | |||
| 4ff1394224 | |||
| 2bbe5a3d0e | |||
| a4237c0462 | |||
| 321e5b1379 | |||
| 7c3e14c473 | |||
| aecce595e8 | |||
| cf17dedac3 | |||
| 661fdbb263 | |||
| 201abf98ce | |||
| 665965fd01 | |||
| 6a99f84025 | |||
| f61474bec0 | |||
| 9bdb18afd0 | |||
| 2af299f3ce | |||
| d9f79c60a1 | |||
| a47ed06ea5 | |||
| 2ef004eecf | |||
| 2616259a0f | |||
| 23b2c3b47d |
@@ -97,6 +97,36 @@ type WorkflowEntry = {
|
|||||||
|
|
||||||
Workflow bundles (`.esm.js`) follow the same rule: export `const run` and `const descriptor`, not `export default`.
|
Workflow bundles (`.esm.js`) follow the same rule: export `const run` and `const descriptor`, not `export default`.
|
||||||
|
|
||||||
|
### Folder Module Discipline
|
||||||
|
|
||||||
|
Every folder under `src/` is a **module boundary**. Four rules:
|
||||||
|
|
||||||
|
| # | Rule | Rationale |
|
||||||
|
|---|------|-----------|
|
||||||
|
| 1 | **Every folder exports via `index.ts`** | Single entry point for the module |
|
||||||
|
| 2 | **Types live in `types.ts`** | Each folder's type definitions go in `<folder>/types.ts`, not scattered across files |
|
||||||
|
| 3 | **Single export source** | Only `index.ts` may re-export. No file may re-export from another module's internals. Cross-module imports must go through `index.ts` — never reach past it to import a specific file |
|
||||||
|
| 4 | **`index.ts` is pure re-exports** | No type definitions, no function implementations — only `export { ... } from` statements |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Good — import through module boundary
|
||||||
|
import { createCasStore } from "../cas/index.js";
|
||||||
|
import type { CasStore } from "../cas/index.js";
|
||||||
|
|
||||||
|
// ❌ Bad — reaching past index.ts
|
||||||
|
import { createCasStore } from "../cas/cas.js";
|
||||||
|
|
||||||
|
// ❌ Bad — re-exporting from non-index file
|
||||||
|
// in engine/engine.ts:
|
||||||
|
export { createCasStore } from "../cas/cas.js";
|
||||||
|
|
||||||
|
// ❌ Bad — types defined in index.ts
|
||||||
|
// in cas/index.ts:
|
||||||
|
export type CasStore = { ... }; // should be in cas/types.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exception**: The package-level `src/index.ts` is the public API surface and re-exports from folder `index.ts` files. Files that remain at `src/` root (e.g. `types.ts`, `workflow-as-agent.ts`) are not inside a folder module and follow normal rules.
|
||||||
|
|
||||||
## Naming
|
## Naming
|
||||||
|
|
||||||
| Type | Style | Example |
|
| Type | Style | Example |
|
||||||
@@ -197,9 +227,8 @@ Test files (`__tests__/**`) are exempt.
|
|||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run check # biome check (lint + format)
|
bun run check # tsc --build + biome check
|
||||||
bun run format # biome format --write
|
bun run format # biome format --write
|
||||||
bun run build # full build
|
|
||||||
bun test # run tests
|
bun test # run tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# @uncaged/cli-workflow
|
||||||
|
|
||||||
|
Command-line interface for the Uncaged workflow engine (`uncaged-workflow`).
|
||||||
|
|
||||||
|
The CLI reads and writes the workflow registry, starts and inspects threads, manages CAS blobs, and prints agent-oriented reference docs via `skill`. It uses the same storage layout as `@uncaged/workflow` (default `~/.uncaged/workflow`).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/cli-workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo: `"@uncaged/cli-workflow": "workspace:*"`. Depends on `"@uncaged/workflow": "workspace:*"`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uncaged-workflow workflow list
|
||||||
|
uncaged-workflow run <name> --prompt "Your task"
|
||||||
|
uncaged-workflow thread show <id>
|
||||||
|
uncaged-workflow skill
|
||||||
|
```
|
||||||
|
|
||||||
|
Invoking the CLI with no command (or from this repo: `bun packages/cli-workflow/src/cli.ts`) prints:
|
||||||
|
|
||||||
|
```
|
||||||
|
uncaged-workflow — workflow engine CLI
|
||||||
|
|
||||||
|
Workflow registry:
|
||||||
|
workflow add <name> <file.esm.js> [--types <path>] Register a workflow bundle in the registry
|
||||||
|
workflow list List all registered workflows
|
||||||
|
workflow show <name> Show details of a registered workflow
|
||||||
|
workflow rm <name> Remove a workflow from the registry
|
||||||
|
workflow history <name> Show version history of a workflow
|
||||||
|
workflow rollback <name> [hash] Rollback a workflow to a previous version
|
||||||
|
|
||||||
|
Thread execution:
|
||||||
|
thread run <name> [--prompt <text>] [--max-rounds N] Start a new thread executing a workflow
|
||||||
|
thread list [name] List threads, optionally filtered by workflow name
|
||||||
|
thread show <id> Show thread details and state
|
||||||
|
thread rm <id> Remove a thread
|
||||||
|
thread fork <thread-id> [--from-role <role>] Fork a thread, optionally from a specific role
|
||||||
|
thread ps List running threads
|
||||||
|
thread kill <thread-id> Kill a running thread
|
||||||
|
thread live <thread-id> | --latest [--debug] [--role <name>] Attach to a thread and stream output live
|
||||||
|
thread pause <thread-id> Pause a running thread
|
||||||
|
thread resume <thread-id> Resume a paused thread
|
||||||
|
|
||||||
|
Content-addressable storage:
|
||||||
|
cas get <hash> Retrieve content by hash from CAS
|
||||||
|
cas put <content> Store content in CAS, prints hash
|
||||||
|
cas list List all hashes in CAS
|
||||||
|
cas rm <hash> Remove a CAS entry by hash
|
||||||
|
cas gc Garbage-collect unreferenced CAS entries
|
||||||
|
|
||||||
|
Development:
|
||||||
|
init workspace <name> Initialize a new workflow workspace
|
||||||
|
init template <name> Initialize a new workflow template
|
||||||
|
|
||||||
|
Shortcuts:
|
||||||
|
run <name> [...] → thread run
|
||||||
|
live <id> [...] → thread live
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
skill [topic] Agent-consumable docs (cli, develop, author)
|
||||||
|
|
||||||
|
Use <command> --help for subcommand details.
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)
|
||||||
|
UNCAGED_WORKFLOW_STORAGE_ROOT Internal override (takes priority over WORKFLOW_STORAGE_ROOT)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
This package is bin-only; programmatic use is via `@uncaged/workflow`. Entry: `src/cli.ts` → `runCli` in `src/cli-dispatch.js`.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ParsedAddArgv } from "../src/cmd-add.js";
|
import type { ParsedAddArgv } from "../src/commands/workflow/index.js";
|
||||||
|
|
||||||
export function addCliArgs(name: string, filePath: string): ParsedAddArgv {
|
export function addCliArgs(name: string, filePath: string): ParsedAddArgv {
|
||||||
return { name, filePath, typesPath: null };
|
return { name, filePath, typesPath: null };
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { getGlobalCasDir, getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow";
|
import { getGlobalCasDir, getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow";
|
||||||
import { cmdAdd } from "../src/cmd-add.js";
|
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/commands/cas/index.js";
|
||||||
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/cmd-cas.js";
|
import {
|
||||||
import { cmdHistory } from "../src/cmd-history.js";
|
cmdAdd,
|
||||||
import { cmdList, formatListLines } from "../src/cmd-list.js";
|
cmdHistory,
|
||||||
import { cmdRemove } from "../src/cmd-remove.js";
|
cmdList,
|
||||||
import { cmdRollback } from "../src/cmd-rollback.js";
|
cmdRemove,
|
||||||
import { cmdShow } from "../src/cmd-show.js";
|
cmdRollback,
|
||||||
|
cmdShow,
|
||||||
|
formatListLines,
|
||||||
|
} from "../src/commands/workflow/index.js";
|
||||||
import { addCliArgs } from "./bundle-fixture.js";
|
import { addCliArgs } from "./bundle-fixture.js";
|
||||||
|
|
||||||
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {} };
|
const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {} };
|
||||||
@@ -399,7 +402,7 @@ export const run = async function* (input, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("cas put/get/list/rm use global cas dir (thread id not required for storage)", async () => {
|
test("cas put/get/list/rm use global cas dir (thread id not required for storage)", async () => {
|
||||||
const put = await cmdCasPut(storageRoot, "nonexistent-thread-id", "phase doc");
|
const put = await cmdCasPut(storageRoot, "phase doc");
|
||||||
expect(put.ok).toBe(true);
|
expect(put.ok).toBe(true);
|
||||||
if (!put.ok) {
|
if (!put.ok) {
|
||||||
return;
|
return;
|
||||||
@@ -408,24 +411,24 @@ export const run = async function* (input, options) {
|
|||||||
const blobPath = join(getGlobalCasDir(storageRoot), `${hash}.txt`);
|
const blobPath = join(getGlobalCasDir(storageRoot), `${hash}.txt`);
|
||||||
expect(await readFile(blobPath, "utf8")).toBe("phase doc");
|
expect(await readFile(blobPath, "utf8")).toBe("phase doc");
|
||||||
|
|
||||||
const got = await cmdCasGet(storageRoot, "other-thread", hash);
|
const got = await cmdCasGet(storageRoot, hash);
|
||||||
expect(got.ok).toBe(true);
|
expect(got.ok).toBe(true);
|
||||||
if (!got.ok) {
|
if (!got.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
expect(got.value).toBe("phase doc");
|
expect(got.value).toBe("phase doc");
|
||||||
|
|
||||||
const listed = await cmdCasList(storageRoot, "another-thread");
|
const listed = await cmdCasList(storageRoot);
|
||||||
expect(listed.ok).toBe(true);
|
expect(listed.ok).toBe(true);
|
||||||
if (!listed.ok) {
|
if (!listed.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
expect(listed.value).toContain(hash);
|
expect(listed.value).toContain(hash);
|
||||||
|
|
||||||
const removed = await cmdCasRm(storageRoot, "rm-thread", hash);
|
const removed = await cmdCasRm(storageRoot, hash);
|
||||||
expect(removed.ok).toBe(true);
|
expect(removed.ok).toBe(true);
|
||||||
|
|
||||||
const missing = await cmdCasGet(storageRoot, "after-rm", hash);
|
const missing = await cmdCasGet(storageRoot, hash);
|
||||||
expect(missing.ok).toBe(false);
|
expect(missing.ok).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { createCasStore, getContentMerklePayload, getGlobalCasDir } from "@uncaged/workflow";
|
import { createCasStore, getContentMerklePayload, getGlobalCasDir } from "@uncaged/workflow";
|
||||||
import { cmdAdd } from "../src/cmd-add.js";
|
import { cmdFork, cmdRun } from "../src/commands/thread/index.js";
|
||||||
import { cmdFork } from "../src/cmd-fork.js";
|
import { cmdAdd } from "../src/commands/workflow/index.js";
|
||||||
import { cmdRun } from "../src/cmd-run.js";
|
|
||||||
import { pathExists } from "../src/fs-utils.js";
|
import { pathExists } from "../src/fs-utils.js";
|
||||||
import { addCliArgs } from "./bundle-fixture.js";
|
import { addCliArgs } from "./bundle-fixture.js";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
getGlobalCasDir,
|
getGlobalCasDir,
|
||||||
putContentMerkleNode,
|
putContentMerkleNode,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
import { cmdThreadRemove } from "../src/cmd-thread.js";
|
import { cmdThreadRemove } from "../src/commands/thread/index.js";
|
||||||
import { pathExists } from "../src/fs-utils.js";
|
import { pathExists } from "../src/fs-utils.js";
|
||||||
|
|
||||||
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
formatSkillIndex,
|
formatSkillIndex,
|
||||||
formatSkillTopic,
|
formatSkillTopic,
|
||||||
getSkillTopics,
|
getSkillTopics,
|
||||||
} from "../src/cmd-help.js";
|
} from "../src/skill.js";
|
||||||
|
|
||||||
const STORAGE_ROOT = "/tmp/help-test-storage";
|
const STORAGE_ROOT = "/tmp/help-test-storage";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { runCli } from "../src/cli-dispatch.js";
|
import { runCli } from "../src/cli-dispatch.js";
|
||||||
import { cmdInitTemplate, cmdInitWorkspace } from "../src/cmd-init.js";
|
import { cmdInitTemplate, cmdInitWorkspace } from "../src/commands/init/index.js";
|
||||||
import { pathExists } from "../src/fs-utils.js";
|
import { pathExists } from "../src/fs-utils.js";
|
||||||
|
|
||||||
describe("init template", () => {
|
describe("init template", () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
import { formatCliUsage, runCli } from "../src/cli-dispatch.js";
|
||||||
import { cmdInitWorkspace } from "../src/cmd-init.js";
|
import { cmdInitWorkspace } from "../src/commands/init/index.js";
|
||||||
import { pathExists } from "../src/fs-utils.js";
|
import { pathExists } from "../src/fs-utils.js";
|
||||||
|
|
||||||
describe("init workspace", () => {
|
describe("init workspace", () => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
LIVE_CONTENT_MAX_LINES,
|
LIVE_CONTENT_MAX_LINES,
|
||||||
type LiveRoleRow,
|
type LiveRoleRow,
|
||||||
renderLiveRoleStepLines,
|
renderLiveRoleStepLines,
|
||||||
} from "../src/cmd-live.js";
|
} from "../src/commands/thread/index.js";
|
||||||
import { parseLiveArgv } from "../src/live-argv.js";
|
import { parseLiveArgv } from "../src/live-argv.js";
|
||||||
|
|
||||||
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
|
||||||
|
|||||||
@@ -5,15 +5,18 @@ import { tmpdir } from "node:os";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { getGlobalCasDir } from "@uncaged/workflow";
|
import { getGlobalCasDir } from "@uncaged/workflow";
|
||||||
import { cmdAdd } from "../src/cmd-add.js";
|
import { cmdCasPut } from "../src/commands/cas/index.js";
|
||||||
import { cmdCasPut } from "../src/cmd-cas.js";
|
import {
|
||||||
import { cmdKill } from "../src/cmd-kill.js";
|
cmdKill,
|
||||||
import { cmdPause } from "../src/cmd-pause.js";
|
cmdPause,
|
||||||
import { cmdPs } from "../src/cmd-ps.js";
|
cmdPs,
|
||||||
import { cmdResume } from "../src/cmd-resume.js";
|
cmdResume,
|
||||||
import { cmdRun } from "../src/cmd-run.js";
|
cmdRun,
|
||||||
import { cmdThreadRemove, cmdThreadShow } from "../src/cmd-thread.js";
|
cmdThreadRemove,
|
||||||
import { cmdThreads } from "../src/cmd-threads.js";
|
cmdThreadShow,
|
||||||
|
cmdThreads,
|
||||||
|
} from "../src/commands/thread/index.js";
|
||||||
|
import { cmdAdd } from "../src/commands/workflow/index.js";
|
||||||
import { pathExists, readTextFileIfExists } from "../src/fs-utils.js";
|
import { pathExists, readTextFileIfExists } from "../src/fs-utils.js";
|
||||||
import { addCliArgs } from "./bundle-fixture.js";
|
import { addCliArgs } from "./bundle-fixture.js";
|
||||||
|
|
||||||
@@ -233,7 +236,7 @@ describe("cli thread commands", () => {
|
|||||||
const runningPath = join(dirname(dataPath), `${threadId}.running`);
|
const runningPath = join(dirname(dataPath), `${threadId}.running`);
|
||||||
await waitUntilRunningFileAbsent(runningPath, 120);
|
await waitUntilRunningFileAbsent(runningPath, 120);
|
||||||
|
|
||||||
const put = await cmdCasPut(storageRoot, threadId, "keep-after-thread-rm");
|
const put = await cmdCasPut(storageRoot, "keep-after-thread-rm");
|
||||||
expect(put.ok).toBe(true);
|
expect(put.ok).toBe(true);
|
||||||
if (!put.ok) {
|
if (!put.ok) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { err, ok, type Result } from "@uncaged/workflow";
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
async function pathExists(path: string): Promise<boolean> {
|
import { pathExists } from "./fs-utils.js";
|
||||||
try {
|
|
||||||
await stat(path);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BundleFileSource = { kind: "text"; text: string } | { kind: "path"; path: string };
|
export type BundleFileSource = { kind: "text"; text: string } | { kind: "path"; path: string };
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export function shouldUseColor(): boolean {
|
||||||
|
return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightLiveRole(name: string): string {
|
||||||
|
if (!shouldUseColor()) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return `\x1b[1m\x1b[36m${name}\x1b[0m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dimGreyLine(line: string): string {
|
||||||
|
if (!shouldUseColor()) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return `\x1b[2m\x1b[90m${line}\x1b[0m`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export type DispatchFn = (storageRoot: string, argv: string[]) => Promise<number>;
|
||||||
|
|
||||||
|
export type CommandEntry = {
|
||||||
|
handler: DispatchFn;
|
||||||
|
args: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommandGroup = {
|
||||||
|
name: string;
|
||||||
|
commands: ReadonlyArray<{ name: string; args: string; description: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DispatchGroupFn = (
|
||||||
|
tableName: string,
|
||||||
|
table: Record<string, CommandEntry>,
|
||||||
|
storageRoot: string,
|
||||||
|
argv: string[],
|
||||||
|
) => Promise<number> | null;
|
||||||
@@ -1,613 +1,33 @@
|
|||||||
|
import type { CommandEntry, DispatchFn } from "./cli-command-types.js";
|
||||||
import { printCliError, printCliLine, printCliWarn } from "./cli-output.js";
|
import { printCliError, printCliLine, printCliWarn } from "./cli-output.js";
|
||||||
import { cmdAdd, formatAddSuccess, parseAddArgv } from "./cmd-add.js";
|
import { getCommandRegistry } from "./cli-registry.js";
|
||||||
import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "./cmd-cas.js";
|
import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js";
|
||||||
import { cmdFork, parseForkArgv } from "./cmd-fork.js";
|
import { createCasDispatcher, dispatchGc } from "./commands/cas/index.js";
|
||||||
import { cmdGc } from "./cmd-gc.js";
|
import { createInitDispatcher } from "./commands/init/index.js";
|
||||||
import {
|
import {
|
||||||
formatSkillDoc,
|
createThreadDispatcher,
|
||||||
formatSkillIndex,
|
dispatchFork,
|
||||||
formatSkillTopic,
|
dispatchKill,
|
||||||
getSkillTopics,
|
dispatchLive,
|
||||||
} from "./cmd-help.js";
|
dispatchPause,
|
||||||
import { cmdHistory } from "./cmd-history.js";
|
dispatchPs,
|
||||||
import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js";
|
dispatchResume,
|
||||||
import { cmdKill } from "./cmd-kill.js";
|
dispatchRun,
|
||||||
import { cmdList, formatListLines } from "./cmd-list.js";
|
dispatchThreadList,
|
||||||
import { cmdLive } from "./cmd-live.js";
|
} from "./commands/thread/index.js";
|
||||||
import { cmdPause } from "./cmd-pause.js";
|
import {
|
||||||
import { cmdPs } from "./cmd-ps.js";
|
createWorkflowDispatcher,
|
||||||
import { cmdRemove } from "./cmd-remove.js";
|
dispatchAdd,
|
||||||
import { cmdResume } from "./cmd-resume.js";
|
dispatchHistory,
|
||||||
import { cmdRollback } from "./cmd-rollback.js";
|
dispatchList,
|
||||||
import { cmdRun } from "./cmd-run.js";
|
dispatchRemove,
|
||||||
import { cmdShow, formatShowYaml } from "./cmd-show.js";
|
dispatchRollback,
|
||||||
import { cmdThreadRemove, cmdThreadShow } from "./cmd-thread.js";
|
dispatchShow,
|
||||||
import { cmdThreads } from "./cmd-threads.js";
|
} from "./commands/workflow/index.js";
|
||||||
import { parseLiveArgv } from "./live-argv.js";
|
import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "./skill.js";
|
||||||
import { parseRunArgv } from "./run-argv.js";
|
|
||||||
|
|
||||||
type DispatchFn = (storageRoot: string, argv: string[]) => Promise<number>;
|
export type { CommandEntry, CommandGroup, DispatchFn } from "./cli-command-types.js";
|
||||||
|
export { getCommandRegistry } from "./cli-registry.js";
|
||||||
type CommandEntry = {
|
|
||||||
handler: DispatchFn;
|
|
||||||
args: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CommandGroup = {
|
|
||||||
name: string;
|
|
||||||
commands: ReadonlyArray<{ name: string; args: string; description: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Individual dispatch functions ──────────────────────────────────────
|
|
||||||
|
|
||||||
async function dispatchInitWorkspace(_storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const name = argv[0];
|
|
||||||
if (name === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: init workspace requires <name>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdInitWorkspace(process.cwd(), name);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`initialized workflow workspace at ${result.value.rootPath}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchInitTemplate(_storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const name = argv[0];
|
|
||||||
if (name === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: init template requires <name>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdInitTemplate(process.cwd(), name);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`initialized template at ${result.value.templatePath}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const parsed = parseAddArgv(argv);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdAdd(storageRoot, parsed.value);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const w of result.value.warnings) {
|
|
||||||
printCliWarn(w);
|
|
||||||
}
|
|
||||||
printCliLine(formatAddSuccess(parsed.value.name, parsed.value.filePath, result.value.hash));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchList(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
if (argv.length > 0) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: list takes no arguments`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdList(storageRoot);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const line of formatListLines(result.value)) {
|
|
||||||
printCliLine(line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchShow(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const name = argv[0];
|
|
||||||
if (name === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: show requires <name>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdShow(storageRoot, name);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(formatShowYaml(name, result.value));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchRemove(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const name = argv[0];
|
|
||||||
if (name === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: remove requires <name>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdRemove(storageRoot, name);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`removed workflow "${name}" from registry`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchRun(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const parsed = parseRunArgv(argv);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await cmdRun(
|
|
||||||
storageRoot,
|
|
||||||
parsed.value.name,
|
|
||||||
parsed.value.prompt,
|
|
||||||
parsed.value.maxRounds,
|
|
||||||
);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printCliLine(result.value.threadId);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchPs(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
if (argv.length > 0) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: ps takes no arguments`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const line of await cmdPs(storageRoot)) {
|
|
||||||
printCliLine(line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchKill(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const threadId = argv[0];
|
|
||||||
if (threadId === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: kill requires <thread-id>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdKill(storageRoot, threadId);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`kill sent for thread ${threadId}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchLive(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const parsed = parseLiveArgv(argv);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return cmdLive(storageRoot, parsed.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchHistory(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const name = argv[0];
|
|
||||||
if (name === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: history requires <name>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdHistory(storageRoot, name);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const line of result.value) {
|
|
||||||
printCliLine(line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchRollback(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const name = argv[0];
|
|
||||||
if (name === undefined || argv.length > 2) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: rollback requires <name> [hash]`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const hashArg = argv[1];
|
|
||||||
const result = await cmdRollback(storageRoot, name, hashArg === undefined ? null : hashArg);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`rolled back workflow "${name}"`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchPause(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const threadId = argv[0];
|
|
||||||
if (threadId === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: pause requires <thread-id>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdPause(storageRoot, threadId);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`pause sent for thread ${threadId}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchResume(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const threadId = argv[0];
|
|
||||||
if (threadId === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: resume requires <thread-id>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdResume(storageRoot, threadId);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`resume sent for thread ${threadId}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchThreadList(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const result = await cmdThreads(storageRoot, argv);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const line of result.value) {
|
|
||||||
printCliLine(line);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchThreadShow(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const id = argv[0];
|
|
||||||
if (id === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: thread show requires <id>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdThreadShow(storageRoot, id);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(result.value);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchThreadRm(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const id = argv[0];
|
|
||||||
if (id === undefined || argv.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: thread rm requires <id>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdThreadRemove(storageRoot, id);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`removed thread ${id}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchGc(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
if (argv.length > 0) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: gc takes no arguments`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdGc(storageRoot);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const stats = result.value;
|
|
||||||
printCliLine(
|
|
||||||
`scanned ${stats.scannedThreads} threads, ${stats.activeRefs} active refs, deleted ${stats.deletedEntries} entries`,
|
|
||||||
);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchFork(storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const parsed = parseForkArgv(argv);
|
|
||||||
if (!parsed.ok) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdFork(storageRoot, parsed.value.threadId, parsed.value.fromRole);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(result.value.threadId);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── CAS subcommand table ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function dispatchCasGet(storageRoot: string, rest: string[]): Promise<number> {
|
|
||||||
const threadId = rest[0];
|
|
||||||
const hash = rest[1];
|
|
||||||
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: cas get requires <thread-id> <hash>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdCasGet(storageRoot, threadId, hash);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(result.value);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchCasPut(storageRoot: string, rest: string[]): Promise<number> {
|
|
||||||
const threadId = rest[0];
|
|
||||||
const content = rest[1];
|
|
||||||
if (threadId === undefined || content === undefined || rest.length > 2) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: cas put requires <thread-id> <content>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdCasPut(storageRoot, threadId, content);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(result.value);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchCasList(storageRoot: string, rest: string[]): Promise<number> {
|
|
||||||
const threadId = rest[0];
|
|
||||||
if (threadId === undefined || rest.length > 1) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: cas list requires <thread-id>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdCasList(storageRoot, threadId);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
for (const hash of result.value) {
|
|
||||||
printCliLine(hash);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchCasRm(storageRoot: string, rest: string[]): Promise<number> {
|
|
||||||
const threadId = rest[0];
|
|
||||||
const hash = rest[1];
|
|
||||||
if (threadId === undefined || hash === undefined || rest.length > 2) {
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: cas rm requires <thread-id> <hash>`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const result = await cmdCasRm(storageRoot, threadId, hash);
|
|
||||||
if (!result.ok) {
|
|
||||||
printCliError(result.error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(`removed cas entry ${hash}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Subcommand tables with metadata ────────────────────────────────────
|
|
||||||
|
|
||||||
const WORKFLOW_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
|
||||||
add: {
|
|
||||||
handler: dispatchAdd,
|
|
||||||
args: "<name> <file.esm.js> [--types <path>]",
|
|
||||||
description: "Register a workflow bundle in the registry",
|
|
||||||
},
|
|
||||||
list: { handler: dispatchList, args: "", description: "List all registered workflows" },
|
|
||||||
show: {
|
|
||||||
handler: dispatchShow,
|
|
||||||
args: "<name>",
|
|
||||||
description: "Show details of a registered workflow",
|
|
||||||
},
|
|
||||||
rm: {
|
|
||||||
handler: dispatchRemove,
|
|
||||||
args: "<name>",
|
|
||||||
description: "Remove a workflow from the registry",
|
|
||||||
},
|
|
||||||
history: {
|
|
||||||
handler: dispatchHistory,
|
|
||||||
args: "<name>",
|
|
||||||
description: "Show version history of a workflow",
|
|
||||||
},
|
|
||||||
rollback: {
|
|
||||||
handler: dispatchRollback,
|
|
||||||
args: "<name> [hash]",
|
|
||||||
description: "Rollback a workflow to a previous version",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const THREAD_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
|
||||||
run: {
|
|
||||||
handler: dispatchRun,
|
|
||||||
args: "<name> [--prompt <text>] [--max-rounds N]",
|
|
||||||
description: "Start a new thread executing a workflow",
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
handler: dispatchThreadList,
|
|
||||||
args: "[name]",
|
|
||||||
description: "List threads, optionally filtered by workflow name",
|
|
||||||
},
|
|
||||||
show: { handler: dispatchThreadShow, args: "<id>", description: "Show thread details and state" },
|
|
||||||
rm: { handler: dispatchThreadRm, args: "<id>", description: "Remove a thread" },
|
|
||||||
fork: {
|
|
||||||
handler: dispatchFork,
|
|
||||||
args: "<thread-id> [--from-role <role>]",
|
|
||||||
description: "Fork a thread, optionally from a specific role",
|
|
||||||
},
|
|
||||||
ps: { handler: dispatchPs, args: "", description: "List running threads" },
|
|
||||||
kill: { handler: dispatchKill, args: "<thread-id>", description: "Kill a running thread" },
|
|
||||||
live: {
|
|
||||||
handler: dispatchLive,
|
|
||||||
args: "<thread-id> | --latest [--debug] [--role <name>]",
|
|
||||||
description: "Attach to a thread and stream output live",
|
|
||||||
},
|
|
||||||
pause: { handler: dispatchPause, args: "<thread-id>", description: "Pause a running thread" },
|
|
||||||
resume: { handler: dispatchResume, args: "<thread-id>", description: "Resume a paused thread" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const CAS_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
|
||||||
get: {
|
|
||||||
handler: dispatchCasGet,
|
|
||||||
args: "<thread-id> <hash>",
|
|
||||||
description: "Retrieve content by hash from a thread's CAS",
|
|
||||||
},
|
|
||||||
put: {
|
|
||||||
handler: dispatchCasPut,
|
|
||||||
args: "<thread-id> <content>",
|
|
||||||
description: "Store content in a thread's CAS, returns hash",
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
handler: dispatchCasList,
|
|
||||||
args: "<thread-id>",
|
|
||||||
description: "List all CAS entries for a thread",
|
|
||||||
},
|
|
||||||
rm: { handler: dispatchCasRm, args: "<thread-id> <hash>", description: "Remove a CAS entry" },
|
|
||||||
gc: { handler: dispatchGc, args: "", description: "Garbage-collect unreferenced CAS entries" },
|
|
||||||
};
|
|
||||||
|
|
||||||
const INIT_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
|
||||||
workspace: {
|
|
||||||
handler: dispatchInitWorkspace,
|
|
||||||
args: "<name>",
|
|
||||||
description: "Initialize a new workflow workspace",
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
handler: dispatchInitTemplate,
|
|
||||||
args: "<name>",
|
|
||||||
description: "Initialize a new workflow template",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Command registry ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "workflow",
|
|
||||||
commands: Object.entries(WORKFLOW_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
|
||||||
name,
|
|
||||||
args: e.args,
|
|
||||||
description: e.description,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "thread",
|
|
||||||
commands: Object.entries(THREAD_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
|
||||||
name,
|
|
||||||
args: e.args,
|
|
||||||
description: e.description,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cas",
|
|
||||||
commands: Object.entries(CAS_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
|
||||||
name,
|
|
||||||
args: e.args,
|
|
||||||
description: e.description,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "init",
|
|
||||||
commands: Object.entries(INIT_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
|
||||||
name,
|
|
||||||
args: e.args,
|
|
||||||
description: e.description,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Auto-generated CLI usage ───────────────────────────────────────────
|
|
||||||
|
|
||||||
const USAGE_SECTION_BY_GROUP: Record<string, string> = {
|
|
||||||
workflow: "Workflow registry:",
|
|
||||||
thread: "Thread execution:",
|
|
||||||
cas: "Content-addressable storage:",
|
|
||||||
init: "Development:",
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatUsageCommandLines(
|
|
||||||
rows: ReadonlyArray<{ prefix: string; description: string }>,
|
|
||||||
): string[] {
|
|
||||||
const maxPrefix = rows.reduce((max, row) => Math.max(max, row.prefix.length), 0);
|
|
||||||
const gap = 2;
|
|
||||||
return rows.map((row) => {
|
|
||||||
const pad = " ".repeat(maxPrefix - row.prefix.length + gap);
|
|
||||||
return ` ${row.prefix}${pad}${row.description}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatCliUsage(): string {
|
|
||||||
const groups = getCommandRegistry();
|
|
||||||
const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""];
|
|
||||||
|
|
||||||
for (const group of groups) {
|
|
||||||
const sectionTitle = USAGE_SECTION_BY_GROUP[group.name];
|
|
||||||
if (sectionTitle === undefined) {
|
|
||||||
throw new Error(`BUG: missing usage section title for group "${group.name}"`);
|
|
||||||
}
|
|
||||||
lines.push(sectionTitle);
|
|
||||||
const rows = group.commands.map((cmd) => {
|
|
||||||
const args = cmd.args ? ` ${cmd.args}` : "";
|
|
||||||
return {
|
|
||||||
prefix: `${group.name} ${cmd.name}${args}`,
|
|
||||||
description: cmd.description,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
lines.push(...formatUsageCommandLines(rows));
|
|
||||||
lines.push("");
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push("Shortcuts:");
|
|
||||||
lines.push(
|
|
||||||
...formatUsageCommandLines([
|
|
||||||
{ prefix: "run <name> [...]", description: "→ thread run" },
|
|
||||||
{ prefix: "live <id> [...]", description: "→ thread live" },
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
lines.push("");
|
|
||||||
|
|
||||||
lines.push("Reference:");
|
|
||||||
const skillTopicNames = getSkillTopics()
|
|
||||||
.map((t) => t.name)
|
|
||||||
.join(", ");
|
|
||||||
lines.push(
|
|
||||||
...formatUsageCommandLines([
|
|
||||||
{
|
|
||||||
prefix: "skill [topic]",
|
|
||||||
description: `Agent-consumable docs (${skillTopicNames})`,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
lines.push("");
|
|
||||||
lines.push("Use <command> --help for subcommand details.");
|
|
||||||
lines.push("");
|
|
||||||
lines.push("Environment variables:");
|
|
||||||
lines.push(
|
|
||||||
" WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)",
|
|
||||||
);
|
|
||||||
lines.push(
|
|
||||||
" UNCAGED_WORKFLOW_STORAGE_ROOT Internal override (takes priority over WORKFLOW_STORAGE_ROOT)",
|
|
||||||
);
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function printDeprecation(oldCmd: string, newCmd: string): void {
|
|
||||||
printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Group dispatchers ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function dispatchGroup(
|
function dispatchGroup(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
@@ -634,54 +54,20 @@ function dispatchGroup(
|
|||||||
return entry.handler(storageRoot, argv.slice(1));
|
return entry.handler(storageRoot, argv.slice(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchInit(storageRoot: string, argv: string[]): Promise<number> {
|
function printDeprecation(oldCmd: string, newCmd: string): void {
|
||||||
const result = dispatchGroup("init", INIT_SUBCOMMAND_TABLE, storageRoot, argv);
|
printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`);
|
||||||
if (result !== null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const sub = argv[0];
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: unknown init subcommand: ${sub}`);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise<number> {
|
export function formatCliUsage(): string {
|
||||||
const result = dispatchGroup("workflow", WORKFLOW_SUBCOMMAND_TABLE, storageRoot, argv);
|
return formatCliUsageWithGroups(getCommandRegistry(), getSkillTopics());
|
||||||
if (result !== null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const sub = argv[0];
|
|
||||||
if (sub === "remove") {
|
|
||||||
printDeprecation("workflow remove", "workflow rm");
|
|
||||||
return dispatchRemove(storageRoot, argv.slice(1));
|
|
||||||
}
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: unknown workflow subcommand: ${sub}`);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchThread(storageRoot: string, argv: string[]): Promise<number> {
|
const dispatchWorkflow = createWorkflowDispatcher({ dispatchGroup, printDeprecation });
|
||||||
const result = dispatchGroup("thread", THREAD_SUBCOMMAND_TABLE, storageRoot, argv);
|
const dispatchThread = createThreadDispatcher({ dispatchGroup });
|
||||||
if (result !== null) {
|
const dispatchCas = createCasDispatcher({ dispatchGroup });
|
||||||
return result;
|
const dispatchInit = createInitDispatcher({ dispatchGroup });
|
||||||
}
|
|
||||||
const sub = argv[0];
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: ${sub}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dispatchCas(storageRoot: string, argv: string[]): Promise<number> {
|
async function showSkillDocOrIndex(topic: string | undefined): Promise<number> {
|
||||||
const result = dispatchGroup("cas", CAS_SUBCOMMAND_TABLE, storageRoot, argv);
|
|
||||||
if (result !== null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const sub = argv[0];
|
|
||||||
printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: ${sub}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Help ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<number> {
|
|
||||||
const topic = argv[0];
|
|
||||||
if (topic === undefined) {
|
if (topic === undefined) {
|
||||||
printCliLine(formatSkillIndex());
|
printCliLine(formatSkillIndex());
|
||||||
return 0;
|
return 0;
|
||||||
@@ -695,44 +81,31 @@ async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<numb
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
return showSkillDocOrIndex(argv[0]);
|
||||||
|
}
|
||||||
|
|
||||||
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
||||||
// Legacy compat: help --skill [topic] → skill [topic]
|
printCliWarn('⚠ "help" is deprecated, use "skill" instead');
|
||||||
const skillIdx = argv.indexOf("--skill");
|
const skillIdx = argv.indexOf("--skill");
|
||||||
if (skillIdx !== -1) {
|
if (skillIdx !== -1) {
|
||||||
const topic = argv[skillIdx + 1];
|
return showSkillDocOrIndex(argv[skillIdx + 1]);
|
||||||
if (topic === undefined) {
|
|
||||||
printCliLine(formatSkillIndex());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const doc = formatSkillTopic(topic);
|
|
||||||
if (doc === null) {
|
|
||||||
printCliError(`unknown skill topic: ${topic}\n\n${formatSkillIndex()}`);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
printCliLine(doc);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
printCliLine(formatCliUsage());
|
printCliLine(formatCliUsage());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Top-level command table (Phase 3) ──────────────────────────────────
|
|
||||||
|
|
||||||
const COMMAND_TABLE: Record<string, DispatchFn> = {
|
const COMMAND_TABLE: Record<string, DispatchFn> = {
|
||||||
// Grouped commands (primary)
|
|
||||||
workflow: dispatchWorkflow,
|
workflow: dispatchWorkflow,
|
||||||
thread: dispatchThread,
|
thread: dispatchThread,
|
||||||
cas: dispatchCas,
|
cas: dispatchCas,
|
||||||
init: dispatchInit,
|
init: dispatchInit,
|
||||||
help: dispatchHelp,
|
help: dispatchHelp,
|
||||||
skill: dispatchSkill,
|
skill: dispatchSkill,
|
||||||
|
|
||||||
// Top-level shortcuts (no deprecation)
|
|
||||||
run: dispatchRun,
|
run: dispatchRun,
|
||||||
live: dispatchLive,
|
live: dispatchLive,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Deprecated flat commands that delegate to grouped commands
|
|
||||||
const DEPRECATED_ALIASES: Record<string, { newCmd: string; handler: DispatchFn }> = {
|
const DEPRECATED_ALIASES: Record<string, { newCmd: string; handler: DispatchFn }> = {
|
||||||
add: { newCmd: "workflow add", handler: dispatchAdd },
|
add: { newCmd: "workflow add", handler: dispatchAdd },
|
||||||
list: { newCmd: "workflow list", handler: dispatchList },
|
list: { newCmd: "workflow list", handler: dispatchList },
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { CommandGroup } from "./cli-command-types.js";
|
||||||
|
import { setCommandGroupsForUsage } from "./cli-usage-context.js";
|
||||||
|
import { CAS_SUBCOMMAND_TABLE } from "./commands/cas/index.js";
|
||||||
|
import { INIT_SUBCOMMAND_TABLE } from "./commands/init/index.js";
|
||||||
|
import { THREAD_SUBCOMMAND_TABLE } from "./commands/thread/index.js";
|
||||||
|
import { WORKFLOW_SUBCOMMAND_TABLE } from "./commands/workflow/index.js";
|
||||||
|
|
||||||
|
export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "workflow",
|
||||||
|
commands: Object.entries(WORKFLOW_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||||
|
name,
|
||||||
|
args: e.args,
|
||||||
|
description: e.description,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "thread",
|
||||||
|
commands: Object.entries(THREAD_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||||
|
name,
|
||||||
|
args: e.args,
|
||||||
|
description: e.description,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cas",
|
||||||
|
commands: Object.entries(CAS_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||||
|
name,
|
||||||
|
args: e.args,
|
||||||
|
description: e.description,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "init",
|
||||||
|
commands: Object.entries(INIT_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||||
|
name,
|
||||||
|
args: e.args,
|
||||||
|
description: e.description,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
setCommandGroupsForUsage(getCommandRegistry());
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { CommandGroup } from "./cli-command-types.js";
|
||||||
|
|
||||||
|
let commandGroupsForUsage: ReadonlyArray<CommandGroup> | null = null;
|
||||||
|
|
||||||
|
export function setCommandGroupsForUsage(groups: ReadonlyArray<CommandGroup>): void {
|
||||||
|
commandGroupsForUsage = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommandGroupsForUsage(): ReadonlyArray<CommandGroup> {
|
||||||
|
if (commandGroupsForUsage === null) {
|
||||||
|
throw new Error("BUG: command groups for usage not initialized");
|
||||||
|
}
|
||||||
|
return commandGroupsForUsage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import type { CommandGroup } from "./cli-command-types.js";
|
||||||
|
|
||||||
|
/** Keep aligned with `getSkillTopics()` in skill.ts (names only, for error usage lines). */
|
||||||
|
export const USAGE_SKILL_TOPIC_ROWS: ReadonlyArray<{ name: string }> = [
|
||||||
|
{ name: "cli" },
|
||||||
|
{ name: "develop" },
|
||||||
|
{ name: "author" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const USAGE_SECTION_BY_GROUP: Record<string, string> = {
|
||||||
|
workflow: "Workflow registry:",
|
||||||
|
thread: "Thread execution:",
|
||||||
|
cas: "Content-addressable storage:",
|
||||||
|
init: "Development:",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatUsageCommandLines(
|
||||||
|
rows: ReadonlyArray<{ prefix: string; description: string }>,
|
||||||
|
): string[] {
|
||||||
|
const maxPrefix = rows.reduce((max, row) => Math.max(max, row.prefix.length), 0);
|
||||||
|
const gap = 2;
|
||||||
|
return rows.map((row) => {
|
||||||
|
const pad = " ".repeat(maxPrefix - row.prefix.length + gap);
|
||||||
|
return ` ${row.prefix}${pad}${row.description}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCliUsage(
|
||||||
|
groups: ReadonlyArray<CommandGroup>,
|
||||||
|
skillTopics: ReadonlyArray<{ name: string }>,
|
||||||
|
): string {
|
||||||
|
const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""];
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
const sectionTitle = USAGE_SECTION_BY_GROUP[group.name];
|
||||||
|
if (sectionTitle === undefined) {
|
||||||
|
throw new Error(`BUG: missing usage section title for group "${group.name}"`);
|
||||||
|
}
|
||||||
|
lines.push(sectionTitle);
|
||||||
|
const rows = group.commands.map((cmd) => {
|
||||||
|
const args = cmd.args ? ` ${cmd.args}` : "";
|
||||||
|
return {
|
||||||
|
prefix: `${group.name} ${cmd.name}${args}`,
|
||||||
|
description: cmd.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
lines.push(...formatUsageCommandLines(rows));
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("Shortcuts:");
|
||||||
|
lines.push(
|
||||||
|
...formatUsageCommandLines([
|
||||||
|
{ prefix: "run <name> [...]", description: "→ thread run" },
|
||||||
|
{ prefix: "live <id> [...]", description: "→ thread live" },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
lines.push("Reference:");
|
||||||
|
const skillTopicNames = skillTopics.map((t) => t.name).join(", ");
|
||||||
|
lines.push(
|
||||||
|
...formatUsageCommandLines([
|
||||||
|
{
|
||||||
|
prefix: "skill [topic]",
|
||||||
|
description: `Agent-consumable docs (${skillTopicNames})`,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Use <command> --help for subcommand details.");
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Environment variables:");
|
||||||
|
lines.push(
|
||||||
|
" WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)",
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
" UNCAGED_WORKFLOW_STORAGE_ROOT Internal override (takes priority over WORKFLOW_STORAGE_ROOT)",
|
||||||
|
);
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { createCasStore, err, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
|
||||||
|
|
||||||
export async function cmdCasGet(
|
|
||||||
storageRoot: string,
|
|
||||||
_threadId: string,
|
|
||||||
hash: string,
|
|
||||||
): Promise<Result<string, string>> {
|
|
||||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
|
||||||
const content = await cas.get(hash);
|
|
||||||
if (content === null) {
|
|
||||||
return err(`cas entry not found: ${hash}`);
|
|
||||||
}
|
|
||||||
return ok(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cmdCasPut(
|
|
||||||
storageRoot: string,
|
|
||||||
_threadId: string,
|
|
||||||
content: string,
|
|
||||||
): Promise<Result<string, string>> {
|
|
||||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
|
||||||
const hash = await cas.put(content);
|
|
||||||
return ok(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cmdCasList(
|
|
||||||
storageRoot: string,
|
|
||||||
_threadId: string,
|
|
||||||
): Promise<Result<string[], string>> {
|
|
||||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
|
||||||
const hashes = await cas.list();
|
|
||||||
return ok(hashes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cmdCasRm(
|
|
||||||
storageRoot: string,
|
|
||||||
_threadId: string,
|
|
||||||
hash: string,
|
|
||||||
): Promise<Result<void, string>> {
|
|
||||||
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
|
||||||
await cas.delete(hash);
|
|
||||||
return ok(undefined);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
import { err, type Result } from "@uncaged/workflow";
|
|
||||||
|
|
||||||
import { readTextFileIfExists } from "./fs-utils.js";
|
|
||||||
import {
|
|
||||||
resolveRunningHashForThread,
|
|
||||||
sendWorkerTcpCommand,
|
|
||||||
type WorkerCtl,
|
|
||||||
} from "./worker-spawn.js";
|
|
||||||
|
|
||||||
export async function cmdKill(
|
|
||||||
storageRoot: string,
|
|
||||||
threadId: string,
|
|
||||||
): Promise<Result<void, string>> {
|
|
||||||
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
|
||||||
if (!hashResult.ok) {
|
|
||||||
return hashResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctlPath = join(storageRoot, "workers", `${hashResult.value}.json`);
|
|
||||||
const ctlText = await readTextFileIfExists(ctlPath);
|
|
||||||
if (ctlText === null) {
|
|
||||||
return err(`worker control file missing for bundle hash ${hashResult.value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctl: WorkerCtl;
|
|
||||||
try {
|
|
||||||
ctl = JSON.parse(ctlText) as WorkerCtl;
|
|
||||||
} catch {
|
|
||||||
return err(`corrupt worker control file: ${ctlPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
|
||||||
return err(`invalid worker control file: ${ctlPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await sendWorkerTcpCommand(
|
|
||||||
ctl.port,
|
|
||||||
{ type: "kill", threadId },
|
|
||||||
{ awaitResponseLine: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
import { err, type Result } from "@uncaged/workflow";
|
|
||||||
|
|
||||||
import { readTextFileIfExists } from "./fs-utils.js";
|
|
||||||
import {
|
|
||||||
resolveRunningHashForThread,
|
|
||||||
sendWorkerTcpCommand,
|
|
||||||
type WorkerCtl,
|
|
||||||
} from "./worker-spawn.js";
|
|
||||||
|
|
||||||
export async function cmdPause(
|
|
||||||
storageRoot: string,
|
|
||||||
threadId: string,
|
|
||||||
): Promise<Result<void, string>> {
|
|
||||||
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
|
||||||
if (!hashResult.ok) {
|
|
||||||
return hashResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctlPath = join(storageRoot, "workers", `${hashResult.value}.json`);
|
|
||||||
const ctlText = await readTextFileIfExists(ctlPath);
|
|
||||||
if (ctlText === null) {
|
|
||||||
return err(`worker control file missing for bundle hash ${hashResult.value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctl: WorkerCtl;
|
|
||||||
try {
|
|
||||||
ctl = JSON.parse(ctlText) as WorkerCtl;
|
|
||||||
} catch {
|
|
||||||
return err(`corrupt worker control file: ${ctlPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
|
||||||
return err(`invalid worker control file: ${ctlPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await sendWorkerTcpCommand(
|
|
||||||
ctl.port,
|
|
||||||
{ type: "pause", threadId },
|
|
||||||
{ awaitResponseLine: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
import { err, type Result } from "@uncaged/workflow";
|
|
||||||
|
|
||||||
import { readTextFileIfExists } from "./fs-utils.js";
|
|
||||||
import {
|
|
||||||
resolveRunningHashForThread,
|
|
||||||
sendWorkerTcpCommand,
|
|
||||||
type WorkerCtl,
|
|
||||||
} from "./worker-spawn.js";
|
|
||||||
|
|
||||||
export async function cmdResume(
|
|
||||||
storageRoot: string,
|
|
||||||
threadId: string,
|
|
||||||
): Promise<Result<void, string>> {
|
|
||||||
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
|
||||||
if (!hashResult.ok) {
|
|
||||||
return hashResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctlPath = join(storageRoot, "workers", `${hashResult.value}.json`);
|
|
||||||
const ctlText = await readTextFileIfExists(ctlPath);
|
|
||||||
if (ctlText === null) {
|
|
||||||
return err(`worker control file missing for bundle hash ${hashResult.value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctl: WorkerCtl;
|
|
||||||
try {
|
|
||||||
ctl = JSON.parse(ctlText) as WorkerCtl;
|
|
||||||
} catch {
|
|
||||||
return err(`corrupt worker control file: ${ctlPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
|
||||||
return err(`invalid worker control file: ${ctlPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await sendWorkerTcpCommand(
|
|
||||||
ctl.port,
|
|
||||||
{ type: "resume", threadId },
|
|
||||||
{ awaitResponseLine: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import type { CommandEntry } from "../../cli-command-types.js";
|
||||||
|
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||||
|
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||||
|
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||||
|
import { cmdGc } from "./gc.js";
|
||||||
|
import { cmdCasGet } from "./get.js";
|
||||||
|
import { cmdCasList } from "./list.js";
|
||||||
|
import { cmdCasPut } from "./put.js";
|
||||||
|
import { cmdCasRm } from "./rm.js";
|
||||||
|
import type { CasDispatchDeps } from "./types.js";
|
||||||
|
|
||||||
|
function usageText(): string {
|
||||||
|
return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchGc(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
if (argv.length > 0) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: gc takes no arguments`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdGc(storageRoot);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const stats = result.value;
|
||||||
|
printCliLine(
|
||||||
|
`scanned ${stats.scannedThreads} threads, ${stats.activeRefs} active refs, deleted ${stats.deletedEntries} entries`,
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchCasGet(storageRoot: string, rest: string[]): Promise<number> {
|
||||||
|
const hash = rest[0];
|
||||||
|
if (hash === undefined || rest.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: cas get requires <hash>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdCasGet(storageRoot, hash);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(result.value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchCasPut(storageRoot: string, rest: string[]): Promise<number> {
|
||||||
|
const content = rest[0];
|
||||||
|
if (content === undefined || rest.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: cas put requires <content>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdCasPut(storageRoot, content);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(result.value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchCasList(storageRoot: string, rest: string[]): Promise<number> {
|
||||||
|
if (rest.length > 0) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: cas list takes no arguments`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdCasList(storageRoot);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (const hash of result.value) {
|
||||||
|
printCliLine(hash);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchCasRm(storageRoot: string, rest: string[]): Promise<number> {
|
||||||
|
const hash = rest[0];
|
||||||
|
if (hash === undefined || rest.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: cas rm requires <hash>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdCasRm(storageRoot, hash);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`removed cas entry ${hash}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CAS_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||||
|
get: {
|
||||||
|
handler: dispatchCasGet,
|
||||||
|
args: "<hash>",
|
||||||
|
description: "Retrieve content by hash from CAS",
|
||||||
|
},
|
||||||
|
put: {
|
||||||
|
handler: dispatchCasPut,
|
||||||
|
args: "<content>",
|
||||||
|
description: "Store content in CAS, prints hash",
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
handler: dispatchCasList,
|
||||||
|
args: "",
|
||||||
|
description: "List all hashes in CAS",
|
||||||
|
},
|
||||||
|
rm: { handler: dispatchCasRm, args: "<hash>", description: "Remove a CAS entry by hash" },
|
||||||
|
gc: { handler: dispatchGc, args: "", description: "Garbage-collect unreferenced CAS entries" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createCasDispatcher(deps: CasDispatchDeps) {
|
||||||
|
const { dispatchGroup } = deps;
|
||||||
|
return async function dispatchCas(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const result = dispatchGroup("cas", CAS_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||||
|
if (result !== null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const sub = argv[0];
|
||||||
|
printCliError(`${usageText()}\n\nerror: unknown cas subcommand: ${sub}`);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createCasStore, err, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
export async function cmdCasGet(
|
||||||
|
storageRoot: string,
|
||||||
|
hash: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||||
|
const content = await cas.get(hash);
|
||||||
|
if (content === null) {
|
||||||
|
return err(`cas entry not found: ${hash}`);
|
||||||
|
}
|
||||||
|
return ok(content);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export {
|
||||||
|
CAS_SUBCOMMAND_TABLE,
|
||||||
|
createCasDispatcher,
|
||||||
|
dispatchCasGet,
|
||||||
|
dispatchCasList,
|
||||||
|
dispatchCasPut,
|
||||||
|
dispatchCasRm,
|
||||||
|
dispatchGc,
|
||||||
|
} from "./dispatch.js";
|
||||||
|
export { cmdGc } from "./gc.js";
|
||||||
|
export { cmdCasGet } from "./get.js";
|
||||||
|
export { cmdCasList } from "./list.js";
|
||||||
|
export { cmdCasPut } from "./put.js";
|
||||||
|
export { cmdCasRm } from "./rm.js";
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
export async function cmdCasList(storageRoot: string): Promise<Result<string[], string>> {
|
||||||
|
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||||
|
const hashes = await cas.list();
|
||||||
|
return ok(hashes);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
export async function cmdCasPut(
|
||||||
|
storageRoot: string,
|
||||||
|
content: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||||
|
const hash = await cas.put(content);
|
||||||
|
return ok(hash);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createCasStore, getGlobalCasDir, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
export async function cmdCasRm(storageRoot: string, hash: string): Promise<Result<void, string>> {
|
||||||
|
const cas = createCasStore(getGlobalCasDir(storageRoot));
|
||||||
|
await cas.delete(hash);
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||||
|
|
||||||
|
export type CasDispatchDeps = {
|
||||||
|
dispatchGroup: DispatchGroupFn;
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import type { CommandEntry } from "../../cli-command-types.js";
|
||||||
|
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||||
|
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||||
|
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||||
|
import { cmdInitTemplate } from "./template.js";
|
||||||
|
import type { InitDispatchDeps } from "./types.js";
|
||||||
|
import { cmdInitWorkspace } from "./workspace.js";
|
||||||
|
|
||||||
|
function usageText(): string {
|
||||||
|
return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchInitWorkspace(_storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const name = argv[0];
|
||||||
|
if (name === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: init workspace requires <name>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdInitWorkspace(process.cwd(), name);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`initialized workflow workspace at ${result.value.rootPath}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchInitTemplate(_storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const name = argv[0];
|
||||||
|
if (name === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: init template requires <name>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdInitTemplate(process.cwd(), name);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`initialized template at ${result.value.templatePath}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INIT_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||||
|
workspace: {
|
||||||
|
handler: dispatchInitWorkspace,
|
||||||
|
args: "<name>",
|
||||||
|
description: "Initialize a new workflow workspace",
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
handler: dispatchInitTemplate,
|
||||||
|
args: "<name>",
|
||||||
|
description: "Initialize a new workflow template",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createInitDispatcher(deps: InitDispatchDeps) {
|
||||||
|
const { dispatchGroup } = deps;
|
||||||
|
return async function dispatchInit(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const result = dispatchGroup("init", INIT_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||||
|
if (result !== null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const sub = argv[0];
|
||||||
|
printCliError(`${usageText()}\n\nerror: unknown init subcommand: ${sub}`);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export {
|
||||||
|
createInitDispatcher,
|
||||||
|
dispatchInitTemplate,
|
||||||
|
dispatchInitWorkspace,
|
||||||
|
INIT_SUBCOMMAND_TABLE,
|
||||||
|
} from "./dispatch.js";
|
||||||
|
export { cmdInitTemplate } from "./template.js";
|
||||||
|
export type { CmdInitTemplateSuccess, CmdInitWorkspaceSuccess } from "./types.js";
|
||||||
|
export { cmdInitWorkspace } from "./workspace.js";
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
|
||||||
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import { pathExists } from "../../fs-utils.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
templateIndexTs,
|
||||||
|
templateModeratorTs,
|
||||||
|
templatePackageJson,
|
||||||
|
templateRolesTs,
|
||||||
|
templateTsconfigJson,
|
||||||
|
} from "./templates.js";
|
||||||
|
import type { CmdInitTemplateSuccess } from "./types.js";
|
||||||
|
|
||||||
|
function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||||
|
if (name.length === 0) {
|
||||||
|
return err("workspace name must not be empty");
|
||||||
|
}
|
||||||
|
if (name === "." || name === "..") {
|
||||||
|
return err("invalid workspace name");
|
||||||
|
}
|
||||||
|
if (name.includes("/") || name.includes("\\")) {
|
||||||
|
return err("workspace name must not contain path separators");
|
||||||
|
}
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTemplatesWorkspaceGlob(workspaces: unknown): boolean {
|
||||||
|
return Array.isArray(workspaces) && workspaces.includes("templates/*");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPackageJsonWorkspaces(dir: string): Promise<unknown | null> {
|
||||||
|
const pkgPath = join(dir, "package.json");
|
||||||
|
if (!(await pathExists(pkgPath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
raw = await readFile(pkgPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw) as unknown;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof parsed !== "object" || parsed === null || !("workspaces" in parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (parsed as { workspaces: unknown }).workspaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve uncaged-workflow workspace root (package.json with `templates/*` in `workspaces`). */
|
||||||
|
async function findWorkflowWorkspaceRoot(startDir: string): Promise<Result<string, string>> {
|
||||||
|
let dir = resolve(startDir);
|
||||||
|
for (;;) {
|
||||||
|
const workspaces = await readPackageJsonWorkspaces(dir);
|
||||||
|
if (workspaces !== null && hasTemplatesWorkspaceGlob(workspaces)) {
|
||||||
|
return ok(dir);
|
||||||
|
}
|
||||||
|
const parent = dirname(dir);
|
||||||
|
if (parent === dir) {
|
||||||
|
return err(
|
||||||
|
'not inside a workflow workspace (no package.json with workspaces containing "templates/*")',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdInitTemplate(
|
||||||
|
startDir: string,
|
||||||
|
templateName: string,
|
||||||
|
): Promise<Result<CmdInitTemplateSuccess, string>> {
|
||||||
|
const validated = validateWorkspaceSegment(templateName);
|
||||||
|
if (!validated.ok) {
|
||||||
|
return validated;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootResult = await findWorkflowWorkspaceRoot(startDir);
|
||||||
|
if (!rootResult.ok) {
|
||||||
|
return rootResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceRoot = rootResult.value;
|
||||||
|
const templateDir = join(workspaceRoot, "templates", templateName);
|
||||||
|
if (await pathExists(templateDir)) {
|
||||||
|
return err(`template already exists: ${templateDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mkdir(join(templateDir, "src"), { recursive: true });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
writeFile(join(templateDir, "package.json"), templatePackageJson(templateName), "utf8"),
|
||||||
|
writeFile(join(templateDir, "tsconfig.json"), templateTsconfigJson(), "utf8"),
|
||||||
|
writeFile(join(templateDir, "src", "roles.ts"), templateRolesTs(), "utf8"),
|
||||||
|
writeFile(join(templateDir, "src", "moderator.ts"), templateModeratorTs(), "utf8"),
|
||||||
|
writeFile(join(templateDir, "src", "index.ts"), templateIndexTs(), "utf8"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ok({ templatePath: templateDir });
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
export function templatePackageJson(templateName: string): string {
|
||||||
|
return `${JSON.stringify(
|
||||||
|
{
|
||||||
|
name: `template-${templateName}`,
|
||||||
|
version: "0.0.0",
|
||||||
|
private: true,
|
||||||
|
type: "module",
|
||||||
|
dependencies: {
|
||||||
|
"@uncaged/workflow": "^0.1.0",
|
||||||
|
zod: "^4.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function templateTsconfigJson(): string {
|
||||||
|
return `${JSON.stringify(
|
||||||
|
{
|
||||||
|
extends: "../../tsconfig.json",
|
||||||
|
compilerOptions: {
|
||||||
|
rootDir: "src",
|
||||||
|
outDir: "dist",
|
||||||
|
},
|
||||||
|
include: ["src/**/*.ts"],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function templateRolesTs(): string {
|
||||||
|
return `import type { RoleDefinition } from "@uncaged/workflow";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
export const HELLO_TEMPLATE_DESCRIPTION =
|
||||||
|
"Minimal starter template: one greeter role, then END.";
|
||||||
|
|
||||||
|
export type HelloTemplateMeta = {
|
||||||
|
greeter: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const greeterMetaSchema = z.object({
|
||||||
|
message: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
|
||||||
|
description: "Says hello — replace with your first role.",
|
||||||
|
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
|
||||||
|
extractPrompt: "Extract the assistant's greeting as message.",
|
||||||
|
schema: greeterMetaSchema,
|
||||||
|
extractRefs: null,
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function templateModeratorTs(): string {
|
||||||
|
return `import { END, type Moderator, type ModeratorContext } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import type { HelloTemplateMeta } from "./roles.js";
|
||||||
|
|
||||||
|
export const helloTemplateModerator: Moderator<HelloTemplateMeta> = (
|
||||||
|
ctx: ModeratorContext<HelloTemplateMeta>,
|
||||||
|
) => {
|
||||||
|
if (ctx.steps.length === 0) {
|
||||||
|
return "greeter";
|
||||||
|
}
|
||||||
|
return END;
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function templateIndexTs(): string {
|
||||||
|
return `import type { WorkflowDefinition } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import { helloTemplateModerator } from "./moderator.js";
|
||||||
|
import {
|
||||||
|
HELLO_TEMPLATE_DESCRIPTION,
|
||||||
|
type HelloTemplateMeta,
|
||||||
|
greeterRole,
|
||||||
|
} from "./roles.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
HELLO_TEMPLATE_DESCRIPTION,
|
||||||
|
type HelloTemplateMeta,
|
||||||
|
greeterRole,
|
||||||
|
} from "./roles.js";
|
||||||
|
export { helloTemplateModerator } from "./moderator.js";
|
||||||
|
|
||||||
|
export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = {
|
||||||
|
description: HELLO_TEMPLATE_DESCRIPTION,
|
||||||
|
roles: {
|
||||||
|
greeter: greeterRole,
|
||||||
|
},
|
||||||
|
moderator: helloTemplateModerator,
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||||
|
|
||||||
|
export type CmdInitTemplateSuccess = {
|
||||||
|
templatePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CmdInitWorkspaceSuccess = {
|
||||||
|
rootPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InitDispatchDeps = {
|
||||||
|
dispatchGroup: DispatchGroupFn;
|
||||||
|
};
|
||||||
+4
-191
@@ -1,17 +1,10 @@
|
|||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { err, ok, type Result } from "@uncaged/workflow";
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
import { pathExists } from "./fs-utils.js";
|
import { pathExists } from "../../fs-utils.js";
|
||||||
|
import type { CmdInitWorkspaceSuccess } from "./types.js";
|
||||||
export type CmdInitWorkspaceSuccess = {
|
|
||||||
rootPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CmdInitTemplateSuccess = {
|
|
||||||
templatePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function validateWorkspaceSegment(name: string): Result<void, string> {
|
function validateWorkspaceSegment(name: string): Result<void, string> {
|
||||||
if (name.length === 0) {
|
if (name.length === 0) {
|
||||||
@@ -233,183 +226,3 @@ export async function cmdInitWorkspace(
|
|||||||
|
|
||||||
return ok({ rootPath });
|
return ok({ rootPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasTemplatesWorkspaceGlob(workspaces: unknown): boolean {
|
|
||||||
return Array.isArray(workspaces) && workspaces.includes("templates/*");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readPackageJsonWorkspaces(dir: string): Promise<unknown | null> {
|
|
||||||
const pkgPath = join(dir, "package.json");
|
|
||||||
if (!(await pathExists(pkgPath))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let raw: string;
|
|
||||||
try {
|
|
||||||
raw = await readFile(pkgPath, "utf8");
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let parsed: unknown;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(raw) as unknown;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (typeof parsed !== "object" || parsed === null || !("workspaces" in parsed)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (parsed as { workspaces: unknown }).workspaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolve uncaged-workflow workspace root (package.json with `templates/*` in `workspaces`). */
|
|
||||||
async function findWorkflowWorkspaceRoot(startDir: string): Promise<Result<string, string>> {
|
|
||||||
let dir = resolve(startDir);
|
|
||||||
for (;;) {
|
|
||||||
const workspaces = await readPackageJsonWorkspaces(dir);
|
|
||||||
if (workspaces !== null && hasTemplatesWorkspaceGlob(workspaces)) {
|
|
||||||
return ok(dir);
|
|
||||||
}
|
|
||||||
const parent = dirname(dir);
|
|
||||||
if (parent === dir) {
|
|
||||||
return err(
|
|
||||||
'not inside a workflow workspace (no package.json with workspaces containing "templates/*")',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
dir = parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function templatePackageJson(templateName: string): string {
|
|
||||||
return `${JSON.stringify(
|
|
||||||
{
|
|
||||||
name: `template-${templateName}`,
|
|
||||||
version: "0.0.0",
|
|
||||||
private: true,
|
|
||||||
type: "module",
|
|
||||||
dependencies: {
|
|
||||||
"@uncaged/workflow": "^0.1.0",
|
|
||||||
zod: "^4.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateTsconfigJson(): string {
|
|
||||||
return `${JSON.stringify(
|
|
||||||
{
|
|
||||||
extends: "../../tsconfig.json",
|
|
||||||
compilerOptions: {
|
|
||||||
rootDir: "src",
|
|
||||||
outDir: "dist",
|
|
||||||
},
|
|
||||||
include: ["src/**/*.ts"],
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateRolesTs(): string {
|
|
||||||
return `import type { RoleDefinition } from "@uncaged/workflow";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
export const HELLO_TEMPLATE_DESCRIPTION =
|
|
||||||
"Minimal starter template: one greeter role, then END.";
|
|
||||||
|
|
||||||
export type HelloTemplateMeta = {
|
|
||||||
greeter: {
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const greeterMetaSchema = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const greeterRole: RoleDefinition<HelloTemplateMeta["greeter"]> = {
|
|
||||||
description: "Says hello — replace with your first role.",
|
|
||||||
systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.",
|
|
||||||
extractPrompt: "Extract the assistant's greeting as message.",
|
|
||||||
schema: greeterMetaSchema,
|
|
||||||
extractRefs: null,
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateModeratorTs(): string {
|
|
||||||
return `import { END, type Moderator, type ModeratorContext } from "@uncaged/workflow";
|
|
||||||
|
|
||||||
import type { HelloTemplateMeta } from "./roles.js";
|
|
||||||
|
|
||||||
export const helloTemplateModerator: Moderator<HelloTemplateMeta> = (
|
|
||||||
ctx: ModeratorContext<HelloTemplateMeta>,
|
|
||||||
) => {
|
|
||||||
if (ctx.steps.length === 0) {
|
|
||||||
return "greeter";
|
|
||||||
}
|
|
||||||
return END;
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function templateIndexTs(): string {
|
|
||||||
return `import type { WorkflowDefinition } from "@uncaged/workflow";
|
|
||||||
|
|
||||||
import { helloTemplateModerator } from "./moderator.js";
|
|
||||||
import {
|
|
||||||
HELLO_TEMPLATE_DESCRIPTION,
|
|
||||||
type HelloTemplateMeta,
|
|
||||||
greeterRole,
|
|
||||||
} from "./roles.js";
|
|
||||||
|
|
||||||
export {
|
|
||||||
HELLO_TEMPLATE_DESCRIPTION,
|
|
||||||
type HelloTemplateMeta,
|
|
||||||
greeterRole,
|
|
||||||
} from "./roles.js";
|
|
||||||
export { helloTemplateModerator } from "./moderator.js";
|
|
||||||
|
|
||||||
export const helloTemplateWorkflowDefinition: WorkflowDefinition<HelloTemplateMeta> = {
|
|
||||||
description: HELLO_TEMPLATE_DESCRIPTION,
|
|
||||||
roles: {
|
|
||||||
greeter: greeterRole,
|
|
||||||
},
|
|
||||||
moderator: helloTemplateModerator,
|
|
||||||
};
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cmdInitTemplate(
|
|
||||||
startDir: string,
|
|
||||||
templateName: string,
|
|
||||||
): Promise<Result<CmdInitTemplateSuccess, string>> {
|
|
||||||
const validated = validateWorkspaceSegment(templateName);
|
|
||||||
if (!validated.ok) {
|
|
||||||
return validated;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootResult = await findWorkflowWorkspaceRoot(startDir);
|
|
||||||
if (!rootResult.ok) {
|
|
||||||
return rootResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceRoot = rootResult.value;
|
|
||||||
const templateDir = join(workspaceRoot, "templates", templateName);
|
|
||||||
if (await pathExists(templateDir)) {
|
|
||||||
return err(`template already exists: ${templateDir}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await mkdir(join(templateDir, "src"), { recursive: true });
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
writeFile(join(templateDir, "package.json"), templatePackageJson(templateName), "utf8"),
|
|
||||||
writeFile(join(templateDir, "tsconfig.json"), templateTsconfigJson(), "utf8"),
|
|
||||||
writeFile(join(templateDir, "src", "roles.ts"), templateRolesTs(), "utf8"),
|
|
||||||
writeFile(join(templateDir, "src", "moderator.ts"), templateModeratorTs(), "utf8"),
|
|
||||||
writeFile(join(templateDir, "src", "index.ts"), templateIndexTs(), "utf8"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return ok({ templatePath: templateDir });
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import {
|
||||||
|
readWorkerCtl,
|
||||||
|
resolveRunningHashForThread,
|
||||||
|
sendWorkerTcpCommand,
|
||||||
|
} from "../../worker-spawn.js";
|
||||||
|
|
||||||
|
type ThreadControlAction = "kill" | "pause" | "resume";
|
||||||
|
|
||||||
|
async function cmdThreadControl(
|
||||||
|
storageRoot: string,
|
||||||
|
threadId: string,
|
||||||
|
action: ThreadControlAction,
|
||||||
|
): Promise<Result<void, string>> {
|
||||||
|
const hashResult = await resolveRunningHashForThread(storageRoot, threadId);
|
||||||
|
if (!hashResult.ok) {
|
||||||
|
return hashResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctlResult = await readWorkerCtl(storageRoot, hashResult.value);
|
||||||
|
if (!ctlResult.ok) {
|
||||||
|
return ctlResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sendWorkerTcpCommand(
|
||||||
|
ctlResult.value.port,
|
||||||
|
{ type: action, threadId },
|
||||||
|
{ awaitResponseLine: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdKill(
|
||||||
|
storageRoot: string,
|
||||||
|
threadId: string,
|
||||||
|
): Promise<Result<void, string>> {
|
||||||
|
return cmdThreadControl(storageRoot, threadId, "kill");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdPause(
|
||||||
|
storageRoot: string,
|
||||||
|
threadId: string,
|
||||||
|
): Promise<Result<void, string>> {
|
||||||
|
return cmdThreadControl(storageRoot, threadId, "pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdResume(
|
||||||
|
storageRoot: string,
|
||||||
|
threadId: string,
|
||||||
|
): Promise<Result<void, string>> {
|
||||||
|
return cmdThreadControl(storageRoot, threadId, "resume");
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import type { CommandEntry } from "../../cli-command-types.js";
|
||||||
|
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||||
|
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||||
|
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||||
|
import { parseLiveArgv } from "../../live-argv.js";
|
||||||
|
import { parseRunArgv } from "../../run-argv.js";
|
||||||
|
import { cmdKill, cmdPause, cmdResume } from "./control.js";
|
||||||
|
import { cmdFork } from "./fork.js";
|
||||||
|
import { parseForkArgv } from "./fork-argv.js";
|
||||||
|
import { cmdThreads } from "./list.js";
|
||||||
|
import { cmdLive } from "./live.js";
|
||||||
|
import { cmdPs } from "./ps.js";
|
||||||
|
import { cmdThreadRemove } from "./rm.js";
|
||||||
|
import { cmdRun } from "./run.js";
|
||||||
|
import { cmdThreadShow } from "./show.js";
|
||||||
|
import type { ThreadDispatchDeps } from "./types.js";
|
||||||
|
|
||||||
|
function usageText(): string {
|
||||||
|
return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchRun(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const parsed = parseRunArgv(argv);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: ${parsed.error}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await cmdRun(
|
||||||
|
storageRoot,
|
||||||
|
parsed.value.name,
|
||||||
|
parsed.value.prompt,
|
||||||
|
parsed.value.maxRounds,
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printCliLine(result.value.threadId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchPs(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
if (argv.length > 0) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: ps takes no arguments`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (const line of await cmdPs(storageRoot)) {
|
||||||
|
printCliLine(line);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchKill(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const threadId = argv[0];
|
||||||
|
if (threadId === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: kill requires <thread-id>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdKill(storageRoot, threadId);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`kill sent for thread ${threadId}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchLive(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const parsed = parseLiveArgv(argv);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: ${parsed.error}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return cmdLive(storageRoot, parsed.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchPause(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const threadId = argv[0];
|
||||||
|
if (threadId === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: pause requires <thread-id>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdPause(storageRoot, threadId);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`pause sent for thread ${threadId}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchResume(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const threadId = argv[0];
|
||||||
|
if (threadId === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: resume requires <thread-id>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdResume(storageRoot, threadId);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`resume sent for thread ${threadId}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchThreadList(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const result = await cmdThreads(storageRoot, argv);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (const line of result.value) {
|
||||||
|
printCliLine(line);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchThreadShow(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const id = argv[0];
|
||||||
|
if (id === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: thread show requires <id>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdThreadShow(storageRoot, id);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(result.value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchThreadRm(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const id = argv[0];
|
||||||
|
if (id === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: thread rm requires <id>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdThreadRemove(storageRoot, id);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`removed thread ${id}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchFork(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const parsed = parseForkArgv(argv);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: ${parsed.error}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdFork(storageRoot, parsed.value.threadId, parsed.value.fromRole);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(result.value.threadId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const THREAD_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||||
|
run: {
|
||||||
|
handler: dispatchRun,
|
||||||
|
args: "<name> [--prompt <text>] [--max-rounds N]",
|
||||||
|
description: "Start a new thread executing a workflow",
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
handler: dispatchThreadList,
|
||||||
|
args: "[name]",
|
||||||
|
description: "List threads, optionally filtered by workflow name",
|
||||||
|
},
|
||||||
|
show: { handler: dispatchThreadShow, args: "<id>", description: "Show thread details and state" },
|
||||||
|
rm: { handler: dispatchThreadRm, args: "<id>", description: "Remove a thread" },
|
||||||
|
fork: {
|
||||||
|
handler: dispatchFork,
|
||||||
|
args: "<thread-id> [--from-role <role>]",
|
||||||
|
description: "Fork a thread, optionally from a specific role",
|
||||||
|
},
|
||||||
|
ps: { handler: dispatchPs, args: "", description: "List running threads" },
|
||||||
|
kill: { handler: dispatchKill, args: "<thread-id>", description: "Kill a running thread" },
|
||||||
|
live: {
|
||||||
|
handler: dispatchLive,
|
||||||
|
args: "<thread-id> | --latest [--debug] [--role <name>]",
|
||||||
|
description: "Attach to a thread and stream output live",
|
||||||
|
},
|
||||||
|
pause: { handler: dispatchPause, args: "<thread-id>", description: "Pause a running thread" },
|
||||||
|
resume: { handler: dispatchResume, args: "<thread-id>", description: "Resume a paused thread" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createThreadDispatcher(deps: ThreadDispatchDeps) {
|
||||||
|
const { dispatchGroup } = deps;
|
||||||
|
return async function dispatchThread(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const result = dispatchGroup("thread", THREAD_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||||
|
if (result !== null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const sub = argv[0];
|
||||||
|
printCliError(`${usageText()}\n\nerror: unknown thread subcommand: ${sub}`);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import type { ParsedForkArgv } from "./types.js";
|
||||||
|
|
||||||
|
export function parseForkArgv(argv: string[]): Result<ParsedForkArgv, string> {
|
||||||
|
if (argv.length === 0) {
|
||||||
|
return err("fork requires <thread-id>");
|
||||||
|
}
|
||||||
|
const threadId = argv[0];
|
||||||
|
if (threadId === undefined || threadId === "") {
|
||||||
|
return err("fork requires <thread-id>");
|
||||||
|
}
|
||||||
|
let fromRole: string | null = null;
|
||||||
|
for (let i = 1; i < argv.length; i++) {
|
||||||
|
const a = argv[i];
|
||||||
|
if (a === "--from-role") {
|
||||||
|
const r = argv[i + 1];
|
||||||
|
if (r === undefined || r === "") {
|
||||||
|
return err("--from-role requires a role name");
|
||||||
|
}
|
||||||
|
fromRole = r;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return err(`unexpected argument: ${a}`);
|
||||||
|
}
|
||||||
|
return ok({ threadId, fromRole });
|
||||||
|
}
|
||||||
+3
-30
@@ -2,36 +2,9 @@ import { join } from "node:path";
|
|||||||
|
|
||||||
import { buildForkPlan, err, generateUlid, ok, type Result } from "@uncaged/workflow";
|
import { buildForkPlan, err, generateUlid, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
import { pathExists, readTextFileIfExists } from "./fs-utils.js";
|
import { pathExists, readTextFileIfExists } from "../../fs-utils.js";
|
||||||
import { resolveThreadDataPath } from "./thread-scan.js";
|
import { resolveThreadDataPath } from "../../thread-scan.js";
|
||||||
import { ensureWorkerForHash, sendWorkerTcpCommand } from "./worker-spawn.js";
|
import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js";
|
||||||
|
|
||||||
export function parseForkArgv(
|
|
||||||
argv: string[],
|
|
||||||
): Result<{ threadId: string; fromRole: string | null }, string> {
|
|
||||||
if (argv.length === 0) {
|
|
||||||
return err("fork requires <thread-id>");
|
|
||||||
}
|
|
||||||
const threadId = argv[0];
|
|
||||||
if (threadId === undefined || threadId === "") {
|
|
||||||
return err("fork requires <thread-id>");
|
|
||||||
}
|
|
||||||
let fromRole: string | null = null;
|
|
||||||
for (let i = 1; i < argv.length; i++) {
|
|
||||||
const a = argv[i];
|
|
||||||
if (a === "--from-role") {
|
|
||||||
const r = argv[i + 1];
|
|
||||||
if (r === undefined || r === "") {
|
|
||||||
return err("--from-role requires a role name");
|
|
||||||
}
|
|
||||||
fromRole = r;
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return err(`unexpected argument: ${a}`);
|
|
||||||
}
|
|
||||||
return ok({ threadId, fromRole });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cmdFork(
|
export async function cmdFork(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
export { cmdKill, cmdPause, cmdResume } from "./control.js";
|
||||||
|
export {
|
||||||
|
createThreadDispatcher,
|
||||||
|
dispatchFork,
|
||||||
|
dispatchKill,
|
||||||
|
dispatchLive,
|
||||||
|
dispatchPause,
|
||||||
|
dispatchPs,
|
||||||
|
dispatchResume,
|
||||||
|
dispatchRun,
|
||||||
|
dispatchThreadList,
|
||||||
|
dispatchThreadRm,
|
||||||
|
dispatchThreadShow,
|
||||||
|
THREAD_SUBCOMMAND_TABLE,
|
||||||
|
} from "./dispatch.js";
|
||||||
|
export { cmdFork } from "./fork.js";
|
||||||
|
export { parseForkArgv } from "./fork-argv.js";
|
||||||
|
export { cmdThreads } from "./list.js";
|
||||||
|
export {
|
||||||
|
cmdLive,
|
||||||
|
formatLiveDebugLine,
|
||||||
|
formatLiveTimeLabel,
|
||||||
|
LIVE_CONTENT_MAX_LINES,
|
||||||
|
renderLiveRoleStepLines,
|
||||||
|
} from "./live.js";
|
||||||
|
export { cmdPs } from "./ps.js";
|
||||||
|
export { cmdThreadRemove } from "./rm.js";
|
||||||
|
export { cmdRun } from "./run.js";
|
||||||
|
export { cmdThreadShow } from "./show.js";
|
||||||
|
export type { LiveRoleRow } from "./types.js";
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import { err, ok, type Result } from "@uncaged/workflow";
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
import { listHistoricalThreads } from "./thread-scan.js";
|
import { listHistoricalThreads } from "../../thread-scan.js";
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||||
|
|
||||||
export async function cmdThreads(
|
export async function cmdThreads(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
+6
-29
@@ -12,20 +12,15 @@ import {
|
|||||||
type WorkflowCompletion,
|
type WorkflowCompletion,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
import { printCliError, printCliLine } from "./cli-output.js";
|
import { dimGreyLine, highlightLiveRole } from "../../cli-color.js";
|
||||||
import { pathExists } from "./fs-utils.js";
|
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||||
import type { ParsedLiveArgv } from "./live-argv.js";
|
import { pathExists } from "../../fs-utils.js";
|
||||||
import { findLatestThreadDataPath, resolveThreadDataPath } from "./thread-scan.js";
|
import type { ParsedLiveArgv } from "../../live-argv.js";
|
||||||
|
import { findLatestThreadDataPath, resolveThreadDataPath } from "../../thread-scan.js";
|
||||||
|
import type { LiveRoleRow } from "./types.js";
|
||||||
|
|
||||||
export const LIVE_CONTENT_MAX_LINES = 10;
|
export const LIVE_CONTENT_MAX_LINES = 10;
|
||||||
|
|
||||||
export type LiveRoleRow = {
|
|
||||||
role: string;
|
|
||||||
content: string;
|
|
||||||
meta: Record<string, unknown>;
|
|
||||||
timestamp: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function formatLiveTimeLabel(timestampMs: number): string {
|
export function formatLiveTimeLabel(timestampMs: number): string {
|
||||||
const d = new Date(timestampMs);
|
const d = new Date(timestampMs);
|
||||||
const hh = String(d.getHours()).padStart(2, "0");
|
const hh = String(d.getHours()).padStart(2, "0");
|
||||||
@@ -34,24 +29,6 @@ export function formatLiveTimeLabel(timestampMs: number): string {
|
|||||||
return `${hh}:${mm}:${ss}`;
|
return `${hh}:${mm}:${ss}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldUseColor(): boolean {
|
|
||||||
return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlightLiveRole(name: string): string {
|
|
||||||
if (!shouldUseColor()) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return `\x1b[1m\x1b[36m${name}\x1b[0m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dimGreyLine(line: string): string {
|
|
||||||
if (!shouldUseColor()) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
return `\x1b[2m\x1b[90m${line}\x1b[0m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatLiveDebugLine(timestampMs: number, tag: string, message: string): string {
|
export function formatLiveDebugLine(timestampMs: number, tag: string, message: string): string {
|
||||||
const label = `[${formatLiveTimeLabel(timestampMs)}] [${tag}] ${message.replace(/\n/g, " ")}`;
|
const label = `[${formatLiveTimeLabel(timestampMs)}] [${tag}] ${message.replace(/\n/g, " ")}`;
|
||||||
return dimGreyLine(label);
|
return dimGreyLine(label);
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { listRunningThreads } from "./thread-scan.js";
|
import { listRunningThreads } from "../../thread-scan.js";
|
||||||
|
|
||||||
export async function cmdPs(storageRoot: string): Promise<string[]> {
|
export async function cmdPs(storageRoot: string): Promise<string[]> {
|
||||||
const rows = await listRunningThreads(storageRoot);
|
const rows = await listRunningThreads(storageRoot);
|
||||||
+1
-17
@@ -3,23 +3,7 @@ import { dirname, join } from "node:path";
|
|||||||
|
|
||||||
import { err, garbageCollectCas, ok, type Result } from "@uncaged/workflow";
|
import { err, garbageCollectCas, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
import { readTextFileIfExists } from "./fs-utils.js";
|
import { resolveThreadDataPath } from "../../thread-scan.js";
|
||||||
import { resolveThreadDataPath } from "./thread-scan.js";
|
|
||||||
|
|
||||||
export async function cmdThreadShow(
|
|
||||||
storageRoot: string,
|
|
||||||
threadId: string,
|
|
||||||
): Promise<Result<string, string>> {
|
|
||||||
const dataPath = await resolveThreadDataPath(storageRoot, threadId);
|
|
||||||
if (dataPath === null) {
|
|
||||||
return err(`thread not found: ${threadId}`);
|
|
||||||
}
|
|
||||||
const text = await readTextFileIfExists(dataPath);
|
|
||||||
if (text === null) {
|
|
||||||
return err(`thread data missing: ${threadId}`);
|
|
||||||
}
|
|
||||||
return ok(text.endsWith("\n") ? text.slice(0, -1) : text);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cmdThreadRemove(
|
export async function cmdThreadRemove(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
+2
-2
@@ -8,8 +8,8 @@ import {
|
|||||||
type Result,
|
type Result,
|
||||||
readWorkflowRegistry,
|
readWorkflowRegistry,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
import { ensureWorkerForHash, sendWorkerTcpCommand } from "./worker-spawn.js";
|
import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js";
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||||
|
|
||||||
export async function cmdRun(
|
export async function cmdRun(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import { readTextFileIfExists } from "../../fs-utils.js";
|
||||||
|
import { resolveThreadDataPath } from "../../thread-scan.js";
|
||||||
|
|
||||||
|
export async function cmdThreadShow(
|
||||||
|
storageRoot: string,
|
||||||
|
threadId: string,
|
||||||
|
): Promise<Result<string, string>> {
|
||||||
|
const dataPath = await resolveThreadDataPath(storageRoot, threadId);
|
||||||
|
if (dataPath === null) {
|
||||||
|
return err(`thread not found: ${threadId}`);
|
||||||
|
}
|
||||||
|
const text = await readTextFileIfExists(dataPath);
|
||||||
|
if (text === null) {
|
||||||
|
return err(`thread data missing: ${threadId}`);
|
||||||
|
}
|
||||||
|
return ok(text.endsWith("\n") ? text.slice(0, -1) : text);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||||
|
|
||||||
|
export type LiveRoleRow = {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParsedForkArgv = {
|
||||||
|
threadId: string;
|
||||||
|
fromRole: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThreadDispatchDeps = {
|
||||||
|
dispatchGroup: DispatchGroupFn;
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { err, ok, type Result } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import type { ParsedAddArgv } from "./types.js";
|
||||||
|
|
||||||
|
type ParsedLongFlag = { advance: 2; kind: "types"; value: string };
|
||||||
|
|
||||||
|
function tryParseAddLongFlag(argv: string[], index: number): Result<ParsedLongFlag | null, string> {
|
||||||
|
const tok = argv[index];
|
||||||
|
if (tok !== "--types") {
|
||||||
|
return ok(null);
|
||||||
|
}
|
||||||
|
const value = argv[index + 1];
|
||||||
|
if (value === undefined || value.startsWith("--")) {
|
||||||
|
return err("missing value for --types");
|
||||||
|
}
|
||||||
|
return ok({ advance: 2, kind: "types", value });
|
||||||
|
}
|
||||||
|
|
||||||
|
type PositionalSlots = {
|
||||||
|
name: string | undefined;
|
||||||
|
filePath: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function assignPositional(tok: string, slots: PositionalSlots): Result<void, string> {
|
||||||
|
if (slots.name === undefined) {
|
||||||
|
slots.name = tok;
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
if (slots.filePath === undefined) {
|
||||||
|
slots.filePath = tok;
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
return err("too many arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAddArgv(argv: string[]): Result<ParsedAddArgv, string> {
|
||||||
|
const slots: PositionalSlots = { name: undefined, filePath: undefined };
|
||||||
|
let typesPath: string | null = null;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
while (i < argv.length) {
|
||||||
|
const flag = tryParseAddLongFlag(argv, i);
|
||||||
|
if (!flag.ok) {
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
if (flag.value !== null) {
|
||||||
|
typesPath = flag.value.value;
|
||||||
|
i += flag.value.advance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tok = argv[i];
|
||||||
|
if (tok?.startsWith("--")) {
|
||||||
|
return err(`unknown add flag: ${tok}`);
|
||||||
|
}
|
||||||
|
if (tok === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const placed = assignPositional(tok, slots);
|
||||||
|
if (!placed.ok) {
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, filePath } = slots;
|
||||||
|
if (name === undefined || name === "" || filePath === undefined || filePath === "") {
|
||||||
|
return err("add requires <name> <file>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({ name, filePath, typesPath });
|
||||||
|
}
|
||||||
+3
-82
@@ -14,20 +14,10 @@ import {
|
|||||||
writeWorkflowRegistry,
|
writeWorkflowRegistry,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
import { storeWorkflowBundleArtifacts } from "./bundle-store.js";
|
import { storeWorkflowBundleArtifacts } from "../../bundle-store.js";
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||||
|
|
||||||
export type ParsedAddArgv = {
|
import type { CmdAddSuccess, ParsedAddArgv } from "./types.js";
|
||||||
name: string;
|
|
||||||
filePath: string;
|
|
||||||
/** Override path to `.d.ts` when adding a bundle. */
|
|
||||||
typesPath: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CmdAddSuccess = {
|
|
||||||
hash: string;
|
|
||||||
warnings: ReadonlyArray<string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isEsmBundle(path: string): boolean {
|
function isEsmBundle(path: string): boolean {
|
||||||
return path.endsWith(".esm.js");
|
return path.endsWith(".esm.js");
|
||||||
@@ -37,75 +27,6 @@ function defaultTypesPath(bundlePath: string): string {
|
|||||||
return bundlePath.replace(/\.esm\.js$/i, ".d.ts");
|
return bundlePath.replace(/\.esm\.js$/i, ".d.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParsedLongFlag = { advance: 2; kind: "types"; value: string };
|
|
||||||
|
|
||||||
function tryParseAddLongFlag(argv: string[], index: number): Result<ParsedLongFlag | null, string> {
|
|
||||||
const tok = argv[index];
|
|
||||||
if (tok !== "--types") {
|
|
||||||
return ok(null);
|
|
||||||
}
|
|
||||||
const value = argv[index + 1];
|
|
||||||
if (value === undefined || value.startsWith("--")) {
|
|
||||||
return err("missing value for --types");
|
|
||||||
}
|
|
||||||
return ok({ advance: 2, kind: "types", value });
|
|
||||||
}
|
|
||||||
|
|
||||||
type PositionalSlots = {
|
|
||||||
name: string | undefined;
|
|
||||||
filePath: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function assignPositional(tok: string, slots: PositionalSlots): Result<void, string> {
|
|
||||||
if (slots.name === undefined) {
|
|
||||||
slots.name = tok;
|
|
||||||
return ok(undefined);
|
|
||||||
}
|
|
||||||
if (slots.filePath === undefined) {
|
|
||||||
slots.filePath = tok;
|
|
||||||
return ok(undefined);
|
|
||||||
}
|
|
||||||
return err("too many arguments");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseAddArgv(argv: string[]): Result<ParsedAddArgv, string> {
|
|
||||||
const slots: PositionalSlots = { name: undefined, filePath: undefined };
|
|
||||||
let typesPath: string | null = null;
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
while (i < argv.length) {
|
|
||||||
const flag = tryParseAddLongFlag(argv, i);
|
|
||||||
if (!flag.ok) {
|
|
||||||
return flag;
|
|
||||||
}
|
|
||||||
if (flag.value !== null) {
|
|
||||||
typesPath = flag.value.value;
|
|
||||||
i += flag.value.advance;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tok = argv[i];
|
|
||||||
if (tok?.startsWith("--")) {
|
|
||||||
return err(`unknown add flag: ${tok}`);
|
|
||||||
}
|
|
||||||
if (tok === undefined) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const placed = assignPositional(tok, slots);
|
|
||||||
if (!placed.ok) {
|
|
||||||
return placed;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, filePath } = slots;
|
|
||||||
if (name === undefined || name === "" || filePath === undefined || filePath === "") {
|
|
||||||
return err("add requires <name> <file>");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok({ name, filePath, typesPath });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerHash(
|
async function registerHash(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
name: string,
|
name: string,
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import type { CommandEntry } from "../../cli-command-types.js";
|
||||||
|
import { printCliError, printCliLine, printCliWarn } from "../../cli-output.js";
|
||||||
|
import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js";
|
||||||
|
import { getCommandGroupsForUsage } from "../../cli-usage-context.js";
|
||||||
|
import { cmdAdd, formatAddSuccess } from "./add.js";
|
||||||
|
import { parseAddArgv } from "./add-argv.js";
|
||||||
|
import { cmdHistory } from "./history.js";
|
||||||
|
import { cmdList, formatListLines } from "./list.js";
|
||||||
|
import { cmdRemove } from "./rm.js";
|
||||||
|
import { cmdRollback } from "./rollback.js";
|
||||||
|
import { cmdShow, formatShowYaml } from "./show.js";
|
||||||
|
import type { WorkflowDispatchDeps } from "./types.js";
|
||||||
|
|
||||||
|
function usageText(): string {
|
||||||
|
return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchAdd(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const parsed = parseAddArgv(argv);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: ${parsed.error}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdAdd(storageRoot, parsed.value);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (const w of result.value.warnings) {
|
||||||
|
printCliWarn(w);
|
||||||
|
}
|
||||||
|
printCliLine(formatAddSuccess(parsed.value.name, parsed.value.filePath, result.value.hash));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchList(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
if (argv.length > 0) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: list takes no arguments`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdList(storageRoot);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (const line of formatListLines(result.value)) {
|
||||||
|
printCliLine(line);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchShow(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const name = argv[0];
|
||||||
|
if (name === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: show requires <name>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdShow(storageRoot, name);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(formatShowYaml(name, result.value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchRemove(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const name = argv[0];
|
||||||
|
if (name === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: remove requires <name>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdRemove(storageRoot, name);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`removed workflow "${name}" from registry`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchHistory(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const name = argv[0];
|
||||||
|
if (name === undefined || argv.length > 1) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: history requires <name>`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdHistory(storageRoot, name);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (const line of result.value) {
|
||||||
|
printCliLine(line);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchRollback(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const name = argv[0];
|
||||||
|
if (name === undefined || argv.length > 2) {
|
||||||
|
printCliError(`${usageText()}\n\nerror: rollback requires <name> [hash]`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const hashArg = argv[1];
|
||||||
|
const result = await cmdRollback(storageRoot, name, hashArg === undefined ? null : hashArg);
|
||||||
|
if (!result.ok) {
|
||||||
|
printCliError(result.error);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(`rolled back workflow "${name}"`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WORKFLOW_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||||
|
add: {
|
||||||
|
handler: dispatchAdd,
|
||||||
|
args: "<name> <file.esm.js> [--types <path>]",
|
||||||
|
description: "Register a workflow bundle in the registry",
|
||||||
|
},
|
||||||
|
list: { handler: dispatchList, args: "", description: "List all registered workflows" },
|
||||||
|
show: {
|
||||||
|
handler: dispatchShow,
|
||||||
|
args: "<name>",
|
||||||
|
description: "Show details of a registered workflow",
|
||||||
|
},
|
||||||
|
rm: {
|
||||||
|
handler: dispatchRemove,
|
||||||
|
args: "<name>",
|
||||||
|
description: "Remove a workflow from the registry",
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
handler: dispatchHistory,
|
||||||
|
args: "<name>",
|
||||||
|
description: "Show version history of a workflow",
|
||||||
|
},
|
||||||
|
rollback: {
|
||||||
|
handler: dispatchRollback,
|
||||||
|
args: "<name> [hash]",
|
||||||
|
description: "Rollback a workflow to a previous version",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createWorkflowDispatcher(deps: WorkflowDispatchDeps) {
|
||||||
|
const { dispatchGroup, printDeprecation } = deps;
|
||||||
|
return async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const result = dispatchGroup("workflow", WORKFLOW_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||||
|
if (result !== null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const sub = argv[0];
|
||||||
|
if (sub === "remove") {
|
||||||
|
printDeprecation("workflow remove", "workflow rm");
|
||||||
|
return dispatchRemove(storageRoot, argv.slice(1));
|
||||||
|
}
|
||||||
|
printCliError(`${usageText()}\n\nerror: unknown workflow subcommand: ${sub}`);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
|||||||
readWorkflowRegistry,
|
readWorkflowRegistry,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||||
|
|
||||||
export async function cmdHistory(
|
export async function cmdHistory(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export { cmdAdd, formatAddSuccess } from "./add.js";
|
||||||
|
export { parseAddArgv } from "./add-argv.js";
|
||||||
|
export {
|
||||||
|
createWorkflowDispatcher,
|
||||||
|
dispatchAdd,
|
||||||
|
dispatchHistory,
|
||||||
|
dispatchList,
|
||||||
|
dispatchRemove,
|
||||||
|
dispatchRollback,
|
||||||
|
dispatchShow,
|
||||||
|
WORKFLOW_SUBCOMMAND_TABLE,
|
||||||
|
} from "./dispatch.js";
|
||||||
|
export { cmdHistory } from "./history.js";
|
||||||
|
export { cmdList, formatListLines } from "./list.js";
|
||||||
|
export { cmdRemove } from "./rm.js";
|
||||||
|
export { cmdRollback } from "./rollback.js";
|
||||||
|
export { cmdShow, formatShowYaml } from "./show.js";
|
||||||
|
export type { CmdAddSuccess, ParsedAddArgv } from "./types.js";
|
||||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
|||||||
writeWorkflowRegistry,
|
writeWorkflowRegistry,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||||
|
|
||||||
export async function cmdRemove(storageRoot: string, name: string): Promise<Result<void, string>> {
|
export async function cmdRemove(storageRoot: string, name: string): Promise<Result<void, string>> {
|
||||||
const nameOk = validateCliWorkflowName(name);
|
const nameOk = validateCliWorkflowName(name);
|
||||||
+2
-2
@@ -10,8 +10,8 @@ import {
|
|||||||
writeWorkflowRegistry,
|
writeWorkflowRegistry,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
import { pathExists } from "./fs-utils.js";
|
import { pathExists } from "../../fs-utils.js";
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||||
|
|
||||||
export async function cmdRollback(
|
export async function cmdRollback(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow";
|
||||||
import { stringify } from "yaml";
|
import { stringify } from "yaml";
|
||||||
|
|
||||||
import { validateCliWorkflowName } from "./workflow-name.js";
|
import { validateCliWorkflowName } from "../../workflow-name.js";
|
||||||
|
|
||||||
export async function cmdShow(
|
export async function cmdShow(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||||
|
|
||||||
|
export type ParsedAddArgv = {
|
||||||
|
name: string;
|
||||||
|
filePath: string;
|
||||||
|
/** Override path to `.d.ts` when adding a bundle. */
|
||||||
|
typesPath: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CmdAddSuccess = {
|
||||||
|
hash: string;
|
||||||
|
warnings: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowDispatchDeps = {
|
||||||
|
dispatchGroup: DispatchGroupFn;
|
||||||
|
printDeprecation: (oldCmd: string, newCmd: string) => void;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getCommandRegistry } from "./cli-dispatch.js";
|
import { getCommandRegistry } from "./cli-registry.js";
|
||||||
|
|
||||||
type SkillTopic = {
|
type SkillTopic = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -126,13 +126,13 @@ uncaged-workflow thread list
|
|||||||
|
|
||||||
## CAS (Content-Addressable Storage)
|
## CAS (Content-Addressable Storage)
|
||||||
|
|
||||||
Store and retrieve content by hash, scoped to the current thread.
|
Store and retrieve content by hash in workflow storage (global CAS directory).
|
||||||
|
|
||||||
| Operation | Command |
|
| Operation | Command |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| **Store** | \`uncaged-workflow cas put <THREAD_ID> '<content>'\` → prints hash |
|
| **Store** | \`uncaged-workflow cas put '<content>'\` → prints hash |
|
||||||
| **Read** | \`uncaged-workflow cas get <THREAD_ID> <HASH>\` → prints content |
|
| **Read** | \`uncaged-workflow cas get <HASH>\` → prints content |
|
||||||
| **List** | \`uncaged-workflow cas list <THREAD_ID>\` |
|
| **List** | \`uncaged-workflow cas list\` |
|
||||||
|
|
||||||
CAS is the **only** supported way to persist structured data (phase plans, review notes, etc.) within a thread. Do not use temp files.
|
CAS is the **only** supported way to persist structured data (phase plans, review notes, etc.) within a thread. Do not use temp files.
|
||||||
|
|
||||||
@@ -3,6 +3,23 @@ import { join } from "node:path";
|
|||||||
|
|
||||||
import { pathExists, readTextFileIfExists } from "./fs-utils.js";
|
import { pathExists, readTextFileIfExists } from "./fs-utils.js";
|
||||||
|
|
||||||
|
function parseFirstJsonLineObject(text: string): Record<string, unknown> | null {
|
||||||
|
const firstLine = text.split("\n")[0];
|
||||||
|
if (firstLine === undefined || firstLine.trim() === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(firstLine) as unknown;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (parsed === null || typeof parsed !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
export type RunningThreadRow = {
|
export type RunningThreadRow = {
|
||||||
threadId: string;
|
threadId: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
@@ -20,20 +37,11 @@ async function readThreadStartTimestampMs(dataPath: string): Promise<number | nu
|
|||||||
if (text === null) {
|
if (text === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const firstLine = text.split("\n")[0];
|
const parsed = parseFirstJsonLineObject(text);
|
||||||
if (firstLine === undefined || firstLine.trim() === "") {
|
if (parsed === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let parsed: unknown;
|
const ts = parsed.timestamp;
|
||||||
try {
|
|
||||||
parsed = JSON.parse(firstLine) as unknown;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (parsed === null || typeof parsed !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const ts = (parsed as Record<string, unknown>).timestamp;
|
|
||||||
return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
|
return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,20 +50,11 @@ async function readWorkflowNameFromDataJsonl(dataPath: string): Promise<string |
|
|||||||
if (text === null) {
|
if (text === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const firstLine = text.split("\n")[0];
|
const parsed = parseFirstJsonLineObject(text);
|
||||||
if (firstLine === undefined || firstLine.trim() === "") {
|
if (parsed === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let parsed: unknown;
|
const name = parsed.name;
|
||||||
try {
|
|
||||||
parsed = JSON.parse(firstLine) as unknown;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (parsed === null || typeof parsed !== "object") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const name = (parsed as Record<string, unknown>).name;
|
|
||||||
return typeof name === "string" ? name : null;
|
return typeof name === "string" ? name : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,30 @@ export async function sendWorkerTcpCommand(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function readWorkerCtl(
|
||||||
|
storageRoot: string,
|
||||||
|
hash: string,
|
||||||
|
): Promise<Result<WorkerCtl, string>> {
|
||||||
|
const ctlPath = join(storageRoot, "workers", `${hash}.json`);
|
||||||
|
const ctlText = await readTextFileIfExists(ctlPath);
|
||||||
|
if (ctlText === null) {
|
||||||
|
return err(`worker control file missing for bundle hash ${hash}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctl: WorkerCtl;
|
||||||
|
try {
|
||||||
|
ctl = JSON.parse(ctlText) as WorkerCtl;
|
||||||
|
} catch {
|
||||||
|
return err(`corrupt worker control file: ${ctlPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof ctl.port !== "number" || ctl.port <= 0) {
|
||||||
|
return err(`invalid worker control file: ${ctlPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(ctl);
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveRunningHashForThread(
|
export async function resolveRunningHashForThread(
|
||||||
storageRoot: string,
|
storageRoot: string,
|
||||||
threadId: string,
|
threadId: string,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# @uncaged/workflow-agent-cursor
|
||||||
|
|
||||||
|
`AgentFn` adapter that runs the `cursor-agent` CLI against a workspace path derived from the thread.
|
||||||
|
|
||||||
|
The agent builds a full prompt (system + task + step history via `@uncaged/workflow-util-agent`), extracts the absolute workspace path with your `extract` + Zod schema, then spawns `cursor-agent` with `--workspace`, model, and non-interactive flags.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/workflow-agent-cursor @uncaged/workflow @uncaged/workflow-util-agent zod
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo: `"@uncaged/workflow-agent-cursor": "workspace:*"` plus `workspace:*` for `@uncaged/workflow` and `@uncaged/workflow-util-agent`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createCursorAgent } from "@uncaged/workflow-agent-cursor";
|
||||||
|
|
||||||
|
const agent = createCursorAgent({
|
||||||
|
model: null, // null → "auto"
|
||||||
|
timeout: 0, // ms; 0 = no limit (spawnCli timeout disabled)
|
||||||
|
extract: myExtractFn,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
| Export | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `createCursorAgent(config)` | Returns `AgentFn` that runs `cursor-agent` with `buildAgentPrompt(ctx)` |
|
||||||
|
| `CursorAgentConfig` | `model`, `timeout`, `extract` (must supply workspace path) |
|
||||||
|
| `validateCursorAgentConfig` | Config validation result |
|
||||||
|
| `buildAgentPrompt` | Re-exported from `@uncaged/workflow-util-agent` |
|
||||||
|
|
||||||
|
Requires `cursor-agent` on `PATH` at runtime.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# @uncaged/workflow-agent-hermes
|
||||||
|
|
||||||
|
`AgentFn` adapter that runs the `hermes` CLI in non-interactive `chat` mode (Nerve-style flags: `-q`, `--yolo`, `--quiet`, bounded `--max-turns`).
|
||||||
|
|
||||||
|
The agent composes the same thread-aware prompt as other CLI-backed agents via `buildAgentPrompt` from `@uncaged/workflow-util-agent`, then spawns `hermes` and returns stdout on success.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/workflow-agent-hermes @uncaged/workflow @uncaged/workflow-util-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo: use `workspace:*` for all three `@uncaged/*` packages.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createHermesAgent } from "@uncaged/workflow-agent-hermes";
|
||||||
|
|
||||||
|
const agent = createHermesAgent({
|
||||||
|
model: "your-model", // or null to omit --model
|
||||||
|
timeout: 600_000, // ms, or null for no timeout
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
| Export | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `createHermesAgent(config)` | Returns `AgentFn` wrapping `hermes chat -q ...` |
|
||||||
|
| `HermesAgentConfig` | `model`, `timeout` |
|
||||||
|
| `validateHermesAgentConfig` | Config validation result |
|
||||||
|
| `buildAgentPrompt` | Re-exported from `@uncaged/workflow-util-agent` |
|
||||||
|
|
||||||
|
Requires `hermes` on `PATH` at runtime.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# @uncaged/workflow-agent-llm
|
||||||
|
|
||||||
|
`AgentFn` adapter that calls an OpenAI-compatible `POST /chat/completions` endpoint using `@uncaged/workflow`’s `LlmProvider` (base URL, API key, model).
|
||||||
|
|
||||||
|
Single-turn: system text is the current role’s `systemPrompt`, user text is the thread’s initial prompt (`ctx.start.content`). Errors from HTTP, JSON, or empty choices are thrown as `Error` with a JSON payload string.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/workflow-agent-llm @uncaged/workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo: `"@uncaged/workflow-agent-llm": "workspace:*"`, `"@uncaged/workflow": "workspace:*"`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createLlmAdapter } from "@uncaged/workflow-agent-llm";
|
||||||
|
|
||||||
|
const agent = createLlmAdapter({
|
||||||
|
baseUrl: "https://api.openai.com/v1",
|
||||||
|
apiKey: process.env.OPENAI_API_KEY!,
|
||||||
|
model: "gpt-4.1-mini",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
| Export | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `createLlmAdapter(provider)` | `LlmProvider` → `AgentFn` |
|
||||||
|
| `chatCompletionText({ provider, messages })` | Low-level `Result<string, LlmChatError>` helper |
|
||||||
|
| `LlmMessage` | `{ role: "system" \| "user" \| "assistant"; content: string }` |
|
||||||
|
| `LlmChatError` | Discriminated error kinds for failed completions |
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# @uncaged/workflow-template-develop
|
||||||
|
|
||||||
|
Reference **develop** workflow template: plan phases, implement in a loop, review, test, then commit.
|
||||||
|
|
||||||
|
Export a `WorkflowDefinition` and `createDevelopRun` so a host can bind agents/LLM and run the same graph the bundled `.esm.js` would use. Use `buildDevelopDescriptor()` when assembling `descriptor` metadata for a bundle.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/workflow-template-develop @uncaged/workflow zod
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo: `workspace:*` for `@uncaged/workflow-template-develop` and `@uncaged/workflow`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createDevelopRun, developWorkflowDefinition } from "@uncaged/workflow-template-develop";
|
||||||
|
|
||||||
|
const run = createDevelopRun(binding, extract, llmProvider);
|
||||||
|
// run(...) executes the develop moderator graph with your AgentBinding
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
| Role | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| **planner** | Break work into ordered phases (hashes) |
|
||||||
|
| **coder** | Implement current phase; repeats until phases complete or limits hit |
|
||||||
|
| **reviewer** | Code review gate (`approved` vs send back to coder) |
|
||||||
|
| **tester** | Verify via tests/build/lint (`passed` vs send back to coder) |
|
||||||
|
| **committer** | Final commit step |
|
||||||
|
|
||||||
|
Also exported: role factories/meta schemas (`plannerRole`, `coderRole`, …), `DevelopMeta`, `developRoles`.
|
||||||
|
|
||||||
|
## Moderator flow
|
||||||
|
|
||||||
|
1. **Start** → `planner`
|
||||||
|
2. After **planner** → `coder`
|
||||||
|
3. After **coder** → if all planned phases are done (or last phase completed) → `reviewer`; else `coder` again, until `maxRounds` then `END`
|
||||||
|
4. After **reviewer** → if approved → `tester`; else `coder` (or `END` if out of rounds)
|
||||||
|
5. After **tester** → if passed → `committer`; else `coder` (or `END` if out of rounds)
|
||||||
|
6. After **committer** → `END`
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
| Export | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `createDevelopRun` | `createWorkflow(developWorkflowDefinition, …)` factory |
|
||||||
|
| `developWorkflowDefinition` | `description`, `roles`, `developModerator` |
|
||||||
|
| `developModerator` | `Moderator<DevelopMeta>` |
|
||||||
|
| `buildDevelopDescriptor` | `buildDescriptor({ … })` for bundle metadata |
|
||||||
|
| `DEVELOP_WORKFLOW_DESCRIPTION` | Human-readable one-liner |
|
||||||
@@ -12,8 +12,27 @@ export const reviewerMetaSchema = z.discriminatedUnion("status", [
|
|||||||
]);
|
]);
|
||||||
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
||||||
|
|
||||||
const REVIEWER_SYSTEM = `You are a code reviewer. Review the current git diff. Give a clear approve or reject verdict.
|
const REVIEWER_SYSTEM = `You are a code reviewer. Review the git diff for correctness, consistency, and adherence to project conventions.
|
||||||
Only reject for blocking issues. End with your verdict.`;
|
|
||||||
|
## Review process
|
||||||
|
|
||||||
|
1. Read the **preparer**'s output in the thread for project conventions (coding style, naming, commit format, etc.).
|
||||||
|
2. Review the diff against these conventions.
|
||||||
|
3. For documentation changes, verify that names, paths, and references match the actual codebase.
|
||||||
|
|
||||||
|
## Review checklist
|
||||||
|
|
||||||
|
- **Correctness** — does the code do what it claims? Logic bugs, off-by-one, missing returns?
|
||||||
|
- **Conventions** — naming, imports, code style per project rules?
|
||||||
|
- **Consistency** — do docs/comments match actual code? Are references current and accurate?
|
||||||
|
- **Edge cases** — missing error handling, null checks, boundary conditions?
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
- **Approve** only if there are zero issues
|
||||||
|
- **Reject** with specific issues that must be fixed — every issue you find is blocking
|
||||||
|
|
||||||
|
Be thorough. A false approve costs more than a false reject.`;
|
||||||
|
|
||||||
export const reviewerRole: RoleDefinition<ReviewerMeta> = {
|
export const reviewerRole: RoleDefinition<ReviewerMeta> = {
|
||||||
description: "Runs git diff checks and sets approved when the change is ready.",
|
description: "Runs git diff checks and sets approved when the change is ready.",
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# @uncaged/workflow-template-solve-issue
|
||||||
|
|
||||||
|
Reference **solve-issue** workflow template: prepare a repo, delegate implementation to the **develop** workflow, then submit (e.g. open a PR).
|
||||||
|
|
||||||
|
`createSolveIssueRun` wires the `developer` role to `workflowAsAgent("develop")` by default; `binding.overrides.developer` wins if you pass one (for tests or custom hosts).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/workflow-template-solve-issue @uncaged/workflow zod
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo: `workspace:*` for this package and `@uncaged/workflow`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createSolveIssueRun, solveIssueWorkflowDefinition } from "@uncaged/workflow-template-solve-issue";
|
||||||
|
|
||||||
|
const run = createSolveIssueRun(binding, extract, llmProvider);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
| Role | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| **preparer** | Set up context / repo state for the issue |
|
||||||
|
| **developer** | Implementation; default runs the registered `develop` workflow as a sub-agent |
|
||||||
|
| **submitter** | Finalize and submit the outcome (e.g. PR) |
|
||||||
|
|
||||||
|
Also exported: `preparerRole`, `developerRole`, `submitterRole` and their Zod meta schemas, `SolveIssueMeta`, `solveIssueRoles`.
|
||||||
|
|
||||||
|
## Moderator flow
|
||||||
|
|
||||||
|
1. **Start** → `preparer`
|
||||||
|
2. After **preparer** → `developer`
|
||||||
|
3. After **developer** → `submitter`
|
||||||
|
4. After **submitter** → `END`
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
| Export | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `createSolveIssueRun` | Merges `developer` override with `workflowAsAgent("develop")`, then `createWorkflow` |
|
||||||
|
| `solveIssueWorkflowDefinition` | `description`, `roles`, `solveIssueModerator` |
|
||||||
|
| `solveIssueModerator` | Linear `Moderator<SolveIssueMeta>` |
|
||||||
|
| `buildSolveIssueDescriptor` | Descriptor helper for bundles |
|
||||||
|
| `SOLVE_ISSUE_WORKFLOW_DESCRIPTION` | Human-readable one-liner |
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# @uncaged/workflow-util-agent
|
||||||
|
|
||||||
|
Shared helpers for CLI-backed workflow agents: assemble prompts from thread context and spawn subprocesses with timeouts.
|
||||||
|
|
||||||
|
Used by `@uncaged/workflow-agent-cursor` and `@uncaged/workflow-agent-hermes`. Depends on `@uncaged/workflow` for CAS reads (`getContentMerklePayload`) and `Result` typing.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/workflow-util-agent @uncaged/workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo: `workspace:*` for both packages.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { buildAgentPrompt, spawnCli } from "@uncaged/workflow-util-agent";
|
||||||
|
|
||||||
|
const prompt = await buildAgentPrompt(agentContext);
|
||||||
|
const result = await spawnCli("my-cli", ["--json"], { cwd: "/tmp", timeoutMs: 60_000 });
|
||||||
|
if (!result.ok) { /* handle SpawnCliError */ }
|
||||||
|
const stdout = result.value;
|
||||||
|
```
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
| Export | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `buildAgentPrompt(ctx)` | System prompt + task + prior step summaries + latest body from CAS; appends `uncaged-workflow thread <id>` tool hint |
|
||||||
|
| `spawnCli(cmd, args, { cwd, timeoutMs })` | `Promise<Result<string, SpawnCliError>>`; captures stdout, non-zero exit and spawn failures as `err` |
|
||||||
|
| `SpawnCliConfig` | `cwd: string \| null`, `timeoutMs: number \| null` |
|
||||||
|
| `SpawnCliError` | `non_zero_exit` \| `timeout` \| `spawn_failed` |
|
||||||
|
| `SpawnCliResult` | Alias for `Result<string, SpawnCliError>` |
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# @uncaged/workflow
|
||||||
|
|
||||||
|
Core workflow engine: registry, CAS, thread execution, bundle validation, and role/workflow types.
|
||||||
|
|
||||||
|
This package implements the three-phase engine loop that runs single-file ESM workflow bundles (each exports `run` and `descriptor`). It persists threads under `~/.uncaged/workflow/` by default and hashes bundles with XXH64 (Crockford Base32). See the repo root [README](../../README.md) for workflow, bundle, thread, role, and registry concepts.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/workflow zod
|
||||||
|
```
|
||||||
|
|
||||||
|
In this monorepo, depend with `"@uncaged/workflow": "workspace:*"`. `zod` is a peer dependency (used by bundle/shape validation at the integration boundary).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createWorkflow, readWorkflowRegistry, executeThread } from "@uncaged/workflow";
|
||||||
|
// Wire a WorkflowDefinition + AgentBinding + extract + optional LlmProvider into createWorkflow,
|
||||||
|
// then run the returned WorkflowFn inside your host (or use executeThread for disk-backed runs).
|
||||||
|
```
|
||||||
|
|
||||||
|
## API overview
|
||||||
|
|
||||||
|
| Area | Exports (representative) |
|
||||||
|
|------|--------------------------|
|
||||||
|
| **Types** | `WorkflowDefinition`, `WorkflowFn`, `AgentFn`, `AgentBinding`, `Moderator`, `RoleDefinition`, `ThreadContext`, `LlmProvider`, `Result` shape via `ok` / `err`, `START` / `END` |
|
||||||
|
| **Bundle** | `buildDescriptor`, `extractBundleExports`, `validateWorkflowBundle`, `validateWorkflowDescriptor`, `WorkflowDescriptor`, `WorkflowRoleDescriptor` |
|
||||||
|
| **Registry** | `readWorkflowRegistry`, `writeWorkflowRegistry`, `registerWorkflowVersion`, `workflowRegistryPath`, YAML helpers |
|
||||||
|
| **CAS** | `createCasStore`, `createThreadCas`, Merkle helpers (`putStepMerkleNode`, `getContentMerklePayload`, …), `hashWorkflowBundleBytes` |
|
||||||
|
| **Engine** | `createWorkflow`, `executeThread`, `parseThreadDataJsonl`, fork helpers, `garbageCollectCas` |
|
||||||
|
| **Extract / LLM tools** | `llmExtract`, `reactExtract`, `createExtract`, `getExtractProvider` |
|
||||||
|
| **Agent bridge** | `workflowAsAgent` — expose a registered workflow as an agent-backed role |
|
||||||
|
| **Utilities** | `createLogger`, ULID / Crockford Base32 codecs, `getDefaultWorkflowStorageRoot`, paths |
|
||||||
|
|
||||||
|
Full surface is re-exported from `src/index.ts`.
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
decodeCrockfordToUint64,
|
decodeCrockfordToUint64,
|
||||||
encodeCrockfordBase32Bits,
|
encodeCrockfordBase32Bits,
|
||||||
encodeUint64AsCrockford,
|
encodeUint64AsCrockford,
|
||||||
} from "../src/base32.js";
|
} from "../src/util/base32.js";
|
||||||
|
|
||||||
describe("Crockford Base32", () => {
|
describe("Crockford Base32", () => {
|
||||||
test("roundtrip 64-bit hash encoding", () => {
|
test("roundtrip 64-bit hash encoding", () => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import { buildDescriptor } from "../src/build-descriptor.js";
|
import { buildDescriptor } from "../src/bundle/build-descriptor.js";
|
||||||
|
import { validateWorkflowDescriptor } from "../src/bundle/workflow-descriptor.js";
|
||||||
import { END } from "../src/types.js";
|
import { END } from "../src/types.js";
|
||||||
import { validateWorkflowDescriptor } from "../src/workflow-descriptor.js";
|
|
||||||
|
|
||||||
describe("buildDescriptor", () => {
|
describe("buildDescriptor", () => {
|
||||||
test("produces a descriptor that validates and includes JSON schemas per role", () => {
|
test("produces a descriptor that validates and includes JSON schemas per role", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { validateWorkflowBundle } from "../src/bundle-validator.js";
|
import { validateWorkflowBundle } from "../src/bundle/bundle-validator.js";
|
||||||
|
|
||||||
const minimalDescriptor = `export const descriptor = { description: "x", roles: {} };
|
const minimalDescriptor = `export const descriptor = { description: "x", roles: {} };
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { createCasStore, createThreadCas } from "../src/cas.js";
|
import { createCasStore, createThreadCas } from "../src/cas/cas.js";
|
||||||
import { hashString } from "../src/hash.js";
|
import { hashString } from "../src/cas/hash.js";
|
||||||
|
|
||||||
describe("cas module exports", () => {
|
describe("cas module exports", () => {
|
||||||
test("createThreadCas is a deprecated alias of createCasStore", () => {
|
test("createThreadCas is a deprecated alias of createCasStore", () => {
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import { createCasStore } from "../src/cas.js";
|
import { createCasStore } from "../src/cas/cas.js";
|
||||||
import { createWorkflow } from "../src/create-workflow.js";
|
|
||||||
import { executeThread } from "../src/engine.js";
|
|
||||||
import { createExtract } from "../src/extract-fn.js";
|
|
||||||
import { createLogger } from "../src/logger.js";
|
|
||||||
import {
|
import {
|
||||||
createContentMerkleNode,
|
createContentMerkleNode,
|
||||||
getContentMerklePayload,
|
getContentMerklePayload,
|
||||||
parseMerkleNode,
|
parseMerkleNode,
|
||||||
serializeMerkleNode,
|
serializeMerkleNode,
|
||||||
} from "../src/merkle.js";
|
} from "../src/cas/merkle.js";
|
||||||
|
import { createWorkflow } from "../src/engine/create-workflow.js";
|
||||||
|
import { executeThread } from "../src/engine/engine.js";
|
||||||
|
import { createExtract } from "../src/extract/extract-fn.js";
|
||||||
import { END, type LlmProvider } from "../src/types.js";
|
import { END, type LlmProvider } from "../src/types.js";
|
||||||
|
import { createLogger } from "../src/util/logger.js";
|
||||||
|
|
||||||
const plannerMetaSchema = z.object({
|
const plannerMetaSchema = z.object({
|
||||||
plan: z.string(),
|
plan: z.string(),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
buildForkPlan,
|
buildForkPlan,
|
||||||
parseThreadDataJsonl,
|
parseThreadDataJsonl,
|
||||||
selectForkHistoricalSteps,
|
selectForkHistoricalSteps,
|
||||||
} from "../src/fork-thread.js";
|
} from "../src/engine/fork-thread.js";
|
||||||
|
|
||||||
const sampleDataJsonl = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"maxRounds":5}},"timestamp":100}
|
const sampleDataJsonl = `{"name":"demo","hash":"C9NMV6V2TQT81","threadId":"01AAA1111111111111111111","parameters":{"prompt":"hi","options":{"maxRounds":5}},"timestamp":100}
|
||||||
{"role":"planner","contentHash":"HP0000000000000000000001","meta":{},"refs":[],"timestamp":101}
|
{"role":"planner","contentHash":"HP0000000000000000000001","meta":{},"refs":[],"timestamp":101}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { hashWorkflowBundleBytes } from "../src/cas/hash.js";
|
||||||
import { decodeCrockfordToUint64 } from "../src/base32.js";
|
import { decodeCrockfordToUint64 } from "../src/util/base32.js";
|
||||||
import { hashWorkflowBundleBytes } from "../src/hash.js";
|
|
||||||
|
|
||||||
describe("hashWorkflowBundleBytes", () => {
|
describe("hashWorkflowBundleBytes", () => {
|
||||||
test("matches XXH64 reference for empty input", () => {
|
test("matches XXH64 reference for empty input", () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { createLogger } from "../src/logger.js";
|
import { createLogger } from "../src/util/logger.js";
|
||||||
|
|
||||||
describe("createLogger", () => {
|
describe("createLogger", () => {
|
||||||
test("writes JSONL records to a file sink", async () => {
|
test("writes JSONL records to a file sink", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { createContentMerkleNode, parseMerkleNode, serializeMerkleNode } from "../src/merkle.js";
|
import {
|
||||||
|
createContentMerkleNode,
|
||||||
|
parseMerkleNode,
|
||||||
|
serializeMerkleNode,
|
||||||
|
} from "../src/cas/merkle.js";
|
||||||
|
|
||||||
describe("merkle", () => {
|
describe("merkle", () => {
|
||||||
test("content node roundtrips through YAML", () => {
|
test("content node roundtrips through YAML", () => {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import { createCasStore } from "../src/cas.js";
|
import { createCasStore } from "../src/cas/cas.js";
|
||||||
import { createContentMerkleNode, serializeMerkleNode } from "../src/merkle.js";
|
import { createContentMerkleNode, serializeMerkleNode } from "../src/cas/merkle.js";
|
||||||
import { reactExtract } from "../src/react-extract.js";
|
import { reactExtract } from "../src/extract/react-extract.js";
|
||||||
import type { LlmProvider } from "../src/types.js";
|
import type { LlmProvider } from "../src/types.js";
|
||||||
|
|
||||||
const metaSchema = z.object({ seen: z.string() });
|
const metaSchema = z.object({ seen: z.string() });
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import { createCasStore } from "../src/cas.js";
|
import { createCasStore } from "../src/cas/cas.js";
|
||||||
import { createWorkflow } from "../src/create-workflow.js";
|
import { createWorkflow } from "../src/engine/create-workflow.js";
|
||||||
import { executeThread } from "../src/engine.js";
|
import { executeThread } from "../src/engine/engine.js";
|
||||||
import { createExtract } from "../src/extract-fn.js";
|
import { buildForkPlan, parseThreadDataJsonl } from "../src/engine/fork-thread.js";
|
||||||
import { buildForkPlan, parseThreadDataJsonl } from "../src/fork-thread.js";
|
import { createExtract } from "../src/extract/extract-fn.js";
|
||||||
import { createLogger } from "../src/logger.js";
|
|
||||||
import { END } from "../src/types.js";
|
import { END } from "../src/types.js";
|
||||||
|
import { createLogger } from "../src/util/logger.js";
|
||||||
|
|
||||||
const phaseSchema = z.object({
|
const phaseSchema = z.object({
|
||||||
hash: z.string(),
|
hash: z.string(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
rollbackWorkflowToHistoryHash,
|
rollbackWorkflowToHistoryHash,
|
||||||
unregisterWorkflow,
|
unregisterWorkflow,
|
||||||
writeWorkflowRegistry,
|
writeWorkflowRegistry,
|
||||||
} from "../src/registry.js";
|
} from "../src/registry/registry.js";
|
||||||
|
|
||||||
describe("workflow registry", () => {
|
describe("workflow registry", () => {
|
||||||
test("roundtrips through workflow.yaml", async () => {
|
test("roundtrips through workflow.yaml", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { err, ok } from "../src/result.js";
|
import { err, ok } from "../src/util/result.js";
|
||||||
|
|
||||||
describe("result helpers", () => {
|
describe("result helpers", () => {
|
||||||
test("ok wraps value", () => {
|
test("ok wraps value", () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "../src/storage-root.js";
|
import { getDefaultWorkflowStorageRoot, getGlobalCasDir } from "../src/util/storage-root.js";
|
||||||
|
|
||||||
describe("getGlobalCasDir", () => {
|
describe("getGlobalCasDir", () => {
|
||||||
test("joins cas segment under explicit storage root", () => {
|
test("joins cas segment under explicit storage root", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { createThreadPauseGate } from "../src/thread-pause-gate.js";
|
import { createThreadPauseGate } from "../src/engine/thread-pause-gate.js";
|
||||||
|
|
||||||
describe("createThreadPauseGate", () => {
|
describe("createThreadPauseGate", () => {
|
||||||
test("pause blocks awaitAfterYield until resume", async () => {
|
test("pause blocks awaitAfterYield until resume", async () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { decodeCrockfordBase32Bits } from "../src/base32.js";
|
import { decodeCrockfordBase32Bits } from "../src/util/base32.js";
|
||||||
import { generateUlid } from "../src/ulid.js";
|
import { generateUlid } from "../src/util/ulid.js";
|
||||||
|
|
||||||
describe("generateUlid", () => {
|
describe("generateUlid", () => {
|
||||||
test("length and decodable Crockford payload", () => {
|
test("length and decodable Crockford payload", () => {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { createConnection } from "node:net";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { createCasStore } from "../src/cas.js";
|
import { createCasStore } from "../src/cas/cas.js";
|
||||||
import { createContentMerkleNode, serializeMerkleNode } from "../src/merkle.js";
|
import { createContentMerkleNode, serializeMerkleNode } from "../src/cas/merkle.js";
|
||||||
import { getWorkerHostScriptPath } from "../src/worker-entry-path.js";
|
import { getWorkerHostScriptPath } from "../src/engine/worker-entry-path.js";
|
||||||
|
|
||||||
const bundleSource = `import { putContentMerkleNode } from "@uncaged/workflow";
|
const bundleSource = `import { putContentMerkleNode } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import { createCasStore } from "../src/cas.js";
|
import { createCasStore } from "../src/cas/cas.js";
|
||||||
import { createWorkflow } from "../src/create-workflow.js";
|
import { hashWorkflowBundleBytes } from "../src/cas/hash.js";
|
||||||
import { executeThread } from "../src/engine.js";
|
import { getContentMerklePayload, parseMerkleNode } from "../src/cas/merkle.js";
|
||||||
import { createExtract } from "../src/extract-fn.js";
|
import { createWorkflow } from "../src/engine/create-workflow.js";
|
||||||
import { hashWorkflowBundleBytes } from "../src/hash.js";
|
import { executeThread } from "../src/engine/engine.js";
|
||||||
import { createLogger } from "../src/logger.js";
|
import { createExtract } from "../src/extract/extract-fn.js";
|
||||||
import { getContentMerklePayload, parseMerkleNode } from "../src/merkle.js";
|
|
||||||
import {
|
import {
|
||||||
readWorkflowRegistry,
|
readWorkflowRegistry,
|
||||||
registerWorkflowVersion,
|
registerWorkflowVersion,
|
||||||
writeWorkflowRegistry,
|
writeWorkflowRegistry,
|
||||||
} from "../src/registry.js";
|
} from "../src/registry/registry.js";
|
||||||
import { END } from "../src/types.js";
|
import { END } from "../src/types.js";
|
||||||
|
import { createLogger } from "../src/util/logger.js";
|
||||||
import { workflowAsAgent } from "../src/workflow-as-agent.js";
|
import { workflowAsAgent } from "../src/workflow-as-agent.js";
|
||||||
|
|
||||||
const callerMetaSchema = z.object({ done: z.literal(true) });
|
const callerMetaSchema = z.object({ done: z.literal(true) });
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { createCasStore } from "../src/cas.js";
|
import { createCasStore } from "../src/cas/cas.js";
|
||||||
import { hashWorkflowBundleBytes } from "../src/hash.js";
|
import { hashWorkflowBundleBytes } from "../src/cas/hash.js";
|
||||||
import { parseMerkleNode } from "../src/merkle.js";
|
import { parseMerkleNode } from "../src/cas/merkle.js";
|
||||||
import {
|
import {
|
||||||
readWorkflowRegistry,
|
readWorkflowRegistry,
|
||||||
registerWorkflowVersion,
|
registerWorkflowVersion,
|
||||||
writeWorkflowRegistry,
|
writeWorkflowRegistry,
|
||||||
} from "../src/registry.js";
|
} from "../src/registry/registry.js";
|
||||||
import { type AgentContext, START } from "../src/types.js";
|
import { type AgentContext, START } from "../src/types.js";
|
||||||
import { workflowAsAgent } from "../src/workflow-as-agent.js";
|
import { workflowAsAgent } from "../src/workflow-as-agent.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { validateWorkflowDescriptor } from "../src/workflow-descriptor.js";
|
import { validateWorkflowDescriptor } from "../src/bundle/workflow-descriptor.js";
|
||||||
|
|
||||||
describe("validateWorkflowDescriptor", () => {
|
describe("validateWorkflowDescriptor", () => {
|
||||||
// 1. Valid minimal descriptor
|
// 1. Valid minimal descriptor
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
import type { RoleMeta, WorkflowDefinition } from "./types.js";
|
import type { RoleMeta, WorkflowDefinition } from "../types.js";
|
||||||
import type { WorkflowDescriptor, WorkflowRoleSchema } from "./workflow-descriptor.js";
|
import type { WorkflowDescriptor, WorkflowRoleSchema } from "./types.js";
|
||||||
|
|
||||||
function stripJsonSchemaMeta(json: Record<string, unknown>): WorkflowRoleSchema {
|
function stripJsonSchemaMeta(json: Record<string, unknown>): WorkflowRoleSchema {
|
||||||
const { $schema: _drop, ...rest } = json;
|
const { $schema: _drop, ...rest } = json;
|
||||||
+5
-11
@@ -10,6 +10,11 @@ import type {
|
|||||||
Program,
|
Program,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
} from "acorn";
|
} from "acorn";
|
||||||
|
import * as acorn from "acorn";
|
||||||
|
|
||||||
|
import { err, ok, type Result } from "../util/index.js";
|
||||||
|
|
||||||
|
import type { WorkflowBundleValidationInput } from "./types.js";
|
||||||
|
|
||||||
/** Acorn Node with index-access for property traversal. */
|
/** Acorn Node with index-access for property traversal. */
|
||||||
type AcornNode = Node & { [key: string]: unknown };
|
type AcornNode = Node & { [key: string]: unknown };
|
||||||
@@ -22,17 +27,6 @@ function narrowNode<T extends Node>(node: Node): T {
|
|||||||
return node as unknown as T;
|
return node as unknown as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
import * as acorn from "acorn";
|
|
||||||
|
|
||||||
import { err, ok, type Result } from "./result.js";
|
|
||||||
|
|
||||||
export type WorkflowBundleValidationInput = {
|
|
||||||
/** Absolute or relative path (used for `.esm.js` suffix checks). */
|
|
||||||
filePath: string;
|
|
||||||
/** UTF-8 source of the bundle. */
|
|
||||||
source: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function endsWithEsmJs(path: string): boolean {
|
function endsWithEsmJs(path: string): boolean {
|
||||||
return path.endsWith(".esm.js");
|
return path.endsWith(".esm.js");
|
||||||
}
|
}
|
||||||
+2
-2
@@ -2,9 +2,9 @@ import { mkdir, readlink, symlink, unlink } from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
/** This module lives in `@uncaged/workflow/src`; parent dir is the package root. */
|
/** This module lives in `@uncaged/workflow/src/bundle`; grandparent dir is the package root. */
|
||||||
function installedWorkflowPackageDir(): string {
|
function installedWorkflowPackageDir(): string {
|
||||||
return fileURLToPath(new URL("..", import.meta.url));
|
return fileURLToPath(new URL("../..", import.meta.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+3
-13
@@ -1,20 +1,10 @@
|
|||||||
|
import type { WorkflowFn } from "../types.js";
|
||||||
|
import { err, ok, type Result } from "../util/index.js";
|
||||||
import { importWorkflowBundleModule } from "./bundle-import-env.js";
|
import { importWorkflowBundleModule } from "./bundle-import-env.js";
|
||||||
import { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js";
|
import { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js";
|
||||||
import { err, ok, type Result } from "./result.js";
|
import type { ExtractBundleExportsOptions, ExtractedBundleExports } from "./types.js";
|
||||||
import type { WorkflowFn } from "./types.js";
|
|
||||||
import type { WorkflowDescriptor } from "./workflow-descriptor.js";
|
|
||||||
import { validateWorkflowDescriptor } from "./workflow-descriptor.js";
|
import { validateWorkflowDescriptor } from "./workflow-descriptor.js";
|
||||||
|
|
||||||
export type ExtractedBundleExports = {
|
|
||||||
run: WorkflowFn;
|
|
||||||
descriptor: WorkflowDescriptor;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ExtractBundleExportsOptions = {
|
|
||||||
/** When set, ensures `node_modules/@uncaged/workflow` exists under this root before import. */
|
|
||||||
storageRoot: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Load a workflow `.esm.js` bundle and read its named exports (`run`, `descriptor`). */
|
/** Load a workflow `.esm.js` bundle and read its named exports (`run`, `descriptor`). */
|
||||||
export async function extractBundleExports(
|
export async function extractBundleExports(
|
||||||
bundlePath: string,
|
bundlePath: string,
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { stringify } from "yaml";
|
import { stringify } from "yaml";
|
||||||
|
|
||||||
import type { WorkflowDescriptor } from "./workflow-descriptor.js";
|
import type { WorkflowDescriptor } from "./types.js";
|
||||||
|
|
||||||
/** Serialize a validated workflow descriptor to YAML for storage next to the bundle. */
|
/** Serialize a validated workflow descriptor to YAML for storage next to the bundle. */
|
||||||
export function stringifyWorkflowDescriptor(descriptor: WorkflowDescriptor): string {
|
export function stringifyWorkflowDescriptor(descriptor: WorkflowDescriptor): string {
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export { buildDescriptor } from "./build-descriptor.js";
|
||||||
|
export { importWorkflowBundleModule } from "./bundle-import-env.js";
|
||||||
|
export { validateWorkflowBundle } from "./bundle-validator.js";
|
||||||
|
export { ensureUncagedWorkflowSymlink } from "./ensure-uncaged-workflow-symlink.js";
|
||||||
|
export { extractBundleExports } from "./extract-bundle-exports.js";
|
||||||
|
export { stringifyWorkflowDescriptor } from "./generate-descriptor.js";
|
||||||
|
export type {
|
||||||
|
ExtractBundleExportsOptions,
|
||||||
|
ExtractedBundleExports,
|
||||||
|
WorkflowBundleValidationInput,
|
||||||
|
WorkflowDescriptor,
|
||||||
|
WorkflowRoleDescriptor,
|
||||||
|
WorkflowRoleSchema,
|
||||||
|
} from "./types.js";
|
||||||
|
export { validateWorkflowDescriptor } from "./workflow-descriptor.js";
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { WorkflowFn } from "../types.js";
|
||||||
|
|
||||||
|
/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */
|
||||||
|
export type WorkflowRoleSchema = Record<string, unknown>;
|
||||||
|
|
||||||
|
export type WorkflowRoleDescriptor = {
|
||||||
|
description: string;
|
||||||
|
schema: WorkflowRoleSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */
|
||||||
|
export type WorkflowDescriptor = {
|
||||||
|
description: string;
|
||||||
|
roles: Record<string, WorkflowRoleDescriptor>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowBundleValidationInput = {
|
||||||
|
/** Absolute or relative path (used for `.esm.js` suffix checks). */
|
||||||
|
filePath: string;
|
||||||
|
/** UTF-8 source of the bundle. */
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtractedBundleExports = {
|
||||||
|
run: WorkflowFn;
|
||||||
|
descriptor: WorkflowDescriptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtractBundleExportsOptions = {
|
||||||
|
/** When set, ensures `node_modules/@uncaged/workflow` exists under this root before import. */
|
||||||
|
storageRoot: string | null;
|
||||||
|
};
|
||||||
+2
-14
@@ -1,18 +1,6 @@
|
|||||||
import { err, ok, type Result } from "./result.js";
|
import { err, ok, type Result } from "../util/index.js";
|
||||||
|
|
||||||
/** JSON Schema fragment describing one role's `meta` shape (subset supported by code generation). */
|
import type { WorkflowDescriptor, WorkflowRoleDescriptor, WorkflowRoleSchema } from "./types.js";
|
||||||
export type WorkflowRoleSchema = Record<string, unknown>;
|
|
||||||
|
|
||||||
export type WorkflowRoleDescriptor = {
|
|
||||||
description: string;
|
|
||||||
schema: WorkflowRoleSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Workflow metadata exported as `export const descriptor` from `.esm.js` bundles. */
|
|
||||||
export type WorkflowDescriptor = {
|
|
||||||
description: string;
|
|
||||||
roles: Record<string, WorkflowRoleDescriptor>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> {
|
export function validateWorkflowDescriptor(value: unknown): Result<WorkflowDescriptor, string> {
|
||||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||||
@@ -2,13 +2,7 @@ import { mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/pro
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { hashString } from "./hash.js";
|
import { hashString } from "./hash.js";
|
||||||
|
import type { CasStore } from "./types.js";
|
||||||
export type CasStore = {
|
|
||||||
put(content: string): Promise<string>;
|
|
||||||
get(hash: string): Promise<string | null>;
|
|
||||||
delete(hash: string): Promise<void>;
|
|
||||||
list(): Promise<string[]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createCasStore(casDir: string): CasStore {
|
export function createCasStore(casDir: string): CasStore {
|
||||||
async function ensureDir(): Promise<void> {
|
async function ensureDir(): Promise<void> {
|
||||||
@@ -2,7 +2,7 @@ import { Buffer } from "node:buffer";
|
|||||||
|
|
||||||
import XXH from "xxhashjs";
|
import XXH from "xxhashjs";
|
||||||
|
|
||||||
import { encodeUint64AsCrockford } from "./base32.js";
|
import { encodeUint64AsCrockford } from "../util/index.js";
|
||||||
|
|
||||||
function digestToUint64(digest: { toString(radix?: number): string }): bigint {
|
function digestToUint64(digest: { toString(radix?: number): string }): bigint {
|
||||||
const hex = digest.toString(16).padStart(16, "0");
|
const hex = digest.toString(16).padStart(16, "0");
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user