akm-cli 0.7.4 → 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 (162) hide show
  1. package/{CHANGELOG.md → .github/CHANGELOG.md} +34 -1
  2. package/.github/LICENSE +374 -0
  3. package/dist/cli/parse-args.js +86 -0
  4. package/dist/cli.js +1223 -650
  5. package/dist/commands/agent-dispatch.js +107 -0
  6. package/dist/commands/agent-support.js +62 -0
  7. package/dist/commands/config-cli.js +68 -84
  8. package/dist/commands/consolidate.js +812 -0
  9. package/dist/commands/curate.js +1 -0
  10. package/dist/commands/distill-promotion-policy.js +658 -0
  11. package/dist/commands/distill.js +224 -39
  12. package/dist/commands/eval-cases.js +40 -0
  13. package/dist/commands/events.js +12 -24
  14. package/dist/commands/graph.js +222 -0
  15. package/dist/commands/health.js +376 -0
  16. package/dist/commands/help/help-accept.md +9 -0
  17. package/dist/commands/help/help-improve.md +53 -0
  18. package/dist/commands/help/help-proposals.md +15 -0
  19. package/dist/commands/help/help-propose.md +17 -0
  20. package/dist/commands/help/help-reject.md +8 -0
  21. package/dist/commands/history.js +3 -30
  22. package/dist/commands/improve.js +1161 -0
  23. package/dist/commands/info.js +2 -2
  24. package/dist/commands/init.js +2 -2
  25. package/dist/commands/install-audit.js +5 -1
  26. package/dist/commands/installed-stashes.js +118 -138
  27. package/dist/commands/knowledge.js +133 -0
  28. package/dist/commands/lint/agent-linter.js +46 -0
  29. package/dist/commands/lint/base-linter.js +291 -0
  30. package/dist/commands/lint/command-linter.js +46 -0
  31. package/dist/commands/lint/default-linter.js +13 -0
  32. package/dist/commands/lint/index.js +145 -0
  33. package/dist/commands/lint/knowledge-linter.js +13 -0
  34. package/dist/commands/lint/memory-linter.js +58 -0
  35. package/dist/commands/lint/registry.js +33 -0
  36. package/dist/commands/lint/skill-linter.js +42 -0
  37. package/dist/commands/lint/task-linter.js +47 -0
  38. package/dist/commands/lint/types.js +1 -0
  39. package/dist/commands/lint/vault-key-rules.js +67 -0
  40. package/dist/commands/lint/workflow-linter.js +53 -0
  41. package/dist/commands/lint.js +1 -0
  42. package/dist/commands/migration-help.js +2 -2
  43. package/dist/commands/proposal.js +8 -7
  44. package/dist/commands/propose.js +106 -43
  45. package/dist/commands/reflect.js +167 -41
  46. package/dist/commands/registry-search.js +2 -2
  47. package/dist/commands/remember.js +55 -1
  48. package/dist/commands/schema-repair.js +130 -0
  49. package/dist/commands/search.js +21 -5
  50. package/dist/commands/show.js +135 -55
  51. package/dist/commands/source-add.js +10 -10
  52. package/dist/commands/source-manage.js +11 -19
  53. package/dist/commands/tasks.js +385 -0
  54. package/dist/commands/url-checker.js +39 -0
  55. package/dist/commands/vault.js +173 -87
  56. package/dist/core/action-contributors.js +25 -0
  57. package/dist/core/asset-ref.js +4 -0
  58. package/dist/core/asset-registry.js +5 -17
  59. package/dist/core/asset-spec.js +11 -1
  60. package/dist/core/common.js +100 -0
  61. package/dist/core/concurrent.js +22 -0
  62. package/dist/core/config.js +240 -127
  63. package/dist/core/events.js +87 -123
  64. package/dist/core/frontmatter.js +0 -6
  65. package/dist/core/markdown.js +17 -0
  66. package/dist/core/memory-improve.js +678 -0
  67. package/dist/core/parse.js +155 -0
  68. package/dist/core/paths.js +101 -3
  69. package/dist/core/proposal-validators.js +61 -0
  70. package/dist/core/proposals.js +49 -38
  71. package/dist/core/state-db.js +731 -0
  72. package/dist/core/time.js +51 -0
  73. package/dist/core/warn.js +59 -1
  74. package/dist/indexer/db-search.js +86 -472
  75. package/dist/indexer/db.js +418 -59
  76. package/dist/indexer/ensure-index.js +133 -0
  77. package/dist/indexer/graph-boost.js +247 -94
  78. package/dist/indexer/graph-db.js +201 -0
  79. package/dist/indexer/graph-dedup.js +99 -0
  80. package/dist/indexer/graph-extraction.js +417 -74
  81. package/dist/indexer/index-context.js +10 -0
  82. package/dist/indexer/indexer.js +480 -298
  83. package/dist/indexer/llm-cache.js +47 -0
  84. package/dist/indexer/matchers.js +124 -160
  85. package/dist/indexer/memory-inference.js +63 -29
  86. package/dist/indexer/metadata-contributors.js +26 -0
  87. package/dist/indexer/metadata.js +196 -197
  88. package/dist/indexer/path-resolver.js +89 -0
  89. package/dist/indexer/ranking-contributors.js +204 -0
  90. package/dist/indexer/ranking.js +74 -0
  91. package/dist/indexer/search-hit-enrichers.js +22 -0
  92. package/dist/indexer/search-source.js +24 -9
  93. package/dist/indexer/semantic-status.js +2 -16
  94. package/dist/indexer/walker.js +25 -0
  95. package/dist/integrations/agent/builders.js +109 -0
  96. package/dist/integrations/agent/config.js +203 -3
  97. package/dist/integrations/agent/index.js +5 -2
  98. package/dist/integrations/agent/model-aliases.js +63 -0
  99. package/dist/integrations/agent/profiles.js +67 -5
  100. package/dist/integrations/agent/prompts.js +114 -29
  101. package/dist/integrations/agent/sdk-runner.js +120 -0
  102. package/dist/integrations/agent/spawn.js +158 -34
  103. package/dist/integrations/lockfile.js +10 -18
  104. package/dist/integrations/session-logs/index.js +65 -0
  105. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  106. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  107. package/dist/integrations/session-logs/types.js +1 -0
  108. package/dist/llm/call-ai.js +74 -0
  109. package/dist/llm/client.js +63 -86
  110. package/dist/llm/feature-gate.js +27 -16
  111. package/dist/llm/graph-extract.js +297 -64
  112. package/dist/llm/memory-infer.js +52 -71
  113. package/dist/llm/metadata-enhance.js +39 -22
  114. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  115. package/dist/output/cli-hints-full.md +277 -0
  116. package/dist/output/cli-hints-short.md +65 -0
  117. package/dist/output/cli-hints.js +2 -309
  118. package/dist/output/renderers.js +226 -257
  119. package/dist/output/shapes.js +109 -96
  120. package/dist/output/text.js +274 -36
  121. package/dist/registry/providers/skills-sh.js +61 -49
  122. package/dist/registry/providers/static-index.js +44 -48
  123. package/dist/registry/resolve.js +8 -16
  124. package/dist/setup/setup.js +510 -11
  125. package/dist/sources/provider-factory.js +2 -1
  126. package/dist/sources/providers/filesystem.js +16 -23
  127. package/dist/sources/providers/git.js +45 -4
  128. package/dist/sources/providers/website.js +15 -22
  129. package/dist/sources/website-ingest.js +4 -0
  130. package/dist/tasks/backends/cron.js +200 -0
  131. package/dist/tasks/backends/exec-utils.js +25 -0
  132. package/dist/tasks/backends/index.js +32 -0
  133. package/dist/tasks/backends/launchd-template.xml +19 -0
  134. package/dist/tasks/backends/launchd.js +184 -0
  135. package/dist/tasks/backends/schtasks-template.xml +29 -0
  136. package/dist/tasks/backends/schtasks.js +212 -0
  137. package/dist/tasks/parser.js +198 -0
  138. package/dist/tasks/resolveAkmBin.js +84 -0
  139. package/dist/tasks/runner.js +432 -0
  140. package/dist/tasks/schedule.js +208 -0
  141. package/dist/tasks/schema.js +13 -0
  142. package/dist/tasks/validator.js +59 -0
  143. package/dist/wiki/index-template.md +12 -0
  144. package/dist/wiki/ingest-workflow-template.md +54 -0
  145. package/dist/wiki/log-template.md +8 -0
  146. package/dist/wiki/schema-template.md +61 -0
  147. package/dist/wiki/wiki-templates.js +12 -0
  148. package/dist/wiki/wiki.js +10 -61
  149. package/dist/workflows/authoring.js +5 -25
  150. package/dist/workflows/db.js +9 -0
  151. package/dist/workflows/renderer.js +8 -3
  152. package/dist/workflows/runs.js +73 -88
  153. package/dist/workflows/scope-key.js +76 -0
  154. package/dist/workflows/validator.js +1 -1
  155. package/dist/workflows/workflow-template.md +24 -0
  156. package/docs/README.md +5 -2
  157. package/docs/migration/release-notes/0.7.0.md +1 -1
  158. package/docs/migration/release-notes/0.7.4.md +1 -1
  159. package/docs/migration/release-notes/0.7.5.md +20 -0
  160. package/docs/migration/release-notes/0.8.0.md +43 -0
  161. package/package.json +4 -3
  162. package/dist/templates/wiki-templates.js +0 -100
@@ -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.
@@ -404,22 +480,7 @@ export function formatRegistryBuildIndexPlain(r) {
404
480
  return `Wrote registry index ${version} (${total} kits) → ${outPath}`.replace(/\s+/g, " ").trim();
405
481
  }
406
482
  export function formatVaultListPlain(r) {
407
- // Single-vault listing: { ref, path, entries: [{ key, comment? }, ...] }
408
- if (typeof r.ref === "string" && Array.isArray(r.entries)) {
409
- const ref = r.ref;
410
- const entries = r.entries;
411
- if (entries.length === 0) {
412
- return `No keys in ${ref}. Set one with \`akm vault set ${ref} KEY=VALUE\`.`;
413
- }
414
- const lines = [ref];
415
- for (const e of entries) {
416
- const key = String(e.key ?? "?");
417
- const comment = typeof e.comment === "string" && e.comment ? ` # ${e.comment}` : "";
418
- lines.push(` ${key}${comment}`);
419
- }
420
- return lines.join("\n");
421
- }
422
- // Multi-vault listing: { vaults: [{ ref, path, keyCount }, ...] }
483
+ // Multi-vault listing: { vaults: [{ ref, path, keys }, ...] }
423
484
  const vaults = Array.isArray(r.vaults) ? r.vaults : [];
424
485
  if (vaults.length === 0) {
425
486
  return "No vaults. Create one with `akm vault create <name>` then `akm vault set vault:<name> KEY=VALUE`.";
@@ -427,8 +488,17 @@ export function formatVaultListPlain(r) {
427
488
  const lines = [];
428
489
  for (const v of vaults) {
429
490
  const ref = String(v.ref ?? "?");
430
- const keyCount = typeof v.keyCount === "number" ? v.keyCount : 0;
431
- lines.push(`${ref}\t${keyCount} key(s)`);
491
+ const keys = Array.isArray(v.keys) ? v.keys.map(String) : [];
492
+ if (lines.length > 0)
493
+ lines.push("");
494
+ lines.push(`## ${ref}`);
495
+ if (keys.length === 0) {
496
+ lines.push("- (no keys)");
497
+ continue;
498
+ }
499
+ for (const key of keys) {
500
+ lines.push(`- ${key}`);
501
+ }
432
502
  }
433
503
  return lines.join("\n");
434
504
  }
@@ -446,6 +516,57 @@ export function formatWorkflowValidatePlain(r) {
446
516
  const stepCount = typeof r.stepCount === "number" ? r.stepCount : 0;
447
517
  return `workflow validate: ok — ${title || pathValue} (${stepCount} step(s))`;
448
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
+ }
449
570
  export function formatProposalProducerPlain(command, r) {
450
571
  if (r.ok === false) {
451
572
  const reason = String(r.reason ?? "unknown");
@@ -470,7 +591,7 @@ export function formatProposalListPlain(r) {
470
591
  const proposals = Array.isArray(r.proposals) ? r.proposals : [];
471
592
  const total = typeof r.totalCount === "number" ? r.totalCount : proposals.length;
472
593
  if (proposals.length === 0) {
473
- 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>\`.`;
474
595
  }
475
596
  const lines = [`${total} proposal(s)`, ""];
476
597
  for (const p of proposals) {
@@ -535,7 +656,7 @@ export function formatDistillPlain(r) {
535
656
  const lessonRef = String(r.lessonRef ?? "?");
536
657
  if (outcome === "queued") {
537
658
  const id = String(r.proposalId ?? "?");
538
- 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.`;
539
660
  }
540
661
  if (outcome === "validation_failed") {
541
662
  const findings = Array.isArray(r.findings) ? r.findings : [];
@@ -549,6 +670,50 @@ export function formatDistillPlain(r) {
549
670
  const message = typeof r.message === "string" ? r.message : "feature disabled or LLM unavailable";
550
671
  return `Distill skipped for ${inputRef}: ${message}`;
551
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
+ }
552
717
  export function formatProposalDiffPlain(r) {
553
718
  const header = r.isNew
554
719
  ? `# proposal ${String(r.id ?? "?")} (new asset: ${String(r.ref ?? "?")})`
@@ -558,18 +723,25 @@ export function formatProposalDiffPlain(r) {
558
723
  return `${header}\n(no changes)`;
559
724
  return `${header}\n${unified}`;
560
725
  }
561
- export function formatEventsPlain(r) {
562
- const events = Array.isArray(r.events) ? r.events : [];
563
- 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 = [];
564
732
  if (typeof r.ref === "string" && r.ref)
565
- headerParts.push(`ref: ${r.ref}`);
733
+ parts.push(`ref: ${r.ref}`);
566
734
  if (typeof r.type === "string" && r.type)
567
- headerParts.push(`type: ${r.type}`);
735
+ parts.push(`type: ${r.type}`);
568
736
  if (typeof r.since === "string" && r.since)
569
- 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 : [];
570
743
  const totalCount = typeof r.totalCount === "number" ? r.totalCount : events.length;
571
- headerParts.push(`${totalCount} event(s)`);
572
- const header = headerParts.join(" ");
744
+ const header = buildEventHeader(r, "event(s)", totalCount);
573
745
  if (events.length === 0) {
574
746
  return `${header}\nNo events.`;
575
747
  }
@@ -592,13 +764,8 @@ export function formatEventLine(event) {
592
764
  }
593
765
  export function formatHistoryPlain(r) {
594
766
  const entries = Array.isArray(r.entries) ? r.entries : [];
595
- const headerParts = [];
596
- if (typeof r.ref === "string" && r.ref)
597
- headerParts.push(`ref: ${r.ref}`);
598
- if (typeof r.since === "string" && r.since)
599
- headerParts.push(`since: ${r.since}`);
600
767
  const totalCount = typeof r.totalCount === "number" ? r.totalCount : entries.length;
601
- headerParts.push(`${totalCount} event(s)`);
768
+ const headerParts = [buildEventHeader(r, "event(s)", totalCount)];
602
769
  // Show active event sources so operators know which streams were consulted.
603
770
  if (Array.isArray(r.sources) && r.sources.length > 0) {
604
771
  headerParts.push(`sources: ${r.sources.join(", ")}`);
@@ -676,6 +843,19 @@ function formatShowPlain(r, detail) {
676
843
  if (r.schemaVersion !== undefined)
677
844
  lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
678
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
+ }
679
859
  const payloads = [r.content, r.template, r.prompt].filter((value) => value != null).map(String);
680
860
  if (Array.isArray(r.steps) && r.steps.length > 0) {
681
861
  if (lines.length > 0)
@@ -792,10 +972,17 @@ function isCommandOutputSkill(lines) {
792
972
  const yamlCount = codeLines.filter((l) => yamlPattern.test(l)).length;
793
973
  return cliCount > yamlCount && cliCount > 0;
794
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
+ }
795
982
  export function formatWorkflowListPlain(result) {
796
983
  const runs = Array.isArray(result.runs) ? result.runs : [];
797
984
  if (runs.length === 0) {
798
- return "No workflow runs. Start one with `akm workflow next workflow:<name>` or author one with `akm workflow create <name>`.";
985
+ return "No workflow runs in the current working scope. Start one with `akm workflow next workflow:<name>` or author one with `akm workflow create <name>`.";
799
986
  }
800
987
  return runs
801
988
  .map((run) => {
@@ -906,6 +1093,8 @@ export function formatSearchPlain(r, detail) {
906
1093
  lines.push(` ref: ${String(hit.ref)}`);
907
1094
  if (hit.origin !== undefined)
908
1095
  lines.push(` origin: ${String(hit.origin)}`);
1096
+ if (Array.isArray(hit.keys) && hit.keys.length > 0)
1097
+ lines.push(` keys: ${hit.keys.join(", ")}`);
909
1098
  if (hit.size)
910
1099
  lines.push(` size: ${String(hit.size)}`);
911
1100
  if (hit.action)
@@ -922,6 +1111,24 @@ export function formatSearchPlain(r, detail) {
922
1111
  if (Array.isArray(hit.warnings) && hit.warnings.length > 0) {
923
1112
  lines.push(` warnings: ${hit.warnings.join("; ")}`);
924
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
+ }
925
1132
  if (detail === "full") {
926
1133
  if (hit.path)
927
1134
  lines.push(` path: ${String(hit.path)}`);
@@ -1088,6 +1295,9 @@ export function formatCuratePlain(r, detail) {
1088
1295
  lines.push(` ref: ${String(item.ref)}`);
1089
1296
  if (item.id)
1090
1297
  lines.push(` id: ${String(item.id)}`);
1298
+ if (Array.isArray(item.keys) && item.keys.length > 0) {
1299
+ lines.push(` keys: ${item.keys.join(", ")}`);
1300
+ }
1091
1301
  if (Array.isArray(item.parameters) && item.parameters.length > 0) {
1092
1302
  lines.push(` parameters: ${item.parameters.join(", ")}`);
1093
1303
  }
@@ -1114,3 +1324,31 @@ export function formatCuratePlain(r, detail) {
1114
1324
  lines.push("To search further: akm search '<query>'");
1115
1325
  return lines.join("\n");
1116
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
+ }
@@ -1,13 +1,9 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  import { fetchWithRetry } from "../../core/common";
4
- import { getRegistryIndexCacheDir } from "../../core/paths";
2
+ import { closeDatabase, getRegistryIndexCache, openDatabase, upsertRegistryIndexCache } from "../../indexer/db";
5
3
  import { registerProvider } from "../factory";
6
4
  // ── Constants ───────────────────────────────────────────────────────────────
7
5
  /** Per-query cache TTL in milliseconds (15 minutes). */
8
6
  const QUERY_CACHE_TTL_MS = 15 * 60 * 1000;
9
- /** Maximum age before query cache is considered stale but still usable (1 day). */
10
- const QUERY_CACHE_STALE_MS = 24 * 60 * 60 * 1000;
11
7
  // ── Provider class ──────────────────────────────────────────────────────────
12
8
  class SkillsShProvider {
13
9
  type = "skills-sh";
@@ -89,13 +85,33 @@ class SkillsShProvider {
89
85
  return ref.source === "github";
90
86
  }
91
87
  async fetchSkills(query, limit) {
92
- // Check per-query cache first
93
- const cachePath = this.queryCachePath(query, limit);
94
- const cached = this.readQueryCache(cachePath);
95
- if (cached && !isExpired(cached.mtime, QUERY_CACHE_TTL_MS)) {
96
- return cached.entries;
88
+ // Build a stable DB cache key for this query
89
+ const dbCacheKey = this.queryDbCacheKey(query, limit);
90
+ // ── Step 1: Try DB cache (index.db) ───────────────────────────────────
91
+ let db;
92
+ let dbCacheResult;
93
+ try {
94
+ db = openDatabase();
95
+ dbCacheResult = getRegistryIndexCache(db, dbCacheKey, QUERY_CACHE_TTL_MS);
96
+ }
97
+ catch {
98
+ // index.db not available yet (pre-migration install or test env) — fall through
99
+ }
100
+ if (dbCacheResult) {
101
+ try {
102
+ const parsed = JSON.parse(dbCacheResult.indexJson);
103
+ if (Array.isArray(parsed)) {
104
+ const entries = parsed.filter(isValidSkillsEntry);
105
+ if (db)
106
+ closeDatabase(db);
107
+ return entries;
108
+ }
109
+ }
110
+ catch {
111
+ /* corrupt DB entry — fall through */
112
+ }
97
113
  }
98
- // Fetch from API
114
+ // ── Step 2: Fetch from API ─────────────────────────────────────────────
99
115
  const baseUrl = this.config.url.replace(/\/+$/, "");
100
116
  const url = `${baseUrl}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`;
101
117
  try {
@@ -105,13 +121,40 @@ class SkillsShProvider {
105
121
  }
106
122
  const data = (await response.json());
107
123
  const entries = parseSkillsResponse(data);
108
- this.writeQueryCache(cachePath, entries);
124
+ // Write to DB cache (primary)
125
+ if (db) {
126
+ try {
127
+ upsertRegistryIndexCache(db, dbCacheKey, JSON.stringify(entries));
128
+ }
129
+ catch {
130
+ /* best-effort */
131
+ }
132
+ closeDatabase(db);
133
+ }
109
134
  return entries;
110
135
  }
111
136
  catch (err) {
112
- // Fall back to stale cache if available
113
- if (cached && !isExpired(cached.mtime, QUERY_CACHE_STALE_MS)) {
114
- return cached.entries;
137
+ if (db) {
138
+ try {
139
+ closeDatabase(db);
140
+ }
141
+ catch {
142
+ /* ignore */
143
+ }
144
+ }
145
+ // Fetch failed — use stale DB cache if available
146
+ if (dbCacheResult) {
147
+ try {
148
+ const parsed = JSON.parse(dbCacheResult.indexJson);
149
+ if (Array.isArray(parsed)) {
150
+ const entries = parsed.filter(isValidSkillsEntry);
151
+ if (entries.length > 0)
152
+ return entries;
153
+ }
154
+ }
155
+ catch {
156
+ /* ignore */
157
+ }
115
158
  }
116
159
  throw err;
117
160
  }
@@ -167,9 +210,8 @@ class SkillsShProvider {
167
210
  });
168
211
  return hits.length > 0 ? hits : undefined;
169
212
  }
170
- // ── Per-query cache ─────────────────────────────────────────────────────
171
- queryCachePath(query, limit) {
172
- const cacheDir = getRegistryIndexCacheDir();
213
+ // ── DB cache key ────────────────────────────────────────────────────────
214
+ queryDbCacheKey(query, limit) {
173
215
  const hasher = new Bun.CryptoHasher("md5");
174
216
  hasher.update(this.config.url);
175
217
  hasher.update("\0");
@@ -177,33 +219,7 @@ class SkillsShProvider {
177
219
  hasher.update("\0");
178
220
  hasher.update(String(limit));
179
221
  const hash = hasher.digest("hex");
180
- return path.join(cacheDir, `skills-sh-search-${hash}.json`);
181
- }
182
- readQueryCache(cachePath) {
183
- try {
184
- const stat = fs.statSync(cachePath);
185
- const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"));
186
- if (!Array.isArray(raw))
187
- return null;
188
- const entries = raw.filter(isValidSkillsEntry);
189
- return { entries, mtime: stat.mtimeMs };
190
- }
191
- catch {
192
- return null;
193
- }
194
- }
195
- writeQueryCache(cachePath, entries) {
196
- try {
197
- const dir = path.dirname(cachePath);
198
- fs.mkdirSync(dir, { recursive: true });
199
- const tmpPath = `${cachePath}.tmp.${process.pid}`;
200
- // 0o600: owner read/write only — cache may contain search terms tied to API keys
201
- fs.writeFileSync(tmpPath, JSON.stringify(entries), { encoding: "utf8", mode: 0o600 });
202
- fs.renameSync(tmpPath, cachePath);
203
- }
204
- catch {
205
- // Best-effort caching
206
- }
222
+ return `skills-sh:${hash}`;
207
223
  }
208
224
  }
209
225
  // ── Self-register ───────────────────────────────────────────────────────────
@@ -226,7 +242,3 @@ function isValidSkillsEntry(entry) {
226
242
  typeof obj.installs === "number" &&
227
243
  typeof obj.source === "string");
228
244
  }
229
- // ── Utilities ───────────────────────────────────────────────────────────────
230
- function isExpired(mtimeMs, ttlMs) {
231
- return Date.now() - mtimeMs > ttlMs;
232
- }