akm-cli 0.0.21 → 0.0.23

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 (46) hide show
  1. package/README.md +8 -5
  2. package/dist/asset-spec.js +91 -10
  3. package/dist/cli.js +172 -57
  4. package/dist/common.js +15 -2
  5. package/dist/config-cli.js +55 -6
  6. package/dist/config.js +118 -22
  7. package/dist/create-provider-registry.js +18 -0
  8. package/dist/db.js +156 -53
  9. package/dist/embedder.js +36 -18
  10. package/dist/errors.js +6 -0
  11. package/dist/file-context.js +18 -19
  12. package/dist/frontmatter.js +19 -3
  13. package/dist/indexer.js +126 -89
  14. package/dist/{stash-registry.js → installed-kits.js} +16 -24
  15. package/dist/kit-include.js +108 -0
  16. package/dist/local-search.js +429 -0
  17. package/dist/lockfile.js +47 -5
  18. package/dist/matchers.js +6 -0
  19. package/dist/metadata.js +20 -10
  20. package/dist/paths.js +4 -0
  21. package/dist/providers/skills-sh.js +3 -2
  22. package/dist/providers/static-index.js +4 -9
  23. package/dist/registry-build-index.js +356 -0
  24. package/dist/registry-factory.js +19 -0
  25. package/dist/registry-install.js +114 -109
  26. package/dist/registry-resolve.js +44 -9
  27. package/dist/registry-search.js +14 -9
  28. package/dist/renderers.js +23 -7
  29. package/dist/ripgrep-install.js +9 -4
  30. package/dist/self-update.js +31 -4
  31. package/dist/stash-add.js +75 -6
  32. package/dist/stash-clone.js +1 -1
  33. package/dist/stash-provider-factory.js +37 -0
  34. package/dist/stash-provider.js +1 -0
  35. package/dist/stash-providers/filesystem.js +42 -0
  36. package/dist/stash-providers/index.js +9 -0
  37. package/dist/stash-providers/openviking.js +337 -0
  38. package/dist/stash-resolve.js +4 -4
  39. package/dist/stash-search.js +70 -401
  40. package/dist/stash-show.js +24 -5
  41. package/dist/stash-source-manage.js +82 -0
  42. package/dist/stash-source.js +19 -11
  43. package/dist/walker.js +15 -10
  44. package/dist/warn.js +7 -0
  45. package/package.json +1 -1
  46. package/dist/provider-registry.js +0 -8
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
  [![license](https://img.shields.io/npm/l/akm-cli)](LICENSE)
8
8
 
9
9
  A package manager for AI agent capabilities -- scripts, skills, commands,
10
- agents, and knowledge -- that works with any AI coding assistant that can run
11
- shell commands.
10
+ agents, knowledge, and memories -- that works with any AI coding assistant that
11
+ can run shell commands.
12
12
 
13
13
  ## Install
14
14
 
@@ -42,7 +42,7 @@ Any model that can run shell commands can use `akm`. Add this to your
42
42
  ## Resources & Capabilities
43
43
 
44
44
  You have access to a searchable library of scripts, skills, commands, agents,
45
- and knowledge documents via the `akm` CLI. Use `akm -h` for details.
45
+ knowledge, and memories via the `akm` CLI. Use `akm -h` for details.
46
46
  ~~~
47
47
 
48
48
  No plugins, SDKs, or integration code required. Platform-specific plugins
@@ -89,13 +89,16 @@ Registries are indexes of available kits. The official
89
89
  ```sh
90
90
  akm registry search "code review" # Search registries
91
91
  akm registry add https://example.com/registry/index.json --name team # Add a registry
92
+ akm sources add http://host:1933 --provider openviking \
93
+ --options '{"apiKey":"key"}' # Add an OpenViking stash source
92
94
  akm registry list # List configured registries
95
+ akm show viking://resources/my-doc # Fetch remote content from OpenViking
93
96
  ```
94
97
 
95
98
  Private access is supported through:
96
99
  - **GitHub tokens** -- Set `GITHUB_TOKEN` to access private GitHub repos when installing kits
97
- - **Provider options** -- Each registry entry supports an `options` field for provider-specific configuration (tokens, custom headers)
98
- - **Pluggable providers** -- Custom registry providers can implement their own authentication
100
+ - **Provider options** -- `--options` flag accepts JSON for provider-specific configuration (API keys, custom headers)
101
+ - **Pluggable providers** -- Built-in registry providers include `static-index` and `skills-sh`; stash providers include `filesystem` and `openviking`; custom providers can implement their own authentication
99
102
 
100
103
  See the [Registry docs](docs/registry.md) for hosting your own registry and
101
104
  the v2 index format.
@@ -37,7 +37,7 @@ const scriptSpec = {
37
37
  toCanonicalName: (typeRoot, filePath) => toPosix(path.relative(typeRoot, filePath)),
38
38
  toAssetPath: (typeRoot, name) => path.join(typeRoot, name),
39
39
  };
40
- export const ASSET_SPECS = {
40
+ const ASSET_SPECS_INTERNAL = {
41
41
  skill: {
42
42
  stashDir: "skills",
43
43
  isRelevantFile: (fileName) => fileName === "SKILL.md",
@@ -53,24 +53,105 @@ export const ASSET_SPECS = {
53
53
  agent: { stashDir: "agents", ...markdownSpec },
54
54
  knowledge: { stashDir: "knowledge", ...markdownSpec },
55
55
  script: { stashDir: "scripts", ...scriptSpec },
56
+ memory: { stashDir: "memories", ...markdownSpec },
56
57
  };
57
- export const ASSET_TYPES = Object.keys(ASSET_SPECS);
58
- export const TYPE_DIRS = ASSET_TYPES.reduce((acc, type) => {
59
- acc[type] = ASSET_SPECS[type].stashDir;
60
- return acc;
61
- }, {});
58
+ export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
59
+ /**
60
+ * Deferred hooks set by `local-search.ts` at module init time to avoid a
61
+ * circular dependency (asset-spec → local-search → asset-spec).
62
+ *
63
+ * When `registerAssetType` is called with a spec that includes `rendererName`
64
+ * or `actionBuilder`, these hooks are invoked automatically so callers only
65
+ * need a single `registerAssetType(type, spec)` call to fully register a new
66
+ * asset type — no separate `registerTypeRenderer`/`registerActionBuilder` calls
67
+ * are required.
68
+ */
69
+ let _registerTypeRenderer;
70
+ let _registerActionBuilder;
71
+ /**
72
+ * Called once by `local-search.ts` during module initialization to wire in the
73
+ * renderer and action-builder registration hooks.
74
+ *
75
+ * @internal — not part of the public extension API; use `registerAssetType` instead.
76
+ */
77
+ export function _setAssetTypeHooks(rendererHook, actionBuilderHook) {
78
+ _registerTypeRenderer = rendererHook;
79
+ _registerActionBuilder = actionBuilderHook;
80
+ }
81
+ /**
82
+ * Register a custom asset type with the Agentikit asset system.
83
+ *
84
+ * ## Full extension registration API
85
+ *
86
+ * Providing `rendererName` and/or `actionBuilder` in the spec automatically
87
+ * registers the renderer and action builder so that search results and `show`
88
+ * output work out of the box without additional calls.
89
+ *
90
+ * ### Minimal registration (filesystem layout only)
91
+ * ```ts
92
+ * registerAssetType("widget", {
93
+ * stashDir: "widgets",
94
+ * isRelevantFile: (f) => f.endsWith(".widget"),
95
+ * toCanonicalName: (root, fp) => path.basename(fp, ".widget"),
96
+ * toAssetPath: (root, name) => path.join(root, `${name}.widget`),
97
+ * });
98
+ * ```
99
+ *
100
+ * ### Full registration (filesystem + renderer + action)
101
+ * ```ts
102
+ * registerAssetType("widget", {
103
+ * stashDir: "widgets",
104
+ * isRelevantFile: (f) => f.endsWith(".widget"),
105
+ * toCanonicalName: (root, fp) => path.basename(fp, ".widget"),
106
+ * toAssetPath: (root, name) => path.join(root, `${name}.widget`),
107
+ * rendererName: "widget-md", // registered via registerRenderer() separately
108
+ * actionBuilder: (ref) => `akm show ${ref} -> use widget`,
109
+ * });
110
+ * ```
111
+ *
112
+ * If `rendererName` or `actionBuilder` is provided but the hooks have not yet
113
+ * been wired (i.e. `local-search.ts` has not been imported), the values are
114
+ * stored in the spec and will take effect once the hooks are set.
115
+ */
116
+ export function registerAssetType(type, spec) {
117
+ ASSET_SPECS_INTERNAL[type] = spec;
118
+ TYPE_DIRS[type] = spec.stashDir;
119
+ ASSET_TYPES = getAssetTypes();
120
+ // Auto-register renderer and action builder if provided in spec
121
+ if (spec.rendererName && _registerTypeRenderer) {
122
+ _registerTypeRenderer(type, spec.rendererName);
123
+ }
124
+ if (spec.actionBuilder && _registerActionBuilder) {
125
+ _registerActionBuilder(type, spec.actionBuilder);
126
+ }
127
+ }
128
+ export function getAssetTypes() {
129
+ return Object.keys(ASSET_SPECS_INTERNAL);
130
+ }
131
+ /** Warning: mutable `let` — stale if captured before `registerAssetType()` calls. Prefer `getAssetTypes()`. */
132
+ export let ASSET_TYPES = getAssetTypes();
133
+ export const TYPE_DIRS = Object.fromEntries(Object.entries(ASSET_SPECS_INTERNAL).map(([type, spec]) => [type, spec.stashDir]));
62
134
  export function isRelevantAssetFile(assetType, fileName) {
63
- return ASSET_SPECS[assetType].isRelevantFile(fileName);
135
+ return ASSET_SPECS[assetType]?.isRelevantFile(fileName) ?? false;
64
136
  }
65
137
  export function deriveCanonicalAssetName(assetType, typeRoot, filePath) {
66
- return ASSET_SPECS[assetType].toCanonicalName(typeRoot, filePath);
138
+ return ASSET_SPECS[assetType]?.toCanonicalName(typeRoot, filePath);
67
139
  }
68
140
  export function deriveCanonicalAssetNameFromStashRoot(assetType, stashRoot, filePath) {
69
141
  const relPath = toPosix(path.relative(stashRoot, filePath));
70
- const firstSegment = relPath.split("/").filter(Boolean)[0];
142
+ const segments = relPath.split("/").filter(Boolean);
143
+ const firstSegment = segments[0];
144
+ // When the first segment matches the canonical type dir (e.g. "agents"),
145
+ // use it as the type root so canonical names are relative to it.
146
+ // Otherwise fall back to stashRoot — this preserves the full relative path
147
+ // as the canonical name, which is correct for installed kits that live
148
+ // under custom directories (e.g. "tools/agents/svelte-file-editor").
71
149
  const typeRoot = firstSegment === TYPE_DIRS[assetType] ? path.join(stashRoot, firstSegment) : stashRoot;
72
150
  return deriveCanonicalAssetName(assetType, typeRoot, filePath);
73
151
  }
74
152
  export function resolveAssetPathFromName(assetType, typeRoot, name) {
75
- return ASSET_SPECS[assetType].toAssetPath(typeRoot, name);
153
+ const spec = ASSET_SPECS[assetType];
154
+ if (!spec)
155
+ throw new Error(`Unknown asset type: "${assetType}"`);
156
+ return spec.toAssetPath(typeRoot, name);
76
157
  }
package/dist/cli.js CHANGED
@@ -3,20 +3,21 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { defineCommand, runMain } from "citty";
5
5
  import { resolveStashDir } from "./common";
6
- import { getConfigPath, loadConfig, saveConfig } from "./config";
6
+ import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
7
7
  import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
8
8
  import { ConfigError, NotFoundError, UsageError } from "./errors";
9
9
  import { agentikitIndex } from "./indexer";
10
10
  import { agentikitInit } from "./init";
11
+ import { agentikitList, agentikitRemove, agentikitUpdate } from "./installed-kits";
11
12
  import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
13
+ import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
12
14
  import { searchRegistry } from "./registry-search";
13
15
  import { checkForUpdate, performUpgrade } from "./self-update";
14
16
  import { agentikitAdd } from "./stash-add";
15
17
  import { agentikitClone } from "./stash-clone";
16
- import { agentikitList, agentikitRemove, agentikitUpdate } from "./stash-registry";
17
- import { agentikitSearch } from "./stash-search";
18
- import { agentikitShow } from "./stash-show";
19
- import { resolveStashSources } from "./stash-source";
18
+ import { agentikitSearch, parseSearchSource } from "./stash-search";
19
+ import { agentikitShowUnified } from "./stash-show";
20
+ import { addStashSource, listStashSources, removeStashSource } from "./stash-source-manage";
20
21
  import { setQuiet, warn } from "./warn";
21
22
  // Version: prefer compile-time define, then package.json, then fallback
22
23
  const pkgVersion = (() => {
@@ -38,7 +39,7 @@ const pkgVersion = (() => {
38
39
  })();
39
40
  const OUTPUT_FORMATS = ["json", "yaml", "text"];
40
41
  const DETAIL_LEVELS = ["brief", "normal", "full"];
41
- const BRIEF_DESCRIPTION_LIMIT = 160;
42
+ const NORMAL_DESCRIPTION_LIMIT = 250;
42
43
  function hasBunYAML(b) {
43
44
  // biome-ignore lint/suspicious/noExplicitAny: type guard for runtime feature detection
44
45
  return typeof b.YAML?.stringify === "function";
@@ -135,7 +136,8 @@ function shapeRegistrySearchOutput(result, detail) {
135
136
  const assetHits = Array.isArray(result.assetHits) ? result.assetHits : [];
136
137
  // Shape kit hits as registry type
137
138
  const shapedKitHits = hits.map((hit) => shapeSearchHit({ ...hit, type: "registry" }, detail));
138
- const shapedAssetHits = assetHits.map((hit) => shapeSearchHit(hit, detail));
139
+ // Shape asset hits by detail level
140
+ const shapedAssetHits = assetHits.map((hit) => shapeAssetHit(hit, detail));
139
141
  const shaped = {
140
142
  hits: shapedKitHits,
141
143
  ...(shapedAssetHits.length > 0 ? { assetHits: shapedAssetHits } : {}),
@@ -146,42 +148,35 @@ function shapeRegistrySearchOutput(result, detail) {
146
148
  }
147
149
  return shaped;
148
150
  }
151
+ function shapeAssetHit(hit, detail) {
152
+ if (detail === "brief")
153
+ return pickFields(hit, ["assetName", "assetType", "action"]);
154
+ if (detail === "normal") {
155
+ return capDescription(pickFields(hit, ["assetName", "assetType", "description", "kit", "action"]), NORMAL_DESCRIPTION_LIMIT);
156
+ }
157
+ return hit;
158
+ }
149
159
  function shapeSearchHit(hit, detail) {
150
- // Keep local and registry hit models separate internally so search and
151
- // ranking logic can carry source-specific metadata. Normalize the external
152
- // contract here so default CLI output stays compact and consistent.
153
160
  if (hit.type === "registry") {
154
- const brief = withTruncatedDescription(pickFields(hit, ["type", "name", "id", "description", "action", "curated"]));
155
161
  if (detail === "brief")
156
- return brief;
157
- if (detail === "normal")
158
- return pickFields(hit, ["type", "name", "id", "description", "tags", "action", "curated"]);
159
- return hit;
160
- }
161
- if (hit.type === "registry-asset") {
162
- const brief = withTruncatedDescription(pickFields(hit, ["type", "assetType", "assetName", "description", "kit", "action"]));
163
- if (detail === "brief")
164
- return brief;
162
+ return pickFields(hit, ["name", "action"]);
165
163
  if (detail === "normal") {
166
- return pickFields(hit, ["type", "assetType", "assetName", "description", "kit", "registryName", "action"]);
164
+ return capDescription(pickFields(hit, ["name", "description", "action", "curated"]), NORMAL_DESCRIPTION_LIMIT);
167
165
  }
168
166
  return hit;
169
167
  }
170
- const brief = withTruncatedDescription(pickFields(hit, ["type", "name", "description", "action"]));
168
+ // Stash hit (local or remote)
171
169
  if (detail === "brief")
172
- return brief;
170
+ return pickFields(hit, ["type", "name", "action"]);
173
171
  if (detail === "normal") {
174
- return pickFields(hit, ["type", "name", "ref", "origin", "description", "tags", "size", "action", "run"]);
172
+ return capDescription(pickFields(hit, ["type", "name", "description", "action", "score"]), NORMAL_DESCRIPTION_LIMIT);
175
173
  }
176
174
  return hit;
177
175
  }
178
- function withTruncatedDescription(hit) {
176
+ function capDescription(hit, limit) {
179
177
  if (typeof hit.description !== "string")
180
178
  return hit;
181
- return {
182
- ...hit,
183
- description: truncateDescription(hit.description, BRIEF_DESCRIPTION_LIMIT),
184
- };
179
+ return { ...hit, description: truncateDescription(hit.description, limit) };
185
180
  }
186
181
  function truncateDescription(description, limit) {
187
182
  const normalized = description.replace(/\s+/g, " ").trim();
@@ -400,6 +395,13 @@ function formatSearchPlain(r, detail) {
400
395
  }
401
396
  return lines.join("\n").trimEnd();
402
397
  }
398
+ /**
399
+ * Naming Conventions:
400
+ * - stash-* : Operations on the user's local asset store (stash-show, stash-add, stash-clone)
401
+ * - stash-provider-* : Runtime data source providers (filesystem, openviking)
402
+ * - registry-* : Kit discovery from remote registries (npm, GitHub)
403
+ * - installed-kits : Management of kits already installed locally
404
+ */
403
405
  const initCommand = defineCommand({
404
406
  meta: {
405
407
  name: "init",
@@ -433,17 +435,21 @@ const searchCommand = defineCommand({
433
435
  query: { type: "positional", description: "Search query (omit to list all assets)", required: false, default: "" },
434
436
  type: {
435
437
  type: "string",
436
- description: "Asset type filter (skill|command|agent|knowledge|script|any).",
438
+ description: "Asset type filter (e.g. skill, command, agent, knowledge, script, memory, or any).",
437
439
  },
438
440
  limit: { type: "string", description: "Maximum number of results" },
439
- source: { type: "string", description: "Search source (local|registry|both)", default: "local" },
441
+ source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
440
442
  format: { type: "string", description: "Output format (json|text|yaml)" },
441
443
  detail: { type: "string", description: "Detail level (brief|normal|full)" },
442
444
  },
443
445
  async run({ args }) {
444
446
  await runWithJsonErrors(async () => {
445
447
  const type = args.type;
446
- const limit = args.limit ? parseInt(args.limit, 10) : undefined;
448
+ const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
449
+ if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
450
+ throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
451
+ }
452
+ const limit = limitRaw;
447
453
  const source = parseSearchSource(args.source);
448
454
  const result = await agentikitSearch({ query: args.query, type, limit, source });
449
455
  output("search", result);
@@ -506,6 +512,11 @@ const upgradeCommand = defineCommand({
506
512
  args: {
507
513
  check: { type: "boolean", description: "Check for updates without installing", default: false },
508
514
  force: { type: "boolean", description: "Force upgrade even if on latest", default: false },
515
+ skipChecksum: {
516
+ type: "boolean",
517
+ description: "Skip checksum verification (not recommended)",
518
+ default: false,
519
+ },
509
520
  },
510
521
  async run({ args }) {
511
522
  await runWithJsonErrors(async () => {
@@ -514,7 +525,7 @@ const upgradeCommand = defineCommand({
514
525
  output("upgrade", check);
515
526
  return;
516
527
  }
517
- const result = await performUpgrade(check, { force: args.force });
528
+ const result = await performUpgrade(check, { force: args.force, skipChecksum: args.skipChecksum });
518
529
  output("upgrade", result);
519
530
  });
520
531
  },
@@ -557,7 +568,7 @@ const showCommand = defineCommand({
557
568
  throw new UsageError(`Unknown view mode: ${args.akmView}. Expected one of: full|toc|frontmatter|section|lines`);
558
569
  }
559
570
  }
560
- const result = await agentikitShow({ ref: args.ref, view });
571
+ const result = await agentikitShowUnified({ ref: args.ref, view });
561
572
  output("show", result);
562
573
  });
563
574
  },
@@ -689,7 +700,7 @@ const registryCommand = defineCommand({
689
700
  run() {
690
701
  return runWithJsonErrors(() => {
691
702
  const config = loadConfig();
692
- const registries = config.registries ?? [];
703
+ const registries = config.registries ?? DEFAULT_CONFIG.registries;
693
704
  output("registry-list", { registries });
694
705
  });
695
706
  },
@@ -700,12 +711,16 @@ const registryCommand = defineCommand({
700
711
  url: { type: "positional", description: "Registry index URL", required: true },
701
712
  name: { type: "string", description: "Human-friendly name for the registry" },
702
713
  provider: { type: "string", description: "Provider type (e.g. static-index, skills-sh)" },
714
+ options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
703
715
  },
704
716
  run({ args }) {
705
717
  return runWithJsonErrors(() => {
706
718
  if (!args.url.startsWith("http")) {
707
719
  throw new UsageError("Registry URL must start with http:// or https://");
708
720
  }
721
+ if (args.url.startsWith("http://")) {
722
+ warn("Warning: registry URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
723
+ }
709
724
  const config = loadConfig();
710
725
  const registries = [...(config.registries ?? [])];
711
726
  // Deduplicate by URL
@@ -718,6 +733,14 @@ const registryCommand = defineCommand({
718
733
  entry.name = args.name;
719
734
  if (args.provider)
720
735
  entry.provider = args.provider;
736
+ if (args.options) {
737
+ try {
738
+ entry.options = JSON.parse(args.options);
739
+ }
740
+ catch {
741
+ throw new UsageError("--options must be valid JSON");
742
+ }
743
+ }
721
744
  registries.push(entry);
722
745
  saveConfig({ ...config, registries });
723
746
  output("registry-add", { registries, added: true });
@@ -753,24 +776,117 @@ const registryCommand = defineCommand({
753
776
  },
754
777
  async run({ args }) {
755
778
  await runWithJsonErrors(async () => {
756
- const limit = args.limit ? parseInt(args.limit, 10) : undefined;
757
- const result = await searchRegistry(args.query, { limit, includeAssets: args.assets });
779
+ const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
780
+ if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
781
+ throw new UsageError(`Invalid --limit value: "${args.limit}". Must be a positive integer.`);
782
+ }
783
+ const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
758
784
  output("registry-search", result);
759
785
  });
760
786
  },
761
787
  }),
788
+ "build-index": defineCommand({
789
+ meta: { name: "build-index", description: "Build a v2 registry index from discovery and manual entries" },
790
+ args: {
791
+ out: { type: "string", description: "Output path for the generated index", default: "index.json" },
792
+ manual: { type: "string", description: "Manual entries JSON file", default: "manual-entries.json" },
793
+ npmRegistry: { type: "string", description: "Override npm registry base URL" },
794
+ githubApi: { type: "string", description: "Override GitHub API base URL" },
795
+ },
796
+ async run({ args }) {
797
+ await runWithJsonErrors(async () => {
798
+ const result = await buildRegistryIndex({
799
+ manualEntriesPath: args.manual,
800
+ npmRegistryBase: args.npmRegistry,
801
+ githubApiBase: args.githubApi,
802
+ });
803
+ const outPath = writeRegistryIndex(result.index, args.out);
804
+ output("registry-build-index", {
805
+ outPath,
806
+ version: result.index.version,
807
+ updatedAt: result.index.updatedAt,
808
+ totalKits: result.counts.total,
809
+ counts: result.counts,
810
+ manualEntriesPath: result.paths.manualEntriesPath,
811
+ });
812
+ });
813
+ },
814
+ }),
762
815
  },
763
816
  });
817
+ /**
818
+ * Shared subcommand definitions for stash source management.
819
+ * Used by both `akm stash` (preferred) and `akm sources` (legacy alias).
820
+ */
821
+ function buildSourceSubCommands(outputPrefix) {
822
+ return {
823
+ list: defineCommand({
824
+ meta: { name: "list", description: "List all stash sources" },
825
+ run() {
826
+ return runWithJsonErrors(() => {
827
+ output(`${outputPrefix}`, listStashSources());
828
+ });
829
+ },
830
+ }),
831
+ add: defineCommand({
832
+ meta: { name: "add", description: "Add a stash source (filesystem path or remote URL)" },
833
+ args: {
834
+ target: { type: "positional", description: "Path or URL to add", required: true },
835
+ name: { type: "string", description: "Human-friendly name for the source" },
836
+ provider: { type: "string", description: "Provider type (e.g. openviking). Required for URLs." },
837
+ options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
838
+ },
839
+ run({ args }) {
840
+ return runWithJsonErrors(() => {
841
+ if (args.target.startsWith("http://")) {
842
+ warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
843
+ }
844
+ let parsedOptions;
845
+ if (args.options) {
846
+ try {
847
+ const parsed = JSON.parse(args.options);
848
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
849
+ throw new UsageError("--options must be a JSON object");
850
+ }
851
+ parsedOptions = parsed;
852
+ }
853
+ catch (err) {
854
+ if (err instanceof UsageError)
855
+ throw err;
856
+ throw new UsageError("--options must be valid JSON");
857
+ }
858
+ }
859
+ const result = addStashSource({
860
+ target: args.target,
861
+ name: args.name,
862
+ providerType: args.provider,
863
+ options: parsedOptions,
864
+ });
865
+ output(`${outputPrefix}-add`, result);
866
+ });
867
+ },
868
+ }),
869
+ remove: defineCommand({
870
+ meta: { name: "remove", description: "Remove a stash source by URL, path, or name" },
871
+ args: {
872
+ target: { type: "positional", description: "Source URL, path, or name to remove", required: true },
873
+ },
874
+ run({ args }) {
875
+ return runWithJsonErrors(() => {
876
+ const result = removeStashSource(args.target);
877
+ output(`${outputPrefix}-remove`, result);
878
+ });
879
+ },
880
+ }),
881
+ };
882
+ }
883
+ const stashCommand = defineCommand({
884
+ meta: { name: "stash", description: "Manage stash sources (local paths and remote providers)" },
885
+ subCommands: buildSourceSubCommands("stash"),
886
+ });
764
887
  const sourcesCommand = defineCommand({
765
- meta: { name: "sources", description: "List all stash search paths and their status" },
766
- run() {
767
- return runWithJsonErrors(() => {
768
- const config = loadConfig();
769
- const sources = resolveStashSources();
770
- const registries = config.registries ?? [];
771
- output("sources", { sources, registries });
772
- });
773
- },
888
+ meta: { name: "sources", description: "Manage stash sources (alias for 'akm stash')" },
889
+ subCommands: buildSourceSubCommands("sources"),
774
890
  });
775
891
  const hintsCommand = defineCommand({
776
892
  meta: {
@@ -807,24 +923,19 @@ const main = defineCommand({
807
923
  search: searchCommand,
808
924
  show: showCommand,
809
925
  clone: cloneCommand,
926
+ stash: stashCommand,
810
927
  sources: sourcesCommand,
811
928
  registry: registryCommand,
812
929
  config: configCommand,
813
930
  hints: hintsCommand,
814
931
  },
815
932
  });
816
- const SEARCH_SOURCES = ["local", "registry", "both"];
817
933
  const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
818
934
  const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
819
935
  // citty reads process.argv directly and does not accept a custom argv array,
820
936
  // so we must replace process.argv with the normalized version before runMain.
821
937
  process.argv = normalizeShowArgv(process.argv);
822
938
  runMain(main);
823
- function parseSearchSource(value) {
824
- if (SEARCH_SOURCES.includes(value))
825
- return value;
826
- throw new UsageError(`Invalid value for --source: ${value}. Expected one of: ${SEARCH_SOURCES.join("|")}`);
827
- }
828
939
  // ── Exit codes ──────────────────────────────────────────────────────────────
829
940
  const EXIT_GENERAL = 1;
830
941
  const EXIT_USAGE = 2;
@@ -866,7 +977,7 @@ function buildHint(message) {
866
977
  if (message.includes("remote package fetched but asset not found"))
867
978
  return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
868
979
  if (message.includes("Invalid value for --source"))
869
- return "Pick one of: local, registry, both.";
980
+ return "Pick one of: stash, registry, both.";
870
981
  if (message.includes("Invalid value for --format"))
871
982
  return "Pick one of: json, text, yaml.";
872
983
  if (message.includes("Invalid value for --detail"))
@@ -975,7 +1086,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
975
1086
  \`\`\`sh
976
1087
  akm search "<query>" # Search for assets
977
1088
  akm search "<query>" --type skill # Filter by type
978
- akm search "<query>" --source both # Search registries and local stashes for assets
1089
+ akm search "<query>" --source both # Search stash providers and registries for assets
979
1090
  akm show <ref> # View asset details
980
1091
  akm add <ref> # Install a kit (npm, GitHub, git, local)
981
1092
  akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
@@ -1003,7 +1114,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
1003
1114
  \`\`\`sh
1004
1115
  akm search "<query>" # Search local stash
1005
1116
  akm search "<query>" --type skill # Filter by asset type
1006
- akm search "<query>" --source both # Search local stash and registries
1117
+ akm search "<query>" --source both # Search all stash providers and registries
1007
1118
  akm search "<query>" --source registry # Search registries only
1008
1119
  akm search "<query>" --limit 10 # Limit results
1009
1120
  akm search "<query>" --detail full # Include scores, paths, timing
@@ -1011,8 +1122,8 @@ akm search "<query>" --detail full # Include scores, paths, timing
1011
1122
 
1012
1123
  | Flag | Values | Default |
1013
1124
  | --- | --- | --- |
1014
- | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`any\` | \`any\` |
1015
- | \`--source\` | \`local\`, \`registry\`, \`both\` | \`local\` |
1125
+ | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`memory\`, \`any\` | \`any\` |
1126
+ | \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
1016
1127
  | \`--limit\` | number | \`20\` |
1017
1128
  | \`--format\` | \`json\`, \`text\`, \`yaml\` | \`json\` |
1018
1129
  | \`--detail\` | \`brief\`, \`normal\`, \`full\` | \`brief\` |
@@ -1029,6 +1140,7 @@ akm show agent:architect # Show agent (returns system promp
1029
1140
  akm show knowledge:guide toc # Table of contents
1030
1141
  akm show knowledge:guide section "Auth" # Specific section
1031
1142
  akm show knowledge:guide lines 10 30 # Line range
1143
+ akm show viking://resources/my-doc # Show remote OpenViking content
1032
1144
  \`\`\`
1033
1145
 
1034
1146
  | Type | Key fields returned |
@@ -1038,6 +1150,7 @@ akm show knowledge:guide lines 10 30 # Line range
1038
1150
  | command | \`template\`, \`description\`, \`parameters\` |
1039
1151
  | agent | \`prompt\`, \`description\`, \`modelHint\`, \`toolPolicy\` |
1040
1152
  | knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
1153
+ | memory | \`content\` (recalled context) |
1041
1154
 
1042
1155
  ## Install & Manage Kits
1043
1156
 
@@ -1076,6 +1189,8 @@ akm registry add <url> --provider skills-sh # Specify provider type
1076
1189
  akm registry remove <url-or-name> # Remove a registry
1077
1190
  akm registry search "<query>" # Search all registries
1078
1191
  akm registry search "<query>" --assets # Include asset-level results
1192
+ akm registry build-index # Build ./index.json
1193
+ akm registry build-index --out dist/index.json # Build to a custom path
1079
1194
  \`\`\`
1080
1195
 
1081
1196
  ## Configuration
package/dist/common.js CHANGED
@@ -7,7 +7,7 @@ import { getConfigPath, getDefaultStashDir } from "./paths";
7
7
  export const IS_WINDOWS = process.platform === "win32";
8
8
  // ── Validators ──────────────────────────────────────────────────────────────
9
9
  export function isAssetType(type) {
10
- return type in TYPE_DIRS;
10
+ return Object.hasOwn(TYPE_DIRS, type);
11
11
  }
12
12
  // ── Utilities ───────────────────────────────────────────────────────────────
13
13
  /**
@@ -16,6 +16,11 @@ export function isAssetType(type) {
16
16
  * 2. stashDir field in config.json
17
17
  * 3. Platform default (~/akm or ~/Documents/akm on Windows)
18
18
  *
19
+ * WARNING: May write to config file as a side effect when AKM_STASH_DIR is set.
20
+ * Specifically, when AKM_STASH_DIR is set and `options.readOnly` is not true,
21
+ * this function calls `persistStashDirToConfig()` which writes the resolved
22
+ * path into config.json on disk.
23
+ *
19
24
  * Throws if no valid stash directory is found.
20
25
  */
21
26
  export function resolveStashDir(options) {
@@ -82,6 +87,11 @@ function readStashDirFromConfig() {
82
87
  /**
83
88
  * Persist stashDir to config.json if not already set, so users can
84
89
  * transition away from relying on the AKM_STASH_DIR env var.
90
+ *
91
+ * WARNING: This function writes to disk (config.json). It is called as a side
92
+ * effect of `resolveStashDir()` when AKM_STASH_DIR is set and `readOnly` is
93
+ * not true. Callers that must not touch the filesystem should pass
94
+ * `{ readOnly: true }` to `resolveStashDir()`.
85
95
  */
86
96
  function persistStashDirToConfig(stashDir) {
87
97
  try {
@@ -101,7 +111,7 @@ function persistStashDirToConfig(stashDir) {
101
111
  raw.stashDir = stashDir;
102
112
  const dir = path.dirname(configPath);
103
113
  fs.mkdirSync(dir, { recursive: true });
104
- const tmpPath = `${configPath}.tmp.${process.pid}`;
114
+ const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
105
115
  fs.writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8");
106
116
  fs.renameSync(tmpPath, configPath);
107
117
  }
@@ -196,3 +206,6 @@ function parseRetryAfter(response) {
196
206
  const seconds = parseInt(header, 10);
197
207
  return Number.isNaN(seconds) ? undefined : seconds * 1000;
198
208
  }
209
+ export function toErrorMessage(error) {
210
+ return error instanceof Error ? error.message : String(error);
211
+ }