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.
Files changed (152) hide show
  1. package/.github/CHANGELOG.md +1 -1
  2. package/dist/cli/parse-args.js +43 -0
  3. package/dist/cli.js +853 -479
  4. package/dist/commands/agent-dispatch.js +102 -0
  5. package/dist/commands/agent-support.js +62 -0
  6. package/dist/commands/config-cli.js +68 -84
  7. package/dist/commands/consolidate.js +823 -0
  8. package/dist/commands/distill-promotion-policy.js +658 -0
  9. package/dist/commands/distill.js +244 -52
  10. package/dist/commands/eval-cases.js +40 -0
  11. package/dist/commands/events.js +2 -23
  12. package/dist/commands/graph.js +222 -0
  13. package/dist/commands/health.js +376 -0
  14. package/dist/commands/help/help-accept.md +9 -0
  15. package/dist/commands/help/help-improve.md +53 -0
  16. package/dist/commands/help/help-proposals.md +15 -0
  17. package/dist/commands/help/help-propose.md +17 -0
  18. package/dist/commands/help/help-reject.md +8 -0
  19. package/dist/commands/history.js +3 -30
  20. package/dist/commands/improve.js +1170 -0
  21. package/dist/commands/info.js +2 -2
  22. package/dist/commands/init.js +2 -2
  23. package/dist/commands/install-audit.js +5 -1
  24. package/dist/commands/installed-stashes.js +118 -138
  25. package/dist/commands/knowledge.js +133 -0
  26. package/dist/commands/lint/agent-linter.js +46 -0
  27. package/dist/commands/lint/base-linter.js +285 -0
  28. package/dist/commands/lint/command-linter.js +46 -0
  29. package/dist/commands/lint/default-linter.js +13 -0
  30. package/dist/commands/lint/index.js +107 -0
  31. package/dist/commands/lint/knowledge-linter.js +13 -0
  32. package/dist/commands/lint/memory-linter.js +58 -0
  33. package/dist/commands/lint/registry.js +33 -0
  34. package/dist/commands/lint/skill-linter.js +42 -0
  35. package/dist/commands/lint/task-linter.js +47 -0
  36. package/dist/commands/lint/types.js +1 -0
  37. package/dist/commands/lint/workflow-linter.js +53 -0
  38. package/dist/commands/lint.js +1 -0
  39. package/dist/commands/proposal.js +8 -7
  40. package/dist/commands/propose.js +78 -28
  41. package/dist/commands/reflect.js +143 -35
  42. package/dist/commands/registry-search.js +2 -2
  43. package/dist/commands/remember.js +54 -0
  44. package/dist/commands/schema-repair.js +130 -0
  45. package/dist/commands/search.js +21 -5
  46. package/dist/commands/show.js +121 -17
  47. package/dist/commands/source-add.js +10 -10
  48. package/dist/commands/source-manage.js +11 -19
  49. package/dist/commands/tasks.js +385 -0
  50. package/dist/commands/url-checker.js +39 -0
  51. package/dist/commands/vault.js +8 -26
  52. package/dist/core/action-contributors.js +25 -0
  53. package/dist/core/asset-ref.js +4 -0
  54. package/dist/core/asset-registry.js +4 -16
  55. package/dist/core/asset-spec.js +10 -0
  56. package/dist/core/common.js +94 -0
  57. package/dist/core/concurrent.js +22 -0
  58. package/dist/core/config.js +222 -128
  59. package/dist/core/events.js +73 -126
  60. package/dist/core/frontmatter.js +3 -1
  61. package/dist/core/markdown.js +17 -0
  62. package/dist/core/memory-improve.js +678 -0
  63. package/dist/core/parse.js +155 -0
  64. package/dist/core/paths.js +101 -3
  65. package/dist/core/proposal-validators.js +61 -0
  66. package/dist/core/proposals.js +49 -38
  67. package/dist/core/state-db.js +775 -0
  68. package/dist/core/time.js +51 -0
  69. package/dist/core/warn.js +59 -1
  70. package/dist/indexer/db-search.js +52 -238
  71. package/dist/indexer/db.js +378 -1
  72. package/dist/indexer/ensure-index.js +61 -0
  73. package/dist/indexer/graph-boost.js +247 -94
  74. package/dist/indexer/graph-db.js +201 -0
  75. package/dist/indexer/graph-dedup.js +99 -0
  76. package/dist/indexer/graph-extraction.js +409 -76
  77. package/dist/indexer/index-context.js +10 -0
  78. package/dist/indexer/indexer.js +442 -290
  79. package/dist/indexer/llm-cache.js +47 -0
  80. package/dist/indexer/match-contributors.js +141 -0
  81. package/dist/indexer/matchers.js +24 -190
  82. package/dist/indexer/memory-inference.js +63 -29
  83. package/dist/indexer/metadata-contributors.js +26 -0
  84. package/dist/indexer/metadata.js +194 -175
  85. package/dist/indexer/path-resolver.js +89 -0
  86. package/dist/indexer/ranking-contributors.js +204 -0
  87. package/dist/indexer/ranking.js +74 -0
  88. package/dist/indexer/search-hit-enrichers.js +22 -0
  89. package/dist/indexer/search-source.js +24 -9
  90. package/dist/indexer/semantic-status.js +2 -16
  91. package/dist/indexer/walker.js +25 -0
  92. package/dist/integrations/agent/config.js +175 -3
  93. package/dist/integrations/agent/index.js +3 -1
  94. package/dist/integrations/agent/pipeline.js +39 -0
  95. package/dist/integrations/agent/profiles.js +67 -5
  96. package/dist/integrations/agent/prompts.js +77 -72
  97. package/dist/integrations/agent/runners.js +31 -0
  98. package/dist/integrations/agent/sdk-runner.js +120 -0
  99. package/dist/integrations/agent/spawn.js +71 -16
  100. package/dist/integrations/lockfile.js +10 -18
  101. package/dist/integrations/session-logs/index.js +65 -0
  102. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  103. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  104. package/dist/integrations/session-logs/types.js +1 -0
  105. package/dist/llm/call-ai.js +74 -0
  106. package/dist/llm/client.js +61 -122
  107. package/dist/llm/feature-gate.js +27 -16
  108. package/dist/llm/graph-extract.js +297 -62
  109. package/dist/llm/memory-infer.js +49 -71
  110. package/dist/llm/metadata-enhance.js +39 -22
  111. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  112. package/dist/output/cli-hints-full.md +277 -0
  113. package/dist/output/cli-hints-short.md +65 -0
  114. package/dist/output/cli-hints.js +2 -318
  115. package/dist/output/renderers.js +190 -123
  116. package/dist/output/shapes.js +33 -0
  117. package/dist/output/text.js +239 -2
  118. package/dist/registry/providers/skills-sh.js +61 -49
  119. package/dist/registry/providers/static-index.js +44 -48
  120. package/dist/setup/setup.js +510 -11
  121. package/dist/sources/provider-factory.js +2 -1
  122. package/dist/sources/providers/git.js +2 -2
  123. package/dist/sources/website-ingest.js +4 -0
  124. package/dist/tasks/backends/cron.js +200 -0
  125. package/dist/tasks/backends/exec-utils.js +25 -0
  126. package/dist/tasks/backends/index.js +32 -0
  127. package/dist/tasks/backends/launchd-template.xml +19 -0
  128. package/dist/tasks/backends/launchd.js +184 -0
  129. package/dist/tasks/backends/schtasks-template.xml +29 -0
  130. package/dist/tasks/backends/schtasks.js +212 -0
  131. package/dist/tasks/parser.js +198 -0
  132. package/dist/tasks/resolveAkmBin.js +84 -0
  133. package/dist/tasks/runner.js +432 -0
  134. package/dist/tasks/schedule.js +208 -0
  135. package/dist/tasks/schema.js +13 -0
  136. package/dist/tasks/validator.js +59 -0
  137. package/dist/wiki/index-template.md +12 -0
  138. package/dist/wiki/ingest-workflow-template.md +54 -0
  139. package/dist/wiki/log-template.md +8 -0
  140. package/dist/wiki/schema-template.md +61 -0
  141. package/dist/wiki/wiki-templates.js +12 -0
  142. package/dist/wiki/wiki.js +10 -61
  143. package/dist/workflows/authoring.js +5 -25
  144. package/dist/workflows/renderer.js +8 -3
  145. package/dist/workflows/runs.js +59 -91
  146. package/dist/workflows/validator.js +1 -1
  147. package/dist/workflows/workflow-template.md +24 -0
  148. package/docs/README.md +3 -0
  149. package/docs/migration/release-notes/0.7.0.md +1 -1
  150. package/docs/migration/release-notes/0.8.0.md +43 -0
  151. package/package.json +3 -2
  152. package/dist/templates/wiki-templates.js +0 -100
@@ -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 { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath, getCacheDir } from "./paths";
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
- export function getConfigDir(env, platform) {
23
- return _getConfigDir(env, platform);
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
- export function getConfigPath() {
26
- return _getConfigPath();
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
- cachedConfig = undefined;
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
- cachedConfig = undefined;
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
- // Shallow-merge for top-level scalar fields; deep-merge known object-type config keys.
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 pickKnownKeys(raw) {
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 === "off" || raw.semanticSearchMode === "auto") {
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 === "replace" || raw.stashInheritance === "merge") {
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 readNormalizedConfig(configPath) {
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 pickKnownKeys(expanded);
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 === "json" || obj.format === "yaml" || obj.format === "text") {
351
+ if (isOneOf(obj.format, ["json", "yaml", "text"])) {
265
352
  output.format = obj.format;
266
353
  }
267
- if (obj.detail === "brief" || obj.detail === "normal" || obj.detail === "full") {
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
- const tmpPath = `${configPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
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.startsWith("http://") && !obj.endpoint.startsWith("https://")) {
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
- if (typeof obj.dimension !== "number" ||
441
- !Number.isFinite(obj.dimension) ||
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
- if (typeof obj.contextLength !== "number" ||
456
- !Number.isFinite(obj.contextLength) ||
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
- if (typeof opts.num_ctx === "number" &&
467
- Number.isFinite(opts.num_ctx) &&
468
- Number.isInteger(opts.num_ctx) &&
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.startsWith("http://") && !obj.endpoint.startsWith("https://")) {
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
- if (typeof obj.timeoutMs !== "number" ||
505
- !Number.isFinite(obj.timeoutMs) ||
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
- result.timeoutMs = obj.timeoutMs;
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
- if (typeof obj.maxTokens !== "number" ||
514
- !Number.isFinite(obj.maxTokens) ||
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
- result.maxTokens = obj.maxTokens;
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
- switch (key) {
567
- case "memory_inference":
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
- throw new ConfigError(`Unknown key \`index.${passName}.${key}\`. Per-pass entries only support \`llm\` (boolean opt-out).`, "INVALID_CONFIG_FILE");
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 ${_getConfigPath()} to remove the openviking entry.`);
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) {