akm-cli 0.6.0-rc1 → 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 (108) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +9 -9
  3. package/dist/cli.js +181 -111
  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} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +103 -78
  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} +79 -31
  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} +70 -47
  30. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  31. package/dist/{indexer.js → indexer/indexer.js} +123 -31
  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} +1 -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} +1 -1
  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} +12 -11
  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 +69 -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, 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
  },
@@ -156,6 +160,10 @@ const searchCommand = defineCommand({
156
160
  },
157
161
  async run({ args }) {
158
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
+ }
159
167
  const type = args.type;
160
168
  const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
161
169
  if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
@@ -163,7 +171,7 @@ const searchCommand = defineCommand({
163
171
  }
164
172
  const limit = limitRaw;
165
173
  const source = parseSearchSource(args.source);
166
- const result = await akmSearch({ query: args.query, type, limit, source });
174
+ const result = await akmSearch({ query, type, limit, source });
167
175
  output("search", result);
168
176
  });
169
177
  },
@@ -204,7 +212,7 @@ const addCommand = defineCommand({
204
212
  description: "Path, URL, or registry ref (website URL, npm package, owner/repo, git URL, or local directory)",
205
213
  required: true,
206
214
  },
207
- 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." },
208
216
  options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
209
217
  name: { type: "string", description: "Human-friendly name for the source" },
210
218
  writable: {
@@ -262,7 +270,7 @@ const addCommand = defineCommand({
262
270
  }
263
271
  const websiteOptions = buildWebsiteOptions(args);
264
272
  if (args.type === "wiki") {
265
- const { registerWikiSource } = await import("./stash-add");
273
+ const { registerWikiSource } = await import("./commands/source-add");
266
274
  const result = await registerWikiSource({
267
275
  ref,
268
276
  name: args.name,
@@ -378,7 +386,7 @@ const upgradeCommand = defineCommand({
378
386
  output("upgrade", check);
379
387
  return;
380
388
  }
381
- const skipChecksum = Boolean(args["skip-checksum"]);
389
+ const skipChecksum = getHyphenatedBoolean(args, "skip-checksum");
382
390
  const result = await performUpgrade(check, { force: args.force, skipChecksum });
383
391
  output("upgrade", result);
384
392
  });
@@ -427,7 +435,6 @@ const showCommand = defineCommand({
427
435
  throw new UsageError(`Unknown view mode: ${akmView}. Expected one of: full|toc|frontmatter|section|lines`);
428
436
  }
429
437
  }
430
- // Map CLI detail level to ShowDetailLevel for the show function
431
438
  const cliDetail = getOutputMode().detail;
432
439
  const showDetail = cliDetail === "summary" ? "summary" : undefined;
433
440
  const result = await akmShowUnified({ ref: args.ref, view, detail: showDetail });
@@ -671,7 +678,7 @@ const registryCommand = defineCommand({
671
678
  throw new UsageError("Registry URL must start with http:// or https://");
672
679
  }
673
680
  if (args.url.startsWith("http://")) {
674
- const allowInsecure = Boolean(args["allow-insecure"]);
681
+ const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
675
682
  if (!allowInsecure) {
676
683
  throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
677
684
  "Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
@@ -752,11 +759,10 @@ const registryCommand = defineCommand({
752
759
  },
753
760
  async run({ args }) {
754
761
  await runWithJsonErrors(async () => {
755
- const argsRecord = args;
756
762
  const result = await buildRegistryIndex({
757
763
  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,
764
+ npmRegistryBase: getHyphenatedArg(args, "npm-registry"),
765
+ githubApiBase: getHyphenatedArg(args, "github-api"),
760
766
  });
761
767
  const outPath = writeRegistryIndex(result.index, args.out);
762
768
  output("registry-build-index", {
@@ -868,24 +874,30 @@ function readKnowledgeContent(source) {
868
874
  preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
869
875
  };
870
876
  }
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 });
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");
875
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.
876
888
  const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
877
889
  if (!isWithin(assetPath, typeRoot)) {
878
890
  throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
879
891
  }
880
892
  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.`);
893
+ throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
882
894
  }
883
- fs.mkdirSync(path.dirname(assetPath), { recursive: true });
884
- 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);
885
897
  return {
886
- ref: `${options.type}:${normalizedName}`,
887
- path: assetPath,
888
- stashDir,
898
+ ref: result.ref,
899
+ path: result.path,
900
+ stashDir: source.path,
889
901
  };
890
902
  }
891
903
  const workflowStartCommand = defineCommand({
@@ -1023,8 +1035,8 @@ const workflowCreateCommand = defineCommand({
1023
1035
  default: false,
1024
1036
  },
1025
1037
  },
1026
- run({ args }) {
1027
- return runWithJsonErrors(() => {
1038
+ async run({ args }) {
1039
+ return runWithJsonErrors(async () => {
1028
1040
  const namePattern = /^[a-z0-9][a-z0-9._/-]*$/;
1029
1041
  if (!namePattern.test(args.name)) {
1030
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.");
@@ -1037,6 +1049,10 @@ const workflowCreateCommand = defineCommand({
1037
1049
  from: args.from,
1038
1050
  force: args.force,
1039
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 });
1040
1056
  output("workflow-create", { ok: true, ...result });
1041
1057
  });
1042
1058
  },
@@ -1050,6 +1066,55 @@ const workflowTemplateCommand = defineCommand({
1050
1066
  process.stdout.write(getWorkflowTemplate());
1051
1067
  },
1052
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
+ }
1053
1118
  const workflowResumeCommand = defineCommand({
1054
1119
  meta: {
1055
1120
  name: "resume",
@@ -1079,6 +1144,7 @@ const workflowCommand = defineCommand({
1079
1144
  create: workflowCreateCommand,
1080
1145
  template: workflowTemplateCommand,
1081
1146
  resume: workflowResumeCommand,
1147
+ validate: workflowValidateCommand,
1082
1148
  },
1083
1149
  run({ args }) {
1084
1150
  return runWithJsonErrors(() => {
@@ -1108,6 +1174,10 @@ const rememberCommand = defineCommand({
1108
1174
  description: "Overwrite an existing memory with the same name",
1109
1175
  default: false,
1110
1176
  },
1177
+ description: {
1178
+ type: "string",
1179
+ description: "Short description written to frontmatter (persisted as the memory's description field)",
1180
+ },
1111
1181
  tag: {
1112
1182
  type: "string",
1113
1183
  description: "Tag to add to the memory (repeatable: --tag foo --tag bar)",
@@ -1130,6 +1200,10 @@ const rememberCommand = defineCommand({
1130
1200
  description: "Call the configured LLM to propose tags and description (requires LLM config)",
1131
1201
  default: false,
1132
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
+ },
1133
1207
  },
1134
1208
  async run({ args }) {
1135
1209
  return runWithJsonErrors(async () => {
@@ -1138,15 +1212,15 @@ const rememberCommand = defineCommand({
1138
1212
  // Collect all --tag occurrences directly from process.argv because citty
1139
1213
  // only exposes the last value for repeated string flags.
1140
1214
  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.
1215
+ const hasStructuredArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description || args.auto || args.enrich;
1143
1216
  if (!hasStructuredArgs) {
1144
- const result = writeMarkdownAsset({
1217
+ const result = await writeMarkdownAsset({
1145
1218
  type: "memory",
1146
1219
  content: body,
1147
1220
  name: args.name,
1148
1221
  fallbackPrefix: "memory",
1149
1222
  force: args.force,
1223
+ target: args.target,
1150
1224
  });
1151
1225
  output("remember", { ok: true, ...result });
1152
1226
  return;
@@ -1154,7 +1228,8 @@ const rememberCommand = defineCommand({
1154
1228
  // ── Accumulate metadata from all three modes ──────────────────────────
1155
1229
  // Start with CLI args (Mode 1: always)
1156
1230
  const tags = [...rawTags];
1157
- let description;
1231
+ // --description is persisted as-is; LLM enrichment may fill it if absent.
1232
+ let description = args.description || undefined;
1158
1233
  let source = args.source;
1159
1234
  let observed_at;
1160
1235
  let expires;
@@ -1209,12 +1284,13 @@ const rememberCommand = defineCommand({
1209
1284
  subjective,
1210
1285
  });
1211
1286
  const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
1212
- const result = writeMarkdownAsset({
1287
+ const result = await writeMarkdownAsset({
1213
1288
  type: "memory",
1214
1289
  content: contentWithFrontmatter,
1215
1290
  name: args.name,
1216
1291
  fallbackPrefix: "memory",
1217
1292
  force: args.force,
1293
+ target: args.target,
1218
1294
  });
1219
1295
  output("remember", { ok: true, ...result });
1220
1296
  });
@@ -1240,17 +1316,22 @@ const importKnowledgeCommand = defineCommand({
1240
1316
  description: "Overwrite an existing knowledge document with the same name",
1241
1317
  default: false,
1242
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
+ },
1243
1323
  },
1244
1324
  async run({ args }) {
1245
1325
  return runWithJsonErrors(async () => {
1246
1326
  const { content, preferredName } = readKnowledgeContent(args.source);
1247
- const result = writeMarkdownAsset({
1327
+ const result = await writeMarkdownAsset({
1248
1328
  type: "knowledge",
1249
1329
  content,
1250
1330
  name: args.name,
1251
1331
  fallbackPrefix: "knowledge",
1252
1332
  preferredName,
1253
1333
  force: args.force,
1334
+ target: args.target,
1254
1335
  });
1255
1336
  output("import", { ok: true, source: args.source, ...result });
1256
1337
  });
@@ -1266,7 +1347,7 @@ const hintsCommand = defineCommand({
1266
1347
  },
1267
1348
  run({ args }) {
1268
1349
  if (args.detail !== "normal" && args.detail !== "full") {
1269
- throw new UsageError(`Invalid value for --detail: ${args.detail}. Expected one of: normal|full.`);
1350
+ throw new UsageError(`Invalid value for --detail: ${args.detail}. Expected one of: normal|full.`, "INVALID_DETAIL_VALUE");
1270
1351
  }
1271
1352
  process.stdout.write(loadHints(args.detail));
1272
1353
  },
@@ -1446,14 +1527,14 @@ const vaultListCommand = defineCommand({
1446
1527
  },
1447
1528
  run({ args }) {
1448
1529
  return runWithJsonErrors(async () => {
1449
- const { listKeys } = await import("./vault.js");
1530
+ const { listKeys, listEntries } = await import("./commands/vault.js");
1450
1531
  if (args.ref) {
1451
1532
  const { name, absPath } = resolveVaultPath(args.ref);
1452
1533
  if (!fs.existsSync(absPath)) {
1453
1534
  throw new NotFoundError(`Vault not found: vault:${name}`);
1454
1535
  }
1455
- const { keys, comments } = listKeys(absPath);
1456
- 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 });
1457
1538
  return;
1458
1539
  }
1459
1540
  const vaults = listVaultsRecursive(listKeys);
@@ -1468,7 +1549,7 @@ const vaultCreateCommand = defineCommand({
1468
1549
  },
1469
1550
  run({ args }) {
1470
1551
  return runWithJsonErrors(async () => {
1471
- const { createVault } = await import("./vault.js");
1552
+ const { createVault } = await import("./commands/vault.js");
1472
1553
  const { name, absPath } = resolveVaultPath(args.name);
1473
1554
  createVault(absPath);
1474
1555
  output("vault-create", { ref: `vault:${name}`, path: absPath });
@@ -1492,7 +1573,7 @@ const vaultSetCommand = defineCommand({
1492
1573
  },
1493
1574
  run({ args }) {
1494
1575
  return runWithJsonErrors(async () => {
1495
- const { setKey } = await import("./vault.js");
1576
+ const { setKey } = await import("./commands/vault.js");
1496
1577
  const { name, absPath } = resolveVaultPath(args.ref);
1497
1578
  let realKey;
1498
1579
  let realValue;
@@ -1518,7 +1599,7 @@ const vaultUnsetCommand = defineCommand({
1518
1599
  },
1519
1600
  run({ args }) {
1520
1601
  return runWithJsonErrors(async () => {
1521
- const { unsetKey } = await import("./vault.js");
1602
+ const { unsetKey } = await import("./commands/vault.js");
1522
1603
  const { name, absPath } = resolveVaultPath(args.ref);
1523
1604
  if (!fs.existsSync(absPath)) {
1524
1605
  throw new NotFoundError(`Vault not found: vault:${name}`);
@@ -1544,7 +1625,7 @@ const vaultLoadCommand = defineCommand({
1544
1625
  if (!fs.existsSync(absPath)) {
1545
1626
  throw new NotFoundError(`Vault not found: vault:${name}`);
1546
1627
  }
1547
- const { buildShellExportScript } = await import("./vault.js");
1628
+ const { buildShellExportScript } = await import("./commands/vault.js");
1548
1629
  const crypto = await import("node:crypto");
1549
1630
  const os = await import("node:os");
1550
1631
  // Parse via dotenv (no expansion, no code execution) and build a
@@ -1575,13 +1656,13 @@ const vaultShowCommand = defineCommand({
1575
1656
  },
1576
1657
  run({ args }) {
1577
1658
  return runWithJsonErrors(async () => {
1578
- const { listKeys } = await import("./vault.js");
1659
+ const { listEntries } = await import("./commands/vault.js");
1579
1660
  const { name, absPath } = resolveVaultPath(args.ref);
1580
1661
  if (!fs.existsSync(absPath)) {
1581
1662
  throw new NotFoundError(`Vault not found: vault:${name}`);
1582
1663
  }
1583
- const { keys, comments } = listKeys(absPath);
1584
- 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 });
1585
1666
  });
1586
1667
  },
1587
1668
  });
@@ -1603,7 +1684,7 @@ const vaultCommand = defineCommand({
1603
1684
  if (hasVaultSubcommand(args))
1604
1685
  return;
1605
1686
  // Default action: list all vaults
1606
- const { listKeys } = await import("./vault.js");
1687
+ const { listKeys } = await import("./commands/vault.js");
1607
1688
  output("vault-list", { vaults: listVaultsRecursive(listKeys) });
1608
1689
  });
1609
1690
  },
@@ -1616,7 +1697,7 @@ const wikiCreateCommand = defineCommand({
1616
1697
  },
1617
1698
  run({ args }) {
1618
1699
  return runWithJsonErrors(async () => {
1619
- const { createWiki } = await import("./wiki.js");
1700
+ const { createWiki } = await import("./wiki/wiki.js");
1620
1701
  const stashDir = resolveStashDir();
1621
1702
  const result = createWiki(stashDir, args.name);
1622
1703
  output("wiki-create", result);
@@ -1646,7 +1727,7 @@ const wikiRegisterCommand = defineCommand({
1646
1727
  },
1647
1728
  run({ args }) {
1648
1729
  return runWithJsonErrors(async () => {
1649
- const { registerWikiSource } = await import("./stash-add");
1730
+ const { registerWikiSource } = await import("./commands/source-add");
1650
1731
  const result = await registerWikiSource({
1651
1732
  ref: args.ref.trim(),
1652
1733
  name: args.name,
@@ -1662,7 +1743,7 @@ const wikiListCommand = defineCommand({
1662
1743
  meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
1663
1744
  run() {
1664
1745
  return runWithJsonErrors(async () => {
1665
- const { listWikis } = await import("./wiki.js");
1746
+ const { listWikis } = await import("./wiki/wiki.js");
1666
1747
  const stashDir = resolveStashDir();
1667
1748
  const wikis = listWikis(stashDir);
1668
1749
  output("wiki-list", { wikis });
@@ -1676,7 +1757,7 @@ const wikiShowCommand = defineCommand({
1676
1757
  },
1677
1758
  run({ args }) {
1678
1759
  return runWithJsonErrors(async () => {
1679
- const { showWiki } = await import("./wiki.js");
1760
+ const { showWiki } = await import("./wiki/wiki.js");
1680
1761
  const stashDir = resolveStashDir();
1681
1762
  const result = showWiki(stashDir, args.name);
1682
1763
  output("wiki-show", result);
@@ -1706,9 +1787,9 @@ const wikiRemoveCommand = defineCommand({
1706
1787
  if (!args.force) {
1707
1788
  throw new UsageError("Refusing to remove without --force. Pass `--force` to confirm.");
1708
1789
  }
1709
- const withSources = Boolean(args["with-sources"]);
1710
- const { removeWiki } = await import("./wiki.js");
1711
- 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");
1712
1793
  const stashDir = resolveStashDir();
1713
1794
  const result = removeWiki(stashDir, args.name, { withSources });
1714
1795
  await akmIndex({ stashDir });
@@ -1726,7 +1807,7 @@ const wikiPagesCommand = defineCommand({
1726
1807
  },
1727
1808
  run({ args }) {
1728
1809
  return runWithJsonErrors(async () => {
1729
- const { listPages } = await import("./wiki.js");
1810
+ const { listPages } = await import("./wiki/wiki.js");
1730
1811
  const stashDir = resolveStashDir();
1731
1812
  const pages = listPages(stashDir, args.name);
1732
1813
  output("wiki-pages", { wiki: args.name, pages });
@@ -1745,7 +1826,7 @@ const wikiSearchCommand = defineCommand({
1745
1826
  },
1746
1827
  run({ args }) {
1747
1828
  return runWithJsonErrors(async () => {
1748
- const { resolveWikiSource, searchInWiki } = await import("./wiki.js");
1829
+ const { resolveWikiSource, searchInWiki } = await import("./wiki/wiki.js");
1749
1830
  const stashDir = resolveStashDir();
1750
1831
  resolveWikiSource(stashDir, args.name);
1751
1832
  const parsedLimit = args.limit ? Number(args.limit) : undefined;
@@ -1767,7 +1848,7 @@ const wikiStashCommand = defineCommand({
1767
1848
  },
1768
1849
  run({ args }) {
1769
1850
  return runWithJsonErrors(async () => {
1770
- const { stashRaw } = await import("./wiki.js");
1851
+ const { stashRaw } = await import("./wiki/wiki.js");
1771
1852
  const { content, preferredName } = readKnowledgeContent(args.source);
1772
1853
  const stashDir = resolveStashDir();
1773
1854
  const result = stashRaw({
@@ -1792,7 +1873,7 @@ const wikiLintCommand = defineCommand({
1792
1873
  async run({ args }) {
1793
1874
  let findingCount = 0;
1794
1875
  await runWithJsonErrors(async () => {
1795
- const { lintWiki } = await import("./wiki.js");
1876
+ const { lintWiki } = await import("./wiki/wiki.js");
1796
1877
  const stashDir = resolveStashDir();
1797
1878
  const report = lintWiki(stashDir, args.name);
1798
1879
  output("wiki-lint", report);
@@ -1812,7 +1893,7 @@ const wikiIngestCommand = defineCommand({
1812
1893
  },
1813
1894
  run({ args }) {
1814
1895
  return runWithJsonErrors(async () => {
1815
- const { buildIngestWorkflow } = await import("./wiki.js");
1896
+ const { buildIngestWorkflow } = await import("./wiki/wiki.js");
1816
1897
  const stashDir = resolveStashDir();
1817
1898
  const result = buildIngestWorkflow(stashDir, args.name);
1818
1899
  output("wiki-ingest", result);
@@ -1841,7 +1922,7 @@ const wikiCommand = defineCommand({
1841
1922
  if (hasWikiSubcommand(args))
1842
1923
  return;
1843
1924
  // Default action: list wikis
1844
- const { listWikis } = await import("./wiki.js");
1925
+ const { listWikis } = await import("./wiki/wiki.js");
1845
1926
  output("wiki-list", { wikis: listWikis(resolveStashDir()) });
1846
1927
  });
1847
1928
  },
@@ -1919,7 +2000,7 @@ try {
1919
2000
  }
1920
2001
  catch (error) {
1921
2002
  const message = error instanceof Error ? error.message : String(error);
1922
- const hint = buildHint(message);
2003
+ const hint = extractHint(error);
1923
2004
  const exitCode = classifyExitCode(error);
1924
2005
  const code = error instanceof UsageError || error instanceof ConfigError || error instanceof NotFoundError
1925
2006
  ? error.code
@@ -1947,7 +2028,7 @@ async function runWithJsonErrors(fn) {
1947
2028
  }
1948
2029
  catch (error) {
1949
2030
  const message = error instanceof Error ? error.message : String(error);
1950
- const hint = buildHint(message);
2031
+ const hint = extractHint(error);
1951
2032
  const exitCode = classifyExitCode(error);
1952
2033
  // Surface machine-readable error code from typed errors when present so
1953
2034
  // scripts can branch on `.code` instead of message-string matching.
@@ -1958,25 +2039,14 @@ async function runWithJsonErrors(fn) {
1958
2039
  process.exit(exitCode);
1959
2040
  }
1960
2041
  }
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"}\'.';
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();
1980
2050
  }
1981
2051
  return undefined;
1982
2052
  }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { getAssetTypes } from "./asset-spec";
4
+ import { getAssetTypes } from "../core/asset-spec";
5
5
  // ── Known flag values ────────────────────────────────────────────────────────
6
6
  const FLAG_VALUES = {
7
7
  "--format": ["json", "text", "yaml", "jsonl"],