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
@@ -9,11 +9,12 @@
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
11
  import { listKeys as listVaultKeys } from "../commands/vault";
12
- import { hasErrnoCode } from "../core/common";
13
- import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
12
+ import { asNonEmptyString, hasErrnoCode } from "../core/common";
13
+ import { parseFrontmatter } from "../core/frontmatter";
14
14
  import { extractFrontmatterOnly, extractLineRange, extractSection, formatToc, parseMarkdownToc, } from "../core/markdown";
15
15
  import { registerRenderer } from "../indexer/file-context";
16
16
  import { extractCommentMetadata, extractDescriptionFromComments } from "../indexer/metadata";
17
+ import { registerMetadataContributor } from "../indexer/metadata-contributors";
17
18
  import { buildWorkflowAction, workflowMdRenderer } from "../workflows/renderer";
18
19
  // ── Interpreter auto-detection map ───────────────────────────────────────────
19
20
  const INTERPRETER_MAP = {
@@ -165,7 +166,7 @@ const skillMdRenderer = {
165
166
  name,
166
167
  path: ctx.absPath,
167
168
  action: "Read and follow the instructions below",
168
- description: toStringOrUndefined(parsed.data.description),
169
+ description: asNonEmptyString(parsed.data.description),
169
170
  ...(tags ? { tags } : {}),
170
171
  content: parsed.content,
171
172
  };
@@ -184,11 +185,11 @@ const commandMdRenderer = {
184
185
  name,
185
186
  path: ctx.absPath,
186
187
  action: "Fill $ARGUMENTS placeholders in the template, then dispatch",
187
- description: toStringOrUndefined(parsedMd.data.description),
188
+ description: asNonEmptyString(parsedMd.data.description),
188
189
  ...(tags ? { tags } : {}),
189
190
  template,
190
191
  modelHint: typeof parsedMd.data.model === "string" ? parsedMd.data.model : undefined,
191
- agent: toStringOrUndefined(parsedMd.data.agent),
192
+ agent: asNonEmptyString(parsedMd.data.agent),
192
193
  parameters: extractParameters(template),
193
194
  };
194
195
  },
@@ -204,168 +205,67 @@ const agentMdRenderer = {
204
205
  name,
205
206
  path: ctx.absPath,
206
207
  action: "Dispatch using the prompt below verbatim. Use modelHint and toolPolicy if present.",
207
- description: toStringOrUndefined(parsedMd.data.description),
208
+ description: asNonEmptyString(parsedMd.data.description),
208
209
  prompt: parsedMd.content,
209
210
  toolPolicy: parsedMd.data.tools,
210
211
  modelHint: typeof parsedMd.data.model === "string" ? parsedMd.data.model : undefined,
211
212
  };
212
213
  },
213
214
  };
214
- // ── 4. knowledge-md ──────────────────────────────────────────────────────────
215
- const knowledgeMdRenderer = {
216
- name: "knowledge-md",
217
- buildShowResponse(ctx) {
218
- const name = deriveName(ctx);
219
- const v = ctx.matchResult.meta?.view ?? { mode: "full" };
220
- const content = ctx.content();
221
- switch (v.mode) {
222
- case "toc": {
223
- const toc = parseMarkdownToc(content);
224
- return {
225
- type: "knowledge",
226
- name,
227
- path: ctx.absPath,
228
- action: "Reference material - read the content below. Use 'toc' view for large documents.",
229
- content: formatToc(toc),
230
- };
231
- }
232
- case "frontmatter": {
233
- const fm = extractFrontmatterOnly(content);
234
- return {
235
- type: "knowledge",
236
- name,
237
- path: ctx.absPath,
238
- action: "Reference material - read the content below. Use 'toc' view for large documents.",
239
- content: fm ?? "(no frontmatter)",
240
- };
241
- }
242
- case "section": {
243
- const section = extractSection(content, v.heading);
244
- if (!section) {
245
- return {
246
- type: "knowledge",
247
- name,
248
- path: ctx.absPath,
249
- action: "Reference material - read the content below. Use 'toc' view for large documents.",
250
- content: `Section "${v.heading}" not found in ${name}. Try \`akm show <ref> toc\` to discover available headings.`,
251
- };
252
- }
253
- return {
254
- type: "knowledge",
255
- name,
256
- path: ctx.absPath,
257
- action: "Reference material - read the content below. Use 'toc' view for large documents.",
258
- content: section.content,
259
- };
260
- }
261
- case "lines": {
262
- return {
263
- type: "knowledge",
264
- name,
265
- path: ctx.absPath,
266
- action: "Reference material - read the content below. Use 'toc' view for large documents.",
267
- content: extractLineRange(content, v.start, v.end),
268
- };
269
- }
270
- default: {
271
- return {
272
- type: "knowledge",
273
- name,
274
- path: ctx.absPath,
275
- action: "Reference material - read the content below. Use 'toc' view for large documents.",
276
- content,
277
- };
215
+ // ── 4. knowledge-md / wiki-md shared helper ───────────────────────────────────
216
+ const KNOWLEDGE_ACTION = "Reference material - read the content below. Use 'toc' view for large documents.";
217
+ const WIKI_PAGE_ACTION = "Wiki page — read below. Use 'toc' to scan, 'section <heading>' for depth.";
218
+ /**
219
+ * Shared implementation for knowledge-md and wiki-md `buildShowResponse`.
220
+ *
221
+ * Both renderers handle the same set of view modes (toc, frontmatter, section,
222
+ * lines, full). The only differences are the `type` discriminant and the
223
+ * section-not-found message. Extracting this helper eliminates ~90 lines of
224
+ * byte-for-byte duplication.
225
+ */
226
+ function buildMarkdownViewResponse(ctx, type, action) {
227
+ const name = deriveName(ctx);
228
+ const v = ctx.matchResult.meta?.view ?? { mode: "full" };
229
+ const content = ctx.content();
230
+ switch (v.mode) {
231
+ case "toc": {
232
+ const toc = parseMarkdownToc(content);
233
+ return { type, name, path: ctx.absPath, action, content: formatToc(toc) };
234
+ }
235
+ case "frontmatter": {
236
+ const fm = extractFrontmatterOnly(content);
237
+ return { type, name, path: ctx.absPath, action, content: fm ?? "(no frontmatter)" };
238
+ }
239
+ case "section": {
240
+ const section = extractSection(content, v.heading);
241
+ if (!section) {
242
+ const notFoundMsg = type === "wiki"
243
+ ? `Section "${v.heading}" not found in ${name}. Try \`akm show wiki:${name} toc\` to discover available headings.`
244
+ : `Section "${v.heading}" not found in ${name}. Try \`akm show <ref> toc\` to discover available headings.`;
245
+ return { type, name, path: ctx.absPath, action, content: notFoundMsg };
278
246
  }
247
+ return { type, name, path: ctx.absPath, action, content: section.content };
279
248
  }
280
- },
281
- extractMetadata(entry, ctx) {
282
- try {
283
- const toc = parseMarkdownToc(ctx.content());
284
- if (toc.headings.length > 0)
285
- entry.toc = toc.headings;
249
+ case "lines": {
250
+ return { type, name, path: ctx.absPath, action, content: extractLineRange(content, v.start, v.end) };
286
251
  }
287
- catch {
288
- // Non-fatal: skip TOC if file can't be read
252
+ default: {
253
+ return { type, name, path: ctx.absPath, action, content };
289
254
  }
255
+ }
256
+ }
257
+ // ── 4. knowledge-md ──────────────────────────────────────────────────────────
258
+ const knowledgeMdRenderer = {
259
+ name: "knowledge-md",
260
+ buildShowResponse(ctx) {
261
+ return buildMarkdownViewResponse(ctx, "knowledge", KNOWLEDGE_ACTION);
290
262
  },
291
263
  };
292
264
  // ── 4b. wiki-md ──────────────────────────────────────────────────────────────
293
- const WIKI_PAGE_ACTION = "Wiki page — read below. Use 'toc' to scan, 'section <heading>' for depth.";
294
265
  const wikiMdRenderer = {
295
266
  name: "wiki-md",
296
267
  buildShowResponse(ctx) {
297
- const name = deriveName(ctx);
298
- const v = ctx.matchResult.meta?.view ?? { mode: "full" };
299
- const content = ctx.content();
300
- switch (v.mode) {
301
- case "toc": {
302
- const toc = parseMarkdownToc(content);
303
- return {
304
- type: "wiki",
305
- name,
306
- path: ctx.absPath,
307
- action: WIKI_PAGE_ACTION,
308
- content: formatToc(toc),
309
- };
310
- }
311
- case "frontmatter": {
312
- const fm = extractFrontmatterOnly(content);
313
- return {
314
- type: "wiki",
315
- name,
316
- path: ctx.absPath,
317
- action: WIKI_PAGE_ACTION,
318
- content: fm ?? "(no frontmatter)",
319
- };
320
- }
321
- case "section": {
322
- const section = extractSection(content, v.heading);
323
- if (!section) {
324
- return {
325
- type: "wiki",
326
- name,
327
- path: ctx.absPath,
328
- action: WIKI_PAGE_ACTION,
329
- content: `Section "${v.heading}" not found in ${name}. Try \`akm show wiki:${name} toc\` to discover available headings.`,
330
- };
331
- }
332
- return {
333
- type: "wiki",
334
- name,
335
- path: ctx.absPath,
336
- action: WIKI_PAGE_ACTION,
337
- content: section.content,
338
- };
339
- }
340
- case "lines": {
341
- return {
342
- type: "wiki",
343
- name,
344
- path: ctx.absPath,
345
- action: WIKI_PAGE_ACTION,
346
- content: extractLineRange(content, v.start, v.end),
347
- };
348
- }
349
- default: {
350
- return {
351
- type: "wiki",
352
- name,
353
- path: ctx.absPath,
354
- action: WIKI_PAGE_ACTION,
355
- content,
356
- };
357
- }
358
- }
359
- },
360
- extractMetadata(entry, ctx) {
361
- try {
362
- const toc = parseMarkdownToc(ctx.content());
363
- if (toc.headings.length > 0)
364
- entry.toc = toc.headings;
365
- }
366
- catch {
367
- // Non-fatal: skip TOC if file can't be read
368
- }
268
+ return buildMarkdownViewResponse(ctx, "wiki", WIKI_PAGE_ACTION);
369
269
  },
370
270
  };
371
271
  // ── 4c. lesson-md ────────────────────────────────────────────────────────────
@@ -384,8 +284,8 @@ const lessonMdRenderer = {
384
284
  buildShowResponse(ctx) {
385
285
  const name = deriveName(ctx);
386
286
  const parsed = parseFrontmatter(ctx.content());
387
- const description = toStringOrUndefined(parsed.data.description);
388
- const whenToUse = toStringOrUndefined(parsed.data.when_to_use);
287
+ const description = asNonEmptyString(parsed.data.description);
288
+ const whenToUse = asNonEmptyString(parsed.data.when_to_use);
389
289
  const action = whenToUse
390
290
  ? `Apply this lesson when: ${whenToUse}`
391
291
  : "Apply this lesson when its `when_to_use` trigger matches the current task.";
@@ -398,33 +298,6 @@ const lessonMdRenderer = {
398
298
  content: parsed.content,
399
299
  };
400
300
  },
401
- extractMetadata(entry, ctx) {
402
- try {
403
- const parsed = parseFrontmatter(ctx.content());
404
- const fm = parsed.data;
405
- const desc = toStringOrUndefined(fm.description);
406
- if (desc && !entry.description) {
407
- entry.description = desc;
408
- entry.source = "frontmatter";
409
- entry.confidence = 0.9;
410
- }
411
- const whenToUse = toStringOrUndefined(fm.when_to_use);
412
- if (whenToUse) {
413
- const hints = new Set(entry.searchHints ?? []);
414
- hints.add(`when_to_use:${whenToUse}`);
415
- entry.searchHints = Array.from(hints).filter(Boolean);
416
- }
417
- if (Array.isArray(fm.tags) && fm.tags.length > 0) {
418
- const fmTags = fm.tags.filter((t) => typeof t === "string" && t.trim().length > 0);
419
- if (fmTags.length > 0) {
420
- entry.tags = Array.from(new Set([...(entry.tags ?? []), ...fmTags]));
421
- }
422
- }
423
- }
424
- catch {
425
- // Non-fatal: skip metadata extraction on parse error
426
- }
427
- },
428
301
  };
429
302
  // ── 5. memory-md ─────────────────────────────────────────────────────────────
430
303
  const memoryMdRenderer = {
@@ -439,58 +312,6 @@ const memoryMdRenderer = {
439
312
  content: ctx.content(),
440
313
  };
441
314
  },
442
- extractMetadata(entry, ctx) {
443
- try {
444
- const parsed = parseFrontmatter(ctx.content());
445
- const fm = parsed.data;
446
- // Description from frontmatter
447
- const desc = toStringOrUndefined(fm.description);
448
- if (desc && !entry.description) {
449
- entry.description = desc;
450
- entry.source = "frontmatter";
451
- entry.confidence = 0.9;
452
- }
453
- // Tags from frontmatter
454
- if (Array.isArray(fm.tags) && fm.tags.length > 0) {
455
- const fmTags = fm.tags.filter((t) => typeof t === "string" && t.trim().length > 0);
456
- if (fmTags.length > 0) {
457
- entry.tags = Array.from(new Set([...(entry.tags ?? []), ...fmTags]));
458
- }
459
- }
460
- // Build searchHints from structured memory metadata fields
461
- const hints = new Set(entry.searchHints ?? []);
462
- const source = toStringOrUndefined(fm.source);
463
- if (source)
464
- hints.add(source);
465
- // observed_at: prefer frontmatter value, fall back to file mtime
466
- const fmObservedAt = toStringOrUndefined(fm.observed_at);
467
- if (fmObservedAt) {
468
- hints.add(`observed_at:${fmObservedAt}`);
469
- }
470
- else {
471
- // mtime fallback: format as ISO date (YYYY-MM-DD)
472
- try {
473
- const mtime = ctx.stat().mtime;
474
- const isoDate = mtime.toISOString().slice(0, 10);
475
- hints.add(`observed_at:${isoDate}`);
476
- }
477
- catch {
478
- // Non-fatal: skip mtime fallback on stat error
479
- }
480
- }
481
- const expires = toStringOrUndefined(fm.expires);
482
- if (expires)
483
- hints.add(`expires:${expires}`);
484
- if (fm.subjective === true)
485
- hints.add("subjective");
486
- if (hints.size > 0) {
487
- entry.searchHints = Array.from(hints).filter(Boolean);
488
- }
489
- }
490
- catch {
491
- // Non-fatal: skip metadata extraction on error
492
- }
493
- },
494
315
  };
495
316
  // ── 6. workflow-md ───────────────────────────────────────────────────────────
496
317
  // Defined in src/workflows/renderer.ts and imported above.
@@ -537,16 +358,6 @@ const scriptSourceRenderer = {
537
358
  throw error;
538
359
  }
539
360
  },
540
- extractMetadata(entry, ctx) {
541
- if (ctx.ext !== ".md") {
542
- const commentDesc = extractDescriptionFromComments(ctx.absPath);
543
- if (commentDesc && !entry.description) {
544
- entry.description = commentDesc;
545
- entry.source = "comments";
546
- entry.confidence = 0.7;
547
- }
548
- }
549
- },
550
361
  };
551
362
  // ── 8. vault-env ─────────────────────────────────────────────────────────────
552
363
  /**
@@ -563,28 +374,185 @@ const vaultEnvRenderer = {
563
374
  type: "vault",
564
375
  name,
565
376
  path: ctx.absPath,
566
- action: 'Vault — keys + comments only. Use `eval "$(akm vault load <ref>)"` to load values into the current shell. Values stay on disk and are never written to akm\'s stdout.',
377
+ action: 'Vault — keys + comments only. Use `source "$(akm vault path <ref>)"` to load values into the current shell, or `akm vault run <ref[/KEY]> -- <command>` to run with injected env. Values stay on disk and are never written to akm\'s stdout.',
567
378
  description: comments.length > 0 ? comments.join("\n") : undefined,
568
379
  keys,
569
380
  comments,
570
381
  };
571
382
  },
572
- extractMetadata(entry, ctx) {
573
- // Re-derive from the file directly to guarantee no value ever transits
574
- // through any other code path. Caller already short-circuits in
575
- // generateMetadata{,Flat}, but this is defense in depth.
576
- const { keys, comments } = listVaultKeys(ctx.absPath);
577
- if (comments.length > 0 && !entry.description) {
578
- entry.description = comments.join(" ").slice(0, 500);
579
- entry.source = "comments";
580
- entry.confidence = 0.7;
581
- }
582
- if (keys.length > 0) {
583
- entry.searchHints = keys;
584
- }
585
- entry.tags = Array.from(new Set([...(entry.tags ?? []), "vault", "secrets"]));
383
+ enrichSearchHit(hit, _stashDir) {
384
+ const { keys } = listVaultKeys(hit.path);
385
+ if (keys.length > 0)
386
+ hit.keys = keys;
387
+ },
388
+ };
389
+ // ── 7. task-md ───────────────────────────────────────────────────────────────
390
+ const TASK_PAGE_ACTION = "Scheduled task — `akm tasks show <id>` for parsed details, `akm tasks run <id>` to invoke now.";
391
+ const taskMdRenderer = {
392
+ name: "task-md",
393
+ buildShowResponse(ctx) {
394
+ const name = deriveName(ctx);
395
+ return {
396
+ type: "task",
397
+ name,
398
+ path: ctx.absPath,
399
+ action: TASK_PAGE_ACTION,
400
+ content: ctx.content(),
401
+ };
586
402
  },
587
403
  };
404
+ function applyTocMetadata(entry, ctx) {
405
+ try {
406
+ const toc = parseMarkdownToc(ctx.content());
407
+ if (toc.headings.length > 0)
408
+ entry.toc = toc.headings;
409
+ }
410
+ catch {
411
+ // Non-fatal: skip TOC if file can't be read
412
+ }
413
+ }
414
+ /**
415
+ * Parse frontmatter, apply description (if not already set) and merge tags
416
+ * into `entry`. Returns the raw frontmatter data object so callers can access
417
+ * type-specific fields without re-parsing.
418
+ */
419
+ function applyFrontmatterDescriptionAndTags(entry, ctx) {
420
+ const parsed = parseFrontmatter(ctx.content());
421
+ const fm = parsed.data;
422
+ const desc = asNonEmptyString(fm.description);
423
+ if (desc && !entry.description) {
424
+ entry.description = desc;
425
+ entry.source = "frontmatter";
426
+ entry.confidence = 0.9;
427
+ }
428
+ if (Array.isArray(fm.tags) && fm.tags.length > 0) {
429
+ const fmTags = fm.tags.filter((t) => typeof t === "string" && t.trim().length > 0);
430
+ if (fmTags.length > 0) {
431
+ entry.tags = Array.from(new Set([...(entry.tags ?? []), ...fmTags]));
432
+ }
433
+ }
434
+ return fm;
435
+ }
436
+ function applyLessonMetadata(entry, ctx) {
437
+ try {
438
+ const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
439
+ const whenToUse = asNonEmptyString(fm.when_to_use);
440
+ if (whenToUse) {
441
+ const hints = new Set(entry.searchHints ?? []);
442
+ hints.add(`when_to_use:${whenToUse}`);
443
+ entry.searchHints = Array.from(hints).filter(Boolean);
444
+ }
445
+ }
446
+ catch {
447
+ // Non-fatal: skip metadata extraction on parse error
448
+ }
449
+ }
450
+ function applyMemoryMetadata(entry, ctx) {
451
+ try {
452
+ const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
453
+ const hints = new Set(entry.searchHints ?? []);
454
+ const source = asNonEmptyString(fm.source);
455
+ if (source)
456
+ hints.add(source);
457
+ const fmObservedAt = asNonEmptyString(fm.observed_at);
458
+ if (fmObservedAt) {
459
+ hints.add(`observed_at:${fmObservedAt}`);
460
+ }
461
+ else {
462
+ try {
463
+ const isoDate = ctx.stat().mtime.toISOString().slice(0, 10);
464
+ hints.add(`observed_at:${isoDate}`);
465
+ }
466
+ catch {
467
+ // Non-fatal: skip mtime fallback on stat error
468
+ }
469
+ }
470
+ const expires = asNonEmptyString(fm.expires);
471
+ if (expires)
472
+ hints.add(`expires:${expires}`);
473
+ if (fm.subjective === true)
474
+ hints.add("subjective");
475
+ if (hints.size > 0) {
476
+ entry.searchHints = Array.from(hints).filter(Boolean);
477
+ }
478
+ }
479
+ catch {
480
+ // Non-fatal: skip metadata extraction on error
481
+ }
482
+ }
483
+ function applyScriptMetadata(entry, ctx) {
484
+ if (ctx.ext === ".md")
485
+ return;
486
+ const commentDesc = extractDescriptionFromComments(ctx.absPath);
487
+ if (commentDesc && !entry.description) {
488
+ entry.description = commentDesc;
489
+ entry.source = "comments";
490
+ entry.confidence = 0.7;
491
+ }
492
+ }
493
+ function applyVaultMetadata(entry, ctx) {
494
+ const { keys, comments } = listVaultKeys(ctx.absPath);
495
+ if (comments.length > 0 && !entry.description) {
496
+ entry.description = comments.join(" ").slice(0, 500);
497
+ entry.source = "comments";
498
+ entry.confidence = 0.7;
499
+ }
500
+ if (keys.length > 0) {
501
+ entry.searchHints = keys;
502
+ }
503
+ entry.tags = Array.from(new Set([...(entry.tags ?? []), "vault", "secrets"]));
504
+ }
505
+ function applyTaskMetadata(entry, ctx) {
506
+ try {
507
+ const fm = applyFrontmatterDescriptionAndTags(entry, ctx);
508
+ entry.tags = Array.from(new Set([...(entry.tags ?? []), "task", "scheduled"]));
509
+ const hints = new Set(entry.searchHints ?? []);
510
+ const schedule = asNonEmptyString(fm.schedule);
511
+ if (schedule)
512
+ hints.add(`schedule:${schedule}`);
513
+ const workflow = asNonEmptyString(fm.workflow);
514
+ if (workflow)
515
+ hints.add(`workflow:${workflow}`);
516
+ const prompt = asNonEmptyString(fm.prompt);
517
+ if (prompt)
518
+ hints.add(`prompt:${prompt}`);
519
+ if (hints.size > 0)
520
+ entry.searchHints = Array.from(hints).filter(Boolean);
521
+ }
522
+ catch {
523
+ // Non-fatal: skip metadata extraction on error
524
+ }
525
+ }
526
+ registerMetadataContributor({
527
+ name: "toc-metadata",
528
+ appliesTo: ({ rendererName }) => rendererName === "knowledge-md" || rendererName === "wiki-md",
529
+ contribute: (entry, ctx) => applyTocMetadata(entry, ctx.renderContext),
530
+ });
531
+ registerMetadataContributor({
532
+ name: "lesson-frontmatter-metadata",
533
+ appliesTo: ({ rendererName }) => rendererName === "lesson-md",
534
+ contribute: (entry, ctx) => applyLessonMetadata(entry, ctx.renderContext),
535
+ });
536
+ registerMetadataContributor({
537
+ name: "memory-frontmatter-metadata",
538
+ appliesTo: ({ rendererName }) => rendererName === "memory-md",
539
+ contribute: (entry, ctx) => applyMemoryMetadata(entry, ctx.renderContext),
540
+ });
541
+ registerMetadataContributor({
542
+ name: "script-comment-metadata",
543
+ appliesTo: ({ rendererName }) => rendererName === "script-source",
544
+ contribute: (entry, ctx) => applyScriptMetadata(entry, ctx.renderContext),
545
+ });
546
+ registerMetadataContributor({
547
+ name: "vault-secret-metadata",
548
+ appliesTo: ({ rendererName }) => rendererName === "vault-env",
549
+ contribute: (entry, ctx) => applyVaultMetadata(entry, ctx.renderContext),
550
+ });
551
+ registerMetadataContributor({
552
+ name: "task-frontmatter-metadata",
553
+ appliesTo: ({ rendererName }) => rendererName === "task-md",
554
+ contribute: (entry, ctx) => applyTaskMetadata(entry, ctx.renderContext),
555
+ });
588
556
  // ── Registration ─────────────────────────────────────────────────────────────
589
557
  /** All built-in renderers. */
590
558
  const builtinRenderers = [
@@ -598,6 +566,7 @@ const builtinRenderers = [
598
566
  workflowMdRenderer,
599
567
  scriptSourceRenderer,
600
568
  vaultEnvRenderer,
569
+ taskMdRenderer,
601
570
  ];
602
571
  /**
603
572
  * Register all built-in renderers with the file-context registry.