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
@@ -25,6 +25,8 @@ export function parseConfigValue(key, value) {
25
25
  return { llm: parseLlmConnectionValue(value) };
26
26
  case "registries":
27
27
  return { registries: parseRegistriesValue(value) };
28
+ case "stashes":
29
+ return { stashes: parseStashesValue(value) };
28
30
  case "output.format":
29
31
  return { output: { format: parseOutputFormat(value) } };
30
32
  case "output.detail":
@@ -47,6 +49,8 @@ export function getConfigValue(config, key) {
47
49
  return config.llm ?? null;
48
50
  case "registries":
49
51
  return config.registries ?? DEFAULT_CONFIG.registries ?? [];
52
+ case "stashes":
53
+ return config.stashes ?? [];
50
54
  case "output.format":
51
55
  return config.output?.format ?? null;
52
56
  case "output.detail":
@@ -63,6 +67,7 @@ export function setConfigValue(config, key, rawValue) {
63
67
  case "embedding":
64
68
  case "llm":
65
69
  case "registries":
70
+ case "stashes":
66
71
  case "output.format":
67
72
  case "output.detail":
68
73
  return mergeConfigValue(config, parseConfigValue(key, rawValue));
@@ -80,6 +85,8 @@ export function unsetConfigValue(config, key) {
80
85
  return { ...config, llm: undefined };
81
86
  case "registries":
82
87
  return { ...config, registries: undefined };
88
+ case "stashes":
89
+ return { ...config, stashes: undefined };
83
90
  case "output.format":
84
91
  return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
85
92
  case "output.detail":
@@ -89,15 +96,21 @@ export function unsetConfigValue(config, key) {
89
96
  }
90
97
  }
91
98
  export function listConfig(config) {
92
- return {
93
- ...DEFAULT_CONFIG,
94
- ...config,
99
+ const result = {
100
+ semanticSearch: config.semanticSearch,
101
+ registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
95
102
  output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
96
103
  stashDir: config.stashDir ?? null,
97
- embedding: config.embedding ?? null,
98
- llm: config.llm ?? null,
99
- registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
104
+ installed: config.installed ?? [],
105
+ stashes: config.stashes ?? [],
100
106
  };
107
+ if (config.embedding)
108
+ result.embedding = config.embedding;
109
+ if (config.llm)
110
+ result.llm = config.llm;
111
+ if (config.searchPaths?.length)
112
+ result.searchPaths = config.searchPaths;
113
+ return result;
101
114
  }
102
115
  function mergeConfigValue(config, partial) {
103
116
  return {
@@ -236,3 +249,39 @@ function parseUnknownPositiveInteger(value, key) {
236
249
  }
237
250
  return value;
238
251
  }
252
+ function parseStashesValue(value) {
253
+ if (value === "null" || value === "")
254
+ return undefined;
255
+ let parsed;
256
+ try {
257
+ parsed = JSON.parse(value);
258
+ }
259
+ catch {
260
+ throw new UsageError(`Invalid value for stashes: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
261
+ }
262
+ if (!Array.isArray(parsed)) {
263
+ throw new UsageError(`Invalid value for stashes: expected a JSON array`);
264
+ }
265
+ return parsed.map((entry, i) => {
266
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
267
+ throw new UsageError(`Invalid value for stashes[${i}]: expected an object with a "type" field`);
268
+ }
269
+ const obj = entry;
270
+ if (typeof obj.type !== "string" || !obj.type) {
271
+ throw new UsageError(`Invalid value for stashes[${i}]: "type" is required`);
272
+ }
273
+ const result = { type: obj.type };
274
+ if (typeof obj.path === "string" && obj.path)
275
+ result.path = obj.path;
276
+ if (typeof obj.url === "string" && obj.url)
277
+ result.url = obj.url;
278
+ if (typeof obj.name === "string" && obj.name)
279
+ result.name = obj.name;
280
+ if (typeof obj.enabled === "boolean")
281
+ result.enabled = obj.enabled;
282
+ if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
283
+ result.options = obj.options;
284
+ }
285
+ return result;
286
+ });
287
+ }
package/dist/config.js CHANGED
@@ -25,8 +25,9 @@ export function getConfigPath() {
25
25
  let cachedConfig;
26
26
  export function loadConfig() {
27
27
  const configPath = getConfigPath();
28
+ let stat;
28
29
  try {
29
- const stat = fs.statSync(configPath);
30
+ stat = fs.statSync(configPath);
30
31
  if (cachedConfig && cachedConfig.path === configPath && cachedConfig.mtime === stat.mtimeMs) {
31
32
  return cachedConfig.config;
32
33
  }
@@ -37,10 +38,9 @@ export function loadConfig() {
37
38
  return { ...DEFAULT_CONFIG };
38
39
  }
39
40
  const raw = readConfigObject(configPath);
40
- const config = raw ? pickKnownKeys(raw) : { ...DEFAULT_CONFIG };
41
- // Inject API keys from environment variables.
42
- // API keys should be provided via AKM_EMBED_API_KEY and AKM_LLM_API_KEY
43
- // rather than stored in the config file.
41
+ const expanded = raw ? expandEnvVars(raw) : undefined;
42
+ const config = expanded ? pickKnownKeys(expanded) : { ...DEFAULT_CONFIG };
43
+ // Legacy: inject API keys from well-known env vars when not set via ${} substitution
44
44
  if (config.embedding && !config.embedding.apiKey) {
45
45
  const envKey = process.env.AKM_EMBED_API_KEY?.trim();
46
46
  if (envKey)
@@ -51,14 +51,9 @@ export function loadConfig() {
51
51
  if (envKey)
52
52
  config.llm.apiKey = envKey;
53
53
  }
54
- // Cache the parsed config with its path and mtime for subsequent calls
55
- try {
56
- const stat = fs.statSync(configPath);
57
- cachedConfig = { config, path: configPath, mtime: stat.mtimeMs };
58
- }
59
- catch {
60
- // If we can't stat (unlikely since we just read it), skip caching
61
- }
54
+ // Cache the parsed config with its path and mtime for subsequent calls.
55
+ // Reuse the stat already obtained above (avoids a second syscall + TOCTOU gap).
56
+ cachedConfig = { config, path: configPath, mtime: stat.mtimeMs };
62
57
  return config;
63
58
  }
64
59
  export function saveConfig(config) {
@@ -67,7 +62,7 @@ export function saveConfig(config) {
67
62
  const dir = path.dirname(configPath);
68
63
  fs.mkdirSync(dir, { recursive: true });
69
64
  const sanitized = sanitizeConfigForWrite(config);
70
- const tmpPath = `${configPath}.tmp.${process.pid}`;
65
+ const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
71
66
  try {
72
67
  fs.writeFileSync(tmpPath, `${JSON.stringify(sanitized, null, 2)}\n`, "utf8");
73
68
  fs.renameSync(tmpPath, configPath);
@@ -89,19 +84,35 @@ export function saveConfig(config) {
89
84
  */
90
85
  function sanitizeConfigForWrite(config) {
91
86
  const sanitized = { ...config };
92
- if (sanitized.embedding) {
93
- const { apiKey, ...rest } = sanitized.embedding;
87
+ if (config.embedding) {
88
+ const { apiKey, ...rest } = config.embedding;
94
89
  sanitized.embedding = rest;
95
90
  }
96
- if (sanitized.llm) {
97
- const { apiKey, ...rest } = sanitized.llm;
91
+ if (config.llm) {
92
+ const { apiKey, ...rest } = config.llm;
98
93
  sanitized.llm = rest;
99
94
  }
95
+ // Drop empty keys to keep config clean
96
+ if (!config.searchPaths?.length)
97
+ delete sanitized.searchPaths;
100
98
  return sanitized;
101
99
  }
102
100
  export function updateConfig(partial) {
103
101
  const current = loadConfig();
102
+ // Shallow-merge for top-level scalar fields; deep-merge known object-type config keys.
104
103
  const merged = { ...current, ...partial };
104
+ // Deep-merge output — partial update should not wipe sibling keys
105
+ if (current.output && partial.output && partial.output !== current.output) {
106
+ merged.output = { ...current.output, ...partial.output };
107
+ }
108
+ // Deep-merge embedding — only when both sides are objects and partial does not intend to clear
109
+ if (current.embedding && partial.embedding && partial.embedding !== current.embedding) {
110
+ merged.embedding = { ...current.embedding, ...partial.embedding };
111
+ }
112
+ // Deep-merge llm — same pattern
113
+ if (current.llm && partial.llm && partial.llm !== current.llm) {
114
+ merged.llm = { ...current.llm, ...partial.llm };
115
+ }
105
116
  saveConfig(merged);
106
117
  return merged;
107
118
  }
@@ -129,6 +140,9 @@ function pickKnownKeys(raw) {
129
140
  const registries = parseRegistriesConfig(raw.registries);
130
141
  if (registries)
131
142
  config.registries = registries;
143
+ const stashes = parseStashesConfig(raw.stashes);
144
+ if (stashes)
145
+ config.stashes = stashes;
132
146
  const output = parseOutputConfig(raw.output);
133
147
  if (output)
134
148
  config.output = output;
@@ -147,6 +161,49 @@ function parseOutputConfig(value) {
147
161
  }
148
162
  return Object.keys(output).length > 0 ? output : undefined;
149
163
  }
164
+ /**
165
+ * Field names that hold URLs and must NOT have env var substitution applied.
166
+ * Expanding ${VAR} inside a URL could leak secrets by redirecting requests to
167
+ * an attacker-controlled server if the config file is world-readable.
168
+ */
169
+ const URL_FIELD_NAMES = new Set(["url", "endpoint", "artifactUrl"]);
170
+ /**
171
+ * Recursively expand `${VAR}` references in all string values.
172
+ * Supports `${VAR}`, `${VAR:-default}`, and bare `$VAR` at the start of a value.
173
+ * Non-string values pass through unchanged.
174
+ *
175
+ * URL-type fields (named `url`, `endpoint`, `artifactUrl`, or whose value starts
176
+ * with `http://` / `https://`) are skipped to prevent secret injection into URLs.
177
+ */
178
+ function expandEnvVars(value, fieldName) {
179
+ if (typeof value === "string") {
180
+ // Skip URL-type fields by name or by value prefix
181
+ if ((fieldName !== undefined && URL_FIELD_NAMES.has(fieldName)) ||
182
+ value.startsWith("http://") ||
183
+ value.startsWith("https://")) {
184
+ return value;
185
+ }
186
+ return value.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
187
+ if (braced) {
188
+ const [name, ...rest] = braced.split(":-");
189
+ const fallback = rest.join(":-");
190
+ return process.env[name] ?? fallback ?? "";
191
+ }
192
+ return process.env[bare] ?? "";
193
+ });
194
+ }
195
+ if (Array.isArray(value)) {
196
+ return value.map((item) => expandEnvVars(item));
197
+ }
198
+ if (value !== null && typeof value === "object") {
199
+ const out = {};
200
+ for (const [k, v] of Object.entries(value)) {
201
+ out[k] = expandEnvVars(v, k);
202
+ }
203
+ return out;
204
+ }
205
+ return value;
206
+ }
150
207
  function readConfigObject(configPath) {
151
208
  try {
152
209
  const text = fs.readFileSync(configPath, "utf8");
@@ -168,7 +225,6 @@ export function stripJsonComments(text) {
168
225
  let result = "";
169
226
  let i = 0;
170
227
  let inString = false;
171
- let stringChar = "";
172
228
  while (i < text.length) {
173
229
  if (inString) {
174
230
  if (text[i] === "\\") {
@@ -176,16 +232,16 @@ export function stripJsonComments(text) {
176
232
  i += 2;
177
233
  continue;
178
234
  }
179
- if (text[i] === stringChar) {
235
+ if (text[i] === '"') {
180
236
  inString = false;
181
237
  }
182
238
  result += text[i];
183
239
  i++;
184
240
  continue;
185
241
  }
186
- if (text[i] === '"' || text[i] === "'") {
242
+ // JSON only uses double-quoted strings; single quotes are not valid JSON
243
+ if (text[i] === '"') {
187
244
  inString = true;
188
- stringChar = text[i];
189
245
  result += text[i];
190
246
  i++;
191
247
  continue;
@@ -213,6 +269,10 @@ function parseEmbeddingConfig(value) {
213
269
  const obj = value;
214
270
  if (typeof obj.endpoint !== "string" || !obj.endpoint)
215
271
  return undefined;
272
+ if (!obj.endpoint.startsWith("http://") && !obj.endpoint.startsWith("https://")) {
273
+ console.warn(`[agentikit] Ignoring embedding config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
274
+ return undefined;
275
+ }
216
276
  if (typeof obj.model !== "string" || !obj.model)
217
277
  return undefined;
218
278
  const result = {
@@ -242,6 +302,10 @@ function parseLlmConfig(value) {
242
302
  const obj = value;
243
303
  if (typeof obj.endpoint !== "string" || !obj.endpoint)
244
304
  return undefined;
305
+ if (!obj.endpoint.startsWith("http://") && !obj.endpoint.startsWith("https://")) {
306
+ console.warn(`[agentikit] Ignoring llm config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
307
+ return undefined;
308
+ }
245
309
  if (typeof obj.model !== "string" || !obj.model)
246
310
  return undefined;
247
311
  const result = {
@@ -324,6 +388,38 @@ function parseRegistriesConfig(value) {
324
388
  // which overrides the default. Only return undefined if the field was not an array.
325
389
  return entries;
326
390
  }
391
+ function parseStashesConfig(value) {
392
+ if (!Array.isArray(value))
393
+ return undefined;
394
+ const entries = value
395
+ .map((entry) => parseStashConfigEntry(entry))
396
+ .filter((entry) => entry !== undefined);
397
+ return entries;
398
+ }
399
+ function parseStashConfigEntry(value) {
400
+ if (typeof value !== "object" || value === null || Array.isArray(value))
401
+ return undefined;
402
+ const obj = value;
403
+ const type = asNonEmptyString(obj.type);
404
+ if (!type)
405
+ return undefined;
406
+ const entry = { type };
407
+ const entryPath = asNonEmptyString(obj.path);
408
+ if (entryPath)
409
+ entry.path = entryPath;
410
+ const url = asNonEmptyString(obj.url);
411
+ if (url)
412
+ entry.url = url;
413
+ const name = asNonEmptyString(obj.name);
414
+ if (name)
415
+ entry.name = name;
416
+ if (typeof obj.enabled === "boolean")
417
+ entry.enabled = obj.enabled;
418
+ if (typeof obj.options === "object" && obj.options !== null && !Array.isArray(obj.options)) {
419
+ entry.options = obj.options;
420
+ }
421
+ return entry;
422
+ }
327
423
  function parseRegistryConfigEntry(value) {
328
424
  if (typeof value !== "object" || value === null || Array.isArray(value))
329
425
  return undefined;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Generic factory-map utility.
3
+ *
4
+ * Creates a lightweight registry that maps string keys to factory functions.
5
+ * Both registry-factory.ts (kit discovery) and stash-provider-factory.ts
6
+ * (stash source providers) are built on this utility.
7
+ */
8
+ export function createProviderRegistry() {
9
+ const map = new Map();
10
+ return {
11
+ register(type, factory) {
12
+ map.set(type, factory);
13
+ },
14
+ resolve(type) {
15
+ return map.get(type) ?? null;
16
+ },
17
+ };
18
+ }