akm-cli 0.5.0 → 0.6.0-rc2

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 (118) hide show
  1. package/CHANGELOG.md +53 -5
  2. package/README.md +9 -9
  3. package/dist/cli.js +379 -1448
  4. package/dist/{completions.js → commands/completions.js} +1 -1
  5. package/dist/{config-cli.js → commands/config-cli.js} +109 -11
  6. package/dist/commands/curate.js +263 -0
  7. package/dist/{info.js → commands/info.js} +17 -11
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +14 -2
  10. package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
  11. package/dist/commands/migration-help.js +141 -0
  12. package/dist/{registry-search.js → commands/registry-search.js} +68 -9
  13. package/dist/commands/remember.js +178 -0
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +106 -81
  17. package/dist/{stash-add.js → commands/source-add.js} +133 -67
  18. package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
  19. package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
  20. package/dist/{vault.js → commands/vault.js} +43 -0
  21. package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
  22. package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
  23. package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
  24. package/dist/{common.js → core/common.js} +147 -50
  25. package/dist/{config.js → core/config.js} +288 -29
  26. package/dist/core/errors.js +90 -0
  27. package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
  28. package/dist/{paths.js → core/paths.js} +4 -4
  29. package/dist/core/write-source.js +280 -0
  30. package/dist/{local-search.js → indexer/db-search.js} +49 -32
  31. package/dist/{db.js → indexer/db.js} +210 -81
  32. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  33. package/dist/{indexer.js → indexer/indexer.js} +153 -30
  34. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  35. package/dist/{matchers.js → indexer/matchers.js} +4 -7
  36. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  37. package/dist/{search-source.js → indexer/search-source.js} +97 -55
  38. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  39. package/dist/{walker.js → indexer/walker.js} +1 -1
  40. package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
  41. package/dist/{llm.js → llm/client.js} +12 -48
  42. package/dist/llm/embedder.js +127 -0
  43. package/dist/llm/embedders/cache.js +47 -0
  44. package/dist/llm/embedders/local.js +152 -0
  45. package/dist/llm/embedders/remote.js +121 -0
  46. package/dist/llm/embedders/types.js +39 -0
  47. package/dist/llm/metadata-enhance.js +53 -0
  48. package/dist/output/cli-hints.js +301 -0
  49. package/dist/output/context.js +95 -0
  50. package/dist/{renderers.js → output/renderers.js} +57 -61
  51. package/dist/output/shapes.js +212 -0
  52. package/dist/output/text.js +520 -0
  53. package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
  54. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  55. package/dist/registry/factory.js +33 -0
  56. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  57. package/dist/registry/providers/index.js +11 -0
  58. package/dist/{providers → registry/providers}/skills-sh.js +60 -4
  59. package/dist/{providers → registry/providers}/static-index.js +126 -56
  60. package/dist/registry/providers/types.js +25 -0
  61. package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
  62. package/dist/{detect.js → setup/detect.js} +0 -27
  63. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  64. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  65. package/dist/{setup.js → setup/setup.js} +162 -129
  66. package/dist/setup/steps.js +45 -0
  67. package/dist/{kit-include.js → sources/include.js} +1 -1
  68. package/dist/sources/provider-factory.js +36 -0
  69. package/dist/sources/provider.js +21 -0
  70. package/dist/sources/providers/filesystem.js +35 -0
  71. package/dist/{stash-providers → sources/providers}/git.js +218 -28
  72. package/dist/{stash-providers → sources/providers}/index.js +4 -4
  73. package/dist/sources/providers/install-types.js +14 -0
  74. package/dist/sources/providers/npm.js +160 -0
  75. package/dist/sources/providers/provider-utils.js +173 -0
  76. package/dist/sources/providers/sync-from-ref.js +45 -0
  77. package/dist/sources/providers/tar-utils.js +154 -0
  78. package/dist/{stash-providers → sources/providers}/website.js +60 -20
  79. package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
  80. package/dist/{wiki.js → wiki/wiki.js} +18 -17
  81. package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
  82. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  83. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  84. package/dist/workflows/document-cache.js +20 -0
  85. package/dist/workflows/parser.js +379 -0
  86. package/dist/workflows/renderer.js +78 -0
  87. package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
  88. package/dist/workflows/schema.js +11 -0
  89. package/dist/workflows/validator.js +48 -0
  90. package/docs/README.md +30 -0
  91. package/docs/migration/release-notes/0.0.13.md +4 -0
  92. package/docs/migration/release-notes/0.1.0.md +6 -0
  93. package/docs/migration/release-notes/0.2.0.md +6 -0
  94. package/docs/migration/release-notes/0.3.0.md +5 -0
  95. package/docs/migration/release-notes/0.5.0.md +6 -0
  96. package/docs/migration/release-notes/0.6.0.md +75 -0
  97. package/docs/migration/release-notes/README.md +21 -0
  98. package/package.json +3 -2
  99. package/dist/embedder.js +0 -351
  100. package/dist/errors.js +0 -34
  101. package/dist/migration-help.js +0 -110
  102. package/dist/registry-factory.js +0 -19
  103. package/dist/registry-install.js +0 -532
  104. package/dist/ripgrep.js +0 -2
  105. package/dist/stash-provider-factory.js +0 -35
  106. package/dist/stash-provider.js +0 -1
  107. package/dist/stash-providers/filesystem.js +0 -41
  108. package/dist/stash-providers/openviking.js +0 -348
  109. package/dist/stash-providers/provider-utils.js +0 -11
  110. package/dist/stash-types.js +0 -1
  111. package/dist/workflow-markdown.js +0 -251
  112. /package/dist/{markdown.js → core/markdown.js} +0 -0
  113. /package/dist/{warn.js → core/warn.js} +0 -0
  114. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  115. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  116. /package/dist/{github.js → integrations/github.js} +0 -0
  117. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  118. /package/dist/{registry-types.js → sources/types.js} +0 -0
package/dist/cli.js CHANGED
@@ -2,84 +2,71 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { defineCommand, runMain } from "citty";
5
- import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./asset-spec";
6
- import { isWithin, resolveStashDir } from "./common";
7
- import { generateBashCompletions, installBashCompletions } from "./completions";
8
- import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./config";
9
- import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
10
- import { closeDatabase, openDatabase } from "./db";
11
- import { ConfigError, NotFoundError, UsageError } from "./errors";
12
- import { akmIndex } from "./indexer";
13
- import { assembleInfo } from "./info";
14
- import { akmInit } from "./init";
15
- import { formatInstallAuditSummary } from "./install-audit";
16
- import { akmListSources, akmRemove, akmUpdate } from "./installed-kits";
17
- import { renderMigrationHelp } from "./migration-help";
18
- import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
19
- import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
20
- import { searchRegistry } from "./registry-search";
21
- import { checkForUpdate, performUpgrade } from "./self-update";
22
- import { akmAdd } from "./stash-add";
23
- import { akmClone } from "./stash-clone";
24
- import { saveGitStash } from "./stash-providers/git";
25
- import { parseAssetRef } from "./stash-ref";
26
- import { akmSearch, parseSearchSource } from "./stash-search";
27
- import { akmShowUnified } from "./stash-show";
28
- import { addStash } from "./stash-source-manage";
29
- import { insertUsageEvent } from "./usage-events";
5
+ import { generateBashCompletions, installBashCompletions } from "./commands/completions";
6
+ import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./commands/config-cli";
7
+ import { akmCurate } from "./commands/curate";
8
+ import { assembleInfo } from "./commands/info";
9
+ import { akmInit } from "./commands/init";
10
+ import { akmListSources, akmRemove, akmUpdate } from "./commands/installed-stashes";
11
+ import { renderMigrationHelp } from "./commands/migration-help";
12
+ import { searchRegistry } from "./commands/registry-search";
13
+ import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich, } from "./commands/remember";
14
+ import { akmSearch, parseSearchSource } from "./commands/search";
15
+ import { checkForUpdate, performUpgrade } from "./commands/self-update";
16
+ import { akmShowUnified } from "./commands/show";
17
+ import { akmAdd } from "./commands/source-add";
18
+ import { akmClone } from "./commands/source-clone";
19
+ import { addStash } from "./commands/source-manage";
20
+ import { parseAssetRef } from "./core/asset-ref";
21
+ import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./core/asset-spec";
22
+ import { isWithin, resolveStashDir, tryReadStdinText } from "./core/common";
23
+ import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./core/config";
24
+ import { ConfigError, NotFoundError, UsageError } from "./core/errors";
25
+ import { getCacheDir, getDbPath, getDefaultStashDir } from "./core/paths";
26
+ import { setQuiet, warn } from "./core/warn";
27
+ import { resolveWriteTarget, writeAssetToSource } from "./core/write-source";
28
+ import { closeDatabase, openDatabase } from "./indexer/db";
29
+ import { akmIndex } from "./indexer/indexer";
30
+ import { resolveSourceEntries } from "./indexer/search-source";
31
+ import { insertUsageEvent } from "./indexer/usage-events";
32
+ import { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "./output/cli-hints";
33
+ import { getHyphenatedArg, getHyphenatedBoolean, getOutputMode, initOutputMode, parseFlagValue, } from "./output/context";
34
+ import { shapeForCommand } from "./output/shapes";
35
+ import { formatPlain, outputJsonl } from "./output/text";
36
+ import { buildRegistryIndex, writeRegistryIndex } from "./registry/build-index";
37
+ import { resolveSourcesForOrigin } from "./registry/origin-resolve";
38
+ import { saveGitStash } from "./sources/providers/git";
39
+ import { resolveAssetPath } from "./sources/resolve";
30
40
  import { pkgVersion } from "./version";
31
- import { setQuiet, warn } from "./warn";
32
- import { createWorkflowAsset, getWorkflowTemplate } from "./workflow-authoring";
33
- import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflow-cli";
34
- 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;
41
+ import { createWorkflowAsset, formatWorkflowErrors, getWorkflowTemplate, validateWorkflowSource, } from "./workflows/authoring";
42
+ import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflows/cli";
43
+ import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflows/runs";
38
44
  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
45
  const SKILLS_SH_NAME = "skills.sh";
42
46
  const SKILLS_SH_URL = "https://skills.sh";
43
47
  const SKILLS_SH_PROVIDER = "skills-sh";
44
48
  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) {
49
+ /**
50
+ * Collect all occurrences of a repeatable flag from process.argv.
51
+ * Citty's StringArgDef only exposes the last value when a flag is repeated,
52
+ * so for repeatable CLI args (like `--tag foo --tag bar`) we read argv directly.
53
+ * Supports both `--flag value` and `--flag=value` forms.
54
+ */
55
+ function parseAllFlagValues(flag) {
56
+ const values = [];
60
57
  for (let i = 0; i < process.argv.length; i++) {
61
58
  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);
59
+ if (arg === flag && i + 1 < process.argv.length) {
60
+ values.push(process.argv[i + 1]);
61
+ }
62
+ else if (arg.startsWith(`${flag}=`)) {
63
+ values.push(arg.slice(flag.length + 1));
64
+ }
66
65
  }
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 };
66
+ return values;
80
67
  }
81
68
  function output(command, result) {
82
- const mode = resolveOutputMode();
69
+ const mode = getOutputMode();
83
70
  const shaped = shapeForCommand(command, result, mode.detail, mode.forAgent);
84
71
  if (mode.format === "jsonl") {
85
72
  outputJsonl(command, shaped);
@@ -99,936 +86,12 @@ function output(command, result) {
99
86
  }
100
87
  }
101
88
  }
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
89
  /**
1027
90
  * Module Naming:
1028
- * - stash-* : Asset operations (search, show, add, clone)
1029
- * - stash-provider-* : Runtime data source providers (filesystem, openviking)
1030
- * - registry-* : Discovery from remote registries (npm, GitHub)
1031
- * - installed-kits : Unified source operations (list, remove, update)
91
+ * - sources/* : Asset operations (search, show, add, clone)
92
+ * - sources/providers/* : Runtime data source providers (filesystem, git, website, npm)
93
+ * - registry/* : Discovery from remote registries (npm, GitHub)
94
+ * - installed-stashes : Unified source operations (list, remove, update)
1032
95
  */
1033
96
  const setupCommand = defineCommand({
1034
97
  meta: {
@@ -1037,7 +100,7 @@ const setupCommand = defineCommand({
1037
100
  },
1038
101
  async run() {
1039
102
  await runWithJsonErrors(async () => {
1040
- const { runSetupWizard } = await import("./setup");
103
+ const { runSetupWizard } = await import("./setup/setup");
1041
104
  await runSetupWizard();
1042
105
  });
1043
106
  },
@@ -1093,10 +156,14 @@ const searchCommand = defineCommand({
1093
156
  limit: { type: "string", description: "Maximum number of results" },
1094
157
  source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
1095
158
  format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
1096
- detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
159
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
1097
160
  },
1098
161
  async run({ args }) {
1099
162
  await runWithJsonErrors(async () => {
163
+ const query = (args.query ?? "").trim();
164
+ if (!query) {
165
+ throw new UsageError('A search query is required. Usage: akm search "<query>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT");
166
+ }
1100
167
  const type = args.type;
1101
168
  const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
1102
169
  if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
@@ -1104,7 +171,7 @@ const searchCommand = defineCommand({
1104
171
  }
1105
172
  const limit = limitRaw;
1106
173
  const source = parseSearchSource(args.source);
1107
- const result = await akmSearch({ query: args.query, type, limit, source });
174
+ const result = await akmSearch({ query, type, limit, source });
1108
175
  output("search", result);
1109
176
  });
1110
177
  },
@@ -1129,15 +196,7 @@ const curateCommand = defineCommand({
1129
196
  }
1130
197
  const limit = limitRaw && limitRaw > 0 ? limitRaw : 4;
1131
198
  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);
199
+ const curated = await akmCurate({ query: args.query, type, limit, source });
1141
200
  output("curate", curated);
1142
201
  });
1143
202
  },
@@ -1153,7 +212,7 @@ const addCommand = defineCommand({
1153
212
  description: "Path, URL, or registry ref (website URL, npm package, owner/repo, git URL, or local directory)",
1154
213
  required: true,
1155
214
  },
1156
- provider: { type: "string", description: "Provider type (e.g. openviking). Required for URL sources." },
215
+ provider: { type: "string", description: "Provider type (e.g. website, npm). Required for URL sources." },
1157
216
  options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
1158
217
  name: { type: "string", description: "Human-friendly name for the source" },
1159
218
  writable: {
@@ -1176,16 +235,6 @@ const addCommand = defineCommand({
1176
235
  async run({ args }) {
1177
236
  await runWithJsonErrors(async () => {
1178
237
  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
238
  // URL with --provider → stash source (remote or git provider)
1190
239
  if (args.provider) {
1191
240
  if (shouldWarnOnPlainHttp(ref)) {
@@ -1213,7 +262,7 @@ const addCommand = defineCommand({
1213
262
  options: parsedOptions,
1214
263
  writable: args.writable,
1215
264
  });
1216
- output("stash-add", result);
265
+ output("add", result);
1217
266
  return;
1218
267
  }
1219
268
  if (shouldWarnOnPlainHttp(ref)) {
@@ -1221,7 +270,7 @@ const addCommand = defineCommand({
1221
270
  }
1222
271
  const websiteOptions = buildWebsiteOptions(args);
1223
272
  if (args.type === "wiki") {
1224
- const { registerWikiSource } = await import("./stash-add");
273
+ const { registerWikiSource } = await import("./commands/source-add");
1225
274
  const result = await registerWikiSource({
1226
275
  ref,
1227
276
  name: args.name,
@@ -1324,7 +373,7 @@ const upgradeCommand = defineCommand({
1324
373
  args: {
1325
374
  check: { type: "boolean", description: "Check for updates without installing", default: false },
1326
375
  force: { type: "boolean", description: "Force upgrade even if on latest", default: false },
1327
- skipChecksum: {
376
+ "skip-checksum": {
1328
377
  type: "boolean",
1329
378
  description: "Skip checksum verification (not recommended)",
1330
379
  default: false,
@@ -1337,7 +386,8 @@ const upgradeCommand = defineCommand({
1337
386
  output("upgrade", check);
1338
387
  return;
1339
388
  }
1340
- const result = await performUpgrade(check, { force: args.force, skipChecksum: args.skipChecksum });
389
+ const skipChecksum = getHyphenatedBoolean(args, "skip-checksum");
390
+ const result = await performUpgrade(check, { force: args.force, skipChecksum });
1341
391
  output("upgrade", result);
1342
392
  });
1343
393
  },
@@ -1350,38 +400,42 @@ const showCommand = defineCommand({
1350
400
  args: {
1351
401
  ref: { type: "positional", description: "Asset ref (type:name)", required: true },
1352
402
  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" },
403
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
1358
404
  },
1359
405
  async run({ args }) {
1360
406
  await runWithJsonErrors(async () => {
407
+ // The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
408
+ // is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
409
+ // by `normalizeShowArgv` before citty parses argv. We read those values
410
+ // directly via `parseFlagValue` so the flags don't surface as user-facing
411
+ // options in `akm show --help`.
412
+ const akmView = parseFlagValue(process.argv, "--akmView");
413
+ const akmHeading = parseFlagValue(process.argv, "--akmHeading");
414
+ const akmStart = parseFlagValue(process.argv, "--akmStart");
415
+ const akmEnd = parseFlagValue(process.argv, "--akmEnd");
1361
416
  let view;
1362
- if (args.akmView) {
1363
- switch (args.akmView) {
417
+ if (akmView) {
418
+ switch (akmView) {
1364
419
  case "section":
1365
- view = { mode: "section", heading: args.akmHeading ?? "" };
420
+ view = { mode: "section", heading: akmHeading ?? "" };
1366
421
  break;
1367
422
  case "lines":
1368
423
  view = {
1369
424
  mode: "lines",
1370
- start: Number(args.akmStart ?? "1"),
1371
- end: args.akmEnd ? parseInt(args.akmEnd, 10) : Number.MAX_SAFE_INTEGER,
425
+ start: Number(akmStart ?? "1"),
426
+ end: akmEnd ? parseInt(akmEnd, 10) : Number.MAX_SAFE_INTEGER,
1372
427
  };
1373
428
  break;
1374
429
  case "toc":
1375
430
  case "frontmatter":
1376
431
  case "full":
1377
- view = { mode: args.akmView };
432
+ view = { mode: akmView };
1378
433
  break;
1379
434
  default:
1380
- throw new UsageError(`Unknown view mode: ${args.akmView}. Expected one of: full|toc|frontmatter|section|lines`);
435
+ throw new UsageError(`Unknown view mode: ${akmView}. Expected one of: full|toc|frontmatter|section|lines`);
1381
436
  }
1382
437
  }
1383
- // Map CLI detail level to ShowDetailLevel for the show function
1384
- const cliDetail = resolveOutputMode().detail;
438
+ const cliDetail = getOutputMode().detail;
1385
439
  const showDetail = cliDetail === "summary" ? "summary" : undefined;
1386
440
  const result = await akmShowUnified({ ref: args.ref, view, detail: showDetail });
1387
441
  output("show", result);
@@ -1510,7 +564,7 @@ const saveCommand = defineCommand({
1510
564
  // before any standalone occurrence of the same value in the save
1511
565
  // subcommand's argv slice. This preserves legitimate invocations
1512
566
  // like `akm save json --format json`.
1513
- const parsedFormat = parseFlagValue("--format");
567
+ const parsedFormat = parseFlagValue(process.argv, "--format");
1514
568
  const effectiveName = args.name !== undefined &&
1515
569
  parsedFormat !== undefined &&
1516
570
  args.name === parsedFormat &&
@@ -1593,7 +647,7 @@ const cloneCommand = defineCommand({
1593
647
  },
1594
648
  });
1595
649
  const registryCommand = defineCommand({
1596
- meta: { name: "registry", description: "Manage kit registries" },
650
+ meta: { name: "registry", description: "Manage stash registries" },
1597
651
  subCommands: {
1598
652
  list: defineCommand({
1599
653
  meta: { name: "list", description: "List configured registries" },
@@ -1612,6 +666,11 @@ const registryCommand = defineCommand({
1612
666
  name: { type: "string", description: "Human-friendly name for the registry" },
1613
667
  provider: { type: "string", description: "Provider type (e.g. static-index, skills-sh)" },
1614
668
  options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
669
+ "allow-insecure": {
670
+ type: "boolean",
671
+ description: "Allow a plain HTTP registry URL (otherwise rejected)",
672
+ default: false,
673
+ },
1615
674
  },
1616
675
  run({ args }) {
1617
676
  return runWithJsonErrors(() => {
@@ -1619,7 +678,12 @@ const registryCommand = defineCommand({
1619
678
  throw new UsageError("Registry URL must start with http:// or https://");
1620
679
  }
1621
680
  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.");
681
+ const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
682
+ if (!allowInsecure) {
683
+ throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
684
+ "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
685
+ }
686
+ warn("Warning: registry URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious index.");
1623
687
  }
1624
688
  const config = loadUserConfig();
1625
689
  const registries = [...(config.registries ?? [])];
@@ -1668,7 +732,7 @@ const registryCommand = defineCommand({
1668
732
  },
1669
733
  }),
1670
734
  search: defineCommand({
1671
- meta: { name: "search", description: "Search enabled registries for kits" },
735
+ meta: { name: "search", description: "Search enabled registries for stashes" },
1672
736
  args: {
1673
737
  query: { type: "positional", description: "Search query", required: true },
1674
738
  limit: { type: "string", description: "Maximum number of results" },
@@ -1690,15 +754,15 @@ const registryCommand = defineCommand({
1690
754
  args: {
1691
755
  out: { type: "string", description: "Output path for the generated index", default: "index.json" },
1692
756
  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" },
757
+ "npm-registry": { type: "string", description: "Override npm registry base URL" },
758
+ "github-api": { type: "string", description: "Override GitHub API base URL" },
1695
759
  },
1696
760
  async run({ args }) {
1697
761
  await runWithJsonErrors(async () => {
1698
762
  const result = await buildRegistryIndex({
1699
763
  manualEntriesPath: args.manual,
1700
- npmRegistryBase: args.npmRegistry,
1701
- githubApiBase: args.githubApi,
764
+ npmRegistryBase: getHyphenatedArg(args, "npm-registry"),
765
+ githubApiBase: getHyphenatedArg(args, "github-api"),
1702
766
  });
1703
767
  const outPath = writeRegistryIndex(result.index, args.out);
1704
768
  output("registry-build-index", {
@@ -1755,12 +819,6 @@ const feedbackCommand = defineCommand({
1755
819
  });
1756
820
  },
1757
821
  });
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
822
  function normalizeMarkdownAssetName(name, fallback) {
1765
823
  const trimmed = (name ?? fallback)
1766
824
  .trim()
@@ -1792,13 +850,6 @@ function inferAssetName(content, fallbackPrefix, preferred) {
1792
850
  const basis = preferred?.trim() || firstNonEmptyLine || fallbackPrefix;
1793
851
  return slugifyAssetName(basis, fallbackPrefix);
1794
852
  }
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
853
  function readKnowledgeContent(source) {
1803
854
  if (source === "-") {
1804
855
  const content = tryReadStdinText();
@@ -1823,24 +874,30 @@ function readKnowledgeContent(source) {
1823
874
  preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
1824
875
  };
1825
876
  }
1826
- function writeMarkdownAsset(options) {
1827
- const stashDir = resolveStashDir();
1828
- const typeRoot = path.join(stashDir, options.type === "knowledge" ? "knowledge" : "memories");
1829
- fs.mkdirSync(typeRoot, { recursive: true });
877
+ async function writeMarkdownAsset(options) {
878
+ // Resolve write target via the v1 precedence chain (`--target` →
879
+ // `defaultWriteTarget` working stash). Per spec §10 step 5, this is the
880
+ // single dispatch point — `core/write-source.ts` owns all kind-branching.
881
+ const cfg = loadConfig();
882
+ const { source, config } = resolveWriteTarget(cfg, options.target);
883
+ const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
1830
884
  const normalizedName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
885
+ // Pre-flight: existence + force semantics. The helper itself overwrites
886
+ // unconditionally; the CLI surfaces a friendlier UsageError before any
887
+ // disk activity when --force is absent.
1831
888
  const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
1832
889
  if (!isWithin(assetPath, typeRoot)) {
1833
890
  throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
1834
891
  }
1835
892
  if (fs.existsSync(assetPath) && !options.force) {
1836
- throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`);
893
+ throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
1837
894
  }
1838
- fs.mkdirSync(path.dirname(assetPath), { recursive: true });
1839
- fs.writeFileSync(assetPath, options.content.endsWith("\n") ? options.content : `${options.content}\n`, "utf8");
895
+ // Delegate the actual write (and optional git commit/push) to the helper.
896
+ const result = await writeAssetToSource(source, config, { type: options.type, name: normalizedName }, options.content);
1840
897
  return {
1841
- ref: `${options.type}:${normalizedName}`,
1842
- path: assetPath,
1843
- stashDir,
898
+ ref: result.ref,
899
+ path: result.path,
900
+ stashDir: source.path,
1844
901
  };
1845
902
  }
1846
903
  const workflowStartCommand = defineCommand({
@@ -1928,11 +985,11 @@ const workflowStatusCommand = defineCommand({
1928
985
  const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
1929
986
  const { runs } = listWorkflowRuns({ workflowRef: ref });
1930
987
  if (runs.length === 0) {
1931
- throw new NotFoundError(`No workflow runs found for ${ref}`);
988
+ throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
1932
989
  }
1933
990
  const mostRecent = runs[0];
1934
991
  if (!mostRecent)
1935
- throw new NotFoundError(`No workflow runs found for ${ref}`);
992
+ throw new NotFoundError(`No workflow runs found for ${ref}`, "WORKFLOW_NOT_FOUND");
1936
993
  const result = getWorkflowStatus(mostRecent.id);
1937
994
  output("workflow-status", result);
1938
995
  }
@@ -1978,8 +1035,8 @@ const workflowCreateCommand = defineCommand({
1978
1035
  default: false,
1979
1036
  },
1980
1037
  },
1981
- run({ args }) {
1982
- return runWithJsonErrors(() => {
1038
+ async run({ args }) {
1039
+ return runWithJsonErrors(async () => {
1983
1040
  const namePattern = /^[a-z0-9][a-z0-9._/-]*$/;
1984
1041
  if (!namePattern.test(args.name)) {
1985
1042
  throw new UsageError("Workflow name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, underscores, and slashes.");
@@ -1992,6 +1049,10 @@ const workflowCreateCommand = defineCommand({
1992
1049
  from: args.from,
1993
1050
  force: args.force,
1994
1051
  });
1052
+ // Index the newly-written workflow so `akm workflow start` can resolve
1053
+ // a workflowEntryId without requiring an explicit `akm index` call
1054
+ // first. Uses the same incremental index path that `akm add` uses.
1055
+ await akmIndex({ stashDir: result.stashDir });
1995
1056
  output("workflow-create", { ok: true, ...result });
1996
1057
  });
1997
1058
  },
@@ -2005,6 +1066,55 @@ const workflowTemplateCommand = defineCommand({
2005
1066
  process.stdout.write(getWorkflowTemplate());
2006
1067
  },
2007
1068
  });
1069
+ const workflowValidateCommand = defineCommand({
1070
+ meta: {
1071
+ name: "validate",
1072
+ description: "Validate a workflow markdown file or ref and print any errors",
1073
+ },
1074
+ args: {
1075
+ target: {
1076
+ type: "positional",
1077
+ description: "Workflow ref (workflow:<name>) or filesystem path to a workflow .md",
1078
+ required: true,
1079
+ },
1080
+ },
1081
+ async run({ args }) {
1082
+ return runWithJsonErrors(async () => {
1083
+ const filePath = await resolveWorkflowFilePath(args.target);
1084
+ const { parse } = validateWorkflowSource(filePath);
1085
+ if (parse.ok) {
1086
+ output("workflow-validate", {
1087
+ ok: true,
1088
+ path: filePath,
1089
+ title: parse.document.title,
1090
+ stepCount: parse.document.steps.length,
1091
+ });
1092
+ return;
1093
+ }
1094
+ throw new UsageError(formatWorkflowErrors(filePath, parse.errors));
1095
+ });
1096
+ },
1097
+ });
1098
+ async function resolveWorkflowFilePath(target) {
1099
+ if (!target.startsWith("workflow:"))
1100
+ return target;
1101
+ const parsed = parseAssetRef(target);
1102
+ if (parsed.type !== "workflow") {
1103
+ throw new UsageError(`Expected a workflow ref (workflow:<name>), got "${target}".`);
1104
+ }
1105
+ const config = loadConfig();
1106
+ const allSources = resolveSourceEntries(undefined, config);
1107
+ const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
1108
+ for (const source of searchSources) {
1109
+ try {
1110
+ return await resolveAssetPath(source.path, "workflow", parsed.name);
1111
+ }
1112
+ catch {
1113
+ /* try next source */
1114
+ }
1115
+ }
1116
+ throw new UsageError(`Workflow not found for ref: workflow:${parsed.name}`);
1117
+ }
2008
1118
  const workflowResumeCommand = defineCommand({
2009
1119
  meta: {
2010
1120
  name: "resume",
@@ -2016,7 +1126,7 @@ const workflowResumeCommand = defineCommand({
2016
1126
  run({ args }) {
2017
1127
  return runWithJsonErrors(() => {
2018
1128
  const result = resumeWorkflowRun(args.runId);
2019
- output("workflow-run", result);
1129
+ output("workflow-resume", result);
2020
1130
  });
2021
1131
  },
2022
1132
  });
@@ -2034,6 +1144,7 @@ const workflowCommand = defineCommand({
2034
1144
  create: workflowCreateCommand,
2035
1145
  template: workflowTemplateCommand,
2036
1146
  resume: workflowResumeCommand,
1147
+ validate: workflowValidateCommand,
2037
1148
  },
2038
1149
  run({ args }) {
2039
1150
  return runWithJsonErrors(() => {
@@ -2063,15 +1174,123 @@ const rememberCommand = defineCommand({
2063
1174
  description: "Overwrite an existing memory with the same name",
2064
1175
  default: false,
2065
1176
  },
1177
+ description: {
1178
+ type: "string",
1179
+ description: "Short description written to frontmatter (persisted as the memory's description field)",
1180
+ },
1181
+ tag: {
1182
+ type: "string",
1183
+ description: "Tag to add to the memory (repeatable: --tag foo --tag bar)",
1184
+ },
1185
+ expires: {
1186
+ type: "string",
1187
+ description: "Expiry duration shorthand (e.g. 30d, 12h, 6m). Resolved to an ISO date.",
1188
+ },
1189
+ source: {
1190
+ type: "string",
1191
+ description: "Source reference (URL, asset ref, file path, or any free-form string)",
1192
+ },
1193
+ auto: {
1194
+ type: "boolean",
1195
+ description: "Apply heuristic tagging (code, subjective, source, observed_at) from the body",
1196
+ default: false,
1197
+ },
1198
+ enrich: {
1199
+ type: "boolean",
1200
+ description: "Call the configured LLM to propose tags and description (requires LLM config)",
1201
+ default: false,
1202
+ },
1203
+ target: {
1204
+ type: "string",
1205
+ description: "Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working stash.",
1206
+ },
2066
1207
  },
2067
- run({ args }) {
2068
- return runWithJsonErrors(() => {
2069
- const result = writeMarkdownAsset({
1208
+ async run({ args }) {
1209
+ return runWithJsonErrors(async () => {
1210
+ const body = readMemoryContent(args.content);
1211
+ // Determine if the user has requested any structured metadata mode.
1212
+ // Collect all --tag occurrences directly from process.argv because citty
1213
+ // only exposes the last value for repeated string flags.
1214
+ const rawTags = parseAllFlagValues("--tag");
1215
+ const hasStructuredArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description || args.auto || args.enrich;
1216
+ if (!hasStructuredArgs) {
1217
+ const result = await writeMarkdownAsset({
1218
+ type: "memory",
1219
+ content: body,
1220
+ name: args.name,
1221
+ fallbackPrefix: "memory",
1222
+ force: args.force,
1223
+ target: args.target,
1224
+ });
1225
+ output("remember", { ok: true, ...result });
1226
+ return;
1227
+ }
1228
+ // ── Accumulate metadata from all three modes ──────────────────────────
1229
+ // Start with CLI args (Mode 1: always)
1230
+ const tags = [...rawTags];
1231
+ // --description is persisted as-is; LLM enrichment may fill it if absent.
1232
+ let description = args.description || undefined;
1233
+ let source = args.source;
1234
+ let observed_at;
1235
+ let expires;
1236
+ let subjective;
1237
+ // Resolve --expires to an ISO date string
1238
+ if (args.expires) {
1239
+ const durationMs = parseDuration(args.expires);
1240
+ const expiresDate = new Date(Date.now() + durationMs);
1241
+ expires = expiresDate.toISOString().slice(0, 10);
1242
+ }
1243
+ // Mode 2: --auto heuristics
1244
+ if (args.auto) {
1245
+ const auto = runAutoHeuristics(body);
1246
+ for (const t of auto.tags) {
1247
+ if (!tags.includes(t))
1248
+ tags.push(t);
1249
+ }
1250
+ if (!source && auto.source)
1251
+ source = auto.source;
1252
+ if (!observed_at && auto.observed_at)
1253
+ observed_at = auto.observed_at;
1254
+ if (!subjective && auto.subjective)
1255
+ subjective = auto.subjective;
1256
+ }
1257
+ // Mode 3: --enrich LLM (fail-soft)
1258
+ if (args.enrich) {
1259
+ const enriched = await runLlmEnrich(body);
1260
+ for (const t of enriched.tags) {
1261
+ if (!tags.includes(t))
1262
+ tags.push(t);
1263
+ }
1264
+ if (!description && enriched.description)
1265
+ description = enriched.description;
1266
+ if (!observed_at && enriched.observed_at)
1267
+ observed_at = enriched.observed_at;
1268
+ }
1269
+ // ── Required-field check (before any write) ───────────────────────────
1270
+ const missing = [];
1271
+ if (tags.length === 0)
1272
+ missing.push("tags");
1273
+ if (missing.length > 0) {
1274
+ throw new UsageError(`Memory is missing required frontmatter field(s): ${missing.join(", ")}. ` +
1275
+ "Provide them via --tag <value>, --auto (heuristics), or --enrich (LLM).");
1276
+ }
1277
+ // ── Build frontmatter and write ───────────────────────────────────────
1278
+ const frontmatterBlock = buildMemoryFrontmatter({
1279
+ description,
1280
+ tags,
1281
+ source,
1282
+ observed_at,
1283
+ expires,
1284
+ subjective,
1285
+ });
1286
+ const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
1287
+ const result = await writeMarkdownAsset({
2070
1288
  type: "memory",
2071
- content: readMemoryContent(args.content),
1289
+ content: contentWithFrontmatter,
2072
1290
  name: args.name,
2073
1291
  fallbackPrefix: "memory",
2074
1292
  force: args.force,
1293
+ target: args.target,
2075
1294
  });
2076
1295
  output("remember", { ok: true, ...result });
2077
1296
  });
@@ -2097,17 +1316,22 @@ const importKnowledgeCommand = defineCommand({
2097
1316
  description: "Overwrite an existing knowledge document with the same name",
2098
1317
  default: false,
2099
1318
  },
1319
+ target: {
1320
+ type: "string",
1321
+ description: "Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working stash.",
1322
+ },
2100
1323
  },
2101
1324
  async run({ args }) {
2102
1325
  return runWithJsonErrors(async () => {
2103
1326
  const { content, preferredName } = readKnowledgeContent(args.source);
2104
- const result = writeMarkdownAsset({
1327
+ const result = await writeMarkdownAsset({
2105
1328
  type: "knowledge",
2106
1329
  content,
2107
1330
  name: args.name,
2108
1331
  fallbackPrefix: "knowledge",
2109
1332
  preferredName,
2110
1333
  force: args.force,
1334
+ target: args.target,
2111
1335
  });
2112
1336
  output("import", { ok: true, source: args.source, ...result });
2113
1337
  });
@@ -2122,8 +1346,10 @@ const hintsCommand = defineCommand({
2122
1346
  detail: { type: "string", description: "Detail level (normal|full)", default: "normal" },
2123
1347
  },
2124
1348
  run({ args }) {
2125
- const detail = args.detail === "full" ? "full" : "normal";
2126
- process.stdout.write(loadHints(detail));
1349
+ if (args.detail !== "normal" && args.detail !== "full") {
1350
+ throw new UsageError(`Invalid value for --detail: ${args.detail}. Expected one of: normal|full.`, "INVALID_DETAIL_VALUE");
1351
+ }
1352
+ process.stdout.write(loadHints(args.detail));
2127
1353
  },
2128
1354
  });
2129
1355
  const helpCommand = defineCommand({
@@ -2135,12 +1361,12 @@ const helpCommand = defineCommand({
2135
1361
  migrate: defineCommand({
2136
1362
  meta: {
2137
1363
  name: "migrate",
2138
- description: "Print release notes and migration guidance for a version",
1364
+ 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
1365
  },
2140
1366
  args: {
2141
1367
  version: {
2142
1368
  type: "positional",
2143
- description: "Version to review (for example 0.5.0, v0.5.0, or latest)",
1369
+ description: "Version to review (for example 0.6.0, v0.6.0, 0.6.0-rc1, or latest)",
2144
1370
  required: true,
2145
1371
  },
2146
1372
  },
@@ -2186,9 +1412,10 @@ function normalizeToggleTarget(target) {
2186
1412
  const normalized = target.trim().toLowerCase();
2187
1413
  if (normalized === "skills.sh" || normalized === "skills-sh")
2188
1414
  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`);
1415
+ if (normalized === "context-hub") {
1416
+ 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.');
1417
+ }
1418
+ throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh`);
2192
1419
  }
2193
1420
  function toggleSkillsShRegistry(enabled) {
2194
1421
  const config = loadUserConfig();
@@ -2211,34 +1438,17 @@ function toggleSkillsShRegistry(enabled) {
2211
1438
  saveConfig({ ...config, registries });
2212
1439
  return { changed: true, component: SKILLS_SH_NAME, enabled: true };
2213
1440
  }
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
1441
  function toggleComponent(targetRaw, enabled) {
2233
1442
  const target = normalizeToggleTarget(targetRaw);
2234
1443
  if (target === "skills.sh")
2235
1444
  return toggleSkillsShRegistry(enabled);
2236
- return toggleContextHubStash(enabled);
1445
+ // normalizeToggleTarget throws for any unsupported target; this is unreachable.
1446
+ throw new UsageError(`Unsupported target "${targetRaw}". Supported targets: skills.sh`);
2237
1447
  }
2238
1448
  const enableCommand = defineCommand({
2239
- meta: { name: "enable", description: "Enable an optional component (skills.sh or context-hub)" },
1449
+ meta: { name: "enable", description: "Enable an optional component (skills.sh)" },
2240
1450
  args: {
2241
- target: { type: "positional", description: "Component to enable (skills.sh|context-hub)", required: true },
1451
+ target: { type: "positional", description: "Component to enable (skills.sh)", required: true },
2242
1452
  },
2243
1453
  run({ args }) {
2244
1454
  return runWithJsonErrors(() => {
@@ -2248,9 +1458,9 @@ const enableCommand = defineCommand({
2248
1458
  },
2249
1459
  });
2250
1460
  const disableCommand = defineCommand({
2251
- meta: { name: "disable", description: "Disable an optional component (skills.sh or context-hub)" },
1461
+ meta: { name: "disable", description: "Disable an optional component (skills.sh)" },
2252
1462
  args: {
2253
- target: { type: "positional", description: "Component to disable (skills.sh|context-hub)", required: true },
1463
+ target: { type: "positional", description: "Component to disable (skills.sh)", required: true },
2254
1464
  },
2255
1465
  run({ args }) {
2256
1466
  return runWithJsonErrors(() => {
@@ -2317,14 +1527,14 @@ const vaultListCommand = defineCommand({
2317
1527
  },
2318
1528
  run({ args }) {
2319
1529
  return runWithJsonErrors(async () => {
2320
- const { listKeys } = await import("./vault.js");
1530
+ const { listKeys, listEntries } = await import("./commands/vault.js");
2321
1531
  if (args.ref) {
2322
1532
  const { name, absPath } = resolveVaultPath(args.ref);
2323
1533
  if (!fs.existsSync(absPath)) {
2324
1534
  throw new NotFoundError(`Vault not found: vault:${name}`);
2325
1535
  }
2326
- const { keys, comments } = listKeys(absPath);
2327
- output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
1536
+ const entries = listEntries(absPath);
1537
+ output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
2328
1538
  return;
2329
1539
  }
2330
1540
  const vaults = listVaultsRecursive(listKeys);
@@ -2339,7 +1549,7 @@ const vaultCreateCommand = defineCommand({
2339
1549
  },
2340
1550
  run({ args }) {
2341
1551
  return runWithJsonErrors(async () => {
2342
- const { createVault } = await import("./vault.js");
1552
+ const { createVault } = await import("./commands/vault.js");
2343
1553
  const { name, absPath } = resolveVaultPath(args.name);
2344
1554
  createVault(absPath);
2345
1555
  output("vault-create", { ref: `vault:${name}`, path: absPath });
@@ -2363,7 +1573,7 @@ const vaultSetCommand = defineCommand({
2363
1573
  },
2364
1574
  run({ args }) {
2365
1575
  return runWithJsonErrors(async () => {
2366
- const { setKey } = await import("./vault.js");
1576
+ const { setKey } = await import("./commands/vault.js");
2367
1577
  const { name, absPath } = resolveVaultPath(args.ref);
2368
1578
  let realKey;
2369
1579
  let realValue;
@@ -2389,7 +1599,7 @@ const vaultUnsetCommand = defineCommand({
2389
1599
  },
2390
1600
  run({ args }) {
2391
1601
  return runWithJsonErrors(async () => {
2392
- const { unsetKey } = await import("./vault.js");
1602
+ const { unsetKey } = await import("./commands/vault.js");
2393
1603
  const { name, absPath } = resolveVaultPath(args.ref);
2394
1604
  if (!fs.existsSync(absPath)) {
2395
1605
  throw new NotFoundError(`Vault not found: vault:${name}`);
@@ -2415,7 +1625,7 @@ const vaultLoadCommand = defineCommand({
2415
1625
  if (!fs.existsSync(absPath)) {
2416
1626
  throw new NotFoundError(`Vault not found: vault:${name}`);
2417
1627
  }
2418
- const { buildShellExportScript } = await import("./vault.js");
1628
+ const { buildShellExportScript } = await import("./commands/vault.js");
2419
1629
  const crypto = await import("node:crypto");
2420
1630
  const os = await import("node:os");
2421
1631
  // Parse via dotenv (no expansion, no code execution) and build a
@@ -2446,13 +1656,13 @@ const vaultShowCommand = defineCommand({
2446
1656
  },
2447
1657
  run({ args }) {
2448
1658
  return runWithJsonErrors(async () => {
2449
- const { listKeys } = await import("./vault.js");
1659
+ const { listEntries } = await import("./commands/vault.js");
2450
1660
  const { name, absPath } = resolveVaultPath(args.ref);
2451
1661
  if (!fs.existsSync(absPath)) {
2452
1662
  throw new NotFoundError(`Vault not found: vault:${name}`);
2453
1663
  }
2454
- const { keys, comments } = listKeys(absPath);
2455
- output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
1664
+ const entries = listEntries(absPath);
1665
+ output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
2456
1666
  });
2457
1667
  },
2458
1668
  });
@@ -2474,7 +1684,7 @@ const vaultCommand = defineCommand({
2474
1684
  if (hasVaultSubcommand(args))
2475
1685
  return;
2476
1686
  // Default action: list all vaults
2477
- const { listKeys } = await import("./vault.js");
1687
+ const { listKeys } = await import("./commands/vault.js");
2478
1688
  output("vault-list", { vaults: listVaultsRecursive(listKeys) });
2479
1689
  });
2480
1690
  },
@@ -2487,7 +1697,7 @@ const wikiCreateCommand = defineCommand({
2487
1697
  },
2488
1698
  run({ args }) {
2489
1699
  return runWithJsonErrors(async () => {
2490
- const { createWiki } = await import("./wiki.js");
1700
+ const { createWiki } = await import("./wiki/wiki.js");
2491
1701
  const stashDir = resolveStashDir();
2492
1702
  const result = createWiki(stashDir, args.name);
2493
1703
  output("wiki-create", result);
@@ -2517,7 +1727,7 @@ const wikiRegisterCommand = defineCommand({
2517
1727
  },
2518
1728
  run({ args }) {
2519
1729
  return runWithJsonErrors(async () => {
2520
- const { registerWikiSource } = await import("./stash-add");
1730
+ const { registerWikiSource } = await import("./commands/source-add");
2521
1731
  const result = await registerWikiSource({
2522
1732
  ref: args.ref.trim(),
2523
1733
  name: args.name,
@@ -2533,7 +1743,7 @@ const wikiListCommand = defineCommand({
2533
1743
  meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
2534
1744
  run() {
2535
1745
  return runWithJsonErrors(async () => {
2536
- const { listWikis } = await import("./wiki.js");
1746
+ const { listWikis } = await import("./wiki/wiki.js");
2537
1747
  const stashDir = resolveStashDir();
2538
1748
  const wikis = listWikis(stashDir);
2539
1749
  output("wiki-list", { wikis });
@@ -2547,7 +1757,7 @@ const wikiShowCommand = defineCommand({
2547
1757
  },
2548
1758
  run({ args }) {
2549
1759
  return runWithJsonErrors(async () => {
2550
- const { showWiki } = await import("./wiki.js");
1760
+ const { showWiki } = await import("./wiki/wiki.js");
2551
1761
  const stashDir = resolveStashDir();
2552
1762
  const result = showWiki(stashDir, args.name);
2553
1763
  output("wiki-show", result);
@@ -2577,9 +1787,9 @@ const wikiRemoveCommand = defineCommand({
2577
1787
  if (!args.force) {
2578
1788
  throw new UsageError("Refusing to remove without --force. Pass `--force` to confirm.");
2579
1789
  }
2580
- const withSources = Boolean(args["with-sources"]);
2581
- const { removeWiki } = await import("./wiki.js");
2582
- const { akmIndex } = await import("./indexer");
1790
+ const withSources = getHyphenatedBoolean(args, "with-sources");
1791
+ const { removeWiki } = await import("./wiki/wiki.js");
1792
+ const { akmIndex } = await import("./indexer/indexer");
2583
1793
  const stashDir = resolveStashDir();
2584
1794
  const result = removeWiki(stashDir, args.name, { withSources });
2585
1795
  await akmIndex({ stashDir });
@@ -2597,7 +1807,7 @@ const wikiPagesCommand = defineCommand({
2597
1807
  },
2598
1808
  run({ args }) {
2599
1809
  return runWithJsonErrors(async () => {
2600
- const { listPages } = await import("./wiki.js");
1810
+ const { listPages } = await import("./wiki/wiki.js");
2601
1811
  const stashDir = resolveStashDir();
2602
1812
  const pages = listPages(stashDir, args.name);
2603
1813
  output("wiki-pages", { wiki: args.name, pages });
@@ -2616,7 +1826,7 @@ const wikiSearchCommand = defineCommand({
2616
1826
  },
2617
1827
  run({ args }) {
2618
1828
  return runWithJsonErrors(async () => {
2619
- const { resolveWikiSource, searchInWiki } = await import("./wiki.js");
1829
+ const { resolveWikiSource, searchInWiki } = await import("./wiki/wiki.js");
2620
1830
  const stashDir = resolveStashDir();
2621
1831
  resolveWikiSource(stashDir, args.name);
2622
1832
  const parsedLimit = args.limit ? Number(args.limit) : undefined;
@@ -2638,7 +1848,7 @@ const wikiStashCommand = defineCommand({
2638
1848
  },
2639
1849
  run({ args }) {
2640
1850
  return runWithJsonErrors(async () => {
2641
- const { stashRaw } = await import("./wiki.js");
1851
+ const { stashRaw } = await import("./wiki/wiki.js");
2642
1852
  const { content, preferredName } = readKnowledgeContent(args.source);
2643
1853
  const stashDir = resolveStashDir();
2644
1854
  const result = stashRaw({
@@ -2663,7 +1873,7 @@ const wikiLintCommand = defineCommand({
2663
1873
  async run({ args }) {
2664
1874
  let findingCount = 0;
2665
1875
  await runWithJsonErrors(async () => {
2666
- const { lintWiki } = await import("./wiki.js");
1876
+ const { lintWiki } = await import("./wiki/wiki.js");
2667
1877
  const stashDir = resolveStashDir();
2668
1878
  const report = lintWiki(stashDir, args.name);
2669
1879
  output("wiki-lint", report);
@@ -2683,7 +1893,7 @@ const wikiIngestCommand = defineCommand({
2683
1893
  },
2684
1894
  run({ args }) {
2685
1895
  return runWithJsonErrors(async () => {
2686
- const { buildIngestWorkflow } = await import("./wiki.js");
1896
+ const { buildIngestWorkflow } = await import("./wiki/wiki.js");
2687
1897
  const stashDir = resolveStashDir();
2688
1898
  const result = buildIngestWorkflow(stashDir, args.name);
2689
1899
  output("wiki-ingest", result);
@@ -2712,7 +1922,7 @@ const wikiCommand = defineCommand({
2712
1922
  if (hasWikiSubcommand(args))
2713
1923
  return;
2714
1924
  // Default action: list wikis
2715
- const { listWikis } = await import("./wiki.js");
1925
+ const { listWikis } = await import("./wiki/wiki.js");
2716
1926
  output("wiki-list", { wikis: listWikis(resolveStashDir()) });
2717
1927
  });
2718
1928
  },
@@ -2725,7 +1935,7 @@ const main = defineCommand({
2725
1935
  },
2726
1936
  args: {
2727
1937
  format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
2728
- detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
1938
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary|agent)" },
2729
1939
  quiet: { type: "boolean", alias: "q", description: "Suppress stderr warnings", default: false },
2730
1940
  },
2731
1941
  subCommands: {
@@ -2773,14 +1983,32 @@ const WIKI_SUBCOMMAND_SET = new Set([
2773
1983
  "ingest",
2774
1984
  ]);
2775
1985
  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
1986
  // ── Exit codes ──────────────────────────────────────────────────────────────
2781
1987
  const EXIT_GENERAL = 1;
2782
1988
  const EXIT_USAGE = 2;
2783
1989
  const EXIT_CONFIG = 78;
1990
+ // citty reads process.argv directly and does not accept a custom argv array,
1991
+ // so we must replace process.argv with the normalized version before runMain.
1992
+ process.argv = normalizeShowArgv(process.argv);
1993
+ // Resolve output mode once at startup from the (normalized) argv and persisted
1994
+ // config. All subsequent output() calls read from this in-memory singleton.
1995
+ // `initOutputMode` can throw a UsageError when --format/--detail values are
1996
+ // invalid; surface it through the same JSON-error path the rest of the CLI uses
1997
+ // rather than letting the raw exception escape with a stack trace.
1998
+ try {
1999
+ initOutputMode(process.argv, loadConfig().output ?? {});
2000
+ }
2001
+ catch (error) {
2002
+ const message = error instanceof Error ? error.message : String(error);
2003
+ const hint = extractHint(error);
2004
+ const exitCode = classifyExitCode(error);
2005
+ const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
2006
+ ? error.code
2007
+ : undefined;
2008
+ console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
2009
+ process.exit(exitCode);
2010
+ }
2011
+ runMain(main);
2784
2012
  function classifyExitCode(error) {
2785
2013
  if (error instanceof UsageError)
2786
2014
  return EXIT_USAGE;
@@ -2800,31 +2028,25 @@ async function runWithJsonErrors(fn) {
2800
2028
  }
2801
2029
  catch (error) {
2802
2030
  const message = error instanceof Error ? error.message : String(error);
2803
- const hint = buildHint(message);
2031
+ const hint = extractHint(error);
2804
2032
  const exitCode = classifyExitCode(error);
2805
- console.error(JSON.stringify({ ok: false, error: message, hint }, null, 2));
2033
+ // Surface machine-readable error code from typed errors when present so
2034
+ // scripts can branch on `.code` instead of message-string matching.
2035
+ const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
2036
+ ? error.code
2037
+ : undefined;
2038
+ console.error(JSON.stringify({ ok: false, error: message, ...(code ? { code } : {}), hint }, null, 2));
2806
2039
  process.exit(exitCode);
2807
2040
  }
2808
2041
  }
2809
- function buildHint(message) {
2810
- if (message.includes("No stash directory found"))
2811
- return "Run `akm init` to create the default stash, or set stashDir in your config.";
2812
- if (message.includes("Either <target> or --all is required"))
2813
- return "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg`.";
2814
- if (message.includes("Specify either <target> or --all"))
2815
- return "Use only one: a positional target or `--all`.";
2816
- if (message.includes("No matching source"))
2817
- return "Run `akm list` to view your sources, then retry with one of those values.";
2818
- if (message.includes("remote package fetched but asset not found"))
2819
- return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
2820
- if (message.includes("Invalid value for --source"))
2821
- return "Pick one of: stash, registry, both.";
2822
- if (message.includes("Invalid value for --format"))
2823
- return "Pick one of: json, jsonl, text, yaml.";
2824
- if (message.includes("Invalid value for --detail"))
2825
- return "Pick one of: brief, normal, full, summary.";
2826
- if (message.includes("expected JSON object with endpoint and model")) {
2827
- return 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.';
2042
+ /**
2043
+ * Extract an actionable hint from an error instance. Hints live on the error
2044
+ * classes themselves (see src/errors.ts) either supplied explicitly at the
2045
+ * throw site, or derived from the error code via the per-class default mapping.
2046
+ */
2047
+ function extractHint(error) {
2048
+ if (error instanceof Error && "hint" in error && typeof error.hint === "function") {
2049
+ return error.hint();
2828
2050
  }
2829
2051
  return undefined;
2830
2052
  }
@@ -2926,294 +2148,3 @@ function loadHints(detail = "normal") {
2926
2148
  // Fallback for compiled binary — inline content
2927
2149
  return fallback;
2928
2150
  }
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
- `;