@versatly/workgraph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,643 @@
1
+ import {
2
+ bases_exports,
3
+ command_center_exports,
4
+ ledger_exports,
5
+ registry_exports,
6
+ skill_exports,
7
+ store_exports,
8
+ thread_exports,
9
+ workspace_exports
10
+ } from "./chunk-CRQXDCPR.js";
11
+
12
+ // src/cli.ts
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { Command } from "commander";
16
+ var DEFAULT_ACTOR = process.env.WORKGRAPH_AGENT || process.env.CLAWVAULT_AGENT || process.env.USER || "anonymous";
17
+ var CLI_VERSION = (() => {
18
+ try {
19
+ const pkgUrl = new URL("../package.json", import.meta.url);
20
+ const pkg = JSON.parse(fs.readFileSync(pkgUrl, "utf-8"));
21
+ return pkg.version ?? "0.0.0";
22
+ } catch {
23
+ return "0.0.0";
24
+ }
25
+ })();
26
+ var program = new Command();
27
+ program.name("workgraph").description("Agent-first workgraph workspace for multi-agent collaboration.").version(CLI_VERSION);
28
+ program.showHelpAfterError();
29
+ addWorkspaceOption(
30
+ program.command("init [path]").description("Initialize a pure workgraph workspace (no memory category scaffolding)").option("-n, --name <name>", "Workspace name").option("--no-type-dirs", "Do not pre-create built-in type directories").option("--no-bases", "Do not generate .base files from primitive registry").option("--no-readme", "Do not create README.md").option("--json", "Emit structured JSON output")
31
+ ).action(
32
+ (targetPath, opts) => runCommand(
33
+ opts,
34
+ () => {
35
+ const workspacePath = path.resolve(targetPath || resolveWorkspacePath(opts));
36
+ const result = workspace_exports.initWorkspace(workspacePath, {
37
+ name: opts.name,
38
+ createTypeDirs: opts.typeDirs,
39
+ createBases: opts.bases,
40
+ createReadme: opts.readme
41
+ });
42
+ return result;
43
+ },
44
+ (result) => [
45
+ `Initialized workgraph workspace: ${result.workspacePath}`,
46
+ `Seeded types: ${result.seededTypes.join(", ")}`,
47
+ `Generated .base files: ${result.generatedBases.length}`,
48
+ `Config: ${result.configPath}`
49
+ ]
50
+ )
51
+ );
52
+ var threadCmd = program.command("thread").description("Coordinate work through claimable threads");
53
+ addWorkspaceOption(
54
+ threadCmd.command("create <title>").description("Create a new thread").requiredOption("-g, --goal <goal>", "What success looks like").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("-p, --priority <level>", "urgent | high | medium | low", "medium").option("--deps <paths>", "Comma-separated dependency thread paths").option("--parent <path>", "Parent thread path").option("--space <spaceRef>", "Optional space ref (e.g. spaces/backend.md)").option("--context <refs>", "Comma-separated workspace doc refs for context").option("--tags <tags>", "Comma-separated tags").option("--json", "Emit structured JSON output")
55
+ ).action(
56
+ (title, opts) => runCommand(
57
+ opts,
58
+ () => {
59
+ const workspacePath = resolveWorkspacePath(opts);
60
+ return {
61
+ thread: thread_exports.createThread(workspacePath, title, opts.goal, opts.actor, {
62
+ priority: opts.priority,
63
+ deps: csv(opts.deps),
64
+ parent: opts.parent,
65
+ space: opts.space,
66
+ context_refs: csv(opts.context),
67
+ tags: csv(opts.tags)
68
+ })
69
+ };
70
+ },
71
+ (result) => [
72
+ `Created thread: ${result.thread.path}`,
73
+ `Status: ${String(result.thread.fields.status)}`,
74
+ `Priority: ${String(result.thread.fields.priority)}`
75
+ ]
76
+ )
77
+ );
78
+ addWorkspaceOption(
79
+ threadCmd.command("list").description("List threads (optionally by state/ready status)").option("-s, --status <status>", "open | active | blocked | done | cancelled").option("--space <spaceRef>", "Filter threads by space ref").option("--ready", "Only include threads ready to be claimed now").option("--json", "Emit structured JSON output")
80
+ ).action(
81
+ (opts) => runCommand(
82
+ opts,
83
+ () => {
84
+ const workspacePath = resolveWorkspacePath(opts);
85
+ let threads = opts.space ? store_exports.threadsInSpace(workspacePath, opts.space) : store_exports.list(workspacePath, "thread");
86
+ const readySet = new Set(
87
+ (opts.space ? thread_exports.listReadyThreadsInSpace(workspacePath, opts.space) : thread_exports.listReadyThreads(workspacePath)).map((t) => t.path)
88
+ );
89
+ if (opts.status) threads = threads.filter((t) => t.fields.status === opts.status);
90
+ if (opts.ready) threads = threads.filter((t) => readySet.has(t.path));
91
+ const enriched = threads.map((t) => ({
92
+ ...t,
93
+ ready: readySet.has(t.path)
94
+ }));
95
+ return { threads: enriched, count: enriched.length };
96
+ },
97
+ (result) => {
98
+ if (result.threads.length === 0) return ["No threads found."];
99
+ return [
100
+ ...result.threads.map((t) => {
101
+ const status = String(t.fields.status);
102
+ const owner = t.fields.owner ? ` (${String(t.fields.owner)})` : "";
103
+ const ready = t.ready ? " ready" : "";
104
+ return `[${status}]${ready} ${String(t.fields.title)}${owner} -> ${t.path}`;
105
+ }),
106
+ `${result.count} thread(s)`
107
+ ];
108
+ }
109
+ )
110
+ );
111
+ addWorkspaceOption(
112
+ threadCmd.command("next").description("Pick the next ready thread, optionally claim it").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--space <spaceRef>", "Restrict scheduling to one space").option("--claim", "Immediately claim the next ready thread").option("--fail-on-empty", "Exit non-zero if no ready thread exists").option("--json", "Emit structured JSON output")
113
+ ).action(
114
+ (opts) => runCommand(
115
+ opts,
116
+ () => {
117
+ const workspacePath = resolveWorkspacePath(opts);
118
+ const thread = opts.claim ? opts.space ? thread_exports.claimNextReadyInSpace(workspacePath, opts.actor, opts.space) : thread_exports.claimNextReady(workspacePath, opts.actor) : opts.space ? thread_exports.pickNextReadyThreadInSpace(workspacePath, opts.space) : thread_exports.pickNextReadyThread(workspacePath);
119
+ if (!thread && opts.failOnEmpty) {
120
+ throw new Error("No ready threads available.");
121
+ }
122
+ return {
123
+ thread,
124
+ claimed: !!opts.claim && !!thread
125
+ };
126
+ },
127
+ (result) => {
128
+ if (!result.thread) return ["No ready thread available."];
129
+ return [
130
+ `${result.claimed ? "Claimed" : "Selected"} thread: ${result.thread.path}`,
131
+ `Title: ${String(result.thread.fields.title)}`,
132
+ ...result.thread.fields.space ? [`Space: ${String(result.thread.fields.space)}`] : []
133
+ ];
134
+ }
135
+ )
136
+ );
137
+ addWorkspaceOption(
138
+ threadCmd.command("show <threadPath>").description("Show thread details and ledger history").option("--json", "Emit structured JSON output")
139
+ ).action(
140
+ (threadPath, opts) => runCommand(
141
+ opts,
142
+ () => {
143
+ const workspacePath = resolveWorkspacePath(opts);
144
+ const thread = store_exports.read(workspacePath, threadPath);
145
+ if (!thread) throw new Error(`Thread not found: ${threadPath}`);
146
+ const history = ledger_exports.historyOf(workspacePath, threadPath);
147
+ return { thread, history };
148
+ },
149
+ (result) => [
150
+ `${String(result.thread.fields.title)} (${result.thread.path})`,
151
+ `Status: ${String(result.thread.fields.status)} Owner: ${String(result.thread.fields.owner ?? "unclaimed")}`,
152
+ `History entries: ${result.history.length}`
153
+ ]
154
+ )
155
+ );
156
+ addWorkspaceOption(
157
+ threadCmd.command("claim <threadPath>").description("Claim a thread for this agent").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
158
+ ).action(
159
+ (threadPath, opts) => runCommand(
160
+ opts,
161
+ () => {
162
+ const workspacePath = resolveWorkspacePath(opts);
163
+ return { thread: thread_exports.claim(workspacePath, threadPath, opts.actor) };
164
+ },
165
+ (result) => [`Claimed: ${result.thread.path}`, `Owner: ${String(result.thread.fields.owner)}`]
166
+ )
167
+ );
168
+ addWorkspaceOption(
169
+ threadCmd.command("release <threadPath>").description("Release a claimed thread back to open").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--reason <reason>", "Why you are releasing").option("--json", "Emit structured JSON output")
170
+ ).action(
171
+ (threadPath, opts) => runCommand(
172
+ opts,
173
+ () => {
174
+ const workspacePath = resolveWorkspacePath(opts);
175
+ return { thread: thread_exports.release(workspacePath, threadPath, opts.actor, opts.reason) };
176
+ },
177
+ (result) => [`Released: ${result.thread.path}`, `Status: ${String(result.thread.fields.status)}`]
178
+ )
179
+ );
180
+ addWorkspaceOption(
181
+ threadCmd.command("done <threadPath>").description("Mark a thread done").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("-o, --output <text>", "Output/result summary").option("--json", "Emit structured JSON output")
182
+ ).action(
183
+ (threadPath, opts) => runCommand(
184
+ opts,
185
+ () => {
186
+ const workspacePath = resolveWorkspacePath(opts);
187
+ return { thread: thread_exports.done(workspacePath, threadPath, opts.actor, opts.output) };
188
+ },
189
+ (result) => [`Done: ${result.thread.path}`]
190
+ )
191
+ );
192
+ addWorkspaceOption(
193
+ threadCmd.command("block <threadPath>").description("Mark a thread blocked").requiredOption("-b, --blocked-by <dep>", "Dependency blocking this thread").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--reason <reason>", "Why it is blocked").option("--json", "Emit structured JSON output")
194
+ ).action(
195
+ (threadPath, opts) => runCommand(
196
+ opts,
197
+ () => {
198
+ const workspacePath = resolveWorkspacePath(opts);
199
+ return {
200
+ thread: thread_exports.block(workspacePath, threadPath, opts.actor, opts.blockedBy, opts.reason)
201
+ };
202
+ },
203
+ (result) => [`Blocked: ${result.thread.path}`]
204
+ )
205
+ );
206
+ addWorkspaceOption(
207
+ threadCmd.command("unblock <threadPath>").description("Unblock a thread").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
208
+ ).action(
209
+ (threadPath, opts) => runCommand(
210
+ opts,
211
+ () => {
212
+ const workspacePath = resolveWorkspacePath(opts);
213
+ return { thread: thread_exports.unblock(workspacePath, threadPath, opts.actor) };
214
+ },
215
+ (result) => [`Unblocked: ${result.thread.path}`]
216
+ )
217
+ );
218
+ addWorkspaceOption(
219
+ threadCmd.command("decompose <threadPath>").description("Break a thread into sub-threads").requiredOption("--sub <specs...>", 'Sub-thread specs as "title|goal"').option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--json", "Emit structured JSON output")
220
+ ).action(
221
+ (threadPath, opts) => runCommand(
222
+ opts,
223
+ () => {
224
+ const workspacePath = resolveWorkspacePath(opts);
225
+ const subthreads = opts.sub.map((spec) => {
226
+ const [title, ...goalParts] = spec.split("|");
227
+ const goal = goalParts.join("|").trim() || title.trim();
228
+ return { title: title.trim(), goal };
229
+ });
230
+ return { children: thread_exports.decompose(workspacePath, threadPath, subthreads, opts.actor) };
231
+ },
232
+ (result) => [`Created ${result.children.length} sub-thread(s).`]
233
+ )
234
+ );
235
+ var primitiveCmd = program.command("primitive").description("Manage primitive type definitions and instances");
236
+ addWorkspaceOption(
237
+ primitiveCmd.command("define <name>").description("Define a new primitive type").requiredOption("-d, --description <desc>", "Type description").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--fields <specs...>", 'Field definitions as "name:type"').option("--dir <directory>", "Storage directory override").option("--json", "Emit structured JSON output")
238
+ ).action(
239
+ (name, opts) => runCommand(
240
+ opts,
241
+ () => {
242
+ const workspacePath = resolveWorkspacePath(opts);
243
+ const fields = {};
244
+ for (const spec of opts.fields ?? []) {
245
+ const [fieldName, fieldType = "string"] = String(spec).split(":");
246
+ fields[fieldName.trim()] = { type: fieldType.trim() };
247
+ }
248
+ const type = registry_exports.defineType(
249
+ workspacePath,
250
+ name,
251
+ opts.description,
252
+ fields,
253
+ opts.actor,
254
+ opts.dir
255
+ );
256
+ bases_exports.syncPrimitiveRegistryManifest(workspacePath);
257
+ const baseResult = bases_exports.generateBasesFromPrimitiveRegistry(workspacePath, {
258
+ includeNonCanonical: true
259
+ });
260
+ return {
261
+ type,
262
+ basesGenerated: baseResult.generated.length
263
+ };
264
+ },
265
+ (result) => [
266
+ `Defined type: ${result.type.name}`,
267
+ `Directory: ${result.type.directory}/`,
268
+ `Bases generated: ${result.basesGenerated}`
269
+ ]
270
+ )
271
+ );
272
+ var basesCmd = program.command("bases").description("Generate Obsidian .base files from primitive-registry.yaml");
273
+ addWorkspaceOption(
274
+ basesCmd.command("sync-registry").description("Sync .clawvault/primitive-registry.yaml from active registry").option("--json", "Emit structured JSON output")
275
+ ).action(
276
+ (opts) => runCommand(
277
+ opts,
278
+ () => {
279
+ const workspacePath = resolveWorkspacePath(opts);
280
+ const manifest = bases_exports.syncPrimitiveRegistryManifest(workspacePath);
281
+ return {
282
+ primitiveCount: manifest.primitives.length,
283
+ manifestPath: ".clawvault/primitive-registry.yaml"
284
+ };
285
+ },
286
+ (result) => [
287
+ `Synced primitive registry manifest: ${result.manifestPath}`,
288
+ `Primitives: ${result.primitiveCount}`
289
+ ]
290
+ )
291
+ );
292
+ addWorkspaceOption(
293
+ basesCmd.command("generate").description("Generate .base files by reading primitive-registry.yaml").option("--all", "Include non-canonical primitives").option("--refresh-registry", "Refresh primitive-registry.yaml before generation").option("--output-dir <path>", "Output directory for .base files (default: .clawvault/bases)").option("--json", "Emit structured JSON output")
294
+ ).action(
295
+ (opts) => runCommand(
296
+ opts,
297
+ () => {
298
+ const workspacePath = resolveWorkspacePath(opts);
299
+ if (opts.refreshRegistry) {
300
+ bases_exports.syncPrimitiveRegistryManifest(workspacePath);
301
+ }
302
+ return bases_exports.generateBasesFromPrimitiveRegistry(workspacePath, {
303
+ includeNonCanonical: !!opts.all,
304
+ outputDirectory: opts.outputDir
305
+ });
306
+ },
307
+ (result) => [
308
+ `Generated ${result.generated.length} .base file(s)`,
309
+ `Directory: ${result.outputDirectory}`
310
+ ]
311
+ )
312
+ );
313
+ addWorkspaceOption(
314
+ primitiveCmd.command("list").description("List primitive types").option("--json", "Emit structured JSON output")
315
+ ).action(
316
+ (opts) => runCommand(
317
+ opts,
318
+ () => {
319
+ const workspacePath = resolveWorkspacePath(opts);
320
+ const types = registry_exports.listTypes(workspacePath);
321
+ return { types, count: types.length };
322
+ },
323
+ (result) => result.types.map((t) => `${t.name} (${t.directory}/) ${t.builtIn ? "[built-in]" : ""}`)
324
+ )
325
+ );
326
+ addWorkspaceOption(
327
+ primitiveCmd.command("create <type> <title>").description("Create an instance of any primitive type").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--set <fields...>", 'Set fields as "key=value"').option("--body <text>", "Markdown body content", "").option("--json", "Emit structured JSON output")
328
+ ).action(
329
+ (type, title, opts) => runCommand(
330
+ opts,
331
+ () => {
332
+ const workspacePath = resolveWorkspacePath(opts);
333
+ const fields = { title };
334
+ for (const pair of opts.set ?? []) {
335
+ const eqIdx = String(pair).indexOf("=");
336
+ if (eqIdx === -1) continue;
337
+ const key = String(pair).slice(0, eqIdx).trim();
338
+ let value = String(pair).slice(eqIdx + 1).trim();
339
+ if (typeof value === "string" && value.includes(",")) {
340
+ value = value.split(",").map((v) => v.trim());
341
+ }
342
+ fields[key] = value;
343
+ }
344
+ return {
345
+ instance: store_exports.create(workspacePath, type, fields, opts.body, opts.actor)
346
+ };
347
+ },
348
+ (result) => [`Created ${result.instance.type}: ${result.instance.path}`]
349
+ )
350
+ );
351
+ var skillCmd = program.command("skill").description("Manage native skill primitives in shared workgraph vaults");
352
+ addWorkspaceOption(
353
+ skillCmd.command("write <title>").description("Create or update a skill primitive").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--owner <name>", "Skill owner").option("--version <semver>", "Skill version").option("--status <status>", "draft | proposed | active | deprecated | archived").option("--distribution <mode>", "Distribution mode", "tailscale-shared-vault").option("--tailscale-path <path>", "Shared Tailscale workspace path").option("--reviewers <list>", "Comma-separated reviewer names").option("--tags <list>", "Comma-separated tags").option("--body <text>", "Skill markdown content").option("--body-file <path>", "Read markdown content from file").option("--json", "Emit structured JSON output")
354
+ ).action(
355
+ (title, opts) => runCommand(
356
+ opts,
357
+ () => {
358
+ const workspacePath = resolveWorkspacePath(opts);
359
+ let body = opts.body ?? "";
360
+ if (opts.bodyFile) {
361
+ const absBodyFile = path.resolve(opts.bodyFile);
362
+ body = fs.readFileSync(absBodyFile, "utf-8");
363
+ }
364
+ const instance = skill_exports.writeSkill(
365
+ workspacePath,
366
+ title,
367
+ body,
368
+ opts.actor,
369
+ {
370
+ owner: opts.owner,
371
+ version: opts.version,
372
+ status: opts.status,
373
+ distribution: opts.distribution,
374
+ tailscalePath: opts.tailscalePath,
375
+ reviewers: csv(opts.reviewers),
376
+ tags: csv(opts.tags)
377
+ }
378
+ );
379
+ bases_exports.syncPrimitiveRegistryManifest(workspacePath);
380
+ bases_exports.generateBasesFromPrimitiveRegistry(workspacePath, { includeNonCanonical: true });
381
+ return { skill: instance };
382
+ },
383
+ (result) => [
384
+ `Wrote skill: ${result.skill.path}`,
385
+ `Status: ${String(result.skill.fields.status)} Version: ${String(result.skill.fields.version)}`
386
+ ]
387
+ )
388
+ );
389
+ addWorkspaceOption(
390
+ skillCmd.command("load <skillRef>").description("Load one skill primitive by slug or path").option("--json", "Emit structured JSON output")
391
+ ).action(
392
+ (skillRef, opts) => runCommand(
393
+ opts,
394
+ () => {
395
+ const workspacePath = resolveWorkspacePath(opts);
396
+ return { skill: skill_exports.loadSkill(workspacePath, skillRef) };
397
+ },
398
+ (result) => [
399
+ `Skill: ${String(result.skill.fields.title)}`,
400
+ `Path: ${result.skill.path}`,
401
+ `Status: ${String(result.skill.fields.status)}`
402
+ ]
403
+ )
404
+ );
405
+ addWorkspaceOption(
406
+ skillCmd.command("list").description("List skills").option("--status <status>", "Filter by status").option("--json", "Emit structured JSON output")
407
+ ).action(
408
+ (opts) => runCommand(
409
+ opts,
410
+ () => {
411
+ const workspacePath = resolveWorkspacePath(opts);
412
+ const skills = skill_exports.listSkills(workspacePath, { status: opts.status });
413
+ return { skills, count: skills.length };
414
+ },
415
+ (result) => result.skills.map((skill) => `${String(skill.fields.title)} [${String(skill.fields.status)}] -> ${skill.path}`)
416
+ )
417
+ );
418
+ addWorkspaceOption(
419
+ skillCmd.command("propose <skillRef>").description("Move a skill into proposed state and open review thread").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--proposal-thread <path>", "Explicit proposal thread path").option("--no-create-thread", "Do not create a proposal thread automatically").option("--space <spaceRef>", "Space for created proposal thread").option("--reviewers <list>", "Comma-separated reviewers").option("--json", "Emit structured JSON output")
420
+ ).action(
421
+ (skillRef, opts) => runCommand(
422
+ opts,
423
+ () => {
424
+ const workspacePath = resolveWorkspacePath(opts);
425
+ return {
426
+ skill: skill_exports.proposeSkill(workspacePath, skillRef, opts.actor, {
427
+ proposalThread: opts.proposalThread,
428
+ createThreadIfMissing: opts.createThread,
429
+ space: opts.space,
430
+ reviewers: csv(opts.reviewers)
431
+ })
432
+ };
433
+ },
434
+ (result) => [
435
+ `Proposed skill: ${result.skill.path}`,
436
+ `Proposal thread: ${String(result.skill.fields.proposal_thread ?? "none")}`
437
+ ]
438
+ )
439
+ );
440
+ addWorkspaceOption(
441
+ skillCmd.command("promote <skillRef>").description("Promote a proposed/draft skill to active").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("--version <semver>", "Explicit promoted version").option("--json", "Emit structured JSON output")
442
+ ).action(
443
+ (skillRef, opts) => runCommand(
444
+ opts,
445
+ () => {
446
+ const workspacePath = resolveWorkspacePath(opts);
447
+ return {
448
+ skill: skill_exports.promoteSkill(workspacePath, skillRef, opts.actor, {
449
+ version: opts.version
450
+ })
451
+ };
452
+ },
453
+ (result) => [
454
+ `Promoted skill: ${result.skill.path}`,
455
+ `Status: ${String(result.skill.fields.status)} Version: ${String(result.skill.fields.version)}`
456
+ ]
457
+ )
458
+ );
459
+ var ledgerCmd = program.command("ledger").description("Inspect the append-only workgraph ledger");
460
+ addWorkspaceOption(
461
+ ledgerCmd.command("show").description("Show recent ledger entries").option("-n, --count <n>", "Number of entries", "20").option("--actor <name>", "Filter by actor").option("--json", "Emit structured JSON output")
462
+ ).action(
463
+ (opts) => runCommand(
464
+ opts,
465
+ () => {
466
+ const workspacePath = resolveWorkspacePath(opts);
467
+ const count = Number.parseInt(String(opts.count), 10);
468
+ const safeCount = Number.isNaN(count) ? 20 : count;
469
+ let entries = ledger_exports.recent(workspacePath, safeCount);
470
+ if (opts.actor) entries = entries.filter((e) => e.actor === opts.actor);
471
+ return { entries, count: entries.length };
472
+ },
473
+ (result) => result.entries.map((e) => `${e.ts} ${e.op} ${e.actor} ${e.target}`)
474
+ )
475
+ );
476
+ addWorkspaceOption(
477
+ ledgerCmd.command("history <targetPath>").description("Show full history of a target path").option("--json", "Emit structured JSON output")
478
+ ).action(
479
+ (targetPath, opts) => runCommand(
480
+ opts,
481
+ () => {
482
+ const workspacePath = resolveWorkspacePath(opts);
483
+ const entries = ledger_exports.historyOf(workspacePath, targetPath);
484
+ return { target: targetPath, entries, count: entries.length };
485
+ },
486
+ (result) => result.entries.map((e) => `${e.ts} ${e.op} ${e.actor}`)
487
+ )
488
+ );
489
+ addWorkspaceOption(
490
+ ledgerCmd.command("claims").description("Show active claims").option("--json", "Emit structured JSON output")
491
+ ).action(
492
+ (opts) => runCommand(
493
+ opts,
494
+ () => {
495
+ const workspacePath = resolveWorkspacePath(opts);
496
+ const claimsMap = ledger_exports.allClaims(workspacePath);
497
+ const claims = [...claimsMap.entries()].map(([target, owner]) => ({ target, owner }));
498
+ return { claims, count: claims.length };
499
+ },
500
+ (result) => result.claims.map((c) => `${c.owner} -> ${c.target}`)
501
+ )
502
+ );
503
+ addWorkspaceOption(
504
+ ledgerCmd.command("query").description("Query ledger with structured filters").option("--actor <name>", "Filter by actor").option("--op <operation>", "Filter by operation").option("--type <primitiveType>", "Filter by primitive type").option("--target <path>", "Filter by exact target path").option("--target-includes <text>", "Filter by target substring").option("--since <iso>", "Filter entries on/after ISO timestamp").option("--until <iso>", "Filter entries on/before ISO timestamp").option("--limit <n>", "Limit number of results").option("--offset <n>", "Offset into result set").option("--json", "Emit structured JSON output")
505
+ ).action(
506
+ (opts) => runCommand(
507
+ opts,
508
+ () => {
509
+ const workspacePath = resolveWorkspacePath(opts);
510
+ return {
511
+ entries: ledger_exports.query(workspacePath, {
512
+ actor: opts.actor,
513
+ op: opts.op,
514
+ type: opts.type,
515
+ target: opts.target,
516
+ targetIncludes: opts.targetIncludes,
517
+ since: opts.since,
518
+ until: opts.until,
519
+ limit: opts.limit ? Number.parseInt(String(opts.limit), 10) : void 0,
520
+ offset: opts.offset ? Number.parseInt(String(opts.offset), 10) : void 0
521
+ })
522
+ };
523
+ },
524
+ (result) => result.entries.map((entry) => `${entry.ts} ${entry.op} ${entry.actor} ${entry.target}`)
525
+ )
526
+ );
527
+ addWorkspaceOption(
528
+ ledgerCmd.command("blame <targetPath>").description("Show actor attribution summary for one target").option("--json", "Emit structured JSON output")
529
+ ).action(
530
+ (targetPath, opts) => runCommand(
531
+ opts,
532
+ () => {
533
+ const workspacePath = resolveWorkspacePath(opts);
534
+ return ledger_exports.blame(workspacePath, targetPath);
535
+ },
536
+ (result) => [
537
+ `Target: ${result.target}`,
538
+ `Entries: ${result.totalEntries}`,
539
+ ...result.actors.map((actor) => `${actor.actor}: ${actor.count} change(s)`)
540
+ ]
541
+ )
542
+ );
543
+ addWorkspaceOption(
544
+ ledgerCmd.command("verify").description("Verify tamper-evident ledger hash-chain integrity").option("--strict", "Treat missing hash fields as verification failures").option("--json", "Emit structured JSON output")
545
+ ).action(
546
+ (opts) => runCommand(
547
+ opts,
548
+ () => {
549
+ const workspacePath = resolveWorkspacePath(opts);
550
+ return ledger_exports.verifyHashChain(workspacePath, { strict: !!opts.strict });
551
+ },
552
+ (result) => [
553
+ `Hash-chain valid: ${result.ok}`,
554
+ `Entries: ${result.entries}`,
555
+ `Last hash: ${result.lastHash}`,
556
+ ...result.issues.length > 0 ? result.issues.map((issue) => `ISSUE: ${issue}`) : [],
557
+ ...result.warnings.length > 0 ? result.warnings.map((warning) => `WARN: ${warning}`) : []
558
+ ]
559
+ )
560
+ );
561
+ addWorkspaceOption(
562
+ ledgerCmd.command("seal").description("Rebuild ledger index + hash-chain state from ledger.jsonl").option("--json", "Emit structured JSON output")
563
+ ).action(
564
+ (opts) => runCommand(
565
+ opts,
566
+ () => {
567
+ const workspacePath = resolveWorkspacePath(opts);
568
+ const index = ledger_exports.rebuildIndex(workspacePath);
569
+ const chain = ledger_exports.rebuildHashChainState(workspacePath);
570
+ return {
571
+ indexClaims: Object.keys(index.claims).length,
572
+ chainCount: chain.count,
573
+ chainLastHash: chain.lastHash
574
+ };
575
+ },
576
+ (result) => [
577
+ `Rebuilt ledger index claims: ${result.indexClaims}`,
578
+ `Rebuilt chain entries: ${result.chainCount}`
579
+ ]
580
+ )
581
+ );
582
+ addWorkspaceOption(
583
+ program.command("command-center").description("Generate a markdown command center from workgraph state").option("-a, --actor <name>", "Agent name", DEFAULT_ACTOR).option("-o, --output <path>", "Output markdown path", "Command Center.md").option("-n, --recent <count>", "Recent ledger entries to include", "15").option("--json", "Emit structured JSON output")
584
+ ).action(
585
+ (opts) => runCommand(
586
+ opts,
587
+ () => {
588
+ const workspacePath = resolveWorkspacePath(opts);
589
+ const parsedRecent = Number.parseInt(String(opts.recent), 10);
590
+ const safeRecent = Number.isNaN(parsedRecent) ? 15 : parsedRecent;
591
+ return command_center_exports.generateCommandCenter(workspacePath, {
592
+ actor: opts.actor,
593
+ outputPath: opts.output,
594
+ recentCount: safeRecent
595
+ });
596
+ },
597
+ (result) => [
598
+ `Generated command center: ${result.outputPath}`,
599
+ `Threads: total=${result.stats.totalThreads} open=${result.stats.openThreads} active=${result.stats.activeThreads} blocked=${result.stats.blockedThreads}`,
600
+ `Claims: ${result.stats.activeClaims} Recent events: ${result.stats.recentEvents}`
601
+ ]
602
+ )
603
+ );
604
+ program.parse();
605
+ function addWorkspaceOption(command) {
606
+ return command.option("-w, --workspace <path>", "Workgraph workspace path").option("--vault <path>", "Alias for --workspace").option("--shared-vault <path>", "Shared vault path (e.g. mounted via Tailscale)");
607
+ }
608
+ function resolveWorkspacePath(opts) {
609
+ const explicit = opts.workspace || opts.vault || opts.sharedVault;
610
+ if (explicit) return path.resolve(explicit);
611
+ if (process.env.WORKGRAPH_SHARED_VAULT) return path.resolve(process.env.WORKGRAPH_SHARED_VAULT);
612
+ if (process.env.WORKGRAPH_PATH) return path.resolve(process.env.WORKGRAPH_PATH);
613
+ if (process.env.CLAWVAULT_PATH) return path.resolve(process.env.CLAWVAULT_PATH);
614
+ return process.cwd();
615
+ }
616
+ function csv(value) {
617
+ if (!value) return void 0;
618
+ return String(value).split(",").map((s) => s.trim()).filter(Boolean);
619
+ }
620
+ function wantsJson(opts) {
621
+ if (opts.json) return true;
622
+ if (process.env.WORKGRAPH_JSON === "1") return true;
623
+ return false;
624
+ }
625
+ function runCommand(opts, action, renderText) {
626
+ try {
627
+ const result = action();
628
+ if (wantsJson(opts)) {
629
+ console.log(JSON.stringify({ ok: true, data: result }, null, 2));
630
+ return;
631
+ }
632
+ const lines = renderText(result);
633
+ for (const line of lines) console.log(line);
634
+ } catch (error) {
635
+ const message = error instanceof Error ? error.message : String(error);
636
+ if (wantsJson(opts)) {
637
+ console.error(JSON.stringify({ ok: false, error: message }, null, 2));
638
+ } else {
639
+ console.error(`Error: ${message}`);
640
+ }
641
+ process.exit(1);
642
+ }
643
+ }