Compare commits

..

1 Commits

Author SHA1 Message Date
xiaoju 25ed1bff48 refactor: outputSchema only accepts inline JSON Schema
Remove CAS ref string support from workflow YAML outputSchema.
Always inline, always auto-register via putSchema. Simpler.

小橘 🍊(NEKO Team)
2026-05-18 13:15:48 +00:00
4 changed files with 103 additions and 98 deletions
+31 -45
View File
@@ -22,11 +22,9 @@ import {
cmdCasWalk,
} from "./commands/cas.js";
import { resolveStorageRoot } from "./store.js";
import { type OutputFormat, formatOutput } from "./format.js";
function writeOutput(data: unknown): void {
const fmt = program.opts().format as OutputFormat;
process.stdout.write(`${formatOutput(data, fmt)}\n`);
function writeJson(data: unknown): void {
process.stdout.write(`${JSON.stringify(data)}\n`);
}
function runAction(action: () => Promise<void>): void {
@@ -40,7 +38,6 @@ function runAction(action: () => Promise<void>): void {
const program = new Command();
program.name("uwf").description("Stateless workflow CLI");
program.option("--format <fmt>", "Output format: json or yaml", "json");
const workflow = program.command("workflow").description("Workflow registry and CAS");
@@ -52,7 +49,7 @@ workflow
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdWorkflowPut(storageRoot, file);
writeOutput(result);
writeJson(result);
});
});
@@ -64,7 +61,7 @@ workflow
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdWorkflowShow(storageRoot, id);
writeOutput(result);
writeJson(result);
});
});
@@ -75,7 +72,7 @@ workflow
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdWorkflowList(storageRoot);
writeOutput(result);
writeJson(result);
});
});
@@ -90,7 +87,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadStart(storageRoot, workflow, opts.prompt);
writeOutput(result);
writeJson(result);
});
});
@@ -104,7 +101,7 @@ thread
runAction(async () => {
const agentOverride = opts.agent ?? null;
const result = await cmdThreadStep(storageRoot, threadId, agentOverride);
writeOutput(result);
writeJson(result);
});
});
@@ -116,7 +113,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadShow(storageRoot, threadId);
writeOutput(result);
writeJson(result);
});
});
@@ -128,7 +125,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadList(storageRoot, opts.all);
writeOutput(result);
writeJson(result);
});
});
@@ -140,7 +137,7 @@ thread
const storageRoot = resolveStorageRoot();
runAction(async () => {
const result = await cmdThreadKill(storageRoot, threadId);
writeOutput(result);
writeJson(result);
});
});
@@ -170,7 +167,7 @@ program
agent: opts.agent ?? undefined,
storageRoot,
});
writeOutput(result);
writeJson(result);
} else if (!opts.provider && !opts.baseUrl && !opts.apiKey && !opts.model) {
await cmdSetupInteractive(storageRoot);
} else {
@@ -187,11 +184,10 @@ cas
.command("get")
.description("Read a CAS node as JSON")
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
.option("--json", "Compact JSON output")
.action((hash: string, opts: { json?: boolean }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasGet(storageRoot, hash));
});
runAction(() => cmdCasGet(storageRoot, hash, opts));
});
cas
@@ -199,11 +195,10 @@ cas
.description("Output a CAS node (--payload for payload only)")
.argument("<hash>", "CAS hash (13 char)")
.option("--payload", "Output only the payload")
.action((hash: string, opts: { payload?: boolean }) => {
.option("--json", "Compact JSON output")
.action((hash: string, opts: { payload?: boolean; json?: boolean }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasCat(storageRoot, hash, opts));
});
runAction(() => cmdCasCat(storageRoot, hash, opts));
});
cas
@@ -211,22 +206,19 @@ cas
.description("Store a node, print its hash")
.argument("<type-hash>", "Type (schema) hash")
.argument("<data>", "JSON file path or inline JSON string")
.action((typeHash: string, data: string) => {
.option("--json", "Compact JSON output")
.action((typeHash: string, data: string, opts: { json?: boolean }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasPut(storageRoot, typeHash, data));
});
runAction(() => cmdCasPut(storageRoot, typeHash, data, opts));
});
cas
.command("has")
.description("Check if a hash exists")
.description("Check if a hash exists (prints true/false)")
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasHas(storageRoot, hash));
});
runAction(() => cmdCasHas(storageRoot, hash));
});
cas
@@ -235,43 +227,37 @@ cas
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasRefs(storageRoot, hash));
});
runAction(() => cmdCasRefs(storageRoot, hash));
});
cas
.command("walk")
.description("Recursive traversal from a node")
.argument("<hash>", "CAS hash (13 char)")
.action((hash: string) => {
.option("--format <fmt>", "Output format: flat (default) or tree")
.action((hash: string, opts: { format?: string }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasWalk(storageRoot, hash));
});
runAction(() => cmdCasWalk(storageRoot, hash, opts));
});
const casSchema = cas.command("schema").description("CAS schema operations");
casSchema
.command("list")
.description("List all registered schemas")
.description("List all registered schemas (hash + name)")
.action(() => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasSchemaList(storageRoot));
});
runAction(() => cmdCasSchemaList(storageRoot));
});
casSchema
.command("get")
.description("Show a schema by its type hash")
.argument("<hash>", "Schema type hash")
.action((hash: string) => {
.option("--json", "Compact JSON output")
.action((hash: string, opts: { json?: boolean }) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
writeOutput(await cmdCasSchemaGet(storageRoot, hash));
});
runAction(() => cmdCasSchemaGet(storageRoot, hash, opts));
});
program.parseAsync(process.argv).catch((e: unknown) => {
+69 -40
View File
@@ -11,7 +11,12 @@ function openStore(storageRoot: string): Store {
return createFsStore(join(storageRoot, "cas"));
}
function out(data: unknown, compact = false): void {
console.log(compact ? JSON.stringify(data) : JSON.stringify(data, null, 2));
}
function readJsonArg(fileOrInline: string): unknown {
// Try as inline JSON first, then as file path
try {
return JSON.parse(fileOrInline);
} catch {
@@ -23,114 +28,138 @@ function readJsonArg(fileOrInline: string): unknown {
}
}
// ---- Commands (all return JSON-serializable data) ----
// ---- Commands ----
export async function cmdCasGet(
storageRoot: string,
hash: string,
): Promise<unknown> {
opts: { json?: boolean },
): Promise<void> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
return node;
out(node, opts.json);
}
export async function cmdCasCat(
storageRoot: string,
hash: string,
opts: { payload?: boolean },
): Promise<unknown> {
opts: { payload?: boolean; json?: boolean },
): Promise<void> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
return opts.payload ? node.payload : node;
out(opts.payload ? node.payload : node, opts.json);
}
export async function cmdCasPut(
storageRoot: string,
typeHash: string,
data: string,
): Promise<{ hash: string }> {
opts: { json?: boolean },
): Promise<void> {
const store = openStore(storageRoot);
const payload = readJsonArg(data);
const hash = await store.put(typeHash, payload);
return { hash };
const hash = store.put(typeHash, payload);
console.log(hash);
}
export async function cmdCasHas(
storageRoot: string,
hash: string,
): Promise<{ exists: boolean }> {
): Promise<void> {
const store = openStore(storageRoot);
return { exists: store.has(hash) };
console.log(String(store.has(hash)));
}
export async function cmdCasRefs(
storageRoot: string,
hash: string,
): Promise<{ refs: string[] }> {
export async function cmdCasList(storageRoot: string): Promise<void> {
const store = openStore(storageRoot);
for (const hash of store.list()) {
console.log(hash);
}
}
export async function cmdCasRefs(storageRoot: string, hash: string): Promise<void> {
const store = openStore(storageRoot);
const node = store.get(hash);
if (node === null) {
throw new Error(`Node not found: ${hash}`);
}
return { refs: refs(store, node) };
const refHashes = refs(store, node);
for (const r of refHashes) {
console.log(r);
}
}
export async function cmdCasWalk(
storageRoot: string,
hash: string,
): Promise<{ hashes: string[] }> {
opts: { format?: string },
): Promise<void> {
const store = openStore(storageRoot);
const result: string[] = [];
walk(store, hash, (h) => {
result.push(h);
});
return { hashes: result };
if (opts.format === "tree") {
const childMap = new Map<Hash, Hash[]>();
walk(store, hash, (h, node) => {
childMap.set(h, refs(store, node));
});
const printed = new Set<Hash>();
function printNode(h: Hash, prefix: string, isLast: boolean): void {
const connector = prefix === "" ? "" : isLast ? "└── " : "├── ";
if (printed.has(h)) {
console.log(`${prefix}${connector}${h} (seen)`);
return;
}
printed.add(h);
console.log(`${prefix}${connector}${h}`);
const kids = childMap.get(h) ?? [];
const childPrefix = prefix === "" ? "" : prefix + (isLast ? " " : "│ ");
for (let i = 0; i < kids.length; i++) {
printNode(kids[i] as Hash, childPrefix, i === kids.length - 1);
}
}
printNode(hash, "", true);
} else {
walk(store, hash, (h) => {
console.log(h);
});
}
}
export type SchemaListEntry = {
hash: string;
title: string;
};
export async function cmdCasSchemaList(
storageRoot: string,
): Promise<SchemaListEntry[]> {
export async function cmdCasSchemaList(storageRoot: string): Promise<void> {
const store = openStore(storageRoot);
const metaHash = await bootstrap(store);
const entries: SchemaListEntry[] = [];
// Include meta-schema itself
entries.push({ hash: metaHash, title: "(meta-schema)" });
for (const hash of store.list()) {
if (hash === metaHash) continue;
const node = store.get(hash);
if (node !== null && node.type === metaHash) {
const schema = node.payload as JSONSchema;
const title =
const name =
(schema.title as string | undefined) ??
(schema.description as string | undefined) ??
"(unnamed)";
entries.push({ hash, title });
console.log(`${hash} ${name}`);
}
}
return entries;
}
export async function cmdCasSchemaGet(
storageRoot: string,
hash: string,
): Promise<unknown> {
opts: { json?: boolean },
): Promise<void> {
const store = openStore(storageRoot);
const schema = getSchema(store, hash);
if (schema === null) {
throw new Error(`Schema not found: ${hash}`);
}
return schema;
out(schema, opts.json);
}
-12
View File
@@ -1,12 +0,0 @@
import { stringify } from "yaml";
export type OutputFormat = "json" | "yaml";
export function formatOutput(data: unknown, format: OutputFormat): string {
switch (format) {
case "json":
return JSON.stringify(data);
case "yaml":
return stringify(data).trimEnd();
}
}
+3 -1
View File
@@ -15,7 +15,9 @@ function isRoleDefinition(value: unknown): boolean {
return false;
}
const outputSchema = value.outputSchema;
const schemaOk = isRecord(outputSchema) && typeof outputSchema.type === "string";
const schemaOk =
typeof outputSchema === "string" ||
(isRecord(outputSchema) && typeof outputSchema.type === "string");
return (
typeof value.description === "string" && typeof value.systemPrompt === "string" && schemaOk
);