akm-cli 0.0.22 → 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.
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { agentikitAdd } from "./stash-add";
17
17
  import { agentikitClone } from "./stash-clone";
18
18
  import { agentikitSearch, parseSearchSource } from "./stash-search";
19
19
  import { agentikitShowUnified } from "./stash-show";
20
- import { resolveStashSources } from "./stash-source";
20
+ import { addStashSource, listStashSources, removeStashSource } from "./stash-source-manage";
21
21
  import { setQuiet, warn } from "./warn";
22
22
  // Version: prefer compile-time define, then package.json, then fallback
23
23
  const pkgVersion = (() => {
@@ -814,23 +814,17 @@ const registryCommand = defineCommand({
814
814
  }),
815
815
  },
816
816
  });
817
- const sourcesCommand = defineCommand({
818
- meta: { name: "sources", description: "Manage stash sources (local paths and remote providers)" },
819
- subCommands: {
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 {
820
823
  list: defineCommand({
821
824
  meta: { name: "list", description: "List all stash sources" },
822
825
  run() {
823
826
  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
- });
827
+ output(`${outputPrefix}`, listStashSources());
834
828
  });
835
829
  },
836
830
  }),
@@ -844,50 +838,31 @@ const sourcesCommand = defineCommand({
844
838
  },
845
839
  run({ args }) {
846
840
  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");
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");
871
850
  }
851
+ parsedOptions = parsed;
872
852
  }
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;
853
+ catch (err) {
854
+ if (err instanceof UsageError)
855
+ throw err;
856
+ throw new UsageError("--options must be valid JSON");
881
857
  }
882
- const entry = { type: "filesystem", path: resolvedPath };
883
- if (args.name)
884
- entry.name = args.name;
885
- stashes.push(entry);
886
858
  }
887
- // Migrate: remove remoteStashSources when moving to stashes
888
- const { remoteStashSources, ...rest } = config;
889
- saveConfig({ ...rest, stashes });
890
- output("sources-add", { stashes, added: true });
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);
891
866
  });
892
867
  },
893
868
  }),
@@ -898,21 +873,20 @@ const sourcesCommand = defineCommand({
898
873
  },
899
874
  run({ args }) {
900
875
  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 });
876
+ const result = removeStashSource(args.target);
877
+ output(`${outputPrefix}-remove`, result);
912
878
  });
913
879
  },
914
880
  }),
915
- },
881
+ };
882
+ }
883
+ const stashCommand = defineCommand({
884
+ meta: { name: "stash", description: "Manage stash sources (local paths and remote providers)" },
885
+ subCommands: buildSourceSubCommands("stash"),
886
+ });
887
+ const sourcesCommand = defineCommand({
888
+ meta: { name: "sources", description: "Manage stash sources (alias for 'akm stash')" },
889
+ subCommands: buildSourceSubCommands("sources"),
916
890
  });
917
891
  const hintsCommand = defineCommand({
918
892
  meta: {
@@ -949,6 +923,7 @@ const main = defineCommand({
949
923
  search: searchCommand,
950
924
  show: showCommand,
951
925
  clone: cloneCommand,
926
+ stash: stashCommand,
952
927
  sources: sourcesCommand,
953
928
  registry: registryCommand,
954
929
  config: configCommand,
@@ -25,8 +25,6 @@ export function parseConfigValue(key, value) {
25
25
  return { llm: parseLlmConnectionValue(value) };
26
26
  case "registries":
27
27
  return { registries: parseRegistriesValue(value) };
28
- case "remoteStashSources":
29
- return { remoteStashSources: parseStashesValue(value) };
30
28
  case "stashes":
31
29
  return { stashes: parseStashesValue(value) };
32
30
  case "output.format":
@@ -51,8 +49,6 @@ export function getConfigValue(config, key) {
51
49
  return config.llm ?? null;
52
50
  case "registries":
53
51
  return config.registries ?? DEFAULT_CONFIG.registries ?? [];
54
- case "remoteStashSources":
55
- return config.remoteStashSources ?? [];
56
52
  case "stashes":
57
53
  return config.stashes ?? [];
58
54
  case "output.format":
@@ -71,7 +67,6 @@ export function setConfigValue(config, key, rawValue) {
71
67
  case "embedding":
72
68
  case "llm":
73
69
  case "registries":
74
- case "remoteStashSources":
75
70
  case "stashes":
76
71
  case "output.format":
77
72
  case "output.detail":
@@ -90,8 +85,6 @@ export function unsetConfigValue(config, key) {
90
85
  return { ...config, llm: undefined };
91
86
  case "registries":
92
87
  return { ...config, registries: undefined };
93
- case "remoteStashSources":
94
- return { ...config, remoteStashSources: undefined };
95
88
  case "stashes":
96
89
  return { ...config, stashes: undefined };
97
90
  case "output.format":
@@ -115,11 +108,8 @@ export function listConfig(config) {
115
108
  result.embedding = config.embedding;
116
109
  if (config.llm)
117
110
  result.llm = config.llm;
118
- // Show legacy keys only if they still have content
119
111
  if (config.searchPaths?.length)
120
112
  result.searchPaths = config.searchPaths;
121
- if (config.remoteStashSources?.length)
122
- result.remoteStashSources = config.remoteStashSources;
123
113
  return result;
124
114
  }
125
115
  function mergeConfigValue(config, partial) {
package/dist/config.js CHANGED
@@ -23,7 +23,7 @@ export function getConfigPath() {
23
23
  }
24
24
  // ── Load / Save / Update ────────────────────────────────────────────────────
25
25
  let cachedConfig;
26
- export function loadConfig(opts) {
26
+ export function loadConfig() {
27
27
  const configPath = getConfigPath();
28
28
  let stat;
29
29
  try {
@@ -51,94 +51,11 @@ export function loadConfig(opts) {
51
51
  if (envKey)
52
52
  config.llm.apiKey = envKey;
53
53
  }
54
- if (!opts?.readOnly) {
55
- // Migrate installed[source: "local"] → stashes[type: "filesystem"]
56
- try {
57
- migrateLocalInstalledToStashes(config);
58
- }
59
- catch (err) {
60
- console.warn("[agentikit] Warning: config migration (local→stashes) failed:", err instanceof Error ? err.message : String(err));
61
- }
62
- // Migrate remoteStashSources → stashes[]
63
- try {
64
- migrateRemoteStashSourcesToStashes(config);
65
- }
66
- catch (err) {
67
- console.warn("[agentikit] Warning: config migration (remoteStashSources→stashes) failed:", err instanceof Error ? err.message : String(err));
68
- }
69
- }
70
54
  // Cache the parsed config with its path and mtime for subsequent calls.
71
55
  // Reuse the stat already obtained above (avoids a second syscall + TOCTOU gap).
72
56
  cachedConfig = { config, path: configPath, mtime: stat.mtimeMs };
73
57
  return config;
74
58
  }
75
- /**
76
- * Migrate installed entries with source "local" to stashes[] as filesystem entries.
77
- * Local directories are search paths, not registry kits — they don't need version
78
- * tracking, cache management, or update support.
79
- *
80
- * Mutates the config in place and persists to disk if any entries are migrated.
81
- */
82
- function migrateLocalInstalledToStashes(config) {
83
- const installed = config.installed;
84
- if (!installed)
85
- return;
86
- const localEntries = installed.filter((e) => e.source === "local");
87
- if (localEntries.length === 0)
88
- return;
89
- const stashes = [...(config.stashes ?? [])];
90
- const existingPaths = new Set(stashes.filter((s) => !!s.path).map((s) => path.resolve(s.path)));
91
- let migrated = 0;
92
- for (const entry of localEntries) {
93
- const resolved = path.resolve(entry.stashRoot);
94
- if (existingPaths.has(resolved))
95
- continue;
96
- stashes.push({
97
- type: "filesystem",
98
- path: resolved,
99
- name: entry.id,
100
- });
101
- existingPaths.add(resolved);
102
- migrated++;
103
- }
104
- if (migrated === 0)
105
- return;
106
- // Remove local entries from installed, add to stashes
107
- config.installed = installed.filter((e) => e.source !== "local");
108
- config.stashes = stashes;
109
- saveConfig(config);
110
- }
111
- /**
112
- * Migrate remoteStashSources[] to stashes[] entries.
113
- * Each remote source becomes a typed stash entry (e.g. type: "openviking").
114
- *
115
- * Mutates the config in place and persists to disk if any entries are migrated.
116
- */
117
- function migrateRemoteStashSourcesToStashes(config) {
118
- const remoteSources = config.remoteStashSources;
119
- if (!remoteSources || remoteSources.length === 0)
120
- return;
121
- const stashes = [...(config.stashes ?? [])];
122
- const existingUrls = new Set(stashes.filter((s) => !!s.url).map((s) => s.url));
123
- let migrated = 0;
124
- for (const entry of remoteSources) {
125
- if (!entry.url || existingUrls.has(entry.url))
126
- continue;
127
- stashes.push({
128
- type: entry.type ?? "openviking",
129
- url: entry.url,
130
- name: entry.name,
131
- options: entry.options,
132
- });
133
- existingUrls.add(entry.url);
134
- migrated++;
135
- }
136
- if (migrated === 0)
137
- return;
138
- config.stashes = stashes;
139
- config.remoteStashSources = undefined;
140
- saveConfig(config);
141
- }
142
59
  export function saveConfig(config) {
143
60
  cachedConfig = undefined;
144
61
  const configPath = getConfigPath();
@@ -175,11 +92,9 @@ function sanitizeConfigForWrite(config) {
175
92
  const { apiKey, ...rest } = config.llm;
176
93
  sanitized.llm = rest;
177
94
  }
178
- // Drop empty/migrated keys to keep config clean
95
+ // Drop empty keys to keep config clean
179
96
  if (!config.searchPaths?.length)
180
97
  delete sanitized.searchPaths;
181
- if (!config.remoteStashSources?.length)
182
- delete sanitized.remoteStashSources;
183
98
  return sanitized;
184
99
  }
185
100
  export function updateConfig(partial) {
@@ -225,9 +140,6 @@ function pickKnownKeys(raw) {
225
140
  const registries = parseRegistriesConfig(raw.registries);
226
141
  if (registries)
227
142
  config.registries = registries;
228
- const remoteStash = parseStashesConfig(raw.remoteStashSources);
229
- if (remoteStash)
230
- config.remoteStashSources = remoteStash;
231
143
  const stashes = parseStashesConfig(raw.stashes);
232
144
  if (stashes)
233
145
  config.stashes = stashes;
@@ -19,33 +19,18 @@ export function resolveStashProviderFactory(type) {
19
19
  }
20
20
  /**
21
21
  * Resolve all non-filesystem stash providers from config.
22
- * Sources come from `stashes` (new) or `remoteStashSources` (legacy).
23
22
  * Filesystem entries are excluded — they are handled by resolveStashSources().
24
23
  */
25
24
  export function resolveStashProviders(config) {
26
25
  const providers = [];
27
- // New config: stashes[]
28
- if (config.stashes) {
29
- for (const entry of config.stashes) {
30
- if (entry.enabled === false)
31
- continue;
32
- if (entry.type === "filesystem")
33
- continue;
34
- const factory = registry.resolve(entry.type);
35
- if (factory) {
36
- providers.push(factory(entry));
37
- }
38
- }
39
- }
40
- // Legacy config: remoteStashSources[] → map to stash providers
41
- if (!config.stashes && config.remoteStashSources) {
42
- for (const entry of config.remoteStashSources) {
43
- if (entry.enabled === false)
44
- continue;
45
- const factory = registry.resolve(entry.type ?? "openviking");
46
- if (factory) {
47
- providers.push(factory(entry));
48
- }
26
+ for (const entry of config.stashes ?? []) {
27
+ if (entry.enabled === false)
28
+ continue;
29
+ if (entry.type === "filesystem")
30
+ continue;
31
+ const factory = registry.resolve(entry.type);
32
+ if (factory) {
33
+ providers.push(factory(entry));
49
34
  }
50
35
  }
51
36
  return providers;
@@ -0,0 +1,82 @@
1
+ import path from "node:path";
2
+ import { loadConfig, saveConfig } from "./config";
3
+ import { UsageError } from "./errors";
4
+ import { resolveStashSources } from "./stash-source";
5
+ // ── Operations ──────────────────────────────────────────────────────────────
6
+ /**
7
+ * Add a stash source (filesystem path or remote provider URL) to config.
8
+ *
9
+ * Filesystem paths are auto-detected when `target` does not start with
10
+ * `http://` or `https://`. URL sources require a `providerType` option
11
+ * (e.g. "openviking").
12
+ */
13
+ export function addStashSource(opts) {
14
+ const { target, name, providerType, options: providerOptions } = opts;
15
+ const config = loadConfig();
16
+ const stashes = [...(config.stashes ?? [])];
17
+ const isUrl = target.startsWith("http://") || target.startsWith("https://");
18
+ let entry;
19
+ if (isUrl) {
20
+ if (!providerType) {
21
+ throw new UsageError("--provider is required for URL sources (e.g. --provider openviking)");
22
+ }
23
+ // Deduplicate by URL
24
+ if (stashes.some((s) => s.url === target)) {
25
+ return { stashes, added: false, message: "Source URL already configured" };
26
+ }
27
+ entry = { type: providerType, url: target };
28
+ if (name)
29
+ entry.name = name;
30
+ if (providerOptions)
31
+ entry.options = providerOptions;
32
+ }
33
+ else {
34
+ // Filesystem path
35
+ const resolvedPath = path.resolve(target);
36
+ if (stashes.some((s) => s.path && path.resolve(s.path) === resolvedPath)) {
37
+ return { stashes, added: false, message: "Source path already configured" };
38
+ }
39
+ entry = { type: "filesystem", path: resolvedPath };
40
+ if (name)
41
+ entry.name = name;
42
+ }
43
+ stashes.push(entry);
44
+ saveConfig({ ...config, stashes });
45
+ return { stashes, added: true, entry };
46
+ }
47
+ /**
48
+ * Remove a stash source by URL, path, or name.
49
+ * Match priority: URL > path > name (most specific first).
50
+ */
51
+ export function removeStashSource(target) {
52
+ const config = loadConfig();
53
+ const stashes = [...(config.stashes ?? [])];
54
+ const isUrl = target.startsWith("http://") || target.startsWith("https://");
55
+ const resolvedPath = !isUrl ? path.resolve(target) : undefined;
56
+ // Try URL match first, then path, then name (most specific → least specific)
57
+ let idx = -1;
58
+ if (isUrl) {
59
+ idx = stashes.findIndex((s) => s.url === target);
60
+ }
61
+ if (idx === -1 && resolvedPath) {
62
+ idx = stashes.findIndex((s) => s.path && path.resolve(s.path) === resolvedPath);
63
+ }
64
+ if (idx === -1) {
65
+ idx = stashes.findIndex((s) => s.name === target);
66
+ }
67
+ if (idx === -1) {
68
+ return { stashes, removed: false, message: "No matching source found" };
69
+ }
70
+ const removed = stashes.splice(idx, 1)[0];
71
+ saveConfig({ ...config, stashes });
72
+ return { stashes, removed: true, entry: removed };
73
+ }
74
+ /**
75
+ * List all stash sources (local filesystem + configured stashes).
76
+ */
77
+ export function listStashSources() {
78
+ const config = loadConfig();
79
+ const localSources = resolveStashSources();
80
+ const stashes = config.stashes ?? [];
81
+ return { localSources, stashes };
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "type": "module",
5
5
  "description": "CLI tool to search, open, and run extension assets from an akm stash directory.",
6
6
  "keywords": [