akm-cli 0.9.0-rc.0 → 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.
- package/CHANGELOG.md +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { ConfigError } from "../errors.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { AkmConfigSchema } from "./config-schema.js";
|
|
7
|
+
import { ensureMigrationBackupWithConfigLockHeld } from "../migration-backup.js";
|
|
8
|
+
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
9
|
+
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
10
|
+
import { deepMergeConfig } from "./deep-merge.js";
|
|
10
11
|
export { stripJsonComments } from "./config-io.js";
|
|
11
|
-
import {
|
|
12
|
+
import { materializeLlmConnection, resolveLlmEngineUse } from "../../integrations/agent/engine-resolution.js";
|
|
13
|
+
import { getConfigPath } from "../paths.js";
|
|
12
14
|
import { warn } from "../warn.js";
|
|
13
15
|
// Canonical harness-id source of truth (#565) — runtime value re-export.
|
|
14
16
|
export { VALID_HARNESS_IDS } from "./config-types.js";
|
|
@@ -48,6 +50,7 @@ export function resolveBatchSize(configured, contextLength) {
|
|
|
48
50
|
}
|
|
49
51
|
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
50
52
|
export const DEFAULT_CONFIG = {
|
|
53
|
+
configVersion: "0.9.0",
|
|
51
54
|
semanticSearchMode: "auto",
|
|
52
55
|
registries: [
|
|
53
56
|
{ url: "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json", name: "akm-registry" },
|
|
@@ -70,9 +73,12 @@ export function loadUserConfig() {
|
|
|
70
73
|
try {
|
|
71
74
|
stat = fs.statSync(configPath);
|
|
72
75
|
}
|
|
73
|
-
catch {
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (err.code !== "ENOENT") {
|
|
78
|
+
throw new ConfigError(`Unable to read config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
|
|
79
|
+
}
|
|
74
80
|
cachedConfig = undefined;
|
|
75
|
-
return
|
|
81
|
+
return { ...DEFAULT_CONFIG };
|
|
76
82
|
}
|
|
77
83
|
// Cache key: mtimeMs + size. Tests that write rapidly back-to-back inside
|
|
78
84
|
// the mtime resolution window MUST call resetConfigCache() between writes —
|
|
@@ -87,15 +93,14 @@ export function loadUserConfig() {
|
|
|
87
93
|
try {
|
|
88
94
|
text = fs.readFileSync(configPath, "utf8");
|
|
89
95
|
}
|
|
90
|
-
catch {
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err.code !== "ENOENT") {
|
|
98
|
+
throw new ConfigError(`Unable to read config at ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
|
|
99
|
+
}
|
|
91
100
|
cachedConfig = undefined;
|
|
92
|
-
return
|
|
101
|
+
return { ...DEFAULT_CONFIG };
|
|
93
102
|
}
|
|
94
|
-
|
|
95
|
-
// sees canonical input. AKM_NO_AUTO_MIGRATE=1 skips the disk rewrite (still
|
|
96
|
-
// applies in-memory).
|
|
97
|
-
text = maybeAutoMigrateConfigFile(configPath, text);
|
|
98
|
-
const finalConfig = applyRuntimeEnvApiKeys(parseAndValidate(text, configPath));
|
|
103
|
+
const finalConfig = parseAndValidateConfigText(text, configPath);
|
|
99
104
|
// Re-stat after potential write-back so the cache key reflects the new mtime.
|
|
100
105
|
let finalStat = stat;
|
|
101
106
|
try {
|
|
@@ -113,26 +118,31 @@ export function loadUserConfig() {
|
|
|
113
118
|
return finalConfig;
|
|
114
119
|
}
|
|
115
120
|
/**
|
|
116
|
-
* Parse raw config text
|
|
117
|
-
* ({@link migrateConfigShape}), then validate via Zod
|
|
121
|
+
* Parse raw config text and validate via Zod.
|
|
118
122
|
* ({@link AkmConfigSchema}). Returns the merged-with-defaults AkmConfig.
|
|
119
123
|
*
|
|
120
124
|
* The migration handles all one-time 0.7→0.8 transforms (legacy keys,
|
|
121
125
|
* boolean→string coercions, openviking rename); the schema then validates
|
|
122
126
|
* the canonical shape and throws on anything it doesn't recognise.
|
|
123
127
|
*/
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const parsed = AkmConfigSchema.safeParse(
|
|
128
|
+
export function parseAndValidateConfigText(text, sourcePath) {
|
|
129
|
+
const raw = parseConfigText(text, sourcePath);
|
|
130
|
+
if (raw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
131
|
+
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
132
|
+
}
|
|
133
|
+
const parsed = AkmConfigSchema.safeParse(raw);
|
|
130
134
|
if (!parsed.success) {
|
|
131
135
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
132
136
|
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
133
137
|
throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
|
|
134
138
|
}
|
|
135
|
-
|
|
139
|
+
const merged = deepMergeConfig(DEFAULT_CONFIG, parsed.data);
|
|
140
|
+
const finalResult = AkmConfigSchema.safeParse(merged);
|
|
141
|
+
if (!finalResult.success) {
|
|
142
|
+
const lines = finalResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
143
|
+
throw new ConfigError(`Invalid merged config${sourcePath ? ` at ${sourcePath}` : ""}:\n${lines}`, "INVALID_CONFIG_FILE");
|
|
144
|
+
}
|
|
145
|
+
return finalResult.data;
|
|
136
146
|
}
|
|
137
147
|
export function getSources(config) {
|
|
138
148
|
return config.sources ?? [];
|
|
@@ -140,144 +150,24 @@ export function getSources(config) {
|
|
|
140
150
|
export function getEffectiveRegistries(config) {
|
|
141
151
|
return config.registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
142
152
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Resolve the name of the default LLM profile.
|
|
145
|
-
*
|
|
146
|
-
* Resolution order:
|
|
147
|
-
* 1. `defaults.llm` — explicit pointer set by the user.
|
|
148
|
-
* 2. A profile literally named `default` under `profiles.llm` — implicit
|
|
149
|
-
* fallback. The convention "name your default profile `default`" is
|
|
150
|
-
* what `akm setup` produces, so an unset `defaults.llm` next to a
|
|
151
|
-
* `profiles.llm.default` block is overwhelmingly a config-rewrite
|
|
152
|
-
* casualty (see [[project_akm_config_clobber_trap]]) rather than
|
|
153
|
-
* intent. Treating that shape as configured avoids the silent
|
|
154
|
-
* `getDefaultLlmConfig → undefined → pass-returns-zero` failure mode
|
|
155
|
-
* that produced 18h of no-op memory-inference runs on 2026-05-23.
|
|
156
|
-
*
|
|
157
|
-
* Returns `undefined` when neither path resolves to a profile name.
|
|
158
|
-
*/
|
|
159
|
-
function resolveDefaultLlmProfileName(config) {
|
|
160
|
-
const explicit = config.defaults?.llm;
|
|
161
|
-
if (explicit)
|
|
162
|
-
return explicit;
|
|
163
|
-
if (config.profiles?.llm?.default)
|
|
164
|
-
return "default";
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Resolve the default LLM connection from `profiles.llm[defaults.llm]`.
|
|
169
|
-
*
|
|
170
|
-
* Throws {@link ConfigError} when no default profile can be resolved (neither
|
|
171
|
-
* `defaults.llm` nor an implicit `profiles.llm.default`) or when the named
|
|
172
|
-
* profile does not exist under `profiles.llm`. Use this in code paths that
|
|
173
|
-
* must have an LLM configured (per-pass index calls, distill, consolidate,
|
|
174
|
-
* etc).
|
|
175
|
-
*/
|
|
153
|
+
/** Resolve and materialize the configured default LLM engine at dispatch time. */
|
|
176
154
|
export function requireLlmConfig(config) {
|
|
177
|
-
|
|
178
|
-
if (!defaultName) {
|
|
179
|
-
throw new ConfigError("LLM is not configured. Run `akm setup` or set `defaults.llm` to a profile defined in `profiles.llm`.", "LLM_NOT_CONFIGURED");
|
|
180
|
-
}
|
|
181
|
-
const profile = config.profiles?.llm?.[defaultName];
|
|
182
|
-
if (!profile) {
|
|
183
|
-
throw new ConfigError(`LLM default profile "${defaultName}" not found in profiles.llm.`, "LLM_NOT_CONFIGURED", `Available profiles: ${Object.keys(config.profiles?.llm ?? {}).join(", ") || "none"}. Run \`akm setup\` to configure.`);
|
|
184
|
-
}
|
|
185
|
-
return profile;
|
|
155
|
+
return materializeLlmConnection(resolveLlmEngineUse(config, []));
|
|
186
156
|
}
|
|
187
157
|
/**
|
|
188
158
|
* Like {@link requireLlmConfig} but returns `undefined` instead of throwing
|
|
189
159
|
* when no LLM is configured. Use in code paths where the LLM is optional.
|
|
190
160
|
*/
|
|
191
161
|
export function getDefaultLlmConfig(config) {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
return undefined;
|
|
195
|
-
return config.profiles?.llm?.[defaultName];
|
|
162
|
+
const resolved = resolveLlmEngineUse(config, [], { optional: true });
|
|
163
|
+
return resolved ? materializeLlmConnection(resolved) : undefined;
|
|
196
164
|
}
|
|
197
165
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* `profile?.processes?.<name>` that was previously copy-pasted across the
|
|
201
|
-
* improve command family (20+ call sites).
|
|
202
|
-
*
|
|
203
|
-
* When an `activeProfile` is supplied (the profile resolved for the current
|
|
204
|
-
* `akm improve --profile <name>` run), its per-process override wins; otherwise
|
|
205
|
-
* — and as a fallback when the active profile does not define the section — the
|
|
206
|
-
* lookup falls back to the `"default"` improve profile from the on-disk config.
|
|
207
|
-
* Callers that have not yet threaded the active profile pass only `config` and
|
|
208
|
-
* get the historical default-profile behavior unchanged.
|
|
166
|
+
* Transitional internal accessor. It deliberately never consults a configured
|
|
167
|
+
* default strategy; callers must pass their already selected strategy.
|
|
209
168
|
*/
|
|
210
|
-
export function getImproveProcessConfig(
|
|
211
|
-
|
|
212
|
-
if (fromActiveProfile !== undefined)
|
|
213
|
-
return fromActiveProfile;
|
|
214
|
-
return config.profiles?.improve?.default?.processes?.[processName];
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Run `migrateConfigShape` on the raw text and — unless `AKM_NO_AUTO_MIGRATE=1`
|
|
218
|
-
* is set — persist the migrated result. Returns the (possibly migrated) text
|
|
219
|
-
* for the caller to feed into `parseAndValidate`.
|
|
220
|
-
*
|
|
221
|
-
* If the on-disk config is newer than this binary's known version, the bytes
|
|
222
|
-
* are left untouched (we won't silently strip fields on downgrade).
|
|
223
|
-
*/
|
|
224
|
-
function maybeAutoMigrateConfigFile(configPath, text) {
|
|
225
|
-
let obj;
|
|
226
|
-
try {
|
|
227
|
-
obj = parseConfigText(text);
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
return text; // Malformed JSON — let parseAndValidate surface the error.
|
|
231
|
-
}
|
|
232
|
-
// Downgrade protection. Skip migration when the on-disk config is NEWER than
|
|
233
|
-
// this binary (=== 1), OR when it carries a configVersion we cannot order
|
|
234
|
-
// against ours (compareConfigVersion returns undefined for an unparseable
|
|
235
|
-
// value — e.g. one written by a newer/foreign akm). Migrating such a config
|
|
236
|
-
// could strip fields a newer binary added, a cross-version data-loss path.
|
|
237
|
-
//
|
|
238
|
-
// A MISSING configVersion (absent → undefined) is a legacy pre-versioning
|
|
239
|
-
// config that MUST still migrate, so the unparseable-skip is gated on the
|
|
240
|
-
// field being PRESENT (`onDiskVersion !== undefined`).
|
|
241
|
-
const onDiskVersion = obj.configVersion;
|
|
242
|
-
const versionOrder = compareConfigVersion(onDiskVersion, CURRENT_CONFIG_VERSION);
|
|
243
|
-
if (versionOrder === 1 || (onDiskVersion !== undefined && versionOrder === undefined)) {
|
|
244
|
-
return text;
|
|
245
|
-
}
|
|
246
|
-
const { changed, result } = migrateConfigShape(obj);
|
|
247
|
-
if (!changed)
|
|
248
|
-
return text;
|
|
249
|
-
const migratedText = `${JSON.stringify(result, null, 2)}\n`;
|
|
250
|
-
if (process.env.AKM_NO_AUTO_MIGRATE === "1")
|
|
251
|
-
return migratedText;
|
|
252
|
-
try {
|
|
253
|
-
withConfigLock(() => {
|
|
254
|
-
backupExistingConfig(configPath);
|
|
255
|
-
writeConfigAtomic(configPath, result);
|
|
256
|
-
});
|
|
257
|
-
const newVersion = typeof result.configVersion === "string" ? result.configVersion : "0.8.0";
|
|
258
|
-
const backupDir = `${getCacheDir()}/config-backups`;
|
|
259
|
-
// WS-2: emit a loud banner to BOTH stderr and stdout so pipelines and
|
|
260
|
-
// interactive terminals both see it. Include the backup path (resolved,
|
|
261
|
-
// not ~/...), opt-out env var, and preview diff command.
|
|
262
|
-
const banner = [
|
|
263
|
-
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
264
|
-
` akm: auto-migrated config → ${newVersion} format`,
|
|
265
|
-
` file: ${configPath}`,
|
|
266
|
-
` backup: ${backupDir}/config-<timestamp>.json`,
|
|
267
|
-
" to opt out of future auto-migration: AKM_NO_AUTO_MIGRATE=1",
|
|
268
|
-
" to preview a dry-run diff: akm config migrate --dry-run --print-diff",
|
|
269
|
-
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
270
|
-
].join("\n");
|
|
271
|
-
process.stderr?.write?.(`${banner}\n`);
|
|
272
|
-
process.stdout?.write?.(`${banner}\n`);
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
// #461: never return migrated bytes when disk write fails — that triggers
|
|
276
|
-
// an infinite re-migrate loop on every load. Hard-error so the user
|
|
277
|
-
// notices and either fixes the disk issue or sets AKM_NO_AUTO_MIGRATE=1.
|
|
278
|
-
throw new ConfigError(`Failed to write migrated config to ${configPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE", "Check filesystem permissions, free space, and disk health. To skip auto-migration, set AKM_NO_AUTO_MIGRATE=1.");
|
|
279
|
-
}
|
|
280
|
-
return migratedText;
|
|
169
|
+
export function getImproveProcessConfig(_config, processName, selected) {
|
|
170
|
+
return selected?.processes?.[processName];
|
|
281
171
|
}
|
|
282
172
|
export function loadConfig() {
|
|
283
173
|
// Single-layer load: only the user-level config file is read. Project-level
|
|
@@ -292,39 +182,90 @@ export function _setSaveConfigForTests(fake) {
|
|
|
292
182
|
saveConfigOverride = fake;
|
|
293
183
|
}
|
|
294
184
|
export function saveConfig(config) {
|
|
185
|
+
// Every lifecycle write produces the only config version this binary can load.
|
|
186
|
+
const currentConfig = { ...config, configVersion: CURRENT_CONFIG_VERSION };
|
|
295
187
|
if (saveConfigOverride) {
|
|
296
|
-
saveConfigOverride(
|
|
188
|
+
saveConfigOverride(currentConfig);
|
|
297
189
|
return;
|
|
298
190
|
}
|
|
299
|
-
saveConfigReal(
|
|
191
|
+
saveConfigReal(currentConfig);
|
|
300
192
|
}
|
|
301
193
|
function saveConfigReal(config) {
|
|
302
194
|
cachedConfig = undefined;
|
|
303
195
|
const configPath = getConfigPath();
|
|
304
196
|
const dir = path.dirname(configPath);
|
|
305
197
|
fs.mkdirSync(dir, { recursive: true });
|
|
306
|
-
const sanitized = sanitizeConfigForWrite(config);
|
|
307
|
-
// Final validation gate before bytes hit disk. Runs the FULL schema —
|
|
308
|
-
// including the cross-field superRefine guards (removed `feedbackDistillation`
|
|
309
|
-
// process key, `defaultWriteTarget` resolution, writable npm/website sources)
|
|
310
|
-
// and all type/enum/range checks — so an `akm config set` (leaf OR object
|
|
311
|
-
// form) cannot persist a guard-violating or mistyped value. NOTE: unknown
|
|
312
|
-
// keys are intentionally NOT rejected here — object schemas are `.passthrough()`
|
|
313
|
-
// so cross-version skew round-trips (see config-schema.ts header); the lenient
|
|
314
|
-
// tolerance is by design, not an oversight.
|
|
315
|
-
const parseResult = AkmConfigSchema.safeParse(sanitized);
|
|
316
|
-
if (!parseResult.success) {
|
|
317
|
-
const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
318
|
-
throw new ConfigError(`Refusing to save invalid config:\n${lines}`, "INVALID_CONFIG_FILE", "Fix the listed fields, or undo the offending `akm config set`. " +
|
|
319
|
-
"If this looks like an akm bug, re-run with --debug to attach the traceback.");
|
|
320
|
-
}
|
|
321
|
-
// WS-3: acquire the config write lock so concurrent `akm config set`
|
|
322
|
-
// invocations do not interleave their backup+atomic-write cycles.
|
|
323
198
|
withConfigLock(() => {
|
|
199
|
+
const validated = validateCompleteConfig(config);
|
|
200
|
+
ensureMigrationBackupWithConfigLockHeld();
|
|
324
201
|
backupExistingConfig(configPath);
|
|
325
|
-
writeConfigAtomic(configPath,
|
|
202
|
+
writeConfigAtomic(configPath, sanitizeConfigForWrite(validated));
|
|
326
203
|
});
|
|
327
204
|
}
|
|
205
|
+
export function validateCompleteConfig(config) {
|
|
206
|
+
const parseResult = AkmConfigSchema.safeParse(config);
|
|
207
|
+
if (parseResult.success)
|
|
208
|
+
return parseResult.data;
|
|
209
|
+
const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
210
|
+
throw new ConfigError(`Refusing to save invalid config:\n${lines}`, "INVALID_CONFIG_FILE", "Fix the listed fields, or undo the offending `akm config set`. " +
|
|
211
|
+
"If this looks like an akm bug, re-run with --debug to attach the traceback.");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Mutate config under one fail-closed lock spanning read, merge, validation,
|
|
215
|
+
* migration backup, ordinary backup, and atomic write.
|
|
216
|
+
*/
|
|
217
|
+
export function mutateConfig(mutate, options) {
|
|
218
|
+
cachedConfig = undefined;
|
|
219
|
+
const configPath = getConfigPath();
|
|
220
|
+
return withConfigLock(() => {
|
|
221
|
+
const text = readConfigText(configPath);
|
|
222
|
+
if (text === undefined && options?.absentNoop) {
|
|
223
|
+
return { config: { ...DEFAULT_CONFIG }, written: false };
|
|
224
|
+
}
|
|
225
|
+
const current = text === undefined ? { ...DEFAULT_CONFIG } : parseAndValidateConfigText(text, configPath);
|
|
226
|
+
const mutated = mutate(current);
|
|
227
|
+
if (mutated === current)
|
|
228
|
+
return { config: current, written: false };
|
|
229
|
+
const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
|
|
230
|
+
ensureMigrationBackupWithConfigLockHeld();
|
|
231
|
+
if (text !== undefined)
|
|
232
|
+
backupExistingConfig(configPath);
|
|
233
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
234
|
+
writeConfigAtomic(configPath, sanitizeConfigForWrite(next));
|
|
235
|
+
return { config: next, written: true };
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Mutate config while holding the write lock across one validated pre-commit
|
|
240
|
+
* side effect. Setup uses this to reject a three-way conflict before creating
|
|
241
|
+
* its stash, while preventing another config writer from racing the final save.
|
|
242
|
+
*/
|
|
243
|
+
export async function mutateConfigWithPrecommit(mutate, precommit) {
|
|
244
|
+
cachedConfig = undefined;
|
|
245
|
+
const configPath = getConfigPath();
|
|
246
|
+
const release = acquireConfigLock();
|
|
247
|
+
try {
|
|
248
|
+
const text = readConfigText(configPath);
|
|
249
|
+
const current = text === undefined ? { ...DEFAULT_CONFIG } : parseAndValidateConfigText(text, configPath);
|
|
250
|
+
const mutated = mutate(current);
|
|
251
|
+
const next = validateCompleteConfig({ ...mutated, configVersion: CURRENT_CONFIG_VERSION });
|
|
252
|
+
// Setup's pre-commit may initialize the stash. Prove the rebase and final
|
|
253
|
+
// config are valid, then require the recovery snapshots before that first
|
|
254
|
+
// external side effect.
|
|
255
|
+
ensureMigrationBackupWithConfigLockHeld();
|
|
256
|
+
if (text !== undefined)
|
|
257
|
+
backupExistingConfig(configPath);
|
|
258
|
+
const precommitResult = await precommit(next);
|
|
259
|
+
if (mutated === current)
|
|
260
|
+
return { config: current, written: false, precommit: precommitResult };
|
|
261
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
262
|
+
writeConfigAtomic(configPath, sanitizeConfigForWrite(next));
|
|
263
|
+
return { config: next, written: true, precommit: precommitResult };
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
release();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
328
269
|
/**
|
|
329
270
|
* Strip literal apiKey fields before writing config to disk.
|
|
330
271
|
* API keys are expected to come from environment variables
|
|
@@ -359,50 +300,36 @@ function sanitizeConfigForWrite(config) {
|
|
|
359
300
|
else if (config.embedding) {
|
|
360
301
|
sanitized.embedding = { ...config.embedding };
|
|
361
302
|
}
|
|
362
|
-
if (config.
|
|
363
|
-
const
|
|
364
|
-
for (const [name,
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
const { apiKey: _drop, ...rest } = profile;
|
|
371
|
-
llmProfiles[name] = rest;
|
|
372
|
-
if (profile.apiKey) {
|
|
373
|
-
const envVar = `AKM_PROFILE_${name.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
374
|
-
stripped.push(`profiles.llm.${name}.apiKey (set ${envVar} to provide at runtime)`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
llmProfiles[name] = { ...profile };
|
|
303
|
+
if (config.engines) {
|
|
304
|
+
const engines = {};
|
|
305
|
+
for (const [name, engine] of Object.entries(config.engines)) {
|
|
306
|
+
if (engine.kind !== "llm" || engine.apiKey === undefined || isEnvReference(engine.apiKey)) {
|
|
307
|
+
engines[name] = { ...engine };
|
|
308
|
+
continue;
|
|
380
309
|
}
|
|
310
|
+
const { apiKey: _drop, ...rest } = engine;
|
|
311
|
+
engines[name] = rest;
|
|
312
|
+
if (engine.apiKey)
|
|
313
|
+
stripped.push(`engines.${name}.apiKey`);
|
|
381
314
|
}
|
|
382
|
-
sanitized.
|
|
383
|
-
...(sanitized.profiles ?? {}),
|
|
384
|
-
llm: llmProfiles,
|
|
385
|
-
};
|
|
315
|
+
sanitized.engines = engines;
|
|
386
316
|
}
|
|
387
317
|
if (stripped.length > 0) {
|
|
388
318
|
warn(`Config sanitizer dropped API key(s) before writing to disk:\n - ${stripped.join("\n - ")}\n\nakm does not persist API keys to config.json. Set the listed environment variables to provide them at runtime, or use \`\${VAR}\` references in your config to defer lookup. See docs/data-and-telemetry.md.`);
|
|
389
319
|
}
|
|
390
320
|
return sanitized;
|
|
391
321
|
}
|
|
392
|
-
/** Matches
|
|
322
|
+
/** Matches the only 0.9 symbolic secret forms: `${VAR}` or `$VAR`. */
|
|
393
323
|
function isEnvReference(value) {
|
|
394
|
-
return /^\$\{[
|
|
324
|
+
return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$|^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
395
325
|
}
|
|
396
326
|
export function updateConfig(partial) {
|
|
397
|
-
|
|
398
|
-
const merged = mergeLoadedConfig(current, partial);
|
|
399
|
-
saveConfig(merged);
|
|
400
|
-
return merged;
|
|
327
|
+
return mutateConfig((current) => deepMergeConfig(current, partial)).config;
|
|
401
328
|
}
|
|
402
329
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
403
330
|
/**
|
|
404
|
-
* Resolve a single secret value by expanding `${VAR}` / `$VAR`
|
|
405
|
-
*
|
|
331
|
+
* Resolve a single secret value by expanding `${VAR}` / `$VAR` references
|
|
332
|
+
* against `process.env`. Use this at apiKey /
|
|
406
333
|
* authorization-header consumption sites (LLM client, embedder, agent SDK
|
|
407
334
|
* runner) — NOT on the load path. Non-string inputs pass through unchanged.
|
|
408
335
|
*
|
|
@@ -420,13 +347,8 @@ export function resolveSecret(value) {
|
|
|
420
347
|
return value;
|
|
421
348
|
if (!value.includes("$"))
|
|
422
349
|
return value;
|
|
423
|
-
return value.replace(/\$\{([
|
|
424
|
-
|
|
425
|
-
const [name, ...rest] = braced.split(":-");
|
|
426
|
-
const fallback = rest.join(":-");
|
|
427
|
-
return process.env[name] ?? fallback ?? "";
|
|
428
|
-
}
|
|
429
|
-
return process.env[bare] ?? "";
|
|
350
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
|
|
351
|
+
return process.env[(braced ?? bare)] ?? "";
|
|
430
352
|
});
|
|
431
353
|
}
|
|
432
354
|
/**
|
|
@@ -437,9 +359,10 @@ export function resolveSecret(value) {
|
|
|
437
359
|
/**
|
|
438
360
|
* Reserved well-known keys on IndexConfig that are NOT per-pass entries.
|
|
439
361
|
* `stalenessDetection` is retired (10-Q3) but stays reserved so a leftover
|
|
440
|
-
* config section is never misread as a pass entry.
|
|
362
|
+
* config section is never misread as a pass entry. `indexBodyOpening`
|
|
363
|
+
* (stash-conventions SPEC-8) is a boolean feature flag, not a pass section.
|
|
441
364
|
*/
|
|
442
|
-
const INDEX_RESERVED_KEYS = new Set(["metadataEnhance", "stalenessDetection"]);
|
|
365
|
+
const INDEX_RESERVED_KEYS = new Set(["metadataEnhance", "stalenessDetection", "indexBodyOpening"]);
|
|
443
366
|
export function getIndexPassConfig(config, passName) {
|
|
444
367
|
if (!config)
|
|
445
368
|
return undefined;
|
|
@@ -458,63 +381,6 @@ export { parseSourceSpec, resolveConfiguredSources } from "./config-sources.js";
|
|
|
458
381
|
* (current config + partial patch). Sub-objects with named records (profiles,
|
|
459
382
|
* defaults, etc.) shallow-merge; arrays override wholesale.
|
|
460
383
|
*/
|
|
461
|
-
function mergeLoadedConfig(base, override) {
|
|
462
|
-
if (!override)
|
|
463
|
-
return { ...base };
|
|
464
|
-
const merged = { ...base, ...override };
|
|
465
|
-
// Shallow-merge sub-objects so a partial update to e.g. `output.format`
|
|
466
|
-
// doesn't drop the existing `output.detail`.
|
|
467
|
-
for (const key of ["output", "embedding", "index", "defaults"]) {
|
|
468
|
-
if (base[key] && override[key]) {
|
|
469
|
-
// biome-ignore lint/suspicious/noExplicitAny: heterogeneous structural merge
|
|
470
|
-
merged[key] = { ...base[key], ...override[key] };
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
if (base.profiles && override.profiles) {
|
|
474
|
-
const next = { ...base.profiles };
|
|
475
|
-
for (const k of ["llm", "agent", "improve"]) {
|
|
476
|
-
const ovr = override.profiles[k];
|
|
477
|
-
if (ovr)
|
|
478
|
-
next[k] = { ...(next[k] ?? {}), ...ovr };
|
|
479
|
-
}
|
|
480
|
-
merged.profiles = next;
|
|
481
|
-
}
|
|
482
|
-
return merged;
|
|
483
|
-
}
|
|
484
|
-
function applyRuntimeEnvApiKeys(config) {
|
|
485
|
-
const next = { ...config };
|
|
486
|
-
if (next.embedding && !next.embedding.apiKey) {
|
|
487
|
-
const envKey = process.env.AKM_EMBED_API_KEY?.trim();
|
|
488
|
-
if (envKey)
|
|
489
|
-
next.embedding = { ...next.embedding, apiKey: envKey };
|
|
490
|
-
}
|
|
491
|
-
// LLM profile keys: AKM_LLM_API_KEY for the default profile, then
|
|
492
|
-
// AKM_PROFILE_<UPPER>_API_KEY for any profile (per-profile wins).
|
|
493
|
-
// Resolve the default profile the SAME way the rest of the config layer does
|
|
494
|
-
// (resolveDefaultLlmProfileName), so the implicit `profiles.llm.default`
|
|
495
|
-
// fallback is honored. Keying off the raw `defaults.llm` field alone silently
|
|
496
|
-
// dropped AKM_LLM_API_KEY for configs that rely on the implicit default —
|
|
497
|
-
// the same no-op-run class as the 2026-05-23 incident.
|
|
498
|
-
const defaultProfile = resolveDefaultLlmProfileName(next);
|
|
499
|
-
if (next.profiles?.llm) {
|
|
500
|
-
const updated = { ...next.profiles.llm };
|
|
501
|
-
let changed = false;
|
|
502
|
-
for (const [name, profile] of Object.entries(updated)) {
|
|
503
|
-
if (profile.apiKey)
|
|
504
|
-
continue;
|
|
505
|
-
const perProfile = process.env[`AKM_PROFILE_${name.toUpperCase().replace(/-/g, "_")}_API_KEY`]?.trim();
|
|
506
|
-
const fallback = name === defaultProfile ? process.env.AKM_LLM_API_KEY?.trim() : undefined;
|
|
507
|
-
const envKey = perProfile || fallback;
|
|
508
|
-
if (envKey) {
|
|
509
|
-
updated[name] = { ...profile, apiKey: envKey };
|
|
510
|
-
changed = true;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
if (changed)
|
|
514
|
-
next.profiles = { ...next.profiles, llm: updated };
|
|
515
|
-
}
|
|
516
|
-
return next;
|
|
517
|
-
}
|
|
518
384
|
/**
|
|
519
385
|
* Walk cwd-ancestors looking for `.akm/config.json`. If one is found, emit a
|
|
520
386
|
* one-time deprecation warning per path. The file's contents are NOT read —
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
5
|
+
export function isPlainObject(value) {
|
|
6
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
7
|
+
return false;
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return prototype === Object.prototype || prototype === null;
|
|
10
|
+
}
|
|
11
|
+
function copyConfigValue(value) {
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return value.map(copyConfigValue);
|
|
14
|
+
if (!isPlainObject(value))
|
|
15
|
+
return value;
|
|
16
|
+
const copy = {};
|
|
17
|
+
for (const key of Object.keys(value)) {
|
|
18
|
+
if (UNSAFE_KEYS.has(key))
|
|
19
|
+
throw new TypeError(`Unsafe configuration key: ${key}`);
|
|
20
|
+
copy[key] = copyConfigValue(value[key]);
|
|
21
|
+
}
|
|
22
|
+
return copy;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Merge configuration values without mutating either input. Plain objects merge
|
|
26
|
+
* recursively; arrays and scalar values replace the less-specific value.
|
|
27
|
+
*/
|
|
28
|
+
export function deepMergeConfig(base, override) {
|
|
29
|
+
const result = copyConfigValue(base);
|
|
30
|
+
for (const key of Object.keys(override)) {
|
|
31
|
+
if (UNSAFE_KEYS.has(key))
|
|
32
|
+
throw new TypeError(`Unsafe configuration key: ${key}`);
|
|
33
|
+
const next = override[key];
|
|
34
|
+
if (next === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
const current = result[key];
|
|
37
|
+
result[key] =
|
|
38
|
+
isPlainObject(current) && isPlainObject(next) ? deepMergeConfig(current, next) : copyConfigValue(next);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
export const ENGINE_NAME_PATTERN_SOURCE = "^(?!akm-)[a-z][a-z0-9]*(?:-[a-z0-9]+)*$";
|
|
5
|
+
export const BUILTIN_IMPROVE_STRATEGY_NAMES = [
|
|
6
|
+
"default",
|
|
7
|
+
"quick",
|
|
8
|
+
"thorough",
|
|
9
|
+
"memory-focus",
|
|
10
|
+
"graph-refresh",
|
|
11
|
+
"frequent",
|
|
12
|
+
"consolidate",
|
|
13
|
+
"catchup",
|
|
14
|
+
"synthesize",
|
|
15
|
+
"reflect-distill",
|
|
16
|
+
"proactive-maintenance",
|
|
17
|
+
"recombine-only",
|
|
18
|
+
];
|
|
19
|
+
/** Engine capability required by each configured improve process. `null` means engine-free. */
|
|
20
|
+
export const IMPROVE_PROCESS_ENGINE_CAPABILITIES = {
|
|
21
|
+
reflect: "llm",
|
|
22
|
+
distill: "llm",
|
|
23
|
+
consolidate: "llm",
|
|
24
|
+
memoryInference: "llm",
|
|
25
|
+
graphExtraction: "llm",
|
|
26
|
+
extract: "llm",
|
|
27
|
+
validation: "llm",
|
|
28
|
+
triage: "runner",
|
|
29
|
+
proactiveMaintenance: null,
|
|
30
|
+
recombine: "llm",
|
|
31
|
+
procedural: "llm",
|
|
32
|
+
};
|
package/dist/core/errors.js
CHANGED
|
@@ -10,11 +10,11 @@ const CONFIG_HINTS = {
|
|
|
10
10
|
STASH_DIR_NOT_A_DIRECTORY: "The configured stashDir exists but isn't a directory. Update stashDir to point at a folder.",
|
|
11
11
|
STASH_DIR_UNREADABLE: "Check the path exists and your user has read permission, or update stashDir.",
|
|
12
12
|
EMBEDDING_NOT_CONFIGURED: 'Run `akm config set embedding \'{"endpoint":"...","model":"..."}\'` to enable embeddings.',
|
|
13
|
-
LLM_NOT_CONFIGURED: 'Run `akm setup` or `
|
|
13
|
+
LLM_NOT_CONFIGURED: 'Run `akm setup` or configure an `engines` entry with `kind: "llm"`, then select it with `defaults.llmEngine`.',
|
|
14
14
|
TEST_ISOLATION_MISSING: "Under bun test, when AKM_STASH_DIR is set you MUST also set XDG_DATA_HOME (or AKM_DATA_DIR) and XDG_STATE_HOME (or AKM_STATE_DIR) to temp directories so the test does not touch the developer's real ~/.local/share/akm or ~/.local/state/akm.",
|
|
15
15
|
SETUP_TMP_STASH_REFUSED: "Use a persistent directory, or set AKM_FORCE_SETUP_TMP_STASH=1 to opt in to a sandboxed setup (setup also pre-sets AKM_STASH_DIR so config and cache writes auto-isolate into $stashDir/.akm/ — host config is preserved).",
|
|
16
16
|
UNSAFE_STASH_DIR: "Choose a path inside your home directory (e.g. ~/akm) or another empty workspace. The stash directory cannot be the filesystem root, your home directory itself, or a sensitive system path like /etc, /var, ~/.config, or ~/.ssh.",
|
|
17
|
-
|
|
17
|
+
UNKNOWN_IMPROVE_STRATEGY: "Pass one of the listed strategy names to `--strategy`, or define it under `improve.strategies`. Names are case-sensitive.",
|
|
18
18
|
};
|
|
19
19
|
/** Default hint for each UsageError code. */
|
|
20
20
|
const USAGE_HINTS = {
|