diff --git a/legacy-packages/cli-workflow/CHANGELOG.md b/legacy-packages/cli-workflow/CHANGELOG.md deleted file mode 100644 index 4cd2056..0000000 --- a/legacy-packages/cli-workflow/CHANGELOG.md +++ /dev/null @@ -1,138 +0,0 @@ -# @uncaged/cli-workflow - -## 0.5.0-alpha.4 - -### Patch Changes - -- Updated dependencies -- Updated dependencies [f74b482] -- Updated dependencies [f74b482] - - @uncaged/workflow-util@0.5.0-alpha.4 - - @uncaged/workflow-protocol@0.5.0-alpha.4 - - @uncaged/workflow-cas@0.5.0-alpha.4 - - @uncaged/workflow-execute@0.5.0-alpha.4 - - @uncaged/workflow-gateway@0.5.0-alpha.4 - - @uncaged/workflow-register@0.5.0-alpha.4 - - @uncaged/workflow-runtime@0.5.0-alpha.4 - -## 0.5.0-alpha.3 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.3 - - @uncaged/workflow-cas@0.5.0-alpha.3 - - @uncaged/workflow-execute@0.5.0-alpha.3 - - @uncaged/workflow-gateway@0.5.0-alpha.3 - - @uncaged/workflow-register@0.5.0-alpha.3 - - @uncaged/workflow-runtime@0.5.0-alpha.3 - - @uncaged/workflow-util@0.5.0-alpha.3 - -## 0.5.0-alpha.2 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.2 - - @uncaged/workflow-cas@0.5.0-alpha.2 - - @uncaged/workflow-execute@0.5.0-alpha.2 - - @uncaged/workflow-gateway@0.5.0-alpha.2 - - @uncaged/workflow-register@0.5.0-alpha.2 - - @uncaged/workflow-runtime@0.5.0-alpha.2 - - @uncaged/workflow-util@0.5.0-alpha.2 - -## 0.5.0-alpha.1 - -### Patch Changes - -- @uncaged/workflow-cas@0.5.0-alpha.1 -- @uncaged/workflow-execute@0.5.0-alpha.1 -- @uncaged/workflow-gateway@0.5.0-alpha.1 -- @uncaged/workflow-protocol@0.5.0-alpha.1 -- @uncaged/workflow-register@0.5.0-alpha.1 -- @uncaged/workflow-runtime@0.5.0-alpha.1 -- @uncaged/workflow-util@0.5.0-alpha.1 - -## 0.5.0-alpha.0 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.0 - - @uncaged/workflow-cas@0.5.0-alpha.0 - - @uncaged/workflow-execute@0.5.0-alpha.0 - - @uncaged/workflow-register@0.5.0-alpha.0 - - @uncaged/workflow-runtime@0.5.0-alpha.0 - - @uncaged/workflow-util@0.5.0-alpha.0 - - @uncaged/workflow-gateway@0.5.0-alpha.0 - -## 0.4.5 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.5 - - @uncaged/workflow-cas@0.4.5 - - @uncaged/workflow-execute@0.4.5 - - @uncaged/workflow-gateway@0.4.5 - - @uncaged/workflow-register@0.4.5 - - @uncaged/workflow-runtime@0.4.5 - - @uncaged/workflow-util@0.4.5 - -## 0.4.4 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.4 - - @uncaged/workflow-cas@0.4.4 - - @uncaged/workflow-execute@0.4.4 - - @uncaged/workflow-gateway@0.4.4 - - @uncaged/workflow-register@0.4.4 - - @uncaged/workflow-runtime@0.4.4 - - @uncaged/workflow-util@0.4.4 - -## 0.4.3 - -### Patch Changes - -- Include src/ in published packages so bun runtime can resolve the 'bun' exports condition. -- Updated dependencies - - @uncaged/workflow-cas@0.4.3 - - @uncaged/workflow-execute@0.4.3 - - @uncaged/workflow-gateway@0.4.3 - - @uncaged/workflow-protocol@0.4.3 - - @uncaged/workflow-register@0.4.3 - - @uncaged/workflow-runtime@0.4.3 - - @uncaged/workflow-util@0.4.3 - -## 0.4.2 - -### Patch Changes - -- Fix workspace dependency resolution: use workspace:^ so published packages resolve to compatible versions instead of exact (non-existent) versions. -- Updated dependencies - - @uncaged/workflow-cas@0.4.2 - - @uncaged/workflow-execute@0.4.2 - - @uncaged/workflow-gateway@0.4.2 - - @uncaged/workflow-protocol@0.4.2 - - @uncaged/workflow-register@0.4.2 - - @uncaged/workflow-runtime@0.4.2 - - @uncaged/workflow-util@0.4.2 - -## 0.4.0 - -### Minor Changes - -- Fix package exports for published packages and adopt changesets for version management. - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-cas@0.4.0 - - @uncaged/workflow-execute@0.4.0 - - @uncaged/workflow-gateway@0.4.0 - - @uncaged/workflow-protocol@0.4.0 - - @uncaged/workflow-register@0.4.0 - - @uncaged/workflow-runtime@0.4.0 - - @uncaged/workflow-util@0.4.0 diff --git a/legacy-packages/cli-workflow/README.md b/legacy-packages/cli-workflow/README.md deleted file mode 100644 index c07eb43..0000000 --- a/legacy-packages/cli-workflow/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# @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 --prompt "Your task" -uncaged-workflow thread show -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 [--types ] Register a workflow bundle in the registry - workflow list List all registered workflows - workflow show Show details of a registered workflow - workflow rm Remove a workflow from the registry - workflow history Show version history of a workflow - workflow rollback [hash] Rollback a workflow to a previous version - -Thread execution: - thread run [--prompt ] [--max-rounds N] Start a new thread executing a workflow - thread list [name] List threads, optionally filtered by workflow name - thread show Show thread details and state - thread rm Remove a thread - thread fork [--from-role ] Fork a thread, optionally from a specific role - thread ps List running threads - thread kill Kill a running thread - thread live | --latest [--debug] [--role ] Attach to a thread and stream output live - thread pause Pause a running thread - thread resume Resume a paused thread - -Content-addressable storage: - cas get Retrieve content by hash from CAS - cas put Store content in CAS, prints hash - cas list List all hashes in CAS - cas rm Remove a CAS entry by hash - cas gc Garbage-collect unreferenced CAS entries - -Development: - init workspace Initialize a new workflow workspace - init template Initialize a new workflow template - -Shortcuts: - run [...] → thread run - live [...] → thread live - -Reference: - skill [topic] Agent-consumable docs (cli, develop, author) - -Use --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`. diff --git a/legacy-packages/cli-workflow/__tests__/bundle-fixture.ts b/legacy-packages/cli-workflow/__tests__/bundle-fixture.ts deleted file mode 100644 index e7168be..0000000 --- a/legacy-packages/cli-workflow/__tests__/bundle-fixture.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ParsedAddArgv } from "../src/commands/workflow/index.js"; - -export function addCliArgs(name: string, filePath: string): ParsedAddArgv { - return { name, filePath, typesPath: null }; -} diff --git a/legacy-packages/cli-workflow/__tests__/commands.test.ts b/legacy-packages/cli-workflow/__tests__/commands.test.ts deleted file mode 100644 index 23d9a83..0000000 --- a/legacy-packages/cli-workflow/__tests__/commands.test.ts +++ /dev/null @@ -1,482 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow-cas"; -import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; -import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "../src/commands/cas/index.js"; -import { - cmdAdd, - cmdHistory, - cmdList, - cmdRemove, - cmdRollback, - cmdShow, - formatListLines, -} from "../src/commands/workflow/index.js"; -import { addCliArgs } from "./bundle-fixture.js"; - -const fixtureDescriptor = `export const descriptor = { description: "fixture", roles: {}, graph: { edges: [] } }; -`; - -function casStoredForm(raw: string): string { - return serializeMerkleNode(createContentMerkleNode(raw)); -} - -describe("cli workflow commands", () => { - let prevEnv: string | undefined; - let storageRoot: string; - - beforeEach(async () => { - prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - storageRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-")); - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot; - }); - - afterEach(async () => { - if (prevEnv === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = prevEnv; - } - await rm(storageRoot, { recursive: true, force: true }); - }); - - test("add / list / show / remove roundtrip", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile( - bundlePath, - `${fixtureDescriptor}import fs from "node:fs"; - -export const run = async function* (input, options) { - fs.existsSync("."); - const cas = options.cas; - const h = await cas.put(input.prompt); - yield { role: "noop", contentHash: h, meta: { done: true }, refs: [h] }; - return { returnCode: 0, summary: "done" }; -} -`, - "utf8", - ); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - - const listed = await cmdList(storageRoot); - expect(listed.ok).toBe(true); - if (listed.ok) { - const lines = formatListLines(listed.value); - expect(lines.some((l) => l.startsWith("solve-issue\t"))).toBe(true); - } - - const shown = await cmdShow(storageRoot, "solve-issue"); - expect(shown.ok).toBe(true); - if (!shown.ok) { - return; - } - expect(shown.value.hash.length).toBe(13); - - const bundleOnDisk = await readFile( - join(storageRoot, "bundles", `${shown.value.hash}.esm.js`), - "utf8", - ); - expect(bundleOnDisk.length).toBeGreaterThan(0); - - const removed = await cmdRemove(storageRoot, "solve-issue"); - expect(removed.ok).toBe(true); - - const listedAfter = await cmdList(storageRoot); - expect(listedAfter.ok).toBe(true); - if (listedAfter.ok) { - expect(formatListLines(listedAfter.value)[0]).toBe("(no workflows registered)"); - } - }); - - test("add rejects invalid bundles", async () => { - const bundlePath = join(storageRoot, "bad.esm.js"); - await writeFile( - bundlePath, - `${fixtureDescriptor}import x from "./local"; -export const run = async function* (input) { return { returnCode: 0, summary: input.prompt }; } -`, - "utf8", - ); - const r = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(r.ok).toBe(false); - }); - - test("add rejects .ts sources", async () => { - const tsPath = join(storageRoot, "solo.ts"); - await writeFile(tsPath, "export const x = 1;\n", "utf8"); - const r = await cmdAdd(storageRoot, addCliArgs("solo", tsPath)); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("build your .ts file first"); - } - }); - - test("add rejects bundle without descriptor export", async () => { - const bundlePath = join(storageRoot, "solo.esm.js"); - await writeFile( - bundlePath, - `export const run = async function* () { - yield { role: "x", contentHash: "STUBHASH00000000000000001", meta: {}, refs: [] }; - return { returnCode: 0, summary: "ok" }; -} -`, - "utf8", - ); - const r = await cmdAdd(storageRoot, addCliArgs("solo", bundlePath)); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("descriptor"); - } - }); - - test("add from .esm.js writes yaml from descriptor export", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "hello.esm.js"); - await writeFile( - bundlePath, - `export const descriptor = { - description: "hello world fixture", - roles: { - greeter: { - description: "greet", - schema: { type: "object", properties: { greeting: { type: "string" } } }, - }, - }, - graph: { edges: [] }, -}; -export const run = async function* (input, options) { - const cas = options.cas; - const h = await cas.put( input.prompt); - yield { role: "greeter", contentHash: h, meta: { greeting: "hi" }, refs: [h] }; - return { returnCode: 0, summary: "ok" }; -}; -`, - "utf8", - ); - const added = await cmdAdd(storageRoot, addCliArgs("hello", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - const { hash } = added.value; - const bundles = join(storageRoot, "bundles"); - const esm = await readFile(join(bundles, `${hash}.esm.js`), "utf8"); - expect(esm.length).toBeGreaterThan(100); - const yaml = await readFile(join(bundles, `${hash}.yaml`), "utf8"); - expect(yaml).toContain("hello world fixture"); - expect(yaml).toContain("greeter"); - - const reg = await readWorkflowRegistry(storageRoot); - expect(reg.ok).toBe(true); - if (!reg.ok) { - return; - } - const entry = getRegisteredWorkflow(reg.value, "hello"); - expect(entry).not.toBeNull(); - if (entry === null) { - return; - } - expect(entry.hash).toBe(hash); - }); - - test("add from .esm.js copies optional sidecar .d.ts", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile( - bundlePath, - `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "x"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "x" }; -} -`, - "utf8", - ); - await writeFile( - join(bundleDir, "demo.d.ts"), - "export type DemoHint = { hint: string };\n", - "utf8", - ); - - const added = await cmdAdd(storageRoot, addCliArgs("typed-demo", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - const dts = await readFile(join(storageRoot, "bundles", `${added.value.hash}.d.ts`), "utf8"); - expect(dts).toContain("DemoHint"); - }); - - test("add from .esm.js with --types uses explicit d.ts path", async () => { - const bundleDir = join(storageRoot, "w"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "app.esm.js"); - const dtsPath = join(bundleDir, "types.d.ts"); - await writeFile( - bundlePath, - `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "x"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "x" }; -} -`, - "utf8", - ); - await writeFile(dtsPath, "export type App = 1;\n", "utf8"); - - const added = await cmdAdd(storageRoot, { - name: "app", - filePath: bundlePath, - typesPath: dtsPath, - }); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - const dtsStored = await readFile( - join(storageRoot, "bundles", `${added.value.hash}.d.ts`), - "utf8", - ); - expect(dtsStored).toContain("App"); - }); - - test("add from .esm.js warns when optional .d.ts is missing", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile( - bundlePath, - `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "x"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "x" }; -} -`, - "utf8", - ); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - expect(added.value.warnings.length).toBe(1); - expect(added.value.warnings[0]).toContain("demo.d.ts"); - }); - - test("history lists current + prior versions sorted by time descending", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - const v1 = `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "v1"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "v1" }; -} -`; - const v2 = `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "v2"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "v2" }; -} -`; - await writeFile(bundlePath, v1, "utf8"); - const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add1.ok).toBe(true); - await new Promise((r) => setTimeout(r, 15)); - await writeFile(bundlePath, v2, "utf8"); - const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add2.ok).toBe(true); - - const hist = await cmdHistory(storageRoot, "solve-issue"); - expect(hist.ok).toBe(true); - if (!hist.ok) { - return; - } - expect(hist.value.length).toBe(2); - const dates = hist.value.map((line) => { - const parts = line.split("\t"); - return Date.parse(parts[1] ?? ""); - }); - expect(Number.isFinite(dates[0])).toBe(true); - expect(Number.isFinite(dates[1])).toBe(true); - expect(dates[0] >= dates[1]).toBe(true); - expect(hist.value.some((l) => l.endsWith("(current)"))).toBe(true); - }); - - test("rollback swaps registry head with a history hash", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - const v1 = `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "v1"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "v1" }; -} -`; - const v2 = `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "v2"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "v2" }; -} -`; - await writeFile(bundlePath, v1, "utf8"); - const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add1.ok).toBe(true); - if (!add1.ok) { - return; - } - const hash1 = add1.value.hash; - await writeFile(bundlePath, v2, "utf8"); - const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add2.ok).toBe(true); - if (!add2.ok) { - return; - } - const hash2 = add2.value.hash; - - const rb = await cmdRollback(storageRoot, "solve-issue", null); - expect(rb.ok).toBe(true); - - const reg = await readWorkflowRegistry(storageRoot); - expect(reg.ok).toBe(true); - if (!reg.ok) { - return; - } - const entry = getRegisteredWorkflow(reg.value, "solve-issue"); - expect(entry).not.toBeNull(); - if (entry === null) { - return; - } - expect(entry.hash).toBe(hash1); - expect(entry.history.some((h) => h.hash === hash2)).toBe(true); - }); - - test("rollback rejects a hash that is not in history", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile( - bundlePath, - `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "x"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "x" }; -} -`, - "utf8", - ); - const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add1.ok).toBe(true); - await writeFile( - bundlePath, - `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "y"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "y" }; -} -`, - "utf8", - ); - const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add2.ok).toBe(true); - - const bad = await cmdRollback(storageRoot, "solve-issue", "0000000000000"); - expect(bad.ok).toBe(false); - }); - - test("cas put/get/list/rm use global cas dir (thread id not required for storage)", async () => { - const raw = "phase doc"; - const stored = casStoredForm(raw); - const put = await cmdCasPut(storageRoot, raw); - expect(put.ok).toBe(true); - if (!put.ok) { - return; - } - const hash = put.value; - const blobPath = join(getGlobalCasDir(storageRoot), `${hash}.txt`); - expect(await readFile(blobPath, "utf8")).toBe(stored); - - const got = await cmdCasGet(storageRoot, hash); - expect(got.ok).toBe(true); - if (!got.ok) { - return; - } - expect(got.value).toBe(stored); - - const listed = await cmdCasList(storageRoot); - expect(listed.ok).toBe(true); - if (!listed.ok) { - return; - } - expect(listed.value).toContain(hash); - - const removed = await cmdCasRm(storageRoot, hash); - expect(removed.ok).toBe(true); - - const missing = await cmdCasGet(storageRoot, hash); - expect(missing.ok).toBe(false); - }); - - test("rollback rejects missing bundle file for target hash", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile( - bundlePath, - `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "x"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "x" }; -} -`, - "utf8", - ); - const add1 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add1.ok).toBe(true); - if (!add1.ok) { - return; - } - const hash1 = add1.value.hash; - await writeFile( - bundlePath, - `${fixtureDescriptor}export const run = async function* (_input, options) { - const cas = options.cas; - const h = await cas.put( "y"); - yield { role: "a", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "y" }; -} -`, - "utf8", - ); - const add2 = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(add2.ok).toBe(true); - if (!add2.ok) { - return; - } - - await unlink(join(storageRoot, "bundles", `${hash1}.esm.js`)); - - const rb = await cmdRollback(storageRoot, "solve-issue", hash1); - expect(rb.ok).toBe(false); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/connect.test.ts b/legacy-packages/cli-workflow/__tests__/connect.test.ts deleted file mode 100644 index 3559051..0000000 --- a/legacy-packages/cli-workflow/__tests__/connect.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow-cas"; - -import { createApp } from "../src/commands/connect/app.js"; - -function casStoredForm(raw: string): string { - return serializeMerkleNode(createContentMerkleNode(raw)); -} - -function buildApp(storageRoot: string) { - const app = createApp(storageRoot, null); - return { - fetch: (path: string, init?: RequestInit) => - app.fetch(new Request(`http://localhost${path}`, init)), - }; -} - -describe("serve /healthz", () => { - test("returns ok", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/healthz"); - expect(res.status).toBe(200); - const body = (await res.json()) as { ok: boolean }; - expect(body.ok).toBe(true); - }); -}); - -describe("serve /api/workflows", () => { - test("returns empty list for missing storage", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/workflows"); - // Registry file won't exist, should return error - expect(res.status).toBe(200); - }); -}); - -describe("serve /api/threads", () => { - test("returns empty list for missing storage", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/threads"); - expect(res.status).toBe(200); - const body = (await res.json()) as { threads: unknown[] }; - expect(body.threads).toEqual([]); - }); - - test("returns 404 for missing thread", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/threads/nonexistent-id"); - expect(res.status).toBe(404); - }); -}); - -describe("serve /api/threads/running", () => { - test("returns empty list for missing storage", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/threads/running"); - expect(res.status).toBe(200); - const body = (await res.json()) as { threads: unknown[] }; - expect(body.threads).toEqual([]); - }); -}); - -describe("serve /api/cas", () => { - test("returns empty list for missing storage", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/cas"); - expect(res.status).toBe(200); - const body = (await res.json()) as { hashes: unknown[] }; - expect(body.hashes).toEqual([]); - }); - - test("returns 404 for missing hash", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/cas/nonexistent-hash"); - expect(res.status).toBe(404); - }); -}); - -describe("serve error handling", () => { - test("POST /api/threads with invalid JSON body → 400", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/threads", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: "not json", - }); - expect(res.status).toBe(400); - const body = (await res.json()) as { error: string }; - expect(body.error).toBe("invalid JSON body"); - }); - - test("POST /api/cas with invalid JSON body → 400", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/cas", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: "not json", - }); - expect(res.status).toBe(400); - const body = (await res.json()) as { error: string }; - expect(body.error).toBe("invalid JSON body"); - }); - - test("POST /api/threads with missing required fields → 400", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const res = await fetch("/api/threads", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ foo: "bar" }), - }); - expect(res.status).toBe(400); - const body = (await res.json()) as { error: string }; - expect(body.error).toContain("required"); - }); - - test("global error handler returns 500 with JSON", async () => { - const app = createApp("/tmp/uncaged-serve-test-nonexistent", null); - app.get("/test-error", () => { - throw new Error("boom"); - }); - const res = await app.fetch(new Request("http://localhost/test-error")); - expect(res.status).toBe(500); - const body = (await res.json()) as { error: string }; - expect(body.error).toBe("Internal server error"); - }); -}); - -describe("serve security", () => { - test("CORS headers present on responses", async () => { - const app = createApp("/tmp/uncaged-serve-test-nonexistent", null); - const res2 = await app.fetch( - new Request("http://localhost/healthz", { - headers: { Origin: "http://localhost:5173" }, - }), - ); - expect(res2.headers.get("Access-Control-Allow-Origin")).toBe("http://localhost:5173"); - }); - - test("POST with body > 1MB → 413", async () => { - const { fetch } = buildApp("/tmp/uncaged-serve-test-nonexistent"); - const largeBody = "x".repeat(1_048_577); - const res = await fetch("/api/cas", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": String(largeBody.length), - }, - body: largeBody, - }); - expect(res.status).toBe(413); - const body = (await res.json()) as { error: string }; - expect(body.error).toBe("Payload too large"); - }); -}); - -describe("serve CAS round-trip", () => { - const tmpDir = `/tmp/uncaged-serve-cas-test-${Date.now()}`; - - test("put then get", async () => { - const { fetch } = buildApp(tmpDir); - - const putRes = await fetch("/api/cas", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content: "hello world" }), - }); - expect(putRes.status).toBe(201); - const putBody = (await putRes.json()) as { hash: string }; - expect(typeof putBody.hash).toBe("string"); - - const getRes = await fetch(`/api/cas/${putBody.hash}`); - expect(getRes.status).toBe(200); - const getBody = (await getRes.json()) as { content: string }; - expect(getBody.content).toBe(casStoredForm("hello world")); - - // cleanup - const delRes = await fetch(`/api/cas/${putBody.hash}`, { method: "DELETE" }); - expect(delRes.status).toBe(200); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVECMPLT01DDDDDDDDDDDDG.data.jsonl b/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVECMPLT01DDDDDDDDDDDDG.data.jsonl deleted file mode 100644 index 96fc47d..0000000 --- a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVECMPLT01DDDDDDDDDDDDG.data.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"name":"demo-live","hash":"C9NMV6V2TQT81","threadId":"01LIVECMPLT01DDDDDDDDDDDDG","parameters":{"prompt":"hello","options":{"maxRounds":5,"depth":0}},"timestamp":1714963400000} -{"role":"planner","contentHash":"FF7YQ5W3S2EV6","meta":{"phase":"plan","flags":[1,2]},"refs":[],"timestamp":1714963201000} -{"role":"coder","contentHash":"EN34XX1W4WAFJ","meta":{},"refs":[],"timestamp":1714963202000} -{"returnCode":0,"summary":"fixture completed"} diff --git a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVECMPLT01DDDDDDDDDDDDG.info.jsonl b/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVECMPLT01DDDDDDDDDDDDG.info.jsonl deleted file mode 100644 index d9e2d29..0000000 --- a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVECMPLT01DDDDDDDDDDDDG.info.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"tag":"DEBUGTAG1","content":"bundle loaded","timestamp":1714963400050} -{"tag":"DEBUGTAG2","content":"multi\nline","timestamp":1714963400500} diff --git a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVEINFLY01DDDDDDDDDDDDG.data.jsonl b/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVEINFLY01DDDDDDDDDDDDG.data.jsonl deleted file mode 100644 index fb9cdec..0000000 --- a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVEINFLY01DDDDDDDDDDDDG.data.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"name":"demo-live","hash":"C9NMV6V2TQT81","threadId":"01LIVEINFLY01DDDDDDDDDDDDG","parameters":{"prompt":"hello","options":{"maxRounds":5,"depth":0}},"timestamp":1714963200000} -{"role":"planner","contentHash":"P6M9FHE1GSBN0","meta":{"x":1},"refs":[],"timestamp":1714963201000} diff --git a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVEOLDER01DDDDDDDDDDDDG.data.jsonl b/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVEOLDER01DDDDDDDDDDDDG.data.jsonl deleted file mode 100644 index 63ec7df..0000000 --- a/legacy-packages/cli-workflow/__tests__/fixtures/live/logs/C9NMV6V2TQT81/01LIVEOLDER01DDDDDDDDDDDDG.data.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"name":"demo-live-old","hash":"C9NMV6V2TQT81","threadId":"01LIVEOLDER01DDDDDDDDDDDDG","parameters":{"prompt":"old","options":{"maxRounds":5,"depth":0}},"timestamp":1714963000000} -{"returnCode":0,"summary":"older thread"} diff --git a/legacy-packages/cli-workflow/__tests__/fork-cli.test.ts b/legacy-packages/cli-workflow/__tests__/fork-cli.test.ts deleted file mode 100644 index 0005309..0000000 --- a/legacy-packages/cli-workflow/__tests__/fork-cli.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { createCasStore, getContentMerklePayload } from "@uncaged/workflow-cas"; -import { FORK_BRANCH_ROLE, walkStateFramesNewestFirst } from "@uncaged/workflow-execute"; -import { END } from "@uncaged/workflow-runtime"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -import { cmdFork, cmdRun } from "../src/commands/thread/index.js"; -import { cmdAdd } from "../src/commands/workflow/index.js"; -import { pathExists } from "../src/fs-utils.js"; -import { resolveThreadRecord } from "../src/thread-scan.js"; -import { addCliArgs } from "./bundle-fixture.js"; -import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js"; - -/** Three-role workflow that respects `input.steps` for fork/resume. */ -const threeRoleBundleSource = `export const descriptor = { - description: "fork-cli", - roles: { - planner: { description: "planner", schema: {} }, - coder: { description: "coder", schema: {} }, - reviewer: { description: "reviewer", schema: {} }, - }, - graph: { edges: [] }, -}; -export const run = async function* (input, options) { - const cas = options.cas; - const has = (r) => input.steps.some((s) => s.role === r); - if (!has("planner")) { - const h = await cas.put( "p1"); - yield { role: "planner", contentHash: h, meta: { k: "planner" }, refs: [h] }; - } - if (!has("coder")) { - const h = await cas.put( "c1"); - yield { role: "coder", contentHash: h, meta: { k: "coder" }, refs: [h] }; - } - if (!has("reviewer")) { - const body = "rev-" + String(input.steps.length); - const h = await cas.put( body); - yield { role: "reviewer", contentHash: h, meta: { k: "reviewer" }, refs: [h] }; - } - return { returnCode: 0, summary: "done" }; -}; -`; - -async function waitUntilRunningAbsent(runningPath: string): Promise { - for (let attempt = 0; attempt < 120; attempt++) { - if (!(await pathExists(runningPath))) { - return; - } - await new Promise((r) => setTimeout(r, 25)); - } -} - -async function waitUntilThreadCompletes(storageRoot: string, threadId: string): Promise { - for (let attempt = 0; attempt < 120; attempt++) { - const row = await resolveThreadRecord(storageRoot, threadId); - if (row?.source === "history") { - return; - } - await new Promise((r) => setTimeout(r, 25)); - } -} - -async function listMeaningfulRoleContents( - storageRoot: string, - threadId: string, -): Promise> { - const row = await resolveThreadRecord(storageRoot, threadId); - if (row === null) { - return []; - } - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const frames = await walkStateFramesNewestFirst(cas, row.head); - const chronological = [...frames].reverse(); - const out: Array<{ role: string; content: string }> = []; - for (const fr of chronological) { - if (fr.payload.role === END || fr.payload.role === FORK_BRANCH_ROLE) { - continue; - } - const content = await getContentMerklePayload(cas, fr.payload.content); - out.push({ - role: fr.payload.role, - content: content ?? "", - }); - } - return out; -} - -describe("cli fork", () => { - let prevEnv: string | undefined; - let storageRoot: string; - - beforeEach(async () => { - prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - storageRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-fork-")); - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot; - await ensureTestWorkflowRegistryConfig(storageRoot); - }); - - afterEach(async () => { - if (prevEnv === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = prevEnv; - } - await rm(storageRoot, { recursive: true, force: true }); - }); - - test("fork --from-role planner continues with coder then reviewer", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, threeRoleBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - const hash = added.value.hash; - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - const sourceId = ran.value.threadId; - const sourceRunning = join(storageRoot, "logs", hash, `${sourceId}.running`); - await waitUntilRunningAbsent(sourceRunning); - await waitUntilThreadCompletes(storageRoot, sourceId); - - const histBefore = await resolveThreadRecord(storageRoot, sourceId); - expect(histBefore?.source).toBe("history"); - - const forked = await cmdFork(storageRoot, sourceId, "planner"); - expect(forked.ok).toBe(true); - if (!forked.ok) { - return; - } - const newId = forked.value.threadId; - const newRunning = join(storageRoot, "logs", hash, `${newId}.running`); - await waitUntilRunningAbsent(newRunning); - await waitUntilThreadCompletes(storageRoot, newId); - - const forkHist = await resolveThreadRecord(storageRoot, newId); - expect(forkHist?.source).toBe("history"); - expect(forkHist?.start).toBe(histBefore?.start); - - const steps = await listMeaningfulRoleContents(storageRoot, newId); - const tail = steps[steps.length - 1]; - expect(tail?.role).toBe("reviewer"); - expect(tail?.content).toBe("rev-1"); - }); - - test("fork without --from-role retries last role", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, threeRoleBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - const hash = added.value.hash; - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - const sourceId = ran.value.threadId; - await waitUntilRunningAbsent(join(storageRoot, "logs", hash, `${sourceId}.running`)); - await waitUntilThreadCompletes(storageRoot, sourceId); - - const forked = await cmdFork(storageRoot, sourceId, null); - expect(forked.ok).toBe(true); - if (!forked.ok) { - return; - } - const newId = forked.value.threadId; - await waitUntilRunningAbsent(join(storageRoot, "logs", hash, `${newId}.running`)); - await waitUntilThreadCompletes(storageRoot, newId); - - const steps = await listMeaningfulRoleContents(storageRoot, newId); - expect(steps.length).toBeGreaterThanOrEqual(3); - const coderReplay = steps[steps.length - 2]; - expect(coderReplay?.role).toBe("coder"); - expect(coderReplay?.content).toBe("c1"); - const tail = steps[steps.length - 1]; - expect(tail?.role).toBe("reviewer"); - expect(tail?.content).toBe("rev-2"); - }); - - test("fork rejects unknown role with available names", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, threeRoleBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - const sourceId = ran.value.threadId; - await waitUntilRunningAbsent( - join(storageRoot, "logs", added.value.hash, `${sourceId}.running`), - ); - await waitUntilThreadCompletes(storageRoot, sourceId); - - const bad = await cmdFork(storageRoot, sourceId, "ghost-role"); - expect(bad.ok).toBe(false); - if (bad.ok) { - return; - } - expect(bad.error).toContain("ghost-role"); - expect(bad.error).toContain("planner"); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/gc-cli.test.ts b/legacy-packages/cli-workflow/__tests__/gc-cli.test.ts deleted file mode 100644 index 6b72502..0000000 --- a/legacy-packages/cli-workflow/__tests__/gc-cli.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { createCasStore, putStartNode } from "@uncaged/workflow-cas"; -import { garbageCollectCas, getBundleDir, upsertThreadEntry } from "@uncaged/workflow-execute"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; -import { cmdThreadRemove } from "../src/commands/thread/index.js"; -import { pathExists } from "../src/fs-utils.js"; - -const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); - -describe("gc cli and garbageCollectCas", () => { - let prevEnv: string | undefined; - let storageRoot: string; - - beforeEach(async () => { - prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - storageRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-gc-")); - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot; - }); - - afterEach(async () => { - if (prevEnv === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = prevEnv; - } - await rm(storageRoot, { recursive: true, force: true }); - }); - - test("garbageCollectCas keeps CAS entries reachable from threads.json roots", async () => { - const bundleHash = "C9NMV6V2TQT81"; - const threadId = "01AAA1111111111111111111"; - const bundleDir = getBundleDir(storageRoot, bundleHash); - await mkdir(bundleDir, { recursive: true }); - - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const orphanHash = await cas.put("orphan-blob"); - const promptHash = await cas.put("prompt-text"); - const startHash = await putStartNode( - cas, - { - name: "demo", - hash: bundleHash, - depth: 0, - parentState: null, - }, - promptHash, - ); - - await upsertThreadEntry(bundleDir, threadId, { - head: startHash, - start: startHash, - updatedAt: 100, - }); - - const gc = await garbageCollectCas(storageRoot); - expect(gc.ok).toBe(true); - if (!gc.ok) { - return; - } - expect(gc.value.scannedThreads).toBe(2); - expect(gc.value.deletedEntries).toBe(1); - expect(gc.value.deletedHashes).toEqual([orphanHash]); - - expect(await pathExists(join(getGlobalCasDir(storageRoot), `${promptHash}.txt`))).toBe(true); - expect(await pathExists(join(getGlobalCasDir(storageRoot), `${startHash}.txt`))).toBe(true); - expect(await pathExists(join(getGlobalCasDir(storageRoot), `${orphanHash}.txt`))).toBe(false); - }); - - test("garbageCollectCas deletes orphaned CAS when no threads reference them", async () => { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const orphanHash = await cas.put("lonely"); - - const gc = await garbageCollectCas(storageRoot); - expect(gc.ok).toBe(true); - if (!gc.ok) { - return; - } - expect(gc.value.scannedThreads).toBe(0); - expect(gc.value.activeRefs).toBe(0); - expect(gc.value.deletedEntries).toBe(1); - expect(gc.value.deletedHashes).toEqual([orphanHash]); - expect(await pathExists(join(getGlobalCasDir(storageRoot), `${orphanHash}.txt`))).toBe(false); - }); - - test("cli gc prints stats", async () => { - const bundleHash = "C9NMV6V2TQT81"; - const threadId = "01BBB2222222222222222222"; - const bundleDir = getBundleDir(storageRoot, bundleHash); - await mkdir(bundleDir, { recursive: true }); - - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const promptHash = await cas.put("prompt-text"); - const startHash = await putStartNode( - cas, - { - name: "demo", - hash: bundleHash, - depth: 0, - parentState: null, - }, - promptHash, - ); - await cas.put("drop-me"); - - await upsertThreadEntry(bundleDir, threadId, { - head: startHash, - start: startHash, - updatedAt: 100, - }); - - const env = { ...process.env, UNCAGED_WORKFLOW_STORAGE_ROOT: storageRoot }; - const proc = spawnSync(process.execPath, [cliEntryPath, "cas", "gc"], { - env, - encoding: "utf8", - }); - expect(proc.status).toBe(0); - expect(String(proc.stdout).trim()).toBe("scanned 2 threads, 2 active refs, deleted 1 entries"); - }); - - test("thread rm triggers gc so unreferenced CAS is removed", async () => { - const bundleHash = "C9NMV6V2TQT81"; - const threadId = "01CCC3333333333333333333"; - const bundleDir = getBundleDir(storageRoot, bundleHash); - await mkdir(bundleDir, { recursive: true }); - - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const promptHash = await cas.put("prompt-text"); - const startHash = await putStartNode( - cas, - { - name: "demo", - hash: bundleHash, - depth: 0, - parentState: null, - }, - promptHash, - ); - - await upsertThreadEntry(bundleDir, threadId, { - head: startHash, - start: startHash, - updatedAt: 100, - }); - - const orphanHash = await cas.put("orphan-after-rm"); - const orphanPath = join(getGlobalCasDir(storageRoot), `${orphanHash}.txt`); - - const removed = await cmdThreadRemove(storageRoot, threadId); - expect(removed.ok).toBe(true); - - expect(await pathExists(orphanPath)).toBe(false); - expect(await pathExists(join(getGlobalCasDir(storageRoot), `${promptHash}.txt`))).toBe(false); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/help.test.ts b/legacy-packages/cli-workflow/__tests__/help.test.ts deleted file mode 100644 index 80610b4..0000000 --- a/legacy-packages/cli-workflow/__tests__/help.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { formatCliUsage, runCli } from "../src/cli-dispatch.js"; -import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "../src/skill.js"; - -const STORAGE_ROOT = "/tmp/help-test-storage"; - -describe("runCli usage", () => { - test("no args prints usage and returns 1", async () => { - const code = await runCli(STORAGE_ROOT, []); - expect(code).toBe(1); - }); -}); - -describe("skill command", () => { - test("skill (no topic) lists topics and returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["skill"]); - expect(code).toBe(0); - }); - - test("skill cli returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["skill", "cli"]); - expect(code).toBe(0); - }); - - test("skill develop returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["skill", "develop"]); - expect(code).toBe(0); - }); - - test("skill author returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["skill", "author"]); - expect(code).toBe(0); - }); - - test("skill unknown returns 1", async () => { - const code = await runCli(STORAGE_ROOT, ["skill", "unknown"]); - expect(code).toBe(1); - }); -}); - -describe("--help flag on groups", () => { - test("workflow --help returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["workflow", "--help"]); - expect(code).toBe(0); - }); - - test("thread --help returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["thread", "--help"]); - expect(code).toBe(0); - }); - - test("cas --help returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["cas", "--help"]); - expect(code).toBe(0); - }); - - test("init --help returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["init", "--help"]); - expect(code).toBe(0); - }); - - test("setup --help returns 0", async () => { - const code = await runCli(STORAGE_ROOT, ["setup", "--help"]); - expect(code).toBe(0); - }); -}); - -describe("getSkillTopics", () => { - test("returns all topics", () => { - const topics = getSkillTopics(); - const names = topics.map((t) => t.name); - expect(names).toContain("cli"); - expect(names).toContain("develop"); - expect(names).toContain("author"); - }); -}); - -describe("formatSkillIndex", () => { - test("lists all topics", () => { - const idx = formatSkillIndex(); - expect(idx).toContain("# uncaged-workflow skill"); - expect(idx).not.toContain("# uncaged-workflow help --skill"); - expect(idx).toContain("cli"); - expect(idx).toContain("develop"); - expect(idx).toContain("author"); - expect(idx).toContain("skill "); - }); -}); - -describe("formatCliUsage", () => { - test("has tagline, grouped sections, help hint, and env vars", () => { - const u = formatCliUsage(); - expect(u.startsWith("uncaged-workflow — workflow engine CLI")).toBe(true); - expect(u).toContain("Workflow registry:"); - expect(u).toContain("Thread execution:"); - expect(u).toContain("Content-addressable storage:"); - expect(u).toContain("Development:"); - expect(u).toContain("Configuration:"); - expect(u).toContain("setup [--provider ]"); - expect(u).toContain("Shortcuts:"); - expect(u).toContain("Reference:"); - expect(u).toContain("skill [topic]"); - expect(u).toContain("Agent-consumable docs"); - expect(u).toContain("Use --help for subcommand details."); - expect(u).toContain("Environment variables:"); - expect(u).toContain("WORKFLOW_STORAGE_ROOT"); - expect(u).toContain("UNCAGED_WORKFLOW_STORAGE_ROOT"); - }); - - test("lists commands from registry with descriptions", () => { - const u = formatCliUsage(); - expect(u).toContain("workflow add"); - expect(u).toContain("Register a workflow bundle in the registry"); - expect(u).toContain("thread run"); - expect(u).toContain("Start a new thread executing a workflow"); - expect(u).toContain("cas gc"); - expect(u).toContain("Garbage-collect unreferenced CAS entries"); - }); -}); - -const cliSkillDoc = formatSkillTopic("cli"); -if (cliSkillDoc === null) { - throw new Error("BUG: cli skill topic missing"); -} - -describe("formatSkillTopic('cli')", () => { - const doc = cliSkillDoc; - - test("contains title", () => { - expect(doc).toContain("# uncaged-workflow CLI Reference"); - }); - - test("contains all command group headers", () => { - expect(doc).toContain("### workflow"); - expect(doc).toContain("### thread"); - expect(doc).toContain("### cas"); - expect(doc).toContain("### init"); - expect(doc).toContain("### setup"); - expect(doc).toContain("### Top-level shortcuts"); - }); - - test("contains core concepts", () => { - expect(doc).toContain("## Core Concepts"); - expect(doc).toContain("Workflow"); - expect(doc).toContain("Bundle"); - expect(doc).toContain("Thread"); - expect(doc).toContain("CAS"); - expect(doc).toContain("Registry"); - }); - - test("mentions all workflow subcommands", () => { - for (const sub of ["add", "list", "show", "rm", "history", "rollback"]) { - expect(doc).toContain(`workflow ${sub}`); - } - }); - - test("mentions all thread subcommands", () => { - for (const sub of [ - "run", - "list", - "show", - "rm", - "fork", - "ps", - "kill", - "live", - "pause", - "resume", - ]) { - expect(doc).toContain(`thread ${sub}`); - } - }); - - test("mentions all cas subcommands", () => { - for (const sub of ["get", "put", "list", "rm", "gc"]) { - expect(doc).toContain(`cas ${sub}`); - } - }); - - test("contains exit codes section", () => { - expect(doc).toContain("## Exit Codes"); - }); - - test("contains environment variables section", () => { - expect(doc).toContain("## Environment Variables"); - expect(doc).toContain("UNCAGED_WORKFLOW_STORAGE_ROOT"); - }); - - test("contains typical workflow section", () => { - expect(doc).toContain("## Typical Workflow"); - }); -}); - -describe("formatSkillTopic('develop')", () => { - const doc = formatSkillTopic("develop"); - - test("returns non-null", () => { - expect(doc).not.toBeNull(); - }); - - test("contains thread ID info", () => { - expect(doc).toContain("Thread ID"); - expect(doc).toContain("Crockford Base32"); - }); - - test("contains CAS commands", () => { - expect(doc).toContain("cas put"); - expect(doc).toContain("cas get"); - }); - - test("contains meta output section", () => { - expect(doc).toContain("Meta Output"); - }); -}); - -describe("formatSkillTopic('author')", () => { - const doc = formatSkillTopic("author"); - - test("returns non-null", () => { - expect(doc).not.toBeNull(); - }); - - test("contains bundle structure", () => { - expect(doc).toContain("Bundle Structure"); - expect(doc).toContain(".esm.js"); - }); - - test("contains descriptor info", () => { - expect(doc).toContain("WorkflowDescriptor"); - }); - - test("contains role definition", () => { - expect(doc).toContain("Role Definition"); - }); -}); - -describe("formatSkillTopic unknown", () => { - test("returns null for unknown topic", () => { - expect(formatSkillTopic("nonexistent")).toBeNull(); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/init-template.test.ts b/legacy-packages/cli-workflow/__tests__/init-template.test.ts deleted file mode 100644 index 0fdff01..0000000 --- a/legacy-packages/cli-workflow/__tests__/init-template.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdir, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { runCli } from "../src/cli-dispatch.js"; -import { cmdInitTemplate, cmdInitWorkspace } from "../src/commands/init/index.js"; -import { pathExists } from "../src/fs-utils.js"; - -describe("init template", () => { - let parent: string; - - beforeEach(async () => { - parent = join( - tmpdir(), - `wf-init-template-${Date.now()}-${Math.random().toString(36).slice(2)}`, - ); - await mkdir(parent, { recursive: true }); - }); - - afterEach(async () => { - await rm(parent, { recursive: true, force: true }); - }); - - test("creates templates/ with expected files", async () => { - const ws = await cmdInitWorkspace(parent, "my-workflows"); - expect(ws.ok).toBe(true); - if (!ws.ok) { - return; - } - const root = ws.value.rootPath; - - const created = await cmdInitTemplate(root, "review-pr"); - expect(created.ok).toBe(true); - if (!created.ok) { - return; - } - - const tdir = join(root, "templates", "review-pr"); - expect(created.value.templatePath).toBe(tdir); - expect(await pathExists(join(tdir, "package.json"))).toBe(true); - expect(await pathExists(join(tdir, "tsconfig.json"))).toBe(true); - expect(await pathExists(join(tdir, "src", "roles.ts"))).toBe(true); - expect(await pathExists(join(tdir, "src", "moderator.ts"))).toBe(true); - expect(await pathExists(join(tdir, "src", "index.ts"))).toBe(true); - - const pkg = JSON.parse(await readFile(join(tdir, "package.json"), "utf8")) as { - name: string; - type: string; - dependencies: Record; - }; - expect(pkg.type).toBe("module"); - expect(pkg.dependencies["@uncaged/workflow-runtime"]).toBeDefined(); - expect(pkg.dependencies.zod).toBeDefined(); - expect(pkg.name).toContain("review-pr"); - - const idx = await readFile(join(tdir, "src", "index.ts"), "utf8"); - expect(idx).toContain("WorkflowDefinition"); - - const roles = await readFile(join(tdir, "src", "roles.ts"), "utf8"); - expect(roles).not.toContain("interface "); - expect(roles).not.toContain("?:"); - expect(roles).not.toContain("export default"); - - const moder = await readFile(join(tdir, "src", "moderator.ts"), "utf8"); - expect(moder).not.toContain("export default"); - expect(moder).toContain("ModeratorTable"); - }); - - test("finds workspace walking up from nested cwd", async () => { - const ws = await cmdInitWorkspace(parent, "ws"); - expect(ws.ok).toBe(true); - if (!ws.ok) { - return; - } - const root = ws.value.rootPath; - const nested = join(root, "a", "b"); - await mkdir(nested, { recursive: true }); - - const created = await cmdInitTemplate(nested, "nested-tpl"); - expect(created.ok).toBe(true); - if (!created.ok) { - return; - } - expect(await pathExists(join(root, "templates", "nested-tpl", "src", "index.ts"))).toBe(true); - }); - - test("errors when not inside a workflow workspace", async () => { - const orphan = join(parent, "nowhere"); - await mkdir(orphan, { recursive: true }); - const r = await cmdInitTemplate(orphan, "x"); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("templates/*"); - } - }); - - test("errors when template directory already exists", async () => { - const ws = await cmdInitWorkspace(parent, "ws"); - expect(ws.ok).toBe(true); - if (!ws.ok) { - return; - } - const root = ws.value.rootPath; - - const first = await cmdInitTemplate(root, "dup"); - expect(first.ok).toBe(true); - - const second = await cmdInitTemplate(root, "dup"); - expect(second.ok).toBe(false); - if (!second.ok) { - expect(second.error).toContain("already exists"); - } - }); - - test("errors on invalid template name", async () => { - const ws = await cmdInitWorkspace(parent, "ws"); - expect(ws.ok).toBe(true); - if (!ws.ok) { - return; - } - const bad = await cmdInitTemplate(ws.value.rootPath, "a/b"); - expect(bad.ok).toBe(false); - }); - - test.serial("runCli init template uses cwd and succeeds in workspace", async () => { - const ws = await cmdInitWorkspace(parent, "cli-ws"); - expect(ws.ok).toBe(true); - if (!ws.ok) { - return; - } - const root = ws.value.rootPath; - const prev = process.cwd(); - try { - process.chdir(root); - const code = await runCli(join(parent, "_storage"), ["init", "template", "from-cli"]); - expect(code).toBe(0); - expect(await pathExists(join(root, "templates", "from-cli", "package.json"))).toBe(true); - } finally { - process.chdir(prev); - } - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/init-workspace.test.ts b/legacy-packages/cli-workflow/__tests__/init-workspace.test.ts deleted file mode 100644 index f338dbd..0000000 --- a/legacy-packages/cli-workflow/__tests__/init-workspace.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdir, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { formatCliUsage, runCli } from "../src/cli-dispatch.js"; -import { cmdInitWorkspace } from "../src/commands/init/index.js"; -import { pathExists } from "../src/fs-utils.js"; - -describe("init workspace", () => { - let parent: string; - - beforeEach(async () => { - parent = join(tmpdir(), `wf-init-${Date.now()}-${Math.random().toString(36).slice(2)}`); - await mkdir(parent, { recursive: true }); - }); - - afterEach(async () => { - await rm(parent, { recursive: true, force: true }); - }); - - test("creates expected files and directories", async () => { - const created = await cmdInitWorkspace(parent, "my-workflows"); - expect(created.ok).toBe(true); - if (!created.ok) { - return; - } - - const root = created.value.rootPath; - expect(await pathExists(join(root, "package.json"))).toBe(true); - expect(await pathExists(join(root, "biome.json"))).toBe(true); - expect(await pathExists(join(root, "tsconfig.json"))).toBe(true); - expect(await pathExists(join(root, "AGENTS.md"))).toBe(true); - expect(await pathExists(join(root, "README.md"))).toBe(true); - expect(await pathExists(join(root, "templates"))).toBe(true); - expect(await pathExists(join(root, "templates", ".gitkeep"))).toBe(true); - expect(await pathExists(join(root, "workflows", "package.json"))).toBe(true); - - const rootPkg = JSON.parse(await readFile(join(root, "package.json"), "utf8")) as { - workspaces: string[]; - scripts: { bundle: string }; - }; - expect(rootPkg.workspaces).toEqual(["templates/*", "workflows"]); - expect(rootPkg.scripts.bundle).toBe("bun run scripts/bundle.ts"); - - expect(await pathExists(join(root, "scripts", "bundle.ts"))).toBe(true); - const bundleSrc = await readFile(join(root, "scripts", "bundle.ts"), "utf8"); - expect(bundleSrc).toContain("Bun.build"); - expect(bundleSrc).toContain("-entry.ts"); - expect(bundleSrc).toContain("distDir"); - - const wfPkg = JSON.parse(await readFile(join(root, "workflows", "package.json"), "utf8")) as { - type: string; - dependencies: Record; - }; - expect(wfPkg.type).toBe("module"); - expect(wfPkg.dependencies["@uncaged/workflow-runtime"]).toBeDefined(); - expect(wfPkg.dependencies.zod).toBeDefined(); - - const tsconfig = JSON.parse(await readFile(join(root, "tsconfig.json"), "utf8")) as { - compilerOptions: { strict: boolean; module: string; target: string }; - }; - expect(tsconfig.compilerOptions.strict).toBe(true); - expect(tsconfig.compilerOptions.module).toBe("ESNext"); - expect(tsconfig.compilerOptions.target).toBe("ESNext"); - }); - - test("AGENTS.md contains coding agent guide sections and terms", async () => { - const created = await cmdInitWorkspace(parent, "my-workflows"); - expect(created.ok).toBe(true); - if (!created.ok) { - return; - } - - const agentsPath = join(created.value.rootPath, "AGENTS.md"); - const body = await readFile(agentsPath, "utf8"); - - for (const section of [ - "项目结构", - "核心概念", - "开发流程", - "编码规范", - "Template", - "Build", - "常见陷阱", - ]) { - expect(body).toContain(section); - } - - for (const term of [ - "RoleDefinition", - "WorkflowDefinition", - "ModeratorTable", - "AdapterFn", - "ExtractFn", - "RoleMeta", - ]) { - expect(body).toContain(term); - } - - expect(body).toMatch(/type[\s\S]*interface/i); - expect(body).toMatch(/function[\s\S]*class/i); - expect(body).toContain("Crockford Base32"); - expect(body).toMatch(/no[\s\S]*default export/i); - expect(body).toMatch(/no[\s\S]*console/i); - expect(body).toMatch(/no[\s\S]*dynamic import/i); - - expect(body).toContain("bun run check"); - expect(body).toContain("bun test"); - expect(body).toContain("uncaged-workflow"); - expect(body).toContain("bun build"); - expect(body).toContain("CLAUDE.md"); - expect(body).toContain("docs/architecture.md"); - }); - - test("errors when directory already exists", async () => { - const first = await cmdInitWorkspace(parent, "dup"); - expect(first.ok).toBe(true); - - const second = await cmdInitWorkspace(parent, "dup"); - expect(second.ok).toBe(false); - if (!second.ok) { - expect(second.error).toContain("already exists"); - } - }); - - test("errors on invalid workspace name", async () => { - const dots = await cmdInitWorkspace(parent, ".."); - expect(dots.ok).toBe(false); - - const empty = await cmdInitWorkspace(parent, ""); - expect(empty.ok).toBe(false); - }); - - test("accepts nested path as workspace name", async () => { - const nested = await cmdInitWorkspace(parent, "a/b"); - expect(nested.ok).toBe(true); - if (nested.ok) { - expect(nested.value.rootPath).toContain("a/b"); - } - }); - - test("usage lists init subcommands", () => { - const u = formatCliUsage(); - expect(u).toContain("init workspace "); - expect(u).toContain("init template "); - expect(u).toContain("Development:"); - }); - - test("runCli rejects unknown init subcommand", async () => { - const code = await runCli(join(parent, "_storage"), ["init", "bogus", "name"]); - expect(code).toBe(1); - }); - - test.serial("runCli init workspace uses cwd", async () => { - const prev = process.cwd(); - try { - process.chdir(parent); - const code = await runCli(join(parent, "_storage"), ["init", "workspace", "from-cli"]); - expect(code).toBe(0); - expect(await pathExists(join(parent, "from-cli", "workflows", "package.json"))).toBe(true); - } finally { - process.chdir(prev); - } - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/live.test.ts b/legacy-packages/cli-workflow/__tests__/live.test.ts deleted file mode 100644 index b5a7484..0000000 --- a/legacy-packages/cli-workflow/__tests__/live.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { spawnSync } from "node:child_process"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; - -import { - formatLiveDebugLine, - formatLiveTimeLabel, - LIVE_CONTENT_MAX_LINES, - type LiveRoleRow, - renderLiveRoleStepLines, -} from "../src/commands/thread/index.js"; -import { parseLiveArgv } from "../src/live-argv.js"; - -const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); - -describe("live helpers", () => { - test("formatLiveTimeLabel pads HH:MM:SS", () => { - const label = formatLiveTimeLabel(new Date("2024-06-01T09:08:07.000Z").getTime()); - expect(label).toMatch(/^\d{2}:\d{2}:\d{2}$/); - }); - - test("formatLiveDebugLine flattens newlines in message", () => { - const line = formatLiveDebugLine(0, "TAG1", "a\nb"); - expect(line).toContain("[TAG1]"); - expect(line).toContain("a b"); - expect(line).not.toContain("\n"); - }); - - test("renderLiveRoleStepLines truncates content to LIVE_CONTENT_MAX_LINES", () => { - const lines = Array.from({ length: LIVE_CONTENT_MAX_LINES + 3 }, (_, i) => `L${i + 1}`); - const row: LiveRoleRow = { - role: "r", - content: lines.join("\n"), - meta: { k: "v" }, - timestamp: 0, - }; - const out = renderLiveRoleStepLines(row, "r"); - const body = out.filter((l) => l.startsWith(" L")); - expect(body.length).toBe(LIVE_CONTENT_MAX_LINES); - expect(out.some((l) => l.includes("more line"))).toBe(true); - expect(out.some((l) => l.startsWith(" meta: "))).toBe(true); - }); -}); - -describe("parseLiveArgv", () => { - test("parses thread id and flags in any order", () => { - const a = parseLiveArgv(["01ABC", "--debug", "--role", "planner"]); - expect(a.ok).toBe(true); - if (a.ok) { - expect(a.value.threadId).toBe("01ABC"); - expect(a.value.latest).toBe(false); - expect(a.value.debug).toBe(true); - expect(a.value.role).toBe("planner"); - } - const b = parseLiveArgv(["--latest", "--role", "x"]); - expect(b.ok).toBe(true); - if (b.ok) { - expect(b.value.latest).toBe(true); - expect(b.value.threadId).toBe(null); - expect(b.value.role).toBe("x"); - } - }); - - test("rejects --latest with thread id", () => { - const r = parseLiveArgv(["--latest", "01ABC"]); - expect(r.ok).toBe(false); - }); -}); - -describe("live CLI", () => { - let prevEnv: string | undefined; - let storageRoot: string; - - beforeEach(async () => { - prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - storageRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-live-")); - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot; - }); - - afterEach(async () => { - if (prevEnv === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = prevEnv; - } - await rm(storageRoot, { recursive: true, force: true }); - }); - - test("unknown thread id exits 1", () => { - const env = { ...process.env, UNCAGED_WORKFLOW_STORAGE_ROOT: storageRoot }; - const r = spawnSync(process.execPath, [cliEntryPath, "live", "01UNKNOWNXXXXXXXXXXXXXXXXX"], { - env, - encoding: "utf8", - }); - expect(r.status).toBe(1); - expect(String(r.stderr ?? "")).toContain("thread not found"); - }); -}); - -describe("live --latest with empty storage", () => { - let prevEnv: string | undefined; - let emptyRoot: string; - - beforeEach(async () => { - prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - emptyRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-live-empty-")); - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = emptyRoot; - }); - - afterEach(async () => { - if (prevEnv === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = prevEnv; - } - await rm(emptyRoot, { recursive: true, force: true }); - }); - - test("exits 1 when no threads exist", () => { - const env = { ...process.env, UNCAGED_WORKFLOW_STORAGE_ROOT: emptyRoot }; - const r = spawnSync(process.execPath, [cliEntryPath, "live", "--latest"], { - env, - encoding: "utf8", - }); - expect(r.status).toBe(1); - expect(String(r.stderr ?? "")).toContain("no threads"); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/setup-cli.test.ts b/legacy-packages/cli-workflow/__tests__/setup-cli.test.ts deleted file mode 100644 index 45464d3..0000000 --- a/legacy-packages/cli-workflow/__tests__/setup-cli.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { readWorkflowRegistry } from "@uncaged/workflow-register"; - -import { runCli } from "../src/cli-dispatch.js"; -import { cmdSetup } from "../src/commands/setup/index.js"; - -describe("setup command (CLI mode)", () => { - let prevEnv: string | undefined; - let storageRoot: string; - - beforeEach(async () => { - prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - storageRoot = await mkdtemp(join(tmpdir(), "uncaged-setup-")); - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot; - await mkdir(storageRoot, { recursive: true }); - }); - - afterEach(async () => { - if (prevEnv === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = prevEnv; - } - await rm(storageRoot, { recursive: true, force: true }); - }); - - test("writes workflow.yaml with provider, models.default, and depth defaults", async () => { - const r = await cmdSetup(storageRoot, { - provider: "dashscope", - baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", - apiKey: "sk-test123", - defaultModel: "dashscope/qwen-plus", - initWorkspaceName: null, - }); - expect(r.ok).toBe(true); - if (!r.ok) { - return; - } - - const reg = await readWorkflowRegistry(storageRoot); - expect(reg.ok).toBe(true); - if (!reg.ok) { - return; - } - expect(reg.value.config).not.toBeNull(); - if (reg.value.config === null) { - return; - } - expect(reg.value.config.providers.dashscope).toEqual({ - baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", - apiKey: "sk-test123", - }); - expect(reg.value.config.models.default).toBe("dashscope/qwen-plus"); - expect(reg.value.config.maxDepth).toBe(3); - expect(reg.value.config.supervisorInterval).toBe(3); - - const raw = await readFile(join(storageRoot, "workflow.yaml"), "utf8"); - expect(raw).toContain("dashscope"); - expect(raw).toContain("qwen-plus"); - }); - - test("idempotent: second run updates apiKey and preserves workflows", async () => { - const initialYaml = `config: - maxDepth: 7 - supervisorInterval: 2 - providers: - dashscope: - baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1 - apiKey: sk-old - models: - default: dashscope/qwen-plus -workflows: - keep-me: - hash: "0000000000000" - timestamp: 1 - history: [] -`; - await writeFile(join(storageRoot, "workflow.yaml"), initialYaml, "utf8"); - - const r2 = await cmdSetup(storageRoot, { - provider: "dashscope", - baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", - apiKey: "sk-newkey", - defaultModel: "dashscope/qwen-plus", - initWorkspaceName: null, - }); - expect(r2.ok).toBe(true); - if (!r2.ok) { - return; - } - - const reg = await readWorkflowRegistry(storageRoot); - expect(reg.ok).toBe(true); - if (!reg.ok || reg.value.config === null) { - return; - } - expect(reg.value.config.providers.dashscope.apiKey).toBe("sk-newkey"); - expect(reg.value.config.maxDepth).toBe(7); - expect(reg.value.config.supervisorInterval).toBe(2); - expect(reg.value.workflows["keep-me"]).toBeDefined(); - if (reg.value.workflows["keep-me"] === undefined) { - return; - } - expect(reg.value.workflows["keep-me"].hash).toBe("0000000000000"); - }); - - test("runCli setup dispatches with flags and exits 0", async () => { - const code = await runCli(storageRoot, [ - "setup", - "--provider", - "openai", - "--base-url", - "https://api.openai.com/v1", - "--api-key", - "sk-test", - "--default-model", - "openai/gpt-4o", - ]); - expect(code).toBe(0); - const reg = await readWorkflowRegistry(storageRoot); - expect(reg.ok).toBe(true); - if (!reg.ok || reg.value.config === null) { - return; - } - expect(reg.value.config.providers.openai.apiKey).toBe("sk-test"); - expect(reg.value.config.models.default).toBe("openai/gpt-4o"); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/storage-env.test.ts b/legacy-packages/cli-workflow/__tests__/storage-env.test.ts deleted file mode 100644 index d4d5f60..0000000 --- a/legacy-packages/cli-workflow/__tests__/storage-env.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util"; -import { resolveWorkflowStorageRoot } from "../src/storage-env.js"; - -describe("resolveWorkflowStorageRoot", () => { - let savedInternal: string | undefined; - let savedUser: string | undefined; - - beforeEach(() => { - savedInternal = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - savedUser = process.env.WORKFLOW_STORAGE_ROOT; - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - delete process.env.WORKFLOW_STORAGE_ROOT; - }); - - afterEach(() => { - if (savedInternal === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = savedInternal; - } - if (savedUser === undefined) { - delete process.env.WORKFLOW_STORAGE_ROOT; - } else { - process.env.WORKFLOW_STORAGE_ROOT = savedUser; - } - }); - - test("returns default when no env vars are set", () => { - expect(resolveWorkflowStorageRoot()).toBe(getDefaultWorkflowStorageRoot()); - }); - - test("WORKFLOW_STORAGE_ROOT overrides default", () => { - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/custom-storage"; - expect(resolveWorkflowStorageRoot()).toBe("/tmp/custom-storage"); - }); - - test("UNCAGED_WORKFLOW_STORAGE_ROOT takes priority over WORKFLOW_STORAGE_ROOT", () => { - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/user-path"; - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = "/tmp/internal-path"; - expect(resolveWorkflowStorageRoot()).toBe("/tmp/internal-path"); - }); - - test("ignores empty WORKFLOW_STORAGE_ROOT", () => { - process.env.WORKFLOW_STORAGE_ROOT = ""; - expect(resolveWorkflowStorageRoot()).toBe(getDefaultWorkflowStorageRoot()); - }); - - test("ignores empty UNCAGED_WORKFLOW_STORAGE_ROOT and falls through to WORKFLOW_STORAGE_ROOT", () => { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = ""; - process.env.WORKFLOW_STORAGE_ROOT = "/tmp/user-fallback"; - expect(resolveWorkflowStorageRoot()).toBe("/tmp/user-fallback"); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/thread-cli.test.ts b/legacy-packages/cli-workflow/__tests__/thread-cli.test.ts deleted file mode 100644 index 8c615dd..0000000 --- a/legacy-packages/cli-workflow/__tests__/thread-cli.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { getBundleDir, readThreadsIndex } from "@uncaged/workflow-execute"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; -import { cmdCasPut } from "../src/commands/cas/index.js"; -import { - cmdKill, - cmdPause, - cmdPs, - cmdResume, - cmdRun, - cmdThreadRemove, - 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 { resolveThreadRecord } from "../src/thread-scan.js"; -import { addCliArgs } from "./bundle-fixture.js"; -import { ensureTestWorkflowRegistryConfig } from "./workflow-registry-fixture.js"; - -const threadFixtureDescriptor = `export const descriptor = { - description: "thread-cli", - roles: { - planner: { description: "planner", schema: {} }, - coder: { description: "coder", schema: {} }, - first: { description: "first", schema: {} }, - second: { description: "second", schema: {} }, - only: { description: "only", schema: {} }, - noop: { description: "noop", schema: {} }, - }, - graph: { edges: [] }, -}; -`; - -const fastBundleSource = `${threadFixtureDescriptor} -export const run = async function* (input, options) { - const cas = options.cas; - let h = await cas.put( "plan"); - yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] }; - h = await cas.put( "code"); - yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] }; - return { returnCode: 0, summary: "done" }; -}; -`; - -const slowPlannerBundleSource = `${threadFixtureDescriptor} -export const run = async function* (input, options) { - await new Promise((r) => setTimeout(r, 400)); - const cas = options.cas; - let h = await cas.put( "plan"); - yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] }; - h = await cas.put( "code"); - yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] }; - return { returnCode: 0, summary: "done" }; -}; -`; - -const cliEntryPath = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); - -const abortablePlannerBundleSource = `${threadFixtureDescriptor} -export const run = async function* (input, options) { - const cas = options.cas; - let h = await cas.put( "plan"); - yield { role: "planner", contentHash: h, meta: { plan: input.prompt }, refs: [h] }; - await new Promise((r) => setTimeout(r, 10000)); - h = await cas.put( "code"); - yield { role: "coder", contentHash: h, meta: { diff: "y" }, refs: [h] }; - return { returnCode: 0, summary: "done" }; -}; -`; - -const pauseResumeBundleSource = `${threadFixtureDescriptor} -export const run = async function* (_input, options) { - const cas = options.cas; - let h = await cas.put( "f"); - yield { role: "first", contentHash: h, meta: {}, refs: [h] }; - await new Promise((r) => setTimeout(r, 1500)); - h = await cas.put( "s"); - yield { role: "second", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "done" }; -}; -`; - -const delayedFirstYieldBundleSource = `${threadFixtureDescriptor} -export const run = async function* (_input, options) { - await new Promise((r) => setTimeout(r, 900)); - const cas = options.cas; - const h = await cas.put( "x"); - yield { role: "only", contentHash: h, meta: {}, refs: [h] }; - return { returnCode: 0, summary: "done" }; -}; -`; - -async function waitUntilRunningFileAbsent(runningPath: string, maxAttempts: number): Promise { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - if (!(await pathExists(runningPath))) { - return; - } - await new Promise((r) => setTimeout(r, 25)); - } -} - -async function waitUntilPredicate( - predicate: () => Promise, - maxAttempts: number, -): Promise { - for (let attempt = 0; attempt < maxAttempts; attempt++) { - if (await predicate()) { - return; - } - await new Promise((r) => setTimeout(r, 25)); - } -} - -describe("cli thread commands", () => { - let prevEnv: string | undefined; - let storageRoot: string; - - beforeEach(async () => { - prevEnv = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - storageRoot = await mkdtemp(join(tmpdir(), "uncaged-wf-thread-")); - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = storageRoot; - await ensureTestWorkflowRegistryConfig(storageRoot); - }); - - afterEach(async () => { - if (prevEnv === undefined) { - delete process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - } else { - process.env.UNCAGED_WORKFLOW_STORAGE_ROOT = prevEnv; - } - await rm(storageRoot, { recursive: true, force: true }); - }); - - test("run / threads / thread / thread rm", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, fastBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - - const threadId = ran.value.threadId; - - let threads = await cmdThreads(storageRoot, []); - for ( - let attempt = 0; - attempt < 50 && threads.ok && !threads.value.some((l) => l.includes(threadId)); - attempt++ - ) { - await new Promise((r) => setTimeout(r, 20)); - threads = await cmdThreads(storageRoot, []); - } - expect(threads.ok).toBe(true); - if (!threads.ok) { - return; - } - expect(threads.value.some((l) => l.includes(threadId))).toBe(true); - - const runningPath = join(storageRoot, "logs", added.value.hash, `${threadId}.running`); - await waitUntilRunningFileAbsent(runningPath, 120); - - const shown = await cmdThreadShow(storageRoot, threadId); - expect(shown.ok).toBe(true); - if (!shown.ok) { - return; - } - expect(shown.value.includes('"threadId"')).toBe(true); - - const parsed = JSON.parse(shown.value) as Record; - expect(parsed.parentState).toBeNull(); - const parsedSteps = parsed.steps as Array>; - for (const step of parsedSteps) { - expect(step).toHaveProperty("childThread"); - expect(step.childThread).toBeNull(); - } - - const removed = await cmdThreadRemove(storageRoot, threadId); - expect(removed.ok).toBe(true); - - expect(await resolveThreadRecord(storageRoot, threadId)).toBeNull(); - }); - - test("thread rm runs GC and removes CAS blobs not referenced by any remaining thread", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, fastBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - - const threadId = ran.value.threadId; - - let threads = await cmdThreads(storageRoot, []); - for ( - let attempt = 0; - attempt < 50 && threads.ok && !threads.value.some((l) => l.includes(threadId)); - attempt++ - ) { - await new Promise((r) => setTimeout(r, 20)); - threads = await cmdThreads(storageRoot, []); - } - - const runningPath = join(storageRoot, "logs", added.value.hash, `${threadId}.running`); - await waitUntilRunningFileAbsent(runningPath, 120); - expect((await resolveThreadRecord(storageRoot, threadId))?.source).toBe("history"); - - const put = await cmdCasPut(storageRoot, "keep-after-thread-rm"); - expect(put.ok).toBe(true); - if (!put.ok) { - return; - } - const hash = put.value; - const casBlob = join(getGlobalCasDir(storageRoot), `${hash}.txt`); - - const removed = await cmdThreadRemove(storageRoot, threadId); - expect(removed.ok).toBe(true); - - const stillThere = await readTextFileIfExists(casBlob); - expect(stillThere).toBeNull(); - }); - - test("cli entrypoint dispatches threads / ps (spawn)", () => { - const env = { ...process.env, UNCAGED_WORKFLOW_STORAGE_ROOT: storageRoot }; - const threads = spawnSync(process.execPath, [cliEntryPath, "thread", "list"], { - env, - encoding: "utf8", - }); - expect(threads.status).toBe(0); - - const ps = spawnSync(process.execPath, [cliEntryPath, "thread", "ps"], { - env, - encoding: "utf8", - }); - expect(ps.status).toBe(0); - }); - - test("ps lists running threads while planner role is in-flight", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, slowPlannerBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - - const threadId = ran.value.threadId; - - await new Promise((r) => setTimeout(r, 50)); - const psEarly = await cmdPs(storageRoot); - expect(psEarly.some((l) => l.includes(threadId))).toBe(true); - - await new Promise((r) => setTimeout(r, 900)); - - const psLate = await cmdPs(storageRoot); - expect(psLate).toEqual(["(no running threads)"]); - }); - - test("kill stops thread after the in-flight role (before subsequent roles)", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, abortablePlannerBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - - const threadId = ran.value.threadId; - const killBundleDir = getBundleDir(storageRoot, added.value.hash); - - await waitUntilPredicate(async () => { - const idx = await readThreadsIndex(killBundleDir); - const ent = idx[threadId]; - return ent !== undefined && ent.head !== ent.start; - }, 80); - - const killed = await cmdKill(storageRoot, threadId); - expect(killed.ok).toBe(true); - - await waitUntilPredicate(async () => { - return (await resolveThreadRecord(storageRoot, threadId))?.source === "history"; - }, 120); - - expect((await resolveThreadRecord(storageRoot, threadId))?.source).toBe("history"); - - const runningPath = join(storageRoot, "logs", added.value.hash, `${threadId}.running`); - expect(await pathExists(runningPath)).toBe(false); - }); - - test("pause stops between yields and resume completes thread", async () => { - const srcDir = join(storageRoot, "src"); - await mkdir(srcDir, { recursive: true }); - const bundlePath = join(srcDir, "demo.esm.js"); - await writeFile(bundlePath, pauseResumeBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - - const threadId = ran.value.threadId; - const bundleDir = getBundleDir(storageRoot, added.value.hash); - - await waitUntilPredicate(async () => { - const idx = await readThreadsIndex(bundleDir); - const ent = idx[threadId]; - return ent !== undefined && ent.head !== ent.start; - }, 80); - - const idxBeforePause = await readThreadsIndex(bundleDir); - const headAtPause = idxBeforePause[threadId]?.head; - - const paused = await cmdPause(storageRoot, threadId); - expect(paused.ok).toBe(true); - - await new Promise((r) => setTimeout(r, 400)); - const idxPaused = await readThreadsIndex(bundleDir); - expect(idxPaused[threadId]?.head).toBe(headAtPause); - - const resumed = await cmdResume(storageRoot, threadId); - expect(resumed.ok).toBe(true); - - await waitUntilPredicate(async () => { - const row = await resolveThreadRecord(storageRoot, threadId); - return row?.source === "history"; - }, 120); - - const runningPath = join(storageRoot, "logs", added.value.hash, `${threadId}.running`); - await waitUntilRunningFileAbsent(runningPath, 100); - expect(await pathExists(runningPath)).toBe(false); - }); - - test("pause on completed thread errors", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, fastBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - - const threadId = ran.value.threadId; - const runningPath = join(storageRoot, "logs", added.value.hash, `${threadId}.running`); - - await waitUntilRunningFileAbsent(runningPath, 100); - expect(await pathExists(runningPath)).toBe(false); - - const paused = await cmdPause(storageRoot, threadId); - expect(paused.ok).toBe(false); - }); - - test("resume while thread is running but not paused errors", async () => { - const bundleDir = join(storageRoot, "src"); - await mkdir(bundleDir, { recursive: true }); - const bundlePath = join(bundleDir, "demo.esm.js"); - await writeFile(bundlePath, delayedFirstYieldBundleSource, "utf8"); - - const added = await cmdAdd(storageRoot, addCliArgs("solve-issue", bundlePath)); - expect(added.ok).toBe(true); - if (!added.ok) { - return; - } - - const ran = await cmdRun(storageRoot, "solve-issue", "hello", 5); - expect(ran.ok).toBe(true); - if (!ran.ok) { - return; - } - - const threadId = ran.value.threadId; - await new Promise((r) => setTimeout(r, 40)); - - const resumed = await cmdResume(storageRoot, threadId); - expect(resumed.ok).toBe(false); - }); -}); diff --git a/legacy-packages/cli-workflow/__tests__/workflow-registry-fixture.ts b/legacy-packages/cli-workflow/__tests__/workflow-registry-fixture.ts deleted file mode 100644 index 6f2878a..0000000 --- a/legacy-packages/cli-workflow/__tests__/workflow-registry-fixture.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -/** Minimal valid global config so {@link executeThread} can resolve the extract scene (CLI integration tests). */ -export const TEST_WORKFLOW_REGISTRY_YAML = `config: - maxDepth: 3 - providers: - stub: - baseUrl: http://127.0.0.1:9 - apiKey: test - models: - default: stub/m -workflows: {} -`; - -export async function ensureTestWorkflowRegistryConfig(storageRoot: string): Promise { - await writeFile(join(storageRoot, "workflow.yaml"), TEST_WORKFLOW_REGISTRY_YAML, "utf8"); -} diff --git a/legacy-packages/cli-workflow/package.json b/legacy-packages/cli-workflow/package.json deleted file mode 100644 index 26ded55..0000000 --- a/legacy-packages/cli-workflow/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@uncaged/cli-workflow", - "version": "0.5.0-alpha.4", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "bin": { - "uncaged-workflow": "src/cli.ts" - }, - "dependencies": { - "@uncaged/workflow-gateway": "workspace:^", - "@uncaged/workflow-protocol": "workspace:^", - "@uncaged/workflow-util": "workspace:^", - "@uncaged/workflow-cas": "workspace:^", - "@uncaged/workflow-execute": "workspace:^", - "@uncaged/workflow-register": "workspace:^", - "@uncaged/workflow-runtime": "workspace:^", - "hono": "^4.12.18", - "yaml": "^2.8.4" - }, - "scripts": { - "test": "bun test" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/cli-workflow/pnpm-lock.yaml b/legacy-packages/cli-workflow/pnpm-lock.yaml deleted file mode 100644 index 38390fd..0000000 --- a/legacy-packages/cli-workflow/pnpm-lock.yaml +++ /dev/null @@ -1,51 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@uncaged/workflow-cas': - specifier: workspace:* - version: link:../workflow-cas - '@uncaged/workflow-execute': - specifier: workspace:* - version: link:../workflow-execute - '@uncaged/workflow-protocol': - specifier: workspace:* - version: link:../workflow-protocol - '@uncaged/workflow-register': - specifier: workspace:* - version: link:../workflow-register - '@uncaged/workflow-runtime': - specifier: workspace:* - version: link:../workflow-runtime - '@uncaged/workflow-util': - specifier: workspace:* - version: link:../workflow-util - hono: - specifier: ^4.12.18 - version: 4.12.18 - yaml: - specifier: ^2.8.4 - version: 2.8.4 - -packages: - - hono@4.12.18: - resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} - engines: {node: '>=16.9.0'} - - yaml@2.8.4: - resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} - engines: {node: '>= 14.6'} - hasBin: true - -snapshots: - - hono@4.12.18: {} - - yaml@2.8.4: {} diff --git a/legacy-packages/cli-workflow/src/bundle-store.ts b/legacy-packages/cli-workflow/src/bundle-store.ts deleted file mode 100644 index 1305c36..0000000 --- a/legacy-packages/cli-workflow/src/bundle-store.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import { pathExists } from "./fs-utils.js"; - -export type BundleFileSource = { kind: "text"; text: string } | { kind: "path"; path: string }; - -export type WorkflowBundleStoreInput = { - esmJs: BundleFileSource; - yaml: BundleFileSource; - dts: BundleFileSource | null; -}; - -async function resolveSourceText(src: BundleFileSource): Promise> { - if (src.kind === "text") { - return ok(src.text); - } - try { - return ok(await readFile(src.path, "utf8")); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - return err(`failed to read bundle artifact: ${message}`); - } -} - -async function ensureMatchingOrWrite( - destPath: string, - text: string, - label: string, -): Promise> { - if (!(await pathExists(destPath))) { - try { - await writeFile(destPath, text, "utf8"); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - return err(`failed to write ${label}: ${message}`); - } - return ok(undefined); - } - - let existing: string; - try { - existing = await readFile(destPath, "utf8"); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - return err(`failed to read existing ${label}: ${message}`); - } - if (existing !== text) { - return err( - `${label} for this hash already exists with different contents; refusing to overwrite`, - ); - } - return ok(undefined); -} - -/** Store `.esm.js`, `.yaml`, and optional `.d.ts` under `bundles/` keyed by hash. */ -export async function storeWorkflowBundleArtifacts( - storageRoot: string, - hash: string, - input: WorkflowBundleStoreInput, -): Promise> { - const bundlesDir = join(storageRoot, "bundles"); - try { - await mkdir(bundlesDir, { recursive: true }); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - return err(`failed to store bundle: ${message}`); - } - - const esmText = await resolveSourceText(input.esmJs); - if (!esmText.ok) { - return esmText; - } - const yamlText = await resolveSourceText(input.yaml); - if (!yamlText.ok) { - return yamlText; - } - - let dtsText: string | null = null; - if (input.dts !== null) { - const dtsResolved = await resolveSourceText(input.dts); - if (!dtsResolved.ok) { - return dtsResolved; - } - dtsText = dtsResolved.value; - } - - const destEsm = join(bundlesDir, `${hash}.esm.js`); - const destYaml = join(bundlesDir, `${hash}.yaml`); - const destDts = join(bundlesDir, `${hash}.d.ts`); - - const w1 = await ensureMatchingOrWrite(destEsm, esmText.value, "bundle"); - if (!w1.ok) { - return w1; - } - const w2 = await ensureMatchingOrWrite(destYaml, yamlText.value, "descriptor"); - if (!w2.ok) { - return w2; - } - - if (dtsText !== null) { - const w3 = await ensureMatchingOrWrite(destDts, dtsText, "types"); - if (!w3.ok) { - return w3; - } - } - - return ok(undefined); -} diff --git a/legacy-packages/cli-workflow/src/cli-color.ts b/legacy-packages/cli-workflow/src/cli-color.ts deleted file mode 100644 index 095d718..0000000 --- a/legacy-packages/cli-workflow/src/cli-color.ts +++ /dev/null @@ -1,17 +0,0 @@ -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`; -} diff --git a/legacy-packages/cli-workflow/src/cli-command-types.ts b/legacy-packages/cli-workflow/src/cli-command-types.ts deleted file mode 100644 index f69bd61..0000000 --- a/legacy-packages/cli-workflow/src/cli-command-types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type DispatchFn = (storageRoot: string, argv: string[]) => Promise; - -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, - storageRoot: string, - argv: string[], -) => Promise | null; diff --git a/legacy-packages/cli-workflow/src/cli-dispatch.ts b/legacy-packages/cli-workflow/src/cli-dispatch.ts deleted file mode 100644 index 3f3cad3..0000000 --- a/legacy-packages/cli-workflow/src/cli-dispatch.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { CommandEntry, DispatchFn } from "./cli-command-types.js"; -import { printCliError, printCliLine } from "./cli-output.js"; -import { getCommandRegistry } from "./cli-registry.js"; -import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js"; -import { createCasDispatcher } from "./commands/cas/index.js"; -import { dispatchConnect } from "./commands/connect/index.js"; -import { createInitDispatcher } from "./commands/init/index.js"; -import { dispatchSetup } from "./commands/setup/index.js"; -import { createThreadDispatcher, dispatchLive, dispatchRun } from "./commands/thread/index.js"; -import { createWorkflowDispatcher } from "./commands/workflow/index.js"; -import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "./skill.js"; - -function dispatchGroup( - tableName: string, - table: Record, - storageRoot: string, - argv: string[], -): Promise | null { - const sub = argv[0]; - if (sub === undefined || sub === "--help" || sub === "-h") { - const entries = Object.entries(table); - const lines = [`${tableName} subcommands:\n`]; - for (const [name, e] of entries) { - const args = e.args ? ` ${e.args}` : ""; - lines.push(` uncaged-workflow ${tableName} ${name}${args}`); - lines.push(` ${e.description}\n`); - } - printCliLine(lines.join("\n")); - return Promise.resolve(sub === undefined ? 1 : 0); - } - const entry = table[sub]; - if (entry === undefined) { - return null; - } - return entry.handler(storageRoot, argv.slice(1)); -} - -export function formatCliUsage(): string { - return formatCliUsageWithGroups(getCommandRegistry(), getSkillTopics()); -} - -const dispatchWorkflow = createWorkflowDispatcher({ dispatchGroup }); -const dispatchThread = createThreadDispatcher({ dispatchGroup }); -const dispatchCas = createCasDispatcher({ dispatchGroup }); -const dispatchInit = createInitDispatcher({ dispatchGroup }); - -async function showSkillDocOrIndex(topic: string | undefined): Promise { - 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; -} - -async function dispatchSkill(_storageRoot: string, argv: string[]): Promise { - return showSkillDocOrIndex(argv[0]); -} - -const COMMAND_TABLE: Record = { - workflow: dispatchWorkflow, - thread: dispatchThread, - cas: dispatchCas, - init: dispatchInit, - setup: dispatchSetup, - skill: dispatchSkill, - run: dispatchRun, - live: dispatchLive, - connect: dispatchConnect, -}; - -export async function runCli(storageRoot: string, argv: string[]): Promise { - if (argv.length === 0) { - printCliLine(formatCliUsage()); - return 1; - } - const command = argv[0]; - if (command === undefined) { - printCliLine(formatCliUsage()); - return 1; - } - const rest = argv.slice(1); - - const dispatch = COMMAND_TABLE[command]; - if (dispatch !== undefined) { - return dispatch(storageRoot, rest); - } - - printCliError(`${formatCliUsage()}\n\nerror: unknown command ${command}`); - return 1; -} diff --git a/legacy-packages/cli-workflow/src/cli-output.ts b/legacy-packages/cli-workflow/src/cli-output.ts deleted file mode 100644 index bf9a769..0000000 --- a/legacy-packages/cli-workflow/src/cli-output.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function printCliLine(line: string): void { - // biome-ignore lint/suspicious/noConsole: CLI user-facing output - console.log(line); -} - -export function printCliError(line: string): void { - // biome-ignore lint/suspicious/noConsole: CLI user-facing errors - console.error(line); -} - -export function printCliWarn(line: string): void { - // biome-ignore lint/suspicious/noConsole: CLI user-facing warnings - console.warn(line); -} diff --git a/legacy-packages/cli-workflow/src/cli-registry.ts b/legacy-packages/cli-workflow/src/cli-registry.ts deleted file mode 100644 index cfc54b9..0000000 --- a/legacy-packages/cli-workflow/src/cli-registry.ts +++ /dev/null @@ -1,58 +0,0 @@ -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"; - -const SETUP_USAGE_COMMANDS = [ - { - name: "", - args: "[--provider ] [--base-url ] [--api-key ] [--default-model ] [--init-workspace ]", - description: - "Configure workflow.yaml LLM providers and default model (interactive when no flags)", - }, -] as const; - -export function getCommandRegistry(): ReadonlyArray { - 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, - })), - }, - { - name: "setup", - commands: [...SETUP_USAGE_COMMANDS], - }, - ]; -} - -setCommandGroupsForUsage(getCommandRegistry()); diff --git a/legacy-packages/cli-workflow/src/cli-usage-context.ts b/legacy-packages/cli-workflow/src/cli-usage-context.ts deleted file mode 100644 index e748e27..0000000 --- a/legacy-packages/cli-workflow/src/cli-usage-context.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CommandGroup } from "./cli-command-types.js"; - -let commandGroupsForUsage: ReadonlyArray | null = null; - -export function setCommandGroupsForUsage(groups: ReadonlyArray): void { - commandGroupsForUsage = groups; -} - -export function getCommandGroupsForUsage(): ReadonlyArray { - if (commandGroupsForUsage === null) { - throw new Error("BUG: command groups for usage not initialized"); - } - return commandGroupsForUsage; -} diff --git a/legacy-packages/cli-workflow/src/cli-usage.ts b/legacy-packages/cli-workflow/src/cli-usage.ts deleted file mode 100644 index 2bbb2d5..0000000 --- a/legacy-packages/cli-workflow/src/cli-usage.ts +++ /dev/null @@ -1,94 +0,0 @@ -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 = { - workflow: "Workflow registry:", - thread: "Thread execution:", - cas: "Content-addressable storage:", - init: "Development:", - setup: "Configuration:", -}; - -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, - 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 namePart = cmd.name === "" ? "" : ` ${cmd.name}`; - const args = cmd.args ? ` ${cmd.args}` : ""; - return { - prefix: `${group.name}${namePart}${args}`, - description: cmd.description, - }; - }); - lines.push(...formatUsageCommandLines(rows)); - lines.push(""); - } - - lines.push("Shortcuts:"); - lines.push( - ...formatUsageCommandLines([ - { prefix: "run [...]", description: "→ thread run" }, - { prefix: "live [...]", description: "→ thread live" }, - ]), - ); - lines.push(""); - - lines.push("Gateway:"); - lines.push( - ...formatUsageCommandLines([ - { - prefix: "connect [--name NAME] [--gateway URL]", - description: "Connect to workflow gateway via WebSocket", - }, - ]), - ); - 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 --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"); -} diff --git a/legacy-packages/cli-workflow/src/cli.ts b/legacy-packages/cli-workflow/src/cli.ts deleted file mode 100755 index 120ad9e..0000000 --- a/legacy-packages/cli-workflow/src/cli.ts +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bun - -import { runCli } from "./cli-dispatch.js"; -import { resolveWorkflowStorageRoot } from "./storage-env.js"; - -const argv = process.argv.slice(2); -const storageRoot = resolveWorkflowStorageRoot(); -const code = await runCli(storageRoot, argv); -process.exit(code); diff --git a/legacy-packages/cli-workflow/src/commands/cas/dispatch.ts b/legacy-packages/cli-workflow/src/commands/cas/dispatch.ts deleted file mode 100644 index 0fcf1f5..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/dispatch.ts +++ /dev/null @@ -1,125 +0,0 @@ -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 { - 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 { - const hash = rest[0]; - if (hash === undefined || rest.length > 1) { - printCliError(`${usageText()}\n\nerror: cas get requires `); - 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 { - const content = rest[0]; - if (content === undefined || rest.length > 1) { - printCliError(`${usageText()}\n\nerror: cas put requires `); - 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 { - 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 { - const hash = rest[0]; - if (hash === undefined || rest.length > 1) { - printCliError(`${usageText()}\n\nerror: cas rm requires `); - 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 = { - get: { - handler: dispatchCasGet, - args: "", - description: "Retrieve content by hash from CAS", - }, - put: { - handler: dispatchCasPut, - args: "", - description: "Store content in CAS, prints hash", - }, - list: { - handler: dispatchCasList, - args: "", - description: "List all hashes in CAS", - }, - rm: { handler: dispatchCasRm, args: "", 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 { - 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; - }; -} diff --git a/legacy-packages/cli-workflow/src/commands/cas/gc.ts b/legacy-packages/cli-workflow/src/commands/cas/gc.ts deleted file mode 100644 index 5bdc462..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/gc.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type GcResult, garbageCollectCas } from "@uncaged/workflow-execute"; -import type { Result } from "@uncaged/workflow-protocol"; - -export async function cmdGc(storageRoot: string): Promise> { - return garbageCollectCas(storageRoot); -} diff --git a/legacy-packages/cli-workflow/src/commands/cas/get.ts b/legacy-packages/cli-workflow/src/commands/cas/get.ts deleted file mode 100644 index 5e34f3a..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/get.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createCasStore } from "@uncaged/workflow-cas"; -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -export async function cmdCasGet( - storageRoot: string, - hash: string, -): Promise> { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const content = await cas.get(hash); - if (content === null) { - return err(`cas entry not found: ${hash}`); - } - return ok(content); -} diff --git a/legacy-packages/cli-workflow/src/commands/cas/index.ts b/legacy-packages/cli-workflow/src/commands/cas/index.ts deleted file mode 100644 index f06dd18..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -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"; diff --git a/legacy-packages/cli-workflow/src/commands/cas/list.ts b/legacy-packages/cli-workflow/src/commands/cas/list.ts deleted file mode 100644 index c19fbec..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/list.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createCasStore } from "@uncaged/workflow-cas"; -import { ok, type Result } from "@uncaged/workflow-protocol"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -export async function cmdCasList(storageRoot: string): Promise> { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const hashes = await cas.list(); - return ok(hashes); -} diff --git a/legacy-packages/cli-workflow/src/commands/cas/put.ts b/legacy-packages/cli-workflow/src/commands/cas/put.ts deleted file mode 100644 index 2977c8e..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/put.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createCasStore } from "@uncaged/workflow-cas"; -import { ok, type Result } from "@uncaged/workflow-protocol"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -export async function cmdCasPut( - storageRoot: string, - content: string, -): Promise> { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const hash = await cas.put(content); - return ok(hash); -} diff --git a/legacy-packages/cli-workflow/src/commands/cas/rm.ts b/legacy-packages/cli-workflow/src/commands/cas/rm.ts deleted file mode 100644 index bef1d7f..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/rm.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createCasStore } from "@uncaged/workflow-cas"; -import { ok, type Result } from "@uncaged/workflow-protocol"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -export async function cmdCasRm(storageRoot: string, hash: string): Promise> { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - await cas.delete(hash); - return ok(undefined); -} diff --git a/legacy-packages/cli-workflow/src/commands/cas/types.ts b/legacy-packages/cli-workflow/src/commands/cas/types.ts deleted file mode 100644 index cb6e65b..0000000 --- a/legacy-packages/cli-workflow/src/commands/cas/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { DispatchGroupFn } from "../../cli-command-types.js"; - -export type CasDispatchDeps = { - dispatchGroup: DispatchGroupFn; -}; diff --git a/legacy-packages/cli-workflow/src/commands/connect/app.ts b/legacy-packages/cli-workflow/src/commands/connect/app.ts deleted file mode 100644 index 6bfb434..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/app.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Hono } from "hono"; -import { cors } from "hono/cors"; - -import { createCasRoutes } from "./routes-cas.js"; -import { createLiveRoutes } from "./routes-live.js"; -import { createThreadRoutes } from "./routes-thread.js"; -import { createWorkflowRoutes } from "./routes-workflow.js"; - -const MAX_BODY_SIZE = 1_048_576; // 1 MB - -export function createApp(storageRoot: string, clientToken: string | null): Hono { - const app = new Hono(); - - app.onError((_err, c) => { - return c.json({ error: "Internal server error" }, 500); - }); - - app.use( - "*", - cors({ - origin: [ - "http://localhost:5173", - "http://127.0.0.1:5173", - "http://localhost:7860", - "http://127.0.0.1:7860", - ], - }), - ); - - app.use("*", async (c, next) => { - if (c.req.method === "POST") { - const contentLength = c.req.header("content-length"); - if (contentLength !== undefined && Number(contentLength) > MAX_BODY_SIZE) { - return c.json({ error: "Payload too large" }, 413); - } - } - await next(); - }); - - // ── Client token auth (skip healthz) ─────────────────────────────── - if (clientToken !== null) { - app.use("/api/*", async (c, next) => { - const token = c.req.header("X-Client-Token"); - if (token !== clientToken) { - return c.json({ error: "unauthorized" }, 401); - } - await next(); - }); - } - - app.get("/healthz", (c) => c.json({ ok: true })); - app.get("/api/healthz", (c) => c.json({ ok: true })); - - app.route("/api/workflows", createWorkflowRoutes(storageRoot)); - app.route("/api/threads", createThreadRoutes(storageRoot)); - app.route("/api/threads", createLiveRoutes(storageRoot)); - app.route("/api/cas", createCasRoutes(storageRoot)); - - return app; -} diff --git a/legacy-packages/cli-workflow/src/commands/connect/connect.ts b/legacy-packages/cli-workflow/src/commands/connect/connect.ts deleted file mode 100644 index 478393e..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/connect.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { hostname as osHostname } from "node:os"; -import { ok, type Result } from "@uncaged/workflow-protocol"; -import { createLogger } from "@uncaged/workflow-util"; - -import { printCliLine } from "../../cli-output.js"; -import { createApp } from "./app.js"; -import { registerWithGateway, startHeartbeat, unregisterFromGateway } from "./gateway.js"; -import type { ConnectOptions } from "./types.js"; -import { startGatewayWsClient } from "./ws-client.js"; - -const DEFAULT_GATEWAY_URL = "https://workflow-gateway.shazhou.workers.dev"; -const HEARTBEAT_INTERVAL_MS = 60_000; - -function requireNextArg(argv: string[], i: number, flag: string): Result { - const next = argv[i + 1]; - if (next === undefined) { - return { ok: false, error: `${flag} requires a value` }; - } - return ok(next); -} - -function parseConnectArgv(argv: string[]): Result { - let name = osHostname().split(".")[0].toLowerCase(); - let gatewayUrl = DEFAULT_GATEWAY_URL; - const gatewaySecret = process.env.WORKFLOW_DASHBOARD_SECRET ?? ""; - const stringFlags: Record void> = { - "--name": (v) => { - name = v; - }, - "--gateway": (v) => { - gatewayUrl = v; - }, - }; - - for (let i = 0; i < argv.length; i++) { - const arg = argv[i]; - if (arg in stringFlags) { - const r = requireNextArg(argv, i, arg); - if (!r.ok) return r; - stringFlags[arg](r.value); - i++; - } - } - - return ok({ name, gatewayUrl, gatewaySecret }); -} - -export async function dispatchConnect(storageRoot: string, argv: string[]): Promise { - const parsed = parseConnectArgv(argv); - if (!parsed.ok) { - printCliLine(`error: ${parsed.error}`); - return 1; - } - - const options = parsed.value; - - if (options.gatewaySecret === "") { - printCliLine("error: WORKFLOW_DASHBOARD_SECRET is required"); - return 1; - } - - const clientToken = randomUUID(); - const app = createApp(storageRoot, clientToken); - - const log = createLogger({ sink: { kind: "stderr" } }); - const stopWsClient = startGatewayWsClient({ - gatewayUrl: options.gatewayUrl, - name: options.name, - secret: options.gatewaySecret, - appFetch: app.fetch, - log, - }); - - printCliLine("connected to gateway via WebSocket"); - - // Register with gateway for discovery - const registered = await registerWithGateway( - options.gatewayUrl, - options.name, - `ws://${options.name}`, - options.gatewaySecret, - clientToken, - ); - if (registered) { - printCliLine(`registered with gateway as "${options.name}"`); - } - - const heartbeatTimer = startHeartbeat( - options.gatewayUrl, - options.name, - `ws://${options.name}`, - options.gatewaySecret, - clientToken, - HEARTBEAT_INTERVAL_MS, - ); - - const cleanup = async () => { - clearInterval(heartbeatTimer); - stopWsClient(); - printCliLine("unregistering from gateway..."); - await unregisterFromGateway(options.gatewayUrl, options.name, options.gatewaySecret); - process.exit(0); - }; - - process.on("SIGINT", cleanup); - process.on("SIGTERM", cleanup); - - await new Promise(() => {}); - return 0; -} diff --git a/legacy-packages/cli-workflow/src/commands/connect/gateway.ts b/legacy-packages/cli-workflow/src/commands/connect/gateway.ts deleted file mode 100644 index 3d8b34b..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/gateway.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { printCliLine } from "../../cli-output.js"; - -export async function registerWithGateway( - gatewayUrl: string, - name: string, - localUrl: string, - secret: string, - clientToken: string, -): Promise { - try { - const resp = await fetch(`${gatewayUrl}/api/gateway/register`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, url: localUrl, secret, clientToken }), - }); - if (!resp.ok) { - const body = await resp.text(); - printCliLine(`gateway registration failed: ${resp.status} ${body}`); - return false; - } - return true; - } catch (e) { - printCliLine(`gateway registration error: ${e}`); - return false; - } -} - -export async function unregisterFromGateway( - gatewayUrl: string, - name: string, - secret: string, -): Promise { - try { - await fetch(`${gatewayUrl}/api/gateway/register/${name}`, { - method: "DELETE", - headers: { Authorization: `Bearer ${secret}` }, - }); - } catch { - // Best effort — process is exiting - } -} - -export function startHeartbeat( - gatewayUrl: string, - name: string, - localUrl: string, - secret: string, - clientToken: string, - intervalMs: number, -): ReturnType { - return setInterval(() => { - registerWithGateway(gatewayUrl, name, localUrl, secret, clientToken).catch(() => {}); - }, intervalMs); -} diff --git a/legacy-packages/cli-workflow/src/commands/connect/index.ts b/legacy-packages/cli-workflow/src/commands/connect/index.ts deleted file mode 100644 index 0b00564..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { dispatchConnect } from "./connect.js"; -export type { ConnectOptions } from "./types.js"; diff --git a/legacy-packages/cli-workflow/src/commands/connect/routes-cas.ts b/legacy-packages/cli-workflow/src/commands/connect/routes-cas.ts deleted file mode 100644 index bb851d7..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/routes-cas.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createCasStore } from "@uncaged/workflow-cas"; -import { garbageCollectCas } from "@uncaged/workflow-execute"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; -import { Hono } from "hono"; - -export function createCasRoutes(storageRoot: string): Hono { - const app = new Hono(); - const casDir = getGlobalCasDir(storageRoot); - const cas = createCasStore(casDir); - - app.get("/", async (c) => { - const hashes = await cas.list(); - return c.json({ hashes }); - }); - - app.get("/:hash", async (c) => { - const content = await cas.get(c.req.param("hash")); - if (content === null) { - return c.json({ error: "not found" }, 404); - } - return c.json({ hash: c.req.param("hash"), content }); - }); - - app.post("/", async (c) => { - let body: { content: string }; - try { - body = (await c.req.json()) as { content: string }; - } catch { - return c.json({ error: "invalid JSON body" }, 400); - } - if (typeof body.content !== "string") { - return c.json({ error: "content field required" }, 400); - } - const hash = await cas.put(body.content); - return c.json({ hash }, 201); - }); - - app.delete("/:hash", async (c) => { - const hash = c.req.param("hash"); - const content = await cas.get(hash); - if (content === null) { - return c.json({ error: "not found" }, 404); - } - await cas.delete(hash); - return c.json({ ok: true }); - }); - - app.post("/gc", async (c) => { - const result = await garbageCollectCas(storageRoot); - if (!result.ok) { - return c.json({ error: result.error }, 500); - } - return c.json(result.value); - }); - - return app; -} diff --git a/legacy-packages/cli-workflow/src/commands/connect/routes-live.ts b/legacy-packages/cli-workflow/src/commands/connect/routes-live.ts deleted file mode 100644 index 6321ec0..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/routes-live.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { existsSync, statSync, watch } from "node:fs"; -import { join } from "node:path"; -import { createCasStore, getContentMerklePayload } from "@uncaged/workflow-cas"; -import { - FORK_BRANCH_ROLE, - readThreadsIndex, - type ThreadIndex, - walkStateFramesNewestFirst, -} from "@uncaged/workflow-execute"; -import { END } from "@uncaged/workflow-runtime"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; -import { Hono } from "hono"; -import { streamSSE } from "hono/streaming"; - -import { resolveThreadRecord } from "../../thread-scan.js"; - -type PumpState = { - contentOffset: number; - carry: string; -}; - -function fileSize(path: string): number { - try { - return statSync(path).size; - } catch { - return 0; - } -} - -async function readNewBytes(path: string, state: PumpState): Promise { - const size = fileSize(path); - if (size < state.contentOffset) { - state.contentOffset = 0; - state.carry = ""; - } - if (size <= state.contentOffset) { - return null; - } - const blob = Bun.file(path).slice(state.contentOffset, size); - const chunk = await blob.text(); - state.contentOffset = size; - return chunk; -} - -function parseJsonLine(line: string): unknown { - try { - return JSON.parse(line) as unknown; - } catch { - return { raw: line }; - } -} - -function parseNewLines(chunk: string, state: PumpState): string[] { - state.carry += chunk; - - const parts = state.carry.split("\n"); - state.carry = parts.pop() ?? ""; - - const lines: string[] = []; - for (const line of parts) { - const trimmed = line.trim(); - if (trimmed !== "") { - lines.push(trimmed); - } - } - return lines; -} - -type CasSseState = { - printedHashes: Set; - lastHead: string | null; - completionEmitted: boolean; -}; - -type LiveSseStream = { - writeSSE: (opts: { event: string; data: string; id: string }) => Promise; -}; - -function completionFromEndMeta(meta: Record): { - returnCode: number; - summary: string; -} | null { - const returnCode = meta.returnCode; - const summary = meta.summary; - if (typeof returnCode !== "number" || typeof summary !== "string") { - return null; - } - return { returnCode, summary }; -} - -async function emitRecordsForHead(params: { - storageRoot: string; - bundleDir: string; - threadId: string; - headHash: string; - sseState: CasSseState; - stream: LiveSseStream; - eventId: { n: number }; -}): Promise { - const cas = createCasStore(getGlobalCasDir(params.storageRoot)); - const frames = await walkStateFramesNewestFirst(cas, params.headHash); - const chronological = [...frames].reverse(); - - for (const fr of chronological) { - if (params.sseState.printedHashes.has(fr.hash)) { - continue; - } - params.sseState.printedHashes.add(fr.hash); - - const role = fr.payload.role; - if (role === FORK_BRANCH_ROLE) { - continue; - } - - if (role === END) { - const wf = completionFromEndMeta(fr.payload.meta); - if (wf !== null) { - params.eventId.n++; - await params.stream.writeSSE({ - event: "record", - data: JSON.stringify({ - type: "workflow-result", - returnCode: wf.returnCode, - content: wf.summary, - timestamp: null, - }), - id: String(params.eventId.n), - }); - return true; - } - continue; - } - - const payloadText = await getContentMerklePayload(cas, fr.payload.content); - const content = - payloadText !== null - ? payloadText - : `(content not in CAS; contentHash=${fr.payload.content})`; - - params.eventId.n++; - await params.stream.writeSSE({ - event: "record", - data: JSON.stringify({ - type: "role", - role: fr.payload.role, - contentHash: fr.payload.content, - content, - meta: fr.payload.meta, - timestamp: fr.payload.timestamp, - }), - id: String(params.eventId.n), - }); - } - - return false; -} - -async function pumpThreadsJsonSse(params: { - storageRoot: string; - bundleDir: string; - threadId: string; - sseState: CasSseState; - stream: LiveSseStream; - eventId: { n: number }; -}): Promise { - let idx: ThreadIndex; - try { - idx = await readThreadsIndex(params.bundleDir); - } catch { - idx = {}; - } - - const active = idx[params.threadId]; - - if (active === undefined) { - if (params.sseState.completionEmitted) { - return false; - } - const hist = await resolveThreadRecord(params.storageRoot, params.threadId); - if (hist === null || hist.source !== "history") { - return false; - } - params.sseState.completionEmitted = true; - return await emitRecordsForHead({ - storageRoot: params.storageRoot, - bundleDir: params.bundleDir, - threadId: params.threadId, - headHash: hist.head, - sseState: params.sseState, - stream: params.stream, - eventId: params.eventId, - }); - } - - const head = active.head; - if (params.sseState.lastHead === null) { - params.sseState.lastHead = head; - return await emitRecordsForHead({ - storageRoot: params.storageRoot, - bundleDir: params.bundleDir, - threadId: params.threadId, - headHash: head, - sseState: params.sseState, - stream: params.stream, - eventId: params.eventId, - }); - } - - if (head !== params.sseState.lastHead) { - params.sseState.lastHead = head; - return await emitRecordsForHead({ - storageRoot: params.storageRoot, - bundleDir: params.bundleDir, - threadId: params.threadId, - headHash: head, - sseState: params.sseState, - stream: params.stream, - eventId: params.eventId, - }); - } - - return false; -} - -export function createLiveRoutes(storageRoot: string): Hono { - const app = new Hono(); - - app.get("/:threadId/live", async (c) => { - const threadId = c.req.param("threadId"); - const resolved = await resolveThreadRecord(storageRoot, threadId); - if (resolved === null) { - return c.json({ error: `thread not found: ${threadId}` }, 404); - } - - const threadTarget = resolved; - const threadsJsonPath = join(threadTarget.bundleDir, "threads.json"); - const infoPath = join(storageRoot, "logs", threadTarget.bundleHash, `${threadId}.info.jsonl`); - - return streamSSE(c, async (stream) => { - const infoState: PumpState = { contentOffset: 0, carry: "" }; - const sseThreadState: CasSseState = { - printedHashes: new Set(), - lastHead: null, - completionEmitted: false, - }; - const eventId = { n: 0 }; - - async function pumpData(): Promise { - const finished = await pumpThreadsJsonSse({ - storageRoot, - bundleDir: threadTarget.bundleDir, - threadId, - sseState: sseThreadState, - stream, - eventId, - }); - return finished; - } - - // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: SSE newline framing mirrors legacy pump - async function pumpInfo(): Promise { - let chunk: string | null; - try { - chunk = await readNewBytes(infoPath, infoState); - } catch { - return; - } - if (chunk === null) { - return; - } - - const lines = parseNewLines(chunk, infoState); - for (const line of lines) { - const record = parseJsonLine(line); - if ( - typeof record === "object" && - record !== null && - "raw" in (record as Record) - ) { - continue; - } - eventId.n++; - await stream.writeSSE({ - event: "info", - data: JSON.stringify(record), - id: String(eventId.n), - }); - } - } - - eventId.n++; - await stream.writeSSE({ - event: "record", - data: JSON.stringify({ - type: "thread-start", - threadId: threadTarget.threadId, - bundleHash: threadTarget.bundleHash, - head: threadTarget.head, - start: threadTarget.start, - source: threadTarget.source, - }), - id: String(eventId.n), - }); - - const done = await pumpData(); - try { - await pumpInfo(); - } catch { - // optional info file - } - if (done) { - return; - } - - // If thread is not actively running, emit all records and close — don't keep SSE open - const runningPath = join(storageRoot, "logs", threadTarget.bundleHash, `${threadId}.running`); - if (!existsSync(runningPath)) { - eventId.n++; - await stream.writeSSE({ - event: "done", - data: JSON.stringify({ reason: "not-running" }), - id: String(eventId.n), - }); - return; - } - - const controller = new AbortController(); - let completed = false; - - const threadsJsonWatcher = watch(threadsJsonPath, async () => { - if (completed) { - return; - } - const finished = await pumpData(); - if (finished) { - completed = true; - controller.abort(); - } - }); - - let infoWatcher: ReturnType | null = null; - try { - infoWatcher = watch(infoPath, async () => { - if (completed) { - return; - } - await pumpInfo(); - }); - } catch { - // info file may not exist - } - - stream.onAbort(() => { - completed = true; - threadsJsonWatcher.close(); - infoWatcher?.close(); - }); - - await new Promise((resolve) => { - if (completed) { - resolve(); - return; - } - controller.signal.addEventListener("abort", () => resolve(), { once: true }); - stream.onAbort(() => resolve()); - }); - - threadsJsonWatcher.close(); - infoWatcher?.close(); - }); - }); - - return app; -} diff --git a/legacy-packages/cli-workflow/src/commands/connect/routes-thread.ts b/legacy-packages/cli-workflow/src/commands/connect/routes-thread.ts deleted file mode 100644 index c8879ee..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/routes-thread.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { join } from "node:path"; -import { createCasStore, getContentMerklePayload, parseCasThreadNode } from "@uncaged/workflow-cas"; -import { FORK_BRANCH_ROLE, walkStateFramesNewestFirst } from "@uncaged/workflow-execute"; -import { END } from "@uncaged/workflow-runtime"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; -import { Hono } from "hono"; - -import { pathExists } from "../../fs-utils.js"; -import type { HistoricalThreadRow, ResolvedThreadRecord } from "../../thread-scan.js"; -import { - listHistoricalThreads, - listRunningThreads, - resolveThreadListStatus, - resolveThreadRecord, -} from "../../thread-scan.js"; -import { cmdKill, cmdPause, cmdResume } from "../thread/control.js"; -import { cmdRun } from "../thread/run.js"; - -async function readStartInfo( - cas: ReturnType, - startHash: string, -): Promise<{ name: string | null; prompt: string | null }> { - const raw = await cas.get(startHash); - if (raw === null) return { name: null, prompt: null }; - const parsed = parseCasThreadNode(raw); - if (parsed === null || parsed.kind !== "start") return { name: null, prompt: null }; - const name = parsed.node.payload.name; - const promptHash = parsed.node.refs[0] ?? null; - let prompt: string | null = null; - if (promptHash !== null) { - prompt = await getContentMerklePayload(cas, promptHash); - } - return { name, prompt }; -} - -async function buildThreadDetailRecords( - storageRoot: string, - resolved: ResolvedThreadRecord, - runningMarkerPresent: boolean, - statusRow: HistoricalThreadRow, -): Promise { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const frames = await walkStateFramesNewestFirst(cas, resolved.head); - const chronological = [...frames].reverse(); - - const { name: workflowName, prompt } = await readStartInfo(cas, resolved.start); - - const status = await resolveThreadListStatus(storageRoot, statusRow, runningMarkerPresent); - - const records: unknown[] = [ - { - type: "thread-start", - workflow: workflowName ?? "unknown", - prompt: prompt ?? null, - threadId: resolved.threadId, - status, - timestamp: null, - }, - ]; - - for (const fr of chronological) { - if (fr.payload.role === FORK_BRANCH_ROLE) { - continue; - } - if (fr.payload.role === END) { - const returnCode = fr.payload.meta.returnCode; - const summary = fr.payload.meta.summary; - if (typeof returnCode === "number" && typeof summary === "string") { - records.push({ - type: "workflow-result", - returnCode, - content: summary, - timestamp: fr.payload.timestamp, - }); - } - continue; - } - const payloadText = await getContentMerklePayload(cas, fr.payload.content); - const content = - payloadText !== null - ? payloadText - : `(content not in CAS; contentHash=${fr.payload.content})`; - records.push({ - type: "role", - role: fr.payload.role, - contentHash: fr.payload.content, - content, - meta: fr.payload.meta, - timestamp: fr.payload.timestamp, - }); - } - - return records; -} - -export function createThreadRoutes(storageRoot: string): Hono { - const app = new Hono(); - - app.get("/", async (c) => { - const nameFilter = c.req.query("workflow") ?? null; - const rows = await listHistoricalThreads(storageRoot, nameFilter); - const threads = await Promise.all( - rows.map(async (r) => { - const runningPath = join(storageRoot, "logs", r.hash, `${r.threadId}.running`); - const runningMarkerPresent = await pathExists(runningPath); - const status = await resolveThreadListStatus(storageRoot, r, runningMarkerPresent); - return { - threadId: r.threadId, - workflow: r.workflowName, - hash: r.hash, - startedAt: new Date(r.activityTs).toISOString(), - status, - }; - }), - ); - return c.json({ threads }); - }); - - app.get("/running", async (c) => { - const rows = await listRunningThreads(storageRoot); - return c.json({ threads: rows }); - }); - - app.get("/:threadId", async (c) => { - const threadId = c.req.param("threadId"); - const resolved = await resolveThreadRecord(storageRoot, threadId); - if (resolved === null) { - return c.json({ error: `thread not found: ${threadId}` }, 404); - } - const runningPath = join(storageRoot, "logs", resolved.bundleHash, `${threadId}.running`); - const runningMarkerPresent = await pathExists(runningPath); - const statusRow = { - threadId: resolved.threadId, - hash: resolved.bundleHash, - workflowName: null, - source: resolved.source, - activityTs: 0, - head: resolved.head, - }; - const records = await buildThreadDetailRecords( - storageRoot, - resolved, - runningMarkerPresent, - statusRow, - ); - return c.json({ threadId, records }); - }); - - app.post("/", async (c) => { - let body: Record; - try { - body = (await c.req.json()) as Record; - } catch { - return c.json({ error: "invalid JSON body" }, 400); - } - - const name = body.workflow; - const prompt = body.prompt; - - if (typeof name !== "string" || typeof prompt !== "string") { - return c.json({ error: "workflow (string) and prompt (string) are required" }, 400); - } - - const result = await cmdRun(storageRoot, name, prompt); - if (!result.ok) { - return c.json({ error: result.error }, 400); - } - return c.json({ threadId: result.value.threadId }, 201); - }); - - app.post("/:threadId/kill", async (c) => { - const threadId = c.req.param("threadId"); - const result = await cmdKill(storageRoot, threadId); - if (!result.ok) { - return c.json({ error: result.error }, 400); - } - return c.json({ ok: true }); - }); - - app.post("/:threadId/pause", async (c) => { - const threadId = c.req.param("threadId"); - const result = await cmdPause(storageRoot, threadId); - if (!result.ok) { - return c.json({ error: result.error }, 400); - } - return c.json({ ok: true }); - }); - - app.post("/:threadId/resume", async (c) => { - const threadId = c.req.param("threadId"); - const result = await cmdResume(storageRoot, threadId); - if (!result.ok) { - return c.json({ error: result.error }, 400); - } - return c.json({ ok: true }); - }); - - return app; -} diff --git a/legacy-packages/cli-workflow/src/commands/connect/routes-workflow.ts b/legacy-packages/cli-workflow/src/commands/connect/routes-workflow.ts deleted file mode 100644 index 34c91b2..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/routes-workflow.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; -import type { WorkflowDescriptor } from "@uncaged/workflow-protocol"; -import { - getRegisteredWorkflow, - listRegisteredWorkflowNames, - readWorkflowRegistry, - validateWorkflowDescriptor, -} from "@uncaged/workflow-register"; -import { Hono } from "hono"; -import { parse as parseYaml } from "yaml"; - -export function createWorkflowRoutes(storageRoot: string): Hono { - const app = new Hono(); - - app.get("/", async (c) => { - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return c.json({ error: reg.error.message }, 500); - } - const names = listRegisteredWorkflowNames(reg.value); - const workflows = names.map((name) => { - const entry = reg.value.workflows[name]; - return { - name, - hash: entry?.hash ?? null, - timestamp: entry?.timestamp ?? null, - }; - }); - return c.json({ workflows }); - }); - - app.get("/:name", async (c) => { - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return c.json({ error: reg.error.message }, 500); - } - const name = c.req.param("name"); - const entry = getRegisteredWorkflow(reg.value, name); - if (entry === null) { - return c.json({ error: `workflow not found: ${name}` }, 404); - } - let descriptor: WorkflowDescriptor | null = null; - try { - const yamlPath = join(storageRoot, "bundles", `${entry.hash}.yaml`); - const yamlText = await readFile(yamlPath, "utf8"); - const parsed: unknown = parseYaml(yamlText); - const validated = validateWorkflowDescriptor(parsed); - descriptor = validated.ok ? validated.value : null; - } catch { - descriptor = null; - } - return c.json({ name, ...entry, descriptor }); - }); - - app.get("/:name/history", async (c) => { - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return c.json({ error: reg.error.message }, 500); - } - const name = c.req.param("name"); - const entry = getRegisteredWorkflow(reg.value, name); - if (entry === null) { - return c.json({ error: `workflow not found: ${name}` }, 404); - } - return c.json({ name, history: entry.history }); - }); - - return app; -} diff --git a/legacy-packages/cli-workflow/src/commands/connect/types.ts b/legacy-packages/cli-workflow/src/commands/connect/types.ts deleted file mode 100644 index b5bc4dc..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ConnectOptions = { - name: string; - gatewayUrl: string; - gatewaySecret: string; -}; diff --git a/legacy-packages/cli-workflow/src/commands/connect/ws-client.ts b/legacy-packages/cli-workflow/src/commands/connect/ws-client.ts deleted file mode 100644 index aa59bb5..0000000 --- a/legacy-packages/cli-workflow/src/commands/connect/ws-client.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { parseWsRequestJson, type WsResponse } from "@uncaged/workflow-gateway/ws-protocol"; -import type { LogFn } from "@uncaged/workflow-util"; - -export type GatewayWsClientParams = { - gatewayUrl: string; - name: string; - secret: string; - appFetch: (request: Request) => Response | Promise; - log: LogFn; -}; - -const INITIAL_BACKOFF_MS = 1000; -const MAX_BACKOFF_MS = 30_000; - -export function buildGatewayWsConnectUrl(gatewayUrl: string, name: string, secret: string): string { - const u = new URL(gatewayUrl); - if (u.protocol === "https:") { - u.protocol = "wss:"; - } else if (u.protocol === "http:") { - u.protocol = "ws:"; - } - u.pathname = "/ws/connect"; - u.search = ""; - u.searchParams.set("name", name); - u.searchParams.set("secret", secret); - return u.href; -} - -function headersToRecord(h: Headers): Record { - const out: Record = {}; - for (const [k, v] of h) { - out[k] = v; - } - return out; -} - -async function handleGatewayMessage( - ws: WebSocket, - raw: string, - params: GatewayWsClientParams, -): Promise { - const req = parseWsRequestJson(raw); - if (req === null) { - params.log("ZM8K2PQ1", "gateway WebSocket dropped non-request message"); - return; - } - const localUrl = `http://localhost${req.path}`; - const headers = new Headers(req.headers); - let resp: Response; - try { - resp = await params.appFetch( - new Request(localUrl, { - method: req.method, - headers, - body: req.body === null ? undefined : req.body, - }), - ); - } catch (e) { - params.log("R4N7BQ3C", `app.fetch failed: ${String(e)}`); - const errBody: WsResponse = { - id: req.id, - status: 502, - headers: { "content-type": "application/json" }, - body: JSON.stringify({ error: "local fetch failed", detail: String(e) }), - }; - ws.send(JSON.stringify(errBody)); - return; - } - const bodyText = await resp.text(); - const headerRecord = headersToRecord(resp.headers); - const out: WsResponse = { - id: req.id, - status: resp.status, - headers: headerRecord, - body: bodyText, - }; - ws.send(JSON.stringify(out)); -} - -/** Maintains a reverse WebSocket to the workflow gateway; reconnects with exponential backoff. */ -export function startGatewayWsClient(params: GatewayWsClientParams): () => void { - const wsUrl = buildGatewayWsConnectUrl(params.gatewayUrl, params.name, params.secret); - let socket: WebSocket | null = null; - let reconnectTimer: ReturnType | null = null; - let stopped = false; - let attempt = 0; - - const clearReconnectTimer = (): void => { - if (reconnectTimer !== null) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - }; - - const scheduleReconnect = (): void => { - if (stopped) { - return; - } - clearReconnectTimer(); - const delayMs = Math.min(INITIAL_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS); - attempt++; - params.log("6CJX2R8P", `gateway WebSocket reconnect in ${delayMs}ms (attempt ${attempt})`); - reconnectTimer = setTimeout(connect, delayMs); - }; - - const connect = (): void => { - if (stopped) { - return; - } - clearReconnectTimer(); - params.log("2XK7HM9Q", "gateway WebSocket connecting..."); - try { - socket = new WebSocket(wsUrl); - } catch (e) { - params.log("7NQW4HBT", `gateway WebSocket create failed: ${String(e)}`); - scheduleReconnect(); - return; - } - - const ws = socket; - - ws.addEventListener("open", () => { - attempt = 0; - params.log("4PWN3V82", "gateway WebSocket connected"); - }); - - ws.addEventListener("close", (ev) => { - socket = null; - params.log( - "8QTR6ZKC", - `gateway WebSocket closed code=${String(ev.code)} reason=${ev.reason} wasClean=${String(ev.wasClean)}`, - ); - if (!stopped) { - scheduleReconnect(); - } - }); - - ws.addEventListener("error", () => { - params.log("9BWS1M7F", "gateway WebSocket error"); - }); - - ws.addEventListener("message", (ev) => { - const data = ev.data; - if (typeof data !== "string") { - params.log("T9W2K35H", "gateway WebSocket non-text frame ignored"); - return; - } - void handleGatewayMessage(ws, data, params).catch((e: unknown) => { - params.log("V7KX2M9P", `gateway WebSocket handler error: ${String(e)}`); - }); - }); - }; - - connect(); - - return (): void => { - stopped = true; - clearReconnectTimer(); - if (socket !== null && socket.readyState === WebSocket.OPEN) { - socket.close(1000, "shutdown"); - } - socket = null; - }; -} diff --git a/legacy-packages/cli-workflow/src/commands/init/dispatch.ts b/legacy-packages/cli-workflow/src/commands/init/dispatch.ts deleted file mode 100644 index 040c5f3..0000000 --- a/legacy-packages/cli-workflow/src/commands/init/dispatch.ts +++ /dev/null @@ -1,67 +0,0 @@ -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 { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: init workspace requires `); - 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 { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: init template requires `); - 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 = { - workspace: { - handler: dispatchInitWorkspace, - args: "", - description: "Initialize a new workflow workspace", - }, - template: { - handler: dispatchInitTemplate, - args: "", - description: "Initialize a new workflow template", - }, -}; - -export function createInitDispatcher(deps: InitDispatchDeps) { - const { dispatchGroup } = deps; - return async function dispatchInit(storageRoot: string, argv: string[]): Promise { - 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; - }; -} diff --git a/legacy-packages/cli-workflow/src/commands/init/index.ts b/legacy-packages/cli-workflow/src/commands/init/index.ts deleted file mode 100644 index b8138c0..0000000 --- a/legacy-packages/cli-workflow/src/commands/init/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -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"; diff --git a/legacy-packages/cli-workflow/src/commands/init/template.ts b/legacy-packages/cli-workflow/src/commands/init/template.ts deleted file mode 100644 index f3a6c38..0000000 --- a/legacy-packages/cli-workflow/src/commands/init/template.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; - -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import { pathExists } from "../../fs-utils.js"; - -import { - templateIndexTs, - templateModeratorTs, - templatePackageJson, - templateRolesTs, - templateTsconfigJson, -} from "./templates.js"; -import type { CmdInitTemplateSuccess } from "./types.js"; -import { validateWorkspaceSegment } from "./validate.js"; - -function hasTemplatesWorkspaceGlob(workspaces: unknown): boolean { - return Array.isArray(workspaces) && workspaces.includes("templates/*"); -} - -async function readPackageJsonWorkspaces(dir: string): Promise { - 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> { - 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> { - 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 }); -} diff --git a/legacy-packages/cli-workflow/src/commands/init/templates.ts b/legacy-packages/cli-workflow/src/commands/init/templates.ts deleted file mode 100644 index eb5ca9e..0000000 --- a/legacy-packages/cli-workflow/src/commands/init/templates.ts +++ /dev/null @@ -1,95 +0,0 @@ -export function templatePackageJson(templateName: string): string { - return `${JSON.stringify( - { - name: `template-${templateName}`, - version: "0.0.0", - private: true, - type: "module", - dependencies: { - "@uncaged/workflow-runtime": "^0.3.1", - 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-runtime"; -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 = { - description: "Says hello — replace with your first role.", - systemPrompt: "You are a helpful assistant. Reply with one short friendly sentence.", - schema: greeterMetaSchema, -}; -`; -} - -export function templateModeratorTs(): string { - return `import { END, START, type ModeratorTable } from "@uncaged/workflow-runtime"; - -import type { HelloTemplateMeta } from "./roles.js"; - -export const helloTemplateTable: ModeratorTable = { - [START]: [{ condition: "FALLBACK", role: "greeter" }], - greeter: [{ condition: "FALLBACK", role: END }], -}; -`; -} - -export function templateIndexTs(): string { - return `import type { WorkflowDefinition } from "@uncaged/workflow-runtime"; - -import { helloTemplateTable } from "./moderator.js"; -import { - HELLO_TEMPLATE_DESCRIPTION, - type HelloTemplateMeta, - greeterRole, -} from "./roles.js"; - -export { - HELLO_TEMPLATE_DESCRIPTION, - type HelloTemplateMeta, - greeterRole, -} from "./roles.js"; -export { helloTemplateTable } from "./moderator.js"; - -export const helloTemplateWorkflowDefinition: WorkflowDefinition = { - description: HELLO_TEMPLATE_DESCRIPTION, - roles: { - greeter: greeterRole, - }, - table: helloTemplateTable, -}; -`; -} diff --git a/legacy-packages/cli-workflow/src/commands/init/types.ts b/legacy-packages/cli-workflow/src/commands/init/types.ts deleted file mode 100644 index fba5959..0000000 --- a/legacy-packages/cli-workflow/src/commands/init/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { DispatchGroupFn } from "../../cli-command-types.js"; - -export type CmdInitTemplateSuccess = { - templatePath: string; -}; - -export type CmdInitWorkspaceSuccess = { - rootPath: string; -}; - -export type InitDispatchDeps = { - dispatchGroup: DispatchGroupFn; -}; diff --git a/legacy-packages/cli-workflow/src/commands/init/validate.ts b/legacy-packages/cli-workflow/src/commands/init/validate.ts deleted file mode 100644 index 55dbdb3..0000000 --- a/legacy-packages/cli-workflow/src/commands/init/validate.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -/** Validates a single path segment for workspace / template names (no separators, not `.` / `..`). */ -export function validateWorkspaceSegment(name: string): Result { - 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); -} diff --git a/legacy-packages/cli-workflow/src/commands/init/workspace.ts b/legacy-packages/cli-workflow/src/commands/init/workspace.ts deleted file mode 100644 index 317eb6e..0000000 --- a/legacy-packages/cli-workflow/src/commands/init/workspace.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { basename, join, resolve } from "node:path"; - -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import { pathExists } from "../../fs-utils.js"; -import type { CmdInitWorkspaceSuccess } from "./types.js"; - -function rootPackageJson(workspaceName: string): string { - return `${JSON.stringify( - { - name: workspaceName, - private: true, - type: "module", - workspaces: ["templates/*", "workflows"], - scripts: { - bundle: "bun run scripts/bundle.ts", - }, - }, - null, - 2, - )}\n`; -} - -function workflowsPackageJson(): string { - return `${JSON.stringify( - { - name: "workflows", - version: "0.0.0", - private: true, - type: "module", - dependencies: { - "@uncaged/workflow-runtime": "^0.3.1", - zod: "^4.0.0", - }, - }, - null, - 2, - )}\n`; -} - -function biomeJson(): string { - return `${JSON.stringify( - { - $schema: "https://biomejs.dev/schemas/2.4.14/schema.json", - files: { - // Exclude generated bundle script — it uses Bun globals and console that - // conflict with the workspace's Biome rules (noConsole, etc.). - includes: ["**", "!**/node_modules", "!**/dist", "!scripts/bundle.ts"], - }, - formatter: { - indentWidth: 2, - }, - linter: { - enabled: true, - rules: { - recommended: true, - }, - }, - }, - null, - 2, - )}\n`; -} - -function tsconfigJson(): string { - return `${JSON.stringify( - { - compilerOptions: { - strict: true, - target: "ESNext", - module: "ESNext", - moduleResolution: "Bundler", - skipLibCheck: true, - }, - }, - null, - 2, - )}\n`; -} - -function agentsMd(): string { - return `# AGENTS — Workflow 工作区开发指南 - -面向在本仓库中编写 workflow 的 coding agent。引擎层术语与架构细节与 **@uncaged/workflow** 上游文档一致,编写时可对照 \`CLAUDE.md\` 与 \`docs/architecture.md\`。 - -## 1. 项目结构(workspace / template / workflow instance) - -| 层级 | 目录 / 产物 | 职责 | -|------|----------------|------| -| **Workspace** | 仓库根(\`package.json\` 含 \`workspaces: ["templates/*", "workflows"]\`) | Bun monorepo:统一管理本地模板包与 workflow 实例 | -| **Template** | \`templates//\`(如 \`src/roles.ts\`、\`src/moderator.ts\`、\`src/index.ts\`) | 纯数据:**WorkflowDefinition**(各 **RoleDefinition** + **ModeratorTable**),**不绑定**具体 Agent | -| **Workflow instance** | \`workflows/\`(或单独包) | 把模板与运行时 **AdapterFn** / **ExtractFn** 组合,产出可注册的 **单文件 ESM bundle**(\`run\` + \`descriptor\` 命名导出) | - -Init 生成的骨架:\`templates/\` 下放可复用定义,\`workflows/\` 下放绑定与打包入口。 - -## 2. 核心概念 - -- **RoleMeta**:\`Record>\`,角色名 → 该角色结构化 meta 的形状约定。 -- **RoleDefinition**:纯数据——\`description\`、\`systemPrompt\`、\`schema\`(Zod v4)。不含执行逻辑。 -- **WorkflowDefinition**:\`description\` + \`roles\`(各角色定义)+ **ModeratorTable**(声明式路由表)。 -- **ModeratorTable**:从 \`START\` 与各角色名映射到有序 transition 列表(条件 + 下一角色或 \`END\`);可序列化,供描述符提取 **graph**。 -- **AdapterFn**:接收系统提示词与 Zod schema,返回角色执行函数(RoleFn)。 -- **ExtractFn**:从 CAS content hash 解析结构化数据(引擎与 Adapter 都可使用)。 - -引擎循环简述:按 **ModeratorTable** 选下一角色 → **Adapter** 产出 typed meta → 追加 step,重复直至 **END**。详见 \`docs/architecture.md\` 中的三阶段说明。 - -## 3. 开发流程 - -1. **定义 RoleMeta**:为每个角色约定 meta 的 TypeScript 类型(与 Zod schema 对齐)。 -2. **编写 RoleDefinition**:为每个角色写 Zod \`schema\`,补齐 \`systemPrompt\` / \`description\`。 -3. **编写 ModeratorTable**:为 \`START\` 与各角色声明 transition(\`FALLBACK\` 或命名条件 + \`check\`)。 -4. **组装 WorkflowDefinition**:在模板 \`index\` 中导出 definition(以及必要的角色 / table 导出)。 -5. **实例化**:在 workflow 包中使用 \`createWorkflow(def, binding)\`(或项目约定的封装)绑定 **AdapterFn**;**ExtractFn** 由引擎从 **workflow.yaml** 注入 \`WorkflowRuntime\`。 -6. **构建**:打包为单个 **.esm.js** bundle,使用 **uncaged-workflow add** 注册。 - -## 4. 编码规范 - -与 **CLAUDE.md** 对齐,摘要如下: - -- **Functional-first**:优先 \`function\` + \`type\`,避免面向对象业务模型。 -- **type 而非 interface**:类型别名一律用 \`type\`,不要使用 \`interface\`。 -- **显式可空**:不要用 \`?:\`;可空字段写成 \`T | null\`。 -- **function 而非 class**:不用 class(第三方库要求或 \`Error\` 子类除外)。 -- **Crockford Base32**:日志 tag、bundle hash、thread id 等标识约定(引擎侧);工作区内自定义日志若沿用引擎 logger,tag 为 8 字符 Crockford Base32,且每个调用点唯一。 -- **Named exports only**:不要使用 **default export**;workflow bundle 须 **export const run** 与 **export const descriptor**。 -- **No console.log**:库代码用结构化 logger;CLI 用户输出可按项目 Biome 规则例外标注。 -- **No dynamic import**:业务与 bundle 内禁止 \`import()\`;例外仅限「运行时路径由用户提供的 bundle 加载器」(引擎内部)。 - -## 5. Template 复用 - -- **已发布模板**:可通过 npm 依赖 \`@uncaged/workflow-template-*\` 等包,在 workflow 实例中 import 其 **WorkflowDefinition** 再绑定 Agent。 -- **本地模板**:放在本仓库 \`templates//\`,由 workspace 协议引用(如 \`"template-foo": "workspace:*"\` 或相对路径),便于同源修改与版本控制。 - -选择模板时保持 **definition 与 agent 绑定分离**:模板只描述「做什么、顺序如何」,实例决定「谁执行、如何抽取 meta」。 - -## 6. Build and Test - -日常命令: - -\`\`\`sh -bun install -bun run check # Biome:lint + format -bun test -bun build # 若包内配置了 build 脚本则用于产出 dist / bundle -uncaged-workflow add -\`\`\` - -提交前至少运行 **bun run check** 与 **bun test**;registry 与本地运行流参见 README 与 CLI 文档。 - -## 7. 常见陷阱 - -- **No dynamic import**:bundle 须静态可分析;动态 \`import()\` 会破坏哈希与加载约束。 -- **No default export**:引擎只接受命名导出 \`run\` / \`descriptor\`。 -- **No console.log**:避免在可被 Biome \`noConsole\` 规则覆盖的代码路径直接使用 console。 -- **Single-file ESM bundle**:交付物是单一 \`.esm.js\`;静态 import 仅限 Node 内置(见 architecture 文档中的 Bundle Contract)。 - ---- - -编写新 workflow 时,先对齐 **RoleMeta → RoleDefinition(Zod)→ ModeratorTable → 绑定 → 单文件 bundle**,再对照本节规范自检。 -`; -} - -function bunfigToml(): string { - return `[install.scopes] -"@uncaged" = "https://git.shazhou.work/api/packages/shazhou/npm/" -`; -} - -function readmeMd(workspaceName: string): string { - return `# ${workspaceName} - -Local workflow development workspace (Bun monorepo). - -## Layout - -- \`templates/\` — reusable workflow definition packages (roles + ModeratorTable), no agent binding -- \`workflows/\` — workflow instances that bind templates to agents and export \`run\` + \`descriptor\` - -## Commands - -\`\`\`sh -bun install -bun run check # after you add scripts / Biome -uncaged-workflow add -uncaged-workflow run -\`\`\` - -Create this skeleton with: - -\`\`\`sh -uncaged-workflow init workspace ${workspaceName} -\`\`\` -`; -} - -function bundleTs(): string { - return [ - 'import { mkdir, readdir, writeFile } from "node:fs/promises";', - 'import { join } from "node:path";', - "", - 'const rootDir = join(import.meta.dir, "..");', - 'const workflowsDir = join(rootDir, "workflows");', - 'const distDir = join(rootDir, "dist");', - "", - "function isEntryFile(name: string): boolean {", - ' return name.endsWith("-entry.ts");', - "}", - "", - "function entryStem(name: string): string {", - ' return name.slice(0, -".ts".length);', - "}", - "", - "async function main(): Promise {", - " await mkdir(distDir, { recursive: true });", - " let files: string[];", - " try {", - " files = await readdir(workflowsDir);", - " } catch {", - ' console.error("bundle: missing workflows/ directory");', - " process.exitCode = 1;", - " return;", - " }", - " const entries = files.filter(isEntryFile);", - " if (entries.length === 0) {", - ' console.warn("bundle: no *-entry.ts files under workflows/");', - " return;", - " }", - " for (const file of entries) {", - " const stem = entryStem(file);", - " const entryPath = join(workflowsDir, file);", - " const result = await Bun.build({", - " entrypoints: [entryPath],", - " outdir: distDir,", - ' format: "esm",', - ' target: "node",', - " splitting: false,", - ' naming: { entry: "[name].esm.js" },', - " });", - " if (!result.success) {", - " for (const log of result.logs) {", - " console.error(log);", - " }", - ` throw new Error(\`bundle failed for \${file}\`);`, - " }", - " const dts =", - ` 'export { run, descriptor } from "../workflows/' + stem + '.js";\\n';`, - ` await writeFile(join(distDir, \`\${stem}.d.ts\`), dts, "utf8");`, - ` console.log(\`bundle: \${stem} -> dist/\${stem}.esm.js\`);`, - " }", - "}", - "", - "await main();", - "", - ].join("\n"); -} - -export async function cmdInitWorkspace( - parentDir: string, - workspaceName: string, -): Promise> { - // Accept a relative/absolute path: resolve it and derive the dir name for package.json. - const resolved = resolve(parentDir, workspaceName); - const rootPath = resolved; - const dirName = basename(resolved); - - if (dirName === "" || dirName === "." || dirName === "..") { - return err(`invalid workspace path: ${workspaceName}`); - } - - if (await pathExists(rootPath)) { - return err(`directory already exists: ${rootPath}`); - } - - await mkdir(rootPath, { recursive: true }); - await mkdir(join(rootPath, "templates"), { recursive: true }); - await mkdir(join(rootPath, "workflows"), { recursive: true }); - await mkdir(join(rootPath, "scripts"), { recursive: true }); - - await Promise.all([ - writeFile(join(rootPath, "package.json"), rootPackageJson(dirName), "utf8"), - writeFile(join(rootPath, "biome.json"), biomeJson(), "utf8"), - writeFile(join(rootPath, "tsconfig.json"), tsconfigJson(), "utf8"), - writeFile(join(rootPath, "AGENTS.md"), agentsMd(), "utf8"), - writeFile(join(rootPath, "README.md"), readmeMd(dirName), "utf8"), - writeFile(join(rootPath, "templates", ".gitkeep"), "", "utf8"), - writeFile(join(rootPath, "workflows", "package.json"), workflowsPackageJson(), "utf8"), - writeFile(join(rootPath, "workflows", ".gitkeep"), "", "utf8"), - writeFile(join(rootPath, "bunfig.toml"), bunfigToml(), "utf8"), - writeFile(join(rootPath, "scripts", "bundle.ts"), bundleTs(), "utf8"), - ]); - - return ok({ rootPath }); -} diff --git a/legacy-packages/cli-workflow/src/commands/setup/dispatch.ts b/legacy-packages/cli-workflow/src/commands/setup/dispatch.ts deleted file mode 100644 index b2e16fb..0000000 --- a/legacy-packages/cli-workflow/src/commands/setup/dispatch.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { existsSync } from "node:fs"; -import { resolve as resolvePath } from "node:path"; -import { stdin as input, stdout as output } from "node:process"; -import { createInterface } from "node:readline/promises"; - -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import { createLogger } from "@uncaged/workflow-util"; - -import { printCliError, printCliLine, printCliWarn } from "../../cli-output.js"; - -const setupDispatchLog = createLogger({ sink: { kind: "stderr" } }); - -import { loadPresetProviders } from "./preset-providers.js"; -import { cmdSetup, printSetupSummary } from "./setup.js"; -import type { SetupCliArgs } from "./types.js"; - -type OpenAiModelEntry = { - id: string; -}; - -type OpenAiModelsResponse = { - data: OpenAiModelEntry[]; -}; - -function usageSetup(): string { - return [ - "uncaged-workflow setup — configure workflow.yaml providers and default model", - "", - "Non-interactive (agent mode):", - " uncaged-workflow setup \\", - " --provider \\", - " --base-url \\", - " --api-key \\", - " --default-model \\", - " [--init-workspace ]", - "", - "Interactive: run with no flags (prompts for each value).", - "", - "Storage: uses the same root as other commands (see UNCAGED_WORKFLOW_STORAGE_ROOT).", - ].join("\n"); -} - -function requireNext(argv: string[], i: number, flag: string): Result { - const next = argv[i + 1]; - if (next === undefined || next.startsWith("--")) { - return err(`${flag} requires a value`); - } - return ok(next); -} - -type ParsedSetup = SetupCliArgs | "interactive" | "help"; - -type SetupFlagField = "provider" | "baseUrl" | "apiKey" | "defaultModel" | "initWorkspaceName"; - -const SETUP_FLAG_TO_FIELD: Record = { - "--provider": "provider", - "--base-url": "baseUrl", - "--api-key": "apiKey", - "--default-model": "defaultModel", - "--init-workspace": "initWorkspaceName", -}; - -function emptyFlagState(): Record { - return { - provider: null, - baseUrl: null, - apiKey: null, - defaultModel: null, - initWorkspaceName: null, - }; -} - -function finalizeParsedSetup( - state: Record, -): Result { - const hasAnyFlag = - state.provider !== null || - state.baseUrl !== null || - state.apiKey !== null || - state.defaultModel !== null || - state.initWorkspaceName !== null; - - if (!hasAnyFlag) { - return ok("interactive"); - } - - if (state.provider === null) { - return err( - "non-interactive setup requires --provider (or omit all flags for interactive mode)", - ); - } - - const missing: string[] = []; - if (state.baseUrl === null) { - missing.push("--base-url"); - } - if (state.apiKey === null) { - missing.push("--api-key"); - } - if (state.defaultModel === null) { - missing.push("--default-model"); - } - if (missing.length > 0) { - return err(`missing required flag(s): ${missing.join(", ")}`); - } - - const b = state.baseUrl; - const k = state.apiKey; - const m = state.defaultModel; - if (b === null || k === null || m === null) { - return err("internal: missing required flags after validation"); - } - - return ok({ - provider: state.provider, - baseUrl: b, - apiKey: k, - defaultModel: m, - initWorkspaceName: state.initWorkspaceName, - }); -} - -function parseSetupArgv(argv: string[]): Result { - const state = emptyFlagState(); - - for (let i = 0; i < argv.length; i++) { - const tok = argv[i]; - if (tok === undefined) { - break; - } - if (tok === "--help" || tok === "-h") { - return ok("help"); - } - const field = SETUP_FLAG_TO_FIELD[tok]; - if (field === undefined) { - return err(`unknown argument: ${tok}`); - } - const v = requireNext(argv, i, tok); - if (!v.ok) { - return v; - } - state[field] = v.value; - i++; - } - - return finalizeParsedSetup(state); -} - -async function promptLine( - rl: { question: (q: string) => Promise }, - label: string, -): Promise { - const raw = await rl.question(label); - return raw.trim(); -} - -type SecretInputState = { - buf: string; - rawWasSet: boolean; - onData: (chunk: string) => void; - fulfill: (value: string) => void; -}; - -function isLineTerminator(c: string): boolean { - return c === "\n" || c === "\r" || c === "\u0004"; -} - -function handleLineTerminator(state: SecretInputState): void { - if (process.stdin.isTTY) { - process.stdin.setRawMode(state.rawWasSet); - } - process.stdin.pause(); - process.stdin.removeListener("data", state.onData); - process.stdout.write("\n"); - state.fulfill(state.buf.trim()); -} - -function handleBackspace(state: SecretInputState): void { - if (state.buf.length > 0) { - state.buf = state.buf.slice(0, -1); - process.stdout.write("\b \b"); - } -} - -function handleInterrupt(rawWasSet: boolean): void { - if (process.stdin.isTTY) { - process.stdin.setRawMode(rawWasSet); - } - process.exit(130); -} - -function isBackspace(c: string): boolean { - return c === "\u007F" || c === "\b"; -} - -/** Process a single character in secret input. Returns "done" to stop reading. */ -function processSecretChar(c: string, state: SecretInputState): "done" | "skip" | "append" { - if (isLineTerminator(c)) { - handleLineTerminator(state); - return "done"; - } - if (isBackspace(c)) { - handleBackspace(state); - return "skip"; - } - if (c === "\u0003") { - handleInterrupt(state.rawWasSet); - } - state.buf += c; - process.stdout.write("*"); - return "append"; -} - -/** Read a line with terminal echo disabled (for secrets). */ -async function promptSecret(label: string): Promise { - process.stdout.write(label); - return new Promise((fulfill) => { - const rawWasSet = process.stdin.isRaw; - if (process.stdin.isTTY) { - process.stdin.setRawMode(true); - } - process.stdin.resume(); - process.stdin.setEncoding("utf8"); - - const state: SecretInputState = { buf: "", rawWasSet, fulfill, onData: () => {} }; - - const onData = (chunk: string) => { - for (const c of chunk.toString()) { - if (processSecretChar(c, state) === "done") return; - } - }; - - state.onData = onData; - process.stdin.on("data", onData); - }); -} - -/** Fetch available models from an OpenAI-compatible /models endpoint. */ -async function fetchAvailableModels(baseUrl: string, apiKey: string): Promise { - const url = `${baseUrl.replace(/\/+$/, "")}/models`; - try { - const res = await fetch(url, { - headers: { Authorization: `Bearer ${apiKey}` }, - signal: AbortSignal.timeout(10_000), - }); - if (!res.ok) { - setupDispatchLog("R5KH7WM3", `GET ${url} returned ${res.status}`); - return []; - } - const body = (await res.json()) as OpenAiModelsResponse; - if (!Array.isArray(body.data)) { - return []; - } - // Filter out non-chat models. Some patterns are DashScope-specific (sambert, cosyvoice, - // wordart, wanx, wan2, paraformer) but harmless for other providers. - const NON_CHAT_RE = - /speech|embed|image|video|audio|ocr|rerank|tts|asr|paraformer|sambert|cosyvoice|wordart|wanx|wan2|flux|stable-diffusion|z-image|s2s|livetranslate|realtime|gui-/i; - return body.data - .map((m) => m.id) - .filter((id) => !NON_CHAT_RE.test(id)) - .sort(); - } catch (e) { - setupDispatchLog( - "V8NQ4JT6", - `fetch models failed: ${e instanceof Error ? e.message : String(e)}`, - ); - return []; - } -} - -type PresetProvider = ReturnType[number]; - -function printProviderMenu(presets: readonly PresetProvider[]): void { - const numWidth = String(presets.length + 1).length; - printCliLine("Select a provider:\n"); - for (let i = 0; i < presets.length; i++) { - const p = presets.at(i); - if (!p) continue; - const num = String(i + 1).padStart(numWidth); - printCliLine(` ${num}) ${p.label.padEnd(28)} ${p.baseUrl}`); - } - const customNum = String(presets.length + 1).padStart(numWidth); - printCliLine(` ${customNum}) Custom (enter name and URL manually)`); - printCliLine(""); -} - -async function selectProvider( - rl: { question: (q: string) => Promise }, - presets: readonly PresetProvider[], -): Promise> { - const choice = await promptLine(rl, `Choose [1-${presets.length + 1}]: `); - const choiceNum = Number.parseInt(choice, 10); - if (Number.isNaN(choiceNum) || choiceNum < 1 || choiceNum > presets.length + 1) { - return err(`invalid choice: ${choice}`); - } - - if (choiceNum <= presets.length) { - const selected = presets.at(choiceNum - 1); - if (!selected) return err(`invalid choice: ${choice}`); - printCliLine(`\n → ${selected.label} (${selected.baseUrl})\n`); - return ok({ provider: selected.name, baseUrl: selected.baseUrl }); - } - - const provider = await promptLine(rl, "Provider name (e.g. my-proxy): "); - if (provider === "") return err("provider name must not be empty"); - const baseUrl = await promptLine(rl, "OpenAI-compatible API base URL: "); - if (baseUrl === "") return err("base URL must not be empty"); - return ok({ provider, baseUrl }); -} - -function printModelList(models: string[]): void { - const cols = process.stdout.columns || 80; - const nw = String(models.length).length; - const prefixLen = nw + 4; - const maxModelLen = Math.max(...models.map((m) => m.length)); - const cellWidth = prefixLen + maxModelLen + 2; - const numCols = Math.max(1, Math.floor(cols / cellWidth)); - for (let i = 0; i < models.length; i += numCols) { - const cells: string[] = []; - for (let j = i; j < Math.min(i + numCols, models.length); j++) { - const num = String(j + 1).padStart(nw); - const model = models.at(j) ?? ""; - cells.push(` ${num}) ${model.padEnd(maxModelLen + 2)}`); - } - printCliLine(cells.join("")); - } -} - -async function selectModel( - rl: { question: (q: string) => Promise }, - models: string[], -): Promise> { - if (models.length > 0) { - printCliLine(`\nAvailable models (${models.length}):\n`); - printModelList(models); - printCliLine(`\nChoose a number, or type a model name directly.`); - const modelInput = await promptLine(rl, `Default model [1-${models.length}]: `); - if (modelInput === "") return err("default model must not be empty"); - const modelNum = Number.parseInt(modelInput, 10); - if (!Number.isNaN(modelNum) && modelNum >= 1 && modelNum <= models.length) { - return ok(models.at(modelNum - 1) ?? modelInput); - } - return ok(modelInput); - } - - printCliWarn("Could not fetch models (API may not support /models endpoint)."); - const modelInput = await promptLine(rl, `Default model (e.g. qwen-plus, gpt-4o): `); - if (modelInput === "") return err("default model must not be empty"); - return ok(modelInput); -} - -async function selectWorkspace(rl: { - question: (q: string) => Promise; -}): Promise { - while (true) { - const wsPath = await promptLine( - rl, - "\nWorkflow workspace path (default: ./workflows, type 'skip' to skip): ", - ); - if (wsPath.toLowerCase() === "skip") return null; - const candidate = wsPath === "" ? "./workflows" : wsPath; - const resolved = resolvePath(process.cwd(), candidate); - if (existsSync(resolved)) { - printCliWarn(`directory already exists: ${resolved}`); - printCliLine("Please enter a different path, or type 'skip' to skip."); - continue; - } - return candidate; - } -} - -function stripProviderPrefix(model: string): string { - if (model.includes("/")) { - return model.split("/").pop() ?? model; - } - return model; -} - -async function collectInteractiveSetup(): Promise> { - const rl = createInterface({ input, output }); - try { - printCliLine("Configure the LLM provider that workflow agents will use.\n"); - - const presets = loadPresetProviders(); - printProviderMenu(presets); - - const providerResult = await selectProvider(rl, presets); - if (!providerResult.ok) { - rl.close(); - return providerResult; - } - const { provider, baseUrl } = providerResult.value; - - rl.close(); - const apiKey = await promptSecret("API key for this provider: "); - if (apiKey === "") return err("API key must not be empty"); - const rl2 = createInterface({ input, output }); - - printCliLine("\nFetching available models..."); - const models = await fetchAvailableModels(baseUrl, apiKey); - const modelResult = await selectModel(rl2, models); - if (!modelResult.ok) { - rl2.close(); - return modelResult; - } - - const bare = stripProviderPrefix(modelResult.value); - const defaultModel = `${provider}/${bare}`; - printCliLine(` → ${defaultModel}`); - - const initWorkspaceName = await selectWorkspace(rl2); - rl2.close(); - - return ok({ provider, baseUrl, apiKey, defaultModel, initWorkspaceName }); - } catch (e) { - return err(e instanceof Error ? e.message : String(e)); - } -} - -export async function dispatchSetup(storageRoot: string, argv: string[]): Promise { - const parsed = parseSetupArgv(argv); - if (!parsed.ok) { - printCliError(`${parsed.error}\n\n${usageSetup()}`); - return 1; - } - if (parsed.value === "help") { - printCliLine(usageSetup()); - return 0; - } - - let args: SetupCliArgs; - if (parsed.value === "interactive") { - const collected = await collectInteractiveSetup(); - if (!collected.ok) { - printCliError(collected.error); - return 1; - } - args = collected.value; - } else { - args = parsed.value; - } - - const result = await cmdSetup(storageRoot, args); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printSetupSummary(result.value); - return 0; -} diff --git a/legacy-packages/cli-workflow/src/commands/setup/index.ts b/legacy-packages/cli-workflow/src/commands/setup/index.ts deleted file mode 100644 index d1bea40..0000000 --- a/legacy-packages/cli-workflow/src/commands/setup/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { dispatchSetup } from "./dispatch.js"; -export { loadPresetProviders } from "./preset-providers.js"; -export { cmdSetup, printSetupSummary } from "./setup.js"; -export type { CmdSetupSuccess, PresetProvider, SetupCliArgs } from "./types.js"; diff --git a/legacy-packages/cli-workflow/src/commands/setup/preset-providers.ts b/legacy-packages/cli-workflow/src/commands/setup/preset-providers.ts deleted file mode 100644 index 34581d0..0000000 --- a/legacy-packages/cli-workflow/src/commands/setup/preset-providers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { readFileSync } from "node:fs"; -import { join } from "node:path"; - -import { parse as parseYaml } from "yaml"; - -import type { PresetProvider } from "./types.js"; - -type RawPresetEntry = { - name: unknown; - label: unknown; - baseUrl: unknown; -}; - -function isRawEntry(v: unknown): v is RawPresetEntry { - if (typeof v !== "object" || v === null) return false; - const o = v as Record; - return typeof o.name === "string" && typeof o.label === "string" && typeof o.baseUrl === "string"; -} - -let cached: ReadonlyArray | null = null; - -export function loadPresetProviders(): ReadonlyArray { - if (cached !== null) return cached; - - const yamlPath = join(import.meta.dirname, "providers.yaml"); - const raw = readFileSync(yamlPath, "utf8"); - const parsed: unknown = parseYaml(raw); - - if (!Array.isArray(parsed)) { - throw new Error(`providers.yaml: expected array, got ${typeof parsed}`); - } - - const result: PresetProvider[] = []; - for (const entry of parsed) { - if (!isRawEntry(entry)) { - throw new Error(`providers.yaml: invalid entry: ${JSON.stringify(entry)}`); - } - result.push({ - name: entry.name as string, - label: entry.label as string, - baseUrl: entry.baseUrl as string, - }); - } - - cached = result; - return result; -} diff --git a/legacy-packages/cli-workflow/src/commands/setup/providers.yaml b/legacy-packages/cli-workflow/src/commands/setup/providers.yaml deleted file mode 100644 index 637802a..0000000 --- a/legacy-packages/cli-workflow/src/commands/setup/providers.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Preset LLM providers for `uncaged-workflow setup`. -# Each entry needs a provider name (used in workflow.yaml) and an OpenAI-compatible base URL. -# Add new providers here — no code changes required. - -# ── International ────────────────────────────────────────── - -- name: openai - label: OpenAI - baseUrl: https://api.openai.com/v1 - -- name: xai - label: xAI - baseUrl: https://api.x.ai/v1 - -- name: openrouter - label: OpenRouter - baseUrl: https://openrouter.ai/api/v1 - -- name: venice - label: Venice - baseUrl: https://api.venice.ai/api/v1 - -# ── China ────────────────────────────────────────────────── - -- name: dashscope - label: DashScope (Alibaba) - baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1 - -- name: deepseek - label: DeepSeek - baseUrl: https://api.deepseek.com/v1 - -- name: siliconflow - label: SiliconFlow - baseUrl: https://api.siliconflow.cn/v1 - -- name: volcengine - label: Volcengine (ByteDance) - baseUrl: https://ark.cn-beijing.volces.com/api/v3 - -- name: kimi - label: Kimi (Moonshot) - baseUrl: https://api.moonshot.cn/v1 - -- name: glm - label: GLM (Zhipu AI) - baseUrl: https://open.bigmodel.cn/api/paas/v4 - -- name: glm-intl - label: GLM (Zhipu AI Intl) - baseUrl: https://api.z.ai/api/paas/v4 - -- name: stepfun - label: StepFun - baseUrl: https://api.stepfun.com/v1 - -- name: minimax - label: MiniMax - baseUrl: https://api.minimax.io/v1 - -- name: tencent - label: Tencent TokenHub - baseUrl: https://tokenhub.tencentmaas.com/v1 - -- name: xiaomi - label: Xiaomi MiMo - baseUrl: https://api.xiaomimimo.com/v1 - -# ── Local ────────────────────────────────────────────────── - -- name: ollama - label: Ollama (local) - baseUrl: http://localhost:11434/v1 diff --git a/legacy-packages/cli-workflow/src/commands/setup/setup.ts b/legacy-packages/cli-workflow/src/commands/setup/setup.ts deleted file mode 100644 index 7aed75a..0000000 --- a/legacy-packages/cli-workflow/src/commands/setup/setup.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { err, ok, type Result, type WorkflowConfig } from "@uncaged/workflow-protocol"; -import { - readWorkflowRegistry, - splitProviderModelRef, - workflowRegistryPath, - writeWorkflowRegistry, -} from "@uncaged/workflow-register"; -import { createLogger } from "@uncaged/workflow-util"; - -import { printCliLine } from "../../cli-output.js"; -import { cmdInitWorkspace } from "../init/index.js"; -import type { CmdSetupSuccess, SetupCliArgs } from "./types.js"; - -const setupLog = createLogger({ sink: { kind: "stderr" } }); - -function mergeWorkflowConfig( - prev: WorkflowConfig | null, - input: SetupCliArgs, -): Result { - const modelSplit = splitProviderModelRef(input.defaultModel); - if (!modelSplit.ok) { - return err(modelSplit.error); - } - if (modelSplit.value.providerName !== input.provider) { - return err( - `default model provider "${modelSplit.value.providerName}" must match --provider "${input.provider}"`, - ); - } - - const maxDepth = prev === null ? 3 : prev.maxDepth; - const supervisorInterval = prev === null ? 3 : prev.supervisorInterval; - const providers = { - ...(prev === null ? {} : prev.providers), - [input.provider]: { baseUrl: input.baseUrl, apiKey: input.apiKey }, - }; - const models = { ...(prev === null ? {} : prev.models), default: input.defaultModel }; - - return ok({ - maxDepth, - supervisorInterval, - providers, - models, - }); -} - -export async function cmdSetup( - storageRoot: string, - input: SetupCliArgs, -): Promise> { - const readResult = await readWorkflowRegistry(storageRoot); - if (!readResult.ok) { - setupLog("W8JH4Q2K", `read workflow registry failed: ${readResult.error.message}`); - return err(readResult.error.message); - } - - const current = readResult.value; - const merged = mergeWorkflowConfig(current.config, input); - if (!merged.ok) { - return merged; - } - const nextConfig = merged.value; - const nextRegistry = { - config: nextConfig, - workflows: current.workflows, - }; - - const written = await writeWorkflowRegistry(storageRoot, nextRegistry); - if (!written.ok) { - setupLog("M2NB5VX9", `write workflow registry failed: ${written.error.message}`); - return err(written.error.message); - } - - const registryPath = workflowRegistryPath(storageRoot); - - let initWorkspaceRootPath: string | null = null; - if (input.initWorkspaceName !== null) { - const initResult = await cmdInitWorkspace(process.cwd(), input.initWorkspaceName); - if (!initResult.ok) { - setupLog("T7QC4HWP", `init workspace failed: ${initResult.error}`); - return err(initResult.error); - } - initWorkspaceRootPath = initResult.value.rootPath; - } - - return ok({ - registryPath, - provider: input.provider, - defaultModel: input.defaultModel, - maxDepth: nextConfig.maxDepth, - supervisorInterval: nextConfig.supervisorInterval, - initWorkspaceRootPath, - }); -} - -export function printSetupSummary(result: CmdSetupSuccess): void { - printCliLine(`wrote registry: ${result.registryPath}`); - printCliLine(`provider "${result.provider}" (baseUrl + apiKey updated)`); - printCliLine(`config.models.default = "${result.defaultModel}"`); - printCliLine(`maxDepth=${result.maxDepth}, supervisorInterval=${result.supervisorInterval}`); - if (result.initWorkspaceRootPath !== null) { - printCliLine(`initialized workflow workspace at ${result.initWorkspaceRootPath}`); - } -} diff --git a/legacy-packages/cli-workflow/src/commands/setup/types.ts b/legacy-packages/cli-workflow/src/commands/setup/types.ts deleted file mode 100644 index 9150dc8..0000000 --- a/legacy-packages/cli-workflow/src/commands/setup/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** Parsed non-interactive `setup` CLI arguments (all fields required for agent mode). */ -export type SetupCliArgs = { - provider: string; - baseUrl: string; - apiKey: string; - defaultModel: string; - initWorkspaceName: string | null; -}; - -export type PresetProvider = { - name: string; - label: string; - baseUrl: string; -}; - -export type CmdSetupSuccess = { - registryPath: string; - provider: string; - defaultModel: string; - maxDepth: number; - supervisorInterval: number; - initWorkspaceRootPath: string | null; -}; diff --git a/legacy-packages/cli-workflow/src/commands/thread/control.ts b/legacy-packages/cli-workflow/src/commands/thread/control.ts deleted file mode 100644 index feba302..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/control.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Result } from "@uncaged/workflow-protocol"; - -import { - readWorkerCtl, - resolveRunningHashForThread, - sendWorkerTcpCommand, -} from "../../worker-spawn.js"; - -type ThreadControlAction = "kill" | "pause" | "resume"; - -async function cmdThreadControl( - storageRoot: string, - threadId: string, - action: ThreadControlAction, -): Promise> { - 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> { - return cmdThreadControl(storageRoot, threadId, "kill"); -} - -export async function cmdPause( - storageRoot: string, - threadId: string, -): Promise> { - return cmdThreadControl(storageRoot, threadId, "pause"); -} - -export async function cmdResume( - storageRoot: string, - threadId: string, -): Promise> { - return cmdThreadControl(storageRoot, threadId, "resume"); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/dispatch.ts b/legacy-packages/cli-workflow/src/commands/thread/dispatch.ts deleted file mode 100644 index ee48bd5..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/dispatch.ts +++ /dev/null @@ -1,201 +0,0 @@ -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 { - 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); - if (!result.ok) { - printCliError(result.error); - return 1; - } - - printCliLine(result.value.threadId); - return 0; -} - -export async function dispatchPs(storageRoot: string, argv: string[]): Promise { - 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 { - const threadId = argv[0]; - if (threadId === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: kill requires `); - 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 { - 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 { - const threadId = argv[0]; - if (threadId === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: pause requires `); - 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 { - const threadId = argv[0]; - if (threadId === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: resume requires `); - 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 { - 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 { - const id = argv[0]; - if (id === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: thread show requires `); - 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 { - const id = argv[0]; - if (id === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: thread rm requires `); - 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 { - 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 = { - run: { - handler: dispatchRun, - args: " [--prompt ]", - 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: "", description: "Show thread details and state" }, - rm: { handler: dispatchThreadRm, args: "", description: "Remove a thread" }, - fork: { - handler: dispatchFork, - args: " [--from-role ]", - description: "Fork a thread, optionally from a specific role", - }, - ps: { handler: dispatchPs, args: "", description: "List running threads" }, - kill: { handler: dispatchKill, args: "", description: "Kill a running thread" }, - live: { - handler: dispatchLive, - args: " | --latest [--debug] [--role ]", - description: "Attach to a thread and stream output live", - }, - pause: { handler: dispatchPause, args: "", description: "Pause a running thread" }, - resume: { handler: dispatchResume, args: "", description: "Resume a paused thread" }, -}; - -export function createThreadDispatcher(deps: ThreadDispatchDeps) { - const { dispatchGroup } = deps; - return async function dispatchThread(storageRoot: string, argv: string[]): Promise { - 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; - }; -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/fork-argv.ts b/legacy-packages/cli-workflow/src/commands/thread/fork-argv.ts deleted file mode 100644 index a3bf6f7..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/fork-argv.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import type { ParsedForkArgv } from "./types.js"; - -export function parseForkArgv(argv: string[]): Result { - if (argv.length === 0) { - return err("fork requires "); - } - const threadId = argv[0]; - if (threadId === undefined || threadId === "") { - return err("fork requires "); - } - 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 }); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/fork.ts b/legacy-packages/cli-workflow/src/commands/thread/fork.ts deleted file mode 100644 index de73ccb..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/fork.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { join } from "node:path"; -import { createCasStore } from "@uncaged/workflow-cas"; -import { prepareCasFork } from "@uncaged/workflow-execute"; -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { generateUlid, getGlobalCasDir } from "@uncaged/workflow-util"; - -import { pathExists } from "../../fs-utils.js"; -import { resolveThreadRecord } from "../../thread-scan.js"; -import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js"; - -export async function cmdFork( - storageRoot: string, - threadId: string, - fromRole: string | null, -): Promise> { - const resolved = await resolveThreadRecord(storageRoot, threadId); - if (resolved === null) { - return err(`thread not found: ${threadId}`); - } - - const bundlePath = join(storageRoot, "bundles", `${resolved.bundleHash}.esm.js`); - if (!(await pathExists(bundlePath))) { - return err(`bundle file missing for thread hash ${resolved.bundleHash}`); - } - - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const newThreadId = generateUlid(Date.now()); - - const plan = await prepareCasFork({ - cas, - bundleDir: resolved.bundleDir, - bundleHash: resolved.bundleHash, - sourceThreadId: threadId, - headHash: resolved.head, - startHash: resolved.start, - newThreadId, - fromRole, - }); - if (!plan.ok) { - return plan; - } - - const worker = await ensureWorkerForHash(storageRoot, plan.value.hash, bundlePath); - if (!worker.ok) { - return worker; - } - - const p = plan.value; - const sent = await sendWorkerTcpCommand( - worker.value.port, - { - type: "run", - threadId: newThreadId, - workflowName: p.workflowName, - prompt: p.prompt, - options: p.runOptions, - steps: p.steps, - stepTimestamps: p.stepTimestamps.length > 0 ? p.stepTimestamps : null, - forkSourceThreadId: threadId, - forkContinuation: p.forkContinuation, - }, - { awaitResponseLine: false }, - ); - if (!sent.ok) { - return sent; - } - - return ok({ threadId: newThreadId }); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/index.ts b/legacy-packages/cli-workflow/src/commands/thread/index.ts deleted file mode 100644 index e1df341..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -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"; diff --git a/legacy-packages/cli-workflow/src/commands/thread/list.ts b/legacy-packages/cli-workflow/src/commands/thread/list.ts deleted file mode 100644 index 8816210..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/list.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import { listHistoricalThreads } from "../../thread-scan.js"; -import { validateCliWorkflowName } from "../../workflow-name.js"; - -export async function cmdThreads( - storageRoot: string, - argv: string[], -): Promise> { - const nameFilter = argv[0]; - if (argv.length > 1) { - return err("threads expects at most one workflow name argument"); - } - - let workflowNameFilter: string | null = null; - if (nameFilter !== undefined) { - const nameOk = validateCliWorkflowName(nameFilter); - if (!nameOk.ok) { - return nameOk; - } - workflowNameFilter = nameFilter; - } - - const rows = await listHistoricalThreads(storageRoot, workflowNameFilter); - if (rows.length === 0) { - return ok(["(no threads found)"]); - } - - const lines = rows.map((r) => `${r.threadId}\t${r.hash}\t${r.workflowName ?? "(unknown)"}`); - return ok(lines); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/live.ts b/legacy-packages/cli-workflow/src/commands/thread/live.ts deleted file mode 100644 index b5aab62..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/live.ts +++ /dev/null @@ -1,502 +0,0 @@ -import { watch } from "node:fs"; -import { mkdir, readFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; -import { createCasStore, getContentMerklePayload } from "@uncaged/workflow-cas"; -import { - FORK_BRANCH_ROLE, - readThreadsIndex, - type ThreadIndex, - walkStateFramesNewestFirst, -} from "@uncaged/workflow-execute"; -import type { CasStore, WorkflowCompletion } from "@uncaged/workflow-protocol"; -import { END } from "@uncaged/workflow-runtime"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -import { dimGreyLine, highlightLiveRole } from "../../cli-color.js"; -import { printCliError, printCliLine } from "../../cli-output.js"; -import { pathExists } from "../../fs-utils.js"; -import type { ParsedLiveArgv } from "../../live-argv.js"; -import { - findLatestThreadBundleTarget, - type LatestThreadTarget, - resolveThreadRecord, -} from "../../thread-scan.js"; -import type { LiveRoleRow } from "./types.js"; - -export const LIVE_CONTENT_MAX_LINES = 10; - -export function formatLiveTimeLabel(timestampMs: number): string { - const d = new Date(timestampMs); - const hh = String(d.getHours()).padStart(2, "0"); - const mm = String(d.getMinutes()).padStart(2, "0"); - const ss = String(d.getSeconds()).padStart(2, "0"); - return `${hh}:${mm}:${ss}`; -} - -export function formatLiveDebugLine(timestampMs: number, tag: string, message: string): string { - const label = `[${formatLiveTimeLabel(timestampMs)}] [${tag}] ${message.replace(/\n/g, " ")}`; - return dimGreyLine(label); -} - -export function renderLiveRoleStepLines(row: LiveRoleRow, roleDisplay: string): string[] { - const header = `[${formatLiveTimeLabel(row.timestamp)}] ▶ ${roleDisplay}`; - const lines: string[] = [header]; - const parts = row.content.split("\n"); - const shown = parts.slice(0, LIVE_CONTENT_MAX_LINES); - for (const ln of shown) { - lines.push(` ${ln}`); - } - const omitted = parts.length - shown.length; - if (omitted > 0) { - lines.push(` … (${omitted} more line${omitted === 1 ? "" : "s"})`); - } - lines.push(` meta: ${JSON.stringify(row.meta)}`); - return lines; -} - -function printSummary(result: WorkflowCompletion): void { - printCliLine(`completed: returnCode=${result.returnCode} — ${result.summary}`); -} - -type InfoLiveState = { - carry: string; - contentOffset: number; -}; - -type CasLiveState = { - printedHashes: Set; - lastHead: string | null; - completionEmitted: boolean; -}; - -function tryParseInfoRecord(obj: Record): { - tag: string; - content: string; - timestamp: number; -} | null { - const tag = obj.tag; - const content = obj.content; - const timestamp = obj.timestamp; - if ( - typeof tag !== "string" || - typeof content !== "string" || - typeof timestamp !== "number" || - !Number.isFinite(timestamp) - ) { - return null; - } - return { tag, content, timestamp }; -} - -function completionFromEndMeta(meta: Record): WorkflowCompletion | null { - const returnCode = meta.returnCode; - const summary = meta.summary; - if (typeof returnCode !== "number" || typeof summary !== "string") { - return null; - } - return { returnCode, summary }; -} - -async function emitRoleStepPrint(params: { - cas: CasStore; - role: string; - contentHash: string; - meta: Record; - timestamp: number; - roleFilter: string | null; -}): Promise { - if (params.roleFilter !== null && params.role !== params.roleFilter) { - return; - } - const payload = await getContentMerklePayload(params.cas, params.contentHash); - const content = - payload !== null ? payload : `(content not in CAS; contentHash=${params.contentHash})`; - - const row: LiveRoleRow = { - role: params.role, - content, - meta: params.meta, - timestamp: params.timestamp, - }; - for (const outLine of renderLiveRoleStepLines(row, highlightLiveRole(row.role))) { - printCliLine(outLine); - } -} - -async function emitStatesReachableFromHead(params: { - cas: CasStore; - headHash: string; - state: CasLiveState; - roleFilter: string | null; -}): Promise { - const frames = await walkStateFramesNewestFirst(params.cas, params.headHash); - const chronological = [...frames].reverse(); - - for (const fr of chronological) { - if (params.state.printedHashes.has(fr.hash)) { - continue; - } - params.state.printedHashes.add(fr.hash); - - const role = fr.payload.role; - if (role === FORK_BRANCH_ROLE) { - continue; - } - - if (role === END) { - const wf = completionFromEndMeta(fr.payload.meta); - if (wf !== null) { - printSummary(wf); - return wf; - } - continue; - } - - await emitRoleStepPrint({ - cas: params.cas, - role, - contentHash: fr.payload.content, - meta: fr.payload.meta, - timestamp: fr.payload.timestamp, - roleFilter: params.roleFilter, - }); - } - - return null; -} - -async function pumpThreadsJson(params: { - storageRoot: string; - bundleDir: string; - bundleHash: string; - threadId: string; - state: CasLiveState; - roleFilter: string | null; - cas: CasStore; -}): Promise { - let idx: ThreadIndex; - try { - idx = await readThreadsIndex(params.bundleDir); - } catch { - idx = {}; - } - - const active = idx[params.threadId]; - - if (active === undefined) { - if (params.state.completionEmitted) { - return null; - } - const hist = await resolveThreadRecord(params.storageRoot, params.threadId); - if (hist === null || hist.source !== "history") { - return null; - } - params.state.completionEmitted = true; - const wf = await emitStatesReachableFromHead({ - cas: params.cas, - headHash: hist.head, - state: params.state, - roleFilter: params.roleFilter, - }); - return wf !== null ? 0 : null; - } - - const head = active.head; - if (params.state.lastHead === null) { - params.state.lastHead = head; - const wf = await emitStatesReachableFromHead({ - cas: params.cas, - headHash: head, - state: params.state, - roleFilter: params.roleFilter, - }); - return wf !== null ? 0 : null; - } - - if (head !== params.state.lastHead) { - params.state.lastHead = head; - const wf = await emitStatesReachableFromHead({ - cas: params.cas, - headHash: head, - state: params.state, - roleFilter: params.roleFilter, - }); - return wf !== null ? 0 : null; - } - - return null; -} - -async function pumpNewInfoContent(infoPath: string, state: InfoLiveState): Promise { - let text: string; - try { - text = await readFile(infoPath, "utf8"); - } catch { - return; - } - - if (text.length < state.contentOffset) { - state.contentOffset = 0; - state.carry = ""; - } - - const chunk = text.slice(state.contentOffset); - state.contentOffset = text.length; - state.carry += chunk; - - const parts = state.carry.split("\n"); - state.carry = parts.pop() ?? ""; - - for (const line of parts) { - const trimmed = line.trim(); - if (trimmed === "") { - continue; - } - let rec: unknown; - try { - rec = JSON.parse(trimmed) as unknown; - } catch { - continue; - } - if (rec === null || typeof rec !== "object") { - continue; - } - const parsed = tryParseInfoRecord(rec as Record); - if (parsed === null) { - continue; - } - printCliLine(formatLiveDebugLine(parsed.timestamp, parsed.tag, parsed.content)); - } -} - -type WatchPumpTask = { - path: string; - pump: () => Promise; -}; - -async function runWatchPumpStep( - settled: () => boolean, - pump: () => Promise, - closeAll: () => void, - finish: (code: number) => void, -): Promise { - if (settled()) { - return; - } - try { - const code = await pump(); - if (code !== null) { - closeAll(); - finish(code); - } - } catch (e) { - closeAll(); - throw e instanceof Error ? e : new Error(String(e)); - } -} - -function watchLivePaths(params: { tasks: WatchPumpTask[]; signal: AbortSignal }): Promise { - const { tasks, signal } = params; - - return new Promise((resolve, reject) => { - let settled = false; - const finish = (code: number): void => { - if (settled) { - return; - } - settled = true; - resolve(code); - }; - - const pumpChains = new Map>(); - for (const t of tasks) { - pumpChains.set(t.path, Promise.resolve()); - } - - const watchers: ReturnType[] = []; - - const closeAll = (): void => { - for (const w of watchers) { - w.close(); - } - }; - - function schedulePump(path: string, pump: () => Promise): void { - const prev = pumpChains.get(path) ?? Promise.resolve(); - const next = (async () => { - await prev; - await runWatchPumpStep(() => settled, pump, closeAll, finish); - })(); - pumpChains.set(path, next); - } - - for (const { path, pump } of tasks) { - const watcher = watch(path, (eventType) => { - if (eventType === "rename") { - return; - } - schedulePump(path, pump); - }); - watchers.push(watcher); - watcher.on("error", (errObj: Error) => { - closeAll(); - reject(errObj); - }); - } - - const onAbort = (): void => { - closeAll(); - finish(0); - }; - signal.addEventListener("abort", onAbort, { once: true }); - - for (const { path, pump } of tasks) { - schedulePump(path, pump); - } - }); -} - -type LiveThreadTarget = LatestThreadTarget; - -async function resolveLiveThreadTarget( - storageRoot: string, - parsed: ParsedLiveArgv, -): Promise { - if (parsed.latest) { - const found = await findLatestThreadBundleTarget(storageRoot); - if (found === null) { - printCliError("live: no threads found"); - return null; - } - return found; - } - - const id = parsed.threadId; - if (id === null) { - printCliError("live: internal error: missing thread id"); - return null; - } - const resolved = await resolveThreadRecord(storageRoot, id); - if (resolved === null) { - printCliError(`thread not found: ${id}`); - return null; - } - return { - threadId: id, - bundleHash: resolved.bundleHash, - bundleDir: resolved.bundleDir, - threadsJsonPath: join(resolved.bundleDir, "threads.json"), - }; -} - -async function buildLiveWatchTasks(params: { - storageRoot: string; - target: LiveThreadTarget; - debug: boolean; - dataState: CasLiveState; - infoState: InfoLiveState; - roleFilter: string | null; - cas: CasStore; -}): Promise { - const infoPath = join( - params.storageRoot, - "logs", - params.target.bundleHash, - `${params.target.threadId}.info.jsonl`, - ); - - const tasks: WatchPumpTask[] = [ - { - path: params.target.threadsJsonPath, - pump: () => - pumpThreadsJson({ - storageRoot: params.storageRoot, - bundleDir: params.target.bundleDir, - bundleHash: params.target.bundleHash, - threadId: params.target.threadId, - state: params.dataState, - roleFilter: params.roleFilter, - cas: params.cas, - }), - }, - ]; - - if (params.debug && (await pathExists(infoPath))) { - tasks.push({ - path: infoPath, - pump: async () => { - await pumpNewInfoContent(infoPath, params.infoState); - return null; - }, - }); - } - - return tasks; -} - -export async function cmdLive(storageRoot: string, parsed: ParsedLiveArgv): Promise { - const target = await resolveLiveThreadTarget(storageRoot, parsed); - if (target === null) { - return 1; - } - - const roleFilter = parsed.role; - const cas = createCasStore(getGlobalCasDir(storageRoot)); - - const dataState: CasLiveState = { - printedHashes: new Set(), - lastHead: null, - completionEmitted: false, - }; - - const infoState: InfoLiveState = { - carry: "", - contentOffset: 0, - }; - - const controller = new AbortController(); - const onSigInt = (): void => { - controller.abort(); - }; - process.on("SIGINT", onSigInt); - - try { - await mkdir(dirname(target.threadsJsonPath), { recursive: true }); - - const firstData = await pumpThreadsJson({ - storageRoot, - bundleDir: target.bundleDir, - bundleHash: target.bundleHash, - threadId: target.threadId, - state: dataState, - roleFilter, - cas, - }); - const infoPath = join(storageRoot, "logs", target.bundleHash, `${target.threadId}.info.jsonl`); - if (parsed.debug && (await pathExists(infoPath))) { - await pumpNewInfoContent(infoPath, infoState); - } - - if (firstData === 0) { - return 0; - } - - const tasks = await buildLiveWatchTasks({ - storageRoot, - target, - debug: parsed.debug, - dataState, - infoState, - roleFilter, - cas, - }); - - return await watchLivePaths({ tasks, signal: controller.signal }); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - printCliError(`live: ${message}`); - return 1; - } finally { - process.off("SIGINT", onSigInt); - } -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/ps.ts b/legacy-packages/cli-workflow/src/commands/thread/ps.ts deleted file mode 100644 index 213aa49..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/ps.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { listRunningThreads } from "../../thread-scan.js"; - -export async function cmdPs(storageRoot: string): Promise { - const rows = await listRunningThreads(storageRoot); - if (rows.length === 0) { - return ["(no running threads)"]; - } - return rows.map((r) => `${r.threadId}\t${r.hash}\t${r.workflowName ?? "(unknown)"}`); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/rm.ts b/legacy-packages/cli-workflow/src/commands/thread/rm.ts deleted file mode 100644 index fb2d998..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/rm.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { unlink } from "node:fs/promises"; -import { join } from "node:path"; -import { - garbageCollectCas, - removeThreadEntry, - removeThreadHistoryEntries, -} from "@uncaged/workflow-execute"; -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import { resolveThreadRecord } from "../../thread-scan.js"; - -export async function cmdThreadRemove( - storageRoot: string, - threadId: string, -): Promise> { - const resolved = await resolveThreadRecord(storageRoot, threadId); - if (resolved === null) { - return err(`thread not found: ${threadId}`); - } - - // Always clear both stores: between resolve and delete the worker may finish and - // move the thread from threads.json into history; branching only on resolved.source - // would skip history removal and leave a dangling row. - await removeThreadEntry(resolved.bundleDir, threadId); - const hist = await removeThreadHistoryEntries(resolved.bundleDir, threadId); - if (!hist.ok) { - return hist; - } - - const infoPath = join(storageRoot, "logs", resolved.bundleHash, `${threadId}.info.jsonl`); - const runningPath = join(storageRoot, "logs", resolved.bundleHash, `${threadId}.running`); - - await unlink(infoPath).catch(() => {}); - await unlink(runningPath).catch(() => {}); - - await garbageCollectCas(storageRoot); - - return ok(undefined); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/run.ts b/legacy-packages/cli-workflow/src/commands/thread/run.ts deleted file mode 100644 index cf9c29b..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/run.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { join } from "node:path"; - -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register"; -import { generateUlid } from "@uncaged/workflow-util"; -import { ensureWorkerForHash, sendWorkerTcpCommand } from "../../worker-spawn.js"; -import { validateCliWorkflowName } from "../../workflow-name.js"; - -export async function cmdRun( - storageRoot: string, - name: string, - prompt: string, -): Promise> { - const nameOk = validateCliWorkflowName(name); - if (!nameOk.ok) { - return nameOk; - } - - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return err(reg.error.message); - } - - const entry = getRegisteredWorkflow(reg.value, name); - if (entry === null) { - return err(`workflow not registered: ${name}`); - } - - const bundlePath = join(storageRoot, "bundles", `${entry.hash}.esm.js`); - const worker = await ensureWorkerForHash(storageRoot, entry.hash, bundlePath); - if (!worker.ok) { - return worker; - } - - const threadId = generateUlid(Date.now()); - const sent = await sendWorkerTcpCommand( - worker.value.port, - { - type: "run", - threadId, - workflowName: name, - prompt, - options: { depth: 0 }, - }, - { awaitResponseLine: false }, - ); - if (!sent.ok) { - return sent; - } - - return ok({ threadId }); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/show.ts b/legacy-packages/cli-workflow/src/commands/thread/show.ts deleted file mode 100644 index b03ae24..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/show.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { createCasStore, getContentMerklePayload, parseCasThreadNode } from "@uncaged/workflow-cas"; -import { FORK_BRANCH_ROLE, walkStateFramesNewestFirst } from "@uncaged/workflow-execute"; -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { END } from "@uncaged/workflow-runtime"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -import { resolveThreadRecord } from "../../thread-scan.js"; - -async function readParentStateFromStartNode( - cas: { get(hash: string): Promise }, - startHash: string, -): Promise { - const yamlText = await cas.get(startHash); - if (yamlText === null) { - return null; - } - const parsed = parseCasThreadNode(yamlText); - if (parsed === null || parsed.kind !== "start") { - return null; - } - return parsed.node.payload.parentState; -} - -export async function cmdThreadShow( - storageRoot: string, - threadId: string, -): Promise> { - const resolved = await resolveThreadRecord(storageRoot, threadId); - if (resolved === null) { - return err(`thread not found: ${threadId}`); - } - - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const frames = await walkStateFramesNewestFirst(cas, resolved.head); - const chronological = [...frames].reverse(); - - const parentState = await readParentStateFromStartNode(cas, resolved.start); - - const steps: Array<{ - role: string; - hash: string; - timestamp: number; - content: string; - childThread: string | null; - }> = []; - for (const fr of chronological) { - if (fr.payload.role === END || fr.payload.role === FORK_BRANCH_ROLE) { - continue; - } - const payloadText = await getContentMerklePayload(cas, fr.payload.content); - steps.push({ - role: fr.payload.role, - hash: fr.hash, - timestamp: fr.payload.timestamp, - content: - payloadText !== null - ? payloadText - : `(content not in CAS; contentHash=${fr.payload.content})`, - childThread: fr.payload.childThread, - }); - } - - const payload = { - threadId: resolved.threadId, - bundleHash: resolved.bundleHash, - head: resolved.head, - start: resolved.start, - parentState, - source: resolved.source, - steps, - }; - - return ok(JSON.stringify(payload, null, 2)); -} diff --git a/legacy-packages/cli-workflow/src/commands/thread/types.ts b/legacy-packages/cli-workflow/src/commands/thread/types.ts deleted file mode 100644 index 8cf3dd8..0000000 --- a/legacy-packages/cli-workflow/src/commands/thread/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { DispatchGroupFn } from "../../cli-command-types.js"; - -export type LiveRoleRow = { - role: string; - content: string; - meta: Record; - timestamp: number; -}; - -export type ParsedForkArgv = { - threadId: string; - fromRole: string | null; -}; - -export type ThreadDispatchDeps = { - dispatchGroup: DispatchGroupFn; -}; diff --git a/legacy-packages/cli-workflow/src/commands/workflow/add-argv.ts b/legacy-packages/cli-workflow/src/commands/workflow/add-argv.ts deleted file mode 100644 index 91167c0..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/add-argv.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import type { ParsedAddArgv } from "./types.js"; - -type ParsedLongFlag = { advance: 2; kind: "types"; value: string }; - -function tryParseAddLongFlag(argv: string[], index: number): Result { - 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 { - 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 { - 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 "); - } - - return ok({ name, filePath, typesPath }); -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/add.ts b/legacy-packages/cli-workflow/src/commands/workflow/add.ts deleted file mode 100644 index 0e802b0..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/add.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { readFile, stat } from "node:fs/promises"; -import { basename, resolve } from "node:path"; -import { hashWorkflowBundleBytes } from "@uncaged/workflow-cas"; -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { - extractBundleExports, - readWorkflowRegistry, - registerWorkflowVersion, - stringifyWorkflowDescriptor, - validateWorkflowBundle, - writeWorkflowRegistry, -} from "@uncaged/workflow-register"; - -import { storeWorkflowBundleArtifacts } from "../../bundle-store.js"; -import { validateCliWorkflowName } from "../../workflow-name.js"; - -import type { CmdAddSuccess, ParsedAddArgv } from "./types.js"; - -function isEsmBundle(path: string): boolean { - return path.endsWith(".esm.js"); -} - -function defaultTypesPath(bundlePath: string): string { - return bundlePath.replace(/\.esm\.js$/i, ".d.ts"); -} - -async function registerHash( - storageRoot: string, - name: string, - hash: string, -): Promise> { - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return err(reg.error.message); - } - - const next = registerWorkflowVersion(reg.value, name, hash, Date.now()); - const written = await writeWorkflowRegistry(storageRoot, next); - if (!written.ok) { - return err(written.error.message); - } - return ok(undefined); -} - -async function resolveOptionalTypes( - typesPathFlag: string | null, - resolvedBundlePath: string, -): Promise> { - const warnings: string[] = []; - let dtsText: string | null = null; - - if (typesPathFlag !== null) { - const typesResolved = resolve(typesPathFlag); - try { - dtsText = await readFile(typesResolved, "utf8"); - } catch { - return err(`types file not found: ${typesResolved}`); - } - return ok({ dtsText, warnings }); - } - - const typesDefault = defaultTypesPath(resolvedBundlePath); - try { - dtsText = await readFile(typesDefault, "utf8"); - } catch { - warnings.push(`optional types file not found (${basename(typesDefault)}); skipped`); - } - - return ok({ dtsText, warnings }); -} - -export async function cmdAdd( - storageRoot: string, - args: ParsedAddArgv, -): Promise> { - const nameOk = validateCliWorkflowName(args.name); - if (!nameOk.ok) { - return nameOk; - } - - let resolvedPath: string; - try { - resolvedPath = resolve(args.filePath); - await stat(resolvedPath); - } catch { - return err(`file not found: ${args.filePath}`); - } - - if (resolvedPath.endsWith(".ts")) { - return err("build your .ts file first, then add the .esm.js"); - } - - if (!isEsmBundle(resolvedPath)) { - return err('workflow file must end with ".esm.js"'); - } - - let source: string; - try { - source = await readFile(resolvedPath, "utf8"); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - return err(`failed to read bundle: ${message}`); - } - - const validated = validateWorkflowBundle({ - filePath: resolvedPath, - source, - }); - if (!validated.ok) { - return validated; - } - - const extracted = await extractBundleExports(resolvedPath); - if (!extracted.ok) { - return extracted; - } - - const yamlSource = stringifyWorkflowDescriptor(extracted.value.descriptor); - - const companions = await resolveOptionalTypes(args.typesPath, resolvedPath); - if (!companions.ok) { - return companions; - } - - const encoder = new TextEncoder(); - const bytes = encoder.encode(source); - const hash = hashWorkflowBundleBytes(bytes); - - const dts = - companions.value.dtsText === null - ? null - : { kind: "text" as const, text: companions.value.dtsText }; - - const stored = await storeWorkflowBundleArtifacts(storageRoot, hash, { - esmJs: { kind: "text", text: source }, - yaml: { kind: "text", text: yamlSource }, - dts, - }); - if (!stored.ok) { - return stored; - } - - const regResult = await registerHash(storageRoot, args.name, hash); - if (!regResult.ok) { - return regResult; - } - - return ok({ hash, warnings: companions.value.warnings }); -} - -export function formatAddSuccess(name: string, filePath: string, hash: string): string { - return `registered workflow "${name}" from ${basename(filePath)} as ${hash}`; -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/dispatch.ts b/legacy-packages/cli-workflow/src/commands/workflow/dispatch.ts deleted file mode 100644 index 9294811..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/dispatch.ts +++ /dev/null @@ -1,158 +0,0 @@ -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 { - 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 { - 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 { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: show requires `); - 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 { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: remove requires `); - 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 { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${usageText()}\n\nerror: history requires `); - 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 { - const name = argv[0]; - if (name === undefined || argv.length > 2) { - printCliError(`${usageText()}\n\nerror: rollback requires [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 = { - add: { - handler: dispatchAdd, - args: " [--types ]", - description: "Register a workflow bundle in the registry", - }, - list: { handler: dispatchList, args: "", description: "List all registered workflows" }, - show: { - handler: dispatchShow, - args: "", - description: "Show details of a registered workflow", - }, - rm: { - handler: dispatchRemove, - args: "", - description: "Remove a workflow from the registry", - }, - history: { - handler: dispatchHistory, - args: "", - description: "Show version history of a workflow", - }, - rollback: { - handler: dispatchRollback, - args: " [hash]", - description: "Rollback a workflow to a previous version", - }, -}; - -export function createWorkflowDispatcher(deps: WorkflowDispatchDeps) { - const { dispatchGroup } = deps; - return async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise { - const result = dispatchGroup("workflow", WORKFLOW_SUBCOMMAND_TABLE, storageRoot, argv); - if (result !== null) { - return result; - } - const sub = argv[0]; - if (sub === "remove") { - return dispatchRemove(storageRoot, argv.slice(1)); - } - printCliError(`${usageText()}\n\nerror: unknown workflow subcommand: ${sub}`); - return 1; - }; -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/history.ts b/legacy-packages/cli-workflow/src/commands/workflow/history.ts deleted file mode 100644 index 9d94fde..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/history.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { getRegisteredWorkflow, readWorkflowRegistry } from "@uncaged/workflow-register"; - -import { validateCliWorkflowName } from "../../workflow-name.js"; - -export async function cmdHistory( - storageRoot: string, - name: string, -): Promise> { - const nameOk = validateCliWorkflowName(name); - if (!nameOk.ok) { - return nameOk; - } - - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return err(reg.error.message); - } - - const entry = getRegisteredWorkflow(reg.value, name); - if (entry === null) { - return err(`workflow not registered: ${name}`); - } - - type Row = { hash: string; timestamp: number; isCurrent: boolean }; - const rows: Row[] = [ - { hash: entry.hash, timestamp: entry.timestamp, isCurrent: true }, - ...entry.history.map((h) => ({ hash: h.hash, timestamp: h.timestamp, isCurrent: false })), - ]; - rows.sort((a, b) => b.timestamp - a.timestamp); - - const lines = rows.map((r) => { - const date = new Date(r.timestamp).toISOString(); - const suffix = r.isCurrent ? "\t(current)" : ""; - return `${r.hash}\t${date}${suffix}`; - }); - return ok(lines); -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/index.ts b/legacy-packages/cli-workflow/src/commands/workflow/index.ts deleted file mode 100644 index c6a5fd1..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -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"; diff --git a/legacy-packages/cli-workflow/src/commands/workflow/list.ts b/legacy-packages/cli-workflow/src/commands/workflow/list.ts deleted file mode 100644 index 7c1a21c..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/list.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { - listRegisteredWorkflowNames, - readWorkflowRegistry, - type WorkflowRegistryFile, -} from "@uncaged/workflow-register"; - -export async function cmdList(storageRoot: string): Promise> { - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return err(reg.error.message); - } - return ok(reg.value); -} - -export function formatListLines(registry: WorkflowRegistryFile): string[] { - const names = listRegisteredWorkflowNames(registry); - if (names.length === 0) { - return ["(no workflows registered)"]; - } - const lines: string[] = []; - for (const name of names) { - const entry = registry.workflows[name]; - if (entry === undefined) { - continue; - } - lines.push(`${name}\t${entry.hash}\t${entry.timestamp}`); - } - return lines; -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/rm.ts b/legacy-packages/cli-workflow/src/commands/workflow/rm.ts deleted file mode 100644 index 5906bc4..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/rm.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { - readWorkflowRegistry, - unregisterWorkflow, - writeWorkflowRegistry, -} from "@uncaged/workflow-register"; - -import { validateCliWorkflowName } from "../../workflow-name.js"; - -export async function cmdRemove(storageRoot: string, name: string): Promise> { - const nameOk = validateCliWorkflowName(name); - if (!nameOk.ok) { - return nameOk; - } - - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return err(reg.error.message); - } - - const next = unregisterWorkflow(reg.value, name); - if (!next.ok) { - return err(next.error.message); - } - - const written = await writeWorkflowRegistry(storageRoot, next.value); - if (!written.ok) { - return err(written.error.message); - } - - return ok(undefined); -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/rollback.ts b/legacy-packages/cli-workflow/src/commands/workflow/rollback.ts deleted file mode 100644 index dd9c6ea..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/rollback.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { join } from "node:path"; - -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { - getRegisteredWorkflow, - readWorkflowRegistry, - rollbackWorkflowToHistoryHash, - writeWorkflowRegistry, -} from "@uncaged/workflow-register"; - -import { pathExists } from "../../fs-utils.js"; -import { validateCliWorkflowName } from "../../workflow-name.js"; - -export async function cmdRollback( - storageRoot: string, - name: string, - hash: string | null, -): Promise> { - const nameOk = validateCliWorkflowName(name); - if (!nameOk.ok) { - return nameOk; - } - - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return err(reg.error.message); - } - - const entry = getRegisteredWorkflow(reg.value, name); - if (entry === null) { - return err(`workflow not registered: ${name}`); - } - - const rolled = rollbackWorkflowToHistoryHash(entry, hash); - if (!rolled.ok) { - return err(rolled.error.message); - } - - const bundlePath = join(storageRoot, "bundles", `${rolled.value.hash}.esm.js`); - if (!(await pathExists(bundlePath))) { - return err(`bundle file not found for hash ${rolled.value.hash}`); - } - - const nextRegistry = { - config: reg.value.config, - workflows: { ...reg.value.workflows, [name]: rolled.value }, - }; - const written = await writeWorkflowRegistry(storageRoot, nextRegistry); - if (!written.ok) { - return err(written.error.message); - } - - return ok(undefined); -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/show.ts b/legacy-packages/cli-workflow/src/commands/workflow/show.ts deleted file mode 100644 index 128335f..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/show.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; -import { - getRegisteredWorkflow, - readWorkflowRegistry, - type WorkflowRegistryEntry, -} from "@uncaged/workflow-register"; -import { stringify } from "yaml"; - -import { validateCliWorkflowName } from "../../workflow-name.js"; - -export async function cmdShow( - storageRoot: string, - name: string, -): Promise> { - const nameOk = validateCliWorkflowName(name); - if (!nameOk.ok) { - return nameOk; - } - - const reg = await readWorkflowRegistry(storageRoot); - if (!reg.ok) { - return err(reg.error.message); - } - - const entry = getRegisteredWorkflow(reg.value, name); - if (entry === null) { - return err(`workflow not found: ${name}`); - } - return ok(entry); -} - -export function formatShowYaml(name: string, entry: WorkflowRegistryEntry): string { - const payload = { - [name]: { - hash: entry.hash, - timestamp: entry.timestamp, - history: entry.history, - }, - }; - return stringify(payload, { indent: 2, defaultStringType: "QUOTE_DOUBLE" }); -} diff --git a/legacy-packages/cli-workflow/src/commands/workflow/types.ts b/legacy-packages/cli-workflow/src/commands/workflow/types.ts deleted file mode 100644 index cbc678d..0000000 --- a/legacy-packages/cli-workflow/src/commands/workflow/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -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; -}; - -export type WorkflowDispatchDeps = { - dispatchGroup: DispatchGroupFn; -}; diff --git a/legacy-packages/cli-workflow/src/fs-utils.ts b/legacy-packages/cli-workflow/src/fs-utils.ts deleted file mode 100644 index db3682b..0000000 --- a/legacy-packages/cli-workflow/src/fs-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { readFile, stat } from "node:fs/promises"; - -export async function readTextFileIfExists(path: string): Promise { - try { - return await readFile(path, "utf8"); - } catch (e) { - const errObj = e as NodeJS.ErrnoException; - if (errObj.code === "ENOENT") { - return null; - } - throw e; - } -} - -export async function pathExists(path: string): Promise { - try { - await stat(path); - return true; - } catch { - return false; - } -} diff --git a/legacy-packages/cli-workflow/src/live-argv.ts b/legacy-packages/cli-workflow/src/live-argv.ts deleted file mode 100644 index 78735db..0000000 --- a/legacy-packages/cli-workflow/src/live-argv.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -export type ParsedLiveArgv = { - threadId: string | null; - latest: boolean; - debug: boolean; - role: string | null; -}; - -type LiveArgvScan = { - latest: boolean; - debug: boolean; - role: string | null; - threadId: string | null; -}; - -function applyLiveArgvToken(argv: string[], i: number, s: LiveArgvScan): Result { - const a = argv[i]; - if (a === "--latest") { - s.latest = true; - return ok(i + 1); - } - if (a === "--debug") { - s.debug = true; - return ok(i + 1); - } - if (a === "--role") { - const v = argv[i + 1]; - if (v === undefined || v.startsWith("--")) { - return err("missing value for --role"); - } - s.role = v; - return ok(i + 2); - } - if (a.startsWith("--")) { - return err(`unknown live flag: ${a}`); - } - if (s.threadId !== null) { - return err("unexpected extra argument"); - } - s.threadId = a; - return ok(i + 1); -} - -export function parseLiveArgv(argv: string[]): Result { - const s: LiveArgvScan = { - latest: false, - debug: false, - role: null, - threadId: null, - }; - - let i = 0; - while (i < argv.length) { - const step = applyLiveArgvToken(argv, i, s); - if (!step.ok) { - return step; - } - i = step.value; - } - - if (s.latest && s.threadId !== null) { - return err("live --latest does not take "); - } - if (!s.latest && s.threadId === null) { - return err("live requires or --latest"); - } - - return ok({ - threadId: s.threadId, - latest: s.latest, - debug: s.debug, - role: s.role, - }); -} diff --git a/legacy-packages/cli-workflow/src/run-argv.ts b/legacy-packages/cli-workflow/src/run-argv.ts deleted file mode 100644 index 101c77e..0000000 --- a/legacy-packages/cli-workflow/src/run-argv.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -export type ParsedRunArgv = { - name: string; - prompt: string; -}; - -function parseFlagAt( - argv: string[], - index: number, -): Result<{ kind: "prompt"; value: string }, string> | null { - const flag = argv[index]; - if (flag === "--prompt") { - const value = argv[index + 1]; - if (value === undefined) { - return err("missing value for --prompt"); - } - return ok({ kind: "prompt", value }); - } - return null; -} - -export function parseRunArgv(argv: string[]): Result { - let name: string | undefined; - let prompt = ""; - - let i = 0; - const first = argv[0]; - if (first !== undefined && !first.startsWith("--")) { - name = first; - i = 1; - } - - while (i < argv.length) { - const parsed = parseFlagAt(argv, i); - if (parsed === null) { - const unknown = argv[i]; - return err(`unknown run flag: ${unknown}`); - } - if (!parsed.ok) { - return parsed; - } - - const flag = parsed.value; - prompt = flag.value; - i += 2; - } - - if (name === undefined || name === "") { - return err("run requires "); - } - - return ok({ name, prompt }); -} diff --git a/legacy-packages/cli-workflow/src/skill.ts b/legacy-packages/cli-workflow/src/skill.ts deleted file mode 100644 index 780e8be..0000000 --- a/legacy-packages/cli-workflow/src/skill.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { getCommandRegistry } from "./cli-registry.js"; - -type SkillTopic = { - name: string; - description: string; - format: () => string; -}; - -const SKILL_TOPICS: ReadonlyArray = [ - { name: "cli", description: "Full CLI command reference", format: formatSkillCli }, - { - name: "develop", - description: "Guide for agents executing roles inside a workflow", - format: formatSkillDevelop, - }, - { - name: "author", - description: "Guide for building and publishing workflow bundles", - format: formatSkillAuthor, - }, -]; - -export function getSkillTopics(): ReadonlyArray<{ name: string; description: string }> { - return SKILL_TOPICS.map((t) => ({ name: t.name, description: t.description })); -} - -export function formatSkillTopic(topic: string): string | null { - const entry = SKILL_TOPICS.find((t) => t.name === topic); - if (entry === undefined) { - return null; - } - return entry.format(); -} - -export function formatSkillIndex(): string { - const rows = SKILL_TOPICS.map((t) => `| \`${t.name}\` | ${t.description} |`); - return `# uncaged-workflow skill - -Available topics: - -| Topic | Description | -|-------|-------------| -${rows.join("\n")} - -Usage: \`uncaged-workflow skill \` -`; -} - -// ── cli topic (existing full reference) ──────────────────────────────── - -function formatSkillCli(): string { - const groups = getCommandRegistry(); - - const commandSections: string[] = []; - for (const group of groups) { - const rows = group.commands.map((cmd) => { - const namePart = cmd.name === "" ? "" : ` ${cmd.name}`; - const args = cmd.args ? `\`${cmd.args}\`` : "(none)"; - return `| \`${group.name}${namePart}\` | ${args} | ${cmd.description} |`; - }); - commandSections.push( - `### ${group.name}\n\n| Command | Args | Description |\n|---------|------|-------------|\n${rows.join("\n")}`, - ); - } - - return `# uncaged-workflow CLI Reference - -## Core Concepts - -| Concept | Description | -|---------|-------------| -| **Workflow** | A single-file ESM bundle (\`.esm.js\`) that exports \`run\` and \`descriptor\`. Identified by name and XXH64 hash. | -| **Bundle** | The physical \`.esm.js\` file stored in the bundles directory. Immutable once written. | -| **Thread** | A single execution of a workflow, identified by a ULID. CAS state chain; \`threads.json\` for active; \`history/*.jsonl\` when done; \`.info.jsonl\` for debug logs. | -| **CAS** | Global content-addressable blob store (\`cas/\`), keyed by hash. | -| **Registry** | \`workflow.yaml\` — maps workflow names to their current and historical bundle hashes. | - -## Commands - -${commandSections.join("\n\n")} - -### Top-level shortcuts - -| Command | Equivalent | Description | -|---------|------------|-------------| -| \`run\` | \`thread run\` | Shortcut to start a thread | -| \`live\` | \`thread live\` | Shortcut to attach to a thread | - -### connect - -| Command | Args | Description | -|---------|------|-------------| -| \`connect\` | \`[--name NAME] [--gateway URL]\` | Connect to workflow gateway via WebSocket. \`--name\` registers with the gateway. | - -## Typical Workflow - -1. \`uncaged-workflow workflow add my-wf ./my-wf.esm.js\` — register a workflow -2. \`uncaged-workflow run my-wf --prompt "do the thing"\` — start a thread -3. \`uncaged-workflow live --latest\` — attach and watch output -4. \`uncaged-workflow thread show \` — inspect completed thread - -## Thread Status - -| Status | Meaning | -|--------|---------| -| \`running\` | Worker process is alive (\`.running\` marker + live PID) | -| \`active\` | In \`threads.json\` but not currently running (paused or waiting) | -| \`completed\` | Finished with \`returnCode === 0\` (has \`__end__\` frame in CAS) | -| \`failed\` | Finished with non-zero return code, or worker crashed (dead PID / no ctl) | - -## Exit Codes - -| Code | Meaning | -|------|---------| -| 0 | Success | -| 1 | Error | - -## Environment Variables - -| Variable | Description | -|----------|-------------| -| \`WORKFLOW_STORAGE_ROOT\` | Override the default storage directory for all workflow data | -| \`UNCAGED_WORKFLOW_STORAGE_ROOT\` | Same as above (takes priority) | -| \`WORKFLOW_LLM_API_KEY\` | API key for LLM calls during workflow execution | -`; -} - -// ── develop topic (for agents inside a workflow) ─────────────────────── - -function formatSkillDevelop(): string { - return `# Workflow Role Guide - -Reference for agents executing roles (planner, coder, reviewer, etc.) inside a running workflow thread. - -## Thread ID - -Every thread has a 26-character Crockford Base32 ULID (e.g. \`06F03H5V6JTMDST6P3TVH42RWM\`). - -It appears in the **first message** of the conversation. If unsure: - -\`\`\` -uncaged-workflow thread list -\`\`\` - -## CAS (Content-Addressable Storage) - -Store and retrieve content by hash in workflow storage (global CAS directory). - -| Operation | Command | -|-----------|---------| -| **Store** | \`uncaged-workflow cas put ''\` → prints hash | -| **Read** | \`uncaged-workflow cas get \` → prints content | -| **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. - -## Meta Output - -Each role must produce structured output that the moderator extracts. The exact schema depends on the role, but the pattern is: - -1. Do your work (write code, run tests, etc.) -2. Output a compact JSON object matching the role's schema -3. The moderator extracts and validates it automatically - -## Thread Context - -The conversation history contains outputs from previous roles. Read it to understand: -- What task was requested (from the initial prompt) -- What previous roles produced (plans, code changes, review results) -- What the moderator decided (which phase to work on, whether to retry) -`; -} - -// ── author topic (for workflow developers) ───────────────────────────── - -function formatSkillAuthor(): string { - return `# Workflow Authoring Guide - -How to build, test, and publish workflow bundles for uncaged-workflow. - -## Bundle Structure - -A workflow bundle is a single ESM file (\`.esm.js\`) that exports: - -\`\`\`typescript -// Required named exports (no default export) -export const descriptor: WorkflowDescriptor; -export const run: WorkflowFn; -\`\`\` - -## WorkflowDescriptor - -Serialized metadata for the registry. Every role must include both \`description\` and \`schema\` (JSON Schema object). The graph uses an edges array where each edge has \`from\`, \`to\`, and \`condition\`. - -\`\`\`typescript -type WorkflowDescriptor = { - description: string; - roles: Record; - graph: { - edges: Array<{ - from: string; // role name, or "__start__" - to: string; // role name, or "__end__" - condition: string; // e.g. "FALLBACK" - conditionDescription?: string | null; - }>; - }; -}; -\`\`\` - -**descriptor is static data** — it is read at \`workflow add\` (register) time via \`import()\`. It must NOT trigger any side effects or read environment variables. - -## WorkflowFn - -Async generator from \`createWorkflow(definition, binding)\` (**@uncaged/workflow-runtime**) — yields each role output until the workflow completes. - -## ModeratorTable - -Declarative routing table. Transitions use the \`role\` field (not \`next\`): - -\`\`\`typescript -import { START, END, type ModeratorTable } from "@uncaged/workflow-runtime"; - -const table: ModeratorTable = { - [START]: [{ condition: "FALLBACK", role: "firstRole" }], - firstRole: [{ condition: "FALLBACK", role: END }], -}; -\`\`\` - -## AdapterFn / AdapterBinding - -The adapter receives a system prompt and Zod schema, returns a \`RoleFn\` that produces typed meta: - -\`\`\`typescript -type AdapterFn = (prompt: string, schema: ZodType) => RoleFn; -type AdapterBinding = { - adapter: AdapterFn; - overrides: Partial> | null; -}; -\`\`\` - -## Role Definition - -Each role has: - -| Field | Type | Purpose | -|-------|------|---------| -| \`description\` | string | What the role does | -| \`systemPrompt\` | string | System prompt for the agent | -| \`schema\` | ZodSchema | Validates meta; annotate CAS hash strings with \`.meta({ casRef: true })\` for DAG linking | - -## Development Workflow - -\`\`\`bash -# 1. Initialize a workspace -uncaged-workflow init workspace my-workflow - -# 2. Write your template (roles + ModeratorTable + definition) -# 3. Write entry file (workflows/*-entry.ts) with adapter binding + descriptor - -# 4. Build the ESM bundle -bun run bundle # uses scripts/bundle.ts - -# 5. Register locally -uncaged-workflow workflow add my-workflow ./dist/my-workflow-entry.esm.js - -# 6. Test -uncaged-workflow run my-workflow --prompt "test task" -uncaged-workflow live --latest -\`\`\` - -## Versioning - -Bundles are immutable and identified by XXH64 hash. Re-registering a workflow with a new bundle creates a new version. Use \`workflow history\` and \`workflow rollback\` to manage versions. - -## Pitfalls - -### Lazy initialization is mandatory - -The bundle is \`import()\`-ed at register time (\`workflow add\`) to read the descriptor. At that point, no runtime env vars (API keys, etc.) are available. - -**Never read env at module top-level.** Wrap provider/adapter creation in a lazy closure: - -\`\`\`typescript -// ❌ WRONG — breaks register -const provider = { apiKey: process.env.MY_KEY! }; -const adapter = createAdapter(provider); - -// ✅ CORRECT — only reads env when run() is called -function createLazyAdapter(): AdapterFn { - let cached: Provider | null = null; - return (prompt, schema) => { - return async (ctx, runtime) => { - if (!cached) cached = { apiKey: process.env.MY_KEY! }; - // ... use cached provider - }; - }; -} -\`\`\` - -### Agent CLI paths: use env() with absolute path defaults - -Every env var in a bundle must have a sensible default — bundles must run without any env vars set. Use \`env(name, fallback)\` from \`@uncaged/workflow-util\`. - -Discover the correct CLI path yourself (e.g. \`which cursor-agent\`, \`which hermes\`) and hardcode it as the fallback: - -\`\`\`typescript -import { env } from "@uncaged/workflow-util"; - -// ❌ WRONG — requireEnv and optionalEnv no longer exist -const adapter = createCursorAgent({ - command: requireEnv("WORKFLOW_CURSOR_COMMAND", "set it"), - ... -}); - -// ✅ CORRECT — env var is an override, fallback is the discovered absolute path -const adapter = createCursorAgent({ - command: env("WORKFLOW_CURSOR_COMMAND", "/home/you/.local/bin/cursor-agent"), - model: env("WORKFLOW_CURSOR_MODEL", "auto"), - timeout: Number(env("WORKFLOW_CURSOR_TIMEOUT", "300000")), - ... -}); -\`\`\` - -### Bundle import restrictions - -The bundle validator only allows these import specifiers: -- Node built-ins (\`node:fs\`, \`node:path\`, etc.) - -All other dependencies — including \`@uncaged/workflow-*\` packages, zod, and any third-party code — must be bundled into the \`.esm.js\` file. Bundles are fully self-contained: same Node/Bun version = same behavior. - -### No default exports - -The engine only reads named exports \`run\` and \`descriptor\`. Using \`export default\` will cause registration to fail silently. - -### Single-file ESM - -The bundle must be a single \`.esm.js\` file. No dynamic \`import()\` inside the bundle — it breaks hash verification and the loader sandbox. -`; -} diff --git a/legacy-packages/cli-workflow/src/storage-env.ts b/legacy-packages/cli-workflow/src/storage-env.ts deleted file mode 100644 index 100a0d6..0000000 --- a/legacy-packages/cli-workflow/src/storage-env.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getDefaultWorkflowStorageRoot } from "@uncaged/workflow-util"; - -/** - * Resolve storage root with env var override support. - * - * Priority (highest first): - * 1. `UNCAGED_WORKFLOW_STORAGE_ROOT` — internal/test override - * 2. `WORKFLOW_STORAGE_ROOT` — user-facing override - * 3. Default (`~/.uncaged/workflow`) - */ -export function resolveWorkflowStorageRoot(): string { - const internal = process.env.UNCAGED_WORKFLOW_STORAGE_ROOT; - if (internal !== undefined && internal !== "") { - return internal; - } - const userOverride = process.env.WORKFLOW_STORAGE_ROOT; - if (userOverride !== undefined && userOverride !== "") { - return userOverride; - } - return getDefaultWorkflowStorageRoot(); -} diff --git a/legacy-packages/cli-workflow/src/thread-scan.ts b/legacy-packages/cli-workflow/src/thread-scan.ts deleted file mode 100644 index 7b7ef0d..0000000 --- a/legacy-packages/cli-workflow/src/thread-scan.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { readdir, stat } from "node:fs/promises"; -import { join } from "node:path"; -import { createCasStore, parseCasThreadNode } from "@uncaged/workflow-cas"; -import { - readThreadsIndex, - type ThreadHistoryEntry, - type ThreadIndex, - walkStateFramesNewestFirst, -} from "@uncaged/workflow-execute"; -import { END } from "@uncaged/workflow-runtime"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -import { pathExists, readTextFileIfExists } from "./fs-utils.js"; -import { readWorkerCtl } from "./worker-spawn.js"; - -async function readWorkflowNameFromStartHash( - storageRoot: string, - startHash: string, -): Promise { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const yamlText = await cas.get(startHash); - if (yamlText === null) { - return null; - } - const parsed = parseCasThreadNode(yamlText); - if (parsed === null || parsed.kind !== "start") { - return null; - } - return parsed.node.payload.name; -} - -async function listBundleHashDirs(storageRoot: string): Promise { - const bundlesRoot = join(storageRoot, "bundles"); - if (!(await pathExists(bundlesRoot))) { - return []; - } - const names = await readdir(bundlesRoot); - const out: string[] = []; - for (const name of names) { - const p = join(bundlesRoot, name); - try { - const st = await stat(p); - if (st.isDirectory()) { - out.push(name); - } - } catch {} - } - out.sort(); - return out; -} - -async function parseHistoryFile(path: string): Promise { - const text = await readTextFileIfExists(path); - if (text === null) { - return []; - } - const out: ThreadHistoryEntry[] = []; - for (const line of text.split("\n")) { - const trimmed = line.trim(); - if (trimmed === "") { - continue; - } - let raw: unknown; - try { - raw = JSON.parse(trimmed) as unknown; - } catch { - continue; - } - if (raw === null || typeof raw !== "object") { - continue; - } - const rec = raw as Record; - const threadId = rec.threadId; - const head = rec.head; - const start = rec.start; - const completedAt = rec.completedAt; - if ( - typeof threadId !== "string" || - typeof head !== "string" || - typeof start !== "string" || - typeof completedAt !== "number" - ) { - continue; - } - out.push({ threadId, head, start, completedAt }); - } - return out; -} - -export type RunningThreadRow = { - threadId: string; - hash: string; - workflowName: string | null; -}; - -export type HistoricalThreadRow = { - threadId: string; - hash: string; - workflowName: string | null; - /** Active entry from `threads.json` vs completed line from `history/*.jsonl`. */ - source: "active" | "history"; - /** `updatedAt` for active threads; `completedAt` for history (ms since epoch). */ - activityTs: number; - /** Current CAS head (`threads.json` / history row). */ - head: string; -}; - -export type ResolvedThreadRecord = { - threadId: string; - bundleHash: string; - bundleDir: string; - head: string; - start: string; - source: "active" | "history"; -}; - -/** Resolve a thread via `threads.json` (active) or `history/*.jsonl` (completed). */ -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: scans all bundle dirs for thread id -export async function resolveThreadRecord( - storageRoot: string, - threadId: string, -): Promise { - const hashes = await listBundleHashDirs(storageRoot); - for (const bundleHash of hashes) { - const bundleDir = join(storageRoot, "bundles", bundleHash); - let index: ThreadIndex; - try { - index = await readThreadsIndex(bundleDir); - } catch { - continue; - } - const active = index[threadId]; - if (active !== undefined) { - return { - threadId, - bundleHash, - bundleDir, - head: active.head, - start: active.start, - source: "active", - }; - } - } - - for (const bundleHash of hashes) { - const bundleDir = join(storageRoot, "bundles", bundleHash); - const histDir = join(bundleDir, "history"); - if (!(await pathExists(histDir))) { - continue; - } - let files: string[]; - try { - files = await readdir(histDir); - } catch { - continue; - } - for (const name of files) { - if (!name.endsWith(".jsonl")) { - continue; - } - const entries = await parseHistoryFile(join(histDir, name)); - for (const e of entries) { - if (e.threadId === threadId) { - return { - threadId, - bundleHash, - bundleDir, - head: e.head, - start: e.start, - source: "history", - }; - } - } - } - } - - return null; -} - -export type ThreadHeadTerminal = - | { kind: "non-terminal" } - | { kind: "terminal"; returnCode: number }; - -/** True when the newest frame at `headHash` is `__end__` (workflow finished in CAS). */ -export async function readThreadTerminalFromHead( - storageRoot: string, - headHash: string, -): Promise { - const cas = createCasStore(getGlobalCasDir(storageRoot)); - const frames = await walkStateFramesNewestFirst(cas, headHash); - const newest = frames[0]; - if (newest === undefined) { - return { kind: "non-terminal" }; - } - if (newest.payload.role !== END) { - return { kind: "non-terminal" }; - } - const rc = newest.payload.meta.returnCode; - if (typeof rc !== "number") { - return { kind: "terminal", returnCode: 1 }; - } - return { kind: "terminal", returnCode: rc }; -} - -export type ThreadListStatus = "running" | "active" | "completed" | "failed"; - -/** Combines `.running` marker with CAS head: stale markers do not imply `running`. */ -export async function resolveThreadListStatus( - storageRoot: string, - row: HistoricalThreadRow, - runningMarkerPresent: boolean, -): Promise { - const terminal = await readThreadTerminalFromHead(storageRoot, row.head); - if (terminal.kind === "terminal") { - return terminal.returnCode !== 0 ? "failed" : "completed"; - } - if (row.source === "history") { - return "completed"; - } - if (runningMarkerPresent) { - const ctlResult = await readWorkerCtl(storageRoot, row.hash); - if (ctlResult.ok) { - try { - process.kill(ctlResult.value.pid, 0); - return "running"; - } catch { - // Worker PID is dead but .running marker remains — crashed thread - return "failed"; - } - } - return "running"; - } - // No .running marker + no __end__ + source "active" → check if worker is dead (crashed) - const ctlResult = await readWorkerCtl(storageRoot, row.hash); - if (!ctlResult.ok) { - // No ctl file means worker never registered or was already cleaned up — dead thread - return "failed"; - } - try { - process.kill(ctlResult.value.pid, 0); - } catch { - // Worker PID is dead, thread never finished — crashed - return "failed"; - } - return "active"; -} - -async function appendRunningThreadRowIfLive( - storageRoot: string, - hash: string, - threadId: string, - out: RunningThreadRow[], -): Promise { - const resolved = await resolveThreadRecord(storageRoot, threadId); - if (resolved !== null && resolved.bundleHash !== hash) { - return; - } - if (resolved !== null) { - const terminal = await readThreadTerminalFromHead(storageRoot, resolved.head); - if (terminal.kind === "terminal") { - return; - } - } - const workflowName = - resolved !== null ? await readWorkflowNameFromStartHash(storageRoot, resolved.start) : null; - out.push({ threadId, hash, workflowName }); -} - -/** Threads currently executing — identified via `.running` markers. */ -export async function listRunningThreads(storageRoot: string): Promise { - const logsRoot = join(storageRoot, "logs"); - if (!(await pathExists(logsRoot))) { - return []; - } - - const hashes = await readdir(logsRoot); - const out: RunningThreadRow[] = []; - - for (const hash of hashes) { - const dir = join(logsRoot, hash); - let entries: string[]; - try { - entries = await readdir(dir); - } catch { - continue; - } - - for (const fileName of entries) { - if (!fileName.endsWith(".running")) { - continue; - } - const threadId = fileName.slice(0, -".running".length); - await appendRunningThreadRowIfLive(storageRoot, hash, threadId, out); - } - } - - out.sort((a, b) => { - const ha = `${a.hash}/${a.threadId}`; - const hb = `${b.hash}/${b.threadId}`; - return ha.localeCompare(hb); - }); - - return out; -} - -/** - * Threads discovered via `threads.json` (active) and `history/*.jsonl` (completed). - * When `workflowNameFilter` is non-null, only threads whose StartNode `name` matches are returned. - */ -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: merges active index + partitioned history -export async function listHistoricalThreads( - storageRoot: string, - workflowNameFilter: string | null, -): Promise { - const hashes = await listBundleHashDirs(storageRoot); - const seen = new Set(); - const out: HistoricalThreadRow[] = []; - - for (const bundleHash of hashes) { - const bundleDir = join(storageRoot, "bundles", bundleHash); - let index: ThreadIndex; - try { - index = await readThreadsIndex(bundleDir); - } catch { - continue; - } - for (const threadId of Object.keys(index)) { - const key = `${bundleHash}/${threadId}`; - if (seen.has(key)) { - continue; - } - seen.add(key); - const entry = index[threadId]; - if (entry === undefined) { - continue; - } - const workflowName = await readWorkflowNameFromStartHash(storageRoot, entry.start); - if (workflowNameFilter !== null && workflowName !== workflowNameFilter) { - continue; - } - out.push({ - threadId, - hash: bundleHash, - workflowName, - source: "active", - activityTs: entry.updatedAt, - head: entry.head, - }); - } - - const histDir = join(bundleDir, "history"); - if (!(await pathExists(histDir))) { - continue; - } - let files: string[]; - try { - files = await readdir(histDir); - } catch { - continue; - } - for (const name of files) { - if (!name.endsWith(".jsonl")) { - continue; - } - const entries = await parseHistoryFile(join(histDir, name)); - for (const e of entries) { - const key = `${bundleHash}/${e.threadId}`; - if (seen.has(key)) { - continue; - } - seen.add(key); - const workflowName = await readWorkflowNameFromStartHash(storageRoot, e.start); - if (workflowNameFilter !== null && workflowName !== workflowNameFilter) { - continue; - } - out.push({ - threadId: e.threadId, - hash: bundleHash, - workflowName, - source: "history", - activityTs: e.completedAt, - head: e.head, - }); - } - } - } - - out.sort((a, b) => { - const ha = `${a.hash}/${a.threadId}`; - const hb = `${b.hash}/${b.threadId}`; - return ha.localeCompare(hb); - }); - - return out; -} - -export type LatestThreadTarget = { - threadId: string; - bundleHash: string; - bundleDir: string; - threadsJsonPath: string; -}; - -/** - * Picks the newest thread by StartNode timestamp approximation (`updatedAt` active, - * else `completedAt` history), falling back to lexical thread id order. - */ -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: compares active heads vs history tails -export async function findLatestThreadBundleTarget( - storageRoot: string, -): Promise { - const hashes = await listBundleHashDirs(storageRoot); - - let best: { - threadId: string; - bundleHash: string; - bundleDir: string; - ts: number; - } | null = null; - - for (const bundleHash of hashes) { - const bundleDir = join(storageRoot, "bundles", bundleHash); - let index: ThreadIndex; - try { - index = await readThreadsIndex(bundleDir); - } catch { - continue; - } - for (const threadId of Object.keys(index)) { - const ent = index[threadId]; - if (ent === undefined) { - continue; - } - const ts = ent.updatedAt; - const cand = { threadId, bundleHash, bundleDir, ts }; - if ( - best === null || - cand.ts > best.ts || - (cand.ts === best.ts && - `${cand.bundleHash}/${cand.threadId}` > `${best.bundleHash}/${best.threadId}`) - ) { - best = cand; - } - } - - const histDir = join(bundleDir, "history"); - if (!(await pathExists(histDir))) { - continue; - } - let files: string[]; - try { - files = await readdir(histDir); - } catch { - continue; - } - for (const name of files) { - if (!name.endsWith(".jsonl")) { - continue; - } - const entries = await parseHistoryFile(join(histDir, name)); - for (const e of entries) { - const ts = e.completedAt; - const cand = { threadId: e.threadId, bundleHash, bundleDir, ts }; - if ( - best === null || - cand.ts > best.ts || - (cand.ts === best.ts && - `${cand.bundleHash}/${cand.threadId}` > `${best.bundleHash}/${best.threadId}`) - ) { - best = cand; - } - } - } - } - - if (best === null) { - return null; - } - - return { - threadId: best.threadId, - bundleHash: best.bundleHash, - bundleDir: best.bundleDir, - threadsJsonPath: join(best.bundleDir, "threads.json"), - }; -} diff --git a/legacy-packages/cli-workflow/src/worker-spawn.ts b/legacy-packages/cli-workflow/src/worker-spawn.ts deleted file mode 100644 index 1a34f59..0000000 --- a/legacy-packages/cli-workflow/src/worker-spawn.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { type ChildProcess, spawn } from "node:child_process"; -import { mkdir, readdir, unlink, writeFile } from "node:fs/promises"; -import { createConnection } from "node:net"; -import { join } from "node:path"; -import { getWorkerHostScriptPath } from "@uncaged/workflow-execute"; -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import { pathExists, readTextFileIfExists } from "./fs-utils.js"; -import { readThreadTerminalFromHead, resolveThreadRecord } from "./thread-scan.js"; - -export type WorkerCtl = { - pid: number; - port: number; -}; - -function isProcessAlive(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -async function waitForReadyLine( - childStdout: NodeJS.ReadableStream, - child: ChildProcess, -): Promise> { - return await new Promise((resolve) => { - let buf = ""; - let settled = false; - - function finish(result: Result): void { - if (settled) { - return; - } - settled = true; - cleanup(); - resolve(result); - } - - function onData(chunk: Buffer | string): void { - buf += typeof chunk === "string" ? chunk : chunk.toString("utf8"); - const nl = buf.indexOf("\n"); - if (nl < 0) { - return; - } - const line = buf.slice(0, nl).trim(); - const prefix = "READY "; - if (!line.startsWith(prefix)) { - finish(err(`worker did not emit READY line (got: ${line})`)); - return; - } - const portText = line.slice(prefix.length); - const port = Number(portText); - if (!Number.isFinite(port) || port <= 0) { - finish(err(`worker READY line had invalid port: ${portText}`)); - return; - } - finish(ok(port)); - } - - function onEnd(): void { - finish(err("worker stdout ended before READY line")); - } - - function onExit(code: number | null): void { - finish(err(`worker exited before READY line (code ${code})`)); - } - - function cleanup(): void { - childStdout.off("data", onData); - childStdout.off("end", onEnd); - child.off("exit", onExit); - } - - childStdout.on("data", onData); - childStdout.on("end", onEnd); - child.on("exit", onExit); - }); -} - -async function spawnWorkerProcess( - bundlePath: string, - storageRoot: string, - hash: string, -): Promise> { - const scriptPath = getWorkerHostScriptPath(); - const child = spawn(process.execPath, [scriptPath, bundlePath, storageRoot, hash], { - stdio: ["ignore", "pipe", "inherit"], - }); - - if (child.stdout === null || child.pid === undefined) { - return err("failed to spawn worker process"); - } - - const pid = child.pid; - const ready = await waitForReadyLine(child.stdout, child); - if (!ready.ok) { - child.kill(); - return ready; - } - - child.unref(); - child.stdout.destroy(); - - return ok({ pid, port: ready.value }); -} - -export async function ensureWorkerForHash( - storageRoot: string, - hash: string, - bundlePath: string, -): Promise> { - const ctlPath = join(storageRoot, "workers", `${hash}.json`); - const existingText = await readTextFileIfExists(ctlPath); - if (existingText !== null) { - try { - const ctl = JSON.parse(existingText) as WorkerCtl; - if ( - typeof ctl.pid === "number" && - typeof ctl.port === "number" && - ctl.pid > 0 && - ctl.port > 0 && - isProcessAlive(ctl.pid) - ) { - return ok({ port: ctl.port }); - } - } catch { - // Corrupt control file — ignore and respawn. - } - await unlink(ctlPath).catch(() => {}); - } - - const spawned = await spawnWorkerProcess(bundlePath, storageRoot, hash); - if (!spawned.ok) { - return spawned; - } - - await mkdir(join(storageRoot, "workers"), { recursive: true }); - const ctl: WorkerCtl = { pid: spawned.value.pid, port: spawned.value.port }; - await writeFile(ctlPath, `${JSON.stringify(ctl)}\n`, "utf8"); - - return ok({ port: spawned.value.port }); -} - -export type SendWorkerTcpOptions = { - awaitResponseLine: boolean; -}; - -function parseWorkerControlResponseLine(line: string): Result { - let parsed: unknown; - try { - parsed = JSON.parse(line.trim()) as unknown; - } catch { - return err("invalid JSON in worker response"); - } - if (parsed === null || typeof parsed !== "object") { - return err("invalid worker response shape"); - } - const rec = parsed as Record; - if (rec.ok === true) { - return ok(undefined); - } - if (rec.ok === false) { - const message = rec.error; - if (typeof message === "string") { - return err(message); - } - return err("worker error response missing error string"); - } - return err("invalid worker response: missing ok field"); -} - -export async function sendWorkerTcpCommand( - port: number, - payload: unknown, - options: SendWorkerTcpOptions = { awaitResponseLine: false }, -): Promise> { - return await new Promise((resolve) => { - let settled = false; - let buf = ""; - const socket = createConnection({ host: "127.0.0.1", port }, () => { - socket.write(`${JSON.stringify(payload)}\n`); - if (!options.awaitResponseLine) { - socket.end(); - } - }); - - function finish(result: Result): void { - if (settled) { - return; - } - settled = true; - if (options.awaitResponseLine && socket.writable) { - socket.end(); - } - resolve(result); - } - - function tryFinishFromBuffer(): void { - if (!options.awaitResponseLine) { - return; - } - const nl = buf.indexOf("\n"); - if (nl < 0) { - return; - } - finish(parseWorkerControlResponseLine(buf.slice(0, nl))); - } - - socket.on("data", (chunk: Buffer | string) => { - if (!options.awaitResponseLine) { - return; - } - buf += typeof chunk === "string" ? chunk : chunk.toString("utf8"); - tryFinishFromBuffer(); - }); - - socket.on("error", (e) => { - if (settled) { - return; - } - const message = e instanceof Error ? e.message : String(e); - finish(err(`failed to send worker command: ${message}`)); - }); - - socket.on("close", () => { - if (options.awaitResponseLine) { - tryFinishFromBuffer(); - if (!settled) { - finish(err("worker closed without control response")); - } - return; - } - finish(ok(undefined)); - }); - }); -} - -export async function readWorkerCtl( - storageRoot: string, - hash: string, -): Promise> { - 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( - storageRoot: string, - threadId: string, -): Promise> { - const logsRoot = join(storageRoot, "logs"); - if (!(await pathExists(logsRoot))) { - return err(`thread not running (no logs dir): ${threadId}`); - } - const resolved = await resolveThreadRecord(storageRoot, threadId); - if (resolved !== null) { - const runningPath = join(logsRoot, resolved.bundleHash, `${threadId}.running`); - if (!(await pathExists(runningPath))) { - return err(`thread not running: ${threadId}`); - } - const terminal = await readThreadTerminalFromHead(storageRoot, resolved.head); - if (terminal.kind === "terminal") { - return err(`thread not running: ${threadId}`); - } - return ok(resolved.bundleHash); - } - - let hashes: string[]; - try { - hashes = await readdir(logsRoot); - } catch { - return err(`thread not running: ${threadId}`); - } - for (const hash of hashes) { - const runningPath = join(logsRoot, hash, `${threadId}.running`); - if (await pathExists(runningPath)) { - return ok(hash); - } - } - return err(`thread not running: ${threadId}`); -} diff --git a/legacy-packages/cli-workflow/src/workflow-name.ts b/legacy-packages/cli-workflow/src/workflow-name.ts deleted file mode 100644 index 2e0c4f4..0000000 --- a/legacy-packages/cli-workflow/src/workflow-name.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -const WORKFLOW_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/; - -export function validateCliWorkflowName(name: string): Result { - if (!WORKFLOW_NAME_RE.test(name)) { - return err( - 'invalid workflow name: use verb-first kebab-case (lowercase letters, digits, hyphens), e.g. "solve-issue"', - ); - } - return ok(undefined); -} diff --git a/legacy-packages/cli-workflow/tsconfig.json b/legacy-packages/cli-workflow/tsconfig.json deleted file mode 100644 index 9ca68cf..0000000 --- a/legacy-packages/cli-workflow/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022"], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "exactOptionalPropertyTypes": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "composite": true, - "outDir": "dist", - "rootDir": "src", - "types": ["bun-types"] - }, - "references": [ - { "path": "../workflow-runtime" }, - { "path": "../workflow-protocol" }, - { "path": "../workflow-util" }, - { "path": "../workflow-cas" }, - { "path": "../workflow-execute" }, - { "path": "../workflow-register" } - ], - "include": ["src/**/*.ts"] -} diff --git a/legacy-packages/workflow-agent-cursor/CHANGELOG.md b/legacy-packages/workflow-agent-cursor/CHANGELOG.md deleted file mode 100644 index d2c7d2e..0000000 --- a/legacy-packages/workflow-agent-cursor/CHANGELOG.md +++ /dev/null @@ -1,119 +0,0 @@ -# @uncaged/workflow-agent-cursor - -## 0.5.0-alpha.4 - -### Patch Changes - -- Updated dependencies -- Updated dependencies [f74b482] -- Updated dependencies [f74b482] - - @uncaged/workflow-util@0.5.0-alpha.4 - - @uncaged/workflow-protocol@0.5.0-alpha.4 - - @uncaged/workflow-cas@0.5.0-alpha.4 - - @uncaged/workflow-runtime@0.5.0-alpha.4 - - @uncaged/workflow-util-agent@0.5.0-alpha.4 - -## 0.5.0-alpha.3 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.3 - - @uncaged/workflow-cas@0.5.0-alpha.3 - - @uncaged/workflow-runtime@0.5.0-alpha.3 - - @uncaged/workflow-util@0.5.0-alpha.3 - - @uncaged/workflow-util-agent@0.5.0-alpha.3 - -## 0.5.0-alpha.2 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.2 - - @uncaged/workflow-cas@0.5.0-alpha.2 - - @uncaged/workflow-runtime@0.5.0-alpha.2 - - @uncaged/workflow-util@0.5.0-alpha.2 - - @uncaged/workflow-util-agent@0.5.0-alpha.2 - -## 0.5.0-alpha.1 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-util-agent@0.5.0-alpha.1 - - @uncaged/workflow-cas@0.5.0-alpha.1 - - @uncaged/workflow-protocol@0.5.0-alpha.1 - - @uncaged/workflow-runtime@0.5.0-alpha.1 - - @uncaged/workflow-util@0.5.0-alpha.1 - -## 0.5.0-alpha.0 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.0 - - @uncaged/workflow-cas@0.5.0-alpha.0 - - @uncaged/workflow-runtime@0.5.0-alpha.0 - - @uncaged/workflow-util@0.5.0-alpha.0 - - @uncaged/workflow-util-agent@0.5.0-alpha.0 - -## 0.4.5 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.5 - - @uncaged/workflow-reactor@0.4.5 - - @uncaged/workflow-runtime@0.4.5 - - @uncaged/workflow-util@0.4.5 - - @uncaged/workflow-util-agent@0.4.5 - -## 0.4.4 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.4 - - @uncaged/workflow-reactor@0.4.4 - - @uncaged/workflow-runtime@0.4.4 - - @uncaged/workflow-util@0.4.4 - - @uncaged/workflow-util-agent@0.4.4 - -## 0.4.3 - -### Patch Changes - -- Include src/ in published packages so bun runtime can resolve the 'bun' exports condition. -- Updated dependencies - - @uncaged/workflow-protocol@0.4.3 - - @uncaged/workflow-reactor@0.4.3 - - @uncaged/workflow-runtime@0.4.3 - - @uncaged/workflow-util-agent@0.4.3 - - @uncaged/workflow-util@0.4.3 - -## 0.4.2 - -### Patch Changes - -- Fix workspace dependency resolution: use workspace:^ so published packages resolve to compatible versions instead of exact (non-existent) versions. -- Updated dependencies - - @uncaged/workflow-protocol@0.4.2 - - @uncaged/workflow-reactor@0.4.2 - - @uncaged/workflow-runtime@0.4.2 - - @uncaged/workflow-util-agent@0.4.2 - - @uncaged/workflow-util@0.4.2 - -## 0.4.0 - -### Minor Changes - -- Fix package exports for published packages and adopt changesets for version management. - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.0 - - @uncaged/workflow-reactor@0.4.0 - - @uncaged/workflow-runtime@0.4.0 - - @uncaged/workflow-util-agent@0.4.0 - - @uncaged/workflow-util@0.4.0 diff --git a/legacy-packages/workflow-agent-cursor/README.md b/legacy-packages/workflow-agent-cursor/README.md deleted file mode 100644 index 51534a1..0000000 --- a/legacy-packages/workflow-agent-cursor/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# @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-runtime @uncaged/workflow-util-agent zod -``` - -In this monorepo: `"@uncaged/workflow-agent-cursor": "workspace:*"` plus `workspace:*` for `@uncaged/workflow-runtime` and `@uncaged/workflow-util-agent`, and `zod` ^4. - -## 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)` from `@uncaged/workflow-util-agent` | -| `CursorAgentConfig` | `model`, `timeout`, `extract` (must supply workspace path) | -| `validateCursorAgentConfig` | Config validation result | - -Requires `cursor-agent` on `PATH` at runtime. diff --git a/legacy-packages/workflow-agent-cursor/__tests__/cursor-agent.test.ts b/legacy-packages/workflow-agent-cursor/__tests__/cursor-agent.test.ts deleted file mode 100644 index b4b1a0f..0000000 --- a/legacy-packages/workflow-agent-cursor/__tests__/cursor-agent.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { createCursorAgent, validateCursorAgentConfig } from "../src/index.js"; - -const baseConfig = { - command: "/usr/local/bin/cursor-agent", - model: null as string | null, - timeout: 0, - workspace: null as string | null, -}; - -describe("validateCursorAgentConfig", () => { - test("accepts valid config", () => { - const r = validateCursorAgentConfig({ - ...baseConfig, - }); - expect(r.ok).toBe(true); - }); - - test("rejects non-absolute command", () => { - const r = validateCursorAgentConfig({ - ...baseConfig, - command: "cursor-agent", - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("absolute path"); - } - }); - - test("rejects negative timeout", () => { - const r = validateCursorAgentConfig({ - ...baseConfig, - timeout: -1, - }); - expect(r.ok).toBe(false); - }); - - test("rejects non-absolute workspace when set", () => { - const r = validateCursorAgentConfig({ - ...baseConfig, - workspace: "relative/path", - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("workspace"); - } - }); -}); - -describe("createCursorAgent", () => { - test("returns an AdapterFn", () => { - const agent = createCursorAgent({ - ...baseConfig, - }); - expect(typeof agent).toBe("function"); - }); - - test("defers validation to call time (invalid config does not throw at construction)", () => { - const agent = createCursorAgent({ - ...baseConfig, - timeout: -1, - }); - expect(typeof agent).toBe("function"); - }); -}); diff --git a/legacy-packages/workflow-agent-cursor/package.json b/legacy-packages/workflow-agent-cursor/package.json deleted file mode 100644 index e538b52..0000000 --- a/legacy-packages/workflow-agent-cursor/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@uncaged/workflow-agent-cursor", - "version": "0.5.0-alpha.4", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "types": "src/index.ts", - "scripts": { - "test": "bun test" - }, - "dependencies": { - "@uncaged/workflow-cas": "workspace:^", - "@uncaged/workflow-protocol": "workspace:^", - "@uncaged/workflow-runtime": "workspace:^", - "@uncaged/workflow-util": "workspace:^", - "@uncaged/workflow-util-agent": "workspace:^", - "zod": "^4.0.0" - }, - "exports": { - ".": { - "bun": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/workflow-agent-cursor/pnpm-lock.yaml b/legacy-packages/workflow-agent-cursor/pnpm-lock.yaml deleted file mode 100644 index dd3cce8..0000000 --- a/legacy-packages/workflow-agent-cursor/pnpm-lock.yaml +++ /dev/null @@ -1,28 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@uncaged/workflow-runtime': - specifier: workspace:* - version: link:../workflow-runtime - '@uncaged/workflow-util-agent': - specifier: workspace:* - version: link:../workflow-util-agent - zod: - specifier: ^4.0.0 - version: 4.4.3 - -packages: - - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - -snapshots: - - zod@4.4.3: {} diff --git a/legacy-packages/workflow-agent-cursor/src/extract-workspace.ts b/legacy-packages/workflow-agent-cursor/src/extract-workspace.ts deleted file mode 100644 index a6e030e..0000000 --- a/legacy-packages/workflow-agent-cursor/src/extract-workspace.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { putContentNodeWithRefs } from "@uncaged/workflow-cas"; -import type { ThreadContext, WorkflowRuntime } from "@uncaged/workflow-runtime"; -import type { LogFn } from "@uncaged/workflow-util"; -import * as z from "zod/v4"; - -const workspaceSchema = z.object({ - workspace: z.string().describe("Absolute filesystem path of the project workspace"), -}); - -function buildExtractionInput(ctx: ThreadContext): string { - const lines: string[] = []; - lines.push("## Task"); - lines.push(ctx.start.content); - - for (const step of ctx.steps) { - lines.push(""); - lines.push(`## Step: ${step.role}`); - lines.push(`Meta: ${JSON.stringify(step.meta)}`); - } - - lines.push(""); - lines.push( - "Extract the absolute filesystem path of the project workspace where code changes should be made.", - ); - - return lines.join("\n"); -} - -export async function extractWorkspacePath( - ctx: ThreadContext, - runtime: WorkflowRuntime, - logger: LogFn, -): Promise { - const input = buildExtractionInput(ctx); - const contentHash = await putContentNodeWithRefs(runtime.cas, input, []); - - const result = await runtime.extract(workspaceSchema, contentHash); - const workspace = result.meta.workspace.trim(); - - if (!workspace.startsWith("/")) { - logger("H4PM7RXV", `workspace extraction returned non-absolute path: ${workspace}`); - return null; - } - - logger("V3KM8QWP", `extracted workspace: ${workspace}`); - return workspace; -} diff --git a/legacy-packages/workflow-agent-cursor/src/index.ts b/legacy-packages/workflow-agent-cursor/src/index.ts deleted file mode 100644 index b3a7fec..0000000 --- a/legacy-packages/workflow-agent-cursor/src/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { AdapterFn, AgentFn, WorkflowRuntime } from "@uncaged/workflow-runtime"; -import { createLogger, type LogFn } from "@uncaged/workflow-util"; -import { - buildThreadInput, - createAgentAdapter, - type SpawnCliError, - spawnCli, -} from "@uncaged/workflow-util-agent"; - -import { extractWorkspacePath } from "./extract-workspace.js"; -import type { CursorAgentConfig } from "./types.js"; -import { validateCursorAgentConfig } from "./validate-config.js"; - -export type { CursorAgentConfig } from "./types.js"; -export { validateCursorAgentConfig } from "./validate-config.js"; - -function throwCursorSpawnError(error: SpawnCliError): never { - if (error.kind === "non_zero_exit") { - throw new Error( - `cursor-agent: exitCode=${error.exitCode} stdout=${error.stdout} stderr=${error.stderr}`, - ); - } - if (error.kind === "timeout") { - throw new Error("cursor-agent: timeout"); - } - if (error.kind === "spawn_failed") { - throw new Error(`cursor-agent: ${error.message}`); - } - throw new Error("cursor-agent: unknown spawn error"); -} - -function resolveCursorModel(model: string | null): string { - return model === null ? "auto" : model; -} - -type CursorAgentOpt = { prompt: string; workspace: string }; - -function createCursorAgentFn( - config: CursorAgentConfig, - modelFlag: string, - timeoutMs: number | null, - logger: LogFn, -): AgentFn { - return async (ctx, { prompt, workspace }) => { - logger("R5HN3YKQ", `cursor-agent workspace: ${workspace}`); - const threadInput = await buildThreadInput(ctx); - const fullPrompt = `${prompt}\n\n${threadInput}`; - const args = [ - "-p", - fullPrompt, - "--model", - modelFlag, - "--workspace", - workspace, - "--output-format", - "text", - "--trust", - "--force", - ]; - const run = await spawnCli(config.command, args, { - cwd: workspace, - timeoutMs, - }); - if (!run.ok) { - throwCursorSpawnError(run.error); - } - return run.value; - }; -} - -/** Runs `cursor-agent` with workspace from config or extracted from thread context via runtime.extract. */ -export function createCursorAgent(config: CursorAgentConfig): AdapterFn { - const modelFlag = resolveCursorModel(config.model); - const timeoutMs = config.timeout > 0 ? config.timeout : null; - const logger = createLogger({ sink: { kind: "stderr" } }); - - return createAgentAdapter( - createCursorAgentFn(config, modelFlag, timeoutMs, logger), - async (ctx, prompt, runtime: WorkflowRuntime) => { - const validated = validateCursorAgentConfig(config); - if (!validated.ok) { - throw new Error(validated.error); - } - - const workspace = - config.workspace !== null - ? config.workspace - : await extractWorkspacePath(ctx, runtime, logger); - if (workspace === null) { - throw new Error( - "cursor-agent: failed to extract workspace path from context. Ensure the task prompt or previous steps include a project path.", - ); - } - return { prompt, workspace }; - }, - ); -} diff --git a/legacy-packages/workflow-agent-cursor/src/types.ts b/legacy-packages/workflow-agent-cursor/src/types.ts deleted file mode 100644 index 08069c4..0000000 --- a/legacy-packages/workflow-agent-cursor/src/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type CursorAgentConfig = { - /** Absolute path to the cursor-agent CLI binary. */ - command: string; - model: string | null; - timeout: number; - /** - * When non-null, use this workspace directory for `cursor-agent` instead of resolving it - * from the thread via runtime extraction. - */ - workspace: string | null; -}; diff --git a/legacy-packages/workflow-agent-cursor/src/validate-config.ts b/legacy-packages/workflow-agent-cursor/src/validate-config.ts deleted file mode 100644 index 9c25d5d..0000000 --- a/legacy-packages/workflow-agent-cursor/src/validate-config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isAbsolute } from "node:path"; - -import { err, ok, type Result } from "@uncaged/workflow-protocol"; - -import type { CursorAgentConfig } from "./types.js"; - -export function validateCursorAgentConfig(config: CursorAgentConfig): Result { - if (!isAbsolute(config.command)) { - return err("command must be an absolute path to the cursor-agent CLI binary"); - } - if (config.timeout < 0) { - return err("timeout must be a non-negative number (milliseconds); use 0 for no limit"); - } - if (config.workspace !== null && !isAbsolute(config.workspace)) { - return err("workspace must be an absolute filesystem path when set"); - } - return ok(undefined); -} diff --git a/legacy-packages/workflow-agent-cursor/tsconfig.json b/legacy-packages/workflow-agent-cursor/tsconfig.json deleted file mode 100644 index c9b8393..0000000 --- a/legacy-packages/workflow-agent-cursor/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "composite": true - }, - "include": ["src/**/*.ts"], - "references": [ - { "path": "../workflow-cas" }, - { "path": "../workflow-runtime" }, - { "path": "../workflow-util-agent" } - ] -} diff --git a/legacy-packages/workflow-agent-docx-diff/__tests__/agent.test.ts b/legacy-packages/workflow-agent-docx-diff/__tests__/agent.test.ts deleted file mode 100644 index 8986c48..0000000 --- a/legacy-packages/workflow-agent-docx-diff/__tests__/agent.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { createDocxDiffAgent } from "../src/agent.js"; -import { packageDescriptor } from "../src/package-descriptor.js"; - -describe("createDocxDiffAgent", () => { - test("returns an AdapterFn (function)", () => { - const agent = createDocxDiffAgent({ command: null }); - expect(typeof agent).toBe("function"); - }); - - test("AdapterFn returns a RoleFn (function)", () => { - const agent = createDocxDiffAgent({ command: null }); - const roleFn = agent("", expect.anything() as never); - expect(typeof roleFn).toBe("function"); - }); -}); - -describe("packageDescriptor", () => { - test("has correct name", () => { - expect(packageDescriptor.name).toBe("@uncaged/workflow-agent-docx-diff"); - }); -}); diff --git a/legacy-packages/workflow-agent-docx-diff/__tests__/runner.test.ts b/legacy-packages/workflow-agent-docx-diff/__tests__/runner.test.ts deleted file mode 100644 index 47ba217..0000000 --- a/legacy-packages/workflow-agent-docx-diff/__tests__/runner.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { err, ok } from "@uncaged/workflow-util"; -import type { SpawnCliConfig } from "@uncaged/workflow-util-agent"; -import { runDocxDiff } from "../src/runner.js"; - -type MockSpawnResult = Awaited>; - -function makeSpawn(result: MockSpawnResult) { - return mock(async (_cmd: string, _args: string[], _opts: SpawnCliConfig) => result); -} - -function tempDir(): string { - const dir = join(tmpdir(), `diff-test-${Date.now()}`); - mkdirSync(dir, { recursive: true }); - return dir; -} - -describe("runDocxDiff", () => { - test("exit 0: success, returns DifferMeta JSON", async () => { - const dir = tempDir(); - const sourceDocx = join(dir, "original.docx"); - const modifiedDocx = join(dir, "modified.docx"); - const diffDocx = join(dir, "diff.docx"); - writeFileSync(sourceDocx, ""); - writeFileSync(modifiedDocx, ""); - - const spawnFn = makeSpawn(ok("") as MockSpawnResult); - // simulate docx-diff creating the diff file - writeFileSync(diffDocx, ""); - - const raw = await runDocxDiff( - { command: "docx-diff" }, - sourceDocx, - modifiedDocx, - diffDocx, - spawnFn, - ); - const meta = JSON.parse(raw); - expect(meta.sourceDocx).toBe(sourceDocx); - expect(meta.modifiedDocx).toBe(modifiedDocx); - expect(meta.diffDocx).toBe(diffDocx); - - expect(spawnFn.mock.calls[0][1]).toEqual([ - sourceDocx, - modifiedDocx, - "--output", - "docx", - "--out-file", - diffDocx, - ]); - }); - - test("exit 1 (changes found): treated as success", async () => { - const dir = tempDir(); - const sourceDocx = join(dir, "s.docx"); - const modifiedDocx = join(dir, "m.docx"); - const diffDocx = join(dir, "diff.docx"); - writeFileSync(sourceDocx, ""); - writeFileSync(modifiedDocx, ""); - writeFileSync(diffDocx, ""); - - const spawnFn = makeSpawn( - err({ kind: "non_zero_exit", exitCode: 1, stdout: "", stderr: "" }) as MockSpawnResult, - ); - - await expect( - runDocxDiff({ command: "docx-diff" }, sourceDocx, modifiedDocx, diffDocx, spawnFn), - ).resolves.toBeDefined(); - }); - - test("exit 2: throws error", async () => { - const dir = tempDir(); - const spawnFn = makeSpawn( - err({ - kind: "non_zero_exit", - exitCode: 2, - stdout: "", - stderr: "fatal error", - }) as MockSpawnResult, - ); - - await expect( - runDocxDiff({ command: null }, "s.docx", "m.docx", "diff.docx", spawnFn), - ).rejects.toThrow("docx-diff failed"); - }); - - test("timeout: throws error", async () => { - const spawnFn = makeSpawn(err({ kind: "timeout" }) as MockSpawnResult); - - await expect( - runDocxDiff({ command: null }, "s.docx", "m.docx", "diff.docx", spawnFn), - ).rejects.toThrow("timed out"); - }); - - test("throws when diff file not created", async () => { - const dir = tempDir(); - const spawnFn = makeSpawn(ok("") as MockSpawnResult); - // do NOT create diffDocx - - await expect( - runDocxDiff({ command: null }, "s.docx", "m.docx", join(dir, "missing.docx"), spawnFn), - ).rejects.toThrow("diff file not found"); - }); - - test("uses PATH docx-diff when command is null", async () => { - const dir = tempDir(); - const diffDocx = join(dir, "diff.docx"); - writeFileSync(diffDocx, ""); - const spawnFn = makeSpawn(ok("") as MockSpawnResult); - - await runDocxDiff({ command: null }, "s.docx", "m.docx", diffDocx, spawnFn); - - expect(spawnFn.mock.calls[0][0]).toBe("docx-diff"); - }); -}); diff --git a/legacy-packages/workflow-agent-docx-diff/package.json b/legacy-packages/workflow-agent-docx-diff/package.json deleted file mode 100644 index fd28635..0000000 --- a/legacy-packages/workflow-agent-docx-diff/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@uncaged/workflow-agent-docx-diff", - "version": "0.1.0", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "types": "src/index.ts", - "exports": { - ".": { - "bun": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "test": "bun test" - }, - "dependencies": { - "@uncaged/workflow-runtime": "workspace:^", - "@uncaged/workflow-util-agent": "workspace:^", - "@uncaged/workflow-template-document": "workspace:^", - "zod": "^4.0.0" - }, - "devDependencies": { - "@uncaged/workflow-util": "workspace:^" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/workflow-agent-docx-diff/src/agent.ts b/legacy-packages/workflow-agent-docx-diff/src/agent.ts deleted file mode 100644 index 49d1300..0000000 --- a/legacy-packages/workflow-agent-docx-diff/src/agent.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { dirname, join } from "node:path"; -import type { - AdapterFn, - RoleResult, - ThreadContext, - WorkflowRuntime, -} from "@uncaged/workflow-runtime"; -import type { WriterMeta } from "@uncaged/workflow-template-document"; -import type * as z from "zod/v4"; -import { runDocxDiff } from "./runner.js"; -import type { DocxDiffAgentConfig } from "./types.js"; - -export function createDocxDiffAgent(config: DocxDiffAgentConfig): AdapterFn { - return (_prompt: string, schema: z.ZodType) => - async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise> => { - const writerStep = ctx.steps.find((s) => s.role === "writer"); - if (writerStep === undefined) throw new Error("differ: no writer step found"); - - const writerMeta = writerStep.meta as WriterMeta; - if (writerMeta.mode !== "edit") throw new Error("differ: writer did not run in edit mode"); - - const diffDocx = join(dirname(writerMeta.outputDocx), "diff.docx"); - const raw = await runDocxDiff(config, writerMeta.sourceDocx, writerMeta.outputDocx, diffDocx); - - const meta = schema.parse(JSON.parse(raw)) as T; - return { meta, childThread: null }; - }; -} diff --git a/legacy-packages/workflow-agent-docx-diff/src/index.ts b/legacy-packages/workflow-agent-docx-diff/src/index.ts deleted file mode 100644 index 1dcc0a3..0000000 --- a/legacy-packages/workflow-agent-docx-diff/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { createDocxDiffAgent } from "./agent.js"; -export { packageDescriptor } from "./package-descriptor.js"; -export type { DocxDiffAgentConfig } from "./types.js"; diff --git a/legacy-packages/workflow-agent-docx-diff/src/package-descriptor.ts b/legacy-packages/workflow-agent-docx-diff/src/package-descriptor.ts deleted file mode 100644 index 60c9925..0000000 --- a/legacy-packages/workflow-agent-docx-diff/src/package-descriptor.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { PackageDescriptor } from "@uncaged/workflow-runtime"; - -export const packageDescriptor: PackageDescriptor = { - name: "@uncaged/workflow-agent-docx-diff", - version: "0.1.0", - capabilities: ["docx-diff-cli", "docx-diff-report"], - configSchema: { - type: "object", - properties: { - command: { - anyOf: [{ type: "string" }, { type: "null" }], - description: "Path to docx-diff CLI binary; null uses PATH.", - }, - }, - additionalProperties: false, - }, -}; diff --git a/legacy-packages/workflow-agent-docx-diff/src/runner.ts b/legacy-packages/workflow-agent-docx-diff/src/runner.ts deleted file mode 100644 index 05eb230..0000000 --- a/legacy-packages/workflow-agent-docx-diff/src/runner.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { stat } from "node:fs/promises"; -import type { SpawnCliError } from "@uncaged/workflow-util-agent"; -import { spawnCli } from "@uncaged/workflow-util-agent"; -import type { DocxDiffAgentConfig } from "./types.js"; - -type SpawnCliFn = typeof spawnCli; - -function throwSpawnError(e: SpawnCliError): never { - if (e.kind === "non_zero_exit") - throw new Error(`docx-diff failed (exit ${e.exitCode}): ${e.stderr}`); - if (e.kind === "timeout") throw new Error("docx-diff: timed out"); - throw new Error(`docx-diff: spawn failed: ${e.message}`); -} - -export async function runDocxDiff( - config: DocxDiffAgentConfig, - sourceDocx: string, - modifiedDocx: string, - diffDocx: string, - spawnCliFn: SpawnCliFn = spawnCli, -): Promise { - const command = config.command ?? "docx-diff"; - const result = await spawnCliFn( - command, - [sourceDocx, modifiedDocx, "--output", "docx", "--out-file", diffDocx], - { cwd: null, timeoutMs: null }, - ); - - if (!result.ok) { - const e = result.error; - // exit 1 = changes found (normal for docx-diff) - if (e.kind === "non_zero_exit" && e.exitCode === 1) { - // fall through to file check - } else { - throwSpawnError(e); - } - } - - try { - await stat(diffDocx); - } catch { - throw new Error(`docx-diff: diff file not found: ${diffDocx}`); - } - - return JSON.stringify({ sourceDocx, modifiedDocx, diffDocx }); -} diff --git a/legacy-packages/workflow-agent-docx-diff/src/types.ts b/legacy-packages/workflow-agent-docx-diff/src/types.ts deleted file mode 100644 index 4bcc174..0000000 --- a/legacy-packages/workflow-agent-docx-diff/src/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type DocxDiffAgentConfig = { - command: string | null; -}; diff --git a/legacy-packages/workflow-agent-docx-diff/tsconfig.json b/legacy-packages/workflow-agent-docx-diff/tsconfig.json deleted file mode 100644 index e0a2bf1..0000000 --- a/legacy-packages/workflow-agent-docx-diff/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "composite": true - }, - "include": ["src/**/*.ts"], - "references": [ - { "path": "../workflow-protocol" }, - { "path": "../workflow-runtime" }, - { "path": "../workflow-util-agent" }, - { "path": "../workflow-template-document" } - ] -} diff --git a/legacy-packages/workflow-agent-hermes/CHANGELOG.md b/legacy-packages/workflow-agent-hermes/CHANGELOG.md deleted file mode 100644 index 322682e..0000000 --- a/legacy-packages/workflow-agent-hermes/CHANGELOG.md +++ /dev/null @@ -1,81 +0,0 @@ -# @uncaged/workflow-agent-hermes - -## 0.5.0-alpha.4 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.4 -- @uncaged/workflow-util-agent@0.5.0-alpha.4 - -## 0.5.0-alpha.3 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.3 -- @uncaged/workflow-util-agent@0.5.0-alpha.3 - -## 0.5.0-alpha.2 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.2 -- @uncaged/workflow-util-agent@0.5.0-alpha.2 - -## 0.5.0-alpha.1 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-util-agent@0.5.0-alpha.1 - - @uncaged/workflow-runtime@0.5.0-alpha.1 - -## 0.5.0-alpha.0 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.0 -- @uncaged/workflow-util-agent@0.5.0-alpha.0 - -## 0.4.5 - -### Patch Changes - -- @uncaged/workflow-runtime@0.4.5 -- @uncaged/workflow-util-agent@0.4.5 - -## 0.4.4 - -### Patch Changes - -- @uncaged/workflow-runtime@0.4.4 -- @uncaged/workflow-util-agent@0.4.4 - -## 0.4.3 - -### Patch Changes - -- Include src/ in published packages so bun runtime can resolve the 'bun' exports condition. -- Updated dependencies - - @uncaged/workflow-runtime@0.4.3 - - @uncaged/workflow-util-agent@0.4.3 - -## 0.4.2 - -### Patch Changes - -- Fix workspace dependency resolution: use workspace:^ so published packages resolve to compatible versions instead of exact (non-existent) versions. -- Updated dependencies - - @uncaged/workflow-runtime@0.4.2 - - @uncaged/workflow-util-agent@0.4.2 - -## 0.4.0 - -### Minor Changes - -- Fix package exports for published packages and adopt changesets for version management. - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-runtime@0.4.0 - - @uncaged/workflow-util-agent@0.4.0 diff --git a/legacy-packages/workflow-agent-hermes/README.md b/legacy-packages/workflow-agent-hermes/README.md deleted file mode 100644 index ba3bcf0..0000000 --- a/legacy-packages/workflow-agent-hermes/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# @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-runtime @uncaged/workflow-util-agent -``` - -In this monorepo: use `workspace:*` for `@uncaged/workflow-agent-hermes`, `@uncaged/workflow-runtime`, and `@uncaged/workflow-util-agent`. - -## 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 | - -Requires `hermes` on `PATH` at runtime. diff --git a/legacy-packages/workflow-agent-hermes/__tests__/hermes-agent.test.ts b/legacy-packages/workflow-agent-hermes/__tests__/hermes-agent.test.ts deleted file mode 100644 index fe4dd8e..0000000 --- a/legacy-packages/workflow-agent-hermes/__tests__/hermes-agent.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { createHermesAgent, validateHermesAgentConfig } from "../src/index.js"; - -describe("validateHermesAgentConfig", () => { - test("accepts valid config", () => { - const r = validateHermesAgentConfig({ - command: "/usr/local/bin/hermes", - model: null, - timeout: null, - }); - expect(r.ok).toBe(true); - }); - - test("rejects non-absolute command", () => { - const r = validateHermesAgentConfig({ - command: "hermes", - model: null, - timeout: null, - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("absolute path"); - } - }); - - test("rejects negative timeout", () => { - const r = validateHermesAgentConfig({ - command: "/usr/local/bin/hermes", - model: null, - timeout: -5, - }); - expect(r.ok).toBe(false); - if (!r.ok) { - expect(r.error).toContain("timeout"); - } - }); -}); - -describe("createHermesAgent", () => { - test("returns an AdapterFn even with invalid config (validation deferred to call)", () => { - const agent = createHermesAgent({ - command: "/usr/local/bin/hermes", - model: null, - timeout: -5, - }); - expect(typeof agent).toBe("function"); - }); -}); diff --git a/legacy-packages/workflow-agent-hermes/package.json b/legacy-packages/workflow-agent-hermes/package.json deleted file mode 100644 index e3f7201..0000000 --- a/legacy-packages/workflow-agent-hermes/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@uncaged/workflow-agent-hermes", - "version": "0.5.0-alpha.4", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "types": "src/index.ts", - "scripts": { - "test": "bun test" - }, - "dependencies": { - "@uncaged/workflow-runtime": "workspace:^", - "@uncaged/workflow-util-agent": "workspace:^" - }, - "exports": { - ".": { - "bun": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/workflow-agent-hermes/pnpm-lock.yaml b/legacy-packages/workflow-agent-hermes/pnpm-lock.yaml deleted file mode 100644 index 94ef1d7..0000000 --- a/legacy-packages/workflow-agent-hermes/pnpm-lock.yaml +++ /dev/null @@ -1,16 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@uncaged/workflow-runtime': - specifier: workspace:* - version: link:../workflow-runtime - '@uncaged/workflow-util-agent': - specifier: workspace:* - version: link:../workflow-util-agent diff --git a/legacy-packages/workflow-agent-hermes/src/index.ts b/legacy-packages/workflow-agent-hermes/src/index.ts deleted file mode 100644 index 2ab9273..0000000 --- a/legacy-packages/workflow-agent-hermes/src/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { AdapterFn, AgentFn } from "@uncaged/workflow-runtime"; -import { - buildThreadInput, - createAgentAdapter, - type SpawnCliError, - spawnCli, -} from "@uncaged/workflow-util-agent"; - -import type { HermesAgentConfig } from "./types.js"; -import { validateHermesAgentConfig } from "./validate-config.js"; - -const HERMES_DEFAULT_MAX_TURNS = 90; - -type HermesAgentOpt = { prompt: string }; - -export type { HermesAgentConfig } from "./types.js"; -export { validateHermesAgentConfig } from "./validate-config.js"; - -function throwHermesSpawnError(error: SpawnCliError): never { - if (error.kind === "non_zero_exit") { - throw new Error( - `hermes: exitCode=${error.exitCode} stdout=${error.stdout} stderr=${error.stderr}`, - ); - } - if (error.kind === "timeout") { - throw new Error("hermes: timeout"); - } - if (error.kind === "spawn_failed") { - throw new Error(`hermes: ${error.message}`); - } - throw new Error("hermes: unknown spawn error"); -} - -function createHermesAgentFn(config: HermesAgentConfig): AgentFn { - const timeoutMs = config.timeout; - - return async (ctx, { prompt }) => { - const threadInput = await buildThreadInput(ctx); - const fullPrompt = `${prompt}\n\n${threadInput}`; - const args = [ - "chat", - "-q", - fullPrompt, - "--yolo", - "--max-turns", - String(HERMES_DEFAULT_MAX_TURNS), - "--quiet", - ]; - if (config.model !== null) { - args.push("--model", config.model); - } - const run = await spawnCli(config.command, args, { - cwd: null, - timeoutMs, - }); - if (!run.ok) { - throwHermesSpawnError(run.error); - } - return run.value; - }; -} - -/** Runs `hermes chat` non-interactively with the Nerve-style argv contract (`-q`, `--yolo`, `--quiet`). */ -export function createHermesAgent(config: HermesAgentConfig): AdapterFn { - return createAgentAdapter(createHermesAgentFn(config), async (_ctx, prompt, _runtime) => { - const validated = validateHermesAgentConfig(config); - if (!validated.ok) { - throw new Error(validated.error); - } - return { prompt }; - }); -} diff --git a/legacy-packages/workflow-agent-hermes/src/types.ts b/legacy-packages/workflow-agent-hermes/src/types.ts deleted file mode 100644 index 453a463..0000000 --- a/legacy-packages/workflow-agent-hermes/src/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type HermesAgentConfig = { - /** Absolute path to the hermes CLI binary. */ - command: string; - model: string | null; - timeout: number | null; -}; diff --git a/legacy-packages/workflow-agent-hermes/src/validate-config.ts b/legacy-packages/workflow-agent-hermes/src/validate-config.ts deleted file mode 100644 index 4edcd17..0000000 --- a/legacy-packages/workflow-agent-hermes/src/validate-config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { isAbsolute } from "node:path"; - -import { err, ok, type Result } from "@uncaged/workflow-runtime"; - -import type { HermesAgentConfig } from "./types.js"; - -export function validateHermesAgentConfig(config: HermesAgentConfig): Result { - if (!isAbsolute(config.command)) { - return err("command must be an absolute path to the hermes CLI binary"); - } - if (config.timeout !== null && config.timeout < 0) { - return err("timeout must be null or a non-negative number (milliseconds)"); - } - return ok(undefined); -} diff --git a/legacy-packages/workflow-agent-hermes/tsconfig.json b/legacy-packages/workflow-agent-hermes/tsconfig.json deleted file mode 100644 index d9141ff..0000000 --- a/legacy-packages/workflow-agent-hermes/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "composite": true - }, - "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow-runtime" }, { "path": "../workflow-util-agent" }] -} diff --git a/legacy-packages/workflow-agent-llm/CHANGELOG.md b/legacy-packages/workflow-agent-llm/CHANGELOG.md deleted file mode 100644 index 3686d87..0000000 --- a/legacy-packages/workflow-agent-llm/CHANGELOG.md +++ /dev/null @@ -1,81 +0,0 @@ -# @uncaged/workflow-agent-llm - -## 0.5.0-alpha.4 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.4 -- @uncaged/workflow-util-agent@0.5.0-alpha.4 - -## 0.5.0-alpha.3 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.3 -- @uncaged/workflow-util-agent@0.5.0-alpha.3 - -## 0.5.0-alpha.2 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.2 -- @uncaged/workflow-util-agent@0.5.0-alpha.2 - -## 0.5.0-alpha.1 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-util-agent@0.5.0-alpha.1 - - @uncaged/workflow-runtime@0.5.0-alpha.1 - -## 0.5.0-alpha.0 - -### Patch Changes - -- @uncaged/workflow-runtime@0.5.0-alpha.0 -- @uncaged/workflow-util-agent@0.5.0-alpha.0 - -## 0.4.5 - -### Patch Changes - -- @uncaged/workflow-runtime@0.4.5 -- @uncaged/workflow-util-agent@0.4.5 - -## 0.4.4 - -### Patch Changes - -- @uncaged/workflow-runtime@0.4.4 -- @uncaged/workflow-util-agent@0.4.4 - -## 0.4.3 - -### Patch Changes - -- Include src/ in published packages so bun runtime can resolve the 'bun' exports condition. -- Updated dependencies - - @uncaged/workflow-runtime@0.4.3 - - @uncaged/workflow-util-agent@0.4.3 - -## 0.4.2 - -### Patch Changes - -- Fix workspace dependency resolution: use workspace:^ so published packages resolve to compatible versions instead of exact (non-existent) versions. -- Updated dependencies - - @uncaged/workflow-runtime@0.4.2 - - @uncaged/workflow-util-agent@0.4.2 - -## 0.4.0 - -### Minor Changes - -- Fix package exports for published packages and adopt changesets for version management. - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-runtime@0.4.0 - - @uncaged/workflow-util-agent@0.4.0 diff --git a/legacy-packages/workflow-agent-llm/README.md b/legacy-packages/workflow-agent-llm/README.md deleted file mode 100644 index 4973cfc..0000000 --- a/legacy-packages/workflow-agent-llm/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# @uncaged/workflow-agent-llm - -`AgentFn` adapter that calls an OpenAI-compatible `POST /chat/completions` endpoint using `LlmProvider` from `@uncaged/workflow-runtime`. - -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-runtime zod -``` - -In this monorepo: `"@uncaged/workflow-agent-llm": "workspace:*"`, `"@uncaged/workflow-runtime": "workspace:*"` (and satisfy `zod` ^4 as required by `@uncaged/workflow-runtime`). - -## 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` helper | -| `LlmMessage` | `{ role: "system" \| "user" \| "assistant"; content: string }` | -| `LlmChatError` | Discriminated error kinds for failed completions | diff --git a/legacy-packages/workflow-agent-llm/__tests__/create-llm-adapter.test.ts b/legacy-packages/workflow-agent-llm/__tests__/create-llm-adapter.test.ts deleted file mode 100644 index 9d7991e..0000000 --- a/legacy-packages/workflow-agent-llm/__tests__/create-llm-adapter.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - type CasStore, - type ExtractFn, - START, - type ThreadContext, - type WorkflowRuntime, -} from "@uncaged/workflow-runtime"; -import * as z from "zod"; - -import { createLlmAdapter } from "../src/create-llm-adapter.js"; - -function makeCtx(userContent: string): ThreadContext { - return { - start: { - role: START, - content: userContent, - meta: {}, - timestamp: 1, - parentState: null, - }, - depth: 0, - bundleHash: "TESTHASH00001", - steps: [], - threadId: "01TEST000000000000000000TR", - }; -} - -const testSchema = z.object({ summary: z.string() }); - -function makeRuntime(): WorkflowRuntime { - let stored = ""; - const cas: CasStore = { - put: async (content: string) => { - stored = content; - return "HASH001"; - }, - get: async () => stored, - delete: async () => {}, - list: async () => [], - }; - const extract: ExtractFn = async (_schema, _contentHash) => ({ - meta: { summary: "extracted" }, - contentPayload: stored, - refs: [], - }); - return { cas, extract }; -} - -describe("createLlmAdapter", () => { - const originalFetch = globalThis.fetch; - - test("posts system + user (start.content) and returns typed meta with childThread: null", async () => { - globalThis.fetch = (() => - Promise.resolve( - new Response(JSON.stringify({ choices: [{ message: { content: "model reply" } }] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - )) as unknown as typeof fetch; - - const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" }; - const adapter = createLlmAdapter(provider); - const roleFn = adapter("system instructions", testSchema); - const result = await roleFn(makeCtx("trigger text"), makeRuntime()); - - globalThis.fetch = originalFetch; - - expect(result.meta).toEqual({ summary: "extracted" }); - expect(result.childThread).toBeNull(); - }); - - test("throws on non-ok fetch response", async () => { - globalThis.fetch = (() => - Promise.resolve( - new Response("Internal Server Error", { - status: 500, - headers: { "Content-Type": "text/plain" }, - }), - )) as unknown as typeof fetch; - - const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" }; - const adapter = createLlmAdapter(provider); - const roleFn = adapter("system", testSchema); - - await expect(roleFn(makeCtx("hi"), makeRuntime())).rejects.toThrow("llm:"); - globalThis.fetch = originalFetch; - }); - - test("throws on fetch network failure", async () => { - globalThis.fetch = (() => Promise.reject(new Error("ECONNREFUSED"))) as unknown as typeof fetch; - - const provider = { baseUrl: "https://api.example/v1", apiKey: "k", model: "m" }; - const adapter = createLlmAdapter(provider); - const roleFn = adapter("system", testSchema); - - await expect(roleFn(makeCtx("hi"), makeRuntime())).rejects.toThrow(); - globalThis.fetch = originalFetch; - }); -}); diff --git a/legacy-packages/workflow-agent-llm/package.json b/legacy-packages/workflow-agent-llm/package.json deleted file mode 100644 index bbcf4f7..0000000 --- a/legacy-packages/workflow-agent-llm/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@uncaged/workflow-agent-llm", - "version": "0.5.0-alpha.4", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "types": "src/index.ts", - "scripts": { - "test": "bun test" - }, - "dependencies": { - "@uncaged/workflow-runtime": "workspace:^", - "@uncaged/workflow-util-agent": "workspace:^" - }, - "devDependencies": { - "zod": "^4.0.0" - }, - "exports": { - ".": { - "bun": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/workflow-agent-llm/pnpm-lock.yaml b/legacy-packages/workflow-agent-llm/pnpm-lock.yaml deleted file mode 100644 index 933401d..0000000 --- a/legacy-packages/workflow-agent-llm/pnpm-lock.yaml +++ /dev/null @@ -1,13 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@uncaged/workflow-runtime': - specifier: workspace:* - version: link:../workflow-runtime diff --git a/legacy-packages/workflow-agent-llm/src/create-llm-adapter.ts b/legacy-packages/workflow-agent-llm/src/create-llm-adapter.ts deleted file mode 100644 index ebf2736..0000000 --- a/legacy-packages/workflow-agent-llm/src/create-llm-adapter.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - type AdapterFn, - type AgentFn, - err, - type LlmProvider, - ok, - type Result, -} from "@uncaged/workflow-runtime"; -import { createAgentAdapter } from "@uncaged/workflow-util-agent"; - -/** OpenAI chat completion message shape (passed to `/chat/completions`). */ -export type LlmMessage = { role: "system" | "user" | "assistant"; content: string }; - -export type LlmChatError = - | { kind: "http_error"; status: number; body: string } - | { kind: "invalid_response_json"; message: string } - | { kind: "network_error"; message: string } - | { kind: "empty_choices" } - | { kind: "no_assistant_text" }; - -function chatUrl(baseUrl: string): string { - const trimmed = baseUrl.replace(/\/+$/, ""); - return `${trimmed}/chat/completions`; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function formatLlmChatError(e: LlmChatError): string { - return JSON.stringify(e); -} - -async function fetchChatJson( - provider: LlmProvider, - body: Record, -): Promise> { - let response: Response; - try { - response = await fetch(chatUrl(provider.baseUrl), { - method: "POST", - headers: { - Authorization: `Bearer ${provider.apiKey}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - } catch (cause) { - const message = cause instanceof Error ? cause.message : String(cause); - return err({ kind: "network_error", message }); - } - const responseText = await response.text(); - if (!response.ok) { - return err({ kind: "http_error", status: response.status, body: responseText.slice(0, 4000) }); - } - let parsed: unknown; - try { - parsed = JSON.parse(responseText) as unknown; - } catch (cause) { - const message = cause instanceof Error ? cause.message : String(cause); - return err({ kind: "invalid_response_json", message }); - } - return ok(parsed); -} - -function parseAssistantText(parsed: unknown): Result { - if (!isRecord(parsed)) { - return err({ kind: "invalid_response_json", message: "Not an object" }); - } - const choices = parsed.choices; - if (!Array.isArray(choices) || choices.length === 0) { - return err({ kind: "empty_choices" }); - } - const c0 = choices[0]; - if (!isRecord(c0)) { - return err({ kind: "empty_choices" }); - } - const messageObj = c0.message; - if (!isRecord(messageObj)) { - return err({ kind: "no_assistant_text" }); - } - const content = messageObj.content; - if (typeof content === "string") { - return ok(content); - } - return err({ kind: "no_assistant_text" }); -} - -export async function chatCompletionText(options: { - provider: LlmProvider; - messages: LlmMessage[]; -}): Promise> { - const body = { model: options.provider.model, messages: options.messages }; - const res = await fetchChatJson(options.provider, body); - if (!res.ok) { - return res; - } - return parseAssistantText(res.value); -} - -type LlmAgentOpt = { prompt: string }; - -function createLlmAgent(provider: LlmProvider): AgentFn { - return async (ctx, { prompt }) => { - const result = await chatCompletionText({ - provider, - messages: [ - { role: "system", content: prompt }, - { role: "user", content: ctx.start.content }, - ], - }); - if (!result.ok) { - throw new Error(`llm: ${formatLlmChatError(result.error)}`); - } - return result.value; - }; -} - -/** Single-turn chat adapter: system prompt is passed by the workflow engine. */ -export function createLlmAdapter(provider: LlmProvider): AdapterFn { - return createAgentAdapter(createLlmAgent(provider), async (_ctx, prompt, _runtime) => ({ - prompt, - })); -} diff --git a/legacy-packages/workflow-agent-llm/src/index.ts b/legacy-packages/workflow-agent-llm/src/index.ts deleted file mode 100644 index 3038f2e..0000000 --- a/legacy-packages/workflow-agent-llm/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - chatCompletionText, - createLlmAdapter, - type LlmChatError, - type LlmMessage, -} from "./create-llm-adapter.js"; diff --git a/legacy-packages/workflow-agent-llm/tsconfig.json b/legacy-packages/workflow-agent-llm/tsconfig.json deleted file mode 100644 index d9141ff..0000000 --- a/legacy-packages/workflow-agent-llm/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "composite": true - }, - "include": ["src/**/*.ts"], - "references": [{ "path": "../workflow-runtime" }, { "path": "../workflow-util-agent" }] -} diff --git a/legacy-packages/workflow-agent-office/__tests__/agent.test.ts b/legacy-packages/workflow-agent-office/__tests__/agent.test.ts deleted file mode 100644 index 89a8238..0000000 --- a/legacy-packages/workflow-agent-office/__tests__/agent.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { createOfficeAgent } from "../src/agent.js"; -import { packageDescriptor } from "../src/package-descriptor.js"; - -describe("createOfficeAgent", () => { - test("returns an AdapterFn (function)", () => { - const agent = createOfficeAgent({ outputDir: "/tmp", command: null, timeout: null }); - expect(typeof agent).toBe("function"); - }); - - test("AdapterFn returns a RoleFn (function)", () => { - const agent = createOfficeAgent({ outputDir: "/tmp", command: null, timeout: null }); - const roleFn = agent("", expect.anything() as never); - expect(typeof roleFn).toBe("function"); - }); -}); - -describe("packageDescriptor", () => { - test("has correct name", () => { - expect(packageDescriptor.name).toBe("@uncaged/workflow-agent-office"); - }); - - test("has outputDir in configSchema required", () => { - const schema = packageDescriptor.configSchema as { required: string[] }; - expect(schema.required).toContain("outputDir"); - }); -}); diff --git a/legacy-packages/workflow-agent-office/__tests__/runner.test.ts b/legacy-packages/workflow-agent-office/__tests__/runner.test.ts deleted file mode 100644 index c691454..0000000 --- a/legacy-packages/workflow-agent-office/__tests__/runner.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { err, ok } from "@uncaged/workflow-util"; -import type { SpawnCliConfig } from "@uncaged/workflow-util-agent"; -import { editDocument, generateDocument } from "../src/runner.js"; - -type MockSpawnResult = Awaited>; - -function makeSpawn(result: MockSpawnResult) { - return mock(async (_cmd: string, _args: string[], _opts: SpawnCliConfig) => result); -} - -function tempDir(): string { - const dir = join(tmpdir(), `office-test-${Date.now()}`); - mkdirSync(dir, { recursive: true }); - return dir; -} - -describe("generateDocument", () => { - test("calls office-agent create with correct args and returns outputDocx path", async () => { - const base = tempDir(); - const spawnFn = makeSpawn(ok("agent reply") as MockSpawnResult); - // Simulate CLI creating the file - const outFile = join(base, "thread1", "output.docx"); - mkdirSync(join(base, "thread1"), { recursive: true }); - writeFileSync(outFile, ""); - - const result = await generateDocument( - { outputDir: base, command: "office-agent", timeout: null }, - "thread1", - "Write a report", - spawnFn, - ); - - expect(result.outputDocx).toBe(outFile); - expect(result.sourceDocx).toBeNull(); - expect(spawnFn.mock.calls[0][0]).toBe("office-agent"); - expect(spawnFn.mock.calls[0][1]).toEqual(["create", "Write a report", "-o", "output.docx"]); - expect(spawnFn.mock.calls[0][2].cwd).toBe(join(base, "thread1")); - }); - - test("uses PATH office-agent when command is null", async () => { - const base = tempDir(); - const spawnFn = makeSpawn(ok("") as MockSpawnResult); - mkdirSync(join(base, "t2"), { recursive: true }); - writeFileSync(join(base, "t2", "output.docx"), ""); - - await generateDocument( - { outputDir: base, command: null, timeout: null }, - "t2", - "Generate", - spawnFn, - ); - - expect(spawnFn.mock.calls[0][0]).toBe("office-agent"); - }); - - test("throws on non_zero_exit", async () => { - const base = tempDir(); - const spawnFn = makeSpawn( - err({ kind: "non_zero_exit", exitCode: 1, stdout: "", stderr: "error" }) as MockSpawnResult, - ); - - await expect( - generateDocument({ outputDir: base, command: null, timeout: null }, "t3", "fail", spawnFn), - ).rejects.toThrow("office-agent failed (exit 1)"); - }); - - test("throws on timeout", async () => { - const base = tempDir(); - const spawnFn = makeSpawn(err({ kind: "timeout" }) as MockSpawnResult); - - await expect( - generateDocument({ outputDir: base, command: null, timeout: null }, "t4", "slow", spawnFn), - ).rejects.toThrow("office-agent: timed out"); - }); - - test("throws when output file not created", async () => { - const base = tempDir(); - const spawnFn = makeSpawn(ok("") as MockSpawnResult); - // Do NOT create output.docx - - await expect( - generateDocument({ outputDir: base, command: null, timeout: null }, "t5", "no file", spawnFn), - ).rejects.toThrow("output file not found"); - }); -}); - -describe("editDocument", () => { - test("copies input to original.docx and modified.docx, calls edit, returns paths", async () => { - const base = tempDir(); - // Create a fake inputDocx - const inputFile = join(base, "source.docx"); - writeFileSync(inputFile, "original content"); - - const spawnFn = makeSpawn(ok("") as MockSpawnResult); - // Simulate CLI overwriting modified.docx - const outDir = join(base, "te1"); - mkdirSync(outDir, { recursive: true }); - writeFileSync(join(outDir, "modified.docx"), "modified content"); - - const result = await editDocument( - { outputDir: base, command: "office-agent", timeout: null }, - "te1", - "Edit the doc", - inputFile, - spawnFn, - ); - - expect(result.outputDocx).toBe(join(outDir, "modified.docx")); - expect(result.sourceDocx).toBe(join(outDir, "original.docx")); - expect(spawnFn.mock.calls[0][1]).toEqual(["edit", "modified.docx", "Edit the doc"]); - }); - - test("throws on spawn_failed", async () => { - const base = tempDir(); - const inputFile = join(base, "src.docx"); - writeFileSync(inputFile, ""); - const spawnFn = makeSpawn( - err({ kind: "spawn_failed", message: "not found" }) as MockSpawnResult, - ); - - await expect( - editDocument( - { outputDir: base, command: null, timeout: null }, - "te2", - "edit", - inputFile, - spawnFn, - ), - ).rejects.toThrow("spawn failed"); - }); -}); diff --git a/legacy-packages/workflow-agent-office/package.json b/legacy-packages/workflow-agent-office/package.json deleted file mode 100644 index 970e70c..0000000 --- a/legacy-packages/workflow-agent-office/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@uncaged/workflow-agent-office", - "version": "0.1.0", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "types": "src/index.ts", - "exports": { - ".": { - "bun": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "test": "bun test" - }, - "dependencies": { - "@uncaged/workflow-runtime": "workspace:^", - "@uncaged/workflow-util": "workspace:^", - "@uncaged/workflow-util-agent": "workspace:^" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/workflow-agent-office/src/agent.ts b/legacy-packages/workflow-agent-office/src/agent.ts deleted file mode 100644 index a55e5d9..0000000 --- a/legacy-packages/workflow-agent-office/src/agent.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { - AdapterFn, - RoleResult, - ThreadContext, - WorkflowRuntime, -} from "@uncaged/workflow-runtime"; -import { createLogger } from "@uncaged/workflow-util"; -import type * as z from "zod/v4"; -import { editDocument, generateDocument } from "./runner.js"; -import type { OfficeAgentConfig } from "./types.js"; - -const log = createLogger({ sink: { kind: "stderr" } }); - -type ParsedInput = { prompt: string; inputDocx: string | null }; - -function parseStartInput(content: string): ParsedInput { - try { - const parsed = JSON.parse(content) as Record; - if (typeof parsed.prompt === "string") { - return { - prompt: parsed.prompt, - inputDocx: typeof parsed.inputDocx === "string" ? parsed.inputDocx : null, - }; - } - } catch { - // not JSON — treat whole content as prompt, generate mode - } - return { prompt: content, inputDocx: null }; -} - -export function createOfficeAgent(config: OfficeAgentConfig): AdapterFn { - return (_systemPrompt: string, schema: z.ZodType) => - async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise> => { - const { prompt, inputDocx } = parseStartInput(ctx.start.content); - log( - "8FQKP3NV", - `office-agent: mode=${inputDocx === null ? "generate" : "edit"} thread=${ctx.threadId}`, - ); - - let raw: string; - if (inputDocx === null) { - const result = await generateDocument(config, ctx.threadId, prompt); - raw = JSON.stringify({ mode: "generate", outputDocx: result.outputDocx, sourceDocx: null }); - } else { - const result = await editDocument(config, ctx.threadId, prompt, inputDocx); - raw = JSON.stringify({ - mode: "edit", - outputDocx: result.outputDocx, - sourceDocx: result.sourceDocx, - }); - } - - const meta = schema.parse(JSON.parse(raw)) as T; - return { meta, childThread: null }; - }; -} diff --git a/legacy-packages/workflow-agent-office/src/index.ts b/legacy-packages/workflow-agent-office/src/index.ts deleted file mode 100644 index 0964445..0000000 --- a/legacy-packages/workflow-agent-office/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { createOfficeAgent } from "./agent.js"; -export { packageDescriptor } from "./package-descriptor.js"; -export type { OfficeAgentConfig } from "./types.js"; diff --git a/legacy-packages/workflow-agent-office/src/package-descriptor.ts b/legacy-packages/workflow-agent-office/src/package-descriptor.ts deleted file mode 100644 index b2fdba7..0000000 --- a/legacy-packages/workflow-agent-office/src/package-descriptor.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { PackageDescriptor } from "@uncaged/workflow-runtime"; - -export const packageDescriptor: PackageDescriptor = { - name: "@uncaged/workflow-agent-office", - version: "0.1.0", - capabilities: ["office-agent-cli", "docx-generate", "docx-edit"], - configSchema: { - type: "object", - required: ["outputDir"], - properties: { - outputDir: { - type: "string", - description: "Root directory for workflow outputs; subdirs are created per threadId.", - }, - command: { - anyOf: [{ type: "string" }, { type: "null" }], - description: "Path to office-agent CLI binary; null uses PATH.", - }, - timeout: { - anyOf: [{ type: "number" }, { type: "null" }], - description: "Timeout in milliseconds; null means no limit.", - }, - }, - additionalProperties: false, - }, -}; diff --git a/legacy-packages/workflow-agent-office/src/runner.ts b/legacy-packages/workflow-agent-office/src/runner.ts deleted file mode 100644 index d50891f..0000000 --- a/legacy-packages/workflow-agent-office/src/runner.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { copyFile, mkdir, stat } from "node:fs/promises"; -import { join } from "node:path"; -import type { SpawnCliError } from "@uncaged/workflow-util-agent"; -import { spawnCli } from "@uncaged/workflow-util-agent"; -import type { OfficeAgentConfig } from "./types.js"; - -type SpawnCliFn = typeof spawnCli; - -function throwSpawnError(e: SpawnCliError): never { - if (e.kind === "non_zero_exit") - throw new Error(`office-agent failed (exit ${e.exitCode}): ${e.stderr}`); - if (e.kind === "timeout") throw new Error("office-agent: timed out"); - throw new Error(`office-agent: spawn failed: ${e.message}`); -} - -async function assertFileExists(path: string): Promise { - try { - await stat(path); - } catch { - throw new Error(`office-agent: output file not found: ${path}`); - } -} - -export async function generateDocument( - config: OfficeAgentConfig, - threadId: string, - prompt: string, - spawnCliFn: SpawnCliFn = spawnCli, -): Promise<{ outputDocx: string; sourceDocx: null }> { - const outputDir = join(config.outputDir, threadId); - await mkdir(outputDir, { recursive: true }); - const command = config.command ?? "office-agent"; - const result = await spawnCliFn(command, ["create", prompt, "-o", "output.docx"], { - cwd: outputDir, - timeoutMs: config.timeout, - }); - if (!result.ok) throwSpawnError(result.error); - const outputDocx = join(outputDir, "output.docx"); - await assertFileExists(outputDocx); - return { outputDocx, sourceDocx: null }; -} - -export async function editDocument( - config: OfficeAgentConfig, - threadId: string, - prompt: string, - inputDocx: string, - spawnCliFn: SpawnCliFn = spawnCli, -): Promise<{ outputDocx: string; sourceDocx: string }> { - const outputDir = join(config.outputDir, threadId); - await mkdir(outputDir, { recursive: true }); - const originalDocx = join(outputDir, "original.docx"); - const modifiedDocx = join(outputDir, "modified.docx"); - await copyFile(inputDocx, originalDocx); - await copyFile(inputDocx, modifiedDocx); - const command = config.command ?? "office-agent"; - const result = await spawnCliFn(command, ["edit", "modified.docx", prompt], { - cwd: outputDir, - timeoutMs: config.timeout, - }); - if (!result.ok) throwSpawnError(result.error); - await assertFileExists(modifiedDocx); - return { outputDocx: modifiedDocx, sourceDocx: originalDocx }; -} diff --git a/legacy-packages/workflow-agent-office/src/types.ts b/legacy-packages/workflow-agent-office/src/types.ts deleted file mode 100644 index f1ffc54..0000000 --- a/legacy-packages/workflow-agent-office/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type OfficeAgentConfig = { - outputDir: string; - command: string | null; - timeout: number | null; -}; diff --git a/legacy-packages/workflow-agent-office/tsconfig.json b/legacy-packages/workflow-agent-office/tsconfig.json deleted file mode 100644 index e778e4c..0000000 --- a/legacy-packages/workflow-agent-office/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "composite": true - }, - "include": ["src/**/*.ts"], - "references": [ - { "path": "../workflow-protocol" }, - { "path": "../workflow-runtime" }, - { "path": "../workflow-util" }, - { "path": "../workflow-util-agent" } - ] -} diff --git a/legacy-packages/workflow-agent-react/CHANGELOG.md b/legacy-packages/workflow-agent-react/CHANGELOG.md deleted file mode 100644 index d1d672a..0000000 --- a/legacy-packages/workflow-agent-react/CHANGELOG.md +++ /dev/null @@ -1,98 +0,0 @@ -# @uncaged/workflow-agent-react - -## 0.5.0-alpha.4 - -### Patch Changes - -- Updated dependencies [f74b482] -- Updated dependencies [f74b482] - - @uncaged/workflow-protocol@0.5.0-alpha.4 - - @uncaged/workflow-reactor@0.5.0-alpha.4 - - @uncaged/workflow-util-agent@0.5.0-alpha.4 - -## 0.5.0-alpha.3 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.3 - - @uncaged/workflow-reactor@0.5.0-alpha.3 - - @uncaged/workflow-util-agent@0.5.0-alpha.3 - -## 0.5.0-alpha.2 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.2 - - @uncaged/workflow-reactor@0.5.0-alpha.2 - - @uncaged/workflow-util-agent@0.5.0-alpha.2 - -## 0.5.0-alpha.1 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-util-agent@0.5.0-alpha.1 - - @uncaged/workflow-protocol@0.5.0-alpha.1 - - @uncaged/workflow-reactor@0.5.0-alpha.1 - -## 0.5.0-alpha.0 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.0 - - @uncaged/workflow-reactor@0.5.0-alpha.0 - - @uncaged/workflow-util-agent@0.5.0-alpha.0 - -## 0.4.5 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.5 - - @uncaged/workflow-reactor@0.4.5 - - @uncaged/workflow-util-agent@0.4.5 - -## 0.4.4 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.4 - - @uncaged/workflow-reactor@0.4.4 - - @uncaged/workflow-util-agent@0.4.4 - -## 0.4.3 - -### Patch Changes - -- Include src/ in published packages so bun runtime can resolve the 'bun' exports condition. -- Updated dependencies - - @uncaged/workflow-protocol@0.4.3 - - @uncaged/workflow-reactor@0.4.3 - - @uncaged/workflow-util-agent@0.4.3 - -## 0.4.2 - -### Patch Changes - -- Fix workspace dependency resolution: use workspace:^ so published packages resolve to compatible versions instead of exact (non-existent) versions. -- Updated dependencies - - @uncaged/workflow-protocol@0.4.2 - - @uncaged/workflow-reactor@0.4.2 - - @uncaged/workflow-util-agent@0.4.2 - -## 0.4.0 - -### Minor Changes - -- Fix package exports for published packages and adopt changesets for version management. - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.0 - - @uncaged/workflow-reactor@0.4.0 - - @uncaged/workflow-util-agent@0.4.0 diff --git a/legacy-packages/workflow-agent-react/__tests__/create-react-adapter.test.ts b/legacy-packages/workflow-agent-react/__tests__/create-react-adapter.test.ts deleted file mode 100644 index 04b155f..0000000 --- a/legacy-packages/workflow-agent-react/__tests__/create-react-adapter.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { ok, START, type ThreadContext, type WorkflowRuntime } from "@uncaged/workflow-protocol"; -import type { LlmFn, ToolDefinition } from "@uncaged/workflow-reactor"; -import * as z from "zod/v4"; - -import { createReactAdapter } from "../src/create-react-adapter.js"; -import type { ReactAdapterConfig } from "../src/types.js"; - -// ── Helpers ───────────────────────────────────────────────────────── - -function makeThread(prompt: string): ThreadContext { - return { - threadId: "01TEST000000000000000000TR", - depth: 0, - bundleHash: "TESTHASH00001", - start: { - role: START, - content: prompt, - meta: {}, - timestamp: Date.now(), - parentState: null, - }, - steps: [], - }; -} - -const STUB_RUNTIME: WorkflowRuntime = { - cas: { - put: async (_content: string) => "STUBHASH", - get: async (_hash: string) => null, - delete: async (_hash: string) => {}, - list: async () => [], - }, - extract: async (_schema, _contentHash) => ({ - meta: {}, - contentPayload: "", - refs: [], - }), -}; - -const TEST_SCHEMA = z - .object({ - summary: z.string(), - score: z.number(), - }) - .meta({ title: "resolve", description: "Submit the final result." }); - -function makeChatResponse(content: string | null, toolCalls: unknown[] | null): string { - const message: Record = { role: "assistant" }; - if (content !== null) { - message.content = content; - } - if (toolCalls !== null) { - message.tool_calls = toolCalls; - } - return JSON.stringify({ choices: [{ message }] }); -} - -function makeToolCallResponse(name: string, args: Record, id: string): string { - return makeChatResponse(null, [ - { - id, - type: "function", - function: { name, arguments: JSON.stringify(args) }, - }, - ]); -} - -// ── Tests ─────────────────────────────────────────────────────────── - -describe("createReactAdapter", () => { - test("direct resolve: LLM immediately calls resolve tool with valid args", async () => { - const llm: LlmFn = async (_input) => { - return ok(makeToolCallResponse("resolve", { summary: "done", score: 42 }, "call_1")); - }; - - const config: ReactAdapterConfig = { - llm, - tools: [], - toolHandler: async () => "unused", - maxRounds: 5, - }; - - const adapter = createReactAdapter(config); - const roleFn = adapter("You are a test agent.", TEST_SCHEMA); - const result = await roleFn(makeThread("test task"), STUB_RUNTIME); - - expect(result.meta).toEqual({ summary: "done", score: 42 }); - expect(result.childThread).toBeNull(); - }); - - test("tool call then resolve: LLM calls user tool first, then resolves", async () => { - let callCount = 0; - const llm: LlmFn = async (_input) => { - callCount += 1; - if (callCount === 1) { - return ok(makeToolCallResponse("search", { query: "test" }, "call_1")); - } - return ok(makeToolCallResponse("resolve", { summary: "found it", score: 99 }, "call_2")); - }; - - const searchTool: ToolDefinition = { - type: "function", - function: { - name: "search", - description: "Search for information", - parameters: { - type: "object", - properties: { query: { type: "string" } }, - required: ["query"], - }, - }, - }; - - const toolResults: string[] = []; - const config: ReactAdapterConfig = { - llm, - tools: [searchTool], - toolHandler: async (name, args) => { - toolResults.push(`${name}:${args}`); - return "search result: found the answer"; - }, - maxRounds: 5, - }; - - const adapter = createReactAdapter(config); - const roleFn = adapter("You are a test agent.", TEST_SCHEMA); - const result = await roleFn(makeThread("test task"), STUB_RUNTIME); - - expect(result.meta).toEqual({ summary: "found it", score: 99 }); - expect(toolResults).toHaveLength(1); - expect(toolResults[0]).toContain("search:"); - }); - - test("plain JSON response accepted", async () => { - const llm: LlmFn = async (_input) => { - return ok(makeChatResponse(JSON.stringify({ summary: "plain", score: 7 }), null)); - }; - - const config: ReactAdapterConfig = { - llm, - tools: [], - toolHandler: async () => "unused", - maxRounds: 5, - }; - - const adapter = createReactAdapter(config); - const roleFn = adapter("You are a test agent.", TEST_SCHEMA); - const result = await roleFn(makeThread("test task"), STUB_RUNTIME); - - expect(result.meta).toEqual({ summary: "plain", score: 7 }); - }); - - test("schema validation failure + retry: invalid args then valid args", async () => { - let callCount = 0; - const llm: LlmFn = async (_input) => { - callCount += 1; - if (callCount === 1) { - // Invalid: score should be number, not string - return ok( - makeToolCallResponse("resolve", { summary: "bad", score: "not-a-number" }, "call_1"), - ); - } - return ok(makeToolCallResponse("resolve", { summary: "fixed", score: 10 }, "call_2")); - }; - - const config: ReactAdapterConfig = { - llm, - tools: [], - toolHandler: async () => "unused", - maxRounds: 5, - }; - - const adapter = createReactAdapter(config); - const roleFn = adapter("You are a test agent.", TEST_SCHEMA); - const result = await roleFn(makeThread("test task"), STUB_RUNTIME); - - expect(result.meta).toEqual({ summary: "fixed", score: 10 }); - expect(callCount).toBe(2); - }); - - test("max rounds exceeded: throws error", async () => { - const searchTool: ToolDefinition = { - type: "function", - function: { - name: "search", - description: "Search", - parameters: { type: "object", properties: {}, required: [] }, - }, - }; - - const llm: LlmFn = async (_input) => { - // Always call search, never resolve - return ok(makeToolCallResponse("search", {}, "call_n")); - }; - - const config: ReactAdapterConfig = { - llm, - tools: [searchTool], - toolHandler: async () => "still searching...", - maxRounds: 3, - }; - - const adapter = createReactAdapter(config); - const roleFn = adapter("You are a test agent.", TEST_SCHEMA); - - await expect(roleFn(makeThread("test task"), STUB_RUNTIME)).rejects.toThrow( - "max_react_rounds_exceeded", - ); - }); -}); diff --git a/legacy-packages/workflow-agent-react/__tests__/tools.test.ts b/legacy-packages/workflow-agent-react/__tests__/tools.test.ts deleted file mode 100644 index f718fb8..0000000 --- a/legacy-packages/workflow-agent-react/__tests__/tools.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { afterAll, describe, expect, test } from "bun:test"; -import { randomBytes } from "node:crypto"; -import { mkdirSync, readFileSync, unlinkSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { patchFileTool, readFileTool, shellExecTool, writeFileTool } from "../src/tools/index.js"; - -const TMP_DIR = join(tmpdir(), `tools-test-${randomBytes(4).toString("hex")}`); -mkdirSync(TMP_DIR, { recursive: true }); - -const tmpFile = (name: string) => join(TMP_DIR, name); - -const cleanupFiles: string[] = []; - -afterAll(() => { - for (const f of cleanupFiles) { - try { - unlinkSync(f); - } catch { - /* ignore */ - } - } - try { - unlinkSync(TMP_DIR); - } catch { - /* ignore */ - } -}); - -describe("read_file", () => { - test("reads file with line numbers", async () => { - const p = tmpFile("read-test.txt"); - cleanupFiles.push(p); - const content = "line1\nline2\nline3\n"; - require("node:fs").writeFileSync(p, content); - - const result = await readFileTool.handler( - JSON.stringify({ path: p, offset: null, limit: null }), - ); - expect(result).toContain("1|line1"); - expect(result).toContain("2|line2"); - expect(result).toContain("3|line3"); - }); - - test("reads with offset and limit", async () => { - const p = tmpFile("read-test2.txt"); - cleanupFiles.push(p); - require("node:fs").writeFileSync(p, "a\nb\nc\nd\ne\n"); - - const result = await readFileTool.handler(JSON.stringify({ path: p, offset: 2, limit: 2 })); - expect(result).toBe("2|b\n3|c"); - }); - - test("returns error for missing file", async () => { - const result = await readFileTool.handler( - JSON.stringify({ path: "/nonexistent/file.txt", offset: null, limit: null }), - ); - expect(result).toContain("Error:"); - }); -}); - -describe("write_file", () => { - test("writes file and creates dirs", async () => { - const p = tmpFile("sub/write-test.txt"); - cleanupFiles.push(p); - - const result = await writeFileTool.handler(JSON.stringify({ path: p, content: "hello world" })); - expect(result).toContain("11 bytes"); - expect(readFileSync(p, "utf-8")).toBe("hello world"); - }); -}); - -describe("patch_file", () => { - test("patches file content", async () => { - const p = tmpFile("patch-test.txt"); - cleanupFiles.push(p); - require("node:fs").writeFileSync(p, "foo bar baz"); - - const result = await patchFileTool.handler( - JSON.stringify({ path: p, old_string: "bar", new_string: "qux" }), - ); - expect(result).toContain("Successfully"); - expect(readFileSync(p, "utf-8")).toBe("foo qux baz"); - }); - - test("errors on not found", async () => { - const p = tmpFile("patch-test2.txt"); - cleanupFiles.push(p); - require("node:fs").writeFileSync(p, "foo"); - - const result = await patchFileTool.handler( - JSON.stringify({ path: p, old_string: "xyz", new_string: "abc" }), - ); - expect(result).toContain("not found"); - }); - - test("errors on non-unique match", async () => { - const p = tmpFile("patch-test3.txt"); - cleanupFiles.push(p); - require("node:fs").writeFileSync(p, "aaa bbb aaa"); - - const result = await patchFileTool.handler( - JSON.stringify({ path: p, old_string: "aaa", new_string: "ccc" }), - ); - expect(result).toContain("not unique"); - }); -}); - -describe("shell_exec", () => { - test("runs echo", async () => { - const result = await shellExecTool.handler( - JSON.stringify({ command: "echo hello", timeout: null }), - ); - expect(result.trim()).toBe("hello"); - }); - - test("handles timeout", async () => { - const result = await shellExecTool.handler(JSON.stringify({ command: "sleep 10", timeout: 1 })); - expect(result).toContain("timed out"); - }); -}); diff --git a/legacy-packages/workflow-agent-react/package.json b/legacy-packages/workflow-agent-react/package.json deleted file mode 100644 index 9bb66e1..0000000 --- a/legacy-packages/workflow-agent-react/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@uncaged/workflow-agent-react", - "version": "0.5.0-alpha.4", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "types": "src/index.ts", - "exports": { - ".": { - "bun": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "test": "bun test" - }, - "dependencies": { - "@uncaged/workflow-protocol": "workspace:^", - "@uncaged/workflow-reactor": "workspace:^", - "@uncaged/workflow-util-agent": "workspace:^" - }, - "devDependencies": { - "zod": "^4.0.0" - }, - "peerDependencies": { - "zod": "^4.0.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/workflow-agent-react/src/create-react-adapter.ts b/legacy-packages/workflow-agent-react/src/create-react-adapter.ts deleted file mode 100644 index 58c8399..0000000 --- a/legacy-packages/workflow-agent-react/src/create-react-adapter.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { - AdapterFn, - RoleResult, - ThreadContext, - WorkflowRuntime, -} from "@uncaged/workflow-protocol"; -import { createThreadReactor } from "@uncaged/workflow-reactor"; -import { buildThreadInput } from "@uncaged/workflow-util-agent"; -import * as z from "zod/v4"; - -import type { ReactAdapterConfig } from "./types.js"; - -function stripJsonSchemaMeta(json: Record): Record { - const { $schema: _drop, ...rest } = json; - return rest; -} - -function readToolName(parametersSchema: Record): string { - const title = parametersSchema.title; - if (typeof title === "string" && title.trim().length > 0) { - return title.trim(); - } - return "resolve"; -} - -function readToolDescription(parametersSchema: Record): string { - const d = parametersSchema.description; - if (typeof d === "string" && d.trim().length > 0) { - return d.trim(); - } - return "Submit the final structured result."; -} - -export function createReactAdapter(config: ReactAdapterConfig): AdapterFn { - return (prompt: string, schema: z.ZodType) => { - const reactor = createThreadReactor({ - llm: config.llm, - staticTools: config.tools, - structuredToolFromSchema: (s) => { - const rawJsonSchema = z.toJSONSchema(s) as Record; - const parameters = stripJsonSchemaMeta(rawJsonSchema); - const name = readToolName(parameters); - return { - name, - tool: { - type: "function" as const, - function: { - name, - description: readToolDescription(parameters), - parameters, - }, - }, - }; - }, - systemPromptForStructuredTool: (_name) => prompt, - toolHandler: async (call, _thread) => { - return config.toolHandler(call.function.name, call.function.arguments); - }, - maxRounds: config.maxRounds, - }); - - return async (ctx: ThreadContext, _runtime: WorkflowRuntime): Promise> => { - const input = await buildThreadInput(ctx); - const result = await reactor({ thread: ctx, input, schema }); - if (!result.ok) throw new Error(result.error); - return { meta: result.value, childThread: null }; - }; - }; -} diff --git a/legacy-packages/workflow-agent-react/src/index.ts b/legacy-packages/workflow-agent-react/src/index.ts deleted file mode 100644 index 61b66a4..0000000 --- a/legacy-packages/workflow-agent-react/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { createReactAdapter } from "./create-react-adapter.js"; -export type { ToolEntry, ToolHandler } from "./tools/index.js"; -export { defaultToolHandler, defaultTools } from "./tools/index.js"; -export type { ReactAdapterConfig, ReactToolHandler } from "./types.js"; diff --git a/legacy-packages/workflow-agent-react/src/tools/defaults.ts b/legacy-packages/workflow-agent-react/src/tools/defaults.ts deleted file mode 100644 index 80a5f3a..0000000 --- a/legacy-packages/workflow-agent-react/src/tools/defaults.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ToolDefinition } from "@uncaged/workflow-reactor"; -import { patchFileTool } from "./patch-file.js"; -import { readFileTool } from "./read-file.js"; -import { shellExecTool } from "./shell-exec.js"; -import type { ToolEntry } from "./types.js"; -import { writeFileTool } from "./write-file.js"; - -const ALL_TOOLS: ToolEntry[] = [readFileTool, writeFileTool, patchFileTool, shellExecTool]; - -export const defaultTools: readonly ToolDefinition[] = ALL_TOOLS.map((t) => t.definition); - -export async function defaultToolHandler(name: string, args: string): Promise { - const entry = ALL_TOOLS.find((t) => t.definition.function.name === name); - if (!entry) return `Unknown tool: ${name}`; - return entry.handler(args); -} diff --git a/legacy-packages/workflow-agent-react/src/tools/index.ts b/legacy-packages/workflow-agent-react/src/tools/index.ts deleted file mode 100644 index 07ef759..0000000 --- a/legacy-packages/workflow-agent-react/src/tools/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { defaultToolHandler, defaultTools } from "./defaults.js"; -export { patchFileTool } from "./patch-file.js"; -export { readFileTool } from "./read-file.js"; -export { shellExecTool } from "./shell-exec.js"; -export type { ToolEntry, ToolHandler } from "./types.js"; -export { writeFileTool } from "./write-file.js"; diff --git a/legacy-packages/workflow-agent-react/src/tools/patch-file.ts b/legacy-packages/workflow-agent-react/src/tools/patch-file.ts deleted file mode 100644 index 9bae2c2..0000000 --- a/legacy-packages/workflow-agent-react/src/tools/patch-file.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { readFile, writeFile } from "node:fs/promises"; -import type { ToolEntry } from "./types.js"; - -export const patchFileTool: ToolEntry = { - definition: { - type: "function", - function: { - name: "patch_file", - description: "Find and replace a string in a file (first occurrence only).", - parameters: { - type: "object", - properties: { - path: { type: "string", description: "Path to the file" }, - old_string: { type: "string", description: "Text to find" }, - new_string: { type: "string", description: "Replacement text" }, - }, - required: ["path", "old_string", "new_string"], - }, - }, - }, - handler: async (args: string): Promise => { - try { - const parsed = JSON.parse(args) as { path: string; old_string: string; new_string: string }; - const content = await readFile(parsed.path, "utf-8"); - const firstIdx = content.indexOf(parsed.old_string); - if (firstIdx === -1) { - return `Error: old_string not found in ${parsed.path}`; - } - const secondIdx = content.indexOf(parsed.old_string, firstIdx + 1); - if (secondIdx !== -1) { - return `Error: old_string is not unique in ${parsed.path} (found multiple occurrences)`; - } - const updated = - content.slice(0, firstIdx) + - parsed.new_string + - content.slice(firstIdx + parsed.old_string.length); - await writeFile(parsed.path, updated); - return `Successfully patched ${parsed.path}`; - } catch (err) { - return `Error: ${err instanceof Error ? err.message : String(err)}`; - } - }, -}; diff --git a/legacy-packages/workflow-agent-react/src/tools/read-file.ts b/legacy-packages/workflow-agent-react/src/tools/read-file.ts deleted file mode 100644 index ba8c0f5..0000000 --- a/legacy-packages/workflow-agent-react/src/tools/read-file.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { readFile } from "node:fs/promises"; -import type { ToolEntry } from "./types.js"; - -export const readFileTool: ToolEntry = { - definition: { - type: "function", - function: { - name: "read_file", - description: "Read a text file and return lines with line numbers.", - parameters: { - type: "object", - properties: { - path: { type: "string", description: "Path to the file to read" }, - offset: { - type: ["number", "null"], - description: "Start line number (1-indexed, default: 1)", - }, - limit: { type: ["number", "null"], description: "Max lines to read (default: all)" }, - }, - required: ["path"], - }, - }, - }, - handler: async (args: string): Promise => { - try { - const parsed = JSON.parse(args) as { - path: string; - offset: number | null; - limit: number | null; - }; - const content = await readFile(parsed.path, "utf-8"); - const allLines = content.split("\n"); - const offset = parsed.offset ?? 1; - const start = Math.max(0, offset - 1); - const end = - parsed.limit != null ? Math.min(allLines.length, start + parsed.limit) : allLines.length; - const lines = allLines.slice(start, end); - return lines.map((line, i) => `${start + i + 1}|${line}`).join("\n"); - } catch (err) { - return `Error: ${err instanceof Error ? err.message : String(err)}`; - } - }, -}; diff --git a/legacy-packages/workflow-agent-react/src/tools/shell-exec.ts b/legacy-packages/workflow-agent-react/src/tools/shell-exec.ts deleted file mode 100644 index 0c6e085..0000000 --- a/legacy-packages/workflow-agent-react/src/tools/shell-exec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { execSync } from "node:child_process"; -import type { ToolEntry } from "./types.js"; - -const MAX_OUTPUT = 10000; - -function truncate(text: string): string { - return text.length > MAX_OUTPUT ? `${text.slice(0, MAX_OUTPUT)}\n...(truncated)` : text; -} - -function classifyExecError(err: unknown): string { - if ( - err && - typeof err === "object" && - "status" in err && - (err as { status: unknown }).status === null - ) { - return "Error: command timed out"; - } - if (err && typeof err === "object" && "stderr" in err) { - const e = err as { stderr: string; stdout: string; status: number }; - const combined = `${e.stdout ?? ""}${e.stderr ?? ""}`; - return truncate(combined) || `Error: command exited with status ${e.status}`; - } - return `Error: ${err instanceof Error ? err.message : String(err)}`; -} - -export const shellExecTool: ToolEntry = { - definition: { - type: "function", - function: { - name: "shell_exec", - description: "Execute a shell command and return stdout + stderr.", - parameters: { - type: "object", - properties: { - command: { type: "string", description: "Shell command to run" }, - timeout: { type: ["number", "null"], description: "Timeout in seconds (default: 30)" }, - }, - required: ["command"], - }, - }, - }, - handler: async (args: string): Promise => { - try { - const parsed = JSON.parse(args) as { command: string; timeout: number | null }; - const timeoutMs = (parsed.timeout ?? 30) * 1000; - const output = execSync(parsed.command, { - encoding: "utf-8", - timeout: timeoutMs, - stdio: ["pipe", "pipe", "pipe"], - maxBuffer: MAX_OUTPUT * 2, - }); - return truncate(output); - } catch (err: unknown) { - return classifyExecError(err); - } - }, -}; diff --git a/legacy-packages/workflow-agent-react/src/tools/types.ts b/legacy-packages/workflow-agent-react/src/tools/types.ts deleted file mode 100644 index 255094a..0000000 --- a/legacy-packages/workflow-agent-react/src/tools/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ToolDefinition } from "@uncaged/workflow-reactor"; - -export type ToolHandler = (args: string) => Promise; - -export type ToolEntry = { - definition: ToolDefinition; - handler: ToolHandler; -}; diff --git a/legacy-packages/workflow-agent-react/src/tools/write-file.ts b/legacy-packages/workflow-agent-react/src/tools/write-file.ts deleted file mode 100644 index f6a2e74..0000000 --- a/legacy-packages/workflow-agent-react/src/tools/write-file.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { dirname } from "node:path"; -import type { ToolEntry } from "./types.js"; - -export const writeFileTool: ToolEntry = { - definition: { - type: "function", - function: { - name: "write_file", - description: "Write content to a file, creating parent directories as needed.", - parameters: { - type: "object", - properties: { - path: { type: "string", description: "Path to write" }, - content: { type: "string", description: "File content" }, - }, - required: ["path", "content"], - }, - }, - }, - handler: async (args: string): Promise => { - try { - const parsed = JSON.parse(args) as { path: string; content: string }; - await mkdir(dirname(parsed.path), { recursive: true }); - const buf = Buffer.from(parsed.content, "utf-8"); - await writeFile(parsed.path, buf); - return `Successfully wrote ${buf.length} bytes to ${parsed.path}`; - } catch (err) { - return `Error: ${err instanceof Error ? err.message : String(err)}`; - } - }, -}; diff --git a/legacy-packages/workflow-agent-react/src/types.ts b/legacy-packages/workflow-agent-react/src/types.ts deleted file mode 100644 index d8b2abf..0000000 --- a/legacy-packages/workflow-agent-react/src/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { LlmFn, ToolDefinition } from "@uncaged/workflow-reactor"; - -export type ReactToolHandler = (name: string, args: string) => Promise; - -export type ReactAdapterConfig = { - llm: LlmFn; - tools: readonly ToolDefinition[]; - toolHandler: ReactToolHandler; - maxRounds: number; -}; diff --git a/legacy-packages/workflow-agent-react/tsconfig.json b/legacy-packages/workflow-agent-react/tsconfig.json deleted file mode 100644 index 1b9f208..0000000 --- a/legacy-packages/workflow-agent-react/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "composite": true - }, - "include": ["src/**/*.ts"], - "references": [ - { "path": "../workflow-protocol" }, - { "path": "../workflow-reactor" }, - { "path": "../workflow-util-agent" } - ] -} diff --git a/legacy-packages/workflow-cas/CHANGELOG.md b/legacy-packages/workflow-cas/CHANGELOG.md deleted file mode 100644 index 6cdda10..0000000 --- a/legacy-packages/workflow-cas/CHANGELOG.md +++ /dev/null @@ -1,88 +0,0 @@ -# @uncaged/workflow-cas - -## 0.5.0-alpha.4 - -### Patch Changes - -- Updated dependencies -- Updated dependencies [f74b482] -- Updated dependencies [f74b482] - - @uncaged/workflow-util@0.5.0-alpha.4 - - @uncaged/workflow-protocol@0.5.0-alpha.4 - -## 0.5.0-alpha.3 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.3 - - @uncaged/workflow-util@0.5.0-alpha.3 - -## 0.5.0-alpha.2 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.2 - - @uncaged/workflow-util@0.5.0-alpha.2 - -## 0.5.0-alpha.1 - -### Patch Changes - -- @uncaged/workflow-protocol@0.5.0-alpha.1 -- @uncaged/workflow-util@0.5.0-alpha.1 - -## 0.5.0-alpha.0 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.5.0-alpha.0 - - @uncaged/workflow-util@0.5.0-alpha.0 - -## 0.4.5 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.5 - - @uncaged/workflow-util@0.4.5 - -## 0.4.4 - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.4 - - @uncaged/workflow-util@0.4.4 - -## 0.4.3 - -### Patch Changes - -- Include src/ in published packages so bun runtime can resolve the 'bun' exports condition. -- Updated dependencies - - @uncaged/workflow-protocol@0.4.3 - - @uncaged/workflow-util@0.4.3 - -## 0.4.2 - -### Patch Changes - -- Fix workspace dependency resolution: use workspace:^ so published packages resolve to compatible versions instead of exact (non-existent) versions. -- Updated dependencies - - @uncaged/workflow-protocol@0.4.2 - - @uncaged/workflow-util@0.4.2 - -## 0.4.0 - -### Minor Changes - -- Fix package exports for published packages and adopt changesets for version management. - -### Patch Changes - -- Updated dependencies - - @uncaged/workflow-protocol@0.4.0 - - @uncaged/workflow-util@0.4.0 diff --git a/legacy-packages/workflow-cas/README.md b/legacy-packages/workflow-cas/README.md deleted file mode 100644 index 909e44a..0000000 --- a/legacy-packages/workflow-cas/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# @uncaged/workflow-cas - -Content-addressable storage implementation, bundle hashing, and Merkle helpers. - -## What This Package Does - -It implements `CasStore` from `@uncaged/workflow-protocol`, hashes workflow bundle bytes and strings with XXH64, and builds serializable Merkle nodes for thread/step/content payloads used when persisting execution artifacts. - -## Key Exports - -From `src/index.ts`: - -- **CAS:** `createCasStore` -- **Hash:** `hashString`, `hashWorkflowBundleBytes` -- **Merkle:** `createContentMerkleNode`, `getContentMerklePayload`, `parseMerkleNode`, `putContentMerkleNode`, `putStepMerkleNode`, `putThreadMerkleNode`, `serializeMerkleNode` -- **Types:** `CasStore`, `MerkleNode`, `MerkleNodeType`, `StepMerklePayload`, `ThreadMerklePayload` - -## Dependencies - -- **Workspace:** `@uncaged/workflow-protocol` (`CasStore` contract), `@uncaged/workflow-util` -- **npm:** `xxhashjs`, `yaml` - -## Usage - -```typescript -import { createCasStore, hashWorkflowBundleBytes } from "@uncaged/workflow-cas"; -import { getGlobalCasDir } from "@uncaged/workflow-util"; - -const store = createCasStore(getGlobalCasDir()); -const hash = await hashWorkflowBundleBytes(esmJsBytes); -``` diff --git a/legacy-packages/workflow-cas/__tests__/collect-refs.test.ts b/legacy-packages/workflow-cas/__tests__/collect-refs.test.ts deleted file mode 100644 index cc7812e..0000000 --- a/legacy-packages/workflow-cas/__tests__/collect-refs.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { StateNode } from "@uncaged/workflow-protocol"; - -import { collectRefs } from "../src/collect-refs.js"; - -function payload( - partial: Partial & Pick, -): StateNode["payload"] { - return { - role: partial.role, - meta: partial.meta ?? {}, - start: partial.start ?? "STARTHASH000000000000001", - content: partial.content ?? "CONTENTHASH00000000000001", - ancestors: partial.ancestors ?? [], - compact: partial.compact ?? null, - timestamp: partial.timestamp ?? 0, - childThread: partial.childThread ?? null, - }; -} - -describe("collectRefs", () => { - test("collects start, content, ancestors, and compact hashes in order", () => { - const refs = collectRefs( - payload({ - role: "coder", - start: "01START00000000000000001", - content: "01CONTENT0000000000000001", - ancestors: ["01PARENT0000000000000001", "01GRAND000000000000000001"], - compact: "01COMPACT0000000000000001", - }), - ); - expect(refs).toEqual([ - "01START00000000000000001", - "01CONTENT0000000000000001", - "01PARENT0000000000000001", - "01GRAND000000000000000001", - "01COMPACT0000000000000001", - ]); - }); - - test("does not collect compact when compact is null", () => { - const refs = collectRefs( - payload({ - role: "coder", - start: "S1", - content: "C1", - ancestors: ["A1"], - compact: null, - }), - ); - expect(refs).toEqual(["S1", "C1", "A1"]); - }); - - test("returns only start and content when ancestors is empty", () => { - const refs = collectRefs( - payload({ - role: "coder", - start: "S2", - content: "C2", - ancestors: [], - compact: null, - }), - ); - expect(refs).toEqual(["S2", "C2"]); - }); - - test("includes childThread hash when childThread is non-null", () => { - const refs = collectRefs( - payload({ - role: "developer", - start: "S3", - content: "C3", - ancestors: ["A3"], - compact: null, - childThread: "CHILDEND000000000000001", - }), - ); - expect(refs).toEqual(["S3", "C3", "A3", "CHILDEND000000000000001"]); - }); - - test("does not include childThread when childThread is null", () => { - const refs = collectRefs( - payload({ - role: "developer", - start: "S4", - content: "C4", - ancestors: [], - compact: null, - childThread: null, - }), - ); - expect(refs).toEqual(["S4", "C4"]); - }); -}); diff --git a/legacy-packages/workflow-cas/__tests__/nodes.test.ts b/legacy-packages/workflow-cas/__tests__/nodes.test.ts deleted file mode 100644 index f8e4748..0000000 --- a/legacy-packages/workflow-cas/__tests__/nodes.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { stringify } from "yaml"; - -import { createCasStore } from "../src/cas.js"; -import { parseCasThreadNode, putStartNode, putStateNode } from "../src/nodes.js"; - -describe("putStartNode — parentState in refs", () => { - let dir: string; - - beforeEach(async () => { - dir = await mkdtemp(join(tmpdir(), "wf-cas-nodes-")); - }); - - afterEach(async () => { - await rm(dir, { recursive: true, force: true }); - }); - - test("refs contains only promptHash when parentState is null", async () => { - const cas = createCasStore(join(dir, "cas")); - const promptHash = await cas.put("hello"); - const startHash = await putStartNode( - cas, - { name: "demo", hash: "BUNDLEAAAAAAAAA", depth: 0, parentState: null }, - promptHash, - ); - - const blob = await cas.get(startHash); - expect(blob).not.toBeNull(); - const parsed = parseCasThreadNode(blob ?? ""); - expect(parsed).not.toBeNull(); - expect(parsed?.kind).toBe("start"); - if (parsed?.kind !== "start") return; - - expect(parsed.node.refs).toEqual([promptHash]); - expect(parsed.node.payload.parentState).toBeNull(); - }); - - test("refs contains [promptHash, parentStateHash] when parentState is set", async () => { - const cas = createCasStore(join(dir, "cas")); - const parentStateHash = await cas.put("fake-parent-state"); - const promptHash = await cas.put("child-prompt"); - const startHash = await putStartNode( - cas, - { name: "develop", hash: "BUNDLEBBBBBBBBB", depth: 1, parentState: parentStateHash }, - promptHash, - ); - - const blob = await cas.get(startHash); - expect(blob).not.toBeNull(); - const parsed = parseCasThreadNode(blob ?? ""); - expect(parsed).not.toBeNull(); - expect(parsed?.kind).toBe("start"); - if (parsed?.kind !== "start") return; - - expect(parsed.node.refs).toEqual([promptHash, parentStateHash]); - expect(parsed.node.payload.parentState).toBe(parentStateHash); - }); -}); - -describe("putStateNode — childThread in refs", () => { - let dir: string; - - beforeEach(async () => { - dir = await mkdtemp(join(tmpdir(), "wf-cas-nodes-state-")); - }); - - afterEach(async () => { - await rm(dir, { recursive: true, force: true }); - }); - - test("refs does not include childThread when childThread is null", async () => { - const cas = createCasStore(join(dir, "cas")); - const startHash = await cas.put("start"); - const contentHash = await cas.put("content"); - const stateHash = await putStateNode(cas, { - role: "planner", - meta: {}, - start: startHash, - content: contentHash, - ancestors: [], - compact: null, - timestamp: 1000, - childThread: null, - }); - - const blob = await cas.get(stateHash); - expect(blob).not.toBeNull(); - const parsed = parseCasThreadNode(blob ?? ""); - expect(parsed?.kind).toBe("state"); - if (parsed?.kind !== "state") return; - - expect(parsed.node.refs).not.toContain("anything-else"); - expect(parsed.node.refs).toEqual([startHash, contentHash]); - expect(parsed.node.payload.childThread).toBeNull(); - }); - - test("refs includes childThread hash when childThread is set", async () => { - const cas = createCasStore(join(dir, "cas")); - const startHash = await cas.put("start"); - const contentHash = await cas.put("content"); - const childEndHash = await cas.put("child-end-state"); - const stateHash = await putStateNode(cas, { - role: "developer", - meta: { pr: 42 }, - start: startHash, - content: contentHash, - ancestors: [], - compact: null, - timestamp: 2000, - childThread: childEndHash, - }); - - const blob = await cas.get(stateHash); - expect(blob).not.toBeNull(); - const parsed = parseCasThreadNode(blob ?? ""); - expect(parsed?.kind).toBe("state"); - if (parsed?.kind !== "state") return; - - expect(parsed.node.refs).toContain(childEndHash); - expect(parsed.node.payload.childThread).toBe(childEndHash); - }); -}); - -describe("parseCasThreadNode — legacy node compatibility", () => { - test("start node without parentState field defaults to null", () => { - const yaml = stringify({ - type: "start", - payload: { name: "demo", hash: "BUNDLEAAAAAAAAA", depth: 0 }, - refs: ["PROMPTHASH00001"], - }); - const parsed = parseCasThreadNode(yaml); - expect(parsed).not.toBeNull(); - expect(parsed?.kind).toBe("start"); - if (parsed?.kind !== "start") return; - expect(parsed.node.payload.parentState).toBeNull(); - }); - - test("state node without childThread field defaults to null", () => { - const yaml = stringify({ - type: "state", - payload: { - role: "planner", - meta: {}, - start: "STARTHASH00001", - content: "CONTENTHASH0001", - ancestors: [], - compact: null, - timestamp: 1000, - }, - refs: ["STARTHASH00001", "CONTENTHASH0001"], - }); - const parsed = parseCasThreadNode(yaml); - expect(parsed).not.toBeNull(); - expect(parsed?.kind).toBe("state"); - if (parsed?.kind !== "state") return; - expect(parsed.node.payload.childThread).toBeNull(); - }); -}); diff --git a/legacy-packages/workflow-cas/__tests__/reachable.test.ts b/legacy-packages/workflow-cas/__tests__/reachable.test.ts deleted file mode 100644 index 507869d..0000000 --- a/legacy-packages/workflow-cas/__tests__/reachable.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import type { CasStore } from "@uncaged/workflow-protocol"; -import { stringify } from "yaml"; - -import { findReachableHashes } from "../src/reachable.js"; - -function yamlBlob(refs: readonly string[]): string { - return stringify({ type: "node", payload: {}, refs: [...refs] }, { indent: 2 }); -} - -function memoryCas(entries: Record): CasStore { - const map = { ...entries }; - return { - async put(): Promise { - throw new Error("memoryCas.put not used in tests"); - }, - async get(hash: string): Promise { - return map[hash] ?? null; - }, - async delete(): Promise {}, - async list(): Promise { - return Object.keys(map); - }, - }; -} - -describe("findReachableHashes", () => { - test("walks refs recursively from a single root", async () => { - const cas = memoryCas({ - R1: yamlBlob(["R2"]), - R2: yamlBlob(["R3"]), - R3: yamlBlob([]), - }); - const reachable = await findReachableHashes(["R1"], cas); - expect([...reachable].sort()).toEqual(["R1", "R2", "R3"]); - }); - - test("union of reachability from multiple roots", async () => { - const cas = memoryCas({ - A: yamlBlob(["X"]), - B: yamlBlob(["Y"]), - X: yamlBlob([]), - Y: yamlBlob(["Z"]), - Z: yamlBlob([]), - }); - const reachable = await findReachableHashes(["A", "B"], cas); - expect([...reachable].sort()).toEqual(["A", "B", "X", "Y", "Z"]); - }); - - test("handles cycles via visited set", async () => { - const cas = memoryCas({ - C1: yamlBlob(["C2"]), - C2: yamlBlob(["C1"]), - }); - const reachable = await findReachableHashes(["C1"], cas); - expect(reachable.size).toBe(2); - expect(reachable.has("C1")).toBe(true); - expect(reachable.has("C2")).toBe(true); - }); - - test("does not throw when a ref points to a missing blob", async () => { - const cas = memoryCas({ - H1: yamlBlob(["MISSINGHASH0000000000001"]), - }); - const reachable = await findReachableHashes(["H1"], cas); - expect(reachable.has("H1")).toBe(true); - expect(reachable.has("MISSINGHASH0000000000001")).toBe(false); - }); -}); diff --git a/legacy-packages/workflow-cas/package.json b/legacy-packages/workflow-cas/package.json deleted file mode 100644 index 0bbf834..0000000 --- a/legacy-packages/workflow-cas/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@uncaged/workflow-cas", - "version": "0.5.0-alpha.4", - "files": [ - "src", - "dist", - "package.json" - ], - "type": "module", - "scripts": { - "test": "bun test" - }, - "exports": { - ".": { - "bun": "./src/index.ts", - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "dependencies": { - "@uncaged/workflow-protocol": "workspace:^", - "@uncaged/workflow-util": "workspace:^", - "xxhashjs": "^0.2.2", - "yaml": "^2.7.1" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/legacy-packages/workflow-cas/pnpm-lock.yaml b/legacy-packages/workflow-cas/pnpm-lock.yaml deleted file mode 100644 index 4a7fc1b..0000000 --- a/legacy-packages/workflow-cas/pnpm-lock.yaml +++ /dev/null @@ -1,75 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@uncaged/workflow-protocol': - specifier: workspace:* - version: link:../workflow-protocol - '@uncaged/workflow-util': - specifier: workspace:* - version: link:../workflow-util - xxhashjs: - specifier: ^0.2.2 - version: 0.2.2 - yaml: - specifier: ^2.7.1 - version: 2.8.4 - devDependencies: - '@types/bun': - specifier: latest - version: 1.3.13 - -packages: - - '@types/bun@1.3.13': - resolution: {integrity: sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw==} - - '@types/node@25.6.2': - resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} - - bun-types@1.3.13: - resolution: {integrity: sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA==} - - cuint@0.2.2: - resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==} - - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - - xxhashjs@0.2.2: - resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==} - - yaml@2.8.4: - resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} - engines: {node: '>= 14.6'} - hasBin: true - -snapshots: - - '@types/bun@1.3.13': - dependencies: - bun-types: 1.3.13 - - '@types/node@25.6.2': - dependencies: - undici-types: 7.19.2 - - bun-types@1.3.13: - dependencies: - '@types/node': 25.6.2 - - cuint@0.2.2: {} - - undici-types@7.19.2: {} - - xxhashjs@0.2.2: - dependencies: - cuint: 0.2.2 - - yaml@2.8.4: {} diff --git a/legacy-packages/workflow-cas/src/cas.ts b/legacy-packages/workflow-cas/src/cas.ts deleted file mode 100644 index e84b0a4..0000000 --- a/legacy-packages/workflow-cas/src/cas.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import { hashString } from "./hash.js"; -import { createContentMerkleNode, parseMerkleNode, serializeMerkleNode } from "./merkle.js"; -import { isCasNodeYaml } from "./nodes.js"; -import type { CasStore } from "./types.js"; - -/** Raw strings become content merkle YAML; already-valid merkle documents pass through. */ -function normalizeCasPutContent(content: string): string { - if (isCasNodeYaml(content)) { - return content; - } - try { - parseMerkleNode(content); - return content; - } catch { - return serializeMerkleNode(createContentMerkleNode(content)); - } -} - -export function createCasStore(casDir: string): CasStore { - async function ensureDir(): Promise { - await mkdir(casDir, { recursive: true }); - } - - function filePath(hash: string): string { - return join(casDir, `${hash}.txt`); - } - - return { - async put(content: string): Promise { - const toStore = normalizeCasPutContent(content); - const hash = hashString(toStore); - await ensureDir(); - const target = filePath(hash); - const tmp = `${target}.tmp.${Date.now()}`; - await writeFile(tmp, toStore, "utf8"); - await rename(tmp, target); - return hash; - }, - - async get(hash: string): Promise { - try { - return await readFile(filePath(hash), "utf8"); - } catch (e) { - const errObj = e as NodeJS.ErrnoException; - if (errObj.code === "ENOENT") { - return null; - } - throw e; - } - }, - - async delete(hash: string): Promise { - try { - await unlink(filePath(hash)); - } catch (e) { - const errObj = e as NodeJS.ErrnoException; - if (errObj.code === "ENOENT") { - return; - } - throw e; - } - }, - - async list(): Promise { - try { - const entries = await readdir(casDir); - return entries.filter((name) => name.endsWith(".txt")).map((name) => name.slice(0, -4)); - } catch (e) { - const errObj = e as NodeJS.ErrnoException; - if (errObj.code === "ENOENT") { - return []; - } - throw e; - } - }, - }; -} diff --git a/legacy-packages/workflow-cas/src/collect-refs.ts b/legacy-packages/workflow-cas/src/collect-refs.ts deleted file mode 100644 index 7fbbd52..0000000 --- a/legacy-packages/workflow-cas/src/collect-refs.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { StateNode } from "@uncaged/workflow-protocol"; - -/** Collects CAS hashes from {@link StateNode} payload fields for GC `refs[]` derivation. */ -export function collectRefs(payload: StateNode["payload"]): string[] { - const out: string[] = [payload.start, payload.content]; - for (const h of payload.ancestors) { - out.push(h); - } - if (payload.compact !== null) { - out.push(payload.compact); - } - if (payload.childThread !== null) { - out.push(payload.childThread); - } - return out; -} diff --git a/legacy-packages/workflow-cas/src/hash.ts b/legacy-packages/workflow-cas/src/hash.ts deleted file mode 100644 index 3fb9d6e..0000000 --- a/legacy-packages/workflow-cas/src/hash.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Buffer } from "node:buffer"; -import { encodeUint64AsCrockford } from "@uncaged/workflow-util"; -import XXH from "xxhashjs"; - -function digestToUint64(digest: { toString(radix?: number): string }): bigint { - const hex = digest.toString(16).padStart(16, "0"); - return BigInt(`0x${hex}`); -} - -/** XXH64 (seed 0) over bundle bytes, encoded as 13-char Crockford Base32. */ -export function hashWorkflowBundleBytes(data: Uint8Array): string { - const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); - const digest = XXH.h64(0).update(buf).digest(); - return encodeUint64AsCrockford(digestToUint64(digest)); -} - -/** XXH64 (seed 0) over a UTF-8 string, encoded as 13-char Crockford Base32. */ -export function hashString(content: string): string { - const buf = Buffer.from(content, "utf8"); - const digest = XXH.h64(0).update(buf).digest(); - return encodeUint64AsCrockford(digestToUint64(digest)); -} diff --git a/legacy-packages/workflow-cas/src/index.ts b/legacy-packages/workflow-cas/src/index.ts deleted file mode 100644 index 3686f59..0000000 --- a/legacy-packages/workflow-cas/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { createCasStore } from "./cas.js"; -export { hashWorkflowBundleBytes } from "./hash.js"; -export { - createContentMerkleNode, - getContentMerklePayload, - putContentMerkleNode, - serializeMerkleNode, -} from "./merkle.js"; -export { - parseCasThreadNode, - putContentNodeWithRefs, - putStartNode, - putStateNode, -} from "./nodes.js"; -export { findReachableHashes } from "./reachable.js"; -export type { CasStore } from "./types.js"; diff --git a/legacy-packages/workflow-cas/src/merkle.ts b/legacy-packages/workflow-cas/src/merkle.ts deleted file mode 100644 index 2a78528..0000000 --- a/legacy-packages/workflow-cas/src/merkle.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { parse, stringify } from "yaml"; - -import type { - CasStore, - MerkleNode, - MerkleNodeType, - StepMerklePayload, - ThreadMerklePayload, -} from "./types.js"; - -function requireStringHashArray(value: unknown, notArrayMessage: string): string[] { - if (!Array.isArray(value)) { - throw new Error(notArrayMessage); - } - const out: string[] = []; - for (const c of value) { - if (typeof c !== "string") { - throw new Error("merkle: hash entry must be a string"); - } - out.push(c); - } - return out; -} - -function edgeListRaw(rec: Record, type: MerkleNodeType): unknown { - if (type === "content") { - return rec.refs !== undefined ? rec.refs : rec.children; - } - return rec.children; -} - -export function serializeMerkleNode(node: MerkleNode): string { - if (node.type === "content") { - return stringify( - { type: node.type, payload: node.payload, refs: node.children }, - { indent: 2 }, - ); - } - return stringify( - { type: node.type, payload: node.payload, children: node.children }, - { indent: 2 }, - ); -} - -export function parseMerkleNode(yamlText: string): MerkleNode { - const raw = parse(yamlText) as unknown; - if (raw === null || typeof raw !== "object") { - throw new Error("merkle: YAML root must be an object"); - } - const rec = raw as Record; - const type = rec.type; - const payload = rec.payload; - if (type !== "content" && type !== "step" && type !== "thread") { - throw new Error("merkle: invalid or missing type"); - } - if (typeof payload !== "string" && (payload === null || typeof payload !== "object")) { - throw new Error("merkle: payload must be a string or object"); - } - - const notArrayMsg = - type === "content" - ? "merkle: content node requires refs or children array" - : "merkle: children must be an array"; - const childHashes = requireStringHashArray(edgeListRaw(rec, type), notArrayMsg); - return { - type, - payload: typeof payload === "string" ? payload : (payload as Record), - children: childHashes, - }; -} - -export function createContentMerkleNode(payload: string): MerkleNode { - return { type: "content", payload, children: [] }; -} - -/** Serializes a step Merkle node (role + meta + content child) and stores it in CAS. */ -export async function putStepMerkleNode( - store: CasStore, - payload: StepMerklePayload, - contentHash: string, -): Promise { - const node: MerkleNode = { - type: "step", - payload: { role: payload.role, meta: payload.meta }, - children: [contentHash], - }; - return store.put(serializeMerkleNode(node)); -} - -/** Serializes the thread root Merkle node and stores it in CAS. */ -export async function putThreadMerkleNode( - store: CasStore, - payload: ThreadMerklePayload, - stepHashes: readonly string[], -): Promise { - const node: MerkleNode = { - type: "thread", - payload: { - workflow: payload.workflow, - threadId: payload.threadId, - result: payload.result, - }, - children: [...stepHashes], - }; - return store.put(serializeMerkleNode(node)); -} - -/** Stores agent/content text via CAS; {@link createCasStore} wraps raw strings as merkle content nodes. */ -export async function putContentMerkleNode(store: CasStore, content: string): Promise { - return store.put(content); -} - -/** - * Loads a CAS blob and returns the payload string for a `content` node. - * - * Accepts both the legacy `{ type:content, payload, children }` Merkle layout - * and the RFC-aligned `{ type:content, payload, refs }` content node layout. - */ -export async function getContentMerklePayload( - store: CasStore, - hash: string, -): Promise { - const yamlText = await store.get(hash); - if (yamlText === null) { - return null; - } - const raw = parse(yamlText) as unknown; - if (raw === null || typeof raw !== "object") { - return null; - } - const rec = raw as Record; - if (rec.type !== "content" || typeof rec.payload !== "string") { - return null; - } - return rec.payload; -} diff --git a/legacy-packages/workflow-cas/src/nodes.ts b/legacy-packages/workflow-cas/src/nodes.ts deleted file mode 100644 index 9db4f70..0000000 --- a/legacy-packages/workflow-cas/src/nodes.ts +++ /dev/null @@ -1,221 +0,0 @@ -import type { - ContentMerkleNode, - StartNode, - StartNodePayload, - StateNode, - StateNodePayload, -} from "@uncaged/workflow-protocol"; -import { parse, stringify } from "yaml"; - -import { collectRefs } from "./collect-refs.js"; -import type { CasStore } from "./types.js"; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function isStartPayload(value: unknown): value is StartNodePayload { - if (!isRecord(value)) { - return false; - } - const parentState = value.parentState; - if (parentState !== undefined && parentState !== null && typeof parentState !== "string") { - return false; - } - return ( - typeof value.name === "string" && - typeof value.hash === "string" && - typeof value.depth === "number" - ); -} - -/** Normalizes a raw start payload, defaulting `parentState` to `null` for legacy nodes. */ -function normalizeStartPayload(raw: StartNodePayload): StartNodePayload { - return { - name: raw.name, - hash: raw.hash, - depth: raw.depth, - parentState: raw.parentState ?? null, - }; -} - -function isStatePayload(value: unknown): value is StateNodePayload { - if (!isRecord(value)) { - return false; - } - const compact = value.compact; - if (!(compact === null || typeof compact === "string")) { - return false; - } - const ancestors = value.ancestors; - if (!Array.isArray(ancestors) || !ancestors.every((h) => typeof h === "string")) { - return false; - } - const meta = value.meta; - if (!isRecord(meta)) { - return false; - } - const childThread = value.childThread; - if (childThread !== undefined && childThread !== null && typeof childThread !== "string") { - return false; - } - return ( - typeof value.role === "string" && - typeof value.start === "string" && - typeof value.content === "string" && - typeof value.timestamp === "number" - ); -} - -/** Normalizes a raw state payload, defaulting `childThread` to `null` for legacy nodes. */ -function normalizeStatePayload(raw: StateNodePayload): StateNodePayload { - return { - role: raw.role, - meta: raw.meta, - start: raw.start, - content: raw.content, - ancestors: raw.ancestors, - compact: raw.compact, - timestamp: raw.timestamp, - childThread: raw.childThread ?? null, - }; -} - -/** Parses a YAML CAS blob into a typed RFC v3 thread node (or legacy content layout with `children`). */ -export function parseCasThreadNode(yamlText: string): ParsedCasThreadNode | null { - let raw: unknown; - try { - raw = parse(yamlText) as unknown; - } catch { - return null; - } - if (!isRecord(raw)) { - return null; - } - const type = raw.type; - if (type !== "start" && type !== "state" && type !== "content") { - return null; - } - - let refsRaw: unknown = raw.refs; - if (refsRaw === undefined && type === "content") { - refsRaw = raw.children; - } - if (!Array.isArray(refsRaw) || !refsRaw.every((r) => typeof r === "string")) { - return null; - } - const refs = refsRaw as string[]; - - if (type === "content") { - if (typeof raw.payload !== "string") { - return null; - } - const node: ContentMerkleNode = { type: "content", payload: raw.payload, refs: [...refs] }; - return { kind: "content", node }; - } - - if (type === "start") { - if (!isStartPayload(raw.payload)) { - return null; - } - const node: StartNode = { - type: "start", - payload: normalizeStartPayload(raw.payload), - refs: [...refs], - }; - return { kind: "start", node }; - } - - if (!isStatePayload(raw.payload)) { - return null; - } - const node: StateNode = { - type: "state", - payload: normalizeStatePayload(raw.payload), - refs: [...refs], - }; - return { kind: "state", node }; -} - -export type ParsedCasThreadNode = - | { kind: "start"; node: StartNode } - | { kind: "state"; node: StateNode } - | { kind: "content"; node: ContentMerkleNode }; - -/** YAML-serialize a CAS node carrying `{type, payload, refs}` (RFC v3 thread storage format). */ -export function serializeCasNode(node: StartNode | StateNode | ContentMerkleNode): string { - return stringify({ type: node.type, payload: node.payload, refs: node.refs }, { indent: 2 }); -} - -/** - * Recognizes a YAML CAS blob with the `{type, payload, refs[]}` shape used by - * `start` / `state` / `content` thread nodes. Used by {@link createCasStore} - * to skip the legacy auto-wrap step when the caller already supplied a - * pre-serialized RFC v3 node. - */ -export function isCasNodeYaml(content: string): boolean { - let raw: unknown; - try { - raw = parse(content) as unknown; - } catch { - return false; - } - if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { - return false; - } - const rec = raw as Record; - if (typeof rec.type !== "string") { - return false; - } - if (!Array.isArray(rec.refs)) { - return false; - } - for (const r of rec.refs) { - if (typeof r !== "string") { - return false; - } - } - return true; -} - -export async function putStartNode( - store: CasStore, - payload: StartNode["payload"], - promptHash: string, -): Promise { - const refs = [promptHash]; - if (payload.parentState !== null) { - refs.push(payload.parentState); - } - const node: StartNode = { - type: "start", - payload, - refs, - }; - return store.put(serializeCasNode(node)); -} - -export async function putStateNode( - store: CasStore, - payload: StateNode["payload"], -): Promise { - const node: StateNode = { - type: "state", - payload, - refs: collectRefs(payload), - }; - return store.put(serializeCasNode(node)); -} - -export async function putContentNodeWithRefs( - store: CasStore, - payload: string, - refs: readonly string[], -): Promise { - const node: ContentMerkleNode = { - type: "content", - payload, - refs: [...refs], - }; - return store.put(serializeCasNode(node)); -} diff --git a/legacy-packages/workflow-cas/src/reachable.ts b/legacy-packages/workflow-cas/src/reachable.ts deleted file mode 100644 index 4c18ebb..0000000 --- a/legacy-packages/workflow-cas/src/reachable.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { parse } from "yaml"; - -import type { CasStore } from "./types.js"; - -function refsFromBlob(content: string): string[] { - try { - const raw = parse(content) as unknown; - if (raw === null || typeof raw !== "object") { - return []; - } - const rec = raw as Record; - let refs = rec.refs; - if (!Array.isArray(refs) && Array.isArray(rec.children)) { - refs = rec.children; - } - if (!Array.isArray(refs)) { - return []; - } - const out: string[] = []; - for (const r of refs) { - if (typeof r === "string") { - out.push(r); - } - } - return out; - } catch { - return []; - } -} - -/** Recursively collects all CAS hashes reachable from `roots` via each blob's `refs[]`. */ -export async function findReachableHashes( - roots: readonly string[], - cas: CasStore, -): Promise> { - const visited = new Set(); - const stack = [...roots]; - while (stack.length > 0) { - const hash = stack.pop(); - if (hash === undefined) { - break; - } - if (visited.has(hash)) { - continue; - } - const blob = await cas.get(hash); - if (blob === null) { - continue; - } - visited.add(hash); - for (const ref of refsFromBlob(blob)) { - if (!visited.has(ref)) { - stack.push(ref); - } - } - } - return visited; -} diff --git a/legacy-packages/workflow-cas/src/types.ts b/legacy-packages/workflow-cas/src/types.ts deleted file mode 100644 index fd706cf..0000000 --- a/legacy-packages/workflow-cas/src/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type { CasStore } from "@uncaged/workflow-protocol"; - -export type MerkleNodeType = "content" | "step" | "thread"; - -export type MerkleNode = { - type: MerkleNodeType; - payload: string | Record; - children: string[]; -}; - -export type StepMerklePayload = { - role: string; - meta: Record; -}; - -export type ThreadMerklePayload = { - workflow: string; - threadId: string; - result: { - returnCode: number; - summary: string; - }; -}; diff --git a/legacy-packages/workflow-cas/tsconfig.json b/legacy-packages/workflow-cas/tsconfig.json deleted file mode 100644 index b852fc3..0000000 --- a/legacy-packages/workflow-cas/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "composite": true - }, - "include": ["src"], - "references": [{ "path": "../workflow-protocol" }, { "path": "../workflow-util" }] -} diff --git a/legacy-packages/workflow-dashboard/.env.production b/legacy-packages/workflow-dashboard/.env.production deleted file mode 100644 index fb39940..0000000 --- a/legacy-packages/workflow-dashboard/.env.production +++ /dev/null @@ -1 +0,0 @@ -VITE_GATEWAY_URL=https://workflow-gateway.shazhou.workers.dev diff --git a/legacy-packages/workflow-dashboard/README.md b/legacy-packages/workflow-dashboard/README.md deleted file mode 100644 index a95b3fd..0000000 --- a/legacy-packages/workflow-dashboard/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# @uncaged/workflow-dashboard - -Web dashboard for the Uncaged Workflow engine. Connects to the local -`uncaged-workflow serve` API to display threads, workflows, and CAS data. - -## Development - -```bash -# Start the local API server (in another terminal) -uncaged-workflow serve - -# Start the dashboard dev server -bun run dev -``` - -Opens at http://localhost:5173. Vite proxies `/api/*` to `localhost:7860`. - -## Build - -```bash -bun run build -``` - -Output goes to `dist/` — static files ready for CF Pages or any host. diff --git a/legacy-packages/workflow-dashboard/index.html b/legacy-packages/workflow-dashboard/index.html deleted file mode 100644 index 3ae7430..0000000 --- a/legacy-packages/workflow-dashboard/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - Workflow Dashboard - - - -
- - - diff --git a/legacy-packages/workflow-dashboard/package.json b/legacy-packages/workflow-dashboard/package.json deleted file mode 100644 index dbf27ee..0000000 --- a/legacy-packages/workflow-dashboard/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@uncaged/workflow-dashboard", - "version": "0.1.0", - "files": [ - "dist", - "package.json" - ], - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tooltip": "^1.2.8", - "@xyflow/react": "^12.10.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^1.16.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "react-markdown": "^10.1.0", - "react-router": "^7.15.1", - "shiki": "^4.0.2", - "tailwind-merge": "^3.6.0" - }, - "devDependencies": { - "@tailwindcss/vite": "^4.2.4", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", - "tailwindcss": "^4.2.4", - "typescript": "^6.0.3", - "vite": "^8.0.11" - } -} diff --git a/legacy-packages/workflow-dashboard/plugins/vite-limit-line-plugin.ts b/legacy-packages/workflow-dashboard/plugins/vite-limit-line-plugin.ts deleted file mode 100644 index 7d8e1f5..0000000 --- a/legacy-packages/workflow-dashboard/plugins/vite-limit-line-plugin.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { createFilter, type Plugin } from "vite"; - -type LimitLineOverride = { - files: string; - maxReactFCLines: number | null; - maxFileLines: number | null; -}; - -type LimitLineOptions = { - maxReactFCLines: number; - maxFileLines: number; - include: RegExp; - exclude: RegExp | null; - overrides: Array; -}; - -const DEFAULT_OPTIONS: LimitLineOptions = { - maxReactFCLines: 300, - maxFileLines: 600, - include: /\.[tj]sx$/, - exclude: null, - overrides: [], -}; - -type ResolvedLimits = { - maxReactFCLines: number | null; - maxFileLines: number | null; -}; - -type ComponentInfo = { - name: string; - startLine: number; - lineCount: number; -}; - -const PASCAL_CASE = /^[A-Z][A-Za-z0-9]*$/; - -// --- AST types (Rolldown ESTree subset) --- - -type Identifier = { - type: "Identifier"; - name: string; -}; - -type MemberExpression = { - type: "MemberExpression"; - object: AstExpression; - property: Identifier; -}; - -type CallExpression = { - type: "CallExpression"; - callee: AstExpression; - arguments: Array; -}; - -type AstExpression = - | Identifier - | MemberExpression - | CallExpression - | { - type: string; - [key: string]: unknown; - }; - -type VariableDeclarator = { - id: Identifier | null; - init: AstExpression | null; -}; - -type AstStatement = { - type: string; - id: Identifier | null; - declaration: AstStatement | null; - declarations: Array; - body: Array; - [key: string]: unknown; -}; - -type AstProgram = { - type: "Program"; - body: Array; -}; - -// --- AST helpers --- - -function isFunctionLike(node: AstExpression): boolean { - return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression"; -} - -const WRAPPER_NAMES = new Set(["memo", "forwardRef", "lazy"]); - -function isWrapperCall(node: AstExpression): boolean { - if (node.type !== "CallExpression") return false; - const call = node as CallExpression; - const callee = call.callee; - - if (callee.type === "Identifier") { - return WRAPPER_NAMES.has((callee as Identifier).name); - } - - if (callee.type === "MemberExpression") { - const member = callee as MemberExpression; - return member.property.type === "Identifier" && WRAPPER_NAMES.has(member.property.name); - } - - return false; -} - -function extractComponentNames(ast: AstProgram): Array { - const names: Array = []; - - for (const node of ast.body) { - if (node.type === "FunctionDeclaration" && node.id && PASCAL_CASE.test(node.id.name)) { - names.push(node.id.name); - continue; - } - - if (node.type === "ExportNamedDeclaration" && node.declaration) { - const decl = node.declaration; - if (decl.type === "FunctionDeclaration" && decl.id && PASCAL_CASE.test(decl.id.name)) { - names.push(decl.id.name); - continue; - } - if (decl.type === "VariableDeclaration") { - collectNamesFromVarDeclaration(decl, names); - continue; - } - } - - if (node.type === "VariableDeclaration") { - collectNamesFromVarDeclaration(node, names); - } - } - - return names; -} - -function collectNamesFromVarDeclaration(node: AstStatement, names: Array): void { - for (const declarator of node.declarations ?? []) { - if (!declarator.id || !PASCAL_CASE.test(declarator.id.name) || !declarator.init) continue; - const init = declarator.init; - if (isFunctionLike(init)) { - names.push(declarator.id.name); - } else if (isWrapperCall(init)) { - const args = (init as CallExpression).arguments; - if (args.length > 0 && isFunctionLike(args[0])) { - names.push(declarator.id.name); - } - } - } -} - -// --- Source measurement --- - -function measureComponentInSource(name: string, lines: Array): ComponentInfo | null { - const fnPattern = new RegExp(`^(?:export\\s+)?function\\s+${name}\\s*[(<]`); - const varPattern = new RegExp(`^(?:export\\s+)?const\\s+${name}\\s*[=:]`); - - for (let i = 0; i < lines.length; i++) { - const trimmed = lines[i].trimStart(); - const isFnDecl = fnPattern.test(trimmed); - const isVarDecl = varPattern.test(trimmed); - if (!isFnDecl && !isVarDecl) continue; - - if (isFnDecl) { - const result = measureFromParams(i, lines); - if (result) return { ...result, name }; - return null; - } - const result = measureFromArrow(i, lines); - if (result) return { ...result, name }; - return null; - } - - return null; -} - -// function Foo(...) { ... } — skip params via parens, then brace-match the body -function measureFromParams(startLine: number, lines: Array): ComponentInfo | null { - let parenDepth = 0; - let pastParams = false; - let braceDepth = 0; - - for (let j = startLine; j < lines.length; j++) { - for (const ch of lines[j]) { - if (!pastParams) { - if (ch === "(") parenDepth++; - else if (ch === ")") { - parenDepth--; - if (parenDepth === 0) pastParams = true; - } - } else { - if (ch === "{") braceDepth++; - else if (ch === "}") { - braceDepth--; - if (braceDepth === 0) { - return { name: "", startLine: startLine + 1, lineCount: j - startLine + 1 }; - } - } - } - } - } - - return null; -} - -// const Foo = (...) => { ... } / const Foo = memo((...) => { ... }) -// Find `=>` first, then brace-match from there to skip type annotations in params -function measureFromArrow(startLine: number, lines: Array): ComponentInfo | null { - let arrowFound = false; - let braceDepth = 0; - let foundBrace = false; - - for (let j = startLine; j < lines.length; j++) { - const line = lines[j]; - for (let c = 0; c < line.length; c++) { - if (!arrowFound) { - if (line[c] === "=" && line[c + 1] === ">") { - arrowFound = true; - c++; - } - continue; - } - if (line[c] === "{") { - braceDepth++; - foundBrace = true; - } else if (line[c] === "}") { - braceDepth--; - if (foundBrace && braceDepth === 0) { - return { name: "", startLine: startLine + 1, lineCount: j - startLine + 1 }; - } - } - } - } - - return null; -} - -// --- Config resolution --- - -function createLimitResolver(options: LimitLineOptions): (id: string) => ResolvedLimits { - const matchers = options.overrides.map((override) => ({ - match: createFilter(override.files), - maxReactFCLines: override.maxReactFCLines, - maxFileLines: override.maxFileLines, - })); - - return (id: string): ResolvedLimits => { - let maxReactFCLines: number | null = options.maxReactFCLines; - let maxFileLines: number | null = options.maxFileLines; - - for (const matcher of matchers) { - if (matcher.match(id)) { - maxReactFCLines = matcher.maxReactFCLines; - maxFileLines = matcher.maxFileLines; - } - } - - return { maxReactFCLines, maxFileLines }; - }; -} - -function shouldProcess(id: string, options: LimitLineOptions): boolean { - return ( - options.include.test(id) && - !id.includes("node_modules") && - (options.exclude === null || !options.exclude.test(id)) - ); -} - -// --- Plugin --- - -function viteLimitLinePlugin(userOptions: Partial = {}): Array { - const options: LimitLineOptions = { - ...DEFAULT_OPTIONS, - ...userOptions, - overrides: userOptions.overrides ?? [], - }; - const resolve = createLimitResolver(options); - - const rawCodeCache = new Map(); - - return [ - { - name: "vite-plugin-limit-line:pre", - enforce: "pre", - - transform(code, id) { - if (!shouldProcess(id, options)) return null; - - rawCodeCache.set(id, code); - - const limits = resolve(id); - if (limits.maxFileLines === null) return null; - - const totalLines = code.split("\n").length; - if (totalLines > limits.maxFileLines) { - this.error( - [ - `[vite-limit-line] File too long: ${totalLines} lines (limit: ${limits.maxFileLines})`, - ` file: ${id}`, - "", - "How to fix:", - " Split this file into smaller modules — extract related types, helpers,", - " or sub-components into separate files and re-export from an index.ts.", - ].join("\n"), - ); - } - - return null; - }, - }, - { - name: "vite-plugin-limit-line:fc", - - transform(code, id) { - if (!shouldProcess(id, options)) return null; - - const limits = resolve(id); - if (limits.maxReactFCLines === null) return null; - - const ast = this.parse(code) as unknown as AstProgram; - const componentNames = extractComponentNames(ast); - if (componentNames.length === 0) return null; - - const raw = rawCodeCache.get(id) ?? code; - rawCodeCache.delete(id); - const rawLines = raw.split("\n"); - - const maxFCLines = limits.maxReactFCLines; - const violations: Array = []; - for (const name of componentNames) { - const info = measureComponentInSource(name, rawLines); - if (info && info.lineCount > maxFCLines) { - violations.push(info); - } - } - - if (violations.length > 0) { - const details = violations - .map( - (v) => - ` ${v.name} (line ${v.startLine}): ${v.lineCount} lines (limit: ${maxFCLines})`, - ) - .join("\n"); - - this.error( - [ - `[vite-limit-line] React component too long in ${id}:`, - details, - "", - "How to fix:", - " Break each oversized component into smaller ones. Extract reusable", - " sections into child components, move complex logic into custom hooks,", - " and keep each component focused on a single responsibility.", - ].join("\n"), - ); - } - - return null; - }, - - buildEnd() { - rawCodeCache.clear(); - }, - }, - ]; -} - -export type { LimitLineOptions, LimitLineOverride }; -export { viteLimitLinePlugin }; diff --git a/legacy-packages/workflow-dashboard/pnpm-lock.yaml b/legacy-packages/workflow-dashboard/pnpm-lock.yaml deleted file mode 100644 index 9a34734..0000000 --- a/legacy-packages/workflow-dashboard/pnpm-lock.yaml +++ /dev/null @@ -1,1668 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - react: - specifier: ^19.2.6 - version: 19.2.6 - react-dom: - specifier: ^19.2.6 - version: 19.2.6(react@19.2.6) - react-markdown: - specifier: ^10.1.0 - version: 10.1.0(@types/react@19.2.14)(react@19.2.6) - shiki: - specifier: ^4.0.2 - version: 4.0.2 - devDependencies: - '@tailwindcss/vite': - specifier: ^4.2.4 - version: 4.3.0(vite@8.0.12(jiti@2.7.0)) - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.12(jiti@2.7.0)) - tailwindcss: - specifier: ^4.2.4 - version: 4.3.0 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vite: - specifier: ^8.0.11 - version: 8.0.12(jiti@2.7.0) - -packages: - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@oxc-project/types@0.129.0': - resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} - - '@rolldown/binding-android-arm64@1.0.0': - resolution: {integrity: sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.0': - resolution: {integrity: sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.0': - resolution: {integrity: sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.0': - resolution: {integrity: sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0': - resolution: {integrity: sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.0': - resolution: {integrity: sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.0': - resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.0': - resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.0': - resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.0': - resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.0': - resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.0': - resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.0': - resolution: {integrity: sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0': - resolution: {integrity: sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.0': - resolution: {integrity: sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.0': - resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==} - - '@rolldown/pluginutils@1.0.0-rc.7': - resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} - - '@shikijs/core@4.0.2': - resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} - engines: {node: '>=20'} - - '@shikijs/engine-javascript@4.0.2': - resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} - engines: {node: '>=20'} - - '@shikijs/engine-oniguruma@4.0.2': - resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} - engines: {node: '>=20'} - - '@shikijs/langs@4.0.2': - resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} - engines: {node: '>=20'} - - '@shikijs/primitive@4.0.2': - resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} - engines: {node: '>=20'} - - '@shikijs/themes@4.0.2': - resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} - engines: {node: '>=20'} - - '@shikijs/types@4.0.2': - resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} - engines: {node: '>=20'} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - - '@tailwindcss/node@4.3.0': - resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} - - '@tailwindcss/oxide-android-arm64@4.3.0': - resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.3.0': - resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.3.0': - resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.3.0': - resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': - resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': - resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': - resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': - resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.3.0': - resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.3.0': - resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': - resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': - resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.3.0': - resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} - engines: {node: '>= 20'} - - '@tailwindcss/vite@4.3.0': - resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 || ^8 - - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - - '@types/debug@4.1.13': - resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} - - '@types/estree-jsx@1.0.5': - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - '@types/unist@2.0.11': - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@ungap/structured-clone@1.3.1': - resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - - '@vitejs/plugin-react@6.0.1': - resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - - character-reference-invalid@2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decode-named-character-reference@1.3.0: - resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - - enhanced-resolve@5.21.2: - resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==} - engines: {node: '>=10.13.0'} - - estree-util-is-identifier-name@3.0.0: - resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-to-jsx-runtime@2.3.6: - resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - html-url-attributes@3.0.1: - resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - inline-style-parser@0.2.7: - resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - - is-alphabetical@2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - - is-alphanumerical@2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - - is-decimal@2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - - is-hexadecimal@2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - mdast-util-from-markdown@2.0.3: - resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} - - mdast-util-mdx-expression@2.0.1: - resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} - - mdast-util-mdx-jsx@3.2.0: - resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} - - mdast-util-mdxjs-esm@2.0.1: - resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - - mdast-util-to-hast@13.2.1: - resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - - mdast-util-to-markdown@2.1.2: - resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - - micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - - micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - - micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - - micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - - micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - - micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - - micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - - micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - - micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - - micromark-util-decode-string@2.0.1: - resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - - micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - - micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - - micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - oniguruma-parser@0.12.2: - resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} - - oniguruma-to-es@4.3.6: - resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - - parse-entities@4.0.2: - resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - postcss@8.5.14: - resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} - engines: {node: ^10 || ^12 || >=14} - - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} - peerDependencies: - react: ^19.2.6 - - react-markdown@10.1.0: - resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} - peerDependencies: - '@types/react': '>=18' - react: '>=18' - - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} - engines: {node: '>=0.10.0'} - - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-rehype@11.1.2: - resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - - rolldown@1.0.0: - resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - shiki@4.0.2: - resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} - engines: {node: '>=20'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - - style-to-js@1.1.21: - resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} - - style-to-object@1.0.14: - resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - - tailwindcss@4.3.0: - resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} - - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} - engines: {node: '>=6'} - - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - - unist-util-is@6.0.1: - resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.2: - resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - - unist-util-visit@5.1.0: - resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - - vfile-message@4.0.3: - resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - vite@8.0.12: - resolution: {integrity: sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -snapshots: - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - - '@oxc-project/types@0.129.0': {} - - '@rolldown/binding-android-arm64@1.0.0': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.0': - optional: true - - '@rolldown/binding-darwin-x64@1.0.0': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.0': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.0': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.0': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.0': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.0': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.0': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.0': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.0': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.0': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.0': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.0': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.0': - optional: true - - '@rolldown/pluginutils@1.0.0': {} - - '@rolldown/pluginutils@1.0.0-rc.7': {} - - '@shikijs/core@4.0.2': - dependencies: - '@shikijs/primitive': 4.0.2 - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - - '@shikijs/engine-oniguruma@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - - '@shikijs/primitive@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/themes@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - - '@shikijs/types@4.0.2': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - - '@tailwindcss/node@4.3.0': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.21.2 - jiti: 2.7.0 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.3.0 - - '@tailwindcss/oxide-android-arm64@4.3.0': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.3.0': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.3.0': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.3.0': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.3.0': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.3.0': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': - optional: true - - '@tailwindcss/oxide@4.3.0': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-x64': 4.3.0 - '@tailwindcss/oxide-freebsd-x64': 4.3.0 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-x64-musl': 4.3.0 - '@tailwindcss/oxide-wasm32-wasi': 4.3.0 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - - '@tailwindcss/vite@4.3.0(vite@8.0.12(jiti@2.7.0))': - dependencies: - '@tailwindcss/node': 4.3.0 - '@tailwindcss/oxide': 4.3.0 - tailwindcss: 4.3.0 - vite: 8.0.12(jiti@2.7.0) - - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/debug@4.1.13': - dependencies: - '@types/ms': 2.1.0 - - '@types/estree-jsx@1.0.5': - dependencies: - '@types/estree': 1.0.9 - - '@types/estree@1.0.9': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/ms@2.1.0': {} - - '@types/react-dom@19.2.3(@types/react@19.2.14)': - dependencies: - '@types/react': 19.2.14 - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - '@types/unist@2.0.11': {} - - '@types/unist@3.0.3': {} - - '@ungap/structured-clone@1.3.1': {} - - '@vitejs/plugin-react@6.0.1(vite@8.0.12(jiti@2.7.0))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.12(jiti@2.7.0) - - bail@2.0.2: {} - - ccount@2.0.1: {} - - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - character-entities@2.0.2: {} - - character-reference-invalid@2.0.1: {} - - comma-separated-tokens@2.0.3: {} - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decode-named-character-reference@1.3.0: - dependencies: - character-entities: 2.0.2 - - dequal@2.0.3: {} - - detect-libc@2.1.2: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - enhanced-resolve@5.21.2: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - estree-util-is-identifier-name@3.0.0: {} - - extend@3.0.2: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - fsevents@2.3.3: - optional: true - - graceful-fs@4.2.11: {} - - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-to-jsx-runtime@2.3.6: - dependencies: - '@types/estree': 1.0.9 - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.21 - unist-util-position: 5.0.0 - vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - html-url-attributes@3.0.1: {} - - html-void-elements@3.0.0: {} - - inline-style-parser@0.2.7: {} - - is-alphabetical@2.0.1: {} - - is-alphanumerical@2.0.1: - dependencies: - is-alphabetical: 2.0.1 - is-decimal: 2.0.1 - - is-decimal@2.0.1: {} - - is-hexadecimal@2.0.1: {} - - is-plain-obj@4.1.0: {} - - jiti@2.7.0: {} - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - longest-streak@3.1.0: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - mdast-util-from-markdown@2.0.3: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-decode-string: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-expression@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-mdx-jsx@3.2.0: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - parse-entities: 4.0.2 - stringify-entities: 4.0.4 - unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.3 - transitivePeerDependencies: - - supports-color - - mdast-util-mdxjs-esm@2.0.1: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.1 - - mdast-util-to-hast@13.2.1: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.1 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - - mdast-util-to-markdown@2.1.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-classify-character: 2.0.1 - micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.1.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - - micromark-core-commonmark@2.0.3: - dependencies: - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-factory-destination: 2.0.1 - micromark-factory-label: 2.0.1 - micromark-factory-space: 2.0.1 - micromark-factory-title: 2.0.1 - micromark-factory-whitespace: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-html-tag-name: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-destination@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-label@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-space@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-types: 2.0.2 - - micromark-factory-title@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-whitespace@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-chunked@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-classify-character@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-combine-extensions@2.0.1: - dependencies: - micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-decode-numeric-character-reference@2.0.2: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-decode-string@2.0.1: - dependencies: - decode-named-character-reference: 1.3.0 - micromark-util-character: 2.1.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-symbol: 2.0.1 - - micromark-util-encode@2.0.1: {} - - micromark-util-html-tag-name@2.0.1: {} - - micromark-util-normalize-identifier@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-resolve-all@2.0.1: - dependencies: - micromark-util-types: 2.0.2 - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-subtokenize@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - - micromark@4.0.2: - dependencies: - '@types/debug': 4.1.13 - debug: 4.4.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-combine-extensions: 2.0.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-encode: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color - - ms@2.1.3: {} - - nanoid@3.3.12: {} - - oniguruma-parser@0.12.2: {} - - oniguruma-to-es@4.3.6: - dependencies: - oniguruma-parser: 0.12.2 - regex: 6.1.0 - regex-recursion: 6.0.2 - - parse-entities@4.0.2: - dependencies: - '@types/unist': 2.0.11 - character-entities-legacy: 3.0.0 - character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.3.0 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - is-hexadecimal: 2.0.1 - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - postcss@8.5.14: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - property-information@7.1.0: {} - - react-dom@19.2.6(react@19.2.6): - dependencies: - react: 19.2.6 - scheduler: 0.27.0 - - react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.6): - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@types/react': 19.2.14 - devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.6 - html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.1 - react: 19.2.6 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - unified: 11.0.5 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - - react@19.2.6: {} - - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - - remark-parse@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - micromark-util-types: 2.0.2 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-rehype@11.1.2: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.1 - unified: 11.0.5 - vfile: 6.0.3 - - rolldown@1.0.0: - dependencies: - '@oxc-project/types': 0.129.0 - '@rolldown/pluginutils': 1.0.0 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0 - '@rolldown/binding-darwin-arm64': 1.0.0 - '@rolldown/binding-darwin-x64': 1.0.0 - '@rolldown/binding-freebsd-x64': 1.0.0 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0 - '@rolldown/binding-linux-arm64-gnu': 1.0.0 - '@rolldown/binding-linux-arm64-musl': 1.0.0 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0 - '@rolldown/binding-linux-s390x-gnu': 1.0.0 - '@rolldown/binding-linux-x64-gnu': 1.0.0 - '@rolldown/binding-linux-x64-musl': 1.0.0 - '@rolldown/binding-openharmony-arm64': 1.0.0 - '@rolldown/binding-wasm32-wasi': 1.0.0 - '@rolldown/binding-win32-arm64-msvc': 1.0.0 - '@rolldown/binding-win32-x64-msvc': 1.0.0 - - scheduler@0.27.0: {} - - shiki@4.0.2: - dependencies: - '@shikijs/core': 4.0.2 - '@shikijs/engine-javascript': 4.0.2 - '@shikijs/engine-oniguruma': 4.0.2 - '@shikijs/langs': 4.0.2 - '@shikijs/themes': 4.0.2 - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - source-map-js@1.2.1: {} - - space-separated-tokens@2.0.2: {} - - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - - style-to-js@1.1.21: - dependencies: - style-to-object: 1.0.14 - - style-to-object@1.0.14: - dependencies: - inline-style-parser: 0.2.7 - - tailwindcss@4.3.0: {} - - tapable@2.3.3: {} - - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - trim-lines@3.0.1: {} - - trough@2.2.0: {} - - tslib@2.8.1: - optional: true - - typescript@6.0.3: {} - - unified@11.0.5: - dependencies: - '@types/unist': 3.0.3 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.3 - - unist-util-is@6.0.1: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-visit@5.1.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - vfile-message@4.0.3: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.3 - - vite@8.0.12(jiti@2.7.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.14 - rolldown: 1.0.0 - tinyglobby: 0.2.16 - optionalDependencies: - fsevents: 2.3.3 - jiti: 2.7.0 - - zwitch@2.0.4: {} diff --git a/legacy-packages/workflow-dashboard/src/api.ts b/legacy-packages/workflow-dashboard/src/api.ts deleted file mode 100644 index ea00bcf..0000000 --- a/legacy-packages/workflow-dashboard/src/api.ts +++ /dev/null @@ -1,202 +0,0 @@ -const GATEWAY_URL = import.meta.env.VITE_GATEWAY_URL || ""; - -export function getApiKey(): string | null { - try { - return localStorage.getItem("workflow-api-key"); - } catch { - return null; - } -} - -export function setApiKey(key: string): void { - localStorage.setItem("workflow-api-key", key); -} - -export function clearApiKey(): void { - localStorage.removeItem("workflow-api-key"); -} - -export function hasApiKey(): boolean { - return getApiKey() !== null && getApiKey() !== ""; -} - -function authHeaders(): Record { - const key = getApiKey(); - if (key) return { Authorization: `Bearer ${key}` }; - return {}; -} - -function clientBase(client: string): string { - if (GATEWAY_URL) { - return `${GATEWAY_URL}/api/clients/${client}`; - } - // Local dev: proxy via vite, no client prefix - return "/api"; -} - -async function postJson(base: string, path: string, body: unknown): Promise { - const res = await fetch(`${base}${path}`, { - method: "POST", - headers: { "Content-Type": "application/json", ...authHeaders() }, - body: JSON.stringify(body), - }); - if (!res.ok) { - const err = (await res.json().catch(() => ({ error: res.statusText }))) as { error: string }; - throw new Error(err.error || `API ${res.status}`); - } - return res.json() as Promise; -} - -async function fetchJson(base: string, path: string): Promise { - const res = await fetch(`${base}${path}`, { headers: authHeaders() }); - if (!res.ok) { - throw new Error(`API ${res.status}: ${path}`); - } - return res.json() as Promise; -} - -// ── Endpoint types ────────────────────────────────────────────────── - -export type ClientEndpoint = { - name: string; - url: string; - status: string; - lastHeartbeat: number; -}; - -export type WorkflowSummary = { - name: string; - hash: string | null; - timestamp: number | null; -}; - -export type WorkflowHistoryEntry = { - hash: string; - timestamp: number; -}; - -export type ThreadSummary = { - threadId: string; - workflow: string | null; - hash: string | null; - startedAt: string | null; - status: string | null; -}; - -export type ThreadStartRecord = { - type: "thread-start"; - workflow: string; - prompt: string | null; - threadId: string; - status: string; - timestamp: null; -}; - -export type RoleRecord = { - type: "role"; - role: string; - content: string; - timestamp: number | null; - meta: Record; -}; - -export type WorkflowResultRecord = { - type: "workflow-result"; - returnCode: number; - content: string; - timestamp: number | null; -}; - -export type ThreadRecord = ThreadStartRecord | RoleRecord | WorkflowResultRecord; - -export type WorkflowGraphEdge = { - from: string; - to: string; - condition: string; - conditionDescription: string | null; -}; - -export type WorkflowGraph = { - edges: readonly WorkflowGraphEdge[]; -}; - -export type WorkflowRoleDescriptor = { - description: string; - systemPrompt: string; - schema: Record; -}; - -export type WorkflowDescriptor = { - description: string; - roles: Record; - graph: WorkflowGraph; -}; - -export type WorkflowDetail = { - name: string; - hash: string; - timestamp: number; - history: readonly WorkflowHistoryEntry[]; - descriptor: WorkflowDescriptor | null; -}; - -// ── Gateway endpoints ─────────────────────────────────────────────── - -export function listClients(): Promise { - const url = GATEWAY_URL || ""; - return fetchJson(url, "/api/gateway/endpoints"); -} - -// ── Client-scoped endpoints ────────────────────────────────────────── - -export function listWorkflows(client: string): Promise<{ workflows: WorkflowSummary[] }> { - return fetchJson(clientBase(client), "/workflows"); -} - -export async function getWorkflowDetail(client: string, name: string): Promise { - return fetchJson(clientBase(client), `/workflows/${encodeURIComponent(name)}`); -} - -export async function getWorkflowDescriptor( - client: string, - name: string, -): Promise { - const res = await getWorkflowDetail(client, name); - return res.descriptor; -} - -export function listThreads(client: string): Promise<{ threads: ThreadSummary[] }> { - return fetchJson(clientBase(client), "/threads"); -} - -export function listRunningThreads(client: string): Promise<{ threads: ThreadSummary[] }> { - return fetchJson(clientBase(client), "/threads/running"); -} - -export function getThread(client: string, id: string): Promise<{ records: ThreadRecord[] }> { - return fetchJson(clientBase(client), `/threads/${id}`); -} - -export function runThread( - client: string, - workflow: string, - prompt: string, -): Promise<{ threadId: string }> { - return postJson(clientBase(client), "/threads", { workflow, prompt }); -} - -export function killThread(client: string, threadId: string): Promise<{ ok: boolean }> { - return postJson(clientBase(client), `/threads/${threadId}/kill`, {}); -} - -export function pauseThread(client: string, threadId: string): Promise<{ ok: boolean }> { - return postJson(clientBase(client), `/threads/${threadId}/pause`, {}); -} - -export function resumeThread(client: string, threadId: string): Promise<{ ok: boolean }> { - return postJson(clientBase(client), `/threads/${threadId}/resume`, {}); -} - -export function getClientHealth(client: string): Promise<{ ok: boolean }> { - return fetchJson(clientBase(client), "/healthz"); -} diff --git a/legacy-packages/workflow-dashboard/src/app.tsx b/legacy-packages/workflow-dashboard/src/app.tsx deleted file mode 100644 index 3ace0e7..0000000 --- a/legacy-packages/workflow-dashboard/src/app.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useState } from "react"; -import { Navigate, Outlet, useParams } from "react-router"; -import { clearApiKey, hasApiKey } from "./api.ts"; -import { RunDialog } from "./components/run-dialog.tsx"; -import { Sidebar } from "./components/sidebar.tsx"; -import { StatusBar } from "./components/status-bar.tsx"; -import { useTheme } from "./hooks/use-theme.tsx"; - -export function Layout() { - const [authed, setAuthed] = useState(hasApiKey()); - const { client } = useParams(); - const [showRun, setShowRun] = useState(false); - const { theme, toggleTheme } = useTheme(); - - if (!authed) { - return ; - } - - return ( -
- { - clearApiKey(); - setAuthed(false); - }} - theme={theme} - onToggleTheme={toggleTheme} - /> -
- setShowRun(true)} /> -
- -
-
- {client && } -
- ); -} diff --git a/legacy-packages/workflow-dashboard/src/components/client-redirect.tsx b/legacy-packages/workflow-dashboard/src/components/client-redirect.tsx deleted file mode 100644 index 6381e16..0000000 --- a/legacy-packages/workflow-dashboard/src/components/client-redirect.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Loader2, Users } from "lucide-react"; -import { Navigate } from "react-router"; -import { listClients } from "../api.ts"; -import { useFetch } from "../hooks.ts"; - -export function ClientRedirect() { - const { status, data } = useFetch(() => listClients(), []); - - if (status === "loading") { - return ( -
- -

Loading clients...

-
- ); - } - - if (status === "ok" && data.length > 0) { - return ; - } - - return ( -
- -

No client selected

-

- Select a client from the sidebar to get started. -

-
- ); -} diff --git a/legacy-packages/workflow-dashboard/src/components/login.tsx b/legacy-packages/workflow-dashboard/src/components/login.tsx deleted file mode 100644 index 7e6eeb9..0000000 --- a/legacy-packages/workflow-dashboard/src/components/login.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { AlertCircle, Loader2, Moon, Settings, Sun } from "lucide-react"; -import { useState } from "react"; -import { useNavigate } from "react-router"; -import { setApiKey } from "../api.ts"; -import { useTheme } from "../hooks/use-theme.tsx"; -import { Button } from "./ui/button.tsx"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card.tsx"; -import { Input } from "./ui/input.tsx"; - -export function LoginPage() { - const navigate = useNavigate(); - const [key, setKey] = useState(""); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - const { theme, toggleTheme } = useTheme(); - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!key.trim()) return; - - setLoading(true); - setError(null); - - const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || ""; - try { - const res = await fetch(`${gatewayUrl}/api/gateway/endpoints`, { - headers: { Authorization: `Bearer ${key.trim()}` }, - }); - if (res.status === 401) { - setError("Invalid API key"); - setLoading(false); - return; - } - if (!res.ok) { - setError(`Server error: ${res.status}`); - setLoading(false); - return; - } - } catch (err) { - setError(`Connection failed: ${err instanceof Error ? err.message : String(err)}`); - setLoading(false); - return; - } - - setApiKey(key.trim()); - navigate("/", { replace: true }); - } - - return ( -
- - - - - - Workflow Dashboard - - Enter your API key to continue - - -
- setKey(e.target.value)} - placeholder="API Key" - className="transition-all duration-200" - /> - {error && ( -

- - {error} -

- )} - -
-
-
-
- ); -} diff --git a/legacy-packages/workflow-dashboard/src/components/markdown.tsx b/legacy-packages/workflow-dashboard/src/components/markdown.tsx deleted file mode 100644 index 4407e90..0000000 --- a/legacy-packages/workflow-dashboard/src/components/markdown.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useEffect, useState } from "react"; -import ReactMarkdown from "react-markdown"; -import { - type BundledLanguage, - type BundledTheme, - createHighlighter, - type HighlighterGeneric, -} from "shiki"; - -let highlighterPromise: Promise> | null = null; - -const LANGS: BundledLanguage[] = [ - "typescript", - "javascript", - "json", - "yaml", - "bash", - "python", - "markdown", -]; - -function getHighlighter(): Promise> { - if (highlighterPromise === null) { - highlighterPromise = createHighlighter({ - themes: ["github-dark"], - langs: LANGS, - }); - } - return highlighterPromise; -} - -function CodeBlock({ className, children }: { className?: string; children?: React.ReactNode }) { - const [html, setHtml] = useState(null); - const code = String(children).replace(/\n$/, ""); - const lang = className?.replace("language-", "") ?? "text"; - - useEffect(() => { - let cancelled = false; - getHighlighter().then((hl) => { - if (cancelled) return; - try { - const result = hl.codeToHtml(code, { lang, theme: "github-dark" }); - setHtml(result); - } catch { - setHtml(null); - } - }); - return () => { - cancelled = true; - }; - }, [code, lang]); - - if (html !== null) { - return ( -
- {lang !== "text" && ( - - {lang} - - )} -
-
- ); - } - - return ( -
-      {code}
-    
- ); -} - -export function Markdown({ content }: { content: string }) { - return ( -
- - {children} - - ); - } - return {children}; - }, - p({ children }) { - return

{children}

; - }, - ul({ children }) { - return
    {children}
; - }, - ol({ children }) { - return
    {children}
; - }, - h1({ children }) { - return ( -

- {children} -

- ); - }, - h2({ children }) { - return ( -

- {children} -

- ); - }, - h3({ children }) { - return

{children}

; - }, - blockquote({ children }) { - return ( -
- {children} -
- ); - }, - }} - > - {content} -
-
- ); -} diff --git a/legacy-packages/workflow-dashboard/src/components/record-card.tsx b/legacy-packages/workflow-dashboard/src/components/record-card.tsx deleted file mode 100644 index 1c9e007..0000000 --- a/legacy-packages/workflow-dashboard/src/components/record-card.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { CheckCircle2, Clock, MessageSquare, Rocket, User, XCircle } from "lucide-react"; -import type { RoleRecord, ThreadRecord, ThreadStartRecord, WorkflowResultRecord } from "../api.ts"; -import { cn } from "../lib/utils.ts"; -import { Markdown } from "./markdown.tsx"; -import { Badge } from "./ui/badge.tsx"; -import { Card } from "./ui/card.tsx"; - -const ROLE_HUES = [262, 210, 35, 150, 330, 180, 15, 280, 55, 195, 345, 120, 240, 75, 305]; - -function roleHue(role: string): number { - let hash = 0; - for (let i = 0; i < role.length; i++) { - hash = (hash * 31 + role.charCodeAt(i)) | 0; - } - return ROLE_HUES[Math.abs(hash) % ROLE_HUES.length]; -} - -function roleBadgeStyle(role: string): { backgroundColor: string; borderColor: string } { - const hue = roleHue(role); - return { - backgroundColor: `oklch(0.58 0.12 ${hue} / 0.85)`, - borderColor: `oklch(0.58 0.12 ${hue} / 0.25)`, - }; -} - -function formatTime(ts: number | null): string | null { - if (ts === null) return null; - return new Date(ts).toLocaleTimeString(); -} - -function StartCard({ record }: { record: ThreadStartRecord }) { - return ( - -
-
- - {record.workflow} - - {record.status} - -
- {record.prompt !== null && ( -
-
- - Prompt -
- -
- )} - - ); -} - -function RoleMessage({ record, highlighted }: { record: RoleRecord; highlighted: boolean }) { - const style = roleBadgeStyle(record.role); - return ( - -
- - - {record.role} - - {formatTime(record.timestamp) !== null && ( - - - {formatTime(record.timestamp)} - - )} -
- -
- ); -} - -function ResultCard({ record }: { record: WorkflowResultRecord }) { - const success = record.returnCode === 0; - return ( - -
- {success ? ( - - ) : ( - - )} - {success ? "Completed" : "Failed"} - - exit {record.returnCode} - - {formatTime(record.timestamp) !== null && ( - - - {formatTime(record.timestamp)} - - )} -
- -
- ); -} - -type RecordCardProps = { - record: ThreadRecord; - highlighted: boolean; -}; - -export function RecordCard({ record, highlighted }: RecordCardProps) { - switch (record.type) { - case "thread-start": - return ; - case "role": - return ; - case "workflow-result": - return ; - } -} diff --git a/legacy-packages/workflow-dashboard/src/components/run-dialog.tsx b/legacy-packages/workflow-dashboard/src/components/run-dialog.tsx deleted file mode 100644 index 6d94050..0000000 --- a/legacy-packages/workflow-dashboard/src/components/run-dialog.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useState } from "react"; -import { useNavigate } from "react-router"; -import { listWorkflows, runThread } from "../api.ts"; -import { useFetch } from "../hooks.ts"; -import { Button } from "./ui/button.tsx"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "./ui/dialog.tsx"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select.tsx"; -import { Textarea } from "./ui/textarea.tsx"; - -type Props = { - client: string; - open: boolean; - onOpenChange: (open: boolean) => void; -}; - -export function RunDialog({ client, open, onOpenChange }: Props) { - const navigate = useNavigate(); - const workflows = useFetch(() => listWorkflows(client), [client]); - const [workflow, setWorkflow] = useState(""); - const [prompt, setPrompt] = useState(""); - const [submitting, setSubmitting] = useState(false); - const [error, setError] = useState(null); - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!workflow || !prompt) return; - setSubmitting(true); - setError(null); - try { - const result = await runThread(client, workflow, prompt); - onOpenChange(false); - navigate(`/${client}/threads/${result.threadId}`); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - setSubmitting(false); - } - } - - return ( - - - - Run Thread - Start a new thread on {client} - -
-
- - -
-
- -