akm-cli 0.0.21 → 0.0.22

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 (45) hide show
  1. package/README.md +8 -5
  2. package/dist/asset-spec.js +91 -10
  3. package/dist/cli.js +195 -55
  4. package/dist/common.js +15 -2
  5. package/dist/config-cli.js +65 -6
  6. package/dist/config.js +206 -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 +52 -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.js +19 -11
  42. package/dist/walker.js +15 -10
  43. package/dist/warn.js +7 -0
  44. package/package.json +1 -1
  45. 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,19 +3,20 @@ 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";
18
+ import { agentikitSearch, parseSearchSource } from "./stash-search";
19
+ import { agentikitShowUnified } from "./stash-show";
19
20
  import { resolveStashSources } from "./stash-source";
20
21
  import { setQuiet, warn } from "./warn";
21
22
  // Version: prefer compile-time define, then package.json, then fallback
@@ -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,23 +776,142 @@ 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
  });
764
817
  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
- });
818
+ meta: { name: "sources", description: "Manage stash sources (local paths and remote providers)" },
819
+ subCommands: {
820
+ list: defineCommand({
821
+ meta: { name: "list", description: "List all stash sources" },
822
+ run() {
823
+ return runWithJsonErrors(() => {
824
+ const config = loadConfig();
825
+ const localSources = resolveStashSources();
826
+ const stashes = config.stashes ?? [];
827
+ // Legacy fallback: show remoteStashSources if no stashes config
828
+ const legacyRemote = !config.stashes ? (config.remoteStashSources ?? []) : [];
829
+ output("sources", {
830
+ localSources,
831
+ stashes,
832
+ ...(legacyRemote.length > 0 ? { remoteSources: legacyRemote } : {}),
833
+ });
834
+ });
835
+ },
836
+ }),
837
+ add: defineCommand({
838
+ meta: { name: "add", description: "Add a stash source (filesystem path or remote URL)" },
839
+ args: {
840
+ target: { type: "positional", description: "Path or URL to add", required: true },
841
+ name: { type: "string", description: "Human-friendly name for the source" },
842
+ provider: { type: "string", description: "Provider type (e.g. openviking). Required for URLs." },
843
+ options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
844
+ },
845
+ run({ args }) {
846
+ return runWithJsonErrors(() => {
847
+ const config = loadConfig();
848
+ const stashes = [...(config.stashes ?? [])];
849
+ const isUrl = args.target.startsWith("http://") || args.target.startsWith("https://");
850
+ if (isUrl) {
851
+ if (args.target.startsWith("http://")) {
852
+ warn("Warning: source URL uses plain HTTP (not HTTPS). For security, prefer https:// to protect against eavesdropping and tampering.");
853
+ }
854
+ const providerType = args.provider;
855
+ if (!providerType) {
856
+ throw new UsageError("--provider is required for URL sources (e.g. --provider openviking)");
857
+ }
858
+ if (stashes.some((s) => s.url === args.target)) {
859
+ output("sources-add", { stashes, added: false, message: "Source URL already configured" });
860
+ return;
861
+ }
862
+ const entry = { type: providerType, url: args.target };
863
+ if (args.name)
864
+ entry.name = args.name;
865
+ if (args.options) {
866
+ try {
867
+ entry.options = JSON.parse(args.options);
868
+ }
869
+ catch {
870
+ throw new UsageError("--options must be valid JSON");
871
+ }
872
+ }
873
+ stashes.push(entry);
874
+ }
875
+ else {
876
+ // Filesystem path
877
+ const resolvedPath = path.resolve(args.target);
878
+ if (stashes.some((s) => s.path === resolvedPath)) {
879
+ output("sources-add", { stashes, added: false, message: "Source path already configured" });
880
+ return;
881
+ }
882
+ const entry = { type: "filesystem", path: resolvedPath };
883
+ if (args.name)
884
+ entry.name = args.name;
885
+ stashes.push(entry);
886
+ }
887
+ // Migrate: remove remoteStashSources when moving to stashes
888
+ const { remoteStashSources, ...rest } = config;
889
+ saveConfig({ ...rest, stashes });
890
+ output("sources-add", { stashes, added: true });
891
+ });
892
+ },
893
+ }),
894
+ remove: defineCommand({
895
+ meta: { name: "remove", description: "Remove a stash source by URL, path, or name" },
896
+ args: {
897
+ target: { type: "positional", description: "Source URL, path, or name to remove", required: true },
898
+ },
899
+ run({ args }) {
900
+ return runWithJsonErrors(() => {
901
+ const config = loadConfig();
902
+ const stashes = [...(config.stashes ?? [])];
903
+ const resolvedTarget = args.target.startsWith("http") ? args.target : path.resolve(args.target);
904
+ const idx = stashes.findIndex((s) => s.url === resolvedTarget || s.path === resolvedTarget || s.name === resolvedTarget);
905
+ if (idx === -1) {
906
+ output("sources-remove", { stashes, removed: false, message: "No matching source found" });
907
+ return;
908
+ }
909
+ const removed = stashes.splice(idx, 1)[0];
910
+ saveConfig({ ...config, stashes });
911
+ output("sources-remove", { stashes, removed: true, entry: removed });
912
+ });
913
+ },
914
+ }),
773
915
  },
774
916
  });
775
917
  const hintsCommand = defineCommand({
@@ -813,18 +955,12 @@ const main = defineCommand({
813
955
  hints: hintsCommand,
814
956
  },
815
957
  });
816
- const SEARCH_SOURCES = ["local", "registry", "both"];
817
958
  const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
818
959
  const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
819
960
  // citty reads process.argv directly and does not accept a custom argv array,
820
961
  // so we must replace process.argv with the normalized version before runMain.
821
962
  process.argv = normalizeShowArgv(process.argv);
822
963
  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
964
  // ── Exit codes ──────────────────────────────────────────────────────────────
829
965
  const EXIT_GENERAL = 1;
830
966
  const EXIT_USAGE = 2;
@@ -866,7 +1002,7 @@ function buildHint(message) {
866
1002
  if (message.includes("remote package fetched but asset not found"))
867
1003
  return "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.";
868
1004
  if (message.includes("Invalid value for --source"))
869
- return "Pick one of: local, registry, both.";
1005
+ return "Pick one of: stash, registry, both.";
870
1006
  if (message.includes("Invalid value for --format"))
871
1007
  return "Pick one of: json, text, yaml.";
872
1008
  if (message.includes("Invalid value for --detail"))
@@ -975,7 +1111,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
975
1111
  \`\`\`sh
976
1112
  akm search "<query>" # Search for assets
977
1113
  akm search "<query>" --type skill # Filter by type
978
- akm search "<query>" --source both # Search registries and local stashes for assets
1114
+ akm search "<query>" --source both # Search stash providers and registries for assets
979
1115
  akm show <ref> # View asset details
980
1116
  akm add <ref> # Install a kit (npm, GitHub, git, local)
981
1117
  akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
@@ -1003,7 +1139,7 @@ You have access to a searchable library of scripts, skills, commands, agents, an
1003
1139
  \`\`\`sh
1004
1140
  akm search "<query>" # Search local stash
1005
1141
  akm search "<query>" --type skill # Filter by asset type
1006
- akm search "<query>" --source both # Search local stash and registries
1142
+ akm search "<query>" --source both # Search all stash providers and registries
1007
1143
  akm search "<query>" --source registry # Search registries only
1008
1144
  akm search "<query>" --limit 10 # Limit results
1009
1145
  akm search "<query>" --detail full # Include scores, paths, timing
@@ -1011,8 +1147,8 @@ akm search "<query>" --detail full # Include scores, paths, timing
1011
1147
 
1012
1148
  | Flag | Values | Default |
1013
1149
  | --- | --- | --- |
1014
- | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`any\` | \`any\` |
1015
- | \`--source\` | \`local\`, \`registry\`, \`both\` | \`local\` |
1150
+ | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`memory\`, \`any\` | \`any\` |
1151
+ | \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
1016
1152
  | \`--limit\` | number | \`20\` |
1017
1153
  | \`--format\` | \`json\`, \`text\`, \`yaml\` | \`json\` |
1018
1154
  | \`--detail\` | \`brief\`, \`normal\`, \`full\` | \`brief\` |
@@ -1029,6 +1165,7 @@ akm show agent:architect # Show agent (returns system promp
1029
1165
  akm show knowledge:guide toc # Table of contents
1030
1166
  akm show knowledge:guide section "Auth" # Specific section
1031
1167
  akm show knowledge:guide lines 10 30 # Line range
1168
+ akm show viking://resources/my-doc # Show remote OpenViking content
1032
1169
  \`\`\`
1033
1170
 
1034
1171
  | Type | Key fields returned |
@@ -1038,6 +1175,7 @@ akm show knowledge:guide lines 10 30 # Line range
1038
1175
  | command | \`template\`, \`description\`, \`parameters\` |
1039
1176
  | agent | \`prompt\`, \`description\`, \`modelHint\`, \`toolPolicy\` |
1040
1177
  | knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
1178
+ | memory | \`content\` (recalled context) |
1041
1179
 
1042
1180
  ## Install & Manage Kits
1043
1181
 
@@ -1076,6 +1214,8 @@ akm registry add <url> --provider skills-sh # Specify provider type
1076
1214
  akm registry remove <url-or-name> # Remove a registry
1077
1215
  akm registry search "<query>" # Search all registries
1078
1216
  akm registry search "<query>" --assets # Include asset-level results
1217
+ akm registry build-index # Build ./index.json
1218
+ akm registry build-index --out dist/index.json # Build to a custom path
1079
1219
  \`\`\`
1080
1220
 
1081
1221
  ## 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
+ }