akm-cli 0.6.0-rc1 → 0.6.0

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 (108) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +9 -9
  3. package/dist/cli.js +199 -114
  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/{curate.js → commands/curate.js} +8 -3
  7. package/dist/{info.js → commands/info.js} +15 -9
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +4 -7
  10. package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
  11. package/dist/{migration-help.js → commands/migration-help.js} +2 -2
  12. package/dist/{registry-search.js → commands/registry-search.js} +8 -6
  13. package/dist/{remember.js → commands/remember.js} +55 -49
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +69 -3
  16. package/dist/{stash-show.js → commands/show.js} +104 -84
  17. package/dist/{stash-add.js → commands/source-add.js} +42 -32
  18. package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
  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} +1 -1
  23. package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
  24. package/dist/{config.js → core/config.js} +133 -56
  25. package/dist/core/errors.js +90 -0
  26. package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
  27. package/dist/core/write-source.js +280 -0
  28. package/dist/{db-search.js → indexer/db-search.js} +25 -19
  29. package/dist/{db.js → indexer/db.js} +79 -47
  30. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  31. package/dist/{indexer.js → indexer/indexer.js} +132 -33
  32. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  33. package/dist/{matchers.js → indexer/matchers.js} +3 -6
  34. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  35. package/dist/{search-source.js → indexer/search-source.js} +52 -41
  36. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  37. package/dist/{walker.js → indexer/walker.js} +1 -1
  38. package/dist/{lockfile.js → integrations/lockfile.js} +1 -1
  39. package/dist/{llm-client.js → llm/client.js} +1 -1
  40. package/dist/{embedders → llm/embedders}/local.js +2 -2
  41. package/dist/{embedders → llm/embedders}/remote.js +1 -1
  42. package/dist/{embedders → llm/embedders}/types.js +1 -1
  43. package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
  44. package/dist/{cli-hints.js → output/cli-hints.js} +3 -0
  45. package/dist/{output-context.js → output/context.js} +21 -3
  46. package/dist/{renderers.js → output/renderers.js} +9 -65
  47. package/dist/{output-shapes.js → output/shapes.js} +18 -4
  48. package/dist/{output-text.js → output/text.js} +2 -2
  49. package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
  50. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  51. package/dist/registry/factory.js +33 -0
  52. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  53. package/dist/{providers → registry/providers}/index.js +1 -1
  54. package/dist/{providers → registry/providers}/skills-sh.js +59 -3
  55. package/dist/{providers → registry/providers}/static-index.js +80 -12
  56. package/dist/registry/providers/types.js +25 -0
  57. package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
  58. package/dist/{detect.js → setup/detect.js} +0 -27
  59. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  60. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  61. package/dist/{setup.js → setup/setup.js} +16 -56
  62. package/dist/{stash-include.js → sources/include.js} +1 -1
  63. package/dist/sources/provider-factory.js +36 -0
  64. package/dist/sources/provider.js +21 -0
  65. package/dist/sources/providers/filesystem.js +35 -0
  66. package/dist/{stash-providers → sources/providers}/git.js +53 -64
  67. package/dist/{stash-providers → sources/providers}/index.js +3 -4
  68. package/dist/sources/providers/install-types.js +14 -0
  69. package/dist/{stash-providers → sources/providers}/npm.js +42 -41
  70. package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
  71. package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
  72. package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
  73. package/dist/{stash-providers → sources/providers}/website.js +29 -65
  74. package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
  75. package/dist/{wiki.js → wiki/wiki.js} +34 -18
  76. package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
  77. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  78. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  79. package/dist/workflows/document-cache.js +20 -0
  80. package/dist/workflows/parser.js +379 -0
  81. package/dist/workflows/renderer.js +78 -0
  82. package/dist/{workflow-runs.js → workflows/runs.js} +72 -28
  83. package/dist/workflows/schema.js +11 -0
  84. package/dist/workflows/validator.js +48 -0
  85. package/docs/migration/release-notes/0.6.0.md +91 -23
  86. package/package.json +1 -1
  87. package/dist/errors.js +0 -45
  88. package/dist/llm.js +0 -16
  89. package/dist/registry-factory.js +0 -19
  90. package/dist/ripgrep.js +0 -2
  91. package/dist/stash-provider-factory.js +0 -35
  92. package/dist/stash-provider.js +0 -3
  93. package/dist/stash-providers/filesystem.js +0 -71
  94. package/dist/stash-providers/openviking.js +0 -348
  95. package/dist/stash-types.js +0 -1
  96. package/dist/workflow-markdown.js +0 -260
  97. /package/dist/{common.js → core/common.js} +0 -0
  98. /package/dist/{markdown.js → core/markdown.js} +0 -0
  99. /package/dist/{paths.js → core/paths.js} +0 -0
  100. /package/dist/{warn.js → core/warn.js} +0 -0
  101. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  102. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  103. /package/dist/{github.js → integrations/github.js} +0 -0
  104. /package/dist/{embedder.js → llm/embedder.js} +0 -0
  105. /package/dist/{embedders → llm/embedders}/cache.js +0 -0
  106. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  107. /package/dist/{setup-steps.js → setup/steps.js} +0 -0
  108. /package/dist/{registry-types.js → sources/types.js} +0 -0
package/dist/cli.js CHANGED
@@ -2,41 +2,45 @@
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 { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "./cli-hints";
7
- import { isWithin, resolveStashDir, tryReadStdinText } from "./common";
8
- import { generateBashCompletions, installBashCompletions } from "./completions";
9
- import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./config";
10
- import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
11
- import { akmCurate } from "./curate";
12
- import { closeDatabase, openDatabase } from "./db";
13
- import { ConfigError, NotFoundError, UsageError } from "./errors";
14
- import { akmIndex } from "./indexer";
15
- import { assembleInfo } from "./info";
16
- import { akmInit } from "./init";
17
- import { akmListSources, akmRemove, akmUpdate } from "./installed-stashes";
18
- import { renderMigrationHelp } from "./migration-help";
19
- import { getOutputMode, initOutputMode, parseFlagValue } from "./output-context";
20
- import { shapeForCommand } from "./output-shapes";
21
- import { formatPlain, outputJsonl } from "./output-text";
22
- import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
23
- import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
24
- import { searchRegistry } from "./registry-search";
25
- import { buildMemoryFrontmatter, parseDuration, readMemoryContent, runAutoHeuristics, runLlmEnrich } from "./remember";
26
- import { checkForUpdate, performUpgrade } from "./self-update";
27
- import { akmAdd } from "./stash-add";
28
- import { akmClone } from "./stash-clone";
29
- import { saveGitStash } from "./stash-providers/git";
30
- import { parseAssetRef } from "./stash-ref";
31
- import { akmSearch, parseSearchSource } from "./stash-search";
32
- import { akmShowUnified } from "./stash-show";
33
- import { addStash } from "./stash-source-manage";
34
- 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, findEntryIdByRef, 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";
35
40
  import { pkgVersion } from "./version";
36
- import { setQuiet, warn } from "./warn";
37
- import { createWorkflowAsset, getWorkflowTemplate } from "./workflow-authoring";
38
- import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflow-cli";
39
- import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflow-runs";
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";
40
44
  const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
41
45
  const SKILLS_SH_NAME = "skills.sh";
42
46
  const SKILLS_SH_URL = "https://skills.sh";
@@ -84,9 +88,9 @@ function output(command, result) {
84
88
  }
85
89
  /**
86
90
  * Module Naming:
87
- * - stash-* : Asset operations (search, show, add, clone)
88
- * - stash-provider-* : Runtime data source providers (filesystem, openviking)
89
- * - registry-* : Discovery from remote registries (npm, GitHub)
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)
90
94
  * - installed-stashes : Unified source operations (list, remove, update)
91
95
  */
92
96
  const setupCommand = defineCommand({
@@ -96,7 +100,7 @@ const setupCommand = defineCommand({
96
100
  },
97
101
  async run() {
98
102
  await runWithJsonErrors(async () => {
99
- const { runSetupWizard } = await import("./setup");
103
+ const { runSetupWizard } = await import("./setup/setup");
100
104
  await runSetupWizard();
101
105
  });
102
106
  },
@@ -111,7 +115,10 @@ const initCommand = defineCommand({
111
115
  },
112
116
  async run({ args }) {
113
117
  await runWithJsonErrors(async () => {
114
- const result = await akmInit({ dir: args.dir });
118
+ // Accept both historical spellings for backwards compatibility with
119
+ // older docs/scripts that used `--stashDir`.
120
+ const legacyDir = parseFlagValue(process.argv, "--stashDir") ?? parseFlagValue(process.argv, "--stash-dir");
121
+ const result = await akmInit({ dir: args.dir ?? legacyDir });
115
122
  output("init", result);
116
123
  });
117
124
  },
@@ -156,6 +163,10 @@ const searchCommand = defineCommand({
156
163
  },
157
164
  async run({ args }) {
158
165
  await runWithJsonErrors(async () => {
166
+ const query = (args.query ?? "").trim();
167
+ if (!query) {
168
+ throw new UsageError('A search query is required. Usage: akm search "<query>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT");
169
+ }
159
170
  const type = args.type;
160
171
  const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
161
172
  if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
@@ -163,7 +174,7 @@ const searchCommand = defineCommand({
163
174
  }
164
175
  const limit = limitRaw;
165
176
  const source = parseSearchSource(args.source);
166
- const result = await akmSearch({ query: args.query, type, limit, source });
177
+ const result = await akmSearch({ query, type, limit, source });
167
178
  output("search", result);
168
179
  });
169
180
  },
@@ -204,7 +215,7 @@ const addCommand = defineCommand({
204
215
  description: "Path, URL, or registry ref (website URL, npm package, owner/repo, git URL, or local directory)",
205
216
  required: true,
206
217
  },
207
- provider: { type: "string", description: "Provider type (e.g. openviking). Required for URL sources." },
218
+ provider: { type: "string", description: "Provider type (e.g. website, npm). Required for URL sources." },
208
219
  options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
209
220
  name: { type: "string", description: "Human-friendly name for the source" },
210
221
  writable: {
@@ -262,7 +273,7 @@ const addCommand = defineCommand({
262
273
  }
263
274
  const websiteOptions = buildWebsiteOptions(args);
264
275
  if (args.type === "wiki") {
265
- const { registerWikiSource } = await import("./stash-add");
276
+ const { registerWikiSource } = await import("./commands/source-add");
266
277
  const result = await registerWikiSource({
267
278
  ref,
268
279
  name: args.name,
@@ -370,6 +381,11 @@ const upgradeCommand = defineCommand({
370
381
  description: "Skip checksum verification (not recommended)",
371
382
  default: false,
372
383
  },
384
+ "skip-post-upgrade": {
385
+ type: "boolean",
386
+ description: "Skip the post-upgrade `akm index` rebuild (config auto-migration still runs on next `akm` invocation)",
387
+ default: false,
388
+ },
373
389
  },
374
390
  async run({ args }) {
375
391
  await runWithJsonErrors(async () => {
@@ -378,8 +394,9 @@ const upgradeCommand = defineCommand({
378
394
  output("upgrade", check);
379
395
  return;
380
396
  }
381
- const skipChecksum = Boolean(args["skip-checksum"]);
382
- const result = await performUpgrade(check, { force: args.force, skipChecksum });
397
+ const skipChecksum = getHyphenatedBoolean(args, "skip-checksum");
398
+ const skipPostUpgrade = getHyphenatedBoolean(args, "skip-post-upgrade");
399
+ const result = await performUpgrade(check, { force: args.force, skipChecksum, skipPostUpgrade });
383
400
  output("upgrade", result);
384
401
  });
385
402
  },
@@ -427,7 +444,6 @@ const showCommand = defineCommand({
427
444
  throw new UsageError(`Unknown view mode: ${akmView}. Expected one of: full|toc|frontmatter|section|lines`);
428
445
  }
429
446
  }
430
- // Map CLI detail level to ShowDetailLevel for the show function
431
447
  const cliDetail = getOutputMode().detail;
432
448
  const showDetail = cliDetail === "summary" ? "summary" : undefined;
433
449
  const result = await akmShowUnified({ ref: args.ref, view, detail: showDetail });
@@ -671,7 +687,7 @@ const registryCommand = defineCommand({
671
687
  throw new UsageError("Registry URL must start with http:// or https://");
672
688
  }
673
689
  if (args.url.startsWith("http://")) {
674
- const allowInsecure = Boolean(args["allow-insecure"]);
690
+ const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
675
691
  if (!allowInsecure) {
676
692
  throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
677
693
  "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
@@ -752,11 +768,10 @@ const registryCommand = defineCommand({
752
768
  },
753
769
  async run({ args }) {
754
770
  await runWithJsonErrors(async () => {
755
- const argsRecord = args;
756
771
  const result = await buildRegistryIndex({
757
772
  manualEntriesPath: args.manual,
758
- npmRegistryBase: typeof argsRecord["npm-registry"] === "string" ? argsRecord["npm-registry"] : undefined,
759
- githubApiBase: typeof argsRecord["github-api"] === "string" ? argsRecord["github-api"] : undefined,
773
+ npmRegistryBase: getHyphenatedArg(args, "npm-registry"),
774
+ githubApiBase: getHyphenatedArg(args, "github-api"),
760
775
  });
761
776
  const outPath = writeRegistryIndex(result.index, args.out);
762
777
  output("registry-build-index", {
@@ -775,7 +790,7 @@ const registryCommand = defineCommand({
775
790
  const feedbackCommand = defineCommand({
776
791
  meta: {
777
792
  name: "feedback",
778
- description: "Record positive or negative feedback for a stash asset",
793
+ description: "Record positive or negative feedback for any indexed stash asset",
779
794
  },
780
795
  args: {
781
796
  ref: { type: "positional", description: "Asset ref (type:name)", required: true },
@@ -789,6 +804,7 @@ const feedbackCommand = defineCommand({
789
804
  if (!ref) {
790
805
  throw new UsageError("Asset ref is required. Usage: akm feedback <ref> --positive|--negative");
791
806
  }
807
+ parseAssetRef(ref);
792
808
  if (args.positive && args.negative) {
793
809
  throw new UsageError("Specify either --positive or --negative, not both.");
794
810
  }
@@ -799,9 +815,14 @@ const feedbackCommand = defineCommand({
799
815
  const metadata = args.note ? JSON.stringify({ note: args.note }) : undefined;
800
816
  const db = openDatabase();
801
817
  try {
818
+ const entryId = findEntryIdByRef(db, ref);
819
+ if (entryId === undefined) {
820
+ throw new UsageError(`Ref "${ref}" is not in the current index. Run "akm index" and try again.`);
821
+ }
802
822
  insertUsageEvent(db, {
803
823
  event_type: "feedback",
804
824
  entry_ref: ref,
825
+ entry_id: entryId,
805
826
  signal,
806
827
  metadata,
807
828
  });
@@ -868,24 +889,30 @@ function readKnowledgeContent(source) {
868
889
  preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
869
890
  };
870
891
  }
871
- function writeMarkdownAsset(options) {
872
- const stashDir = resolveStashDir();
873
- const typeRoot = path.join(stashDir, options.type === "knowledge" ? "knowledge" : "memories");
874
- fs.mkdirSync(typeRoot, { recursive: true });
892
+ async function writeMarkdownAsset(options) {
893
+ // Resolve write target via the v1 precedence chain (`--target` →
894
+ // `defaultWriteTarget` working stash). Per spec §10 step 5, this is the
895
+ // single dispatch point — `core/write-source.ts` owns all kind-branching.
896
+ const cfg = loadConfig();
897
+ const { source, config } = resolveWriteTarget(cfg, options.target);
898
+ const typeRoot = path.join(source.path, options.type === "knowledge" ? "knowledge" : "memories");
875
899
  const normalizedName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
900
+ // Pre-flight: existence + force semantics. The helper itself overwrites
901
+ // unconditionally; the CLI surfaces a friendlier UsageError before any
902
+ // disk activity when --force is absent.
876
903
  const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
877
904
  if (!isWithin(assetPath, typeRoot)) {
878
905
  throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
879
906
  }
880
907
  if (fs.existsSync(assetPath) && !options.force) {
881
- throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`);
908
+ throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
882
909
  }
883
- fs.mkdirSync(path.dirname(assetPath), { recursive: true });
884
- fs.writeFileSync(assetPath, options.content.endsWith("\n") ? options.content : `${options.content}\n`, "utf8");
910
+ // Delegate the actual write (and optional git commit/push) to the helper.
911
+ const result = await writeAssetToSource(source, config, { type: options.type, name: normalizedName }, options.content);
885
912
  return {
886
- ref: `${options.type}:${normalizedName}`,
887
- path: assetPath,
888
- stashDir,
913
+ ref: result.ref,
914
+ path: result.path,
915
+ stashDir: source.path,
889
916
  };
890
917
  }
891
918
  const workflowStartCommand = defineCommand({
@@ -1023,8 +1050,8 @@ const workflowCreateCommand = defineCommand({
1023
1050
  default: false,
1024
1051
  },
1025
1052
  },
1026
- run({ args }) {
1027
- return runWithJsonErrors(() => {
1053
+ async run({ args }) {
1054
+ return runWithJsonErrors(async () => {
1028
1055
  const namePattern = /^[a-z0-9][a-z0-9._/-]*$/;
1029
1056
  if (!namePattern.test(args.name)) {
1030
1057
  throw new UsageError("Workflow name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, underscores, and slashes.");
@@ -1037,6 +1064,10 @@ const workflowCreateCommand = defineCommand({
1037
1064
  from: args.from,
1038
1065
  force: args.force,
1039
1066
  });
1067
+ // Index the newly-written workflow so `akm workflow start` can resolve
1068
+ // a workflowEntryId without requiring an explicit `akm index` call
1069
+ // first. Uses the same incremental index path that `akm add` uses.
1070
+ await akmIndex({ stashDir: result.stashDir });
1040
1071
  output("workflow-create", { ok: true, ...result });
1041
1072
  });
1042
1073
  },
@@ -1050,6 +1081,55 @@ const workflowTemplateCommand = defineCommand({
1050
1081
  process.stdout.write(getWorkflowTemplate());
1051
1082
  },
1052
1083
  });
1084
+ const workflowValidateCommand = defineCommand({
1085
+ meta: {
1086
+ name: "validate",
1087
+ description: "Validate a workflow markdown file or ref and print any errors",
1088
+ },
1089
+ args: {
1090
+ target: {
1091
+ type: "positional",
1092
+ description: "Workflow ref (workflow:<name>) or filesystem path to a workflow .md",
1093
+ required: true,
1094
+ },
1095
+ },
1096
+ async run({ args }) {
1097
+ return runWithJsonErrors(async () => {
1098
+ const filePath = await resolveWorkflowFilePath(args.target);
1099
+ const { parse } = validateWorkflowSource(filePath);
1100
+ if (parse.ok) {
1101
+ output("workflow-validate", {
1102
+ ok: true,
1103
+ path: filePath,
1104
+ title: parse.document.title,
1105
+ stepCount: parse.document.steps.length,
1106
+ });
1107
+ return;
1108
+ }
1109
+ throw new UsageError(formatWorkflowErrors(filePath, parse.errors));
1110
+ });
1111
+ },
1112
+ });
1113
+ async function resolveWorkflowFilePath(target) {
1114
+ if (!target.startsWith("workflow:"))
1115
+ return target;
1116
+ const parsed = parseAssetRef(target);
1117
+ if (parsed.type !== "workflow") {
1118
+ throw new UsageError(`Expected a workflow ref (workflow:<name>), got "${target}".`);
1119
+ }
1120
+ const config = loadConfig();
1121
+ const allSources = resolveSourceEntries(undefined, config);
1122
+ const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
1123
+ for (const source of searchSources) {
1124
+ try {
1125
+ return await resolveAssetPath(source.path, "workflow", parsed.name);
1126
+ }
1127
+ catch {
1128
+ /* try next source */
1129
+ }
1130
+ }
1131
+ throw new UsageError(`Workflow not found for ref: workflow:${parsed.name}`);
1132
+ }
1053
1133
  const workflowResumeCommand = defineCommand({
1054
1134
  meta: {
1055
1135
  name: "resume",
@@ -1079,6 +1159,7 @@ const workflowCommand = defineCommand({
1079
1159
  create: workflowCreateCommand,
1080
1160
  template: workflowTemplateCommand,
1081
1161
  resume: workflowResumeCommand,
1162
+ validate: workflowValidateCommand,
1082
1163
  },
1083
1164
  run({ args }) {
1084
1165
  return runWithJsonErrors(() => {
@@ -1108,6 +1189,10 @@ const rememberCommand = defineCommand({
1108
1189
  description: "Overwrite an existing memory with the same name",
1109
1190
  default: false,
1110
1191
  },
1192
+ description: {
1193
+ type: "string",
1194
+ description: "Short description written to frontmatter (persisted as the memory's description field)",
1195
+ },
1111
1196
  tag: {
1112
1197
  type: "string",
1113
1198
  description: "Tag to add to the memory (repeatable: --tag foo --tag bar)",
@@ -1130,6 +1215,10 @@ const rememberCommand = defineCommand({
1130
1215
  description: "Call the configured LLM to propose tags and description (requires LLM config)",
1131
1216
  default: false,
1132
1217
  },
1218
+ target: {
1219
+ type: "string",
1220
+ description: "Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working stash.",
1221
+ },
1133
1222
  },
1134
1223
  async run({ args }) {
1135
1224
  return runWithJsonErrors(async () => {
@@ -1138,15 +1227,15 @@ const rememberCommand = defineCommand({
1138
1227
  // Collect all --tag occurrences directly from process.argv because citty
1139
1228
  // only exposes the last value for repeated string flags.
1140
1229
  const rawTags = parseAllFlagValues("--tag");
1141
- const hasStructuredArgs = rawTags.length > 0 || !!args.expires || !!args.source || args.auto || args.enrich;
1142
- // Zero-flag path: write bare memory (no frontmatter). Preserve existing behaviour.
1230
+ const hasStructuredArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description || args.auto || args.enrich;
1143
1231
  if (!hasStructuredArgs) {
1144
- const result = writeMarkdownAsset({
1232
+ const result = await writeMarkdownAsset({
1145
1233
  type: "memory",
1146
1234
  content: body,
1147
1235
  name: args.name,
1148
1236
  fallbackPrefix: "memory",
1149
1237
  force: args.force,
1238
+ target: args.target,
1150
1239
  });
1151
1240
  output("remember", { ok: true, ...result });
1152
1241
  return;
@@ -1154,7 +1243,8 @@ const rememberCommand = defineCommand({
1154
1243
  // ── Accumulate metadata from all three modes ──────────────────────────
1155
1244
  // Start with CLI args (Mode 1: always)
1156
1245
  const tags = [...rawTags];
1157
- let description;
1246
+ // --description is persisted as-is; LLM enrichment may fill it if absent.
1247
+ let description = args.description || undefined;
1158
1248
  let source = args.source;
1159
1249
  let observed_at;
1160
1250
  let expires;
@@ -1209,12 +1299,13 @@ const rememberCommand = defineCommand({
1209
1299
  subjective,
1210
1300
  });
1211
1301
  const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
1212
- const result = writeMarkdownAsset({
1302
+ const result = await writeMarkdownAsset({
1213
1303
  type: "memory",
1214
1304
  content: contentWithFrontmatter,
1215
1305
  name: args.name,
1216
1306
  fallbackPrefix: "memory",
1217
1307
  force: args.force,
1308
+ target: args.target,
1218
1309
  });
1219
1310
  output("remember", { ok: true, ...result });
1220
1311
  });
@@ -1240,17 +1331,22 @@ const importKnowledgeCommand = defineCommand({
1240
1331
  description: "Overwrite an existing knowledge document with the same name",
1241
1332
  default: false,
1242
1333
  },
1334
+ target: {
1335
+ type: "string",
1336
+ description: "Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working stash.",
1337
+ },
1243
1338
  },
1244
1339
  async run({ args }) {
1245
1340
  return runWithJsonErrors(async () => {
1246
1341
  const { content, preferredName } = readKnowledgeContent(args.source);
1247
- const result = writeMarkdownAsset({
1342
+ const result = await writeMarkdownAsset({
1248
1343
  type: "knowledge",
1249
1344
  content,
1250
1345
  name: args.name,
1251
1346
  fallbackPrefix: "knowledge",
1252
1347
  preferredName,
1253
1348
  force: args.force,
1349
+ target: args.target,
1254
1350
  });
1255
1351
  output("import", { ok: true, source: args.source, ...result });
1256
1352
  });
@@ -1266,7 +1362,7 @@ const hintsCommand = defineCommand({
1266
1362
  },
1267
1363
  run({ args }) {
1268
1364
  if (args.detail !== "normal" && args.detail !== "full") {
1269
- throw new UsageError(`Invalid value for --detail: ${args.detail}. Expected one of: normal|full.`);
1365
+ throw new UsageError(`Invalid value for --detail: ${args.detail}. Expected one of: normal|full.`, "INVALID_DETAIL_VALUE");
1270
1366
  }
1271
1367
  process.stdout.write(loadHints(args.detail));
1272
1368
  },
@@ -1446,14 +1542,14 @@ const vaultListCommand = defineCommand({
1446
1542
  },
1447
1543
  run({ args }) {
1448
1544
  return runWithJsonErrors(async () => {
1449
- const { listKeys } = await import("./vault.js");
1545
+ const { listKeys, listEntries } = await import("./commands/vault.js");
1450
1546
  if (args.ref) {
1451
1547
  const { name, absPath } = resolveVaultPath(args.ref);
1452
1548
  if (!fs.existsSync(absPath)) {
1453
1549
  throw new NotFoundError(`Vault not found: vault:${name}`);
1454
1550
  }
1455
- const { keys, comments } = listKeys(absPath);
1456
- output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
1551
+ const entries = listEntries(absPath);
1552
+ output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
1457
1553
  return;
1458
1554
  }
1459
1555
  const vaults = listVaultsRecursive(listKeys);
@@ -1468,7 +1564,7 @@ const vaultCreateCommand = defineCommand({
1468
1564
  },
1469
1565
  run({ args }) {
1470
1566
  return runWithJsonErrors(async () => {
1471
- const { createVault } = await import("./vault.js");
1567
+ const { createVault } = await import("./commands/vault.js");
1472
1568
  const { name, absPath } = resolveVaultPath(args.name);
1473
1569
  createVault(absPath);
1474
1570
  output("vault-create", { ref: `vault:${name}`, path: absPath });
@@ -1492,7 +1588,7 @@ const vaultSetCommand = defineCommand({
1492
1588
  },
1493
1589
  run({ args }) {
1494
1590
  return runWithJsonErrors(async () => {
1495
- const { setKey } = await import("./vault.js");
1591
+ const { setKey } = await import("./commands/vault.js");
1496
1592
  const { name, absPath } = resolveVaultPath(args.ref);
1497
1593
  let realKey;
1498
1594
  let realValue;
@@ -1518,7 +1614,7 @@ const vaultUnsetCommand = defineCommand({
1518
1614
  },
1519
1615
  run({ args }) {
1520
1616
  return runWithJsonErrors(async () => {
1521
- const { unsetKey } = await import("./vault.js");
1617
+ const { unsetKey } = await import("./commands/vault.js");
1522
1618
  const { name, absPath } = resolveVaultPath(args.ref);
1523
1619
  if (!fs.existsSync(absPath)) {
1524
1620
  throw new NotFoundError(`Vault not found: vault:${name}`);
@@ -1544,7 +1640,7 @@ const vaultLoadCommand = defineCommand({
1544
1640
  if (!fs.existsSync(absPath)) {
1545
1641
  throw new NotFoundError(`Vault not found: vault:${name}`);
1546
1642
  }
1547
- const { buildShellExportScript } = await import("./vault.js");
1643
+ const { buildShellExportScript } = await import("./commands/vault.js");
1548
1644
  const crypto = await import("node:crypto");
1549
1645
  const os = await import("node:os");
1550
1646
  // Parse via dotenv (no expansion, no code execution) and build a
@@ -1575,13 +1671,13 @@ const vaultShowCommand = defineCommand({
1575
1671
  },
1576
1672
  run({ args }) {
1577
1673
  return runWithJsonErrors(async () => {
1578
- const { listKeys } = await import("./vault.js");
1674
+ const { listEntries } = await import("./commands/vault.js");
1579
1675
  const { name, absPath } = resolveVaultPath(args.ref);
1580
1676
  if (!fs.existsSync(absPath)) {
1581
1677
  throw new NotFoundError(`Vault not found: vault:${name}`);
1582
1678
  }
1583
- const { keys, comments } = listKeys(absPath);
1584
- output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
1679
+ const entries = listEntries(absPath);
1680
+ output("vault-list", { ref: `vault:${name}`, path: absPath, entries });
1585
1681
  });
1586
1682
  },
1587
1683
  });
@@ -1603,7 +1699,7 @@ const vaultCommand = defineCommand({
1603
1699
  if (hasVaultSubcommand(args))
1604
1700
  return;
1605
1701
  // Default action: list all vaults
1606
- const { listKeys } = await import("./vault.js");
1702
+ const { listKeys } = await import("./commands/vault.js");
1607
1703
  output("vault-list", { vaults: listVaultsRecursive(listKeys) });
1608
1704
  });
1609
1705
  },
@@ -1616,7 +1712,7 @@ const wikiCreateCommand = defineCommand({
1616
1712
  },
1617
1713
  run({ args }) {
1618
1714
  return runWithJsonErrors(async () => {
1619
- const { createWiki } = await import("./wiki.js");
1715
+ const { createWiki } = await import("./wiki/wiki.js");
1620
1716
  const stashDir = resolveStashDir();
1621
1717
  const result = createWiki(stashDir, args.name);
1622
1718
  output("wiki-create", result);
@@ -1646,7 +1742,7 @@ const wikiRegisterCommand = defineCommand({
1646
1742
  },
1647
1743
  run({ args }) {
1648
1744
  return runWithJsonErrors(async () => {
1649
- const { registerWikiSource } = await import("./stash-add");
1745
+ const { registerWikiSource } = await import("./commands/source-add");
1650
1746
  const result = await registerWikiSource({
1651
1747
  ref: args.ref.trim(),
1652
1748
  name: args.name,
@@ -1662,7 +1758,7 @@ const wikiListCommand = defineCommand({
1662
1758
  meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
1663
1759
  run() {
1664
1760
  return runWithJsonErrors(async () => {
1665
- const { listWikis } = await import("./wiki.js");
1761
+ const { listWikis } = await import("./wiki/wiki.js");
1666
1762
  const stashDir = resolveStashDir();
1667
1763
  const wikis = listWikis(stashDir);
1668
1764
  output("wiki-list", { wikis });
@@ -1676,7 +1772,7 @@ const wikiShowCommand = defineCommand({
1676
1772
  },
1677
1773
  run({ args }) {
1678
1774
  return runWithJsonErrors(async () => {
1679
- const { showWiki } = await import("./wiki.js");
1775
+ const { showWiki } = await import("./wiki/wiki.js");
1680
1776
  const stashDir = resolveStashDir();
1681
1777
  const result = showWiki(stashDir, args.name);
1682
1778
  output("wiki-show", result);
@@ -1706,9 +1802,9 @@ const wikiRemoveCommand = defineCommand({
1706
1802
  if (!args.force) {
1707
1803
  throw new UsageError("Refusing to remove without --force. Pass `--force` to confirm.");
1708
1804
  }
1709
- const withSources = Boolean(args["with-sources"]);
1710
- const { removeWiki } = await import("./wiki.js");
1711
- const { akmIndex } = await import("./indexer");
1805
+ const withSources = getHyphenatedBoolean(args, "with-sources");
1806
+ const { removeWiki } = await import("./wiki/wiki.js");
1807
+ const { akmIndex } = await import("./indexer/indexer");
1712
1808
  const stashDir = resolveStashDir();
1713
1809
  const result = removeWiki(stashDir, args.name, { withSources });
1714
1810
  await akmIndex({ stashDir });
@@ -1726,7 +1822,7 @@ const wikiPagesCommand = defineCommand({
1726
1822
  },
1727
1823
  run({ args }) {
1728
1824
  return runWithJsonErrors(async () => {
1729
- const { listPages } = await import("./wiki.js");
1825
+ const { listPages } = await import("./wiki/wiki.js");
1730
1826
  const stashDir = resolveStashDir();
1731
1827
  const pages = listPages(stashDir, args.name);
1732
1828
  output("wiki-pages", { wiki: args.name, pages });
@@ -1745,7 +1841,7 @@ const wikiSearchCommand = defineCommand({
1745
1841
  },
1746
1842
  run({ args }) {
1747
1843
  return runWithJsonErrors(async () => {
1748
- const { resolveWikiSource, searchInWiki } = await import("./wiki.js");
1844
+ const { resolveWikiSource, searchInWiki } = await import("./wiki/wiki.js");
1749
1845
  const stashDir = resolveStashDir();
1750
1846
  resolveWikiSource(stashDir, args.name);
1751
1847
  const parsedLimit = args.limit ? Number(args.limit) : undefined;
@@ -1767,7 +1863,7 @@ const wikiStashCommand = defineCommand({
1767
1863
  },
1768
1864
  run({ args }) {
1769
1865
  return runWithJsonErrors(async () => {
1770
- const { stashRaw } = await import("./wiki.js");
1866
+ const { stashRaw } = await import("./wiki/wiki.js");
1771
1867
  const { content, preferredName } = readKnowledgeContent(args.source);
1772
1868
  const stashDir = resolveStashDir();
1773
1869
  const result = stashRaw({
@@ -1792,7 +1888,7 @@ const wikiLintCommand = defineCommand({
1792
1888
  async run({ args }) {
1793
1889
  let findingCount = 0;
1794
1890
  await runWithJsonErrors(async () => {
1795
- const { lintWiki } = await import("./wiki.js");
1891
+ const { lintWiki } = await import("./wiki/wiki.js");
1796
1892
  const stashDir = resolveStashDir();
1797
1893
  const report = lintWiki(stashDir, args.name);
1798
1894
  output("wiki-lint", report);
@@ -1812,7 +1908,7 @@ const wikiIngestCommand = defineCommand({
1812
1908
  },
1813
1909
  run({ args }) {
1814
1910
  return runWithJsonErrors(async () => {
1815
- const { buildIngestWorkflow } = await import("./wiki.js");
1911
+ const { buildIngestWorkflow } = await import("./wiki/wiki.js");
1816
1912
  const stashDir = resolveStashDir();
1817
1913
  const result = buildIngestWorkflow(stashDir, args.name);
1818
1914
  output("wiki-ingest", result);
@@ -1841,7 +1937,7 @@ const wikiCommand = defineCommand({
1841
1937
  if (hasWikiSubcommand(args))
1842
1938
  return;
1843
1939
  // Default action: list wikis
1844
- const { listWikis } = await import("./wiki.js");
1940
+ const { listWikis } = await import("./wiki/wiki.js");
1845
1941
  output("wiki-list", { wikis: listWikis(resolveStashDir()) });
1846
1942
  });
1847
1943
  },
@@ -1919,7 +2015,7 @@ try {
1919
2015
  }
1920
2016
  catch (error) {
1921
2017
  const message = error instanceof Error ? error.message : String(error);
1922
- const hint = buildHint(message);
2018
+ const hint = extractHint(error);
1923
2019
  const exitCode = classifyExitCode(error);
1924
2020
  const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
1925
2021
  ? error.code
@@ -1947,7 +2043,7 @@ async function runWithJsonErrors(fn) {
1947
2043
  }
1948
2044
  catch (error) {
1949
2045
  const message = error instanceof Error ? error.message : String(error);
1950
- const hint = buildHint(message);
2046
+ const hint = extractHint(error);
1951
2047
  const exitCode = classifyExitCode(error);
1952
2048
  // Surface machine-readable error code from typed errors when present so
1953
2049
  // scripts can branch on `.code` instead of message-string matching.
@@ -1958,25 +2054,14 @@ async function runWithJsonErrors(fn) {
1958
2054
  process.exit(exitCode);
1959
2055
  }
1960
2056
  }
1961
- function buildHint(message) {
1962
- if (message.includes("No stash directory found"))
1963
- return "Run `akm init` to create the default stash, or set stashDir in your config.";
1964
- if (message.includes("Either <target> or --all is required"))
1965
- return "Use `akm update --all` or pass a target like `akm update npm:@scope/pkg`.";
1966
- if (message.includes("Specify either <target> or --all"))
1967
- return "Use only one: a positional target or `--all`.";
1968
- if (message.includes("No matching source"))
1969
- return "Run `akm list` to view your sources, then retry with one of those values.";
1970
- if (message.includes("remote package fetched but asset not found"))
1971
- return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
1972
- if (message.includes("Invalid value for --source"))
1973
- return "Pick one of: stash, registry, both.";
1974
- if (message.includes("Invalid value for --format"))
1975
- return "Pick one of: json, jsonl, text, yaml.";
1976
- if (message.includes("Invalid value for --detail"))
1977
- return "Pick one of: brief, normal, full, summary, agent.";
1978
- if (message.includes("expected JSON object with endpoint and model")) {
1979
- return 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.';
2057
+ /**
2058
+ * Extract an actionable hint from an error instance. Hints live on the error
2059
+ * classes themselves (see src/errors.ts) either supplied explicitly at the
2060
+ * throw site, or derived from the error code via the per-class default mapping.
2061
+ */
2062
+ function extractHint(error) {
2063
+ if (error instanceof Error && "hint" in error && typeof error.hint === "function") {
2064
+ return error.hint();
1980
2065
  }
1981
2066
  return undefined;
1982
2067
  }