Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1a0a135d4 | |||
| 34e00bebdf | |||
| 33cf23ed01 |
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@uncaged/json-cas": "file:../json-cas/packages/json-cas",
|
"@uncaged/json-cas": "^0.1.0",
|
||||||
"@uncaged/json-cas-workflow": "file:../json-cas/packages/json-cas-workflow"
|
"@uncaged/json-cas-workflow": "^0.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bunx tsc --build",
|
"build": "bunx tsc --build",
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import {
|
||||||
|
cmdJsonCasInit,
|
||||||
|
cmdNodeGet,
|
||||||
|
cmdNodeList,
|
||||||
|
cmdNodeWalk,
|
||||||
|
cmdWorkflowRegister,
|
||||||
|
cmdWorkflowShow,
|
||||||
|
formatNodeWalk,
|
||||||
|
formatWorkflowShow,
|
||||||
|
getJsonCasDir,
|
||||||
|
} from "../src/commands/json-cas/index.js";
|
||||||
|
|
||||||
|
const SIMPLE_WORKFLOW = {
|
||||||
|
name: "test-workflow",
|
||||||
|
description: "A test workflow for CLI tests",
|
||||||
|
roles: {
|
||||||
|
analyst: {
|
||||||
|
description: "Analyses the input",
|
||||||
|
systemPrompt: "You are an analyst.",
|
||||||
|
extractPrompt: "Extract the analysis.",
|
||||||
|
schema: { type: "object", properties: { result: { type: "string" } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
moderator: [{ from: "analyst", to: "__end__", when: null }],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("json-cas CLI commands", () => {
|
||||||
|
let storageRoot: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
storageRoot = await mkdtemp(join(tmpdir(), "uncaged-json-cas-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(storageRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getJsonCasDir returns path under storageRoot", () => {
|
||||||
|
const dir = getJsonCasDir(storageRoot);
|
||||||
|
expect(dir).toBe(join(storageRoot, "json-cas"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("init bootstraps the store and returns a workflow type hash", async () => {
|
||||||
|
const workflowTypeHash = await cmdJsonCasInit(storageRoot);
|
||||||
|
expect(typeof workflowTypeHash).toBe("string");
|
||||||
|
expect(workflowTypeHash.length).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("init is idempotent", async () => {
|
||||||
|
const hash1 = await cmdJsonCasInit(storageRoot);
|
||||||
|
const hash2 = await cmdJsonCasInit(storageRoot);
|
||||||
|
expect(hash1).toBe(hash2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("workflow register returns a hash", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
const result = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
expect(typeof result.hash).toBe("string");
|
||||||
|
expect(result.hash.length).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("workflow register is idempotent", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
const r1 = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
const r2 = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
expect(r1.hash).toBe(r2.hash);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("workflow show loads a registered workflow", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
const { hash } = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
const wf = await cmdWorkflowShow(storageRoot, hash);
|
||||||
|
|
||||||
|
expect(wf).not.toBeNull();
|
||||||
|
if (wf === null) return;
|
||||||
|
|
||||||
|
expect(wf.name).toBe("test-workflow");
|
||||||
|
expect(wf.description).toBe("A test workflow for CLI tests");
|
||||||
|
expect(Object.keys(wf.roles)).toContain("analyst");
|
||||||
|
expect(wf.roles.analyst.systemPrompt).toBe("You are an analyst.");
|
||||||
|
expect(wf.moderator).toHaveLength(1);
|
||||||
|
expect(wf.moderator[0].from).toBe("analyst");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("workflow show returns null for unknown hash", async () => {
|
||||||
|
await cmdJsonCasInit(storageRoot);
|
||||||
|
const result = await cmdWorkflowShow(storageRoot, "AAAAAAAAAAAAA");
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatWorkflowShow produces expected output", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
const { hash } = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
const wf = await cmdWorkflowShow(storageRoot, hash);
|
||||||
|
if (wf === null) throw new Error("workflow not found");
|
||||||
|
|
||||||
|
const output = formatWorkflowShow(hash, wf);
|
||||||
|
expect(output).toContain("test-workflow");
|
||||||
|
expect(output).toContain(hash);
|
||||||
|
expect(output).toContain("analyst");
|
||||||
|
expect(output).toContain("moderator:");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node list returns hashes after registration", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
const hashes = await cmdNodeList(storageRoot);
|
||||||
|
|
||||||
|
expect(hashes.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node list returns empty array for empty store", async () => {
|
||||||
|
const hashes = await cmdNodeList(storageRoot);
|
||||||
|
expect(hashes).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node get returns JSON for a known hash", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
const { hash } = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
const json = await cmdNodeGet(storageRoot, hash);
|
||||||
|
|
||||||
|
expect(json).not.toBeNull();
|
||||||
|
if (json === null) return;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(json) as unknown;
|
||||||
|
expect(parsed).toMatchObject({ payload: expect.anything() });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node get returns null for unknown hash", async () => {
|
||||||
|
const result = await cmdNodeGet(storageRoot, "AAAAAAAAAAAAA");
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node walk traverses the workflow DAG", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
const { hash } = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
const entries = await cmdNodeWalk(storageRoot, hash);
|
||||||
|
|
||||||
|
expect(entries).not.toBeNull();
|
||||||
|
if (entries === null) return;
|
||||||
|
|
||||||
|
// should include at least the workflow node, role node, and role-schema node
|
||||||
|
expect(entries.length).toBeGreaterThanOrEqual(3);
|
||||||
|
const hashes = entries.map((e) => e.hash);
|
||||||
|
expect(hashes).toContain(hash);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("node walk returns null for unknown root", async () => {
|
||||||
|
const result = await cmdNodeWalk(storageRoot, "AAAAAAAAAAAAA");
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatNodeWalk produces output with node hashes", async () => {
|
||||||
|
const filePath = join(storageRoot, "wf.json");
|
||||||
|
await writeFile(filePath, JSON.stringify(SIMPLE_WORKFLOW), "utf-8");
|
||||||
|
|
||||||
|
const { hash } = await cmdWorkflowRegister(storageRoot, filePath);
|
||||||
|
const entries = await cmdNodeWalk(storageRoot, hash);
|
||||||
|
if (entries === null) throw new Error("walk failed");
|
||||||
|
|
||||||
|
const output = formatNodeWalk(hash, entries);
|
||||||
|
expect(output).toContain(`walk from: ${hash}`);
|
||||||
|
expect(output).toContain(hash);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,13 +11,17 @@
|
|||||||
"uncaged-workflow": "src/cli.ts"
|
"uncaged-workflow": "src/cli.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/workflow-gateway": "workspace:^",
|
"@uncaged/json-cas": "^0.1.1",
|
||||||
"@uncaged/workflow-protocol": "workspace:^",
|
"@uncaged/json-cas-fs": "^0.1.1",
|
||||||
"@uncaged/workflow-util": "workspace:^",
|
"@uncaged/json-cas-workflow": "^0.1.1",
|
||||||
"@uncaged/workflow-cas": "workspace:^",
|
"@uncaged/workflow-cas": "workspace:^",
|
||||||
"@uncaged/workflow-execute": "workspace:^",
|
"@uncaged/workflow-execute": "workspace:^",
|
||||||
|
"@uncaged/workflow-gateway": "workspace:^",
|
||||||
|
"@uncaged/workflow-json-def": "workspace:*",
|
||||||
|
"@uncaged/workflow-protocol": "workspace:^",
|
||||||
"@uncaged/workflow-register": "workspace:^",
|
"@uncaged/workflow-register": "workspace:^",
|
||||||
"@uncaged/workflow-runtime": "workspace:^",
|
"@uncaged/workflow-runtime": "workspace:^",
|
||||||
|
"@uncaged/workflow-util": "workspace:^",
|
||||||
"hono": "^4.12.18",
|
"hono": "^4.12.18",
|
||||||
"yaml": "^2.8.4"
|
"yaml": "^2.8.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js";
|
|||||||
import { createCasDispatcher } from "./commands/cas/index.js";
|
import { createCasDispatcher } from "./commands/cas/index.js";
|
||||||
import { dispatchConnect } from "./commands/connect/index.js";
|
import { dispatchConnect } from "./commands/connect/index.js";
|
||||||
import { createInitDispatcher } from "./commands/init/index.js";
|
import { createInitDispatcher } from "./commands/init/index.js";
|
||||||
|
import { createJsonCasDispatcher } from "./commands/json-cas/index.js";
|
||||||
import { dispatchSetup } from "./commands/setup/index.js";
|
import { dispatchSetup } from "./commands/setup/index.js";
|
||||||
import { createThreadDispatcher, dispatchLive, dispatchRun } from "./commands/thread/index.js";
|
import { createThreadDispatcher, dispatchLive, dispatchRun } from "./commands/thread/index.js";
|
||||||
import { createWorkflowDispatcher } from "./commands/workflow/index.js";
|
import { createWorkflowDispatcher } from "./commands/workflow/index.js";
|
||||||
@@ -43,6 +44,7 @@ const dispatchWorkflow = createWorkflowDispatcher({ dispatchGroup });
|
|||||||
const dispatchThread = createThreadDispatcher({ dispatchGroup });
|
const dispatchThread = createThreadDispatcher({ dispatchGroup });
|
||||||
const dispatchCas = createCasDispatcher({ dispatchGroup });
|
const dispatchCas = createCasDispatcher({ dispatchGroup });
|
||||||
const dispatchInit = createInitDispatcher({ dispatchGroup });
|
const dispatchInit = createInitDispatcher({ dispatchGroup });
|
||||||
|
const dispatchJsonCas = createJsonCasDispatcher({ dispatchGroup });
|
||||||
|
|
||||||
async function showSkillDocOrIndex(topic: string | undefined): Promise<number> {
|
async function showSkillDocOrIndex(topic: string | undefined): Promise<number> {
|
||||||
if (topic === undefined) {
|
if (topic === undefined) {
|
||||||
@@ -72,6 +74,7 @@ const COMMAND_TABLE: Record<string, DispatchFn> = {
|
|||||||
run: dispatchRun,
|
run: dispatchRun,
|
||||||
live: dispatchLive,
|
live: dispatchLive,
|
||||||
connect: dispatchConnect,
|
connect: dispatchConnect,
|
||||||
|
"json-cas": dispatchJsonCas,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { CommandGroup } from "./cli-command-types.js";
|
|||||||
import { setCommandGroupsForUsage } from "./cli-usage-context.js";
|
import { setCommandGroupsForUsage } from "./cli-usage-context.js";
|
||||||
import { CAS_SUBCOMMAND_TABLE } from "./commands/cas/index.js";
|
import { CAS_SUBCOMMAND_TABLE } from "./commands/cas/index.js";
|
||||||
import { INIT_SUBCOMMAND_TABLE } from "./commands/init/index.js";
|
import { INIT_SUBCOMMAND_TABLE } from "./commands/init/index.js";
|
||||||
|
import { JSON_CAS_SUBCOMMAND_TABLE } from "./commands/json-cas/index.js";
|
||||||
import { THREAD_SUBCOMMAND_TABLE } from "./commands/thread/index.js";
|
import { THREAD_SUBCOMMAND_TABLE } from "./commands/thread/index.js";
|
||||||
import { WORKFLOW_SUBCOMMAND_TABLE } from "./commands/workflow/index.js";
|
import { WORKFLOW_SUBCOMMAND_TABLE } from "./commands/workflow/index.js";
|
||||||
|
|
||||||
@@ -52,6 +53,14 @@ export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
|
|||||||
name: "setup",
|
name: "setup",
|
||||||
commands: [...SETUP_USAGE_COMMANDS],
|
commands: [...SETUP_USAGE_COMMANDS],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "json-cas",
|
||||||
|
commands: Object.entries(JSON_CAS_SUBCOMMAND_TABLE).map(([name, e]) => ({
|
||||||
|
name,
|
||||||
|
args: e.args,
|
||||||
|
description: e.description,
|
||||||
|
})),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const USAGE_SECTION_BY_GROUP: Record<string, string> = {
|
|||||||
cas: "Content-addressable storage:",
|
cas: "Content-addressable storage:",
|
||||||
init: "Development:",
|
init: "Development:",
|
||||||
setup: "Configuration:",
|
setup: "Configuration:",
|
||||||
|
"json-cas": "JSON-CAS engine:",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatUsageCommandLines(
|
export function formatUsageCommandLines(
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import type { CommandEntry } from "../../cli-command-types.js";
|
||||||
|
import { printCliError, printCliLine } from "../../cli-output.js";
|
||||||
|
import { cmdJsonCasInit } from "./init.js";
|
||||||
|
import { cmdNodeGet } from "./node-get.js";
|
||||||
|
import { cmdNodeList } from "./node-list.js";
|
||||||
|
import { cmdNodeWalk, formatNodeWalk } from "./node-walk.js";
|
||||||
|
import { getJsonCasDir } from "./store.js";
|
||||||
|
import { cmdThreadShow, formatThreadShow } from "./thread-show.js";
|
||||||
|
import type { JsonCasDispatchDeps } from "./types.js";
|
||||||
|
import { cmdWorkflowRegister } from "./workflow-register.js";
|
||||||
|
import { cmdWorkflowShow, formatWorkflowShow } from "./workflow-show.js";
|
||||||
|
|
||||||
|
// ── node subcommands ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function dispatchNodeGet(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const hash = argv[0];
|
||||||
|
if (hash === undefined || argv.length > 1) {
|
||||||
|
printCliError("error: json-cas node get requires <hash>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdNodeGet(storageRoot, hash);
|
||||||
|
if (result === null) {
|
||||||
|
printCliError(`error: node not found: ${hash}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(result);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchNodeList(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
if (argv.length > 0) {
|
||||||
|
printCliError("error: json-cas node list takes no arguments");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const hashes = await cmdNodeList(storageRoot);
|
||||||
|
if (hashes.length === 0) {
|
||||||
|
printCliLine("(no nodes)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (const hash of hashes) {
|
||||||
|
printCliLine(hash);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchNodeWalk(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const hash = argv[0];
|
||||||
|
if (hash === undefined || argv.length > 1) {
|
||||||
|
printCliError("error: json-cas node walk requires <hash>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const entries = await cmdNodeWalk(storageRoot, hash);
|
||||||
|
if (entries === null) {
|
||||||
|
printCliError(`error: node not found: ${hash}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(formatNodeWalk(hash, entries));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JSON_CAS_NODE_TABLE: Record<string, CommandEntry> = {
|
||||||
|
get: { handler: dispatchNodeGet, args: "<hash>", description: "Get a CAS node as JSON" },
|
||||||
|
list: { handler: dispatchNodeList, args: "", description: "List all hashes in the store" },
|
||||||
|
walk: {
|
||||||
|
handler: dispatchNodeWalk,
|
||||||
|
args: "<hash>",
|
||||||
|
description: "Walk the DAG from a node, show referenced nodes",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── workflow subcommands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function dispatchWorkflowRegister(
|
||||||
|
storageRoot: string,
|
||||||
|
argv: string[],
|
||||||
|
): Promise<number> {
|
||||||
|
const file = argv[0];
|
||||||
|
if (file === undefined || argv.length > 1) {
|
||||||
|
printCliError("error: json-cas workflow register requires <file.json>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdWorkflowRegister(storageRoot, file);
|
||||||
|
printCliLine(`registered workflow: ${result.hash}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dispatchWorkflowShow(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const hash = argv[0];
|
||||||
|
if (hash === undefined || argv.length > 1) {
|
||||||
|
printCliError("error: json-cas workflow show requires <hash>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const wf = await cmdWorkflowShow(storageRoot, hash);
|
||||||
|
if (wf === null) {
|
||||||
|
printCliError(`error: workflow not found: ${hash}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(formatWorkflowShow(hash, wf));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JSON_CAS_WORKFLOW_TABLE: Record<string, CommandEntry> = {
|
||||||
|
register: {
|
||||||
|
handler: dispatchWorkflowRegister,
|
||||||
|
args: "<file.json>",
|
||||||
|
description: "Register a workflow definition from a JSON file",
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
handler: dispatchWorkflowShow,
|
||||||
|
args: "<hash>",
|
||||||
|
description: "Show a workflow by its CAS hash",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── thread subcommands ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function dispatchThreadShow(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const hash = argv[0];
|
||||||
|
if (hash === undefined || argv.length > 1) {
|
||||||
|
printCliError("error: json-cas thread show requires <start-hash>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const result = await cmdThreadShow(storageRoot, hash);
|
||||||
|
if (result === null) {
|
||||||
|
printCliError(`error: thread start node not found: ${hash}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printCliLine(formatThreadShow(result));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JSON_CAS_THREAD_TABLE: Record<string, CommandEntry> = {
|
||||||
|
show: {
|
||||||
|
handler: dispatchThreadShow,
|
||||||
|
args: "<start-hash>",
|
||||||
|
description: "Walk and display thread steps from a thread-start hash",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── top-level json-cas subcommands ───────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function dispatchJsonCasInit(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
if (argv.length > 0) {
|
||||||
|
printCliError("error: json-cas init takes no arguments");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const workflowTypeHash = await cmdJsonCasInit(storageRoot);
|
||||||
|
const dir = getJsonCasDir(storageRoot);
|
||||||
|
printCliLine(`initialized json-cas store at ${dir}`);
|
||||||
|
printCliLine(`workflow type hash: ${workflowTypeHash}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSubgroupHelp(
|
||||||
|
groupPath: string,
|
||||||
|
table: Record<string, CommandEntry>,
|
||||||
|
sub: string | undefined,
|
||||||
|
): number {
|
||||||
|
if (sub === undefined || sub === "--help" || sub === "-h") {
|
||||||
|
const lines = [`json-cas ${groupPath} subcommands:\n`];
|
||||||
|
for (const [name, e] of Object.entries(table)) {
|
||||||
|
const args = e.args ? ` ${e.args}` : "";
|
||||||
|
lines.push(` uncaged-workflow json-cas ${groupPath} ${name}${args}`);
|
||||||
|
lines.push(` ${e.description}\n`);
|
||||||
|
}
|
||||||
|
printCliLine(lines.join("\n"));
|
||||||
|
return sub === undefined ? 1 : 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispatchSubgroup(
|
||||||
|
groupPath: string,
|
||||||
|
table: Record<string, CommandEntry>,
|
||||||
|
storageRoot: string,
|
||||||
|
argv: string[],
|
||||||
|
): Promise<number> {
|
||||||
|
const sub = argv[0];
|
||||||
|
const helpResult = printSubgroupHelp(groupPath, table, sub);
|
||||||
|
if (helpResult >= 0) return helpResult;
|
||||||
|
|
||||||
|
const entry = table[sub as string];
|
||||||
|
if (entry === undefined) {
|
||||||
|
printCliError(`error: unknown json-cas ${groupPath} subcommand: ${sub}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return entry.handler(storageRoot, argv.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JSON_CAS_SUBCOMMAND_TABLE: Record<string, CommandEntry> = {
|
||||||
|
init: {
|
||||||
|
handler: dispatchJsonCasInit,
|
||||||
|
args: "",
|
||||||
|
description: "Initialize json-cas store and register workflow schemas",
|
||||||
|
},
|
||||||
|
workflow: {
|
||||||
|
handler: (storageRoot, argv) =>
|
||||||
|
dispatchSubgroup("workflow", JSON_CAS_WORKFLOW_TABLE, storageRoot, argv),
|
||||||
|
args: "<register|show>",
|
||||||
|
description: "Manage json-cas workflow definitions",
|
||||||
|
},
|
||||||
|
thread: {
|
||||||
|
handler: (storageRoot, argv) =>
|
||||||
|
dispatchSubgroup("thread", JSON_CAS_THREAD_TABLE, storageRoot, argv),
|
||||||
|
args: "<show>",
|
||||||
|
description: "Inspect json-cas thread execution records",
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
handler: (storageRoot, argv) =>
|
||||||
|
dispatchSubgroup("node", JSON_CAS_NODE_TABLE, storageRoot, argv),
|
||||||
|
args: "<get|list|walk>",
|
||||||
|
description: "Low-level access to json-cas store nodes",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createJsonCasDispatcher(deps: JsonCasDispatchDeps) {
|
||||||
|
const { dispatchGroup } = deps;
|
||||||
|
return async function dispatchJsonCas(storageRoot: string, argv: string[]): Promise<number> {
|
||||||
|
const result = dispatchGroup("json-cas", JSON_CAS_SUBCOMMAND_TABLE, storageRoot, argv);
|
||||||
|
if (result !== null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const sub = argv[0];
|
||||||
|
printCliError(`error: unknown json-cas subcommand: ${sub}`);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export {
|
||||||
|
createJsonCasDispatcher,
|
||||||
|
dispatchJsonCasInit,
|
||||||
|
dispatchNodeGet,
|
||||||
|
dispatchNodeList,
|
||||||
|
dispatchNodeWalk,
|
||||||
|
dispatchThreadShow,
|
||||||
|
dispatchWorkflowRegister,
|
||||||
|
dispatchWorkflowShow,
|
||||||
|
JSON_CAS_NODE_TABLE,
|
||||||
|
JSON_CAS_SUBCOMMAND_TABLE,
|
||||||
|
JSON_CAS_THREAD_TABLE,
|
||||||
|
JSON_CAS_WORKFLOW_TABLE,
|
||||||
|
} from "./dispatch.js";
|
||||||
|
export { cmdJsonCasInit } from "./init.js";
|
||||||
|
export { cmdNodeGet } from "./node-get.js";
|
||||||
|
export { cmdNodeList } from "./node-list.js";
|
||||||
|
export { cmdNodeWalk, formatNodeWalk } from "./node-walk.js";
|
||||||
|
export { getJsonCasDir, openStore } from "./store.js";
|
||||||
|
export type { ThreadShowResult, ThreadStep } from "./thread-show.js";
|
||||||
|
export { cmdThreadShow, formatThreadShow } from "./thread-show.js";
|
||||||
|
export type { JsonCasDispatchDeps } from "./types.js";
|
||||||
|
export type { WorkflowRegisterResult } from "./workflow-register.js";
|
||||||
|
export { cmdWorkflowRegister } from "./workflow-register.js";
|
||||||
|
export { cmdWorkflowShow, formatWorkflowShow } from "./workflow-show.js";
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { openStore } from "./store.js";
|
||||||
|
|
||||||
|
export async function cmdJsonCasInit(storageRoot: string): Promise<string> {
|
||||||
|
const { typeHashes } = await openStore(storageRoot);
|
||||||
|
return typeHashes.workflow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFsStore } from "@uncaged/json-cas-fs";
|
||||||
|
import { getJsonCasDir } from "./store.js";
|
||||||
|
|
||||||
|
export async function cmdNodeGet(storageRoot: string, hash: string): Promise<string | null> {
|
||||||
|
const store = createFsStore(getJsonCasDir(storageRoot));
|
||||||
|
const node = store.get(hash);
|
||||||
|
if (node === null) return null;
|
||||||
|
return JSON.stringify(node, null, 2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFsStore } from "@uncaged/json-cas-fs";
|
||||||
|
import { getJsonCasDir } from "./store.js";
|
||||||
|
|
||||||
|
export async function cmdNodeList(storageRoot: string): Promise<string[]> {
|
||||||
|
const store = createFsStore(getJsonCasDir(storageRoot));
|
||||||
|
return store.list();
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { CasNode, Hash } from "@uncaged/json-cas";
|
||||||
|
import { walk } from "@uncaged/json-cas";
|
||||||
|
import { createFsStore } from "@uncaged/json-cas-fs";
|
||||||
|
import { getJsonCasDir } from "./store.js";
|
||||||
|
|
||||||
|
export type WalkEntry = {
|
||||||
|
hash: Hash;
|
||||||
|
type: Hash;
|
||||||
|
timestamp: number;
|
||||||
|
payloadPreview: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function cmdNodeWalk(
|
||||||
|
storageRoot: string,
|
||||||
|
rootHash: string,
|
||||||
|
): Promise<WalkEntry[] | null> {
|
||||||
|
const store = createFsStore(getJsonCasDir(storageRoot));
|
||||||
|
|
||||||
|
if (!store.has(rootHash)) return null;
|
||||||
|
|
||||||
|
const entries: WalkEntry[] = [];
|
||||||
|
|
||||||
|
walk(store, rootHash, (hash: Hash, node: CasNode) => {
|
||||||
|
const preview = JSON.stringify(node.payload).slice(0, 100);
|
||||||
|
entries.push({
|
||||||
|
hash,
|
||||||
|
type: node.type,
|
||||||
|
timestamp: node.timestamp,
|
||||||
|
payloadPreview: preview,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatNodeWalk(rootHash: string, entries: WalkEntry[]): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`walk from: ${rootHash}`);
|
||||||
|
lines.push(`nodes: ${entries.length}`);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(` hash: ${entry.hash}`);
|
||||||
|
lines.push(` type: ${entry.type}`);
|
||||||
|
lines.push(` time: ${new Date(entry.timestamp).toISOString()}`);
|
||||||
|
lines.push(` data: ${entry.payloadPreview}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import type { Store } from "@uncaged/json-cas";
|
||||||
|
import { bootstrap } from "@uncaged/json-cas";
|
||||||
|
import { createFsStore } from "@uncaged/json-cas-fs";
|
||||||
|
import type { WorkflowSchemaHashes } from "@uncaged/json-cas-workflow";
|
||||||
|
import { registerWorkflowSchemas } from "@uncaged/json-cas-workflow";
|
||||||
|
|
||||||
|
export function getJsonCasDir(storageRoot: string): string {
|
||||||
|
return join(storageRoot, "json-cas");
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenStoreResult = {
|
||||||
|
store: Store;
|
||||||
|
typeHashes: WorkflowSchemaHashes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function openStore(storageRoot: string): Promise<OpenStoreResult> {
|
||||||
|
const dir = getJsonCasDir(storageRoot);
|
||||||
|
const store = createFsStore(dir);
|
||||||
|
await bootstrap(store);
|
||||||
|
const typeHashes = await registerWorkflowSchemas(store);
|
||||||
|
return { store, typeHashes };
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import type { Hash, Store } from "@uncaged/json-cas";
|
||||||
|
import type {
|
||||||
|
ContentPayload,
|
||||||
|
ThreadStepPayload,
|
||||||
|
WorkflowSchemaHashes,
|
||||||
|
} from "@uncaged/json-cas-workflow";
|
||||||
|
import { openStore } from "./store.js";
|
||||||
|
|
||||||
|
type StepEntry = {
|
||||||
|
hash: Hash;
|
||||||
|
payload: ThreadStepPayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
function collectStepsInOrder(
|
||||||
|
store: Store,
|
||||||
|
typeHashes: WorkflowSchemaHashes,
|
||||||
|
startHash: Hash,
|
||||||
|
): StepEntry[] {
|
||||||
|
const stepMap = new Map<Hash, StepEntry>();
|
||||||
|
|
||||||
|
for (const hash of store.list()) {
|
||||||
|
const node = store.get(hash);
|
||||||
|
if (node === null || node.type !== typeHashes.threadStep) continue;
|
||||||
|
const p = node.payload as ThreadStepPayload;
|
||||||
|
if (p.start === startHash) {
|
||||||
|
stepMap.set(hash, { hash, payload: p });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepMap.size === 0) return [];
|
||||||
|
|
||||||
|
// Find the terminal step: the one whose hash is not referenced as `previous` by any other step
|
||||||
|
const previousSet = new Set<Hash>();
|
||||||
|
for (const entry of stepMap.values()) {
|
||||||
|
if (entry.payload.previous !== null) {
|
||||||
|
previousSet.add(entry.payload.previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalEntries = [...stepMap.values()].filter((e) => !previousSet.has(e.hash));
|
||||||
|
if (terminalEntries.length === 0) return [...stepMap.values()];
|
||||||
|
|
||||||
|
// Walk backward from terminal to build ordered list
|
||||||
|
const ordered: StepEntry[] = [];
|
||||||
|
let current: StepEntry | undefined = terminalEntries[0];
|
||||||
|
while (current !== undefined) {
|
||||||
|
ordered.unshift(current);
|
||||||
|
const prevHash = current.payload.previous;
|
||||||
|
if (prevHash === null) break;
|
||||||
|
current = stepMap.get(prevHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentText(store: Store, contentHash: Hash): string {
|
||||||
|
const node = store.get(contentHash);
|
||||||
|
if (node === null) return "(not found)";
|
||||||
|
const payload = node.payload as ContentPayload;
|
||||||
|
return typeof payload.text === "string" ? payload.text : "(no text)";
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThreadStep = {
|
||||||
|
hash: Hash;
|
||||||
|
role: string;
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
contentPreview: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThreadShowResult = {
|
||||||
|
startHash: Hash;
|
||||||
|
steps: ThreadStep[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function cmdThreadShow(
|
||||||
|
storageRoot: string,
|
||||||
|
startHash: string,
|
||||||
|
): Promise<ThreadShowResult | null> {
|
||||||
|
const { store, typeHashes } = await openStore(storageRoot);
|
||||||
|
|
||||||
|
const startNode = store.get(startHash);
|
||||||
|
if (startNode === null) return null;
|
||||||
|
|
||||||
|
const entries = collectStepsInOrder(store, typeHashes, startHash);
|
||||||
|
|
||||||
|
const steps: ThreadStep[] = entries.map((entry) => {
|
||||||
|
const rawText = getContentText(store, entry.payload.content);
|
||||||
|
const preview = rawText.slice(0, 120).replace(/\n/g, " ");
|
||||||
|
return {
|
||||||
|
hash: entry.hash,
|
||||||
|
role: entry.payload.role,
|
||||||
|
meta: entry.payload.meta,
|
||||||
|
contentPreview: preview,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { startHash, steps };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatThreadShow(result: ThreadShowResult): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`thread: ${result.startHash}`);
|
||||||
|
lines.push(`steps: ${result.steps.length}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < result.steps.length; i++) {
|
||||||
|
const step = result.steps[i];
|
||||||
|
lines.push("");
|
||||||
|
lines.push(` [${i + 1}] ${step.role} (${step.hash})`);
|
||||||
|
if (Object.keys(step.meta).length > 0) {
|
||||||
|
lines.push(` meta: ${JSON.stringify(step.meta)}`);
|
||||||
|
}
|
||||||
|
lines.push(` > ${step.contentPreview}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { DispatchGroupFn } from "../../cli-command-types.js";
|
||||||
|
|
||||||
|
export type JsonCasDispatchDeps = {
|
||||||
|
dispatchGroup: DispatchGroupFn;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import type { WorkflowInput } from "@uncaged/workflow-json-def";
|
||||||
|
import { registerWorkflow } from "@uncaged/workflow-json-def";
|
||||||
|
import { openStore } from "./store.js";
|
||||||
|
|
||||||
|
export type WorkflowRegisterResult = {
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function cmdWorkflowRegister(
|
||||||
|
storageRoot: string,
|
||||||
|
filePath: string,
|
||||||
|
): Promise<WorkflowRegisterResult> {
|
||||||
|
const raw = readFileSync(filePath, "utf-8");
|
||||||
|
const workflowDef = JSON.parse(raw) as WorkflowInput;
|
||||||
|
|
||||||
|
const { store, typeHashes } = await openStore(storageRoot);
|
||||||
|
const hash = await registerWorkflow(store, typeHashes, workflowDef);
|
||||||
|
|
||||||
|
return { hash };
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { HydratedWorkflow } from "@uncaged/workflow-json-def";
|
||||||
|
import { loadWorkflow } from "@uncaged/workflow-json-def";
|
||||||
|
import { openStore } from "./store.js";
|
||||||
|
|
||||||
|
export async function cmdWorkflowShow(
|
||||||
|
storageRoot: string,
|
||||||
|
hash: string,
|
||||||
|
): Promise<HydratedWorkflow | null> {
|
||||||
|
const { store, typeHashes } = await openStore(storageRoot);
|
||||||
|
return loadWorkflow(store, typeHashes, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWorkflowShow(hash: string, wf: HydratedWorkflow): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(`workflow: ${wf.name}`);
|
||||||
|
lines.push(`hash: ${hash}`);
|
||||||
|
lines.push(`desc: ${wf.description}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
lines.push("roles:");
|
||||||
|
for (const [name, role] of Object.entries(wf.roles)) {
|
||||||
|
lines.push(` ${name}:`);
|
||||||
|
lines.push(` description: ${role.description}`);
|
||||||
|
lines.push(` systemPrompt: ${role.systemPrompt.slice(0, 80).replace(/\n/g, " ")}...`);
|
||||||
|
lines.push(` extractPrompt: ${role.extractPrompt.slice(0, 80).replace(/\n/g, " ")}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wf.moderator.length > 0) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push("moderator:");
|
||||||
|
for (const rule of wf.moderator) {
|
||||||
|
const when = rule.when === null ? "(always)" : `when: ${rule.when}`;
|
||||||
|
lines.push(` ${rule.from} → ${rule.to} [${when}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
"@uncaged/workflow-cas": "workspace:^",
|
"@uncaged/workflow-cas": "workspace:^",
|
||||||
"@uncaged/workflow-reactor": "workspace:^",
|
"@uncaged/workflow-reactor": "workspace:^",
|
||||||
"@uncaged/workflow-register": "workspace:^",
|
"@uncaged/workflow-register": "workspace:^",
|
||||||
"@uncaged/json-cas": "file:../../../json-cas/packages/json-cas",
|
"@uncaged/json-cas": "^0.1.0",
|
||||||
"@uncaged/json-cas-workflow": "file:../../../json-cas/packages/json-cas-workflow",
|
"@uncaged/json-cas-workflow": "^0.1.0",
|
||||||
"@uncaged/workflow-json-def": "workspace:^",
|
"@uncaged/workflow-json-def": "workspace:^",
|
||||||
"yaml": "^2.7.1"
|
"yaml": "^2.7.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/json-cas": "file:../../../json-cas/packages/json-cas",
|
"@uncaged/json-cas": "^0.1.0",
|
||||||
"@uncaged/json-cas-workflow": "file:../../../json-cas/packages/json-cas-workflow"
|
"@uncaged/json-cas-workflow": "^0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user