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.
- package/README.md +8 -5
- package/dist/asset-spec.js +91 -10
- package/dist/cli.js +172 -57
- package/dist/common.js +15 -2
- package/dist/config-cli.js +55 -6
- package/dist/config.js +118 -22
- package/dist/create-provider-registry.js +18 -0
- package/dist/db.js +156 -53
- package/dist/embedder.js +36 -18
- package/dist/errors.js +6 -0
- package/dist/file-context.js +18 -19
- package/dist/frontmatter.js +19 -3
- package/dist/indexer.js +126 -89
- package/dist/{stash-registry.js → installed-kits.js} +16 -24
- package/dist/kit-include.js +108 -0
- package/dist/local-search.js +429 -0
- package/dist/lockfile.js +47 -5
- package/dist/matchers.js +6 -0
- package/dist/metadata.js +20 -10
- package/dist/paths.js +4 -0
- package/dist/providers/skills-sh.js +3 -2
- package/dist/providers/static-index.js +4 -9
- package/dist/registry-build-index.js +356 -0
- package/dist/registry-factory.js +19 -0
- package/dist/registry-install.js +114 -109
- package/dist/registry-resolve.js +44 -9
- package/dist/registry-search.js +14 -9
- package/dist/renderers.js +23 -7
- package/dist/ripgrep-install.js +9 -4
- package/dist/self-update.js +31 -4
- package/dist/stash-add.js +75 -6
- package/dist/stash-clone.js +1 -1
- package/dist/stash-provider-factory.js +37 -0
- package/dist/stash-provider.js +1 -0
- package/dist/stash-providers/filesystem.js +42 -0
- package/dist/stash-providers/index.js +9 -0
- package/dist/stash-providers/openviking.js +337 -0
- package/dist/stash-resolve.js +4 -4
- package/dist/stash-search.js +70 -401
- package/dist/stash-show.js +24 -5
- package/dist/stash-source-manage.js +82 -0
- package/dist/stash-source.js +19 -11
- package/dist/walker.js +15 -10
- package/dist/warn.js +7 -0
- package/package.json +1 -1
- package/dist/provider-registry.js +0 -8
package/dist/config-cli.js
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
// API keys
|
|
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
|
-
|
|
56
|
-
|
|
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 (
|
|
93
|
-
const { apiKey, ...rest } =
|
|
87
|
+
if (config.embedding) {
|
|
88
|
+
const { apiKey, ...rest } = config.embedding;
|
|
94
89
|
sanitized.embedding = rest;
|
|
95
90
|
}
|
|
96
|
-
if (
|
|
97
|
-
const { apiKey, ...rest } =
|
|
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] ===
|
|
235
|
+
if (text[i] === '"') {
|
|
180
236
|
inString = false;
|
|
181
237
|
}
|
|
182
238
|
result += text[i];
|
|
183
239
|
i++;
|
|
184
240
|
continue;
|
|
185
241
|
}
|
|
186
|
-
|
|
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
|
+
}
|