akm-cli 0.7.5 → 0.8.0-rc.3

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.
Files changed (155) hide show
  1. package/.github/CHANGELOG.md +1 -1
  2. package/dist/cli/parse-args.js +86 -0
  3. package/dist/cli.js +1023 -521
  4. package/dist/commands/agent-dispatch.js +107 -0
  5. package/dist/commands/agent-support.js +62 -0
  6. package/dist/commands/config-cli.js +68 -84
  7. package/dist/commands/consolidate.js +812 -0
  8. package/dist/commands/distill-promotion-policy.js +658 -0
  9. package/dist/commands/distill.js +218 -43
  10. package/dist/commands/eval-cases.js +40 -0
  11. package/dist/commands/events.js +2 -23
  12. package/dist/commands/graph.js +222 -0
  13. package/dist/commands/health.js +376 -0
  14. package/dist/commands/help/help-accept.md +9 -0
  15. package/dist/commands/help/help-improve.md +53 -0
  16. package/dist/commands/help/help-proposals.md +15 -0
  17. package/dist/commands/help/help-propose.md +17 -0
  18. package/dist/commands/help/help-reject.md +8 -0
  19. package/dist/commands/history.js +3 -30
  20. package/dist/commands/improve.js +1161 -0
  21. package/dist/commands/info.js +2 -2
  22. package/dist/commands/init.js +2 -2
  23. package/dist/commands/install-audit.js +5 -1
  24. package/dist/commands/installed-stashes.js +118 -138
  25. package/dist/commands/knowledge.js +133 -0
  26. package/dist/commands/lint/agent-linter.js +46 -0
  27. package/dist/commands/lint/base-linter.js +291 -0
  28. package/dist/commands/lint/command-linter.js +46 -0
  29. package/dist/commands/lint/default-linter.js +13 -0
  30. package/dist/commands/lint/index.js +145 -0
  31. package/dist/commands/lint/knowledge-linter.js +13 -0
  32. package/dist/commands/lint/memory-linter.js +58 -0
  33. package/dist/commands/lint/registry.js +33 -0
  34. package/dist/commands/lint/skill-linter.js +42 -0
  35. package/dist/commands/lint/task-linter.js +47 -0
  36. package/dist/commands/lint/types.js +1 -0
  37. package/dist/commands/lint/vault-key-rules.js +67 -0
  38. package/dist/commands/lint/workflow-linter.js +53 -0
  39. package/dist/commands/lint.js +1 -0
  40. package/dist/commands/proposal.js +8 -7
  41. package/dist/commands/propose.js +71 -28
  42. package/dist/commands/reflect.js +135 -35
  43. package/dist/commands/registry-search.js +2 -2
  44. package/dist/commands/remember.js +54 -0
  45. package/dist/commands/schema-repair.js +130 -0
  46. package/dist/commands/search.js +21 -5
  47. package/dist/commands/show.js +125 -20
  48. package/dist/commands/source-add.js +10 -10
  49. package/dist/commands/source-manage.js +11 -19
  50. package/dist/commands/tasks.js +385 -0
  51. package/dist/commands/url-checker.js +39 -0
  52. package/dist/commands/vault.js +168 -77
  53. package/dist/core/action-contributors.js +25 -0
  54. package/dist/core/asset-ref.js +4 -0
  55. package/dist/core/asset-registry.js +4 -16
  56. package/dist/core/asset-spec.js +10 -0
  57. package/dist/core/common.js +100 -0
  58. package/dist/core/concurrent.js +22 -0
  59. package/dist/core/config.js +233 -133
  60. package/dist/core/events.js +73 -126
  61. package/dist/core/frontmatter.js +0 -6
  62. package/dist/core/markdown.js +17 -0
  63. package/dist/core/memory-improve.js +678 -0
  64. package/dist/core/parse.js +155 -0
  65. package/dist/core/paths.js +101 -3
  66. package/dist/core/proposal-validators.js +61 -0
  67. package/dist/core/proposals.js +49 -38
  68. package/dist/core/state-db.js +731 -0
  69. package/dist/core/time.js +51 -0
  70. package/dist/core/warn.js +59 -1
  71. package/dist/indexer/db-search.js +52 -238
  72. package/dist/indexer/db.js +403 -54
  73. package/dist/indexer/ensure-index.js +61 -0
  74. package/dist/indexer/graph-boost.js +247 -94
  75. package/dist/indexer/graph-db.js +201 -0
  76. package/dist/indexer/graph-dedup.js +99 -0
  77. package/dist/indexer/graph-extraction.js +409 -76
  78. package/dist/indexer/index-context.js +10 -0
  79. package/dist/indexer/indexer.js +456 -290
  80. package/dist/indexer/llm-cache.js +47 -0
  81. package/dist/indexer/matchers.js +124 -160
  82. package/dist/indexer/memory-inference.js +63 -29
  83. package/dist/indexer/metadata-contributors.js +26 -0
  84. package/dist/indexer/metadata.js +196 -197
  85. package/dist/indexer/path-resolver.js +89 -0
  86. package/dist/indexer/ranking-contributors.js +204 -0
  87. package/dist/indexer/ranking.js +74 -0
  88. package/dist/indexer/search-hit-enrichers.js +22 -0
  89. package/dist/indexer/search-source.js +24 -9
  90. package/dist/indexer/semantic-status.js +2 -16
  91. package/dist/indexer/walker.js +25 -0
  92. package/dist/integrations/agent/builders.js +109 -0
  93. package/dist/integrations/agent/config.js +203 -3
  94. package/dist/integrations/agent/index.js +5 -2
  95. package/dist/integrations/agent/model-aliases.js +63 -0
  96. package/dist/integrations/agent/profiles.js +67 -5
  97. package/dist/integrations/agent/prompts.js +77 -72
  98. package/dist/integrations/agent/sdk-runner.js +120 -0
  99. package/dist/integrations/agent/spawn.js +93 -22
  100. package/dist/integrations/lockfile.js +10 -18
  101. package/dist/integrations/session-logs/index.js +65 -0
  102. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  103. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  104. package/dist/integrations/session-logs/types.js +1 -0
  105. package/dist/llm/call-ai.js +74 -0
  106. package/dist/llm/client.js +61 -122
  107. package/dist/llm/feature-gate.js +27 -16
  108. package/dist/llm/graph-extract.js +297 -62
  109. package/dist/llm/memory-infer.js +49 -71
  110. package/dist/llm/metadata-enhance.js +39 -22
  111. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  112. package/dist/output/cli-hints-full.md +277 -0
  113. package/dist/output/cli-hints-short.md +65 -0
  114. package/dist/output/cli-hints.js +2 -318
  115. package/dist/output/renderers.js +220 -256
  116. package/dist/output/shapes.js +101 -93
  117. package/dist/output/text.js +256 -17
  118. package/dist/registry/providers/skills-sh.js +61 -49
  119. package/dist/registry/providers/static-index.js +44 -48
  120. package/dist/registry/resolve.js +8 -16
  121. package/dist/setup/setup.js +510 -11
  122. package/dist/sources/provider-factory.js +2 -1
  123. package/dist/sources/providers/filesystem.js +16 -23
  124. package/dist/sources/providers/git.js +4 -5
  125. package/dist/sources/providers/website.js +15 -22
  126. package/dist/sources/website-ingest.js +4 -0
  127. package/dist/tasks/backends/cron.js +200 -0
  128. package/dist/tasks/backends/exec-utils.js +25 -0
  129. package/dist/tasks/backends/index.js +32 -0
  130. package/dist/tasks/backends/launchd-template.xml +19 -0
  131. package/dist/tasks/backends/launchd.js +184 -0
  132. package/dist/tasks/backends/schtasks-template.xml +29 -0
  133. package/dist/tasks/backends/schtasks.js +212 -0
  134. package/dist/tasks/parser.js +198 -0
  135. package/dist/tasks/resolveAkmBin.js +84 -0
  136. package/dist/tasks/runner.js +432 -0
  137. package/dist/tasks/schedule.js +208 -0
  138. package/dist/tasks/schema.js +13 -0
  139. package/dist/tasks/validator.js +59 -0
  140. package/dist/wiki/index-template.md +12 -0
  141. package/dist/wiki/ingest-workflow-template.md +54 -0
  142. package/dist/wiki/log-template.md +8 -0
  143. package/dist/wiki/schema-template.md +61 -0
  144. package/dist/wiki/wiki-templates.js +12 -0
  145. package/dist/wiki/wiki.js +10 -61
  146. package/dist/workflows/authoring.js +5 -25
  147. package/dist/workflows/renderer.js +8 -3
  148. package/dist/workflows/runs.js +59 -91
  149. package/dist/workflows/validator.js +1 -1
  150. package/dist/workflows/workflow-template.md +24 -0
  151. package/docs/README.md +5 -2
  152. package/docs/migration/release-notes/0.7.0.md +1 -1
  153. package/docs/migration/release-notes/0.8.0.md +43 -0
  154. package/package.json +3 -2
  155. package/dist/templates/wiki-templates.js +0 -100
@@ -6,6 +6,70 @@
6
6
  * `Record<string, unknown>` shapes, which makes them trivial to unit test.
7
7
  */
8
8
  const NORMAL_DESCRIPTION_LIMIT = 250;
9
+ const PASSTHROUGH_COMMANDS = new Set([
10
+ "add",
11
+ "agent-result",
12
+ "clone",
13
+ "config",
14
+ "consolidate",
15
+ "curate",
16
+ "disable",
17
+ "enable",
18
+ "feedback",
19
+ "graph-entities",
20
+ "graph-export",
21
+ "graph-related",
22
+ "graph-relations",
23
+ "graph-summary",
24
+ "health",
25
+ "import",
26
+ "improve",
27
+ "index",
28
+ "info",
29
+ "init",
30
+ "lint",
31
+ "list",
32
+ "registry-add",
33
+ "registry-build-index",
34
+ "registry-list",
35
+ "registry-remove",
36
+ "remember",
37
+ "remove",
38
+ "save",
39
+ "setup",
40
+ "tasks-add",
41
+ "tasks-disable",
42
+ "tasks-doctor",
43
+ "tasks-enable",
44
+ "tasks-history",
45
+ "tasks-list",
46
+ "tasks-remove",
47
+ "tasks-run",
48
+ "tasks-show",
49
+ "tasks-sync",
50
+ "update",
51
+ "upgrade",
52
+ "vault-create",
53
+ "vault-set",
54
+ "vault-unset",
55
+ "wiki-create",
56
+ "wiki-ingest",
57
+ "wiki-lint",
58
+ "wiki-list",
59
+ "wiki-pages",
60
+ "wiki-register",
61
+ "wiki-remove",
62
+ "wiki-show",
63
+ "wiki-stash",
64
+ "workflow-complete",
65
+ "workflow-create",
66
+ "workflow-list",
67
+ "workflow-next",
68
+ "workflow-resume",
69
+ "workflow-start",
70
+ "workflow-status",
71
+ "workflow-validate",
72
+ ]);
9
73
  export function shapeForCommand(command, result, detail, forAgent = false) {
10
74
  switch (command) {
11
75
  case "search":
@@ -45,66 +109,31 @@ export function shapeForCommand(command, result, detail, forAgent = false) {
45
109
  case "reflect":
46
110
  case "propose":
47
111
  return shapeProposalProducerOutput(result, detail);
48
- // Output shape registration for `akm distill <ref>` (#228). The shape is
49
- // simple — outcome + ids + optional payload — so `brief` strips the full
50
- // proposal blob, `normal` keeps the headline fields, and `full` projects
51
- // everything for downstream automation.
52
112
  case "distill":
53
113
  return shapeDistillOutput(result, detail);
54
- // Identity-passthrough commands — registered here so the registry stays
55
- // exhaustive (v1 spec §9). Each result object is already shaped at the
56
- // command boundary; the registry just confirms there's no surprise
57
- // command name slipping through.
58
- case "add":
59
- case "clone":
60
- case "config":
61
- case "curate":
62
- case "disable":
63
- case "enable":
64
- case "feedback":
65
- case "import":
66
- case "index":
67
- case "info":
68
- case "init":
69
- case "list":
70
- case "registry-add":
71
- case "registry-build-index":
72
- case "registry-list":
73
- case "registry-remove":
74
- case "remember":
75
- case "remove":
76
- case "save":
77
- case "update":
78
- case "upgrade":
79
- case "vault-create":
80
- case "vault-list":
81
- case "vault-set":
82
- case "vault-unset":
83
- case "wiki-create":
84
- case "wiki-ingest":
85
- case "wiki-lint":
86
- case "wiki-list":
87
- case "wiki-pages":
88
- case "wiki-register":
89
- case "wiki-remove":
90
- case "wiki-show":
91
- case "wiki-stash":
92
- case "workflow-complete":
93
- case "workflow-create":
94
- case "workflow-list":
95
- case "workflow-next":
96
- case "workflow-resume":
97
- case "workflow-start":
98
- case "workflow-status":
99
- case "workflow-validate":
100
- return result;
114
+ case "vault-list": {
115
+ const r = result;
116
+ const vaults = Array.isArray(r.vaults) ? r.vaults : [];
117
+ return {
118
+ ...r,
119
+ vaults: vaults.map((v) => {
120
+ const { path: _path, ...rest } = v;
121
+ return rest;
122
+ }),
123
+ };
124
+ }
101
125
  default:
102
- // v1 spec §9 (output-shape registry exhaustive): no silent JSON.stringify
103
- // fallback. A missing case here is a registration bug — fail loudly so
104
- // the caller (or its tests) sees the missing command name.
126
+ // v1 spec §9 (output-shape registry exhaustive): identity-passthrough
127
+ // commands are listed in PASSTHROUGH_COMMANDS; anything not in that set
128
+ // is a registration bug fail loudly.
129
+ if (PASSTHROUGH_COMMANDS.has(command))
130
+ return result;
105
131
  throw new Error(`output shape not registered for command: ${command}`);
106
132
  }
107
133
  }
134
+ function maybeAddSchema(base, detail, version) {
135
+ return detail === "full" ? { schemaVersion: version ?? 1, ...base } : base;
136
+ }
108
137
  /**
109
138
  * Shape the result of `akm reflect` / `akm propose`. On success we surface
110
139
  * the queued proposal entry (using the standard proposal-entry shaper so
@@ -141,10 +170,7 @@ export function shapeProposalProducerOutput(result, detail) {
141
170
  ...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
142
171
  proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail),
143
172
  };
144
- if (detail === "full") {
145
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
146
- }
147
- return base;
173
+ return maybeAddSchema(base, detail, result.schemaVersion);
148
174
  }
149
175
  export function shapeProposalEntry(entry, detail) {
150
176
  if (detail === "brief") {
@@ -173,10 +199,7 @@ export function shapeProposalListOutput(result, detail) {
173
199
  totalCount: result.totalCount ?? shaped.length,
174
200
  proposals: shaped,
175
201
  };
176
- if (detail === "full") {
177
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
178
- }
179
- return base;
202
+ return maybeAddSchema(base, detail, result.schemaVersion);
180
203
  }
181
204
  export function shapeProposalShowOutput(result, detail) {
182
205
  const proposal = result.proposal ?? {};
@@ -185,10 +208,7 @@ export function shapeProposalShowOutput(result, detail) {
185
208
  proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail),
186
209
  ...(validation ? { validation } : {}),
187
210
  };
188
- if (detail === "full") {
189
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
190
- }
191
- return base;
211
+ return maybeAddSchema(base, detail, result.schemaVersion);
192
212
  }
193
213
  export function shapeProposalAcceptOutput(result, detail) {
194
214
  const proposal = result.proposal ?? {};
@@ -199,10 +219,7 @@ export function shapeProposalAcceptOutput(result, detail) {
199
219
  assetPath: result.assetPath,
200
220
  proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail),
201
221
  };
202
- if (detail === "full") {
203
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
204
- }
205
- return base;
222
+ return maybeAddSchema(base, detail, result.schemaVersion);
206
223
  }
207
224
  export function shapeProposalRejectOutput(result, detail) {
208
225
  const proposal = result.proposal ?? {};
@@ -213,10 +230,7 @@ export function shapeProposalRejectOutput(result, detail) {
213
230
  ...(result.reason !== undefined ? { reason: result.reason } : {}),
214
231
  proposal: shapeProposalEntry(proposal, detail === "brief" ? "normal" : detail),
215
232
  };
216
- if (detail === "full") {
217
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
218
- }
219
- return base;
233
+ return maybeAddSchema(base, detail, result.schemaVersion);
220
234
  }
221
235
  export function shapeDistillOutput(result, detail) {
222
236
  const proposal = result.proposal;
@@ -233,10 +247,7 @@ export function shapeDistillOutput(result, detail) {
233
247
  ...(Array.isArray(result.findings) && result.findings.length > 0 ? { findings: result.findings } : {}),
234
248
  ...(proposal ? { proposal: shapeProposalEntry(proposal, detail === "summary" ? "normal" : detail) } : {}),
235
249
  };
236
- if (detail === "full") {
237
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
238
- }
239
- return base;
250
+ return maybeAddSchema(base, detail, result.schemaVersion);
240
251
  }
241
252
  export function shapeProposalDiffOutput(result, detail) {
242
253
  const base = {
@@ -246,10 +257,7 @@ export function shapeProposalDiffOutput(result, detail) {
246
257
  unified: result.unified,
247
258
  ...(result.targetPath !== undefined ? { targetPath: result.targetPath } : {}),
248
259
  };
249
- if (detail === "full") {
250
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
251
- }
252
- return base;
260
+ return maybeAddSchema(base, detail, result.schemaVersion);
253
261
  }
254
262
  export function shapeEventsOutput(result, detail) {
255
263
  const events = Array.isArray(result.events) ? result.events : [];
@@ -268,16 +276,10 @@ export function shapeEventsOutput(result, detail) {
268
276
  if (typeof result.reason === "string") {
269
277
  base.reason = result.reason;
270
278
  }
271
- if (detail === "full") {
272
- return { schemaVersion: result.schemaVersion ?? 1, ...base };
273
- }
274
- return base;
279
+ return maybeAddSchema(base, detail, result.schemaVersion);
275
280
  }
276
281
  export function shapeEventEntry(entry, detail) {
277
- if (detail === "brief") {
278
- return pickFields(entry, ["eventType", "ref", "ts"]);
279
- }
280
- if (detail === "normal" || detail === "summary") {
282
+ if (detail === "brief" || detail === "normal" || detail === "summary") {
281
283
  return pickFields(entry, ["eventType", "ref", "ts"]);
282
284
  }
283
285
  // full / agent: project everything the reader emits.
@@ -462,6 +464,7 @@ export function shapeShowOutput(result, detail, forAgent = false) {
462
464
  "steps",
463
465
  "keys",
464
466
  "comments",
467
+ "related",
465
468
  ]);
466
469
  }
467
470
  if (detail === "summary") {
@@ -477,9 +480,10 @@ export function shapeShowOutput(result, detail, forAgent = false) {
477
480
  "origin",
478
481
  "keys",
479
482
  "comments",
483
+ "related",
480
484
  ]);
481
485
  }
482
- const base = pickFields(result, [
486
+ const baseFields = [
483
487
  "type",
484
488
  "name",
485
489
  "origin",
@@ -502,11 +506,15 @@ export function shapeShowOutput(result, detail, forAgent = false) {
502
506
  "activeRun",
503
507
  "keys",
504
508
  "comments",
509
+ "related",
505
510
  // path and editable are always projected so JSON consumers can locate and
506
511
  // edit the asset without needing --detail full (QA #7).
507
- "path",
512
+ // Exception: vault assets omit path to avoid leaking absolute disk paths
513
+ // into structured JSON output (security fix M3).
514
+ ...(result.type === "vault" ? [] : ["path"]),
508
515
  "editable",
509
- ]);
516
+ ];
517
+ const base = pickFields(result, baseFields);
510
518
  if (detail !== "full") {
511
519
  return base;
512
520
  }
@@ -38,6 +38,13 @@ export function formatPlain(command, result, detail) {
38
38
  case "index": {
39
39
  const indexResult = result;
40
40
  let out = `Indexed ${indexResult.totalEntries ?? 0} entries from ${indexResult.directoriesScanned ?? 0} directories (mode: ${indexResult.mode ?? "unknown"})`;
41
+ const graphQuality = indexResult.graphQuality;
42
+ if (graphQuality) {
43
+ const coverage = typeof graphQuality.extractionCoverage === "number"
44
+ ? `${Math.round(graphQuality.extractionCoverage * 100)}%`
45
+ : "n/a";
46
+ out += `\nGraph quality: entities ${graphQuality.entityCount ?? 0}, relations ${graphQuality.relationCount ?? 0}, coverage ${coverage}, density ${graphQuality.density ?? 0}`;
47
+ }
41
48
  const warnings = indexResult.warnings;
42
49
  if (Array.isArray(warnings) && warnings.length > 0) {
43
50
  out += `\nWarnings (${warnings.length}):`;
@@ -221,6 +228,31 @@ export function formatPlain(command, result, detail) {
221
228
  case "distill": {
222
229
  return formatDistillPlain(r);
223
230
  }
231
+ case "graph-summary":
232
+ return formatGraphSummaryPlain(r);
233
+ case "graph-entities":
234
+ return formatGraphEntitiesPlain(r);
235
+ case "graph-relations":
236
+ return formatGraphRelationsPlain(r);
237
+ case "graph-related":
238
+ return formatGraphRelatedPlain(r);
239
+ case "graph-export":
240
+ return formatGraphExportPlain(r);
241
+ case "improve": {
242
+ return formatImprovePlain(r);
243
+ }
244
+ case "consolidate": {
245
+ return formatConsolidatePlain(r);
246
+ }
247
+ // Output shape registration for `akm agent <profile>` (#agent-dispatch).
248
+ // In interactive mode stdout/stderr are empty (they went to the TTY), so
249
+ // we print only the profile name and exit status. In captured mode we
250
+ // emit stdout first, then stderr, then the exit code summary.
251
+ case "agent-result": {
252
+ return formatAgentResultPlain(r);
253
+ }
254
+ case "health":
255
+ return formatHealthPlain(r);
224
256
  case "info":
225
257
  return formatInfoPlain(r);
226
258
  case "config":
@@ -249,7 +281,7 @@ export function formatPlain(command, result, detail) {
249
281
  case "vault-list":
250
282
  return formatVaultListPlain(r);
251
283
  case "vault-create":
252
- return `Created vault ${String(r.ref ?? "?")} at ${String(r.path ?? "?")}`;
284
+ return `Created vault ${String(r.ref ?? "?")}`;
253
285
  case "vault-set":
254
286
  return `Set ${String(r.key ?? "?")} in ${String(r.ref ?? "?")} (value not displayed)`;
255
287
  case "vault-unset": {
@@ -299,6 +331,50 @@ export function formatInfoPlain(r) {
299
331
  return JSON.stringify(r, null, 2);
300
332
  return lines.join("\n");
301
333
  }
334
+ export function formatHealthPlain(r) {
335
+ const lines = [];
336
+ lines.push(`health: ${String(r.status ?? "unknown")}`);
337
+ if (typeof r.since === "string")
338
+ lines.push(`since: ${r.since}`);
339
+ const metrics = typeof r.metrics === "object" && r.metrics !== null ? r.metrics : undefined;
340
+ if (metrics) {
341
+ lines.push("metrics:");
342
+ lines.push(` taskFailRate: ${String(metrics.taskFailRate ?? 0)}`);
343
+ lines.push(` agentFailureRate: ${String(metrics.agentFailureRate ?? 0)}`);
344
+ lines.push(` stuckActiveRuns: ${String(metrics.stuckActiveRuns ?? 0)}`);
345
+ lines.push(` logBackingRate: ${String(metrics.logBackingRate ?? 0)}`);
346
+ lines.push(` probeRoundTripMs: ${String(metrics.probeRoundTripMs ?? "null")}`);
347
+ }
348
+ const improve = typeof r.improve === "object" && r.improve !== null ? r.improve : undefined;
349
+ if (improve) {
350
+ const actions = typeof improve.actions === "object" && improve.actions !== null
351
+ ? improve.actions
352
+ : {};
353
+ lines.push("improve:");
354
+ lines.push(` invoked: ${String(improve.invoked ?? 0)}`);
355
+ lines.push(` completed: ${String(improve.completed ?? 0)}`);
356
+ lines.push(` skipped: ${String(improve.skipped ?? 0)}`);
357
+ lines.push(` plannedRefs: ${String(improve.plannedRefs ?? 0)}`);
358
+ lines.push(` actions: reflect=${String(actions.reflect ?? 0)} distill=${String(actions.distill ?? 0)} distillSkipped=${String(actions.distillSkipped ?? 0)} memoryPrune=${String(actions.memoryPrune ?? 0)} memoryInference=${String(actions.memoryInference ?? 0)} graphExtraction=${String(actions.graphExtraction ?? 0)} error=${String(actions.error ?? 0)}`);
359
+ lines.push(` coverageGapCount: ${String(improve.coverageGapCount ?? 0)}`);
360
+ lines.push(` executionLogCandidateCount: ${String(improve.executionLogCandidateCount ?? 0)}`);
361
+ lines.push(` deadUrlCount: ${String(improve.deadUrlCount ?? 0)}`);
362
+ }
363
+ const sections = [
364
+ ["hardChecks", r.hardChecks],
365
+ ["advisories", r.advisories],
366
+ ];
367
+ for (const [label, value] of sections) {
368
+ const checks = Array.isArray(value) ? value : [];
369
+ if (checks.length === 0)
370
+ continue;
371
+ lines.push(`${label}:`);
372
+ for (const check of checks) {
373
+ lines.push(` - [${String(check.status ?? "unknown")}] ${String(check.name ?? "check")}: ${String(check.message ?? "")}`);
374
+ }
375
+ }
376
+ return lines.join("\n");
377
+ }
302
378
  export function formatConfigPlain(r) {
303
379
  // Recursive flattener: prints `key=value` lines, and nested objects as
304
380
  // `parent.child=value`. Arrays render as JSON for compactness.
@@ -440,6 +516,57 @@ export function formatWorkflowValidatePlain(r) {
440
516
  const stepCount = typeof r.stepCount === "number" ? r.stepCount : 0;
441
517
  return `workflow validate: ok — ${title || pathValue} (${stepCount} step(s))`;
442
518
  }
519
+ export function formatGraphSummaryPlain(r) {
520
+ const lines = [
521
+ `Graph: ${String(r.graphPath ?? "?")}`,
522
+ `Generated: ${String(r.generatedAt ?? "?")}`,
523
+ `Files: ${String(r.fileCount ?? 0)} Entities: ${String(r.entityCount ?? 0)} Relations: ${String(r.relationCount ?? 0)}`,
524
+ ];
525
+ const quality = r.quality;
526
+ if (quality) {
527
+ const coverage = typeof quality.extractionCoverage === "number" ? `${Math.round(quality.extractionCoverage * 100)}%` : "n/a";
528
+ lines.push(`Coverage: ${coverage} Density: ${String(quality.density ?? 0)}`);
529
+ }
530
+ return lines.join("\n");
531
+ }
532
+ export function formatGraphEntitiesPlain(r) {
533
+ const entities = Array.isArray(r.entities) ? r.entities : [];
534
+ if (entities.length === 0)
535
+ return "No entities found in graph.";
536
+ const lines = [`Entities (${String(r.total ?? entities.length)} total):`];
537
+ for (const entity of entities) {
538
+ lines.push(`- ${String(entity.name ?? "?")} (${String(entity.fileCount ?? 0)} files)`);
539
+ }
540
+ return lines.join("\n");
541
+ }
542
+ export function formatGraphRelationsPlain(r) {
543
+ const relations = Array.isArray(r.relations) ? r.relations : [];
544
+ if (relations.length === 0)
545
+ return "No relations found in graph.";
546
+ const lines = [`Relations (${String(r.total ?? relations.length)} total):`];
547
+ for (const relation of relations) {
548
+ const type = relation.type ? ` [${String(relation.type)}]` : "";
549
+ lines.push(`- ${String(relation.from ?? "?")} -> ${String(relation.to ?? "?")}${type} x${String(relation.count ?? 0)}`);
550
+ }
551
+ return lines.join("\n");
552
+ }
553
+ export function formatGraphRelatedPlain(r) {
554
+ const related = Array.isArray(r.related) ? r.related : [];
555
+ if (related.length === 0)
556
+ return String(r.tip ?? "No related graph neighbors were found.");
557
+ const lines = [`Related (${String(r.total ?? related.length)} total) for ${String(r.ref ?? "?")}:`];
558
+ for (const hit of related) {
559
+ const shared = Array.isArray(hit.sharedEntities) ? hit.sharedEntities.map(String).join(", ") : "";
560
+ lines.push(`- ${String(hit.type ?? "?")}: ${formatRelatedLabel(hit)}`);
561
+ if (shared)
562
+ lines.push(` shared: ${shared}`);
563
+ lines.push(` relationCount: ${String(hit.relationCount ?? 0)}`);
564
+ }
565
+ return lines.join("\n");
566
+ }
567
+ export function formatGraphExportPlain(r) {
568
+ return `Exported graph (${String(r.format ?? "json")}, ${String(r.bytes ?? 0)} bytes) to ${String(r.outPath ?? "?")}`;
569
+ }
443
570
  export function formatProposalProducerPlain(command, r) {
444
571
  if (r.ok === false) {
445
572
  const reason = String(r.reason ?? "unknown");
@@ -464,7 +591,7 @@ export function formatProposalListPlain(r) {
464
591
  const proposals = Array.isArray(r.proposals) ? r.proposals : [];
465
592
  const total = typeof r.totalCount === "number" ? r.totalCount : proposals.length;
466
593
  if (proposals.length === 0) {
467
- return `${total} proposal(s).\nNo proposals.\nGenerate one with \`akm reflect <ref>\`, \`akm propose <type> <name> --task ...\`, or \`akm distill <ref>\`.`;
594
+ return `${total} proposal(s).\nNo proposals.\nGenerate one with \`akm improve\`, \`akm propose <type> <name> --task ...\`, or \`akm improve <ref>\`.`;
468
595
  }
469
596
  const lines = [`${total} proposal(s)`, ""];
470
597
  for (const p of proposals) {
@@ -529,7 +656,7 @@ export function formatDistillPlain(r) {
529
656
  const lessonRef = String(r.lessonRef ?? "?");
530
657
  if (outcome === "queued") {
531
658
  const id = String(r.proposalId ?? "?");
532
- return `Distilled ${inputRef} → proposal ${id} (${lessonRef}). Run \`akm proposal show ${id}\` to review.`;
659
+ return `Distilled ${inputRef} → proposal ${id} (${lessonRef}). Run \`akm show proposal ${id}\` to review.`;
533
660
  }
534
661
  if (outcome === "validation_failed") {
535
662
  const findings = Array.isArray(r.findings) ? r.findings : [];
@@ -543,6 +670,50 @@ export function formatDistillPlain(r) {
543
670
  const message = typeof r.message === "string" ? r.message : "feature disabled or LLM unavailable";
544
671
  return `Distill skipped for ${inputRef}: ${message}`;
545
672
  }
673
+ export function formatConsolidatePlain(r) {
674
+ const processed = typeof r.processed === "number" ? r.processed : 0;
675
+ const merged = typeof r.merged === "number" ? r.merged : 0;
676
+ const deleted = typeof r.deleted === "number" ? r.deleted : 0;
677
+ const promoted = Array.isArray(r.promoted) ? r.promoted.length : 0;
678
+ const warnings = Array.isArray(r.warnings) ? r.warnings : [];
679
+ const lines = [];
680
+ if (r.dryRun === true) {
681
+ lines.push(`[consolidate] dry-run: ${processed} memories found, no AI call`);
682
+ }
683
+ else if (r.previewOnly === true) {
684
+ lines.push(`[consolidate] preview: processed=${processed}`);
685
+ const planned = Array.isArray(r.planned) ? r.planned : [];
686
+ for (const op of planned) {
687
+ if (op.op === "merge") {
688
+ lines.push(` merge: ${String(op.primary)} ← ${String(Array.isArray(op.secondaries) ? op.secondaries.join(", ") : "")}`);
689
+ }
690
+ else if (op.op === "delete") {
691
+ lines.push(` delete: ${String(op.ref)} (${String(op.reason ?? "")})`);
692
+ }
693
+ else if (op.op === "promote") {
694
+ lines.push(` promote: ${String(op.ref)} → ${String(op.knowledgeRef)}`);
695
+ }
696
+ }
697
+ }
698
+ else {
699
+ lines.push(`[consolidate] processed=${processed} merged=${merged} deleted=${deleted} promoted=${promoted}`);
700
+ }
701
+ for (const w of warnings) {
702
+ lines.push(` warning: ${w}`);
703
+ }
704
+ return lines.join("\n");
705
+ }
706
+ export function formatImprovePlain(r) {
707
+ const scope = r.scope ?? {};
708
+ const mode = String(scope.mode ?? "all");
709
+ const value = typeof scope.value === "string" ? ` ${scope.value}` : "";
710
+ const plannedRefs = Array.isArray(r.plannedRefs) ? r.plannedRefs.length : 0;
711
+ if (r.dryRun === true) {
712
+ return `Improve dry-run:${mode === "all" ? " all assets" : value} (${plannedRefs} planned ref(s))`;
713
+ }
714
+ const actions = Array.isArray(r.actions) ? r.actions.length : 0;
715
+ return `Improve:${mode === "all" ? " all assets" : value} queued ${actions} action(s) across ${plannedRefs} ref(s)`;
716
+ }
546
717
  export function formatProposalDiffPlain(r) {
547
718
  const header = r.isNew
548
719
  ? `# proposal ${String(r.id ?? "?")} (new asset: ${String(r.ref ?? "?")})`
@@ -552,18 +723,25 @@ export function formatProposalDiffPlain(r) {
552
723
  return `${header}\n(no changes)`;
553
724
  return `${header}\n${unified}`;
554
725
  }
555
- export function formatEventsPlain(r) {
556
- const events = Array.isArray(r.events) ? r.events : [];
557
- const headerParts = [];
726
+ /**
727
+ * Build the summary header line shared by formatEventsPlain and formatHistoryPlain.
728
+ * Accumulates ref/type/since label parts then appends the count label.
729
+ */
730
+ function buildEventHeader(r, countLabel, totalCount) {
731
+ const parts = [];
558
732
  if (typeof r.ref === "string" && r.ref)
559
- headerParts.push(`ref: ${r.ref}`);
733
+ parts.push(`ref: ${r.ref}`);
560
734
  if (typeof r.type === "string" && r.type)
561
- headerParts.push(`type: ${r.type}`);
735
+ parts.push(`type: ${r.type}`);
562
736
  if (typeof r.since === "string" && r.since)
563
- headerParts.push(`since: ${r.since}`);
737
+ parts.push(`since: ${r.since}`);
738
+ parts.push(`${totalCount} ${countLabel}`);
739
+ return parts.join(" ");
740
+ }
741
+ export function formatEventsPlain(r) {
742
+ const events = Array.isArray(r.events) ? r.events : [];
564
743
  const totalCount = typeof r.totalCount === "number" ? r.totalCount : events.length;
565
- headerParts.push(`${totalCount} event(s)`);
566
- const header = headerParts.join(" ");
744
+ const header = buildEventHeader(r, "event(s)", totalCount);
567
745
  if (events.length === 0) {
568
746
  return `${header}\nNo events.`;
569
747
  }
@@ -586,13 +764,8 @@ export function formatEventLine(event) {
586
764
  }
587
765
  export function formatHistoryPlain(r) {
588
766
  const entries = Array.isArray(r.entries) ? r.entries : [];
589
- const headerParts = [];
590
- if (typeof r.ref === "string" && r.ref)
591
- headerParts.push(`ref: ${r.ref}`);
592
- if (typeof r.since === "string" && r.since)
593
- headerParts.push(`since: ${r.since}`);
594
767
  const totalCount = typeof r.totalCount === "number" ? r.totalCount : entries.length;
595
- headerParts.push(`${totalCount} event(s)`);
768
+ const headerParts = [buildEventHeader(r, "event(s)", totalCount)];
596
769
  // Show active event sources so operators know which streams were consulted.
597
770
  if (Array.isArray(r.sources) && r.sources.length > 0) {
598
771
  headerParts.push(`sources: ${r.sources.join(", ")}`);
@@ -670,6 +843,19 @@ function formatShowPlain(r, detail) {
670
843
  if (r.schemaVersion !== undefined)
671
844
  lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
672
845
  }
846
+ const related = typeof r.related === "object" && r.related !== null ? r.related : undefined;
847
+ const relatedHits = related && Array.isArray(related.hits) ? related.hits : [];
848
+ if (related) {
849
+ lines.push("");
850
+ lines.push(`related: ${String(related.total ?? relatedHits.length)}`);
851
+ for (const hit of relatedHits) {
852
+ lines.push(` - ${String(hit.type ?? "?")}: ${formatRelatedLabel(hit)}`);
853
+ const shared = Array.isArray(hit.sharedEntities) ? hit.sharedEntities.map(String) : [];
854
+ if (shared.length > 0)
855
+ lines.push(` shared: ${shared.join(", ")}`);
856
+ lines.push(` relationCount: ${String(hit.relationCount ?? 0)}`);
857
+ }
858
+ }
673
859
  const payloads = [r.content, r.template, r.prompt].filter((value) => value != null).map(String);
674
860
  if (Array.isArray(r.steps) && r.steps.length > 0) {
675
861
  if (lines.length > 0)
@@ -786,6 +972,13 @@ function isCommandOutputSkill(lines) {
786
972
  const yamlCount = codeLines.filter((l) => yamlPattern.test(l)).length;
787
973
  return cliCount > yamlCount && cliCount > 0;
788
974
  }
975
+ function formatRelatedLabel(hit) {
976
+ const ref = typeof hit.ref === "string" ? hit.ref : undefined;
977
+ if (ref)
978
+ return ref;
979
+ const pathValue = typeof hit.path === "string" ? hit.path : "?";
980
+ return pathValue.split("/").pop() ?? pathValue;
981
+ }
789
982
  export function formatWorkflowListPlain(result) {
790
983
  const runs = Array.isArray(result.runs) ? result.runs : [];
791
984
  if (runs.length === 0) {
@@ -918,6 +1111,24 @@ export function formatSearchPlain(r, detail) {
918
1111
  if (Array.isArray(hit.warnings) && hit.warnings.length > 0) {
919
1112
  lines.push(` warnings: ${hit.warnings.join("; ")}`);
920
1113
  }
1114
+ const graph = typeof hit.graph === "object" && hit.graph !== null ? hit.graph : undefined;
1115
+ if (graph) {
1116
+ const entities = Array.isArray(graph.entities) ? graph.entities : [];
1117
+ if (entities.length > 0) {
1118
+ const matched = entities
1119
+ .filter((entity) => String(entity.kind ?? "") === "matched")
1120
+ .map((entity) => String(entity.name ?? "?"));
1121
+ const neighbors = entities
1122
+ .filter((entity) => String(entity.kind ?? "") !== "matched")
1123
+ .map((entity) => String(entity.name ?? "?"));
1124
+ lines.push(` graph: ${[
1125
+ matched.length > 0 ? `query match=${matched.join(", ")}` : undefined,
1126
+ neighbors.length > 0 ? `neighbors=${neighbors.join(", ")}` : undefined,
1127
+ ]
1128
+ .filter(Boolean)
1129
+ .join("; ")}`);
1130
+ }
1131
+ }
921
1132
  if (detail === "full") {
922
1133
  if (hit.path)
923
1134
  lines.push(` path: ${String(hit.path)}`);
@@ -1113,3 +1324,31 @@ export function formatCuratePlain(r, detail) {
1113
1324
  lines.push("To search further: akm search '<query>'");
1114
1325
  return lines.join("\n");
1115
1326
  }
1327
+ /**
1328
+ * Render the result of `akm agent <profile>`.
1329
+ *
1330
+ * Interactive mode: stdout and stderr are empty (output went to the TTY).
1331
+ * Print only the profile name and exit code so the caller knows the agent
1332
+ * finished.
1333
+ *
1334
+ * Captured mode: emit stdout (if non-empty), then stderr (if non-empty),
1335
+ * then the profile/exit-code summary line.
1336
+ */
1337
+ export function formatAgentResultPlain(r) {
1338
+ const profile = String(r.profileName ?? "agent");
1339
+ const exitCode = r.exitCode !== undefined && r.exitCode !== null ? Number(r.exitCode) : 0;
1340
+ const stdout = typeof r.stdout === "string" ? r.stdout : "";
1341
+ const stderr = typeof r.stderr === "string" ? r.stderr : "";
1342
+ // Interactive mode: both streams are empty.
1343
+ if (!stdout && !stderr) {
1344
+ return `[${profile}] agent exited with code ${exitCode}`;
1345
+ }
1346
+ // Captured mode: stream content + summary.
1347
+ const parts = [];
1348
+ if (stdout)
1349
+ parts.push(stdout.trimEnd());
1350
+ if (stderr)
1351
+ parts.push(stderr.trimEnd());
1352
+ parts.push(`[${profile}] agent exited with code ${exitCode}`);
1353
+ return parts.join("\n");
1354
+ }