akm-cli 0.4.1 → 0.5.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/matchers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Built-in asset matchers for the akm file classification system.
3
3
  *
4
- * Four matchers are registered at module load time, each at a different
4
+ * Five matchers are registered at module load time, each at a different
5
5
  * specificity level. Extension and content determine type; directories are
6
6
  * optional specificity boosts, not requirements.
7
7
  *
@@ -15,6 +15,8 @@
15
15
  * and body content for agent/command signals; falls back to "knowledge"
16
16
  * at specificity 5 when no signals are found. Command signals (`agent`
17
17
  * frontmatter, `$ARGUMENTS`/`$1`-`$3` placeholders) return 18.
18
+ * - `wikiMatcher` (20) -- classifies any `.md` under `wikis/<name>/…` as
19
+ * `wiki`. Registered last so the later-wins tiebreaker beats agent at 20.
18
20
  */
19
21
  import { SCRIPT_EXTENSIONS } from "./asset-spec";
20
22
  import { registerMatcher } from "./file-context";
@@ -32,7 +34,9 @@ import { registerMatcher } from "./file-context";
32
34
  export function extensionMatcher(ctx) {
33
35
  // SKILL.md is a skill regardless of location — high specificity beats
34
36
  // smartMdMatcher's knowledge fallback and all directory-based matchers.
35
- if (ctx.fileName === "SKILL.md") {
37
+ // Exception: files under wikis/<name>/… are always wiki pages; the wiki
38
+ // directory is an authoritative signal that outranks the filename.
39
+ if (ctx.fileName === "SKILL.md" && !ctx.ancestorDirs.includes("wikis")) {
36
40
  return { type: "skill", specificity: 25, renderer: "skill-md" };
37
41
  }
38
42
  // Known script extensions (excluding .md, handled by smartMdMatcher)
@@ -68,9 +72,15 @@ export function directoryMatcher(ctx) {
68
72
  if (dir === "knowledge" && ext === ".md") {
69
73
  return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
70
74
  }
75
+ if (dir === "workflows" && ext === ".md") {
76
+ return { type: "workflow", specificity: 10, renderer: "workflow-md" };
77
+ }
71
78
  if (dir === "memories" && ext === ".md") {
72
79
  return { type: "memory", specificity: 10, renderer: "memory-md" };
73
80
  }
81
+ if (dir === "vaults" && (ctx.fileName === ".env" || ctx.fileName.endsWith(".env"))) {
82
+ return { type: "vault", specificity: 10, renderer: "vault-env" };
83
+ }
74
84
  }
75
85
  return null;
76
86
  }
@@ -98,9 +108,15 @@ export function parentDirHintMatcher(ctx) {
98
108
  if (parentDir === "knowledge" && ext === ".md") {
99
109
  return { type: "knowledge", specificity: 15, renderer: "knowledge-md" };
100
110
  }
111
+ if (parentDir === "workflows" && ext === ".md") {
112
+ return { type: "workflow", specificity: 15, renderer: "workflow-md" };
113
+ }
101
114
  if (parentDir === "memories" && ext === ".md") {
102
115
  return { type: "memory", specificity: 15, renderer: "memory-md" };
103
116
  }
117
+ if (parentDir === "vaults" && (fileName === ".env" || fileName.endsWith(".env"))) {
118
+ return { type: "vault", specificity: 15, renderer: "vault-env" };
119
+ }
104
120
  return null;
105
121
  }
106
122
  // ── smartMdMatcher (specificity: 20 / 18 / 8 / 5) ──────────────────────────
@@ -123,6 +139,14 @@ const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
123
139
  export function smartMdMatcher(ctx) {
124
140
  if (ctx.ext !== ".md")
125
141
  return null;
142
+ const body = ctx.content();
143
+ const hasWorkflowSignals = /^#\s+Workflow:\s+/m.test(body) &&
144
+ /^##\s+Step:\s+/m.test(body) &&
145
+ /^Step ID:\s+/m.test(body) &&
146
+ /^###\s+Instructions\s*$/m.test(body);
147
+ if (hasWorkflowSignals) {
148
+ return { type: "workflow", specificity: 19, renderer: "workflow-md" };
149
+ }
126
150
  const fm = ctx.frontmatter();
127
151
  if (fm) {
128
152
  // Agent-exclusive indicators: toolPolicy or tools
@@ -138,7 +162,6 @@ export function smartMdMatcher(ctx) {
138
162
  }
139
163
  // Command signal: body contains $ARGUMENTS or $1/$2/$3 placeholders.
140
164
  // These are definitively command template patterns (OpenCode convention).
141
- const body = ctx.content();
142
165
  if (COMMAND_PLACEHOLDER_RE.test(body)) {
143
166
  return { type: "command", specificity: 18, renderer: "command-md" };
144
167
  }
@@ -154,9 +177,38 @@ export function smartMdMatcher(ctx) {
154
177
  // Weak fallback: any .md file is assumed to be knowledge
155
178
  return { type: "knowledge", specificity: 5, renderer: "knowledge-md" };
156
179
  }
180
+ // ── wikiMatcher (specificity: 20) ──────────────────────────────────────────
181
+ /**
182
+ * Classify any `.md` file that lives under `wikis/<name>/…` as `wiki`.
183
+ *
184
+ * Registered AFTER `smartMdMatcher` so the registered-later-wins tiebreaker
185
+ * puts wiki ahead of agent at specificity 20. That means a wiki page with
186
+ * agent-style frontmatter (e.g. `tools:`) still classifies as a wiki page,
187
+ * not an agent. That's intentional — the directory is the authoritative
188
+ * signal: files under `wikis/` are wiki content.
189
+ *
190
+ * Requires at least one path segment after `wikis/` (the wiki name) — a
191
+ * stray `.md` at the bare `wikis/` root is not a wiki page.
192
+ */
193
+ export function wikiMatcher(ctx) {
194
+ if (ctx.ext !== ".md")
195
+ return null;
196
+ const idx = ctx.ancestorDirs.indexOf("wikis");
197
+ if (idx < 0)
198
+ return null;
199
+ if (idx + 1 >= ctx.ancestorDirs.length)
200
+ return null;
201
+ return { type: "wiki", specificity: 20, renderer: "wiki-md" };
202
+ }
157
203
  // ── Registration ────────────────────────────────────────────────────────────
158
204
  /** All built-in matchers in registration order (later wins ties). */
159
- const builtinMatchers = [extensionMatcher, directoryMatcher, parentDirHintMatcher, smartMdMatcher];
205
+ const builtinMatchers = [
206
+ extensionMatcher,
207
+ directoryMatcher,
208
+ parentDirHintMatcher,
209
+ smartMdMatcher,
210
+ wikiMatcher,
211
+ ];
160
212
  /**
161
213
  * Register all built-in matchers with the file-context registry.
162
214
  * Called once from the CLI entry point (or ensureBuiltinsRegistered).
package/dist/metadata.js CHANGED
@@ -133,6 +133,30 @@ export function validateStashEntry(entry) {
133
133
  result.cwd = e.cwd.trim();
134
134
  if (typeof e.fileSize === "number" && Number.isFinite(e.fileSize) && e.fileSize >= 0)
135
135
  result.fileSize = e.fileSize;
136
+ if (e.wikiRole === "schema" ||
137
+ e.wikiRole === "index" ||
138
+ e.wikiRole === "log" ||
139
+ e.wikiRole === "raw" ||
140
+ e.wikiRole === "page") {
141
+ result.wikiRole = e.wikiRole;
142
+ }
143
+ if (typeof e.pageKind === "string" && e.pageKind.trim().length > 0) {
144
+ result.pageKind = e.pageKind.trim();
145
+ }
146
+ if (Array.isArray(e.xrefs)) {
147
+ const filtered = e.xrefs
148
+ .filter((x) => typeof x === "string" && x.trim().length > 0)
149
+ .map((x) => x.trim());
150
+ if (filtered.length > 0)
151
+ result.xrefs = filtered;
152
+ }
153
+ if (Array.isArray(e.sources)) {
154
+ const filtered = e.sources
155
+ .filter((s) => typeof s === "string" && s.trim().length > 0)
156
+ .map((s) => s.trim());
157
+ if (filtered.length > 0)
158
+ result.sources = filtered;
159
+ }
136
160
  if (Array.isArray(e.parameters)) {
137
161
  const validated = e.parameters
138
162
  .filter((p) => {
@@ -196,6 +220,36 @@ export function extractCommandParameters(template) {
196
220
  }
197
221
  return params.length > 0 ? params : undefined;
198
222
  }
223
+ /**
224
+ * Extract wiki frontmatter fields (wikiRole, pageKind, xrefs, sources) from a parsed
225
+ * frontmatter block and apply them to the entry. Tolerates missing or malformed values.
226
+ */
227
+ export function applyWikiFrontmatter(entry, fmData) {
228
+ const role = fmData.wikiRole;
229
+ if (role === "schema" || role === "index" || role === "log" || role === "raw" || role === "page") {
230
+ entry.wikiRole = role;
231
+ }
232
+ const pageKind = fmData.pageKind;
233
+ if (typeof pageKind === "string" && pageKind.trim().length > 0) {
234
+ entry.pageKind = pageKind.trim();
235
+ }
236
+ const xrefs = fmData.xrefs;
237
+ if (Array.isArray(xrefs)) {
238
+ const filtered = xrefs
239
+ .filter((x) => typeof x === "string" && x.trim().length > 0)
240
+ .map((x) => x.trim());
241
+ if (filtered.length > 0)
242
+ entry.xrefs = filtered;
243
+ }
244
+ const sources = fmData.sources;
245
+ if (Array.isArray(sources)) {
246
+ const filtered = sources
247
+ .filter((s) => typeof s === "string" && s.trim().length > 0)
248
+ .map((s) => s.trim());
249
+ if (filtered.length > 0)
250
+ entry.sources = filtered;
251
+ }
252
+ }
199
253
  /**
200
254
  * Extract `@param` JSDoc tags from a script file's leading comment block.
201
255
  *
@@ -316,6 +370,8 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
316
370
  const fmParams = extractFrontmatterParameters(parsed.data);
317
371
  if (fmParams)
318
372
  entry.parameters = fmParams;
373
+ // Pass wiki-pattern frontmatter through onto the entry
374
+ applyWikiFrontmatter(entry, parsed.data);
319
375
  // Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
320
376
  if (entry.type === "command") {
321
377
  const cmdParams = extractCommandParameters(parsed.content);
@@ -324,8 +380,11 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
324
380
  }
325
381
  }
326
382
  }
327
- // Extract @param from script files
328
- if (ext !== ".md") {
383
+ // Extract @param from script files.
384
+ // Vault files (.env) are deliberately excluded — their contents are secrets
385
+ // and must never be parsed for @param or any other metadata that could
386
+ // embed a value into the entry.
387
+ if (ext !== ".md" && assetType !== "vault") {
329
388
  const scriptParams = extractScriptParameters(file);
330
389
  if (scriptParams)
331
390
  entry.parameters = scriptParams;
@@ -418,6 +477,8 @@ export async function generateMetadataFlat(stashRoot, files) {
418
477
  const fmParams = extractFrontmatterParameters(parsed.data);
419
478
  if (fmParams)
420
479
  entry.parameters = fmParams;
480
+ // Pass wiki-pattern frontmatter through onto the entry
481
+ applyWikiFrontmatter(entry, parsed.data);
421
482
  // Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
422
483
  if (entry.type === "command") {
423
484
  const cmdParams = extractCommandParameters(parsed.content);
@@ -426,8 +487,11 @@ export async function generateMetadataFlat(stashRoot, files) {
426
487
  }
427
488
  }
428
489
  }
429
- // Extract @param from script files
430
- if (ext !== ".md") {
490
+ // Extract @param from script files.
491
+ // Vault files (.env) are deliberately excluded — their contents are secrets
492
+ // and must never be parsed for @param or any other metadata that could
493
+ // embed a value into the entry.
494
+ if (ext !== ".md" && assetType !== "vault") {
431
495
  const scriptParams = extractScriptParameters(file, ctx.content());
432
496
  if (scriptParams)
433
497
  entry.parameters = scriptParams;
package/dist/paths.js CHANGED
@@ -68,6 +68,9 @@ export function getCacheDir() {
68
68
  export function getDbPath() {
69
69
  return path.join(getCacheDir(), "index.db");
70
70
  }
71
+ export function getWorkflowDbPath() {
72
+ return path.join(getCacheDir(), "workflow.db");
73
+ }
71
74
  export function getSemanticStatusPath() {
72
75
  return path.join(getCacheDir(), "semantic-status.json");
73
76
  }
@@ -20,6 +20,9 @@ export async function installRegistryRef(ref, options) {
20
20
  if (parsed.source === "git") {
21
21
  return installGitRegistryRef(parsed, config, options);
22
22
  }
23
+ if (parsed.source === "github") {
24
+ return installGithubRegistryRef(parsed, config, options);
25
+ }
23
26
  const resolved = await resolveRegistryArtifact(parsed);
24
27
  const registryLabels = deriveRegistryLabels({
25
28
  source: resolved.source,
@@ -38,7 +41,7 @@ export async function installRegistryRef(ref, options) {
38
41
  const cachedStashRoot = detectStashRoot(extractedDir);
39
42
  if (cachedStashRoot) {
40
43
  const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
41
- const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
44
+ const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
42
45
  return {
43
46
  id: resolved.id,
44
47
  source: resolved.source,
@@ -51,6 +54,7 @@ export async function installRegistryRef(ref, options) {
51
54
  extractedDir,
52
55
  stashRoot: cachedStashRoot,
53
56
  integrity,
57
+ writable: options?.writable,
54
58
  audit,
55
59
  };
56
60
  }
@@ -70,7 +74,7 @@ export async function installRegistryRef(ref, options) {
70
74
  verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
71
75
  integrity = await computeFileHash(archivePath);
72
76
  extractTarGzSecure(archivePath, extractedDir);
73
- audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
77
+ audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
74
78
  provisionalKitRoot = detectStashRoot(extractedDir);
75
79
  installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
76
80
  stashRoot = detectStashRoot(installRoot);
@@ -98,9 +102,24 @@ export async function installRegistryRef(ref, options) {
98
102
  extractedDir,
99
103
  stashRoot,
100
104
  integrity,
105
+ writable: options?.writable,
101
106
  audit,
102
107
  };
103
108
  }
109
+ async function installGithubRegistryRef(parsed, config, options) {
110
+ const gitParsed = {
111
+ source: "git",
112
+ ref: parsed.ref,
113
+ id: parsed.id,
114
+ url: `https://github.com/${parsed.owner}/${parsed.repo}.git`,
115
+ requestedRef: parsed.requestedRef,
116
+ };
117
+ const installed = await installGitRegistryRef(gitParsed, config, options);
118
+ return {
119
+ ...installed,
120
+ source: "github",
121
+ };
122
+ }
104
123
  async function installLocalRegistryRef(parsed, config, options) {
105
124
  const resolved = await resolveRegistryArtifact(parsed);
106
125
  const installedAt = (options?.now ?? new Date()).toISOString();
@@ -109,7 +128,7 @@ async function installLocalRegistryRef(parsed, config, options) {
109
128
  ref: resolved.ref,
110
129
  artifactUrl: resolved.artifactUrl,
111
130
  });
112
- const audit = runInstallAuditOrThrow(parsed.sourcePath, resolved.source, resolved.ref, registryLabels, config);
131
+ const audit = runInstallAuditOrThrow(parsed.sourcePath, resolved.source, resolved.ref, registryLabels, config, options);
113
132
  // For local directories, detect the stash root within the source path.
114
133
  // If no nested stash is found, the source path itself is used.
115
134
  const stashRoot = detectStashRoot(parsed.sourcePath);
@@ -124,6 +143,7 @@ async function installLocalRegistryRef(parsed, config, options) {
124
143
  cacheDir: parsed.sourcePath,
125
144
  extractedDir: parsed.sourcePath,
126
145
  stashRoot,
146
+ writable: options?.writable,
127
147
  audit,
128
148
  };
129
149
  }
@@ -148,7 +168,7 @@ async function installGitRegistryRef(parsed, config, options) {
148
168
  const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
149
169
  const stashRoot = detectStashRoot(installRoot);
150
170
  if (stashRoot) {
151
- const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
171
+ const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
152
172
  return {
153
173
  id: resolved.id,
154
174
  source: resolved.source,
@@ -160,6 +180,7 @@ async function installGitRegistryRef(parsed, config, options) {
160
180
  cacheDir,
161
181
  extractedDir,
162
182
  stashRoot,
183
+ writable: options?.writable,
163
184
  audit,
164
185
  };
165
186
  }
@@ -193,7 +214,7 @@ async function installGitRegistryRef(parsed, config, options) {
193
214
  copyDirectoryContents(cloneDir, extractedDir);
194
215
  // Clean up the clone dir
195
216
  fs.rmSync(cloneDir, { recursive: true, force: true });
196
- audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
217
+ audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
197
218
  provisionalKitRoot = detectStashRoot(extractedDir);
198
219
  installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
199
220
  stashRoot = detectStashRoot(installRoot);
@@ -220,6 +241,7 @@ async function installGitRegistryRef(parsed, config, options) {
220
241
  cacheDir,
221
242
  extractedDir,
222
243
  stashRoot,
244
+ writable: options?.writable,
223
245
  audit,
224
246
  };
225
247
  }
@@ -494,8 +516,15 @@ async function computeFileHash(filePath) {
494
516
  const hash = createHash("sha256").update(data).digest("hex");
495
517
  return `sha256:${hash}`;
496
518
  }
497
- function runInstallAuditOrThrow(rootDir, source, ref, registryLabels, config) {
498
- const audit = auditInstallCandidate({ rootDir, source, ref, registryLabels, config });
519
+ function runInstallAuditOrThrow(rootDir, source, ref, registryLabels, config, options) {
520
+ const audit = auditInstallCandidate({
521
+ rootDir,
522
+ source,
523
+ ref,
524
+ registryLabels,
525
+ config,
526
+ trustThisInstall: options?.trustThisInstall,
527
+ });
499
528
  if (audit.blocked) {
500
529
  throw new Error(formatInstallAuditFailure(ref, audit));
501
530
  }
@@ -273,6 +273,20 @@ async function resolveNpmArtifact(parsed) {
273
273
  };
274
274
  }
275
275
  async function resolveGithubArtifact(parsed) {
276
+ const gitUrl = `https://github.com/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}.git`;
277
+ // Prefer git-backed installs so private GitHub repos work with the user's
278
+ // normal git credential helper rather than requiring API-specific auth.
279
+ const gitResolvedRevision = resolveGitRevisionFromRemote(gitUrl, parsed.requestedRef);
280
+ if (gitResolvedRevision) {
281
+ return {
282
+ id: parsed.id,
283
+ source: parsed.source,
284
+ ref: parsed.ref,
285
+ artifactUrl: gitUrl,
286
+ resolvedVersion: parsed.requestedRef,
287
+ resolvedRevision: gitResolvedRevision,
288
+ };
289
+ }
276
290
  const headers = githubHeaders();
277
291
  if (parsed.requestedRef) {
278
292
  const commit = await tryFetchJson(`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(parsed.requestedRef)}`, headers);
@@ -315,6 +329,17 @@ async function resolveGithubArtifact(parsed) {
315
329
  resolvedRevision: asString(commit?.sha) ?? defaultBranch,
316
330
  };
317
331
  }
332
+ function resolveGitRevisionFromRemote(url, requestedRef) {
333
+ validateGitUrl(url);
334
+ const ref = requestedRef ?? "HEAD";
335
+ if (requestedRef)
336
+ validateGitRef(requestedRef);
337
+ const result = spawnSync("git", ["ls-remote", url, ref], { encoding: "utf8", timeout: 30_000 });
338
+ if (result.status !== 0)
339
+ return undefined;
340
+ const firstLine = result.stdout.trim().split(/\r?\n/)[0];
341
+ return firstLine?.split(/\s/)[0] || undefined;
342
+ }
318
343
  async function resolveGitArtifact(parsed) {
319
344
  validateGitUrl(parsed.url);
320
345
  const ref = parsed.requestedRef ?? "HEAD";
package/dist/renderers.js CHANGED
@@ -9,10 +9,14 @@
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
11
  import { hasErrnoCode } from "./common";
12
+ import { UsageError } from "./errors";
12
13
  import { registerRenderer } from "./file-context";
13
14
  import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
14
15
  import { extractFrontmatterOnly, extractLineRange, extractSection, formatToc, parseMarkdownToc } from "./markdown";
15
16
  import { extractDescriptionFromComments, loadStashFile } from "./metadata";
17
+ import { makeAssetRef } from "./stash-ref";
18
+ import { listKeys as listVaultKeys } from "./vault";
19
+ import { parseWorkflowMarkdown, WorkflowValidationError } from "./workflow-markdown";
16
20
  // ── Interpreter auto-detection map ───────────────────────────────────────────
17
21
  const INTERPRETER_MAP = {
18
22
  ".sh": "bash",
@@ -149,6 +153,12 @@ function deriveName(ctx) {
149
153
  const ext = path.extname(ctx.relPath);
150
154
  return ext ? ctx.relPath.slice(0, -ext.length) : ctx.relPath;
151
155
  }
156
+ function shellQuote(value) {
157
+ return `'${value.replace(/'/g, `'\\''`)}'`;
158
+ }
159
+ export function buildWorkflowAction(ref) {
160
+ return `Resume the active run or start a new run with \`akm workflow next ${shellQuote(ref)}\`.`;
161
+ }
152
162
  /**
153
163
  * Load the matching StashEntry for a file path from the directory's .stash.json.
154
164
  */
@@ -309,6 +319,85 @@ const knowledgeMdRenderer = {
309
319
  }
310
320
  },
311
321
  };
322
+ // ── 4b. wiki-md ──────────────────────────────────────────────────────────────
323
+ const WIKI_PAGE_ACTION = "Wiki page — read below. Use 'toc' to scan, 'section <heading>' for depth.";
324
+ const wikiMdRenderer = {
325
+ name: "wiki-md",
326
+ buildShowResponse(ctx) {
327
+ const name = deriveName(ctx);
328
+ const v = ctx.matchResult.meta?.view ?? { mode: "full" };
329
+ const content = ctx.content();
330
+ switch (v.mode) {
331
+ case "toc": {
332
+ const toc = parseMarkdownToc(content);
333
+ return {
334
+ type: "wiki",
335
+ name,
336
+ path: ctx.absPath,
337
+ action: WIKI_PAGE_ACTION,
338
+ content: formatToc(toc),
339
+ };
340
+ }
341
+ case "frontmatter": {
342
+ const fm = extractFrontmatterOnly(content);
343
+ return {
344
+ type: "wiki",
345
+ name,
346
+ path: ctx.absPath,
347
+ action: WIKI_PAGE_ACTION,
348
+ content: fm ?? "(no frontmatter)",
349
+ };
350
+ }
351
+ case "section": {
352
+ const section = extractSection(content, v.heading);
353
+ if (!section) {
354
+ return {
355
+ type: "wiki",
356
+ name,
357
+ path: ctx.absPath,
358
+ action: WIKI_PAGE_ACTION,
359
+ content: `Section "${v.heading}" not found in ${name}. Try \`akm show wiki:${name} toc\` to discover available headings.`,
360
+ };
361
+ }
362
+ return {
363
+ type: "wiki",
364
+ name,
365
+ path: ctx.absPath,
366
+ action: WIKI_PAGE_ACTION,
367
+ content: section.content,
368
+ };
369
+ }
370
+ case "lines": {
371
+ return {
372
+ type: "wiki",
373
+ name,
374
+ path: ctx.absPath,
375
+ action: WIKI_PAGE_ACTION,
376
+ content: extractLineRange(content, v.start, v.end),
377
+ };
378
+ }
379
+ default: {
380
+ return {
381
+ type: "wiki",
382
+ name,
383
+ path: ctx.absPath,
384
+ action: WIKI_PAGE_ACTION,
385
+ content,
386
+ };
387
+ }
388
+ }
389
+ },
390
+ extractMetadata(entry, ctx) {
391
+ try {
392
+ const toc = parseMarkdownToc(ctx.content());
393
+ if (toc.headings.length > 0)
394
+ entry.toc = toc.headings;
395
+ }
396
+ catch {
397
+ // Non-fatal: skip TOC if file can't be read
398
+ }
399
+ },
400
+ };
312
401
  // ── 5. memory-md ─────────────────────────────────────────────────────────────
313
402
  const memoryMdRenderer = {
314
403
  name: "memory-md",
@@ -323,7 +412,58 @@ const memoryMdRenderer = {
323
412
  };
324
413
  },
325
414
  };
326
- // ── 6. script-source ─────────────────────────────────────────────────────────
415
+ // ── 6. workflow-md ───────────────────────────────────────────────────────────
416
+ const workflowMdRenderer = {
417
+ name: "workflow-md",
418
+ buildShowResponse(ctx) {
419
+ const name = deriveName(ctx);
420
+ const workflow = parseWorkflowForRendering(ctx.content());
421
+ const ref = makeAssetRef("workflow", name, ctx.origin);
422
+ return {
423
+ type: "workflow",
424
+ name,
425
+ path: ctx.absPath,
426
+ action: buildWorkflowAction(ref),
427
+ description: workflow.description,
428
+ workflowTitle: workflow.title,
429
+ parameters: workflow.parameters?.map((parameter) => parameter.name),
430
+ workflowParameters: workflow.parameters,
431
+ steps: workflow.steps,
432
+ };
433
+ },
434
+ extractMetadata(entry, ctx) {
435
+ const workflow = parseWorkflowForRendering(ctx.content());
436
+ const hints = new Set(entry.searchHints ?? []);
437
+ hints.add(workflow.title);
438
+ for (const step of workflow.steps) {
439
+ hints.add(step.title);
440
+ hints.add(step.id);
441
+ hints.add(step.instructions);
442
+ for (const criterion of step.completionCriteria ?? []) {
443
+ hints.add(criterion);
444
+ }
445
+ }
446
+ entry.searchHints = Array.from(hints).filter(Boolean);
447
+ if (workflow.parameters?.length) {
448
+ entry.parameters = workflow.parameters.map((parameter) => ({
449
+ name: parameter.name,
450
+ ...(parameter.description ? { description: parameter.description } : {}),
451
+ }));
452
+ }
453
+ },
454
+ };
455
+ function parseWorkflowForRendering(content) {
456
+ try {
457
+ return parseWorkflowMarkdown(content);
458
+ }
459
+ catch (error) {
460
+ if (error instanceof WorkflowValidationError) {
461
+ throw new UsageError(error.message);
462
+ }
463
+ throw error;
464
+ }
465
+ }
466
+ // ── 7. script-source ─────────────────────────────────────────────────────────
327
467
  const scriptSourceRenderer = {
328
468
  name: "script-source",
329
469
  buildShowResponse(ctx) {
@@ -379,6 +519,43 @@ const scriptSourceRenderer = {
379
519
  }
380
520
  },
381
521
  };
522
+ // ── 8. vault-env ─────────────────────────────────────────────────────────────
523
+ /**
524
+ * Vault renderer. Returns ONLY key names and start-of-line comments — never
525
+ * values. Deliberately omits content/template/prompt so vault values cannot
526
+ * leak through `akm show`.
527
+ */
528
+ const vaultEnvRenderer = {
529
+ name: "vault-env",
530
+ buildShowResponse(ctx) {
531
+ const name = deriveName(ctx);
532
+ const { keys, comments } = listVaultKeys(ctx.absPath);
533
+ return {
534
+ type: "vault",
535
+ name,
536
+ path: ctx.absPath,
537
+ 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.',
538
+ description: comments.length > 0 ? comments.join("\n") : undefined,
539
+ keys,
540
+ comments,
541
+ };
542
+ },
543
+ extractMetadata(entry, ctx) {
544
+ // Re-derive from the file directly to guarantee no value ever transits
545
+ // through any other code path. Caller already short-circuits in
546
+ // generateMetadata{,Flat}, but this is defense in depth.
547
+ const { keys, comments } = listVaultKeys(ctx.absPath);
548
+ if (comments.length > 0 && !entry.description) {
549
+ entry.description = comments.join(" ").slice(0, 500);
550
+ entry.source = "comments";
551
+ entry.confidence = 0.7;
552
+ }
553
+ if (keys.length > 0) {
554
+ entry.searchHints = keys;
555
+ }
556
+ entry.tags = Array.from(new Set([...(entry.tags ?? []), "vault", "secrets"]));
557
+ },
558
+ };
382
559
  // ── Registration ─────────────────────────────────────────────────────────────
383
560
  /** All built-in renderers. */
384
561
  const builtinRenderers = [
@@ -386,8 +563,11 @@ const builtinRenderers = [
386
563
  commandMdRenderer,
387
564
  agentMdRenderer,
388
565
  knowledgeMdRenderer,
566
+ wikiMdRenderer,
389
567
  memoryMdRenderer,
568
+ workflowMdRenderer,
390
569
  scriptSourceRenderer,
570
+ vaultEnvRenderer,
391
571
  ];
392
572
  /**
393
573
  * Register all built-in renderers with the file-context registry.
@@ -399,4 +579,4 @@ export function registerBuiltinRenderers() {
399
579
  }
400
580
  }
401
581
  // ── Named exports for testing ────────────────────────────────────────────────
402
- export { agentMdRenderer, commandMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, skillMdRenderer, };
582
+ export { agentMdRenderer, commandMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, skillMdRenderer, vaultEnvRenderer, wikiMdRenderer, workflowMdRenderer, };
@@ -41,6 +41,10 @@ export function buildSearchFields(entry) {
41
41
  if (entry.intent.output)
42
42
  hintParts.push(entry.intent.output);
43
43
  }
44
+ if (entry.xrefs)
45
+ hintParts.push(entry.xrefs.join(" "));
46
+ if (entry.pageKind)
47
+ hintParts.push(entry.pageKind);
44
48
  const hints = hintParts.join(" ").toLowerCase();
45
49
  const contentParts = [];
46
50
  if (entry.toc) {