akm-cli 0.9.0-rc.1 → 0.9.0-rc.2

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 (228) hide show
  1. package/CHANGELOG.md +223 -9
  2. package/SECURITY.md +23 -24
  3. package/dist/assets/help/help-improve.md +10 -10
  4. package/dist/assets/hints/cli-hints-full.md +44 -10
  5. package/dist/assets/hints/cli-hints-short.md +6 -2
  6. package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
  7. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
  8. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
  9. package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
  10. package/dist/assets/stash-skeleton/README.md +28 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  23. package/dist/assets/tasks/core/backup.yml +1 -0
  24. package/dist/assets/tasks/core/extract.yml +1 -0
  25. package/dist/assets/tasks/core/improve.yml +1 -0
  26. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  27. package/dist/assets/tasks/core/sync.yml +1 -0
  28. package/dist/assets/tasks/core/version-check.yml +1 -0
  29. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  30. package/dist/assets/templates/html/health.html +5 -1
  31. package/dist/cli/config-migrate.js +31 -138
  32. package/dist/cli/config-validate.js +10 -8
  33. package/dist/cli.js +48 -14
  34. package/dist/commands/agent/agent-dispatch.js +17 -16
  35. package/dist/commands/agent/agent-support.js +0 -24
  36. package/dist/commands/agent/contribute-cli.js +5 -15
  37. package/dist/commands/backup-cli.js +54 -0
  38. package/dist/commands/config-cli.js +45 -159
  39. package/dist/commands/env/secret.js +8 -5
  40. package/dist/commands/health/checks.js +130 -83
  41. package/dist/commands/health/html-report.js +4 -0
  42. package/dist/commands/health/improve-metrics.js +30 -32
  43. package/dist/commands/health/llm-usage.js +19 -19
  44. package/dist/commands/health/md-report.js +4 -0
  45. package/dist/commands/health/metrics.js +2 -1
  46. package/dist/commands/health/surfaces.js +5 -4
  47. package/dist/commands/health.js +1 -1
  48. package/dist/commands/improve/consolidate/chunking.js +2 -2
  49. package/dist/commands/improve/consolidate.js +28 -25
  50. package/dist/commands/improve/distill/promote-memory.js +5 -12
  51. package/dist/commands/improve/distill/quality-gate.js +5 -7
  52. package/dist/commands/improve/distill.js +16 -5
  53. package/dist/commands/improve/eligibility.js +22 -12
  54. package/dist/commands/improve/extract-cli.js +47 -19
  55. package/dist/commands/improve/extract.js +110 -62
  56. package/dist/commands/improve/improve-cli.js +38 -16
  57. package/dist/commands/improve/improve-result-file.js +30 -24
  58. package/dist/commands/improve/improve-strategies.js +137 -0
  59. package/dist/commands/improve/improve.js +60 -30
  60. package/dist/commands/improve/locks.js +66 -45
  61. package/dist/commands/improve/loop-stages.js +75 -33
  62. package/dist/commands/improve/memory/memory-belief.js +79 -7
  63. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  64. package/dist/commands/improve/preparation.js +71 -73
  65. package/dist/commands/improve/procedural.js +3 -2
  66. package/dist/commands/improve/recombine.js +2 -1
  67. package/dist/commands/improve/reflect.js +119 -214
  68. package/dist/commands/improve/shared.js +11 -5
  69. package/dist/commands/lint/base-linter.js +152 -42
  70. package/dist/commands/mv-cli.js +809 -0
  71. package/dist/commands/proposal/proposal-cli.js +18 -8
  72. package/dist/commands/proposal/propose.js +64 -69
  73. package/dist/commands/read/knowledge.js +436 -4
  74. package/dist/commands/read/remember-cli.js +39 -2
  75. package/dist/commands/read/search-cli.js +6 -1
  76. package/dist/commands/registry-cli.js +29 -14
  77. package/dist/commands/remember.js +2 -0
  78. package/dist/commands/sources/init.js +13 -14
  79. package/dist/commands/sources/schema-repair.js +2 -4
  80. package/dist/commands/sources/source-add.js +62 -73
  81. package/dist/commands/sources/source-manage.js +50 -46
  82. package/dist/commands/sources/stash-cli.js +41 -4
  83. package/dist/commands/tasks/default-tasks.js +12 -12
  84. package/dist/commands/tasks/tasks-cli.js +7 -3
  85. package/dist/commands/tasks/tasks.js +113 -18
  86. package/dist/commands/wiki-cli.js +9 -10
  87. package/dist/core/asset/frontmatter.js +12 -2
  88. package/dist/core/common.js +5 -3
  89. package/dist/core/config/config-io.js +28 -17
  90. package/dist/core/config/config-schema.js +358 -66
  91. package/dist/core/config/config-types.js +3 -3
  92. package/dist/core/config/config-version.js +29 -0
  93. package/dist/core/config/config-walker.js +98 -27
  94. package/dist/core/config/config.js +132 -266
  95. package/dist/core/config/deep-merge.js +41 -0
  96. package/dist/core/config/engine-semantics.js +32 -0
  97. package/dist/core/errors.js +2 -2
  98. package/dist/core/extra-params.js +61 -0
  99. package/dist/core/file-lock.js +201 -56
  100. package/dist/core/improve-result.js +178 -0
  101. package/dist/core/maintenance-barrier.js +119 -0
  102. package/dist/core/migration-backup.js +416 -0
  103. package/dist/core/paths.js +3 -0
  104. package/dist/core/redaction.js +358 -0
  105. package/dist/core/state/migrations.js +17 -2
  106. package/dist/core/state-db.js +44 -1
  107. package/dist/indexer/db/db.js +116 -1
  108. package/dist/indexer/graph/graph-extraction.js +28 -16
  109. package/dist/indexer/index-writer-lock.js +31 -24
  110. package/dist/indexer/index-written-assets.js +15 -6
  111. package/dist/indexer/indexer.js +47 -2
  112. package/dist/indexer/passes/memory-inference.js +10 -6
  113. package/dist/indexer/passes/metadata.js +250 -0
  114. package/dist/indexer/search/db-search.js +111 -44
  115. package/dist/indexer/search/fts-query.js +41 -0
  116. package/dist/indexer/search/ranking-contributors.js +48 -0
  117. package/dist/indexer/search/ranking.js +36 -23
  118. package/dist/indexer/search/search-fields.js +11 -1
  119. package/dist/integrations/agent/builder-shared.js +7 -0
  120. package/dist/integrations/agent/builders.js +5 -52
  121. package/dist/integrations/agent/config.js +3 -143
  122. package/dist/integrations/agent/detect.js +17 -2
  123. package/dist/integrations/agent/engine-resolution.js +202 -0
  124. package/dist/integrations/agent/index.js +1 -2
  125. package/dist/integrations/agent/model-aliases.js +7 -2
  126. package/dist/integrations/agent/profiles.js +6 -99
  127. package/dist/integrations/agent/runner-dispatch.js +76 -13
  128. package/dist/integrations/agent/runner.js +76 -207
  129. package/dist/integrations/agent/spawn.js +4 -6
  130. package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
  131. package/dist/integrations/harnesses/aider/index.js +0 -1
  132. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
  133. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  134. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  135. package/dist/integrations/harnesses/claude/index.js +0 -2
  136. package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
  137. package/dist/integrations/harnesses/codex/index.js +0 -1
  138. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
  139. package/dist/integrations/harnesses/copilot/index.js +0 -1
  140. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
  141. package/dist/integrations/harnesses/gemini/index.js +0 -1
  142. package/dist/integrations/harnesses/index.js +1 -24
  143. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  144. package/dist/integrations/harnesses/opencode/index.js +0 -6
  145. package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
  146. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  147. package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
  148. package/dist/integrations/harnesses/openhands/index.js +0 -1
  149. package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
  150. package/dist/integrations/harnesses/pi/index.js +0 -1
  151. package/dist/integrations/harnesses/types.js +1 -32
  152. package/dist/integrations/lockfile.js +32 -21
  153. package/dist/llm/client.js +48 -14
  154. package/dist/llm/feature-gate.js +15 -47
  155. package/dist/llm/graph-extract.js +1 -1
  156. package/dist/llm/index-passes.js +8 -42
  157. package/dist/llm/memory-infer-impl.js +1 -1
  158. package/dist/llm/usage-persist.js +4 -0
  159. package/dist/llm/usage-telemetry.js +35 -5
  160. package/dist/output/shapes/helpers.js +2 -1
  161. package/dist/output/shapes/passthrough.js +2 -0
  162. package/dist/output/text/helpers.js +3 -1
  163. package/dist/schemas/akm-config.json +11013 -8600
  164. package/dist/schemas/akm-task.json +87 -0
  165. package/dist/schemas/akm-workflow.json +57 -13
  166. package/dist/scripts/migrate-storage.js +8591 -509
  167. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
  168. package/dist/setup/detected-engines.js +142 -0
  169. package/dist/setup/engine-config.js +89 -0
  170. package/dist/setup/setup.js +236 -132
  171. package/dist/setup/steps/connection.js +61 -32
  172. package/dist/setup/steps/platforms.js +4 -4
  173. package/dist/setup/steps.js +3 -2
  174. package/dist/storage/database.js +13 -1
  175. package/dist/storage/engines/sqlite-migrations.js +1 -0
  176. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  177. package/dist/storage/repositories/task-history-repository.js +78 -0
  178. package/dist/storage/repositories/workflow-runs-repository.js +9 -8
  179. package/dist/tasks/parser.js +138 -52
  180. package/dist/tasks/runner.js +71 -75
  181. package/dist/tasks/schema.js +1 -1
  182. package/dist/tasks/validator.js +11 -6
  183. package/dist/wiki/wiki.js +9 -8
  184. package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
  185. package/dist/workflows/concurrency-policy.js +15 -0
  186. package/dist/workflows/db.js +65 -13
  187. package/dist/workflows/exec/brief.js +18 -24
  188. package/dist/workflows/exec/frozen-judge.js +47 -0
  189. package/dist/workflows/exec/native-executor.js +124 -65
  190. package/dist/workflows/exec/report.js +93 -33
  191. package/dist/workflows/exec/run-workflow.js +38 -25
  192. package/dist/workflows/exec/scheduler.js +12 -41
  193. package/dist/workflows/exec/step-work.js +91 -35
  194. package/dist/workflows/ir/compile.js +13 -26
  195. package/dist/workflows/ir/freeze.js +243 -0
  196. package/dist/workflows/ir/plan-hash.js +40 -5
  197. package/dist/workflows/ir/schema.js +542 -1
  198. package/dist/workflows/parser.js +7 -0
  199. package/dist/workflows/program/parser.js +132 -23
  200. package/dist/workflows/program/project.js +3 -4
  201. package/dist/workflows/program/schema.js +2 -2
  202. package/dist/workflows/resource-limits.js +20 -0
  203. package/dist/workflows/runtime/plan-classifier.js +187 -0
  204. package/dist/workflows/runtime/runs.js +54 -47
  205. package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
  206. package/dist/workflows/validator.js +25 -0
  207. package/docs/data-and-telemetry.md +2 -2
  208. package/docs/migration/release-notes/0.6.0.md +1 -1
  209. package/docs/migration/release-notes/0.7.0.md +5 -4
  210. package/docs/migration/v0.8-to-v0.9.md +401 -0
  211. package/package.json +4 -2
  212. package/schemas/akm-config.json +16638 -0
  213. package/schemas/akm-task.json +87 -0
  214. package/schemas/akm-workflow.json +372 -0
  215. package/dist/commands/improve/improve-profiles.js +0 -168
  216. package/dist/core/config/config-migration.js +0 -602
  217. package/dist/core/deep-merge.js +0 -38
  218. package/dist/llm/call-ai.js +0 -62
  219. package/dist/setup/legacy-config.js +0 -106
  220. package/docs/README.md +0 -104
  221. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  222. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  223. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  224. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  225. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  226. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  227. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  228. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -0,0 +1,29 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /** Compare legacy numeric or semver-like config versions for diagnostics only. */
5
+ export function compareConfigVersion(a, b) {
6
+ const left = normalize(a);
7
+ const right = normalize(b);
8
+ if (!left || !right)
9
+ return undefined;
10
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
11
+ const l = left[index] ?? 0;
12
+ const r = right[index] ?? 0;
13
+ if (l < r)
14
+ return -1;
15
+ if (l > r)
16
+ return 1;
17
+ }
18
+ return 0;
19
+ }
20
+ function normalize(value) {
21
+ if (typeof value === "number" && Number.isFinite(value))
22
+ return [0, Math.trunc(value), 0];
23
+ if (typeof value !== "string" || !value.trim())
24
+ return undefined;
25
+ const parts = value.trim().replace(/^v/i, "").split(/[-+]/, 1)[0].split(".");
26
+ if (parts.some((part) => !/^\d+$/.test(part)))
27
+ return undefined;
28
+ return parts.map(Number);
29
+ }
@@ -22,7 +22,8 @@
22
22
  */
23
23
  import { z } from "zod";
24
24
  import { UsageError } from "../errors.js";
25
- import { AkmConfigBaseSchema, listTopLevelConfigKeys } from "./config-schema.js";
25
+ import { AkmConfigBaseSchema, EngineConfigSchema, listTopLevelConfigKeys } from "./config-schema.js";
26
+ import { deepMergeConfig } from "./deep-merge.js";
26
27
  /**
27
28
  * Parse a dotted path into segments. Empty segments are rejected. Bracket
28
29
  * notation (e.g. `sources[0]`) is NOT supported — arrays are set as JSON.
@@ -73,9 +74,10 @@ function unwrap(schema) {
73
74
  * AkmConfig schema. Returns `undefined` if any path segment doesn't match a
74
75
  * known schema field.
75
76
  */
76
- function resolveSchemaAt(path) {
77
+ function resolveSchemaAt(path, config, raw) {
77
78
  let schema = AkmConfigBaseSchema;
78
- for (const segment of path) {
79
+ let existing = config;
80
+ for (const [index, segment] of path.entries()) {
79
81
  schema = unwrap(schema);
80
82
  if (schema instanceof z.ZodObject) {
81
83
  const next = schema.shape[segment];
@@ -84,23 +86,56 @@ function resolveSchemaAt(path) {
84
86
  const catchall = schema._def.catchall;
85
87
  if (catchall && !(catchall instanceof z.ZodNever)) {
86
88
  schema = catchall;
87
- continue;
88
89
  }
89
- return undefined;
90
+ else {
91
+ return undefined;
92
+ }
93
+ }
94
+ else {
95
+ schema = next;
90
96
  }
91
- schema = next;
92
97
  }
93
98
  else if (schema instanceof z.ZodRecord) {
94
- // Records (profiles.llm, profiles.agent, profiles.improve, sources, etc.)
99
+ // Named records (engines, improve.strategies, sources, etc.)
95
100
  // accept any string key — descend into the value schema.
96
101
  schema = schema._def.valueType;
97
102
  }
103
+ else if (schema instanceof z.ZodUnion) {
104
+ const option = selectUnionObjectOption(schema._def.options, segment, existing, index === path.length - 1 ? raw : undefined);
105
+ if (!option)
106
+ return undefined;
107
+ schema = option.shape[segment];
108
+ }
98
109
  else {
99
110
  // Cannot descend into a non-object schema.
100
111
  return undefined;
101
112
  }
113
+ existing = isPlainObject(existing) ? existing[segment] : undefined;
102
114
  }
103
- return unwrap(schema);
115
+ // Preserve leaf refinements/transforms for validation. Traversal unwraps at
116
+ // the start of each loop iteration, while coercion unwraps independently.
117
+ return schema;
118
+ }
119
+ function selectUnionObjectOption(candidates, segment, existing, raw) {
120
+ const options = candidates
121
+ .map((candidate) => unwrap(candidate))
122
+ .filter((candidate) => candidate instanceof z.ZodObject);
123
+ if (raw !== undefined) {
124
+ const requested = options.find((option) => {
125
+ const leaf = option.shape[segment];
126
+ if (!leaf || !(unwrap(leaf) instanceof z.ZodLiteral))
127
+ return false;
128
+ return leaf.safeParse(coerceForSchema(leaf, raw, segment)).success;
129
+ });
130
+ if (requested)
131
+ return requested;
132
+ }
133
+ if (isPlainObject(existing)) {
134
+ const selected = options.find((option) => Object.entries(option.shape).some(([key, field]) => key in existing && unwrap(field) instanceof z.ZodLiteral && field.safeParse(existing[key]).success));
135
+ if (selected)
136
+ return segment in selected.shape ? selected : undefined;
137
+ }
138
+ return options.find((option) => segment in option.shape);
104
139
  }
105
140
  /**
106
141
  * Get the value at the dotted path from a config-shaped object. Returns
@@ -109,7 +144,7 @@ function resolveSchemaAt(path) {
109
144
  */
110
145
  export function configGet(config, dotted) {
111
146
  const path = parsePath(dotted);
112
- const schema = resolveSchemaAt(path);
147
+ const schema = resolveSchemaAt(path, config);
113
148
  if (!schema) {
114
149
  throw new UsageError(`Unknown config key: ${dotted}`, "INVALID_FLAG_VALUE", unknownKeyHint(dotted));
115
150
  }
@@ -129,23 +164,38 @@ export function configGet(config, dotted) {
129
164
  *
130
165
  * Throws {@link UsageError} on:
131
166
  * - unknown path
132
- * - apiKey paths (#454)
167
+ * - invalid apiKey paths
133
168
  * - coercion failure
134
169
  */
135
170
  export function configSet(config, dotted, raw) {
136
171
  const path = parsePath(dotted);
172
+ if (path.length === 1 && path[0] === "configVersion") {
173
+ throw new UsageError("configVersion cannot be changed through config set.", "INVALID_FLAG_VALUE");
174
+ }
137
175
  // #454: apiKey paths are not persistable. Throw at set time.
138
176
  rejectApiKeyPath(path, dotted);
139
- const schema = resolveSchemaAt(path);
140
- if (!schema) {
177
+ const schema = resolveSchemaAt(path, config, raw);
178
+ const symbolicApiKey = (path[0] === "engines" && path[2] === "apiKey") ||
179
+ (path[0] === "embedding" && path.length === 2 && path[1] === "apiKey");
180
+ if (!schema && !symbolicApiKey) {
141
181
  throw new UsageError(`Unknown config key: ${dotted}`, "INVALID_FLAG_VALUE", unknownKeyHint(dotted));
142
182
  }
143
- const value = coerceForSchema(schema, raw, dotted);
183
+ const value = path[0] === "engines" && path.length === 2
184
+ ? parseObjectPatch(raw, dotted)
185
+ : symbolicApiKey
186
+ ? raw
187
+ : coerceForSchema(schema, raw, dotted);
144
188
  // Validate the coerced value against the leaf schema. This catches enum
145
189
  // mismatches, out-of-range numbers, schema-level shape errors (writable
146
190
  // npm/website sources via .superRefine, strict-mode unknown keys in nested
147
191
  // objects, etc.) BEFORE we apply the patch.
148
- const parsed = schema.safeParse(value);
192
+ const parsed = path[0] === "engines" && path.length === 2
193
+ ? EngineConfigSchema.safeParse(value)
194
+ : symbolicApiKey
195
+ ? /^\$[A-Za-z_][A-Za-z0-9_]*$|^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(raw)
196
+ ? { success: true, data: value }
197
+ : { success: false, error: { issues: [{ path: [], message: `apiKey must be $VAR or \${VAR}` }] } }
198
+ : schema.safeParse(value);
149
199
  if (!parsed.success) {
150
200
  const lines = parsed.error.issues
151
201
  .map((i) => {
@@ -155,7 +205,12 @@ export function configSet(config, dotted, raw) {
155
205
  .join("\n");
156
206
  throw new UsageError(`Invalid value for ${dotted}:\n${lines}`, "INVALID_FLAG_VALUE");
157
207
  }
158
- const next = setPath(config, path, parsed.data);
208
+ const existing = path.reduce((value, key) => {
209
+ if (value && typeof value === "object")
210
+ return value[key];
211
+ return undefined;
212
+ }, config);
213
+ const next = setPath(config, path, isPlainObject(existing) && isPlainObject(parsed.data) ? deepMergeConfig(existing, parsed.data) : parsed.data);
159
214
  // Targeted invariant: defaultWriteTarget must point at a configured source
160
215
  // (#464.a). Whole-config validation happens at save time; this check fires
161
216
  // at set time so the user sees the typo immediately. Empty-sources case is
@@ -175,8 +230,11 @@ export function configSet(config, dotted, raw) {
175
230
  */
176
231
  export function configUnset(config, dotted) {
177
232
  const path = parsePath(dotted);
233
+ if (path.length === 1 && path[0] === "configVersion") {
234
+ throw new UsageError("configVersion cannot be removed through config unset.", "INVALID_FLAG_VALUE");
235
+ }
178
236
  // Validate the path resolves to a real schema field (so typos don't no-op).
179
- const schema = resolveSchemaAt(path);
237
+ const schema = resolveSchemaAt(path, config);
180
238
  if (!schema) {
181
239
  throw new UsageError(`Unknown config key: ${dotted}`, "INVALID_FLAG_VALUE", unknownKeyHint(dotted));
182
240
  }
@@ -184,12 +242,14 @@ export function configUnset(config, dotted) {
184
242
  }
185
243
  // ── apiKey rejection (#454) ─────────────────────────────────────────────────
186
244
  /**
187
- * Reject any attempt to persist an apiKey via `akm config set`. Matches:
188
- * - `llm.apiKey` (legacy alias mapped to default LLM profile)
189
- * - `embedding.apiKey`
190
- * - `profiles.llm.<name>.apiKey`
245
+ * Embedding credentials are never persisted. Engine credentials are symbolic
246
+ * configuration and are validated by the engine schema below.
191
247
  */
192
248
  function rejectApiKeyPath(path, dotted) {
249
+ if (path[0] === "engines" && path[2] === "apiKey")
250
+ return;
251
+ if (path[0] === "embedding" && path.length === 2 && path[1] === "apiKey")
252
+ return;
193
253
  const last = path[path.length - 1];
194
254
  if (last !== "apiKey")
195
255
  return;
@@ -197,14 +257,25 @@ function rejectApiKeyPath(path, dotted) {
197
257
  throw new UsageError(`apiKey cannot be persisted in config; export ${recipe} instead. (key: ${dotted})`, "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
198
258
  "Use the corresponding environment variable. AKM reads it at request time.");
199
259
  }
260
+ function parseObjectPatch(raw, key) {
261
+ try {
262
+ const value = JSON.parse(raw);
263
+ if (!isPlainObject(value))
264
+ throw new Error("expected an object");
265
+ return value;
266
+ }
267
+ catch (err) {
268
+ throw new UsageError(`Invalid JSON object for ${key}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_JSON_CONFIG_VALUE");
269
+ }
270
+ }
271
+ function isPlainObject(value) {
272
+ return value !== null && typeof value === "object" && !Array.isArray(value);
273
+ }
200
274
  function recipeForApiKey(path, _dotted) {
201
275
  if (path[0] === "embedding")
202
276
  return "AKM_EMBED_API_KEY";
203
- if (path[0] === "llm")
204
- return "AKM_LLM_API_KEY";
205
- if (path[0] === "profiles" && path[1] === "llm" && typeof path[2] === "string") {
206
- const upper = path[2].toUpperCase().replace(/-/g, "_");
207
- return `AKM_PROFILE_${upper}_API_KEY (or AKM_LLM_API_KEY for the default profile)`;
277
+ if (path[0] === "engines" && typeof path[1] === "string") {
278
+ return `AKM_ENGINE_${path[1].toUpperCase().replaceAll("-", "_")}_API_KEY`;
208
279
  }
209
280
  return "AKM_LLM_API_KEY / AKM_EMBED_API_KEY";
210
281
  }
@@ -237,7 +308,7 @@ function coerceForSchema(schema, raw, key) {
237
308
  return raw;
238
309
  }
239
310
  if (target instanceof z.ZodLiteral) {
240
- return target._def.value;
311
+ return typeof target._def.value === "string" ? raw : target._def.value;
241
312
  }
242
313
  if (target instanceof z.ZodArray || target instanceof z.ZodObject || target instanceof z.ZodRecord) {
243
314
  if (raw === "" || raw === "null")
@@ -333,5 +404,5 @@ function unsetPath(config, path) {
333
404
  // ── Hint generation (#460) ──────────────────────────────────────────────────
334
405
  export function unknownKeyHint(_attempted) {
335
406
  const keys = listTopLevelConfigKeys();
336
- return `Valid top-level keys: ${keys.join(", ")}. Use dotted paths for nested values (e.g. embedding.endpoint, profiles.llm.<name>.model).`;
407
+ return `Valid top-level keys: ${keys.join(", ")}. Use dotted paths for nested values (e.g. embedding.endpoint, engines.<name>.model).`;
337
408
  }