akm-cli 0.7.5 → 0.8.0-rc2
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/.github/CHANGELOG.md +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +853 -479
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +285 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +8 -26
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +378 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +194 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
package/dist/core/config.js
CHANGED
|
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseAgentConfig } from "../integrations/agent/config";
|
|
5
|
-
import { filterNonEmptyStrings } from "./common";
|
|
5
|
+
import { asNonEmptyString, filterNonEmptyStrings, writeFileAtomic } from "./common";
|
|
6
6
|
import { ConfigError } from "./errors";
|
|
7
|
-
import {
|
|
7
|
+
import { getCacheDir, getConfigPath } from "./paths";
|
|
8
8
|
import { warn } from "./warn";
|
|
9
9
|
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
10
10
|
export const DEFAULT_CONFIG = {
|
|
@@ -19,19 +19,53 @@ export const DEFAULT_CONFIG = {
|
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Returns `value` if it is a finite positive integer; otherwise `undefined`.
|
|
25
|
+
* Used to validate numeric config fields like `dimension`, `contextLength`,
|
|
26
|
+
* `timeoutMs`, `maxTokens`, and `ollamaOptions.num_ctx`.
|
|
27
|
+
*/
|
|
28
|
+
function parsePositiveInteger(_fieldPath, value) {
|
|
29
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function parseNonNegativeNumber(value) {
|
|
35
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns `value` if it is a string present in `allowed`; otherwise `undefined`.
|
|
41
|
+
*/
|
|
42
|
+
function isOneOf(value, allowed) {
|
|
43
|
+
return typeof value === "string" && allowed.includes(value);
|
|
24
44
|
}
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Validates that `url` starts with `http://` or `https://`. Returns `url` on
|
|
47
|
+
* success and warns+returns `undefined` on failure. `fieldName` is used only
|
|
48
|
+
* in the warning message.
|
|
49
|
+
*/
|
|
50
|
+
function isValidHttpUrl(url, fieldName) {
|
|
51
|
+
if (typeof url !== "string" || !url)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
54
|
+
warn(`[akm] Ignoring ${fieldName}: endpoint must start with http:// or https://, got "${url}"`);
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
function clearAllCaches() {
|
|
60
|
+
cachedConfig = undefined;
|
|
61
|
+
cachedUserConfig = undefined;
|
|
27
62
|
}
|
|
28
63
|
// ── Load / Save / Update ────────────────────────────────────────────────────
|
|
29
64
|
const PROJECT_CONFIG_RELATIVE_PATH = path.join(".akm", "config.json");
|
|
30
65
|
let cachedConfig;
|
|
31
66
|
let cachedUserConfig;
|
|
32
67
|
export function resetConfigCache() {
|
|
33
|
-
|
|
34
|
-
cachedUserConfig = undefined;
|
|
68
|
+
clearAllCaches();
|
|
35
69
|
}
|
|
36
70
|
function hashString(text) {
|
|
37
71
|
// Simple, fast non-cryptographic hash (FNV-1a 32-bit) — sufficient to detect
|
|
@@ -85,6 +119,17 @@ export function loadUserConfig() {
|
|
|
85
119
|
};
|
|
86
120
|
return finalConfig;
|
|
87
121
|
}
|
|
122
|
+
export function getSources(config) {
|
|
123
|
+
return config.sources ?? [];
|
|
124
|
+
}
|
|
125
|
+
export function getEffectiveRegistries(config) {
|
|
126
|
+
return config.registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
127
|
+
}
|
|
128
|
+
export function requireLlmConfig(config) {
|
|
129
|
+
if (!config.llm)
|
|
130
|
+
throw new ConfigError("LLM is not configured. Run `akm config set llm` to configure one.", "LLM_NOT_CONFIGURED");
|
|
131
|
+
return config.llm;
|
|
132
|
+
}
|
|
88
133
|
export function loadConfig() {
|
|
89
134
|
const configPaths = getEffectiveConfigPaths();
|
|
90
135
|
const signature = getConfigSignature(configPaths);
|
|
@@ -103,8 +148,7 @@ export function loadConfig() {
|
|
|
103
148
|
return finalConfig;
|
|
104
149
|
}
|
|
105
150
|
export function saveConfig(config) {
|
|
106
|
-
|
|
107
|
-
cachedUserConfig = undefined;
|
|
151
|
+
clearAllCaches();
|
|
108
152
|
const configPath = getConfigPath();
|
|
109
153
|
const dir = path.dirname(configPath);
|
|
110
154
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -143,31 +187,7 @@ function sanitizeConfigForWrite(config) {
|
|
|
143
187
|
}
|
|
144
188
|
export function updateConfig(partial) {
|
|
145
189
|
const current = loadUserConfig();
|
|
146
|
-
|
|
147
|
-
const merged = { ...current, ...partial };
|
|
148
|
-
// Deep-merge output — partial update should not wipe sibling keys
|
|
149
|
-
if (current.output && partial.output && partial.output !== current.output) {
|
|
150
|
-
merged.output = { ...current.output, ...partial.output };
|
|
151
|
-
}
|
|
152
|
-
// Deep-merge embedding — only when both sides are objects and partial does not intend to clear
|
|
153
|
-
if (current.embedding && partial.embedding && partial.embedding !== current.embedding) {
|
|
154
|
-
merged.embedding = { ...current.embedding, ...partial.embedding };
|
|
155
|
-
}
|
|
156
|
-
// Deep-merge llm — same pattern
|
|
157
|
-
if (current.llm && partial.llm && partial.llm !== current.llm) {
|
|
158
|
-
merged.llm = { ...current.llm, ...partial.llm };
|
|
159
|
-
}
|
|
160
|
-
// Deep-merge index per-pass entries so partial updates don't wipe siblings.
|
|
161
|
-
if (current.index && partial.index && partial.index !== current.index) {
|
|
162
|
-
const mergedIndex = { ...current.index };
|
|
163
|
-
for (const [passName, passOverride] of Object.entries(partial.index)) {
|
|
164
|
-
mergedIndex[passName] = { ...(mergedIndex[passName] ?? {}), ...passOverride };
|
|
165
|
-
}
|
|
166
|
-
merged.index = mergedIndex;
|
|
167
|
-
}
|
|
168
|
-
if (current.security && partial.security && partial.security !== current.security) {
|
|
169
|
-
merged.security = mergeSecurityConfig(current.security, partial.security);
|
|
170
|
-
}
|
|
190
|
+
const merged = mergeLoadedConfig(current, partial);
|
|
171
191
|
saveConfig(merged);
|
|
172
192
|
return merged;
|
|
173
193
|
}
|
|
@@ -179,11 +199,8 @@ export function updateConfig(partial) {
|
|
|
179
199
|
* combining multiple config sources so project config files only override what
|
|
180
200
|
* they set.
|
|
181
201
|
*/
|
|
182
|
-
function
|
|
202
|
+
function parseConfigLayer(raw) {
|
|
183
203
|
const config = {};
|
|
184
|
-
if (Array.isArray(raw.stashes)) {
|
|
185
|
-
throw new ConfigError("The legacy `stashes[]` config key is no longer supported; rename it to `sources[]`.", "INVALID_CONFIG_FILE", `Edit ${_getConfigPath()} and replace \`stashes\` with \`sources\`.`);
|
|
186
|
-
}
|
|
187
204
|
if (typeof raw.stashDir === "string" && raw.stashDir.trim()) {
|
|
188
205
|
config.stashDir = raw.stashDir.trim();
|
|
189
206
|
}
|
|
@@ -191,7 +208,7 @@ function pickKnownKeys(raw) {
|
|
|
191
208
|
if (typeof raw.semanticSearchMode === "boolean") {
|
|
192
209
|
config.semanticSearchMode = raw.semanticSearchMode ? "auto" : "off";
|
|
193
210
|
}
|
|
194
|
-
else if (raw.semanticSearchMode
|
|
211
|
+
else if (isOneOf(raw.semanticSearchMode, ["off", "auto"])) {
|
|
195
212
|
config.semanticSearchMode = raw.semanticSearchMode;
|
|
196
213
|
}
|
|
197
214
|
const embedding = parseEmbeddingConfig(raw.embedding);
|
|
@@ -209,9 +226,12 @@ function pickKnownKeys(raw) {
|
|
|
209
226
|
const registries = parseRegistriesConfig(raw.registries);
|
|
210
227
|
if (registries)
|
|
211
228
|
config.registries = registries;
|
|
212
|
-
if (raw.stashInheritance
|
|
229
|
+
if (isOneOf(raw.stashInheritance, ["replace", "merge"])) {
|
|
213
230
|
config.stashInheritance = raw.stashInheritance;
|
|
214
231
|
}
|
|
232
|
+
if (Array.isArray(raw.stashes)) {
|
|
233
|
+
throw new ConfigError("The legacy `stashes[]` config key is no longer supported. Rename it to `sources`.", "INVALID_CONFIG_FILE");
|
|
234
|
+
}
|
|
215
235
|
const sources = parseSourcesConfig(raw.sources);
|
|
216
236
|
if (sources) {
|
|
217
237
|
config.sources = sources;
|
|
@@ -236,35 +256,102 @@ function pickKnownKeys(raw) {
|
|
|
236
256
|
if (typeof raw.search === "object" && raw.search !== null && !Array.isArray(raw.search)) {
|
|
237
257
|
const searchRaw = raw.search;
|
|
238
258
|
const searchConfig = {};
|
|
259
|
+
for (const key of Object.keys(searchRaw)) {
|
|
260
|
+
if (key !== "minScore" && key !== "graphBoost") {
|
|
261
|
+
warn(`[akm] Ignoring unknown search key "${key}".`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
239
264
|
if (typeof searchRaw.minScore === "number" && Number.isFinite(searchRaw.minScore) && searchRaw.minScore >= 0) {
|
|
240
265
|
searchConfig.minScore = searchRaw.minScore;
|
|
241
266
|
}
|
|
267
|
+
if (typeof searchRaw.graphBoost === "object" &&
|
|
268
|
+
searchRaw.graphBoost !== null &&
|
|
269
|
+
!Array.isArray(searchRaw.graphBoost)) {
|
|
270
|
+
const graphBoostRaw = searchRaw.graphBoost;
|
|
271
|
+
const graphBoostConfig = {};
|
|
272
|
+
for (const key of Object.keys(graphBoostRaw)) {
|
|
273
|
+
if (key !== "directBoostPerEntity" &&
|
|
274
|
+
key !== "directBoostCap" &&
|
|
275
|
+
key !== "hopBoostPerEntity" &&
|
|
276
|
+
key !== "hopBoostCap" &&
|
|
277
|
+
key !== "maxHops" &&
|
|
278
|
+
key !== "confidenceMode" &&
|
|
279
|
+
key !== "confidenceWeight") {
|
|
280
|
+
warn(`[akm] Ignoring unknown search.graphBoost key "${key}".`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const directBoostPerEntity = parseNonNegativeNumber(graphBoostRaw.directBoostPerEntity);
|
|
284
|
+
if (directBoostPerEntity !== undefined)
|
|
285
|
+
graphBoostConfig.directBoostPerEntity = directBoostPerEntity;
|
|
286
|
+
const directBoostCap = parseNonNegativeNumber(graphBoostRaw.directBoostCap);
|
|
287
|
+
if (directBoostCap !== undefined)
|
|
288
|
+
graphBoostConfig.directBoostCap = directBoostCap;
|
|
289
|
+
const hopBoostPerEntity = parseNonNegativeNumber(graphBoostRaw.hopBoostPerEntity);
|
|
290
|
+
if (hopBoostPerEntity !== undefined)
|
|
291
|
+
graphBoostConfig.hopBoostPerEntity = hopBoostPerEntity;
|
|
292
|
+
const hopBoostCap = parseNonNegativeNumber(graphBoostRaw.hopBoostCap);
|
|
293
|
+
if (hopBoostCap !== undefined)
|
|
294
|
+
graphBoostConfig.hopBoostCap = hopBoostCap;
|
|
295
|
+
const maxHops = parsePositiveInteger("search.graphBoost.maxHops", graphBoostRaw.maxHops);
|
|
296
|
+
if (maxHops !== undefined)
|
|
297
|
+
graphBoostConfig.maxHops = Math.min(maxHops, 3);
|
|
298
|
+
if (isOneOf(graphBoostRaw.confidenceMode, ["off", "blend", "multiply"])) {
|
|
299
|
+
graphBoostConfig.confidenceMode = graphBoostRaw.confidenceMode;
|
|
300
|
+
}
|
|
301
|
+
const confidenceWeight = parseNonNegativeNumber(graphBoostRaw.confidenceWeight);
|
|
302
|
+
if (confidenceWeight !== undefined)
|
|
303
|
+
graphBoostConfig.confidenceWeight = Math.min(confidenceWeight, 1);
|
|
304
|
+
if (Object.keys(graphBoostConfig).length > 0)
|
|
305
|
+
searchConfig.graphBoost = graphBoostConfig;
|
|
306
|
+
}
|
|
242
307
|
if (Object.keys(searchConfig).length > 0)
|
|
243
308
|
config.search = searchConfig;
|
|
244
309
|
}
|
|
310
|
+
if (typeof raw.feedback === "object" && raw.feedback !== null && !Array.isArray(raw.feedback)) {
|
|
311
|
+
const feedbackRaw = raw.feedback;
|
|
312
|
+
const feedbackConfig = {};
|
|
313
|
+
if (typeof feedbackRaw.requireReason === "boolean") {
|
|
314
|
+
feedbackConfig.requireReason = feedbackRaw.requireReason;
|
|
315
|
+
}
|
|
316
|
+
if (Object.keys(feedbackConfig).length > 0)
|
|
317
|
+
config.feedback = feedbackConfig;
|
|
318
|
+
}
|
|
319
|
+
if (typeof raw.archiveRetentionDays === "number" &&
|
|
320
|
+
Number.isFinite(raw.archiveRetentionDays) &&
|
|
321
|
+
raw.archiveRetentionDays >= 0) {
|
|
322
|
+
config.archiveRetentionDays = raw.archiveRetentionDays;
|
|
323
|
+
}
|
|
245
324
|
return config;
|
|
246
325
|
}
|
|
247
|
-
function
|
|
248
|
-
const raw = readConfigObject(configPath);
|
|
249
|
-
const expanded = raw ? expandEnvVars(raw) : undefined;
|
|
250
|
-
return expanded ? pickKnownKeys(expanded) : undefined;
|
|
251
|
-
}
|
|
252
|
-
function readNormalizedConfigFromText(text) {
|
|
326
|
+
function parseConfigText(text) {
|
|
253
327
|
const raw = parseConfigObjectFromText(text);
|
|
254
328
|
if (!raw)
|
|
255
329
|
return undefined;
|
|
256
330
|
const expanded = expandEnvVars(raw);
|
|
257
|
-
return
|
|
331
|
+
return parseConfigLayer(expanded);
|
|
332
|
+
}
|
|
333
|
+
function readNormalizedConfig(configPath) {
|
|
334
|
+
let text;
|
|
335
|
+
try {
|
|
336
|
+
text = fs.readFileSync(configPath, "utf8");
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return parseConfigText(text);
|
|
342
|
+
}
|
|
343
|
+
function readNormalizedConfigFromText(text) {
|
|
344
|
+
return parseConfigText(text);
|
|
258
345
|
}
|
|
259
346
|
function parseOutputConfig(value) {
|
|
260
347
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
261
348
|
return undefined;
|
|
262
349
|
const obj = value;
|
|
263
350
|
const output = {};
|
|
264
|
-
if (obj.format
|
|
351
|
+
if (isOneOf(obj.format, ["json", "yaml", "text"])) {
|
|
265
352
|
output.format = obj.format;
|
|
266
353
|
}
|
|
267
|
-
if (obj.detail
|
|
354
|
+
if (isOneOf(obj.detail, ["brief", "normal", "full"])) {
|
|
268
355
|
output.detail = obj.detail;
|
|
269
356
|
}
|
|
270
357
|
return Object.keys(output).length > 0 ? output : undefined;
|
|
@@ -313,15 +400,6 @@ function expandEnvVars(value, fieldName) {
|
|
|
313
400
|
}
|
|
314
401
|
return value;
|
|
315
402
|
}
|
|
316
|
-
function readConfigObject(configPath) {
|
|
317
|
-
try {
|
|
318
|
-
const text = fs.readFileSync(configPath, "utf8");
|
|
319
|
-
return parseConfigObjectFromText(text);
|
|
320
|
-
}
|
|
321
|
-
catch {
|
|
322
|
-
return undefined;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
403
|
function parseConfigObjectFromText(text) {
|
|
326
404
|
try {
|
|
327
405
|
const raw = JSON.parse(stripJsonComments(text));
|
|
@@ -334,20 +412,7 @@ function parseConfigObjectFromText(text) {
|
|
|
334
412
|
}
|
|
335
413
|
}
|
|
336
414
|
function writeConfigObject(configPath, config) {
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
fs.writeFileSync(tmpPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
340
|
-
fs.renameSync(tmpPath, configPath);
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
try {
|
|
344
|
-
fs.unlinkSync(tmpPath);
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
/* ignore cleanup failure */
|
|
348
|
-
}
|
|
349
|
-
throw err;
|
|
350
|
-
}
|
|
415
|
+
writeFileAtomic(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
351
416
|
}
|
|
352
417
|
/**
|
|
353
418
|
* Strip JavaScript-style comments from a JSON string (JSONC support).
|
|
@@ -413,8 +478,7 @@ function parseEmbeddingConfig(value) {
|
|
|
413
478
|
}
|
|
414
479
|
return undefined;
|
|
415
480
|
}
|
|
416
|
-
if (!obj.endpoint
|
|
417
|
-
warn(`[akm] Ignoring embedding config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
|
|
481
|
+
if (!isValidHttpUrl(obj.endpoint, "embedding config")) {
|
|
418
482
|
// Still return localModel-only config if localModel was set
|
|
419
483
|
if (localModel) {
|
|
420
484
|
return { endpoint: "", model: "", localModel };
|
|
@@ -437,13 +501,10 @@ function parseEmbeddingConfig(value) {
|
|
|
437
501
|
result.provider = obj.provider;
|
|
438
502
|
}
|
|
439
503
|
if ("dimension" in obj) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
!Number.isInteger(obj.dimension) ||
|
|
443
|
-
obj.dimension <= 0) {
|
|
504
|
+
const dim = parsePositiveInteger("embedding.dimension", obj.dimension);
|
|
505
|
+
if (dim === undefined)
|
|
444
506
|
return undefined;
|
|
445
|
-
|
|
446
|
-
result.dimension = obj.dimension;
|
|
507
|
+
result.dimension = dim;
|
|
447
508
|
}
|
|
448
509
|
if (typeof obj.apiKey === "string" && obj.apiKey) {
|
|
449
510
|
result.apiKey = obj.apiKey;
|
|
@@ -452,22 +513,17 @@ function parseEmbeddingConfig(value) {
|
|
|
452
513
|
result.localModel = localModel;
|
|
453
514
|
}
|
|
454
515
|
if ("contextLength" in obj) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
!Number.isInteger(obj.contextLength) ||
|
|
458
|
-
obj.contextLength <= 0) {
|
|
516
|
+
const ctx = parsePositiveInteger("embedding.contextLength", obj.contextLength);
|
|
517
|
+
if (ctx === undefined)
|
|
459
518
|
return undefined;
|
|
460
|
-
|
|
461
|
-
result.contextLength = obj.contextLength;
|
|
519
|
+
result.contextLength = ctx;
|
|
462
520
|
}
|
|
463
521
|
if (typeof obj.ollamaOptions === "object" && obj.ollamaOptions !== null && !Array.isArray(obj.ollamaOptions)) {
|
|
464
522
|
const opts = obj.ollamaOptions;
|
|
465
523
|
const parsed = {};
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
opts.num_ctx > 0) {
|
|
470
|
-
parsed.num_ctx = opts.num_ctx;
|
|
524
|
+
const numCtx = parsePositiveInteger("embedding.ollamaOptions.num_ctx", opts.num_ctx);
|
|
525
|
+
if (numCtx !== undefined) {
|
|
526
|
+
parsed.num_ctx = numCtx;
|
|
471
527
|
}
|
|
472
528
|
if (Object.keys(parsed).length > 0) {
|
|
473
529
|
result.ollamaOptions = parsed;
|
|
@@ -481,8 +537,7 @@ function parseLlmConfig(value) {
|
|
|
481
537
|
const obj = value;
|
|
482
538
|
if (typeof obj.endpoint !== "string" || !obj.endpoint)
|
|
483
539
|
return undefined;
|
|
484
|
-
if (!obj.endpoint
|
|
485
|
-
warn(`[akm] Ignoring llm config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
|
|
540
|
+
if (!isValidHttpUrl(obj.endpoint, "llm config")) {
|
|
486
541
|
return undefined;
|
|
487
542
|
}
|
|
488
543
|
if (!obj.endpoint.endsWith("/chat/completions")) {
|
|
@@ -501,22 +556,27 @@ function parseLlmConfig(value) {
|
|
|
501
556
|
result.temperature = obj.temperature;
|
|
502
557
|
}
|
|
503
558
|
if ("timeoutMs" in obj) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
!Number.isInteger(obj.timeoutMs) ||
|
|
507
|
-
obj.timeoutMs <= 0) {
|
|
559
|
+
const t = parsePositiveInteger("llm.timeoutMs", obj.timeoutMs);
|
|
560
|
+
if (t === undefined)
|
|
508
561
|
return undefined;
|
|
509
|
-
|
|
510
|
-
|
|
562
|
+
result.timeoutMs = t;
|
|
563
|
+
}
|
|
564
|
+
if ("concurrency" in obj) {
|
|
565
|
+
const c = parsePositiveInteger("llm.concurrency", obj.concurrency);
|
|
566
|
+
if (c === undefined)
|
|
567
|
+
return undefined;
|
|
568
|
+
result.concurrency = c;
|
|
511
569
|
}
|
|
512
570
|
if ("maxTokens" in obj) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
!Number.isInteger(obj.maxTokens) ||
|
|
516
|
-
obj.maxTokens <= 0) {
|
|
571
|
+
const m = parsePositiveInteger("llm.maxTokens", obj.maxTokens);
|
|
572
|
+
if (m === undefined)
|
|
517
573
|
return undefined;
|
|
518
|
-
|
|
519
|
-
|
|
574
|
+
result.maxTokens = m;
|
|
575
|
+
}
|
|
576
|
+
if ("contextLength" in obj) {
|
|
577
|
+
const ctx = parsePositiveInteger("llm.contextLength", obj.contextLength);
|
|
578
|
+
if (ctx !== undefined)
|
|
579
|
+
result.contextLength = ctx;
|
|
520
580
|
}
|
|
521
581
|
if (typeof obj.apiKey === "string" && obj.apiKey) {
|
|
522
582
|
result.apiKey = obj.apiKey;
|
|
@@ -534,6 +594,9 @@ function parseLlmConfig(value) {
|
|
|
534
594
|
if (Object.keys(features).length > 0)
|
|
535
595
|
result.features = features;
|
|
536
596
|
}
|
|
597
|
+
if (typeof obj.judgeModel === "string" && obj.judgeModel.trim()) {
|
|
598
|
+
result.judgeModel = obj.judgeModel.trim();
|
|
599
|
+
}
|
|
537
600
|
if (typeof obj.extraParams === "object" && obj.extraParams !== null && !Array.isArray(obj.extraParams)) {
|
|
538
601
|
result.extraParams = obj.extraParams;
|
|
539
602
|
}
|
|
@@ -551,6 +614,9 @@ const LOCKED_LLM_FEATURE_KEYS = new Set([
|
|
|
551
614
|
"feedback_distillation",
|
|
552
615
|
"memory_inference",
|
|
553
616
|
"graph_extraction",
|
|
617
|
+
"memory_consolidation",
|
|
618
|
+
"lesson_quality_gate",
|
|
619
|
+
"metadata_enhance",
|
|
554
620
|
]);
|
|
555
621
|
function parseLlmFeatures(raw) {
|
|
556
622
|
const out = {};
|
|
@@ -563,22 +629,8 @@ function parseLlmFeatures(raw) {
|
|
|
563
629
|
warn(`[akm] Ignoring llm.features.${key}: expected boolean, got ${typeof value}.`);
|
|
564
630
|
continue;
|
|
565
631
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
out.memory_inference = value;
|
|
569
|
-
break;
|
|
570
|
-
case "graph_extraction":
|
|
571
|
-
out.graph_extraction = value;
|
|
572
|
-
break;
|
|
573
|
-
case "curate_rerank":
|
|
574
|
-
out.curate_rerank = value;
|
|
575
|
-
break;
|
|
576
|
-
case "feedback_distillation":
|
|
577
|
-
out.feedback_distillation = value;
|
|
578
|
-
break;
|
|
579
|
-
// No default: LOCKED_LLM_FEATURE_KEYS is the source of truth for which
|
|
580
|
-
// keys are accepted. Adding a new locked key requires an arm here AND a
|
|
581
|
-
// field on LlmFeatureFlags above.
|
|
632
|
+
if (LOCKED_LLM_FEATURE_KEYS.has(key)) {
|
|
633
|
+
out[key] = value;
|
|
582
634
|
}
|
|
583
635
|
}
|
|
584
636
|
return out;
|
|
@@ -599,6 +651,17 @@ const PROVIDER_CONFIG_KEYS = new Set([
|
|
|
599
651
|
"maxTokens",
|
|
600
652
|
"capabilities",
|
|
601
653
|
]);
|
|
654
|
+
const GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED = new Set([
|
|
655
|
+
"memory",
|
|
656
|
+
"knowledge",
|
|
657
|
+
"skill",
|
|
658
|
+
"command",
|
|
659
|
+
"agent",
|
|
660
|
+
"workflow",
|
|
661
|
+
"lesson",
|
|
662
|
+
"task",
|
|
663
|
+
"wiki",
|
|
664
|
+
]);
|
|
602
665
|
/**
|
|
603
666
|
* Parse the `index` config block. Each entry is a pass name → small object
|
|
604
667
|
* `{ llm?: boolean }`. Anything richer (a parallel provider config, unknown
|
|
@@ -624,8 +687,11 @@ function parseIndexConfig(value) {
|
|
|
624
687
|
throw new ConfigError(`Duplicate LLM provider configuration: \`index.${passName}.${key}\` is not allowed. ` +
|
|
625
688
|
"Configure provider/model/endpoint under top-level `llm` only; per-pass entries support `{ llm: false }` opt-out.", "INVALID_CONFIG_FILE", 'Move provider settings to the top-level "llm" block, then set `index.<pass>.llm = false` to opt a single pass out.');
|
|
626
689
|
}
|
|
627
|
-
if (key !== "llm"
|
|
628
|
-
|
|
690
|
+
if (key !== "llm" &&
|
|
691
|
+
key !== "graphExtractionBatchSize" &&
|
|
692
|
+
key !== "graphExtractionIncludeTypes" &&
|
|
693
|
+
key !== "memoryInferenceBatchSize") {
|
|
694
|
+
throw new ConfigError(`Unknown key \`index.${passName}.${key}\`. Per-pass entries support \`llm\` (boolean opt-out), \`graphExtractionBatchSize\`, \`graphExtractionIncludeTypes\`, and \`memoryInferenceBatchSize\`.`, "INVALID_CONFIG_FILE");
|
|
629
695
|
}
|
|
630
696
|
}
|
|
631
697
|
const passConfig = {};
|
|
@@ -636,6 +702,28 @@ function parseIndexConfig(value) {
|
|
|
636
702
|
}
|
|
637
703
|
passConfig.llm = llmFlag;
|
|
638
704
|
}
|
|
705
|
+
if ("graphExtractionBatchSize" in passRaw) {
|
|
706
|
+
const n = parsePositiveInteger(`index.${passName}.graphExtractionBatchSize`, passRaw.graphExtractionBatchSize);
|
|
707
|
+
if (n !== undefined)
|
|
708
|
+
passConfig.graphExtractionBatchSize = n;
|
|
709
|
+
}
|
|
710
|
+
if ("graphExtractionIncludeTypes" in passRaw) {
|
|
711
|
+
const rawTypes = passRaw.graphExtractionIncludeTypes;
|
|
712
|
+
if (!Array.isArray(rawTypes) || !rawTypes.every((t) => typeof t === "string" && t.trim().length > 0)) {
|
|
713
|
+
throw new ConfigError(`Invalid \`index.${passName}.graphExtractionIncludeTypes\`: expected a non-empty string array of asset types.`, "INVALID_CONFIG_FILE");
|
|
714
|
+
}
|
|
715
|
+
const normalized = rawTypes.map((t) => t.trim().toLowerCase());
|
|
716
|
+
const invalid = normalized.filter((t) => !GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED.has(t));
|
|
717
|
+
if (invalid.length > 0) {
|
|
718
|
+
throw new ConfigError(`Invalid \`index.${passName}.graphExtractionIncludeTypes\`: unsupported type(s): ${invalid.join(", ")}.`, "INVALID_CONFIG_FILE");
|
|
719
|
+
}
|
|
720
|
+
passConfig.graphExtractionIncludeTypes = normalized;
|
|
721
|
+
}
|
|
722
|
+
if ("memoryInferenceBatchSize" in passRaw) {
|
|
723
|
+
const n = parsePositiveInteger(`index.${passName}.memoryInferenceBatchSize`, passRaw.memoryInferenceBatchSize);
|
|
724
|
+
if (n !== undefined)
|
|
725
|
+
passConfig.memoryInferenceBatchSize = n;
|
|
726
|
+
}
|
|
639
727
|
out[passName] = passConfig;
|
|
640
728
|
}
|
|
641
729
|
return out;
|
|
@@ -686,9 +774,6 @@ function parseInstalledStashEntry(value) {
|
|
|
686
774
|
entry.wikiName = wikiName;
|
|
687
775
|
return entry;
|
|
688
776
|
}
|
|
689
|
-
function asNonEmptyString(value) {
|
|
690
|
-
return typeof value === "string" && value ? value : undefined;
|
|
691
|
-
}
|
|
692
777
|
/**
|
|
693
778
|
* Validate a legacy lockfile/installed-entry source string.
|
|
694
779
|
*
|
|
@@ -741,6 +826,9 @@ function parseInstallAuditConfig(value) {
|
|
|
741
826
|
if (typeof obj.blockUnlistedRegistries === "boolean")
|
|
742
827
|
config.blockUnlistedRegistries = obj.blockUnlistedRegistries;
|
|
743
828
|
const rawAllowlist = filterNonEmptyStrings(obj.registryAllowlist) ?? filterNonEmptyStrings(obj.registryWhitelist);
|
|
829
|
+
if (!obj.registryAllowlist && obj.registryWhitelist) {
|
|
830
|
+
warn("[akm] config: `registryWhitelist` is deprecated; rename it to `registryAllowlist`");
|
|
831
|
+
}
|
|
744
832
|
if (rawAllowlist) {
|
|
745
833
|
config.registryAllowlist = rawAllowlist;
|
|
746
834
|
}
|
|
@@ -786,7 +874,7 @@ function parseSourceConfigEntry(value) {
|
|
|
786
874
|
return undefined;
|
|
787
875
|
if (type === "openviking") {
|
|
788
876
|
const name = asNonEmptyString(obj.name) ?? "unnamed";
|
|
789
|
-
throw new ConfigError(`openviking is not supported in akm v1. API-backed sources will return as a\nseparate QuerySource tier post-v1. Remove the source named "${name}" from your config file\nor downgrade to 0.6.x. See docs/migration/v1.md.`, "INVALID_CONFIG_FILE", `Run \`akm remove ${name}\` then re-run, or edit your config file directly at ${
|
|
877
|
+
throw new ConfigError(`openviking is not supported in akm v1. API-backed sources will return as a\nseparate QuerySource tier post-v1. Remove the source named "${name}" from your config file\nor downgrade to 0.6.x. See docs/migration/v1.md.`, "INVALID_CONFIG_FILE", `Run \`akm remove ${name}\` then re-run, or edit your config file directly at ${getConfigPath()} to remove the openviking entry.`);
|
|
790
878
|
}
|
|
791
879
|
const entry = { type };
|
|
792
880
|
const entryPath = asNonEmptyString(obj.path);
|
|
@@ -965,6 +1053,12 @@ function mergeAgentConfig(base, override) {
|
|
|
965
1053
|
}
|
|
966
1054
|
merged.profiles = profiles;
|
|
967
1055
|
}
|
|
1056
|
+
// Shallow merge per-key: later layer wins per process name (same as profiles).
|
|
1057
|
+
const baseProcesses = base.processes;
|
|
1058
|
+
const overrideProcesses = override.processes;
|
|
1059
|
+
if (baseProcesses || overrideProcesses) {
|
|
1060
|
+
merged.processes = { ...(baseProcesses ?? {}), ...(overrideProcesses ?? {}) };
|
|
1061
|
+
}
|
|
968
1062
|
return merged;
|
|
969
1063
|
}
|
|
970
1064
|
function mergeSecurityConfig(base, override) {
|