akm-cli 0.5.0 → 0.6.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +32 -5
  2. package/dist/asset-registry.js +29 -5
  3. package/dist/asset-spec.js +12 -5
  4. package/dist/cli-hints.js +300 -0
  5. package/dist/cli.js +218 -1357
  6. package/dist/common.js +147 -50
  7. package/dist/config.js +224 -13
  8. package/dist/create-provider-registry.js +1 -1
  9. package/dist/curate.js +258 -0
  10. package/dist/{local-search.js → db-search.js} +30 -19
  11. package/dist/db.js +168 -62
  12. package/dist/embedder.js +49 -273
  13. package/dist/embedders/cache.js +47 -0
  14. package/dist/embedders/local.js +152 -0
  15. package/dist/embedders/remote.js +121 -0
  16. package/dist/embedders/types.js +39 -0
  17. package/dist/errors.js +14 -3
  18. package/dist/frontmatter.js +61 -7
  19. package/dist/indexer.js +38 -7
  20. package/dist/info.js +2 -2
  21. package/dist/install-audit.js +16 -1
  22. package/dist/{installed-kits.js → installed-stashes.js} +48 -22
  23. package/dist/llm-client.js +92 -0
  24. package/dist/llm.js +14 -126
  25. package/dist/lockfile.js +28 -1
  26. package/dist/matchers.js +1 -1
  27. package/dist/metadata-enhance.js +53 -0
  28. package/dist/migration-help.js +75 -44
  29. package/dist/output-context.js +77 -0
  30. package/dist/output-shapes.js +198 -0
  31. package/dist/output-text.js +520 -0
  32. package/dist/paths.js +4 -4
  33. package/dist/providers/index.js +11 -0
  34. package/dist/providers/skills-sh.js +1 -1
  35. package/dist/providers/static-index.js +47 -45
  36. package/dist/registry-build-index.js +36 -29
  37. package/dist/registry-factory.js +2 -2
  38. package/dist/registry-resolve.js +8 -4
  39. package/dist/registry-search.js +62 -5
  40. package/dist/remember.js +172 -0
  41. package/dist/renderers.js +52 -0
  42. package/dist/search-source.js +73 -42
  43. package/dist/setup-steps.js +45 -0
  44. package/dist/setup.js +149 -76
  45. package/dist/stash-add.js +94 -38
  46. package/dist/stash-clone.js +4 -4
  47. package/dist/stash-provider-factory.js +2 -2
  48. package/dist/stash-provider.js +3 -1
  49. package/dist/stash-providers/filesystem.js +31 -1
  50. package/dist/stash-providers/git.js +209 -8
  51. package/dist/stash-providers/index.js +1 -0
  52. package/dist/stash-providers/npm.js +159 -0
  53. package/dist/stash-providers/provider-utils.js +162 -0
  54. package/dist/stash-providers/sync-from-ref.js +45 -0
  55. package/dist/stash-providers/tar-utils.js +151 -0
  56. package/dist/stash-providers/website.js +80 -4
  57. package/dist/stash-resolve.js +5 -5
  58. package/dist/stash-search.js +4 -4
  59. package/dist/stash-show.js +3 -3
  60. package/dist/wiki.js +6 -6
  61. package/dist/workflow-authoring.js +12 -4
  62. package/dist/workflow-markdown.js +9 -0
  63. package/dist/workflow-runs.js +12 -2
  64. package/docs/README.md +30 -0
  65. package/docs/migration/release-notes/0.0.13.md +4 -0
  66. package/docs/migration/release-notes/0.1.0.md +6 -0
  67. package/docs/migration/release-notes/0.2.0.md +6 -0
  68. package/docs/migration/release-notes/0.3.0.md +5 -0
  69. package/docs/migration/release-notes/0.5.0.md +6 -0
  70. package/docs/migration/release-notes/0.6.0.md +29 -0
  71. package/docs/migration/release-notes/README.md +21 -0
  72. package/package.json +3 -2
  73. package/dist/registry-install.js +0 -532
  74. /package/dist/{kit-include.js → stash-include.js} +0 -0
package/dist/cli.js CHANGED
@@ -3,21 +3,26 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { defineCommand, runMain } from "citty";
5
5
  import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./asset-spec";
6
- import { isWithin, resolveStashDir } from "./common";
6
+ import { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "./cli-hints";
7
+ import { isWithin, resolveStashDir, tryReadStdinText } from "./common";
7
8
  import { generateBashCompletions, installBashCompletions } from "./completions";
8
9
  import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./config";
9
10
  import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
11
+ import { akmCurate } from "./curate";
10
12
  import { closeDatabase, openDatabase } from "./db";
11
13
  import { ConfigError, NotFoundError, UsageError } from "./errors";
12
14
  import { akmIndex } from "./indexer";
13
15
  import { assembleInfo } from "./info";
14
16
  import { akmInit } from "./init";
15
- import { formatInstallAuditSummary } from "./install-audit";
16
- import { akmListSources, akmRemove, akmUpdate } from "./installed-kits";
17
+ import { akmListSources, akmRemove, akmUpdate } from "./installed-stashes";
17
18
  import { renderMigrationHelp } from "./migration-help";
19
+ import { getOutputMode, initOutputMode, parseFlagValue } from "./output-context";
20
+ import { shapeForCommand } from "./output-shapes";
21
+ import { formatPlain, outputJsonl } from "./output-text";
18
22
  import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
19
23
  import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
20
24
  import { searchRegistry } from "./registry-search";
25
+ import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich } from "./remember";
21
26
  import { checkForUpdate, performUpgrade } from "./self-update";
22
27
  import { akmAdd } from "./stash-add";
23
28
  import { akmClone } from "./stash-clone";
@@ -32,54 +37,32 @@ import { setQuiet, warn } from "./warn";
32
37
  import { createWorkflowAsset, getWorkflowTemplate } from "./workflow-authoring";
33
38
  import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflow-cli";
34
39
  import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflow-runs";
35
- const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl"];
36
- const DETAIL_LEVELS = ["brief", "normal", "full", "summary"];
37
- const NORMAL_DESCRIPTION_LIMIT = 250;
38
40
  const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
39
- const CONTEXT_HUB_ALIAS_REF = "context-hub";
40
- const CONTEXT_HUB_ALIAS_URL = "https://github.com/andrewyng/context-hub";
41
41
  const SKILLS_SH_NAME = "skills.sh";
42
42
  const SKILLS_SH_URL = "https://skills.sh";
43
43
  const SKILLS_SH_PROVIDER = "skills-sh";
44
44
  import { stringify as yamlStringify } from "yaml";
45
- function parseOutputFormat(value) {
46
- if (!value)
47
- return undefined;
48
- if (OUTPUT_FORMATS.includes(value))
49
- return value;
50
- throw new UsageError(`Invalid value for --format: ${value}. Expected one of: ${OUTPUT_FORMATS.join("|")}`);
51
- }
52
- function parseDetailLevel(value) {
53
- if (!value)
54
- return undefined;
55
- if (DETAIL_LEVELS.includes(value))
56
- return value;
57
- throw new UsageError(`Invalid value for --detail: ${value}. Expected one of: ${DETAIL_LEVELS.join("|")}`);
58
- }
59
- function parseFlagValue(flag) {
45
+ /**
46
+ * Collect all occurrences of a repeatable flag from process.argv.
47
+ * Citty's StringArgDef only exposes the last value when a flag is repeated,
48
+ * so for repeatable CLI args (like `--tag foo --tag bar`) we read argv directly.
49
+ * Supports both `--flag value` and `--flag=value` forms.
50
+ */
51
+ function parseAllFlagValues(flag) {
52
+ const values = [];
60
53
  for (let i = 0; i < process.argv.length; i++) {
61
54
  const arg = process.argv[i];
62
- if (arg === flag)
63
- return process.argv[i + 1];
64
- if (arg.startsWith(`${flag}=`))
65
- return arg.slice(flag.length + 1);
55
+ if (arg === flag && i + 1 < process.argv.length) {
56
+ values.push(process.argv[i + 1]);
57
+ }
58
+ else if (arg.startsWith(`${flag}=`)) {
59
+ values.push(arg.slice(flag.length + 1));
60
+ }
66
61
  }
67
- return undefined;
68
- }
69
- // Uses process.argv directly because the global output() function (called by all
70
- // commands) needs this flag but doesn't have access to citty's parsed args.
71
- function hasBooleanFlag(flag) {
72
- return process.argv.some((arg) => arg === flag || arg === `${flag}=true`);
73
- }
74
- function resolveOutputMode() {
75
- const config = loadConfig();
76
- const format = parseOutputFormat(parseFlagValue("--format")) ?? config.output?.format ?? "json";
77
- const detail = parseDetailLevel(parseFlagValue("--detail")) ?? config.output?.detail ?? "brief";
78
- const forAgent = hasBooleanFlag("--for-agent");
79
- return { format, detail, forAgent };
62
+ return values;
80
63
  }
81
64
  function output(command, result) {
82
- const mode = resolveOutputMode();
65
+ const mode = getOutputMode();
83
66
  const shaped = shapeForCommand(command, result, mode.detail, mode.forAgent);
84
67
  if (mode.format === "jsonl") {
85
68
  outputJsonl(command, shaped);
@@ -99,936 +82,12 @@ function output(command, result) {
99
82
  }
100
83
  }
101
84
  }
102
- function outputJsonl(command, shaped) {
103
- if (command === "search" || command === "registry-search") {
104
- const r = shaped;
105
- const hits = Array.isArray(r.hits) ? r.hits : [];
106
- for (const hit of hits) {
107
- console.log(JSON.stringify(hit));
108
- }
109
- const registryHits = Array.isArray(r.registryHits) ? r.registryHits : [];
110
- for (const hit of registryHits) {
111
- console.log(JSON.stringify(hit));
112
- }
113
- return;
114
- }
115
- // For non-search commands, output the whole object as a single JSONL line
116
- console.log(JSON.stringify(shaped));
117
- }
118
- function shapeForCommand(command, result, detail, forAgent = false) {
119
- switch (command) {
120
- case "search":
121
- return shapeSearchOutput(result, detail, forAgent);
122
- case "registry-search":
123
- return shapeRegistrySearchOutput(result, detail);
124
- case "show":
125
- return shapeShowOutput(result, detail, forAgent);
126
- default:
127
- return result;
128
- }
129
- }
130
- function shapeSearchOutput(result, detail, forAgent = false) {
131
- const hits = Array.isArray(result.hits) ? result.hits : [];
132
- const registryHits = Array.isArray(result.registryHits) ? result.registryHits : [];
133
- const shapedHits = forAgent
134
- ? hits.map((hit) => shapeSearchHitForAgent(hit))
135
- : hits.map((hit) => shapeSearchHit(hit, detail));
136
- const shapedRegistryHits = forAgent
137
- ? registryHits.map((hit) => shapeSearchHitForAgent(hit))
138
- : registryHits.map((hit) => shapeSearchHit(hit, detail));
139
- if (forAgent) {
140
- return {
141
- hits: shapedHits,
142
- ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
143
- ...(result.tip ? { tip: result.tip } : {}),
144
- };
145
- }
146
- if (detail === "full") {
147
- return {
148
- schemaVersion: result.schemaVersion,
149
- stashDir: result.stashDir,
150
- source: result.source,
151
- hits: shapedHits,
152
- ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
153
- ...(result.semanticSearch ? { semanticSearch: result.semanticSearch } : {}),
154
- ...(result.tip ? { tip: result.tip } : {}),
155
- ...(result.warnings ? { warnings: result.warnings } : {}),
156
- ...(result.timing ? { timing: result.timing } : {}),
157
- };
158
- }
159
- return {
160
- hits: shapedHits,
161
- ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
162
- ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
163
- ...(result.tip ? { tip: result.tip } : {}),
164
- };
165
- }
166
- function shapeRegistrySearchOutput(result, detail) {
167
- const hits = Array.isArray(result.hits) ? result.hits : [];
168
- const assetHits = Array.isArray(result.assetHits) ? result.assetHits : [];
169
- // Shape kit hits as registry type
170
- const shapedKitHits = hits.map((hit) => shapeSearchHit({ ...hit, type: "registry" }, detail));
171
- // Shape asset hits by detail level
172
- const shapedAssetHits = assetHits.map((hit) => shapeAssetHit(hit, detail));
173
- const shaped = {
174
- hits: shapedKitHits,
175
- ...(shapedAssetHits.length > 0 ? { assetHits: shapedAssetHits } : {}),
176
- ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
177
- };
178
- if (detail === "full") {
179
- shaped.query = result.query;
180
- }
181
- return shaped;
182
- }
183
- function shapeAssetHit(hit, detail) {
184
- if (detail === "brief")
185
- return pickFields(hit, ["assetName", "assetType", "action", "estimatedTokens"]);
186
- if (detail === "normal") {
187
- return capDescription(pickFields(hit, ["assetName", "assetType", "description", "kit", "action", "estimatedTokens"]), NORMAL_DESCRIPTION_LIMIT);
188
- }
189
- return hit;
190
- }
191
- function shapeSearchHit(hit, detail) {
192
- if (hit.type === "registry") {
193
- if (detail === "brief")
194
- return pickFields(hit, ["name", "action"]);
195
- if (detail === "normal") {
196
- return capDescription(pickFields(hit, ["name", "description", "action", "curated"]), NORMAL_DESCRIPTION_LIMIT);
197
- }
198
- return hit;
199
- }
200
- // Stash hit (local or remote)
201
- if (detail === "brief")
202
- return pickFields(hit, ["type", "name", "action", "estimatedTokens"]);
203
- if (detail === "normal") {
204
- return capDescription(pickFields(hit, ["type", "name", "description", "action", "score", "estimatedTokens"]), NORMAL_DESCRIPTION_LIMIT);
205
- }
206
- return hit;
207
- }
208
- /** Agent-optimized search hit: only fields an LLM agent needs to decide and act */
209
- function shapeSearchHitForAgent(hit) {
210
- const picked = pickFields(hit, ["name", "ref", "type", "description", "action", "score", "estimatedTokens"]);
211
- return capDescription(picked, NORMAL_DESCRIPTION_LIMIT);
212
- }
213
- function capDescription(hit, limit) {
214
- if (typeof hit.description !== "string")
215
- return hit;
216
- return { ...hit, description: truncateDescription(hit.description, limit) };
217
- }
218
- function truncateDescription(description, limit) {
219
- const normalized = description.replace(/\s+/g, " ").trim();
220
- if (normalized.length <= limit)
221
- return normalized;
222
- const truncated = normalized.slice(0, limit - 1);
223
- const lastSpace = truncated.lastIndexOf(" ");
224
- const safe = lastSpace >= Math.floor(limit * 0.6) ? truncated.slice(0, lastSpace) : truncated;
225
- return `${safe.trimEnd()}...`;
226
- }
227
- function shapeShowOutput(result, detail, forAgent = false) {
228
- if (forAgent) {
229
- return pickFields(result, [
230
- "type",
231
- "name",
232
- "description",
233
- "action",
234
- "content",
235
- "template",
236
- "prompt",
237
- "run",
238
- "setup",
239
- "cwd",
240
- "toolPolicy",
241
- "modelHint",
242
- "agent",
243
- "parameters",
244
- "workflowTitle",
245
- "workflowParameters",
246
- "steps",
247
- "keys",
248
- "comments",
249
- ]);
250
- }
251
- if (detail === "summary") {
252
- return pickFields(result, [
253
- "type",
254
- "name",
255
- "description",
256
- "tags",
257
- "parameters",
258
- "workflowTitle",
259
- "action",
260
- "run",
261
- "origin",
262
- "keys",
263
- "comments",
264
- ]);
265
- }
266
- const base = pickFields(result, [
267
- "type",
268
- "name",
269
- "origin",
270
- "action",
271
- "description",
272
- "tags",
273
- "content",
274
- "template",
275
- "prompt",
276
- "toolPolicy",
277
- "modelHint",
278
- "agent",
279
- "parameters",
280
- "workflowTitle",
281
- "workflowParameters",
282
- "steps",
283
- "run",
284
- "setup",
285
- "cwd",
286
- "keys",
287
- "comments",
288
- ]);
289
- if (detail !== "full") {
290
- return base;
291
- }
292
- return {
293
- schemaVersion: 1,
294
- ...base,
295
- ...pickFields(result, ["path", "editable", "editHint"]),
296
- };
297
- }
298
- function pickFields(source, fields) {
299
- const result = {};
300
- for (const field of fields) {
301
- if (source[field] !== undefined) {
302
- result[field] = source[field];
303
- }
304
- }
305
- return result;
306
- }
307
- /**
308
- * Return a plain-text string for commands that are better as short messages,
309
- * or null to fall through to YAML output.
310
- */
311
- function formatPlain(command, result, detail) {
312
- const r = result;
313
- switch (command) {
314
- case "init": {
315
- let out = `Stash initialized at ${r.stashDir ?? "unknown"}`;
316
- if (r.configPath)
317
- out += `\nConfig saved to ${r.configPath}`;
318
- return out;
319
- }
320
- case "index": {
321
- const indexResult = result;
322
- let out = `Indexed ${indexResult.totalEntries ?? 0} entries from ${indexResult.directoriesScanned ?? 0} directories (mode: ${indexResult.mode ?? "unknown"})`;
323
- const warnings = indexResult.warnings;
324
- if (Array.isArray(warnings) && warnings.length > 0) {
325
- out += `\nWarnings (${warnings.length}):`;
326
- for (const message of warnings)
327
- out += `\n - ${String(message)}`;
328
- }
329
- const verification = indexResult.verification;
330
- if (verification?.ok === false && verification.message) {
331
- out += `\nVerification: ${String(verification.message)}`;
332
- }
333
- return out;
334
- }
335
- case "show": {
336
- const lines = [];
337
- if (r.type || r.name) {
338
- lines.push(`# ${String(r.type ?? "asset")}: ${String(r.name ?? "unknown")}`);
339
- }
340
- if (r.origin !== undefined)
341
- lines.push(`# origin: ${String(r.origin)}`);
342
- if (r.action)
343
- lines.push(`# ${String(r.action)}`);
344
- if (r.description)
345
- lines.push(`description: ${String(r.description)}`);
346
- if (r.workflowTitle)
347
- lines.push(`workflowTitle: ${String(r.workflowTitle)}`);
348
- if (r.agent)
349
- lines.push(`agent: ${String(r.agent)}`);
350
- if (Array.isArray(r.parameters) && r.parameters.length > 0)
351
- lines.push(`parameters: ${r.parameters.join(", ")}`);
352
- if (Array.isArray(r.workflowParameters) && r.workflowParameters.length > 0) {
353
- lines.push("workflowParameters:");
354
- for (const parameter of r.workflowParameters) {
355
- const name = typeof parameter.name === "string" ? parameter.name : "unknown";
356
- const description = typeof parameter.description === "string" && parameter.description.trim()
357
- ? `: ${parameter.description}`
358
- : "";
359
- lines.push(` - ${name}${description}`);
360
- }
361
- }
362
- if (r.modelHint != null)
363
- lines.push(`modelHint: ${String(r.modelHint)}`);
364
- if (r.toolPolicy != null)
365
- lines.push(`toolPolicy: ${JSON.stringify(r.toolPolicy)}`);
366
- if (r.run)
367
- lines.push(`run: ${String(r.run)}`);
368
- if (r.setup)
369
- lines.push(`setup: ${String(r.setup)}`);
370
- if (r.cwd)
371
- lines.push(`cwd: ${String(r.cwd)}`);
372
- if (detail === "full") {
373
- if (r.path)
374
- lines.push(`path: ${String(r.path)}`);
375
- if (r.editable !== undefined)
376
- lines.push(`editable: ${String(r.editable)}`);
377
- if (r.editHint)
378
- lines.push(`editHint: ${String(r.editHint)}`);
379
- if (r.schemaVersion !== undefined)
380
- lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
381
- }
382
- const payloads = [r.content, r.template, r.prompt].filter((value) => value != null).map(String);
383
- if (Array.isArray(r.steps) && r.steps.length > 0) {
384
- if (lines.length > 0)
385
- lines.push("");
386
- lines.push("steps:");
387
- for (const [index, step] of r.steps.entries()) {
388
- const title = typeof step.title === "string" ? step.title : "Untitled step";
389
- const id = typeof step.id === "string" ? step.id : "unknown";
390
- lines.push(` ${index + 1}. ${title} [${id}]`);
391
- if (typeof step.instructions === "string" && step.instructions.trim()) {
392
- lines.push(` instructions: ${step.instructions.replace(/\n+/g, " ").trim()}`);
393
- }
394
- if (Array.isArray(step.completionCriteria) && step.completionCriteria.length > 0) {
395
- lines.push(" completion:");
396
- for (const criterion of step.completionCriteria) {
397
- lines.push(` - ${String(criterion)}`);
398
- }
399
- }
400
- }
401
- }
402
- if (payloads.length > 0) {
403
- if (lines.length > 0)
404
- lines.push("");
405
- lines.push(...payloads);
406
- }
407
- return lines.length > 0 ? lines.join("\n") : null;
408
- }
409
- case "search": {
410
- return formatSearchPlain(r, detail);
411
- }
412
- case "curate": {
413
- return formatCuratePlain(r, detail);
414
- }
415
- case "wiki-list": {
416
- return formatWikiListPlain(r);
417
- }
418
- case "wiki-show": {
419
- return formatWikiShowPlain(r);
420
- }
421
- case "wiki-create": {
422
- return formatWikiCreatePlain(r);
423
- }
424
- case "wiki-remove": {
425
- return formatWikiRemovePlain(r);
426
- }
427
- case "wiki-pages": {
428
- return formatWikiPagesPlain(r);
429
- }
430
- case "wiki-stash": {
431
- return formatWikiStashPlain(r);
432
- }
433
- case "wiki-lint": {
434
- return formatWikiLintPlain(r);
435
- }
436
- case "wiki-ingest": {
437
- return formatWikiIngestPlain(r);
438
- }
439
- case "workflow-start":
440
- case "workflow-status":
441
- case "workflow-complete": {
442
- return formatWorkflowStatusPlain(r);
443
- }
444
- case "workflow-next": {
445
- return formatWorkflowNextPlain(r);
446
- }
447
- case "workflow-list": {
448
- return formatWorkflowListPlain(r);
449
- }
450
- case "workflow-create": {
451
- if (r.ref && r.path) {
452
- return `Created ${String(r.ref)} at ${String(r.path)}`;
453
- }
454
- return null;
455
- }
456
- case "list": {
457
- const sources = Array.isArray(r.sources) ? r.sources : [];
458
- if (sources.length === 0)
459
- return "No sources configured. Use `akm add` to add a source.";
460
- const lines = [];
461
- for (const src of sources) {
462
- const kind = typeof src.kind === "string" ? src.kind : "unknown";
463
- const name = typeof src.name === "string" ? src.name : "unnamed";
464
- const ver = typeof src.version === "string" ? ` v${src.version}` : "";
465
- const prov = typeof src.provider === "string" ? ` (${src.provider})` : "";
466
- const flags = [];
467
- if (typeof src.wiki === "string")
468
- flags.push(`wiki:${src.wiki}`);
469
- if (src.updatable === true)
470
- flags.push("updatable");
471
- if (src.writable === true)
472
- flags.push("writable");
473
- const flagText = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
474
- lines.push(`[${kind}] ${name}${ver}${prov}${flagText}`);
475
- }
476
- return lines.join("\n");
477
- }
478
- case "add": {
479
- const index = r.index;
480
- const scanned = index?.directoriesScanned ?? 0;
481
- const total = index?.totalEntries ?? 0;
482
- const lines = [`Installed ${r.ref} (${scanned} directories scanned, ${total} total assets indexed)`];
483
- const warnings = index?.warnings;
484
- if (Array.isArray(warnings) && warnings.length > 0) {
485
- lines.push(`Warnings (${warnings.length}):`);
486
- for (const message of warnings)
487
- lines.push(` - ${String(message)}`);
488
- }
489
- const installed = r.installed;
490
- const audit = installed?.audit;
491
- if (audit && typeof audit === "object") {
492
- lines.push(formatInstallAuditSummary(audit));
493
- }
494
- return lines.join("\n");
495
- }
496
- case "remove": {
497
- const target = r.target ?? r.ref ?? "";
498
- const ok = r.ok !== false ? "OK" : "FAILED";
499
- return `remove: ${target} ${ok}`;
500
- }
501
- case "update": {
502
- const processed = r.processed;
503
- if (!processed?.length)
504
- return `update: nothing to update`;
505
- const lines = processed.map((item) => {
506
- const changed = item.changed;
507
- const installed = item.installed;
508
- const previous = item.previous;
509
- if (changed?.any) {
510
- const prev = previous?.resolvedVersion ?? "unknown";
511
- const next = installed?.resolvedVersion ?? "unknown";
512
- return `update: ${item.id} v${prev} → v${next}`;
513
- }
514
- return `update: ${item.id} (unchanged)`;
515
- });
516
- return lines.join("\n");
517
- }
518
- case "upgrade": {
519
- if (r.upgraded === true) {
520
- return `akm upgraded: v${r.currentVersion} → v${r.newVersion}`;
521
- }
522
- if (r.updateAvailable === true) {
523
- return `akm v${r.currentVersion} → v${r.latestVersion} available (run 'akm upgrade' to install)`;
524
- }
525
- if (r.updateAvailable === false && r.latestVersion) {
526
- return `akm v${r.currentVersion} is already the latest version`;
527
- }
528
- if (r.message)
529
- return String(r.message);
530
- return null;
531
- }
532
- case "clone": {
533
- const dst = r.destination?.path ?? "unknown";
534
- const remote = r.remoteFetched ? " (fetched from remote)" : "";
535
- const over = r.overwritten ? " (overwritten)" : "";
536
- return `Cloned${remote} → ${dst}${over}`;
537
- }
538
- default:
539
- return null; // fall through to YAML
540
- }
541
- }
542
- function formatWorkflowListPlain(result) {
543
- const runs = Array.isArray(result.runs) ? result.runs : [];
544
- if (runs.length === 0)
545
- return "No workflow runs found.";
546
- return runs
547
- .map((run) => {
548
- const id = typeof run.id === "string" ? run.id : "unknown";
549
- const ref = typeof run.workflowRef === "string" ? run.workflowRef : "workflow:unknown";
550
- const status = typeof run.status === "string" ? run.status : "unknown";
551
- const currentStep = typeof run.currentStepId === "string" ? ` (current: ${run.currentStepId})` : "";
552
- return `${id} ${ref} [${status}]${currentStep}`;
553
- })
554
- .join("\n");
555
- }
556
- function formatWorkflowStatusPlain(result) {
557
- const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
558
- const workflow = typeof result.workflow === "object" && result.workflow !== null
559
- ? result.workflow
560
- : undefined;
561
- if (!run || !workflow)
562
- return null;
563
- const lines = [
564
- `workflow: ${String(workflow.ref ?? "workflow:unknown")}`,
565
- `run: ${String(run.id ?? "unknown")}`,
566
- `title: ${String(run.workflowTitle ?? workflow.title ?? "Workflow")}`,
567
- `status: ${String(run.status ?? "unknown")}`,
568
- ];
569
- if (run.currentStepId)
570
- lines.push(`currentStep: ${String(run.currentStepId)}`);
571
- const steps = Array.isArray(workflow.steps) ? workflow.steps : [];
572
- if (steps.length > 0) {
573
- lines.push("steps:");
574
- for (const step of steps) {
575
- const title = typeof step.title === "string" ? step.title : "Untitled step";
576
- const id = typeof step.id === "string" ? step.id : "unknown";
577
- const status = typeof step.status === "string" ? step.status : "unknown";
578
- lines.push(` - ${title} [${id}] (${status})`);
579
- if (typeof step.notes === "string" && step.notes.trim()) {
580
- lines.push(` notes: ${step.notes}`);
581
- }
582
- }
583
- }
584
- return lines.join("\n");
585
- }
586
- function formatWorkflowNextPlain(result) {
587
- const base = formatWorkflowStatusPlain(result);
588
- const step = typeof result.step === "object" && result.step !== null ? result.step : undefined;
589
- if (!step)
590
- return base;
591
- const lines = base ? [base, "", "next:"] : ["next:"];
592
- lines.push(` ${String(step.title ?? "Untitled step")} [${String(step.id ?? "unknown")}]`);
593
- if (typeof step.instructions === "string" && step.instructions.trim()) {
594
- lines.push(` instructions: ${step.instructions.replace(/\n+/g, " ").trim()}`);
595
- }
596
- const completion = Array.isArray(step.completionCriteria) ? step.completionCriteria : [];
597
- if (completion.length > 0) {
598
- lines.push(" completion:");
599
- for (const criterion of completion) {
600
- lines.push(` - ${String(criterion)}`);
601
- }
602
- }
603
- return lines.join("\n");
604
- }
605
- function formatSearchPlain(r, detail) {
606
- const hits = r.hits ?? [];
607
- const registryHits = r.registryHits ?? [];
608
- const allHits = [...hits, ...registryHits];
609
- if (allHits.length === 0) {
610
- return r.tip ? String(r.tip) : "No results found.";
611
- }
612
- const lines = [];
613
- for (const hit of allHits) {
614
- const type = hit.type ?? "unknown";
615
- const name = hit.name ?? "unnamed";
616
- const score = hit.score != null ? ` (score: ${hit.score})` : "";
617
- const desc = hit.description ? ` ${hit.description}` : "";
618
- lines.push(`${type}: ${name}${score}`);
619
- if (desc)
620
- lines.push(desc);
621
- if (hit.id)
622
- lines.push(` id: ${String(hit.id)}`);
623
- if (hit.ref)
624
- lines.push(` ref: ${String(hit.ref)}`);
625
- if (hit.origin !== undefined)
626
- lines.push(` origin: ${String(hit.origin)}`);
627
- if (hit.size)
628
- lines.push(` size: ${String(hit.size)}`);
629
- if (hit.action)
630
- lines.push(` action: ${String(hit.action)}`);
631
- if (hit.run)
632
- lines.push(` run: ${String(hit.run)}`);
633
- if (Array.isArray(hit.tags) && hit.tags.length > 0)
634
- lines.push(` tags: ${hit.tags.join(", ")}`);
635
- if (hit.curated !== undefined)
636
- lines.push(` curated: ${String(hit.curated)}`);
637
- if (detail === "full") {
638
- if (hit.path)
639
- lines.push(` path: ${String(hit.path)}`);
640
- if (hit.editable != null)
641
- lines.push(` editable: ${String(hit.editable)}`);
642
- if (hit.editHint)
643
- lines.push(` editHint: ${String(hit.editHint)}`);
644
- const whyMatched = hit.whyMatched;
645
- if (whyMatched && whyMatched.length > 0) {
646
- lines.push(` whyMatched: ${whyMatched.join(", ")}`);
647
- }
648
- }
649
- lines.push(""); // blank line between hits
650
- }
651
- if (detail === "full" && r.timing) {
652
- const timing = r.timing;
653
- const parts = [];
654
- if (timing.totalMs != null)
655
- parts.push(`total: ${timing.totalMs}ms`);
656
- if (timing.rankMs != null)
657
- parts.push(`rank: ${timing.rankMs}ms`);
658
- if (timing.embedMs != null)
659
- parts.push(`embed: ${timing.embedMs}ms`);
660
- if (parts.length > 0)
661
- lines.push(`timing: ${parts.join(", ")}`);
662
- }
663
- return lines.join("\n").trimEnd();
664
- }
665
- function formatWikiListPlain(r) {
666
- const wikis = Array.isArray(r.wikis) ? r.wikis : [];
667
- if (wikis.length === 0)
668
- return "No wikis. Create one with `akm wiki create <name>` or register one with `akm wiki register <name> <path-or-repo>`.";
669
- const lines = ["NAME\tPAGES\tRAWS\tLAST-MODIFIED"];
670
- for (const w of wikis) {
671
- const name = typeof w.name === "string" ? w.name : "?";
672
- const pages = typeof w.pages === "number" ? w.pages : 0;
673
- const raws = typeof w.raws === "number" ? w.raws : 0;
674
- const modified = typeof w.lastModified === "string" ? w.lastModified : "-";
675
- lines.push(`${name}\t${pages}\t${raws}\t${modified}`);
676
- }
677
- return lines.join("\n");
678
- }
679
- function formatWikiShowPlain(r) {
680
- const lines = [];
681
- if (r.name)
682
- lines.push(`# wiki: ${String(r.name)}`);
683
- if (r.path)
684
- lines.push(`path: ${String(r.path)}`);
685
- if (r.description)
686
- lines.push(`description: ${String(r.description)}`);
687
- if (typeof r.pages === "number")
688
- lines.push(`pages: ${r.pages}`);
689
- if (typeof r.raws === "number")
690
- lines.push(`raws: ${r.raws}`);
691
- if (r.lastModified)
692
- lines.push(`lastModified: ${String(r.lastModified)}`);
693
- const recentLog = Array.isArray(r.recentLog) ? r.recentLog : [];
694
- if (recentLog.length > 0) {
695
- lines.push("", "recent log:");
696
- for (const entry of recentLog) {
697
- lines.push(entry);
698
- lines.push("");
699
- }
700
- }
701
- return lines.join("\n").trimEnd();
702
- }
703
- function formatWikiCreatePlain(r) {
704
- const created = Array.isArray(r.created) ? r.created : [];
705
- const skipped = Array.isArray(r.skipped) ? r.skipped : [];
706
- const lines = [`Created wiki ${String(r.ref ?? r.name)} at ${String(r.path ?? "?")}`];
707
- if (created.length > 0)
708
- lines.push(` created: ${created.length} file(s)`);
709
- if (skipped.length > 0)
710
- lines.push(` skipped: ${skipped.length} existing file(s)`);
711
- return lines.join("\n");
712
- }
713
- function formatWikiRemovePlain(r) {
714
- const preserved = r.preservedRaw === true;
715
- const removed = Array.isArray(r.removed) ? r.removed.length : 0;
716
- const base = `Removed wiki ${String(r.name ?? "?")} (${removed} path(s))`;
717
- return preserved ? `${base}; preserved ${String(r.rawPath ?? "raw/")}` : base;
718
- }
719
- function formatWikiPagesPlain(r) {
720
- const pages = Array.isArray(r.pages) ? r.pages : [];
721
- if (pages.length === 0)
722
- return `No pages in wiki:${String(r.wiki ?? "?")}.`;
723
- const lines = [];
724
- for (const p of pages) {
725
- const ref = String(p.ref ?? "?");
726
- const kind = typeof p.pageKind === "string" ? ` [${p.pageKind}]` : "";
727
- const desc = typeof p.description === "string" && p.description ? ` — ${p.description}` : "";
728
- lines.push(`${ref}${kind}${desc}`);
729
- }
730
- return lines.join("\n");
731
- }
732
- function formatWikiStashPlain(r) {
733
- const slug = String(r.slug ?? "?");
734
- const pathValue = String(r.path ?? "?");
735
- return `Stashed ${slug} → ${pathValue}`;
736
- }
737
- function formatWikiLintPlain(r) {
738
- const findings = Array.isArray(r.findings) ? r.findings : [];
739
- const pagesScanned = typeof r.pagesScanned === "number" ? r.pagesScanned : 0;
740
- const rawsScanned = typeof r.rawsScanned === "number" ? r.rawsScanned : 0;
741
- const header = `${findings.length} finding(s) in wiki:${String(r.wiki ?? "?")} (${pagesScanned} page(s), ${rawsScanned} raw(s))`;
742
- if (findings.length === 0)
743
- return `${header} — clean.`;
744
- const lines = [header];
745
- for (const f of findings) {
746
- const kind = String(f.kind ?? "?");
747
- const message = String(f.message ?? "");
748
- lines.push(`- [${kind}] ${message}`);
749
- }
750
- return lines.join("\n");
751
- }
752
- function formatWikiIngestPlain(r) {
753
- if (typeof r.workflow === "string")
754
- return r.workflow;
755
- return JSON.stringify(r, null, 2);
756
- }
757
- function formatCuratePlain(r, detail) {
758
- const query = typeof r.query === "string" ? r.query : "";
759
- const summary = typeof r.summary === "string" ? r.summary : "";
760
- const items = Array.isArray(r.items) ? r.items : [];
761
- const lines = [`Curated results for "${query}"`];
762
- if (summary)
763
- lines.push(summary);
764
- if (items.length === 0) {
765
- if (r.tip)
766
- lines.push(String(r.tip));
767
- return lines.join("\n");
768
- }
769
- for (const item of items) {
770
- const type = typeof item.type === "string" ? item.type : "unknown";
771
- const name = typeof item.name === "string" ? item.name : "unnamed";
772
- lines.push("");
773
- lines.push(`[${type}] ${name}`);
774
- if (item.description)
775
- lines.push(` ${String(item.description)}`);
776
- if (item.preview)
777
- lines.push(` preview: ${String(item.preview)}`);
778
- if (item.ref)
779
- lines.push(` ref: ${String(item.ref)}`);
780
- if (item.id)
781
- lines.push(` id: ${String(item.id)}`);
782
- if (Array.isArray(item.parameters) && item.parameters.length > 0) {
783
- lines.push(` parameters: ${item.parameters.join(", ")}`);
784
- }
785
- if (item.run)
786
- lines.push(` run: ${String(item.run)}`);
787
- if (item.followUp)
788
- lines.push(` show: ${String(item.followUp)}`);
789
- if (detail !== "brief" && item.reason)
790
- lines.push(` why: ${String(item.reason)}`);
791
- }
792
- const warnings = Array.isArray(r.warnings) ? r.warnings : [];
793
- if (warnings.length > 0) {
794
- lines.push("");
795
- lines.push("Warnings:");
796
- for (const warning of warnings) {
797
- lines.push(`- ${String(warning)}`);
798
- }
799
- }
800
- return lines.join("\n");
801
- }
802
- const CURATE_FALLBACK_FILTER_WORDS = new Set([
803
- "a",
804
- "an",
805
- "and",
806
- "for",
807
- "how",
808
- "i",
809
- "in",
810
- "of",
811
- "or",
812
- "the",
813
- "to",
814
- "with",
815
- ]);
816
- const CURATED_TYPE_FALLBACK_ORDER = ["skill", "command", "script", "knowledge", "agent", "memory"];
817
- const CURATED_TYPE_FALLBACK_INDEX = new Map(CURATED_TYPE_FALLBACK_ORDER.map((type, index) => [type, index]));
818
- const MIN_CURATE_FALLBACK_TOKEN_LENGTH = 3;
819
- const MAX_CURATE_FALLBACK_KEYWORDS = 6;
820
- const CURATE_SEARCH_LIMIT_MULTIPLIER = 4;
821
- const MIN_CURATE_SEARCH_LIMIT = 12;
822
- async function curateSearchResults(query, result, limit, selectedType) {
823
- const stashHits = result.hits.filter((hit) => hit.type !== "registry");
824
- const registryHits = result.registryHits ?? [];
825
- let selectedStashHits;
826
- if (selectedType && selectedType !== "any") {
827
- selectedStashHits = stashHits.slice(0, limit);
828
- }
829
- else {
830
- const bestByType = new Map();
831
- for (const hit of stashHits) {
832
- if (!bestByType.has(hit.type))
833
- bestByType.set(hit.type, hit);
834
- }
835
- const orderedTypes = orderCuratedTypes(query, Array.from(bestByType.keys()));
836
- selectedStashHits = orderedTypes
837
- .map((type) => bestByType.get(type))
838
- .filter((hit) => Boolean(hit));
839
- }
840
- const selectedRegistryHits = selectedStashHits.length >= limit ? [] : registryHits.slice(0, Math.min(2, limit - selectedStashHits.length));
841
- const items = [
842
- ...(await Promise.all(selectedStashHits.slice(0, limit).map((hit) => enrichCuratedStashHit(query, hit)))),
843
- ...selectedRegistryHits.map((hit) => buildCuratedRegistryItem(query, hit)),
844
- ].slice(0, limit);
845
- return {
846
- query,
847
- summary: buildCurateSummary(query, items),
848
- items,
849
- ...(result.warnings?.length ? { warnings: result.warnings } : {}),
850
- ...(result.tip ? { tip: result.tip } : {}),
851
- };
852
- }
853
- function orderCuratedTypes(query, types) {
854
- const lower = query.toLowerCase();
855
- const boosts = new Map();
856
- const addBoost = (type, amount) => boosts.set(type, (boosts.get(type) ?? 0) + amount);
857
- if (/(run|script|bash|shell|cli|execute|automation|deploy|build|test|lint)/.test(lower)) {
858
- addBoost("script", 6);
859
- addBoost("command", 4);
860
- }
861
- if (/(guide|docs?|readme|reference|how|explain|learn|why)/.test(lower)) {
862
- addBoost("knowledge", 6);
863
- addBoost("skill", 4);
864
- }
865
- if (/(agent|assistant|planner|review|analy[sz]e|architect|prompt)/.test(lower)) {
866
- addBoost("agent", 6);
867
- addBoost("skill", 3);
868
- }
869
- if (/(config|template|release|generate|command)/.test(lower)) {
870
- addBoost("command", 5);
871
- }
872
- if (/(memory|context|recall|remember)/.test(lower)) {
873
- addBoost("memory", 6);
874
- }
875
- return [...types].sort((a, b) => {
876
- const boostDiff = (boosts.get(b) ?? 0) - (boosts.get(a) ?? 0);
877
- if (boostDiff !== 0)
878
- return boostDiff;
879
- return ((CURATED_TYPE_FALLBACK_INDEX.get(a) ?? Number.MAX_SAFE_INTEGER) -
880
- (CURATED_TYPE_FALLBACK_INDEX.get(b) ?? Number.MAX_SAFE_INTEGER));
881
- });
882
- }
883
- async function enrichCuratedStashHit(query, hit) {
884
- let shown;
885
- try {
886
- shown = await akmShowUnified({ ref: hit.ref });
887
- }
888
- catch {
889
- shown = undefined;
890
- }
891
- const description = shown?.description ?? hit.description;
892
- const preview = buildCuratedPreview(shown, hit);
893
- return {
894
- source: "stash",
895
- type: shown?.type ?? hit.type,
896
- name: shown?.name ?? hit.name,
897
- ref: hit.ref,
898
- ...(description ? { description } : {}),
899
- ...(preview ? { preview } : {}),
900
- ...(shown?.parameters?.length ? { parameters: shown.parameters } : {}),
901
- ...(shown?.run ? { run: shown.run } : {}),
902
- followUp: `akm show ${hit.ref}`,
903
- reason: buildCuratedReason(query, shown?.type ?? hit.type),
904
- ...(hit.score !== undefined ? { score: hit.score } : {}),
905
- };
906
- }
907
- function buildCuratedRegistryItem(query, hit) {
908
- return {
909
- source: "registry",
910
- type: "registry",
911
- name: hit.name,
912
- id: hit.id,
913
- ...(hit.description ? { description: hit.description } : {}),
914
- followUp: hit.action ?? `akm add ${hit.id}`,
915
- reason: `Useful external source to explore for ${query}.`,
916
- ...(hit.score !== undefined ? { score: hit.score } : {}),
917
- };
918
- }
919
- function firstNonEmpty(values) {
920
- return values.find((value) => typeof value === "string" && value.trim().length > 0);
921
- }
922
- function buildCuratedPreview(shown, hit) {
923
- if (shown?.run)
924
- return truncateDescription(`run ${shown.run}`, 160);
925
- const payload = firstNonEmpty([shown?.template, shown?.prompt, shown?.content, hit.description])
926
- ?.replace(/\s+/g, " ")
927
- .trim();
928
- return payload ? truncateDescription(payload, 160) : undefined;
929
- }
930
- function buildCuratedReason(query, type) {
931
- switch (type) {
932
- case "script":
933
- return `Best runnable script match for "${query}".`;
934
- case "command":
935
- return `Best reusable command/template match for "${query}".`;
936
- case "knowledge":
937
- return `Best reference document match for "${query}".`;
938
- case "skill":
939
- return `Best instructions/workflow match for "${query}".`;
940
- case "agent":
941
- return `Best specialized agent prompt match for "${query}".`;
942
- case "memory":
943
- return `Best saved context match for "${query}".`;
944
- default:
945
- return `Best ${type} match for "${query}".`;
946
- }
947
- }
948
- function buildCurateSummary(query, items) {
949
- if (items.length === 0) {
950
- return `No curated assets were selected for "${query}".`;
951
- }
952
- const labels = items.map((item) => `${item.type}:${item.name}`);
953
- return `Selected ${items.length} high-signal result${items.length === 1 ? "" : "s"}: ${labels.join(", ")}.`;
954
- }
955
- function hasSearchResults(result) {
956
- return result.hits.length > 0 || (result.registryHits?.length ?? 0) > 0;
957
- }
958
- /**
959
- * Extract a small set of fallback keywords when a prompt-style curate query
960
- * returns no hits as a whole phrase.
961
- *
962
- * We keep up to MAX_CURATE_FALLBACK_KEYWORDS distinct keywords and drop short
963
- * or common filler words so follow-up searches stay inexpensive while focusing
964
- * on higher-signal terms.
965
- */
966
- function deriveCurateFallbackQueries(query) {
967
- return Array.from(new Set(query
968
- .toLowerCase()
969
- .split(/[^a-z0-9]+/)
970
- .map((token) => token.trim())
971
- // Keep longer tokens so fallback stays focused on higher-signal terms
972
- // and avoids broad one- and two-letter matches that overwhelm curation.
973
- .filter((token) => token.length >= MIN_CURATE_FALLBACK_TOKEN_LENGTH && !CURATE_FALLBACK_FILTER_WORDS.has(token)))).slice(0, MAX_CURATE_FALLBACK_KEYWORDS);
974
- }
975
- function mergeCurateSearchResponses(base, extras) {
976
- const hitsByRef = new Map();
977
- for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
978
- hitsByRef.set(hit.ref, hit);
979
- }
980
- for (const result of extras) {
981
- for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
982
- const existing = hitsByRef.get(hit.ref);
983
- if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
984
- hitsByRef.set(hit.ref, hit);
985
- }
986
- }
987
- }
988
- const registryById = new Map();
989
- for (const hit of base.registryHits ?? []) {
990
- registryById.set(hit.id, hit);
991
- }
992
- for (const result of extras) {
993
- for (const hit of result.registryHits ?? []) {
994
- const existing = registryById.get(hit.id);
995
- if (!existing || (hit.score ?? 0) > (existing.score ?? 0)) {
996
- registryById.set(hit.id, hit);
997
- }
998
- }
999
- }
1000
- const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
1001
- const mergedHits = [...hitsByRef.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
1002
- const mergedRegistryHits = [...registryById.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
1003
- return {
1004
- ...base,
1005
- hits: mergedHits,
1006
- ...(mergedRegistryHits.length > 0 ? { registryHits: mergedRegistryHits } : {}),
1007
- ...(warnings.length > 0 ? { warnings } : {}),
1008
- ...(mergedHits.length > 0 || mergedRegistryHits.length > 0 ? { tip: undefined } : {}),
1009
- };
1010
- }
1011
- async function searchForCuration(input) {
1012
- const initial = await akmSearch(input);
1013
- if (hasSearchResults(initial))
1014
- return initial;
1015
- const fallbackQueries = deriveCurateFallbackQueries(input.query);
1016
- if (fallbackQueries.length <= 1)
1017
- return initial;
1018
- const fallbackResults = await Promise.all(fallbackQueries.map((token) => akmSearch({
1019
- query: token,
1020
- type: input.type,
1021
- limit: input.limit,
1022
- source: input.source,
1023
- })));
1024
- return mergeCurateSearchResponses(initial, fallbackResults);
1025
- }
1026
85
  /**
1027
86
  * Module Naming:
1028
87
  * - stash-* : Asset operations (search, show, add, clone)
1029
88
  * - stash-provider-* : Runtime data source providers (filesystem, openviking)
1030
89
  * - registry-* : Discovery from remote registries (npm, GitHub)
1031
- * - installed-kits : Unified source operations (list, remove, update)
90
+ * - installed-stashes : Unified source operations (list, remove, update)
1032
91
  */
1033
92
  const setupCommand = defineCommand({
1034
93
  meta: {
@@ -1093,7 +152,7 @@ const searchCommand = defineCommand({
1093
152
  limit: { type: "string", description: "Maximum number of results" },
1094
153
  source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
1095
154
  format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
1096
- detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
155
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
1097
156
  },
1098
157
  async run({ args }) {
1099
158
  await runWithJsonErrors(async () => {
@@ -1129,15 +188,7 @@ const curateCommand = defineCommand({
1129
188
  }
1130
189
  const limit = limitRaw && limitRaw > 0 ? limitRaw : 4;
1131
190
  const source = parseSearchSource(args.source ?? "stash");
1132
- const searchResult = await searchForCuration({
1133
- query: args.query,
1134
- type,
1135
- // Search deeper than the final curated count so we can pick one strong
1136
- // match per type and still have room for fallback retries.
1137
- limit: Math.max(limit * CURATE_SEARCH_LIMIT_MULTIPLIER, MIN_CURATE_SEARCH_LIMIT),
1138
- source,
1139
- });
1140
- const curated = await curateSearchResults(args.query, searchResult, limit, type);
191
+ const curated = await akmCurate({ query: args.query, type, limit, source });
1141
192
  output("curate", curated);
1142
193
  });
1143
194
  },
@@ -1176,16 +227,6 @@ const addCommand = defineCommand({
1176
227
  async run({ args }) {
1177
228
  await runWithJsonErrors(async () => {
1178
229
  const ref = args.ref.trim();
1179
- // Context-hub convenience alias
1180
- if (ref === CONTEXT_HUB_ALIAS_REF) {
1181
- const result = addStash({
1182
- target: CONTEXT_HUB_ALIAS_URL,
1183
- providerType: "git",
1184
- name: "context-hub",
1185
- });
1186
- output("stash-add", result);
1187
- return;
1188
- }
1189
230
  // URL with --provider → stash source (remote or git provider)
1190
231
  if (args.provider) {
1191
232
  if (shouldWarnOnPlainHttp(ref)) {
@@ -1213,7 +254,7 @@ const addCommand = defineCommand({
1213
254
  options: parsedOptions,
1214
255
  writable: args.writable,
1215
256
  });
1216
- output("stash-add", result);
257
+ output("add", result);
1217
258
  return;
1218
259
  }
1219
260
  if (shouldWarnOnPlainHttp(ref)) {
@@ -1324,7 +365,7 @@ const upgradeCommand = defineCommand({
1324
365
  args: {
1325
366
  check: { type: "boolean", description: "Check for updates without installing", default: false },
1326
367
  force: { type: "boolean", description: "Force upgrade even if on latest", default: false },
1327
- skipChecksum: {
368
+ "skip-checksum": {
1328
369
  type: "boolean",
1329
370
  description: "Skip checksum verification (not recommended)",
1330
371
  default: false,
@@ -1337,7 +378,8 @@ const upgradeCommand = defineCommand({
1337
378
  output("upgrade", check);
1338
379
  return;
1339
380
  }
1340
- const result = await performUpgrade(check, { force: args.force, skipChecksum: args.skipChecksum });
381
+ const skipChecksum = Boolean(args["skip-checksum"]);
382
+ const result = await performUpgrade(check, { force: args.force, skipChecksum });
1341
383
  output("upgrade", result);
1342
384
  });
1343
385
  },
@@ -1350,38 +392,43 @@ const showCommand = defineCommand({
1350
392
  args: {
1351
393
  ref: { type: "positional", description: "Asset ref (type:name)", required: true },
1352
394
  format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
1353
- detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
1354
- akmView: { type: "string", description: "Internal positional knowledge view mode parser" },
1355
- akmHeading: { type: "string", description: "Internal positional section heading parser" },
1356
- akmStart: { type: "string", description: "Internal positional start-line parser" },
1357
- akmEnd: { type: "string", description: "Internal positional end-line parser" },
395
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
1358
396
  },
1359
397
  async run({ args }) {
1360
398
  await runWithJsonErrors(async () => {
399
+ // The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
400
+ // is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
401
+ // by `normalizeShowArgv` before citty parses argv. We read those values
402
+ // directly via `parseFlagValue` so the flags don't surface as user-facing
403
+ // options in `akm show --help`.
404
+ const akmView = parseFlagValue(process.argv, "--akmView");
405
+ const akmHeading = parseFlagValue(process.argv, "--akmHeading");
406
+ const akmStart = parseFlagValue(process.argv, "--akmStart");
407
+ const akmEnd = parseFlagValue(process.argv, "--akmEnd");
1361
408
  let view;
1362
- if (args.akmView) {
1363
- switch (args.akmView) {
409
+ if (akmView) {
410
+ switch (akmView) {
1364
411
  case "section":
1365
- view = { mode: "section", heading: args.akmHeading ?? "" };
412
+ view = { mode: "section", heading: akmHeading ?? "" };
1366
413
  break;
1367
414
  case "lines":
1368
415
  view = {
1369
416
  mode: "lines",
1370
- start: Number(args.akmStart ?? "1"),
1371
- end: args.akmEnd ? parseInt(args.akmEnd, 10) : Number.MAX_SAFE_INTEGER,
417
+ start: Number(akmStart ?? "1"),
418
+ end: akmEnd ? parseInt(akmEnd, 10) : Number.MAX_SAFE_INTEGER,
1372
419
  };
1373
420
  break;
1374
421
  case "toc":
1375
422
  case "frontmatter":
1376
423
  case "full":
1377
- view = { mode: args.akmView };
424
+ view = { mode: akmView };
1378
425
  break;
1379
426
  default:
1380
- throw new UsageError(`Unknown view mode: ${args.akmView}. Expected one of: full|toc|frontmatter|section|lines`);
427
+ throw new UsageError(`Unknown view mode: ${akmView}. Expected one of: full|toc|frontmatter|section|lines`);
1381
428
  }
1382
429
  }
1383
430
  // Map CLI detail level to ShowDetailLevel for the show function
1384
- const cliDetail = resolveOutputMode().detail;
431
+ const cliDetail = getOutputMode().detail;
1385
432
  const showDetail = cliDetail === "summary" ? "summary" : undefined;
1386
433
  const result = await akmShowUnified({ ref: args.ref, view, detail: showDetail });
1387
434
  output("show", result);
@@ -1510,7 +557,7 @@ const saveCommand = defineCommand({
1510
557
  // before any standalone occurrence of the same value in the save
1511
558
  // subcommand's argv slice. This preserves legitimate invocations
1512
559
  // like `akm save json --format json`.
1513
- const parsedFormat = parseFlagValue("--format");
560
+ const parsedFormat = parseFlagValue(process.argv, "--format");
1514
561
  const effectiveName = args.name !== undefined &&
1515
562
  parsedFormat !== undefined &&
1516
563
  args.name === parsedFormat &&
@@ -1593,7 +640,7 @@ const cloneCommand = defineCommand({
1593
640
  },
1594
641
  });
1595
642
  const registryCommand = defineCommand({
1596
- meta: { name: "registry", description: "Manage kit registries" },
643
+ meta: { name: "registry", description: "Manage stash registries" },
1597
644
  subCommands: {
1598
645
  list: defineCommand({
1599
646
  meta: { name: "list", description: "List configured registries" },
@@ -1612,6 +659,11 @@ const registryCommand = defineCommand({
1612
659
  name: { type: "string", description: "Human-friendly name for the registry" },
1613
660
  provider: { type: "string", description: "Provider type (e.g. static-index, skills-sh)" },
1614
661
  options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
662
+ "allow-insecure": {
663
+ type: "boolean",
664
+ description: "Allow a plain HTTP registry URL (otherwise rejected)",
665
+ default: false,
666
+ },
1615
667
  },
1616
668
  run({ args }) {
1617
669
  return runWithJsonErrors(() => {
@@ -1619,7 +671,12 @@ const registryCommand = defineCommand({
1619
671
  throw new UsageError("Registry URL must start with http:// or https://");
1620
672
  }
1621
673
  if (args.url.startsWith("http://")) {
1622
- warn("Warning: registry URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
674
+ const allowInsecure = Boolean(args["allow-insecure"]);
675
+ if (!allowInsecure) {
676
+ throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
677
+ "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
678
+ }
679
+ warn("Warning: registry URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious index.");
1623
680
  }
1624
681
  const config = loadUserConfig();
1625
682
  const registries = [...(config.registries ?? [])];
@@ -1668,7 +725,7 @@ const registryCommand = defineCommand({
1668
725
  },
1669
726
  }),
1670
727
  search: defineCommand({
1671
- meta: { name: "search", description: "Search enabled registries for kits" },
728
+ meta: { name: "search", description: "Search enabled registries for stashes" },
1672
729
  args: {
1673
730
  query: { type: "positional", description: "Search query", required: true },
1674
731
  limit: { type: "string", description: "Maximum number of results" },
@@ -1690,15 +747,16 @@ const registryCommand = defineCommand({
1690
747
  args: {
1691
748
  out: { type: "string", description: "Output path for the generated index", default: "index.json" },
1692
749
  manual: { type: "string", description: "Manual entries JSON file", default: "manual-entries.json" },
1693
- npmRegistry: { type: "string", description: "Override npm registry base URL" },
1694
- githubApi: { type: "string", description: "Override GitHub API base URL" },
750
+ "npm-registry": { type: "string", description: "Override npm registry base URL" },
751
+ "github-api": { type: "string", description: "Override GitHub API base URL" },
1695
752
  },
1696
753
  async run({ args }) {
1697
754
  await runWithJsonErrors(async () => {
755
+ const argsRecord = args;
1698
756
  const result = await buildRegistryIndex({
1699
757
  manualEntriesPath: args.manual,
1700
- npmRegistryBase: args.npmRegistry,
1701
- githubApiBase: args.githubApi,
758
+ npmRegistryBase: typeof argsRecord["npm-registry"] === "string" ? argsRecord["npm-registry"] : undefined,
759
+ githubApiBase: typeof argsRecord["github-api"] === "string" ? argsRecord["github-api"] : undefined,
1702
760
  });
1703
761
  const outPath = writeRegistryIndex(result.index, args.out);
1704
762
  output("registry-build-index", {
@@ -1755,12 +813,6 @@ const feedbackCommand = defineCommand({
1755
813
  });
1756
814
  },
1757
815
  });
1758
- function tryReadStdinText() {
1759
- if (process.stdin.isTTY)
1760
- return undefined;
1761
- const input = fs.readFileSync(0, "utf8");
1762
- return input.length > 0 ? input : undefined;
1763
- }
1764
816
  function normalizeMarkdownAssetName(name, fallback) {
1765
817
  const trimmed = (name ?? fallback)
1766
818
  .trim()
@@ -1792,13 +844,6 @@ function inferAssetName(content, fallbackPrefix, preferred) {
1792
844
  const basis = preferred?.trim() || firstNonEmptyLine || fallbackPrefix;
1793
845
  return slugifyAssetName(basis, fallbackPrefix);
1794
846
  }
1795
- function readMemoryContent(contentArg) {
1796
- const content = contentArg ?? tryReadStdinText();
1797
- if (!content?.trim()) {
1798
- throw new UsageError("Memory content is required. Pass quoted text or pipe markdown into stdin.");
1799
- }
1800
- return content;
1801
- }
1802
847
  function readKnowledgeContent(source) {
1803
848
  if (source === "-") {
1804
849
  const content = tryReadStdinText();
@@ -1928,11 +973,11 @@ const workflowStatusCommand = defineCommand({
1928
973
  const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
1929
974
  const { runs } = listWorkflowRuns({ workflowRef: ref });
1930
975
  if (runs.length === 0) {
1931
- throw new NotFoundError(`No workflow runs found for ${ref}`);
976
+ throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
1932
977
  }
1933
978
  const mostRecent = runs[0];
1934
979
  if (!mostRecent)
1935
- throw new NotFoundError(`No workflow runs found for ${ref}`);
980
+ throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
1936
981
  const result = getWorkflowStatus(mostRecent.id);
1937
982
  output("workflow-status", result);
1938
983
  }
@@ -2016,7 +1061,7 @@ const workflowResumeCommand = defineCommand({
2016
1061
  run({ args }) {
2017
1062
  return runWithJsonErrors(() => {
2018
1063
  const result = resumeWorkflowRun(args.runId);
2019
- output("workflow-run", result);
1064
+ output("workflow-resume", result);
2020
1065
  });
2021
1066
  },
2022
1067
  });
@@ -2063,12 +1108,110 @@ const rememberCommand = defineCommand({
2063
1108
  description: "Overwrite an existing memory with the same name",
2064
1109
  default: false,
2065
1110
  },
1111
+ tag: {
1112
+ type: "string",
1113
+ description: "Tag to add to the memory (repeatable: --tag foo --tag bar)",
1114
+ },
1115
+ expires: {
1116
+ type: "string",
1117
+ description: "Expiry duration shorthand (e.g. 30d, 12h, 6m). Resolved to an ISO date.",
1118
+ },
1119
+ source: {
1120
+ type: "string",
1121
+ description: "Source reference (URL, asset ref, file path, or any free-form string)",
1122
+ },
1123
+ auto: {
1124
+ type: "boolean",
1125
+ description: "Apply heuristic tagging (code, subjective, source, observed_at) from the body",
1126
+ default: false,
1127
+ },
1128
+ enrich: {
1129
+ type: "boolean",
1130
+ description: "Call the configured LLM to propose tags and description (requires LLM config)",
1131
+ default: false,
1132
+ },
2066
1133
  },
2067
- run({ args }) {
2068
- return runWithJsonErrors(() => {
1134
+ async run({ args }) {
1135
+ return runWithJsonErrors(async () => {
1136
+ const body = readMemoryContent(args.content);
1137
+ // Determine if the user has requested any structured metadata mode.
1138
+ // Collect all --tag occurrences directly from process.argv because citty
1139
+ // only exposes the last value for repeated string flags.
1140
+ const rawTags = parseAllFlagValues("--tag");
1141
+ const hasStructuredArgs = rawTags.length > 0 || !!args.expires || !!args.source || args.auto || args.enrich;
1142
+ // Zero-flag path: write bare memory (no frontmatter). Preserve existing behaviour.
1143
+ if (!hasStructuredArgs) {
1144
+ const result = writeMarkdownAsset({
1145
+ type: "memory",
1146
+ content: body,
1147
+ name: args.name,
1148
+ fallbackPrefix: "memory",
1149
+ force: args.force,
1150
+ });
1151
+ output("remember", { ok: true, ...result });
1152
+ return;
1153
+ }
1154
+ // ── Accumulate metadata from all three modes ──────────────────────────
1155
+ // Start with CLI args (Mode 1: always)
1156
+ const tags = [...rawTags];
1157
+ let description;
1158
+ let source = args.source;
1159
+ let observed_at;
1160
+ let expires;
1161
+ let subjective;
1162
+ // Resolve --expires to an ISO date string
1163
+ if (args.expires) {
1164
+ const durationMs = parseDuration(args.expires);
1165
+ const expiresDate = new Date(Date.now() + durationMs);
1166
+ expires = expiresDate.toISOString().slice(0, 10);
1167
+ }
1168
+ // Mode 2: --auto heuristics
1169
+ if (args.auto) {
1170
+ const auto = runAutoHeuristics(body);
1171
+ for (const t of auto.tags) {
1172
+ if (!tags.includes(t))
1173
+ tags.push(t);
1174
+ }
1175
+ if (!source && auto.source)
1176
+ source = auto.source;
1177
+ if (!observed_at && auto.observed_at)
1178
+ observed_at = auto.observed_at;
1179
+ if (!subjective && auto.subjective)
1180
+ subjective = auto.subjective;
1181
+ }
1182
+ // Mode 3: --enrich LLM (fail-soft)
1183
+ if (args.enrich) {
1184
+ const enriched = await runLlmEnrich(body);
1185
+ for (const t of enriched.tags) {
1186
+ if (!tags.includes(t))
1187
+ tags.push(t);
1188
+ }
1189
+ if (!description && enriched.description)
1190
+ description = enriched.description;
1191
+ if (!observed_at && enriched.observed_at)
1192
+ observed_at = enriched.observed_at;
1193
+ }
1194
+ // ── Required-field check (before any write) ───────────────────────────
1195
+ const missing = [];
1196
+ if (tags.length === 0)
1197
+ missing.push("tags");
1198
+ if (missing.length > 0) {
1199
+ throw new UsageError(`Memory is missing required frontmatter field(s): ${missing.join(", ")}. ` +
1200
+ "Provide them via --tag <value>, --auto (heuristics), or --enrich (LLM).");
1201
+ }
1202
+ // ── Build frontmatter and write ───────────────────────────────────────
1203
+ const frontmatterBlock = buildMemoryFrontmatter({
1204
+ description,
1205
+ tags,
1206
+ source,
1207
+ observed_at,
1208
+ expires,
1209
+ subjective,
1210
+ });
1211
+ const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
2069
1212
  const result = writeMarkdownAsset({
2070
1213
  type: "memory",
2071
- content: readMemoryContent(args.content),
1214
+ content: contentWithFrontmatter,
2072
1215
  name: args.name,
2073
1216
  fallbackPrefix: "memory",
2074
1217
  force: args.force,
@@ -2122,8 +1265,10 @@ const hintsCommand = defineCommand({
2122
1265
  detail: { type: "string", description: "Detail level (normal|full)", default: "normal" },
2123
1266
  },
2124
1267
  run({ args }) {
2125
- const detail = args.detail === "full" ? "full" : "normal";
2126
- process.stdout.write(loadHints(detail));
1268
+ if (args.detail !== "normal" && args.detail !== "full") {
1269
+ throw new UsageError(`Invalid value for --detail: ${args.detail}. Expected one of: normal|full.`);
1270
+ }
1271
+ process.stdout.write(loadHints(args.detail));
2127
1272
  },
2128
1273
  });
2129
1274
  const helpCommand = defineCommand({
@@ -2135,12 +1280,12 @@ const helpCommand = defineCommand({
2135
1280
  migrate: defineCommand({
2136
1281
  meta: {
2137
1282
  name: "migrate",
2138
- description: "Print release notes and migration guidance for a version",
1283
+ description: "Print release notes and migration guidance for a version. Bundled notes live in docs/migration/release-notes/<version>.md; an unknown version lists what's available.",
2139
1284
  },
2140
1285
  args: {
2141
1286
  version: {
2142
1287
  type: "positional",
2143
- description: "Version to review (for example 0.5.0, v0.5.0, or latest)",
1288
+ description: "Version to review (for example 0.6.0, v0.6.0, 0.6.0-rc1, or latest)",
2144
1289
  required: true,
2145
1290
  },
2146
1291
  },
@@ -2186,9 +1331,10 @@ function normalizeToggleTarget(target) {
2186
1331
  const normalized = target.trim().toLowerCase();
2187
1332
  if (normalized === "skills.sh" || normalized === "skills-sh")
2188
1333
  return "skills.sh";
2189
- if (normalized === "context-hub")
2190
- return "context-hub";
2191
- throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh, context-hub`);
1334
+ if (normalized === "context-hub") {
1335
+ throw new UsageError('The "context-hub" component is no longer toggleable. Run `akm add github:andrewyng/context-hub --name context-hub` to add it as a git stash.');
1336
+ }
1337
+ throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh`);
2192
1338
  }
2193
1339
  function toggleSkillsShRegistry(enabled) {
2194
1340
  const config = loadUserConfig();
@@ -2211,34 +1357,17 @@ function toggleSkillsShRegistry(enabled) {
2211
1357
  saveConfig({ ...config, registries });
2212
1358
  return { changed: true, component: SKILLS_SH_NAME, enabled: true };
2213
1359
  }
2214
- function toggleContextHubStash(enabled) {
2215
- const config = loadUserConfig();
2216
- const stashes = [...(config.stashes ?? [])];
2217
- const idx = stashes.findIndex((stash) => stash.name === CONTEXT_HUB_ALIAS_REF || stash.url === CONTEXT_HUB_ALIAS_URL);
2218
- if (idx >= 0) {
2219
- const existing = stashes[idx];
2220
- const wasEnabled = existing.enabled !== false;
2221
- existing.enabled = enabled;
2222
- saveConfig({ ...config, stashes });
2223
- return { changed: wasEnabled !== enabled, component: CONTEXT_HUB_ALIAS_REF, enabled };
2224
- }
2225
- if (!enabled) {
2226
- return { changed: false, component: CONTEXT_HUB_ALIAS_REF, enabled: false };
2227
- }
2228
- stashes.push({ type: "git", url: CONTEXT_HUB_ALIAS_URL, name: CONTEXT_HUB_ALIAS_REF, enabled: true });
2229
- saveConfig({ ...config, stashes });
2230
- return { changed: true, component: CONTEXT_HUB_ALIAS_REF, enabled: true };
2231
- }
2232
1360
  function toggleComponent(targetRaw, enabled) {
2233
1361
  const target = normalizeToggleTarget(targetRaw);
2234
1362
  if (target === "skills.sh")
2235
1363
  return toggleSkillsShRegistry(enabled);
2236
- return toggleContextHubStash(enabled);
1364
+ // normalizeToggleTarget throws for any unsupported target; this is unreachable.
1365
+ throw new UsageError(`Unsupported target "${targetRaw}". Supported targets: skills.sh`);
2237
1366
  }
2238
1367
  const enableCommand = defineCommand({
2239
- meta: { name: "enable", description: "Enable an optional component (skills.sh or context-hub)" },
1368
+ meta: { name: "enable", description: "Enable an optional component (skills.sh)" },
2240
1369
  args: {
2241
- target: { type: "positional", description: "Component to enable (skills.sh|context-hub)", required: true },
1370
+ target: { type: "positional", description: "Component to enable (skills.sh)", required: true },
2242
1371
  },
2243
1372
  run({ args }) {
2244
1373
  return runWithJsonErrors(() => {
@@ -2248,9 +1377,9 @@ const enableCommand = defineCommand({
2248
1377
  },
2249
1378
  });
2250
1379
  const disableCommand = defineCommand({
2251
- meta: { name: "disable", description: "Disable an optional component (skills.sh or context-hub)" },
1380
+ meta: { name: "disable", description: "Disable an optional component (skills.sh)" },
2252
1381
  args: {
2253
- target: { type: "positional", description: "Component to disable (skills.sh|context-hub)", required: true },
1382
+ target: { type: "positional", description: "Component to disable (skills.sh)", required: true },
2254
1383
  },
2255
1384
  run({ args }) {
2256
1385
  return runWithJsonErrors(() => {
@@ -2725,7 +1854,7 @@ const main = defineCommand({
2725
1854
  },
2726
1855
  args: {
2727
1856
  format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
2728
- detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
1857
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
2729
1858
  quiet: { type: "boolean", alias: "q", description: "Suppress stderr warnings", default: false },
2730
1859
  },
2731
1860
  subCommands: {
@@ -2773,14 +1902,32 @@ const WIKI_SUBCOMMAND_SET = new Set([
2773
1902
  "ingest",
2774
1903
  ]);
2775
1904
  const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
2776
- // citty reads process.argv directly and does not accept a custom argv array,
2777
- // so we must replace process.argv with the normalized version before runMain.
2778
- process.argv = normalizeShowArgv(process.argv);
2779
- runMain(main);
2780
1905
  // ── Exit codes ──────────────────────────────────────────────────────────────
2781
1906
  const EXIT_GENERAL = 1;
2782
1907
  const EXIT_USAGE = 2;
2783
1908
  const EXIT_CONFIG = 78;
1909
+ // citty reads process.argv directly and does not accept a custom argv array,
1910
+ // so we must replace process.argv with the normalized version before runMain.
1911
+ process.argv = normalizeShowArgv(process.argv);
1912
+ // Resolve output mode once at startup from the (normalized) argv and persisted
1913
+ // config. All subsequent output() calls read from this in-memory singleton.
1914
+ // `initOutputMode` can throw a UsageError when --format/--detail values are
1915
+ // invalid; surface it through the same JSON-error path the rest of the CLI uses
1916
+ // rather than letting the raw exception escape with a stack trace.
1917
+ try {
1918
+ initOutputMode(process.argv, loadConfig().output ?? {});
1919
+ }
1920
+ catch (error) {
1921
+ const message = error instanceof Error ? error.message : String(error);
1922
+ const hint = buildHint(message);
1923
+ const exitCode = classifyExitCode(error);
1924
+ const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
1925
+ ? error.code
1926
+ : undefined;
1927
+ console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
1928
+ process.exit(exitCode);
1929
+ }
1930
+ runMain(main);
2784
1931
  function classifyExitCode(error) {
2785
1932
  if (error instanceof UsageError)
2786
1933
  return EXIT_USAGE;
@@ -2802,7 +1949,12 @@ async function runWithJsonErrors(fn) {
2802
1949
  const message = error instanceof Error ? error.message : String(error);
2803
1950
  const hint = buildHint(message);
2804
1951
  const exitCode = classifyExitCode(error);
2805
- console.error(JSON.stringify({ ok: false, error: message, hint }, null, 2));
1952
+ // Surface machine-readable error code from typed errors when present so
1953
+ // scripts can branch on `.code` instead of message-string matching.
1954
+ const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
1955
+ ? error.code
1956
+ : undefined;
1957
+ console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
2806
1958
  process.exit(exitCode);
2807
1959
  }
2808
1960
  }
@@ -2822,7 +1974,7 @@ function buildHint(message) {
2822
1974
  if (message.includes("Invalid value for --format"))
2823
1975
  return "Pick one of: json, jsonl, text, yaml.";
2824
1976
  if (message.includes("Invalid value for --detail"))
2825
- return "Pick one of: brief, normal, full, summary.";
1977
+ return "Pick one of: brief, normal, full, summary, agent.";
2826
1978
  if (message.includes("expected JSON object with endpoint and model")) {
2827
1979
  return 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.';
2828
1980
  }
@@ -2926,294 +2078,3 @@ function loadHints(detail = "normal") {
2926
2078
  // Fallback for compiled binary — inline content
2927
2079
  return fallback;
2928
2080
  }
2929
- const EMBEDDED_HINTS = `# akm CLI
2930
-
2931
- You have access to a searchable library of scripts, skills, commands, agents, knowledge documents, workflows, wikis, and memories via \`akm\`. Search your sources first before writing something from scratch.
2932
-
2933
- ## Quick Reference
2934
-
2935
- \`\`\`sh
2936
- akm search "<query>" # Search all sources
2937
- akm curate "<task>" # Curate the best matches for a task
2938
- akm search "<query>" --type workflow # Filter to workflow assets
2939
- akm search "<query>" --source both # Also search registries
2940
- akm show <ref> # View asset details
2941
- akm workflow next <ref> # Start or resume a workflow
2942
- akm remember "Deployment needs VPN access" # Record a memory in your stash
2943
- akm import ./notes/release-checklist.md # Import a knowledge doc into your stash
2944
- akm wiki list # List available wikis
2945
- akm wiki ingest <name> # Print the ingest workflow for a wiki
2946
- akm feedback <ref> --positive|--negative # Record whether an asset helped
2947
- akm add <ref> # Add a source (npm, GitHub, git, local dir)
2948
- akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
2949
- akm save # Commit (and push if writable remote) changes in the primary stash
2950
- akm registry search "<query>" # Search all registries
2951
- \`\`\`
2952
-
2953
- ## Primary Asset Types
2954
-
2955
- | Type | What \`akm show\` returns |
2956
- | --- | --- |
2957
- | script | A \`run\` command you can execute directly |
2958
- | skill | Instructions to follow (read the full content) |
2959
- | command | A prompt template with placeholders to fill in |
2960
- | agent | A system prompt with model and tool hints |
2961
- | knowledge | A reference doc (use \`toc\` or \`section "..."\` to navigate) |
2962
- | workflow | Parsed steps plus workflow-specific execution commands |
2963
- | memory | Recalled context (read the content for background information) |
2964
- | vault | Key names only; use vault commands to inspect or load values safely |
2965
- | wiki | A page in a multi-wiki knowledge base. For any wiki task, start with \`akm wiki list\`, then \`akm wiki ingest <name>\` for the workflow. Run \`akm wiki -h\` for the full surface. |
2966
-
2967
- When an asset meaningfully helps or fails, record that with \`akm feedback\` so
2968
- future search ranking can learn from real usage.
2969
-
2970
- Run \`akm -h\` for the full command reference.
2971
- `;
2972
- const EMBEDDED_HINTS_FULL = `# akm CLI — Full Reference
2973
-
2974
- You have access to a searchable library of scripts, skills, commands, agents, knowledge documents, workflows, wikis, and memories via \`akm\`. Search your sources first before writing something from scratch.
2975
-
2976
- ## Search
2977
-
2978
- \`\`\`sh
2979
- akm search "<query>" # Search all sources
2980
- akm curate "<task>" # Curate the best matches for a task
2981
- akm search "<query>" --type workflow # Filter by asset type
2982
- akm search "<query>" --source both # Also search registries
2983
- akm search "<query>" --source registry # Search registries only
2984
- akm search "<query>" --limit 10 # Limit results
2985
- akm search "<query>" --detail full # Include scores, paths, timing
2986
- \`\`\`
2987
-
2988
- | Flag | Values | Default |
2989
- | --- | --- | --- |
2990
- | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`workflow\`, \`script\`, \`memory\`, \`vault\`, \`wiki\`, \`any\` | \`any\` |
2991
- | \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
2992
- | \`--limit\` | number | \`20\` |
2993
- | \`--format\` | \`json\`, \`jsonl\`, \`text\`, \`yaml\` | \`json\` |
2994
- | \`--detail\` | \`brief\`, \`normal\`, \`full\`, \`summary\` | \`brief\` |
2995
- | \`--for-agent\` | boolean | \`false\` |
2996
-
2997
- ## Curate
2998
-
2999
- Combine search + follow-up hints into a dense summary for a task or prompt.
3000
-
3001
- \`\`\`sh
3002
- akm curate "plan a release" # Pick top matches across asset types
3003
- akm curate "deploy a Bun app" --limit 3 # Keep the summary shorter
3004
- akm curate "review architecture" --type workflow # Restrict to one asset type
3005
- \`\`\`
3006
-
3007
- ## Show
3008
-
3009
- Display an asset by ref. Knowledge assets support view modes as positional arguments.
3010
-
3011
- \`\`\`sh
3012
- akm show script:deploy.sh # Show script (returns run command)
3013
- akm show skill:code-review # Show skill (returns full content)
3014
- akm show command:release # Show command (returns template)
3015
- akm show agent:architect # Show agent (returns system prompt)
3016
- akm show workflow:ship-release # Show parsed workflow steps
3017
- akm show knowledge:guide toc # Table of contents
3018
- akm show knowledge:guide section "Auth" # Specific section
3019
- akm show knowledge:guide lines 10 30 # Line range
3020
- akm show knowledge:my-doc # Show content (local or remote)
3021
- \`\`\`
3022
-
3023
- | Type | Key fields returned |
3024
- | --- | --- |
3025
- | script | \`run\`, \`setup\`, \`cwd\` |
3026
- | skill | \`content\` (full SKILL.md) |
3027
- | command | \`template\`, \`description\`, \`parameters\` |
3028
- | agent | \`prompt\`, \`description\`, \`modelHint\`, \`toolPolicy\` |
3029
- | knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
3030
- | workflow | \`workflowTitle\`, \`workflowParameters\`, \`steps\` |
3031
- | memory | \`content\` (recalled context) |
3032
- | vault | \`keys\`, \`comments\` |
3033
- | wiki | \`content\` (same view modes as knowledge). For any wiki task, run \`akm wiki list\` then \`akm wiki ingest <name>\` for the workflow. |
3034
-
3035
- ## Capture Knowledge While You Work
3036
-
3037
- \`\`\`sh
3038
- akm remember "Deployment needs VPN access" # Record a memory in your stash
3039
- akm remember --name release-retro < notes.md # Save multiline memory from stdin
3040
- akm import ./docs/auth-flow.md # Import a file as knowledge
3041
- akm import - --name scratch-notes < notes.md # Import stdin as a knowledge doc
3042
- akm workflow create ship-release # Create a workflow asset in the stash
3043
- akm workflow next workflow:ship-release # Start or resume the next workflow step
3044
- akm feedback skill:code-review --positive # Record that an asset helped
3045
- akm feedback agent:reviewer --negative # Record that an asset missed the mark
3046
- \`\`\`
3047
-
3048
- Use \`akm feedback\` whenever an asset materially helps or fails so future search
3049
- ranking can learn from actual usage.
3050
-
3051
- ## Wikis
3052
-
3053
- Multi-wiki knowledge bases (Karpathy-style). A stash-owned wiki lives at
3054
- \`<stashDir>/wikis/<name>/\`; external directories or repos can also be registered
3055
- as first-class wikis. akm owns lifecycle + raw-slug + lint + index regeneration
3056
- for stash-owned wikis; page edits use your native Read/Write/Edit tools.
3057
-
3058
- \`\`\`sh
3059
- akm wiki list # List wikis (name, pages, raws, last-modified)
3060
- akm wiki create research # Scaffold a new wiki
3061
- akm wiki register ics-docs ~/code/ics-documentation # Register an external wiki
3062
- akm wiki show research # Path, description, counts, last 3 log entries
3063
- akm wiki pages research # Page refs + descriptions (excludes schema/index/log/raw)
3064
- akm wiki search research "attention" # Scoped search (equivalent to --type wiki --wiki research)
3065
- akm wiki stash research ./paper.md # Copy source into raw/<slug>.md (never overwrites)
3066
- echo "..." | akm wiki stash research - # stdin form
3067
- akm wiki lint research # Structural checks: orphans, broken xrefs, uncited raws, stale index
3068
- akm wiki ingest research # Print the ingest workflow for this wiki (no action)
3069
- akm wiki remove research --force # Delete pages/schema/index/log; preserves raw/
3070
- akm wiki remove research --force --with-sources # Full nuke, including raw/
3071
- \`\`\`
3072
-
3073
- **For any wiki task, start with \`akm wiki list\`, then \`akm wiki ingest <name>\`
3074
- to get the step-by-step workflow.** Wiki pages are also addressable as
3075
- \`wiki:<name>/<page-path>\` and show up in stash-wide \`akm search\` as
3076
- \`type: wiki\`. Files under \`raw/\` and the wiki root infrastructure files
3077
- \`schema.md\`, \`index.md\`, and \`log.md\` are not indexed and do not appear in
3078
- search results. No \`--llm\` anywhere — akm never reasons about page content.
3079
-
3080
- ## Vaults
3081
-
3082
- Encrypted-at-rest key/value stores for secrets. Each vault is a \`.env\`-format
3083
- file at \`<stashDir>/vaults/<name>.env\`.
3084
-
3085
- \`\`\`sh
3086
- akm vault create prod # Create a new vault
3087
- akm vault set prod DB_URL postgres://... # Set a key (or KEY=VALUE combined form)
3088
- akm vault set prod DB_URL=postgres://... # Combined KEY=VALUE form also works
3089
- akm vault unset prod DB_URL # Remove a key
3090
- akm vault list vault:prod # List key names (no values)
3091
- akm vault show vault:prod # Same as list (alias)
3092
- akm vault load vault:prod # Print export statements to source
3093
- \`\`\`
3094
-
3095
- ## Workflows
3096
-
3097
- Step-based workflows stored as \`<stashDir>/workflows/<name>.md\`.
3098
-
3099
- \`\`\`sh
3100
- akm workflow template # Print a starter workflow template
3101
- akm workflow create ship-release # Scaffold a new workflow asset
3102
- akm workflow start workflow:ship-release # Start a new run
3103
- akm workflow next workflow:ship-release # Advance to the next step (or auto-start)
3104
- akm workflow complete <run-id> # Mark a step complete and advance
3105
- akm workflow status <run-id> # Show current run status
3106
- akm workflow resume <run-id> # Resume a blocked or failed run
3107
- akm workflow list # List all workflow runs
3108
- \`\`\`
3109
-
3110
- ## Clone
3111
-
3112
- Copy an asset to the working stash or a custom destination for editing.
3113
-
3114
- \`\`\`sh
3115
- akm clone <ref> # Clone to working stash
3116
- akm clone <ref> --name new-name # Rename on clone
3117
- akm clone <ref> --dest ./project/.claude # Clone to custom location
3118
- akm clone <ref> --force # Overwrite existing
3119
- akm clone "npm:@scope/pkg//script:deploy.sh" # Clone from remote package
3120
- \`\`\`
3121
-
3122
- When \`--dest\` is provided, \`akm init\` is not required first.
3123
-
3124
- ## Save
3125
-
3126
- Commit local changes in a git-backed stash. Behaviour adapts automatically:
3127
-
3128
- - **Not a git repo** — no-op (silent skip)
3129
- - **Git repo, no remote** — stage and commit only (the default stash always falls here)
3130
- - **Git repo, has remote, not writable** — stage and commit only
3131
- - **Git repo, has remote, \`writable: true\`** — stage, commit, and push
3132
-
3133
- \`\`\`sh
3134
- akm save # Save primary stash (timestamp message)
3135
- akm save -m "Add deploy skill" # Save with explicit message
3136
- akm save my-skills # Save a named writable git stash
3137
- akm save my-skills -m "Update patterns" # Save named stash with message
3138
- \`\`\`
3139
-
3140
- The \`--writable\` flag on \`akm add\` opts a remote git stash into push-on-save:
3141
-
3142
- \`\`\`sh
3143
- akm add git@github.com:org/skills.git --provider git --name my-skills --writable
3144
- \`\`\`
3145
-
3146
- ## Add & Manage Sources
3147
-
3148
- \`\`\`sh
3149
- akm add <ref> # Add a source
3150
- akm add @scope/kit # From npm (managed)
3151
- akm add owner/repo # From GitHub (managed)
3152
- akm add ./path/to/local/kit # Local directory
3153
- akm add git@github.com:org/repo.git --provider git --name my-skills --writable
3154
- akm enable skills.sh # Enable the skills.sh registry
3155
- akm disable skills.sh # Disable the skills.sh registry
3156
- akm enable context-hub # Add/enable the context-hub source
3157
- akm disable context-hub # Disable the context-hub source
3158
- akm list # List all sources
3159
- akm list --kind managed # List managed sources only
3160
- akm remove <target> # Remove by id, ref, path, or name
3161
- akm update --all # Update all managed sources
3162
- akm update <target> --force # Force re-download
3163
- \`\`\`
3164
-
3165
- ## Registries
3166
-
3167
- \`\`\`sh
3168
- akm registry list # List configured registries
3169
- akm registry add <url> # Add a registry
3170
- akm registry add <url> --name my-team # Add with label
3171
- akm registry add <url> --provider skills-sh # Specify provider type
3172
- akm registry remove <url-or-name> # Remove a registry
3173
- akm registry search "<query>" # Search all registries
3174
- akm registry search "<query>" --assets # Include asset-level results
3175
- akm registry build-index # Build ./index.json
3176
- akm registry build-index --out dist/index.json # Build to a custom path
3177
- \`\`\`
3178
-
3179
- ## Configuration
3180
-
3181
- \`\`\`sh
3182
- akm config list # Show current config
3183
- akm config get <key> # Read a value
3184
- akm config set <key> <value> # Set a value
3185
- akm config unset <key> # Remove a key
3186
- akm config path --all # Show all config paths
3187
- \`\`\`
3188
-
3189
- ## Other Commands
3190
-
3191
- \`\`\`sh
3192
- akm init # Initialize working stash
3193
- akm index # Rebuild search index
3194
- akm index --full # Full reindex
3195
- akm list # List all sources
3196
- akm upgrade # Upgrade akm using its install method
3197
- akm upgrade --check # Check for updates
3198
- akm help migrate 0.5.0 # Print migration notes for a release
3199
- akm hints # Print this reference
3200
- akm completions # Print bash completion script
3201
- akm completions --install # Install completions
3202
- \`\`\`
3203
-
3204
- ## Output Control
3205
-
3206
- All commands accept \`--format\` and \`--detail\` flags:
3207
-
3208
- - \`--format json\` (default) — structured JSON
3209
- - \`--format jsonl\` — one JSON object per line (streaming-friendly)
3210
- - \`--format text\` — human-readable plain text
3211
- - \`--format yaml\` — YAML output
3212
- - \`--detail brief\` (default) — compact output
3213
- - \`--detail normal\` — adds tags, refs, origins
3214
- - \`--detail full\` — includes scores, paths, timing, debug info
3215
- - \`--detail summary\` — metadata only (no content/template/prompt), under 200 tokens
3216
- - \`--for-agent\` — agent-optimized output: strips non-actionable fields (takes precedence over \`--detail\`)
3217
-
3218
- Run \`akm -h\` or \`akm <command> -h\` for per-command help.
3219
- `;