akm-cli 0.3.1 → 0.4.1
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 +602 -8
- package/dist/common.js +5 -0
- package/dist/config-cli.js +87 -0
- package/dist/config.js +197 -25
- package/dist/embedder.js +26 -4
- package/dist/init.js +2 -2
- package/dist/install-audit.js +324 -0
- package/dist/installed-kits.js +2 -2
- package/dist/matchers.js +25 -22
- package/dist/registry-install.js +46 -7
- package/dist/setup.js +2 -2
- package/dist/stash-add.js +4 -3
- package/dist/stash-source-manage.js +3 -3
- package/package.json +1 -1
package/dist/common.js
CHANGED
|
@@ -8,6 +8,11 @@ export const IS_WINDOWS = process.platform === "win32";
|
|
|
8
8
|
export function isHttpUrl(value) {
|
|
9
9
|
return !!value && /^https?:\/\//.test(value);
|
|
10
10
|
}
|
|
11
|
+
export function filterNonEmptyStrings(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return undefined;
|
|
14
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
15
|
+
}
|
|
11
16
|
// ── Validators ──────────────────────────────────────────────────────────────
|
|
12
17
|
export function isAssetType(type) {
|
|
13
18
|
return Object.hasOwn(TYPE_DIRS, type);
|
package/dist/config-cli.js
CHANGED
|
@@ -26,6 +26,16 @@ export function parseConfigValue(key, value) {
|
|
|
26
26
|
return { output: { format: parseOutputFormat(value) } };
|
|
27
27
|
case "output.detail":
|
|
28
28
|
return { output: { detail: parseOutputDetail(value) } };
|
|
29
|
+
case "security.installAudit.enabled":
|
|
30
|
+
return { security: { installAudit: { enabled: parseBooleanValue(value, key) } } };
|
|
31
|
+
case "security.installAudit.blockOnCritical":
|
|
32
|
+
return { security: { installAudit: { blockOnCritical: parseBooleanValue(value, key) } } };
|
|
33
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
34
|
+
return { security: { installAudit: { blockUnlistedRegistries: parseBooleanValue(value, key) } } };
|
|
35
|
+
case "security.installAudit.registryAllowlist":
|
|
36
|
+
return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
|
|
37
|
+
case "security.installAudit.registryWhitelist":
|
|
38
|
+
return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
|
|
29
39
|
default:
|
|
30
40
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
31
41
|
}
|
|
@@ -48,6 +58,18 @@ export function getConfigValue(config, key) {
|
|
|
48
58
|
return config.output?.format ?? null;
|
|
49
59
|
case "output.detail":
|
|
50
60
|
return config.output?.detail ?? null;
|
|
61
|
+
case "security":
|
|
62
|
+
return config.security ?? null;
|
|
63
|
+
case "security.installAudit.enabled":
|
|
64
|
+
return config.security?.installAudit?.enabled ?? null;
|
|
65
|
+
case "security.installAudit.blockOnCritical":
|
|
66
|
+
return config.security?.installAudit?.blockOnCritical ?? null;
|
|
67
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
68
|
+
return config.security?.installAudit?.blockUnlistedRegistries ?? null;
|
|
69
|
+
case "security.installAudit.registryAllowlist":
|
|
70
|
+
return getInstallAuditAllowlist(config);
|
|
71
|
+
case "security.installAudit.registryWhitelist":
|
|
72
|
+
return getInstallAuditAllowlist(config);
|
|
51
73
|
default:
|
|
52
74
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
53
75
|
}
|
|
@@ -62,6 +84,11 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
62
84
|
case "stashes":
|
|
63
85
|
case "output.format":
|
|
64
86
|
case "output.detail":
|
|
87
|
+
case "security.installAudit.enabled":
|
|
88
|
+
case "security.installAudit.blockOnCritical":
|
|
89
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
90
|
+
case "security.installAudit.registryAllowlist":
|
|
91
|
+
case "security.installAudit.registryWhitelist":
|
|
65
92
|
return mergeConfigValue(config, parseConfigValue(key, rawValue));
|
|
66
93
|
default:
|
|
67
94
|
throw new UsageError(`Unknown config key: ${key}`);
|
|
@@ -83,6 +110,28 @@ export function unsetConfigValue(config, key) {
|
|
|
83
110
|
return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
|
|
84
111
|
case "output.detail":
|
|
85
112
|
return { ...config, output: mergeOutputConfig(config.output, { detail: undefined }) };
|
|
113
|
+
case "security":
|
|
114
|
+
return { ...config, security: undefined };
|
|
115
|
+
case "security.installAudit.enabled":
|
|
116
|
+
return { ...config, security: mergeSecurityConfig(config.security, { installAudit: { enabled: undefined } }) };
|
|
117
|
+
case "security.installAudit.blockOnCritical":
|
|
118
|
+
return {
|
|
119
|
+
...config,
|
|
120
|
+
security: mergeSecurityConfig(config.security, { installAudit: { blockOnCritical: undefined } }),
|
|
121
|
+
};
|
|
122
|
+
case "security.installAudit.blockUnlistedRegistries":
|
|
123
|
+
return {
|
|
124
|
+
...config,
|
|
125
|
+
security: mergeSecurityConfig(config.security, { installAudit: { blockUnlistedRegistries: undefined } }),
|
|
126
|
+
};
|
|
127
|
+
case "security.installAudit.registryAllowlist":
|
|
128
|
+
case "security.installAudit.registryWhitelist":
|
|
129
|
+
return {
|
|
130
|
+
...config,
|
|
131
|
+
security: mergeSecurityConfig(config.security, {
|
|
132
|
+
installAudit: { registryAllowlist: undefined, registryWhitelist: undefined },
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
86
135
|
default:
|
|
87
136
|
throw new UsageError(`Unknown or unsupported unset key: ${key}`);
|
|
88
137
|
}
|
|
@@ -100,6 +149,8 @@ export function listConfig(config) {
|
|
|
100
149
|
result.embedding = config.embedding;
|
|
101
150
|
if (config.llm)
|
|
102
151
|
result.llm = config.llm;
|
|
152
|
+
if (config.security)
|
|
153
|
+
result.security = config.security;
|
|
103
154
|
return result;
|
|
104
155
|
}
|
|
105
156
|
function mergeConfigValue(config, partial) {
|
|
@@ -107,6 +158,7 @@ function mergeConfigValue(config, partial) {
|
|
|
107
158
|
...config,
|
|
108
159
|
...partial,
|
|
109
160
|
output: mergeOutputConfig(config.output, partial.output),
|
|
161
|
+
security: mergeSecurityConfig(config.security, partial.security),
|
|
110
162
|
};
|
|
111
163
|
}
|
|
112
164
|
function mergeOutputConfig(base, override) {
|
|
@@ -116,6 +168,18 @@ function mergeOutputConfig(base, override) {
|
|
|
116
168
|
};
|
|
117
169
|
return merged.format || merged.detail ? merged : undefined;
|
|
118
170
|
}
|
|
171
|
+
function mergeSecurityConfig(base, override) {
|
|
172
|
+
const mergedInstallAudit = mergeInstallAuditConfig(base?.installAudit, override?.installAudit);
|
|
173
|
+
return mergedInstallAudit ? { installAudit: mergedInstallAudit } : undefined;
|
|
174
|
+
}
|
|
175
|
+
function mergeInstallAuditConfig(base, override) {
|
|
176
|
+
const merged = {
|
|
177
|
+
...(base ?? {}),
|
|
178
|
+
...(override ?? {}),
|
|
179
|
+
};
|
|
180
|
+
const hasValue = Object.values(merged).some((value) => value !== undefined);
|
|
181
|
+
return hasValue ? merged : undefined;
|
|
182
|
+
}
|
|
119
183
|
function parseOutputFormat(value) {
|
|
120
184
|
if (value === "json" || value === "yaml" || value === "text")
|
|
121
185
|
return value;
|
|
@@ -126,6 +190,29 @@ function parseOutputDetail(value) {
|
|
|
126
190
|
return value;
|
|
127
191
|
throw new UsageError(`Invalid value for output.detail: expected one of brief|normal|full`);
|
|
128
192
|
}
|
|
193
|
+
function parseBooleanValue(value, key) {
|
|
194
|
+
if (value === "true")
|
|
195
|
+
return true;
|
|
196
|
+
if (value === "false")
|
|
197
|
+
return false;
|
|
198
|
+
throw new UsageError(`Invalid value for ${key}: expected true or false`);
|
|
199
|
+
}
|
|
200
|
+
function parseStringArrayValue(value, key) {
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = JSON.parse(value);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
|
209
|
+
throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
|
|
210
|
+
}
|
|
211
|
+
return parsed;
|
|
212
|
+
}
|
|
213
|
+
function getInstallAuditAllowlist(config) {
|
|
214
|
+
return config.security?.installAudit?.registryAllowlist ?? config.security?.installAudit?.registryWhitelist ?? null;
|
|
215
|
+
}
|
|
129
216
|
function parseRegistriesValue(value) {
|
|
130
217
|
if (value === "null" || value === "")
|
|
131
218
|
return undefined;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { filterNonEmptyStrings } from "./common";
|
|
3
4
|
import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "./paths";
|
|
4
5
|
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
5
6
|
export const DEFAULT_CONFIG = {
|
|
@@ -21,42 +22,47 @@ export function getConfigPath() {
|
|
|
21
22
|
return _getConfigPath();
|
|
22
23
|
}
|
|
23
24
|
// ── Load / Save / Update ────────────────────────────────────────────────────
|
|
25
|
+
const PROJECT_CONFIG_RELATIVE_PATH = path.join(".akm", "config.json");
|
|
24
26
|
let cachedConfig;
|
|
27
|
+
let cachedUserConfig;
|
|
25
28
|
export function resetConfigCache() {
|
|
26
29
|
cachedConfig = undefined;
|
|
30
|
+
cachedUserConfig = undefined;
|
|
27
31
|
}
|
|
28
|
-
export function
|
|
32
|
+
export function loadUserConfig() {
|
|
29
33
|
const configPath = getConfigPath();
|
|
30
34
|
let stat;
|
|
31
35
|
try {
|
|
32
36
|
stat = fs.statSync(configPath);
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
37
|
+
if (cachedUserConfig && cachedUserConfig.path === configPath && cachedUserConfig.mtime === stat.mtimeMs) {
|
|
38
|
+
return cachedUserConfig.config;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
catch {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return { ...DEFAULT_CONFIG };
|
|
41
|
-
}
|
|
42
|
-
const raw = readConfigObject(configPath);
|
|
43
|
-
const expanded = raw ? expandEnvVars(raw) : undefined;
|
|
44
|
-
const config = expanded ? pickKnownKeys(expanded) : { ...DEFAULT_CONFIG };
|
|
45
|
-
// Legacy: inject API keys from well-known env vars when not set via ${} substitution
|
|
46
|
-
if (config.embedding && !config.embedding.apiKey) {
|
|
47
|
-
const envKey = process.env.AKM_EMBED_API_KEY?.trim();
|
|
48
|
-
if (envKey)
|
|
49
|
-
config.embedding.apiKey = envKey;
|
|
42
|
+
cachedUserConfig = undefined;
|
|
43
|
+
return applyRuntimeEnvApiKeys({ ...DEFAULT_CONFIG });
|
|
50
44
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
const config = mergeLoadedConfig(DEFAULT_CONFIG, readNormalizedConfig(configPath));
|
|
46
|
+
const finalConfig = applyRuntimeEnvApiKeys(config);
|
|
47
|
+
cachedUserConfig = { config: finalConfig, path: configPath, mtime: stat.mtimeMs };
|
|
48
|
+
return finalConfig;
|
|
49
|
+
}
|
|
50
|
+
export function loadConfig() {
|
|
51
|
+
const configPaths = getEffectiveConfigPaths();
|
|
52
|
+
const signature = getConfigSignature(configPaths);
|
|
53
|
+
if (cachedConfig && cachedConfig.signature === signature) {
|
|
54
|
+
return cachedConfig.config;
|
|
55
|
+
}
|
|
56
|
+
let config = loadUserConfig();
|
|
57
|
+
const userConfigPath = getConfigPath();
|
|
58
|
+
for (const configPath of configPaths) {
|
|
59
|
+
if (configPath === userConfigPath)
|
|
60
|
+
continue;
|
|
61
|
+
config = mergeLoadedConfig(config, readNormalizedConfig(configPath));
|
|
55
62
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return config;
|
|
63
|
+
const finalConfig = applyRuntimeEnvApiKeys(config);
|
|
64
|
+
cachedConfig = { config: finalConfig, signature };
|
|
65
|
+
return finalConfig;
|
|
60
66
|
}
|
|
61
67
|
export function saveConfig(config) {
|
|
62
68
|
cachedConfig = undefined;
|
|
@@ -98,7 +104,7 @@ function sanitizeConfigForWrite(config) {
|
|
|
98
104
|
return sanitized;
|
|
99
105
|
}
|
|
100
106
|
export function updateConfig(partial) {
|
|
101
|
-
const current =
|
|
107
|
+
const current = loadUserConfig();
|
|
102
108
|
// Shallow-merge for top-level scalar fields; deep-merge known object-type config keys.
|
|
103
109
|
const merged = { ...current, ...partial };
|
|
104
110
|
// Deep-merge output — partial update should not wipe sibling keys
|
|
@@ -113,12 +119,22 @@ export function updateConfig(partial) {
|
|
|
113
119
|
if (current.llm && partial.llm && partial.llm !== current.llm) {
|
|
114
120
|
merged.llm = { ...current.llm, ...partial.llm };
|
|
115
121
|
}
|
|
122
|
+
if (current.security && partial.security && partial.security !== current.security) {
|
|
123
|
+
merged.security = mergeSecurityConfig(current.security, partial.security);
|
|
124
|
+
}
|
|
116
125
|
saveConfig(merged);
|
|
117
126
|
return merged;
|
|
118
127
|
}
|
|
119
128
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
129
|
+
/**
|
|
130
|
+
* Normalize a raw config object into a sparse config layer containing only
|
|
131
|
+
* recognized keys that were valid in the source object. This function does not
|
|
132
|
+
* merge with DEFAULT_CONFIG; callers are responsible for layering defaults and
|
|
133
|
+
* combining multiple config sources so project config files only override what
|
|
134
|
+
* they set.
|
|
135
|
+
*/
|
|
120
136
|
function pickKnownKeys(raw) {
|
|
121
|
-
const config = {
|
|
137
|
+
const config = {};
|
|
122
138
|
if (typeof raw.stashDir === "string" && raw.stashDir.trim()) {
|
|
123
139
|
config.stashDir = raw.stashDir.trim();
|
|
124
140
|
}
|
|
@@ -159,14 +175,25 @@ function pickKnownKeys(raw) {
|
|
|
159
175
|
const registries = parseRegistriesConfig(raw.registries);
|
|
160
176
|
if (registries)
|
|
161
177
|
config.registries = registries;
|
|
178
|
+
if (typeof raw.disableGlobalStashes === "boolean") {
|
|
179
|
+
config.disableGlobalStashes = raw.disableGlobalStashes;
|
|
180
|
+
}
|
|
162
181
|
const stashes = parseStashesConfig(raw.stashes);
|
|
163
182
|
if (stashes)
|
|
164
183
|
config.stashes = stashes;
|
|
184
|
+
const security = parseSecurityConfig(raw.security);
|
|
185
|
+
if (security)
|
|
186
|
+
config.security = security;
|
|
165
187
|
const output = parseOutputConfig(raw.output);
|
|
166
188
|
if (output)
|
|
167
189
|
config.output = output;
|
|
168
190
|
return config;
|
|
169
191
|
}
|
|
192
|
+
function readNormalizedConfig(configPath) {
|
|
193
|
+
const raw = readConfigObject(configPath);
|
|
194
|
+
const expanded = raw ? expandEnvVars(raw) : undefined;
|
|
195
|
+
return expanded ? pickKnownKeys(expanded) : undefined;
|
|
196
|
+
}
|
|
170
197
|
function parseOutputConfig(value) {
|
|
171
198
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
172
199
|
return undefined;
|
|
@@ -440,6 +467,32 @@ function parseStashesConfig(value) {
|
|
|
440
467
|
.filter((entry) => entry !== undefined);
|
|
441
468
|
return entries;
|
|
442
469
|
}
|
|
470
|
+
function parseSecurityConfig(value) {
|
|
471
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
472
|
+
return undefined;
|
|
473
|
+
const obj = value;
|
|
474
|
+
const installAudit = parseInstallAuditConfig(obj.installAudit);
|
|
475
|
+
if (!installAudit)
|
|
476
|
+
return undefined;
|
|
477
|
+
return { installAudit };
|
|
478
|
+
}
|
|
479
|
+
function parseInstallAuditConfig(value) {
|
|
480
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
481
|
+
return undefined;
|
|
482
|
+
const obj = value;
|
|
483
|
+
const config = {};
|
|
484
|
+
if (typeof obj.enabled === "boolean")
|
|
485
|
+
config.enabled = obj.enabled;
|
|
486
|
+
if (typeof obj.blockOnCritical === "boolean")
|
|
487
|
+
config.blockOnCritical = obj.blockOnCritical;
|
|
488
|
+
if (typeof obj.blockUnlistedRegistries === "boolean")
|
|
489
|
+
config.blockUnlistedRegistries = obj.blockUnlistedRegistries;
|
|
490
|
+
const rawAllowlist = filterNonEmptyStrings(obj.registryAllowlist) ?? filterNonEmptyStrings(obj.registryWhitelist);
|
|
491
|
+
if (rawAllowlist) {
|
|
492
|
+
config.registryAllowlist = rawAllowlist;
|
|
493
|
+
}
|
|
494
|
+
return Object.keys(config).length > 0 ? config : undefined;
|
|
495
|
+
}
|
|
443
496
|
function parseStashConfigEntry(value) {
|
|
444
497
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
445
498
|
return undefined;
|
|
@@ -485,3 +538,122 @@ function parseRegistryConfigEntry(value) {
|
|
|
485
538
|
}
|
|
486
539
|
return entry;
|
|
487
540
|
}
|
|
541
|
+
function mergeSecurityConfig(base, override) {
|
|
542
|
+
if (!base && !override)
|
|
543
|
+
return undefined;
|
|
544
|
+
const installAudit = mergeInstallAuditConfig(base?.installAudit, override?.installAudit);
|
|
545
|
+
return installAudit ? { installAudit } : undefined;
|
|
546
|
+
}
|
|
547
|
+
function mergeInstallAuditConfig(base, override) {
|
|
548
|
+
if (!base && !override)
|
|
549
|
+
return undefined;
|
|
550
|
+
const merged = {
|
|
551
|
+
...(base ?? {}),
|
|
552
|
+
...(override ?? {}),
|
|
553
|
+
};
|
|
554
|
+
return Object.values(merged).some((value) => value !== undefined) ? merged : undefined;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Merge a normalized config layer into an accumulated config.
|
|
558
|
+
*
|
|
559
|
+
* Scalar fields follow normal override semantics. Known nested objects are
|
|
560
|
+
* deep-merged so project config files can override individual fields without
|
|
561
|
+
* clobbering sibling settings. `stashes` are additive by default, but a later
|
|
562
|
+
* layer can set `disableGlobalStashes: true` to drop inherited stashes first.
|
|
563
|
+
*/
|
|
564
|
+
function mergeLoadedConfig(base, override) {
|
|
565
|
+
if (!override)
|
|
566
|
+
return { ...base };
|
|
567
|
+
const merged = {
|
|
568
|
+
...base,
|
|
569
|
+
...override,
|
|
570
|
+
};
|
|
571
|
+
if (base.output && override.output) {
|
|
572
|
+
merged.output = { ...base.output, ...override.output };
|
|
573
|
+
}
|
|
574
|
+
if (base.embedding && override.embedding) {
|
|
575
|
+
merged.embedding = { ...base.embedding, ...override.embedding };
|
|
576
|
+
}
|
|
577
|
+
if (base.llm && override.llm) {
|
|
578
|
+
merged.llm = { ...base.llm, ...override.llm };
|
|
579
|
+
}
|
|
580
|
+
if (base.security && override.security) {
|
|
581
|
+
merged.security = mergeSecurityConfig(base.security, override.security);
|
|
582
|
+
}
|
|
583
|
+
if (override.disableGlobalStashes) {
|
|
584
|
+
merged.stashes = [...(override.stashes ?? [])];
|
|
585
|
+
}
|
|
586
|
+
else if (override.stashes) {
|
|
587
|
+
merged.stashes = [...(base.stashes ?? []), ...override.stashes];
|
|
588
|
+
}
|
|
589
|
+
return merged;
|
|
590
|
+
}
|
|
591
|
+
function applyRuntimeEnvApiKeys(config) {
|
|
592
|
+
const next = { ...config };
|
|
593
|
+
if (next.embedding && !next.embedding.apiKey) {
|
|
594
|
+
const envKey = process.env.AKM_EMBED_API_KEY?.trim();
|
|
595
|
+
if (envKey)
|
|
596
|
+
next.embedding = { ...next.embedding, apiKey: envKey };
|
|
597
|
+
}
|
|
598
|
+
if (next.llm && !next.llm.apiKey) {
|
|
599
|
+
const envKey = process.env.AKM_LLM_API_KEY?.trim();
|
|
600
|
+
if (envKey)
|
|
601
|
+
next.llm = { ...next.llm, apiKey: envKey };
|
|
602
|
+
}
|
|
603
|
+
return next;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Return config file paths in merge order: user config first, then project
|
|
607
|
+
* config files from the outermost parent directory down to the current working
|
|
608
|
+
* directory. Later entries have higher precedence when merged.
|
|
609
|
+
*/
|
|
610
|
+
function getEffectiveConfigPaths() {
|
|
611
|
+
const configPath = getConfigPath();
|
|
612
|
+
const paths = [];
|
|
613
|
+
if (isFile(configPath)) {
|
|
614
|
+
paths.push(configPath);
|
|
615
|
+
}
|
|
616
|
+
return [...paths, ...discoverProjectConfigPaths(process.cwd())];
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Walk from `startDir` up to the filesystem root and collect `.akm/config.json`
|
|
620
|
+
* files. Paths are returned from outermost parent to innermost directory so
|
|
621
|
+
* nearer project directories override broader project settings.
|
|
622
|
+
*/
|
|
623
|
+
function discoverProjectConfigPaths(startDir) {
|
|
624
|
+
const paths = [];
|
|
625
|
+
let currentDir = path.resolve(startDir);
|
|
626
|
+
while (true) {
|
|
627
|
+
const configPath = path.join(currentDir, PROJECT_CONFIG_RELATIVE_PATH);
|
|
628
|
+
if (isFile(configPath)) {
|
|
629
|
+
paths.unshift(configPath);
|
|
630
|
+
}
|
|
631
|
+
const parentDir = path.dirname(currentDir);
|
|
632
|
+
if (parentDir === currentDir) {
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
currentDir = parentDir;
|
|
636
|
+
}
|
|
637
|
+
return paths;
|
|
638
|
+
}
|
|
639
|
+
function getConfigSignature(configPaths) {
|
|
640
|
+
if (configPaths.length === 0)
|
|
641
|
+
return "defaults";
|
|
642
|
+
return configPaths.map((configPath) => `${configPath}:${getFileSignatureToken(configPath)}`).join("|");
|
|
643
|
+
}
|
|
644
|
+
function isFile(filePath) {
|
|
645
|
+
try {
|
|
646
|
+
return fs.statSync(filePath).isFile();
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function getFileSignatureToken(filePath) {
|
|
653
|
+
try {
|
|
654
|
+
return String(fs.statSync(filePath).mtimeMs);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return "missing";
|
|
658
|
+
}
|
|
659
|
+
}
|
package/dist/embedder.js
CHANGED
|
@@ -102,6 +102,28 @@ function l2Normalize(vec) {
|
|
|
102
102
|
return vec.map((v) => v / norm);
|
|
103
103
|
}
|
|
104
104
|
// ── OpenAI-compatible remote embedder ───────────────────────────────────────
|
|
105
|
+
function normalizeEmbeddingEndpoint(endpoint) {
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = new URL(endpoint);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return endpoint;
|
|
112
|
+
}
|
|
113
|
+
const normalizedPath = parsed.pathname.replace(/\/+$/, "");
|
|
114
|
+
if (normalizedPath.endsWith("/embeddings")) {
|
|
115
|
+
return parsed.toString();
|
|
116
|
+
}
|
|
117
|
+
parsed.pathname = normalizedPath ? `${normalizedPath}/embeddings` : "/embeddings";
|
|
118
|
+
return parsed.toString();
|
|
119
|
+
}
|
|
120
|
+
function embeddingEndpointPathHint(endpoint) {
|
|
121
|
+
const normalizedEndpoint = normalizeEmbeddingEndpoint(endpoint);
|
|
122
|
+
if (normalizedEndpoint !== endpoint) {
|
|
123
|
+
return ` Check that your endpoint includes the full embeddings path (for example "${normalizedEndpoint}", not just "${endpoint}").`;
|
|
124
|
+
}
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
105
127
|
async function embedRemote(text, config) {
|
|
106
128
|
const headers = { "Content-Type": "application/json" };
|
|
107
129
|
if (config.apiKey) {
|
|
@@ -114,7 +136,7 @@ async function embedRemote(text, config) {
|
|
|
114
136
|
if (config.dimension) {
|
|
115
137
|
body.dimensions = config.dimension;
|
|
116
138
|
}
|
|
117
|
-
const response = await fetchWithTimeout(config.endpoint, {
|
|
139
|
+
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(config.endpoint), {
|
|
118
140
|
method: "POST",
|
|
119
141
|
headers,
|
|
120
142
|
body: JSON.stringify(body),
|
|
@@ -125,7 +147,7 @@ async function embedRemote(text, config) {
|
|
|
125
147
|
}
|
|
126
148
|
const json = (await response.json());
|
|
127
149
|
if (!json.data?.[0]?.embedding) {
|
|
128
|
-
throw new Error(
|
|
150
|
+
throw new Error(`Unexpected embedding response format: missing data[0].embedding.${embeddingEndpointPathHint(config.endpoint)}`);
|
|
129
151
|
}
|
|
130
152
|
return l2Normalize(json.data[0].embedding);
|
|
131
153
|
}
|
|
@@ -228,7 +250,7 @@ async function embedRemoteBatch(texts, config) {
|
|
|
228
250
|
if (config.dimension) {
|
|
229
251
|
body.dimensions = config.dimension;
|
|
230
252
|
}
|
|
231
|
-
const response = await fetchWithTimeout(config.endpoint, {
|
|
253
|
+
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(config.endpoint), {
|
|
232
254
|
method: "POST",
|
|
233
255
|
headers,
|
|
234
256
|
body: JSON.stringify(body),
|
|
@@ -239,7 +261,7 @@ async function embedRemoteBatch(texts, config) {
|
|
|
239
261
|
}
|
|
240
262
|
const json = (await response.json());
|
|
241
263
|
if (!json.data || json.data.length !== batch.length) {
|
|
242
|
-
throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}`);
|
|
264
|
+
throw new Error(`Unexpected embedding batch response: expected ${batch.length} embeddings, got ${json.data?.length ?? 0}.${embeddingEndpointPathHint(config.endpoint)}`);
|
|
243
265
|
}
|
|
244
266
|
// Sort by index to guarantee correct order (OpenAI API doesn't guarantee order)
|
|
245
267
|
const sorted = [...json.data].sort((a, b) => a.index - b.index);
|
package/dist/init.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { TYPE_DIRS } from "./asset-spec";
|
|
10
|
-
import { getConfigPath,
|
|
10
|
+
import { getConfigPath, loadUserConfig, saveConfig } from "./config";
|
|
11
11
|
import { getBinDir, getDefaultStashDir } from "./paths";
|
|
12
12
|
import { ensureRg } from "./ripgrep-install";
|
|
13
13
|
export async function akmInit(options) {
|
|
@@ -25,7 +25,7 @@ export async function akmInit(options) {
|
|
|
25
25
|
}
|
|
26
26
|
// Persist stashDir in config.json
|
|
27
27
|
const configPath = getConfigPath();
|
|
28
|
-
const existing =
|
|
28
|
+
const existing = loadUserConfig();
|
|
29
29
|
if (!existing.stashDir || existing.stashDir !== stashDir) {
|
|
30
30
|
saveConfig({ ...existing, stashDir });
|
|
31
31
|
}
|