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
package/dist/setup/setup.js
CHANGED
|
@@ -9,19 +9,20 @@
|
|
|
9
9
|
* Collects all choices and writes config once at the end.
|
|
10
10
|
*
|
|
11
11
|
* This module holds the wizard orchestration; the individual wizard steps,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* engine writers, prompt shims, provider table, and semantic-asset preparation
|
|
13
|
+
* live in sibling modules (`steps/*`, `engine-config`, `prompt`,
|
|
14
14
|
* `providers`, `semantic-assets`).
|
|
15
15
|
*/
|
|
16
16
|
import { promises as dnsPromises } from "node:dns";
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import path from "node:path";
|
|
20
|
+
import { isDeepStrictEqual } from "node:util";
|
|
20
21
|
import * as p from "../cli/clack.js";
|
|
21
22
|
import { akmInit } from "../commands/sources/init.js";
|
|
22
|
-
import { DEFAULT_CONFIG,
|
|
23
|
-
import {
|
|
24
|
-
import { deepMergeConfig } from "../core/deep-merge.js";
|
|
23
|
+
import { DEFAULT_CONFIG, loadUserConfig, mutateConfigWithPrecommit, parseAndValidateConfigText, validateCompleteConfig, } from "../core/config/config.js";
|
|
24
|
+
import { readConfigText } from "../core/config/config-io.js";
|
|
25
|
+
import { deepMergeConfig } from "../core/config/deep-merge.js";
|
|
25
26
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
26
27
|
import { getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
|
|
27
28
|
import { warn } from "../core/warn.js";
|
|
@@ -30,10 +31,10 @@ import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticSt
|
|
|
30
31
|
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent/index.js";
|
|
31
32
|
import { defaultProfileName } from "../integrations/harnesses/index.js";
|
|
32
33
|
import { probeLlmCapabilities } from "../llm/client.js";
|
|
33
|
-
import { getOutputMode } from "../output/context.js";
|
|
34
34
|
import { detectEnvironment, detectLMStudio, renderDetectionSummary, } from "./detect.js";
|
|
35
|
+
import { upsertDetectedAgentEngine, upsertDetectedLlmEngine, verifyOpenAiCompatibleEndpoint } from "./detected-engines.js";
|
|
36
|
+
import { readCurrentLlmEngine, writeAgentEngines, writeLlmEngine } from "./engine-config.js";
|
|
35
37
|
import { detectHarnessConfigs } from "./harness-config-import.js";
|
|
36
|
-
import { applyLegacyAgent, applyLegacyLlm } from "./legacy-config.js";
|
|
37
38
|
import { bail, prompt } from "./prompt.js";
|
|
38
39
|
import { PROVIDER_DEFAULTS } from "./providers.js";
|
|
39
40
|
import { prepareSemanticSearchAssets } from "./semantic-assets.js";
|
|
@@ -95,6 +96,110 @@ function applyStashIsolationToEnv(stashDir, dirExplicitlyProvided) {
|
|
|
95
96
|
process.env.AKM_STASH_DIR = stashDir;
|
|
96
97
|
}
|
|
97
98
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
99
|
+
/** Raw preflight used before setup performs prompts, initialization, or writes. */
|
|
100
|
+
export function assertSetupConfigPreflight() {
|
|
101
|
+
const configPath = getConfigPath();
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = readConfigText(configPath);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new ConfigError(`Could not read config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
|
|
108
|
+
}
|
|
109
|
+
if (text !== undefined)
|
|
110
|
+
parseAndValidateConfigText(text, configPath);
|
|
111
|
+
}
|
|
112
|
+
function isPlainRecord(value) {
|
|
113
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
114
|
+
return false;
|
|
115
|
+
const prototype = Object.getPrototypeOf(value);
|
|
116
|
+
return prototype === Object.prototype || prototype === null;
|
|
117
|
+
}
|
|
118
|
+
function sameConfigValue(left, right) {
|
|
119
|
+
return isDeepStrictEqual(left, right);
|
|
120
|
+
}
|
|
121
|
+
function configArrayItemKey(value) {
|
|
122
|
+
if (!isPlainRecord(value))
|
|
123
|
+
return `value:${JSON.stringify(value)}`;
|
|
124
|
+
if (typeof value.id === "string")
|
|
125
|
+
return `id:${value.id}`;
|
|
126
|
+
if (typeof value.type === "string" && typeof value.url === "string")
|
|
127
|
+
return `source:${value.type}:url:${value.url}`;
|
|
128
|
+
if (typeof value.type === "string" && typeof value.path === "string")
|
|
129
|
+
return `source:${value.type}:path:${path.resolve(value.path)}`;
|
|
130
|
+
if (typeof value.url === "string")
|
|
131
|
+
return `url:${value.url}`;
|
|
132
|
+
if (typeof value.name === "string")
|
|
133
|
+
return `name:${value.name}`;
|
|
134
|
+
return `value:${JSON.stringify(value)}`;
|
|
135
|
+
}
|
|
136
|
+
function setupConflict(pathParts) {
|
|
137
|
+
const field = pathParts.length > 0 ? pathParts.join(".") : "(root)";
|
|
138
|
+
throw new ConfigError(`Setup config conflict at ${field}: another process changed the same field after setup started. Rerun setup against the latest config.`, "INVALID_CONFIG_FILE");
|
|
139
|
+
}
|
|
140
|
+
/** Reapply setup's changes while rejecting concurrent edits to the same field. */
|
|
141
|
+
export function rebaseSetupChanges(original, desired, latest, pathParts = []) {
|
|
142
|
+
if (sameConfigValue(original, desired))
|
|
143
|
+
return latest;
|
|
144
|
+
if (Array.isArray(original) && Array.isArray(desired)) {
|
|
145
|
+
if (latest !== undefined && !Array.isArray(latest))
|
|
146
|
+
setupConflict(pathParts);
|
|
147
|
+
const latestItems = Array.isArray(latest) ? latest : [];
|
|
148
|
+
const originalByKey = new Map(original.map((item) => [configArrayItemKey(item), item]));
|
|
149
|
+
const desiredByKey = new Map(desired.map((item) => [configArrayItemKey(item), item]));
|
|
150
|
+
const latestByKey = new Map(latestItems.map((item) => [configArrayItemKey(item), item]));
|
|
151
|
+
const result = [];
|
|
152
|
+
for (const [key, desiredItem] of desiredByKey) {
|
|
153
|
+
const originalItem = originalByKey.get(key);
|
|
154
|
+
const latestItem = latestByKey.get(key);
|
|
155
|
+
if (originalByKey.has(key)) {
|
|
156
|
+
if (!latestByKey.has(key))
|
|
157
|
+
setupConflict([...pathParts, key]);
|
|
158
|
+
result.push(rebaseSetupChanges(originalItem, desiredItem, latestItem, [...pathParts, key]));
|
|
159
|
+
}
|
|
160
|
+
else if (!latestByKey.has(key) || sameConfigValue(latestItem, desiredItem)) {
|
|
161
|
+
result.push(desiredItem);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
setupConflict([...pathParts, key]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const [key, originalItem] of originalByKey) {
|
|
168
|
+
if (desiredByKey.has(key) || !latestByKey.has(key))
|
|
169
|
+
continue;
|
|
170
|
+
if (!sameConfigValue(latestByKey.get(key), originalItem))
|
|
171
|
+
setupConflict([...pathParts, key]);
|
|
172
|
+
}
|
|
173
|
+
for (const item of latestItems) {
|
|
174
|
+
const key = configArrayItemKey(item);
|
|
175
|
+
if (!originalByKey.has(key) && !desiredByKey.has(key))
|
|
176
|
+
result.push(item);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
if (!isPlainRecord(original) || !isPlainRecord(desired)) {
|
|
181
|
+
if (!sameConfigValue(latest, original) && !sameConfigValue(latest, desired))
|
|
182
|
+
setupConflict(pathParts);
|
|
183
|
+
return desired;
|
|
184
|
+
}
|
|
185
|
+
if (latest !== undefined && !isPlainRecord(latest))
|
|
186
|
+
setupConflict(pathParts);
|
|
187
|
+
const result = isPlainRecord(latest) ? { ...latest } : {};
|
|
188
|
+
for (const key of new Set([...Object.keys(original), ...Object.keys(desired)])) {
|
|
189
|
+
if (!Object.hasOwn(desired, key)) {
|
|
190
|
+
if (Object.hasOwn(result, key) && !sameConfigValue(result[key], original[key]))
|
|
191
|
+
setupConflict([...pathParts, key]);
|
|
192
|
+
delete result[key];
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
result[key] = rebaseSetupChanges(original[key], desired[key], result[key], [...pathParts, key]);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
async function saveSetupConfig(original, desired, precommit) {
|
|
200
|
+
const result = await mutateConfigWithPrecommit((latest) => rebaseSetupChanges(original, desired, latest), precommit);
|
|
201
|
+
return { config: result.config, precommit: result.precommit };
|
|
202
|
+
}
|
|
98
203
|
/**
|
|
99
204
|
* Quick connectivity check. Returns true if we can resolve a hostname
|
|
100
205
|
* the user has already implicitly trusted within 3 seconds, false
|
|
@@ -178,7 +283,7 @@ export function buildSetupSteps(options) {
|
|
|
178
283
|
return;
|
|
179
284
|
}
|
|
180
285
|
const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels, lmStudioResult, harnessConfigs);
|
|
181
|
-
ctx.apply(
|
|
286
|
+
ctx.apply(writeLlmEngine(ctx.config, llm));
|
|
182
287
|
},
|
|
183
288
|
},
|
|
184
289
|
{
|
|
@@ -226,11 +331,10 @@ export function buildSetupSteps(options) {
|
|
|
226
331
|
else {
|
|
227
332
|
p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
|
|
228
333
|
}
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
const synthConfig = { ...ctx.config, ...applyLegacyAgent(ctx.config, result.agent) };
|
|
334
|
+
// Apply detected engines to a synthetic config for the selection UI.
|
|
335
|
+
const synthConfig = deepMergeConfig(ctx.config, writeAgentEngines(ctx.config, result.agent));
|
|
232
336
|
const agent = await stepAgentSelection(synthConfig, result.detections);
|
|
233
|
-
ctx.apply(
|
|
337
|
+
ctx.apply(writeAgentEngines(ctx.config, agent));
|
|
234
338
|
},
|
|
235
339
|
},
|
|
236
340
|
{
|
|
@@ -255,6 +359,7 @@ export function buildSetupSteps(options) {
|
|
|
255
359
|
return { steps, outcome };
|
|
256
360
|
}
|
|
257
361
|
export async function runSetupWizard(opts) {
|
|
362
|
+
assertSetupConfigPreflight();
|
|
258
363
|
p.intro("akm setup");
|
|
259
364
|
const current = loadUserConfig();
|
|
260
365
|
const configPath = getConfigPath();
|
|
@@ -264,11 +369,6 @@ export async function runSetupWizard(opts) {
|
|
|
264
369
|
// config from being clobbered with a stashDir that the OS may reap.
|
|
265
370
|
assertSetupSandbox(resolvedStashDir, opts?.dir != null);
|
|
266
371
|
applyStashIsolationToEnv(resolvedStashDir, opts?.dir != null);
|
|
267
|
-
// Bootstrap directory structure before any prompts so the stash exists
|
|
268
|
-
// even if the wizard is interrupted after this point.
|
|
269
|
-
if (!opts?.noInit) {
|
|
270
|
-
await akmInit({ dir: resolvedStashDir, setDefault: true });
|
|
271
|
-
}
|
|
272
372
|
// Quick connectivity check — skip network-dependent steps when offline
|
|
273
373
|
const online = await isOnline();
|
|
274
374
|
if (!online) {
|
|
@@ -313,11 +413,11 @@ export async function runSetupWizard(opts) {
|
|
|
313
413
|
// Step 1/2: Small model connection (for enrichment features)
|
|
314
414
|
const smallModelResult = await stepSmallModelConnection(ctx.config);
|
|
315
415
|
if (!smallModelResult.skipped) {
|
|
316
|
-
ctx.apply(
|
|
416
|
+
ctx.apply(writeLlmEngine(ctx.config, smallModelResult.llm));
|
|
317
417
|
}
|
|
318
418
|
// Step 2/2: Agent connection (for agentic features)
|
|
319
419
|
const agentConfig = await stepAgentConnection(ctx.config, smallModelResult);
|
|
320
|
-
ctx.apply(
|
|
420
|
+
ctx.apply(writeAgentEngines(ctx.config, agentConfig));
|
|
321
421
|
const newConfig = {
|
|
322
422
|
...ctx.config,
|
|
323
423
|
// Preserve fields the steps don't manage explicitly.
|
|
@@ -326,7 +426,7 @@ export async function runSetupWizard(opts) {
|
|
|
326
426
|
const semanticSearchMode = outcome.semantic;
|
|
327
427
|
const stashDir = newConfig.stashDir ?? current.stashDir ?? getDefaultStashDir();
|
|
328
428
|
const embedding = newConfig.embedding;
|
|
329
|
-
const llm =
|
|
429
|
+
const llm = readCurrentLlmEngine(newConfig);
|
|
330
430
|
const registries = newConfig.registries;
|
|
331
431
|
const allStashes = newConfig.sources ?? [];
|
|
332
432
|
// Feature capability summary
|
|
@@ -341,7 +441,7 @@ export async function runSetupWizard(opts) {
|
|
|
341
441
|
`Semantic search: ${semanticSearchMode.mode}`,
|
|
342
442
|
`Registries: ${effectiveRegistries.filter((r) => r.enabled !== false).length} enabled`,
|
|
343
443
|
`Stash sources: ${allStashes.length}`,
|
|
344
|
-
`Agent default: ${newConfig.defaults?.
|
|
444
|
+
`Agent default: ${newConfig.defaults?.engine ?? "disabled"}`,
|
|
345
445
|
`Output: ${newConfig.output?.format ?? "json"} / ${newConfig.output?.detail ?? "brief"}`,
|
|
346
446
|
].join("\n"), "Configuration Summary");
|
|
347
447
|
const shouldSave = await prompt(() => p.confirm({
|
|
@@ -350,22 +450,23 @@ export async function runSetupWizard(opts) {
|
|
|
350
450
|
}));
|
|
351
451
|
if (!shouldSave)
|
|
352
452
|
bail();
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
453
|
+
validateCompleteConfig(newConfig);
|
|
454
|
+
const { config: savedConfig } = await saveSetupConfig(current, newConfig, async () => {
|
|
455
|
+
if (!opts?.noInit)
|
|
456
|
+
await akmInit({ dir: resolvedStashDir, setDefault: true, persistConfig: false });
|
|
457
|
+
});
|
|
357
458
|
if (semanticSearchMode.mode === "off") {
|
|
358
459
|
clearSemanticStatus();
|
|
359
460
|
}
|
|
360
461
|
if (semanticSearchMode.mode === "auto") {
|
|
361
462
|
if (semanticSearchMode.prepareAssets) {
|
|
362
|
-
const ready = await prepareSemanticSearchAssets(
|
|
463
|
+
const ready = await prepareSemanticSearchAssets(savedConfig);
|
|
363
464
|
if (!ready.ok) {
|
|
364
465
|
writeSemanticStatus({
|
|
365
466
|
status: "blocked",
|
|
366
467
|
reason: ready.reason,
|
|
367
468
|
message: ready.message,
|
|
368
|
-
providerFingerprint: deriveSemanticProviderFingerprint(
|
|
469
|
+
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
369
470
|
lastCheckedAt: new Date().toISOString(),
|
|
370
471
|
});
|
|
371
472
|
p.log.warn("Semantic search remains set to auto, but is currently blocked. Re-run `akm index --full --verbose` once the issue is resolved.");
|
|
@@ -374,7 +475,7 @@ export async function runSetupWizard(opts) {
|
|
|
374
475
|
writeSemanticStatus({
|
|
375
476
|
status: "pending",
|
|
376
477
|
message: "Semantic prerequisites verified. Building the index to finish activation.",
|
|
377
|
-
providerFingerprint: deriveSemanticProviderFingerprint(
|
|
478
|
+
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
378
479
|
lastCheckedAt: new Date().toISOString(),
|
|
379
480
|
});
|
|
380
481
|
}
|
|
@@ -383,7 +484,7 @@ export async function runSetupWizard(opts) {
|
|
|
383
484
|
writeSemanticStatus({
|
|
384
485
|
status: "pending",
|
|
385
486
|
message: "Semantic search is enabled, but asset preparation was skipped.",
|
|
386
|
-
providerFingerprint: deriveSemanticProviderFingerprint(
|
|
487
|
+
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
387
488
|
lastCheckedAt: new Date().toISOString(),
|
|
388
489
|
});
|
|
389
490
|
p.log.info("Semantic search is set to auto, but asset preparation was skipped. Run `akm index --full --verbose` later to verify it.");
|
|
@@ -396,7 +497,7 @@ export async function runSetupWizard(opts) {
|
|
|
396
497
|
try {
|
|
397
498
|
const indexResult = await akmIndex({ stashDir });
|
|
398
499
|
spin.stop(`Indexed ${indexResult.totalEntries} assets.`);
|
|
399
|
-
if (
|
|
500
|
+
if (savedConfig.semanticSearchMode === "auto") {
|
|
400
501
|
if (indexResult.verification.ok) {
|
|
401
502
|
p.log.success(indexResult.verification.message);
|
|
402
503
|
}
|
|
@@ -411,12 +512,12 @@ export async function runSetupWizard(opts) {
|
|
|
411
512
|
catch (err) {
|
|
412
513
|
spin.stop("Indexing failed — you can run `akm index` manually later.");
|
|
413
514
|
p.log.warn(String(err));
|
|
414
|
-
if (
|
|
515
|
+
if (savedConfig.semanticSearchMode === "auto") {
|
|
415
516
|
writeSemanticStatus({
|
|
416
517
|
status: "blocked",
|
|
417
518
|
reason: "index-failed",
|
|
418
519
|
message: String(err),
|
|
419
|
-
providerFingerprint: deriveSemanticProviderFingerprint(
|
|
520
|
+
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
420
521
|
lastCheckedAt: new Date().toISOString(),
|
|
421
522
|
});
|
|
422
523
|
}
|
|
@@ -436,54 +537,21 @@ export async function runSetupWizard(opts) {
|
|
|
436
537
|
p.outro(`Configuration saved to ${configPath}`);
|
|
437
538
|
}
|
|
438
539
|
// ── Non-interactive / scripting entry points ─────────────────────────────────
|
|
439
|
-
/**
|
|
440
|
-
* Back up an existing config file and print the real, timestamped backup
|
|
441
|
-
* location (not a generic display string). On a fresh install where there is
|
|
442
|
-
* nothing to back up, print a "nothing to back up" notice instead.
|
|
443
|
-
*/
|
|
444
|
-
function backupAndAnnounce(configPath) {
|
|
445
|
-
const result = backupExistingConfig(configPath);
|
|
446
|
-
const message = result ? `Config backed up to ${result.timestamped}` : "No existing config to back up.";
|
|
447
|
-
// In JSON output mode the structured envelope (which already carries
|
|
448
|
-
// `configPath`) MUST be the only thing on stdout — `setup --yes | jq` is a
|
|
449
|
-
// supported scripting contract. @clack's `p.log.info` writes to stdout, which
|
|
450
|
-
// would corrupt that envelope, so route this human-progress notice to stderr
|
|
451
|
-
// when emitting JSON. Interactive/text runs keep the inline clack banner.
|
|
452
|
-
if (isJsonOutputMode()) {
|
|
453
|
-
process.stderr.write(`${message}\n`);
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
p.log.info(message);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* True when the process-level output mode is JSON (the default machine format).
|
|
461
|
-
* Defensive: setup is also invoked programmatically (tests) where the output
|
|
462
|
-
* mode singleton may not be initialized — treat that as "not JSON" so the
|
|
463
|
-
* human-readable clack banner is used.
|
|
464
|
-
*/
|
|
465
|
-
function isJsonOutputMode() {
|
|
466
|
-
try {
|
|
467
|
-
return getOutputMode().format === "json";
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
540
|
/**
|
|
474
541
|
* Run setup in non-interactive mode, applying all defaults.
|
|
475
542
|
* Safe to call from CI or scripts. Idempotent — re-running produces the same result.
|
|
476
543
|
*/
|
|
477
544
|
export async function runSetupWithDefaults(opts) {
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
let initResult;
|
|
484
|
-
if (!opts.noInit) {
|
|
485
|
-
initResult = await akmInit({ dir: stashDir, setDefault: true });
|
|
545
|
+
assertSetupConfigPreflight();
|
|
546
|
+
const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : undefined;
|
|
547
|
+
if (explicitStashDir) {
|
|
548
|
+
assertSetupSandbox(explicitStashDir, true);
|
|
549
|
+
applyStashIsolationToEnv(explicitStashDir, true);
|
|
486
550
|
}
|
|
551
|
+
const current = loadUserConfig();
|
|
552
|
+
const stashDir = explicitStashDir ?? current.stashDir ?? getDefaultStashDir();
|
|
553
|
+
assertSetupSandbox(stashDir, explicitStashDir != null);
|
|
554
|
+
applyStashIsolationToEnv(stashDir, explicitStashDir != null);
|
|
487
555
|
// Run steps in non-interactive mode (applies defaults, skips prompts)
|
|
488
556
|
const ctx = createSetupContext(current, { nonInteractive: true });
|
|
489
557
|
const { steps } = buildSetupSteps({
|
|
@@ -498,12 +566,12 @@ export async function runSetupWithDefaults(opts) {
|
|
|
498
566
|
// Aggregate environment detection — apply detected values directly.
|
|
499
567
|
const env = await detectEnvironment({ existingStashDir: ctx.config.stashDir });
|
|
500
568
|
// Apply a detected LLM (live local server) when the config has none yet.
|
|
501
|
-
if (!
|
|
569
|
+
if (!readCurrentLlmEngine(ctx.config) && opts.probe) {
|
|
502
570
|
const liveLocal = env.localServers.find((s) => s.available && s.defaultModel);
|
|
503
571
|
if (liveLocal?.defaultModel) {
|
|
504
572
|
const llm = {
|
|
505
573
|
provider: "local",
|
|
506
|
-
endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1`,
|
|
574
|
+
endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1/chat/completions`,
|
|
507
575
|
model: liveLocal.defaultModel,
|
|
508
576
|
};
|
|
509
577
|
// A required field being unresolvable must fail loudly rather than write
|
|
@@ -511,11 +579,22 @@ export async function runSetupWithDefaults(opts) {
|
|
|
511
579
|
if (!llm.endpoint?.trim() || !llm.model?.trim()) {
|
|
512
580
|
throw new UsageError("Detected a local LLM server but could not resolve a required field (endpoint/model). Re-run `akm setup` interactively.", "MISSING_REQUIRED_ARGUMENT");
|
|
513
581
|
}
|
|
514
|
-
|
|
582
|
+
const verified = await verifyOpenAiCompatibleEndpoint(llm);
|
|
583
|
+
if (verified.ok) {
|
|
584
|
+
const applied = upsertDetectedLlmEngine(ctx.config, {
|
|
585
|
+
provider: llm.provider ?? "local",
|
|
586
|
+
endpoint: verified.endpoint,
|
|
587
|
+
model: llm.model,
|
|
588
|
+
});
|
|
589
|
+
ctx.apply(applied.config);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
warn(`[akm setup] Skipping detected local LLM: ${verified.reason}. Verify it and rerun setup.`);
|
|
593
|
+
}
|
|
515
594
|
}
|
|
516
595
|
}
|
|
517
596
|
// Auto-detect agent CLI if not already configured
|
|
518
|
-
if (!ctx.config.defaults?.
|
|
597
|
+
if (!ctx.config.defaults?.engine) {
|
|
519
598
|
let defaultProfile;
|
|
520
599
|
if (env.harness !== "none") {
|
|
521
600
|
defaultProfile = env.harness;
|
|
@@ -525,12 +604,15 @@ export async function runSetupWithDefaults(opts) {
|
|
|
525
604
|
defaultProfile = pickDefaultAgentProfile(detected, undefined);
|
|
526
605
|
}
|
|
527
606
|
if (defaultProfile) {
|
|
528
|
-
ctx.apply(
|
|
607
|
+
ctx.apply(upsertDetectedAgentEngine(ctx.config, defaultProfile).config);
|
|
529
608
|
}
|
|
530
609
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
610
|
+
validateCompleteConfig(ctx.config);
|
|
611
|
+
const { precommit: initResult } = await saveSetupConfig(current, ctx.config, async () => {
|
|
612
|
+
if (opts.noInit)
|
|
613
|
+
return undefined;
|
|
614
|
+
return akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
|
|
615
|
+
});
|
|
534
616
|
return {
|
|
535
617
|
configPath: getConfigPath(),
|
|
536
618
|
stashDir,
|
|
@@ -548,8 +630,7 @@ export async function runSetupWithDefaults(opts) {
|
|
|
548
630
|
* value.
|
|
549
631
|
*/
|
|
550
632
|
export async function runDetectOnly() {
|
|
551
|
-
|
|
552
|
-
return detectEnvironment({ existingStashDir: current.stashDir });
|
|
633
|
+
return detectEnvironment();
|
|
553
634
|
}
|
|
554
635
|
/**
|
|
555
636
|
* Derive opinionated defaults from a detection result.
|
|
@@ -590,6 +671,7 @@ export function deriveRecommendedConfig(env) {
|
|
|
590
671
|
const defaults = PROVIDER_DEFAULTS[cloud.provider];
|
|
591
672
|
if (defaults) {
|
|
592
673
|
result.llm = { provider: cloud.provider, endpoint: defaults.endpoint, model: defaults.model };
|
|
674
|
+
result.llmApiKeyEnvVar = cloud.envVar;
|
|
593
675
|
}
|
|
594
676
|
}
|
|
595
677
|
}
|
|
@@ -603,16 +685,47 @@ export function deriveRecommendedConfig(env) {
|
|
|
603
685
|
* (follows #511 semantics).
|
|
604
686
|
*/
|
|
605
687
|
export async function runResetRecommended(opts) {
|
|
688
|
+
assertSetupConfigPreflight();
|
|
689
|
+
const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : undefined;
|
|
690
|
+
if (explicitStashDir) {
|
|
691
|
+
assertSetupSandbox(explicitStashDir, true);
|
|
692
|
+
applyStashIsolationToEnv(explicitStashDir, true);
|
|
693
|
+
}
|
|
606
694
|
const current = loadUserConfig();
|
|
607
695
|
const env = await detectEnvironment({ existingStashDir: current.stashDir });
|
|
608
696
|
const recommended = deriveRecommendedConfig(env);
|
|
609
|
-
|
|
610
|
-
if (recommended.llm)
|
|
611
|
-
|
|
697
|
+
let incoming = {};
|
|
698
|
+
if (recommended.llm && recommended.llm.provider !== "anthropic") {
|
|
699
|
+
let endpoint = recommended.llm.endpoint;
|
|
700
|
+
let accepted = true;
|
|
701
|
+
if (opts.probe) {
|
|
702
|
+
const verified = await verifyOpenAiCompatibleEndpoint({
|
|
703
|
+
endpoint,
|
|
704
|
+
model: recommended.llm.model,
|
|
705
|
+
apiKeyEnvVar: recommended.llmApiKeyEnvVar,
|
|
706
|
+
});
|
|
707
|
+
if (verified.ok) {
|
|
708
|
+
endpoint = verified.endpoint;
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
accepted = false;
|
|
712
|
+
warn(`[akm setup] Skipping detected LLM: ${verified.reason}. Verify it and rerun setup.`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (accepted) {
|
|
716
|
+
incoming = upsertDetectedLlmEngine(incoming, {
|
|
717
|
+
provider: recommended.llm.provider ?? "local",
|
|
718
|
+
endpoint,
|
|
719
|
+
model: recommended.llm.model,
|
|
720
|
+
apiKeyEnvVar: recommended.llmApiKeyEnvVar,
|
|
721
|
+
}).config;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
612
724
|
if (recommended.embedding)
|
|
613
725
|
incoming.embedding = recommended.embedding;
|
|
614
|
-
if (recommended.agentDefault)
|
|
615
|
-
incoming
|
|
726
|
+
if (recommended.agentDefault) {
|
|
727
|
+
incoming = deepMergeConfig(incoming, writeAgentEngines(incoming, { default: recommended.agentDefault }));
|
|
728
|
+
}
|
|
616
729
|
if (recommended.taskSchedules) {
|
|
617
730
|
incoming.setup = { taskSchedules: recommended.taskSchedules };
|
|
618
731
|
}
|
|
@@ -628,6 +741,8 @@ export async function runResetRecommended(opts) {
|
|
|
628
741
|
* Validates required sub-fields and strips unknown/restricted keys.
|
|
629
742
|
*/
|
|
630
743
|
export async function runSetupFromConfig(opts) {
|
|
744
|
+
assertSetupConfigPreflight();
|
|
745
|
+
// Phase 1: Parse JSON
|
|
631
746
|
let incoming;
|
|
632
747
|
try {
|
|
633
748
|
incoming = JSON.parse(opts.configJson);
|
|
@@ -637,13 +752,18 @@ export async function runSetupFromConfig(opts) {
|
|
|
637
752
|
}
|
|
638
753
|
// Phase 2: Validate — only allow safe top-level keys
|
|
639
754
|
const ALLOWED_KEYS = new Set([
|
|
755
|
+
"configVersion",
|
|
756
|
+
"engines",
|
|
757
|
+
"defaults",
|
|
758
|
+
"improve",
|
|
759
|
+
"modelAliases",
|
|
640
760
|
"stashDir",
|
|
641
|
-
"llm",
|
|
642
761
|
"embedding",
|
|
643
|
-
"agent",
|
|
644
762
|
"semanticSearchMode",
|
|
645
763
|
"output",
|
|
646
|
-
"
|
|
764
|
+
"sources",
|
|
765
|
+
"registries",
|
|
766
|
+
"defaultWriteTarget",
|
|
647
767
|
"defaults",
|
|
648
768
|
"setup",
|
|
649
769
|
]);
|
|
@@ -654,12 +774,6 @@ export async function runSetupFromConfig(opts) {
|
|
|
654
774
|
}
|
|
655
775
|
}
|
|
656
776
|
// Validate required sub-fields
|
|
657
|
-
if (incoming.llm) {
|
|
658
|
-
if (!incoming.llm.endpoint?.trim())
|
|
659
|
-
throw new Error("llm.endpoint is required when llm is provided");
|
|
660
|
-
if (!incoming.llm.model?.trim())
|
|
661
|
-
throw new Error("llm.model is required when llm is provided");
|
|
662
|
-
}
|
|
663
777
|
if (incoming.embedding) {
|
|
664
778
|
if (!incoming.embedding.endpoint?.trim())
|
|
665
779
|
throw new Error("embedding.endpoint is required when embedding is provided");
|
|
@@ -667,34 +781,24 @@ export async function runSetupFromConfig(opts) {
|
|
|
667
781
|
throw new Error("embedding.model is required when embedding is provided");
|
|
668
782
|
}
|
|
669
783
|
// Phase 3: Merge with existing config
|
|
784
|
+
const explicitStashDir = opts.dir != null ? path.resolve(opts.dir) : incoming.stashDir != null ? path.resolve(incoming.stashDir) : undefined;
|
|
785
|
+
if (explicitStashDir) {
|
|
786
|
+
assertSetupSandbox(explicitStashDir, true);
|
|
787
|
+
applyStashIsolationToEnv(explicitStashDir, true);
|
|
788
|
+
}
|
|
670
789
|
const current = loadUserConfig();
|
|
671
|
-
const stashDir =
|
|
672
|
-
|
|
673
|
-
: incoming.stashDir
|
|
674
|
-
? path.resolve(incoming.stashDir)
|
|
675
|
-
: (current.stashDir ?? getDefaultStashDir());
|
|
676
|
-
const stashDirExplicit = opts.dir != null || incoming.stashDir != null;
|
|
790
|
+
const stashDir = explicitStashDir ?? current.stashDir ?? getDefaultStashDir();
|
|
791
|
+
const stashDirExplicit = explicitStashDir != null;
|
|
677
792
|
assertSetupSandbox(stashDir, stashDirExplicit);
|
|
678
793
|
applyStashIsolationToEnv(stashDir, stashDirExplicit);
|
|
679
|
-
let merged =
|
|
680
|
-
|
|
794
|
+
let merged = deepMergeConfig(current, {
|
|
795
|
+
...incoming,
|
|
796
|
+
stashDir,
|
|
797
|
+
});
|
|
798
|
+
// Deep-merge canonical keys: nested objects merge key-by-key so a
|
|
681
799
|
// partial `--file` only updates the keys it carries and never drops sibling
|
|
682
800
|
// subkeys (e.g. output.detail survives an output.format-only file). Arrays
|
|
683
801
|
// and scalars replace wholesale.
|
|
684
|
-
for (const key of Object.keys(incoming)) {
|
|
685
|
-
if (key === "llm" || key === "agent")
|
|
686
|
-
continue;
|
|
687
|
-
const incomingVal = incoming[key];
|
|
688
|
-
const mergedRec = merged;
|
|
689
|
-
mergedRec[key] = deepMergeConfig(mergedRec[key], incomingVal);
|
|
690
|
-
}
|
|
691
|
-
// Translate legacy llm/agent inputs into the new shape.
|
|
692
|
-
if (incoming.llm) {
|
|
693
|
-
merged = { ...merged, ...applyLegacyLlm(merged, incoming.llm) };
|
|
694
|
-
}
|
|
695
|
-
if (incoming.agent) {
|
|
696
|
-
merged = { ...merged, ...applyLegacyAgent(merged, incoming.agent) };
|
|
697
|
-
}
|
|
698
802
|
// With `--yes`, fill keys still missing after the merge with non-interactive
|
|
699
803
|
// defaults. Steps start from `merged` and their nonInteractive path only
|
|
700
804
|
// populates absent values, so nothing the file or existing config supplied
|
|
@@ -709,29 +813,26 @@ export async function runSetupFromConfig(opts) {
|
|
|
709
813
|
await runSetupSteps(steps, ctx);
|
|
710
814
|
if (!ctx.config.stashDir)
|
|
711
815
|
ctx.apply({ stashDir });
|
|
712
|
-
if (!ctx.config.defaults?.
|
|
816
|
+
if (!ctx.config.defaults?.engine) {
|
|
713
817
|
const detected = detectAgentCliProfiles(undefined);
|
|
714
818
|
const defaultProfile = pickDefaultAgentProfile(detected, undefined);
|
|
715
819
|
if (defaultProfile) {
|
|
716
|
-
ctx.apply(
|
|
820
|
+
ctx.apply(upsertDetectedAgentEngine(ctx.config, defaultProfile).config);
|
|
717
821
|
}
|
|
718
822
|
}
|
|
719
823
|
merged = ctx.config;
|
|
720
824
|
}
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
if (!opts.noInit) {
|
|
724
|
-
initResult = await akmInit({ dir: stashDir, setDefault: true });
|
|
725
|
-
}
|
|
825
|
+
// Reject an invalid merged engine graph before probing or touching the stash.
|
|
826
|
+
validateCompleteConfig(merged);
|
|
726
827
|
// Optional probe
|
|
727
|
-
const mergedLlm =
|
|
828
|
+
const mergedLlm = readCurrentLlmEngine(merged);
|
|
728
829
|
if (opts.probe && mergedLlm) {
|
|
729
830
|
try {
|
|
730
831
|
const caps = await probeLlmCapabilities(mergedLlm);
|
|
731
832
|
if (caps.reachable) {
|
|
732
833
|
merged = {
|
|
733
834
|
...merged,
|
|
734
|
-
...
|
|
835
|
+
...writeLlmEngine(merged, {
|
|
735
836
|
...mergedLlm,
|
|
736
837
|
capabilities: { structuredOutput: caps.structuredOutput ?? false },
|
|
737
838
|
}),
|
|
@@ -742,9 +843,12 @@ export async function runSetupFromConfig(opts) {
|
|
|
742
843
|
// Non-fatal: probe failure is informational only
|
|
743
844
|
}
|
|
744
845
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
846
|
+
validateCompleteConfig(merged);
|
|
847
|
+
const { precommit: initResult } = await saveSetupConfig(current, merged, async () => {
|
|
848
|
+
if (opts.noInit)
|
|
849
|
+
return undefined;
|
|
850
|
+
return akmInit({ dir: stashDir, setDefault: true, persistConfig: false });
|
|
851
|
+
});
|
|
748
852
|
return {
|
|
749
853
|
configPath: getConfigPath(),
|
|
750
854
|
stashDir,
|