akm-cli 0.7.5 → 0.8.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +192 -2
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2569 -1449
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +2122 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +1075 -77
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +1302 -0
- package/dist/commands/help/help-accept.md +12 -0
- package/dist/commands/help/help-improve.md +69 -0
- package/dist/commands/help/help-proposals.md +18 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +11 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +217 -0
- package/dist/commands/improve-profiles.js +166 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +2373 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +196 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +67 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1091 -73
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +148 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +437 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +17 -2
- package/dist/core/asset-registry.js +12 -17
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +67 -1
- package/dist/core/common.js +182 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +534 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +364 -981
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -8
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +280 -14
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +512 -42
- package/dist/core/state-db.js +1068 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +64 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +178 -256
- package/dist/indexer/db.js +975 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +523 -301
- package/dist/indexer/llm-cache.js +52 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +167 -160
- package/dist/indexer/memory-inference.js +152 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +275 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +6 -17
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +214 -80
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +69 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +282 -0
- package/dist/integrations/session-logs/providers/opencode.js +258 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +77 -124
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +95 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +77 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +292 -0
- package/dist/output/cli-hints-short.md +66 -0
- package/dist/output/cli-hints.js +7 -320
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +300 -257
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +102 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -516
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1039 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +11 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1092
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +71 -50
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17750 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +138 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +227 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +77 -92
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +10 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +30 -12
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/commands/vault.js +0 -328
- package/dist/templates/wiki-templates.js +0 -100
package/dist/setup/setup.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Interactive configuration wizard for akm.
|
|
3
6
|
*
|
|
@@ -5,22 +8,147 @@
|
|
|
5
8
|
* registry selection, stash sources, and agent platform discovery.
|
|
6
9
|
* Collects all choices and writes config once at the end.
|
|
7
10
|
*/
|
|
11
|
+
import { promises as dnsPromises } from "node:dns";
|
|
8
12
|
import fs from "node:fs";
|
|
9
13
|
import os from "node:os";
|
|
10
14
|
import path from "node:path";
|
|
11
15
|
import * as p from "@clack/prompts";
|
|
12
16
|
import { akmInit } from "../commands/init";
|
|
13
17
|
import { isHttpUrl } from "../core/common";
|
|
14
|
-
import { DEFAULT_CONFIG,
|
|
15
|
-
import {
|
|
18
|
+
import { DEFAULT_CONFIG, getDefaultLlmConfig, loadUserConfig, saveConfig } from "../core/config";
|
|
19
|
+
import { ConfigError } from "../core/errors";
|
|
20
|
+
import { assertSafeStashDir, getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths";
|
|
21
|
+
import { warn } from "../core/warn";
|
|
16
22
|
import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db";
|
|
17
23
|
import { akmIndex } from "../indexer/indexer";
|
|
18
24
|
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/semantic-status";
|
|
19
|
-
import { detectAgentCliProfiles, pickDefaultAgentProfile
|
|
25
|
+
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent";
|
|
20
26
|
import { probeLlmCapabilities } from "../llm/client";
|
|
21
27
|
import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
|
|
22
28
|
import { detectAgentPlatforms, detectOllama } from "./detect";
|
|
23
29
|
import { createSetupContext, runSetupSteps } from "./steps";
|
|
30
|
+
// ── Setup sandbox guard ─────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Refuse to persist an explicit `--dir /tmp/...` stashDir to the user's
|
|
33
|
+
* config. The OS may reap the directory at any time, and the next run will
|
|
34
|
+
* see a `stashDir` that points at a deleted path (falling back to ~/akm
|
|
35
|
+
* silently). Mirrors the `assertInitSandbox` check in commands/init.ts, but
|
|
36
|
+
* fires under all runtimes (not just `bun test`) because `akm setup --dir
|
|
37
|
+
* /tmp/X` is a documented isolation pattern that has been observed to
|
|
38
|
+
* silently clobber the host config — see
|
|
39
|
+
* `docs/technical/incidents/2026-05-23-setup-clobbers-user-config.md`.
|
|
40
|
+
*
|
|
41
|
+
* Escape hatch: set `AKM_FORCE_SETUP_TMP_STASH=1` to override. When the
|
|
42
|
+
* escape hatch is on, `applyStashIsolationToEnv` below also pre-sets
|
|
43
|
+
* `AKM_STASH_DIR` so that the `getConfigDir` / `getCacheDir` isolation
|
|
44
|
+
* rules fire and config + cache writes route into `$stashDir/.akm/`
|
|
45
|
+
* instead of the user's host `~/.config/akm`.
|
|
46
|
+
*/
|
|
47
|
+
function assertSetupSandbox(stashDir, dirExplicitlyProvided) {
|
|
48
|
+
if (!dirExplicitlyProvided)
|
|
49
|
+
return;
|
|
50
|
+
if (process.env.AKM_FORCE_SETUP_TMP_STASH === "1")
|
|
51
|
+
return;
|
|
52
|
+
if (!isTransientStashPath(stashDir))
|
|
53
|
+
return;
|
|
54
|
+
throw new ConfigError(`refusing to run \`akm setup --dir ${stashDir}\`: the path is in a transient/sandbox directory family the OS may reap. ` +
|
|
55
|
+
"Persisting it as the user's stashDir would leave the next run pointing at a deleted path (silently falling back to ~/akm). " +
|
|
56
|
+
"Use a persistent directory, OR set AKM_FORCE_SETUP_TMP_STASH=1 if you intentionally want a sandbox setup " +
|
|
57
|
+
"(setup will also auto-isolate config + cache writes into $stashDir/.akm/ so the host config is preserved).", "SETUP_TMP_STASH_REFUSED");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Propagate the explicit `--dir <stashDir>` choice to the env so that the
|
|
61
|
+
* `getConfigDir` / `getCacheDir` isolation rules in `src/core/paths.ts`
|
|
62
|
+
* actually fire for the duration of this setup run. Without this, a CLI
|
|
63
|
+
* caller who passes `--dir /tmp/X` but doesn't pre-export `AKM_STASH_DIR`
|
|
64
|
+
* would still write config to the host `~/.config/akm/config.json`. We
|
|
65
|
+
* only set the env var when:
|
|
66
|
+
* - `--dir` was explicitly provided (we have an operator-stated stash), AND
|
|
67
|
+
* - `AKM_STASH_DIR` is not already set (caller's explicit env wins).
|
|
68
|
+
* The set is process-wide; for the CLI that's the right scope (the process
|
|
69
|
+
* is about to do all its work against this stash). For tests, each test
|
|
70
|
+
* already isolates env via beforeEach/afterEach so there is no leak.
|
|
71
|
+
*/
|
|
72
|
+
function applyStashIsolationToEnv(stashDir, dirExplicitlyProvided) {
|
|
73
|
+
if (!dirExplicitlyProvided)
|
|
74
|
+
return;
|
|
75
|
+
if (process.env.AKM_STASH_DIR?.trim())
|
|
76
|
+
return;
|
|
77
|
+
process.env.AKM_STASH_DIR = stashDir;
|
|
78
|
+
}
|
|
79
|
+
/** Read the currently-configured LLM connection from a loaded config. */
|
|
80
|
+
function getCurrentLlm(config) {
|
|
81
|
+
return getDefaultLlmConfig(config);
|
|
82
|
+
}
|
|
83
|
+
/** Read a synthesised legacy-shape agent block from the new-shape AkmConfig. */
|
|
84
|
+
function getCurrentAgentBlock(config) {
|
|
85
|
+
if (!config.profiles?.agent && !config.defaults?.agent)
|
|
86
|
+
return undefined;
|
|
87
|
+
const block = {};
|
|
88
|
+
if (config.defaults?.agent)
|
|
89
|
+
block.default = config.defaults.agent;
|
|
90
|
+
if (config.profiles?.agent) {
|
|
91
|
+
const profiles = {};
|
|
92
|
+
for (const [name, raw] of Object.entries(config.profiles.agent)) {
|
|
93
|
+
profiles[name] = {
|
|
94
|
+
...(raw.platform === "opencode-sdk" ? { sdkMode: true } : {}),
|
|
95
|
+
...(raw.model ? { model: raw.model } : {}),
|
|
96
|
+
...(raw.bin ? { bin: raw.bin } : {}),
|
|
97
|
+
...(raw.args ? { args: raw.args } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
block.profiles = profiles;
|
|
101
|
+
}
|
|
102
|
+
return block;
|
|
103
|
+
}
|
|
104
|
+
/** Apply an LLM connection patch onto the new-shape config. */
|
|
105
|
+
function applyLegacyLlm(config, llm) {
|
|
106
|
+
if (!llm) {
|
|
107
|
+
// Clear the default LLM profile.
|
|
108
|
+
const name = config.defaults?.llm ?? "default";
|
|
109
|
+
const remaining = { ...(config.profiles?.llm ?? {}) };
|
|
110
|
+
delete remaining[name];
|
|
111
|
+
return {
|
|
112
|
+
profiles: { ...(config.profiles ?? {}), llm: remaining },
|
|
113
|
+
defaults: { ...(config.defaults ?? {}), llm: undefined },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const name = config.defaults?.llm ?? "default";
|
|
117
|
+
return {
|
|
118
|
+
profiles: {
|
|
119
|
+
...(config.profiles ?? {}),
|
|
120
|
+
llm: { ...(config.profiles?.llm ?? {}), [name]: llm },
|
|
121
|
+
},
|
|
122
|
+
defaults: { ...(config.defaults ?? {}), llm: name },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Apply a legacy-shape agent block onto the new-shape config. */
|
|
126
|
+
function applyLegacyAgent(config, agent) {
|
|
127
|
+
if (!agent) {
|
|
128
|
+
return {
|
|
129
|
+
profiles: { ...(config.profiles ?? {}), agent: undefined },
|
|
130
|
+
defaults: { ...(config.defaults ?? {}), agent: undefined },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const v2Profiles = { ...(config.profiles?.agent ?? {}) };
|
|
134
|
+
for (const [name, profile] of Object.entries(agent.profiles ?? {})) {
|
|
135
|
+
const platform = profile.sdkMode
|
|
136
|
+
? "opencode-sdk"
|
|
137
|
+
: name.toLowerCase().includes("claude")
|
|
138
|
+
? "claude"
|
|
139
|
+
: "opencode";
|
|
140
|
+
v2Profiles[name] = {
|
|
141
|
+
platform,
|
|
142
|
+
...(profile.bin ? { bin: profile.bin } : {}),
|
|
143
|
+
...(profile.args ? { args: profile.args } : {}),
|
|
144
|
+
...(profile.model ? { model: profile.model } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
profiles: { ...(config.profiles ?? {}), agent: v2Profiles },
|
|
149
|
+
defaults: { ...(config.defaults ?? {}), agent: agent.default },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
24
152
|
/**
|
|
25
153
|
* Recommended GitHub repositories shown during setup.
|
|
26
154
|
*/
|
|
@@ -121,7 +249,6 @@ function cloneLlmConfig(llm) {
|
|
|
121
249
|
return {
|
|
122
250
|
...llm,
|
|
123
251
|
...(llm.capabilities ? { capabilities: { ...llm.capabilities } } : {}),
|
|
124
|
-
...(llm.features ? { features: { ...llm.features } } : {}),
|
|
125
252
|
...(llm.extraParams ? { extraParams: { ...llm.extraParams } } : {}),
|
|
126
253
|
};
|
|
127
254
|
}
|
|
@@ -201,15 +328,27 @@ async function stepAdditionalSources(currentSources) {
|
|
|
201
328
|
return sources;
|
|
202
329
|
}
|
|
203
330
|
/**
|
|
204
|
-
* Quick connectivity check. Returns true if we can
|
|
205
|
-
*
|
|
206
|
-
* dependent setup steps gracefully
|
|
331
|
+
* Quick connectivity check. Returns true if we can resolve a hostname
|
|
332
|
+
* the user has already implicitly trusted within 3 seconds, false
|
|
333
|
+
* otherwise. Used to skip network-dependent setup steps gracefully
|
|
334
|
+
* when offline.
|
|
335
|
+
*
|
|
336
|
+
* We use a DNS lookup against `github.com` rather than an HTTP request
|
|
337
|
+
* because (1) it doesn't actually send a request to anyone we aren't
|
|
338
|
+
* already talking to (the user got akm from GitHub and `akm upgrade`
|
|
339
|
+
* polls api.github.com), and (2) DNS is the right layer for "do we have
|
|
340
|
+
* working network" without making the user opt into yet another remote.
|
|
341
|
+
* The previous implementation pinged https://dns.google which
|
|
342
|
+
* contradicted the spirit of "no remote endpoints akm doesn't own."
|
|
207
343
|
*
|
|
208
344
|
* @internal Exported for testing only.
|
|
209
345
|
*/
|
|
210
346
|
export async function isOnline() {
|
|
211
347
|
try {
|
|
212
|
-
await
|
|
348
|
+
await Promise.race([
|
|
349
|
+
dnsPromises.lookup("github.com"),
|
|
350
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("dns lookup timed out")), 3000).unref()),
|
|
351
|
+
]);
|
|
213
352
|
return true;
|
|
214
353
|
}
|
|
215
354
|
catch {
|
|
@@ -337,8 +476,11 @@ async function prepareSemanticSearchAssets(config) {
|
|
|
337
476
|
return { ok: true };
|
|
338
477
|
}
|
|
339
478
|
// ── Steps ───────────────────────────────────────────────────────────────────
|
|
340
|
-
async function stepStashDir(current) {
|
|
341
|
-
const defaultDir = current.stashDir ?? getDefaultStashDir();
|
|
479
|
+
async function stepStashDir(current, options) {
|
|
480
|
+
const defaultDir = options?.preferredDir ?? current.stashDir ?? getDefaultStashDir();
|
|
481
|
+
if (options?.nonInteractive) {
|
|
482
|
+
return defaultDir;
|
|
483
|
+
}
|
|
342
484
|
const choice = await prompt(() => p.select({
|
|
343
485
|
message: "Where should akm store skills, commands, and other assets?",
|
|
344
486
|
options: [
|
|
@@ -354,6 +496,14 @@ async function stepStashDir(current) {
|
|
|
354
496
|
validate: (v) => {
|
|
355
497
|
if (!v?.trim())
|
|
356
498
|
return "Path cannot be empty";
|
|
499
|
+
try {
|
|
500
|
+
assertSafeStashDir(v.trim());
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
if (err instanceof Error)
|
|
504
|
+
return err.message;
|
|
505
|
+
return "Refused: unsafe stash directory";
|
|
506
|
+
}
|
|
357
507
|
},
|
|
358
508
|
}));
|
|
359
509
|
return customPath.trim();
|
|
@@ -488,21 +638,22 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
|
488
638
|
}
|
|
489
639
|
options.push({ value: "custom", label: "Custom OpenAI-compatible endpoint" });
|
|
490
640
|
options.push({ value: "none", label: "Skip LLM", hint: "no metadata enhancement during indexing" });
|
|
491
|
-
|
|
641
|
+
const currentLlm = getCurrentLlm(current);
|
|
642
|
+
if (currentLlm) {
|
|
492
643
|
options.push({
|
|
493
644
|
value: "keep",
|
|
494
|
-
label: `Keep current: ${
|
|
495
|
-
hint:
|
|
645
|
+
label: `Keep current: ${currentLlm.provider ?? currentLlm.endpoint}`,
|
|
646
|
+
hint: currentLlm.model,
|
|
496
647
|
});
|
|
497
648
|
}
|
|
498
|
-
const initialValue =
|
|
649
|
+
const initialValue = currentLlm ? "keep" : ollamaAvailable ? "ollama" : (LLM_PRESETS[0]?.value ?? "none");
|
|
499
650
|
const choice = await prompt(() => p.select({
|
|
500
651
|
message: "Configure an LLM for richer metadata during indexing:",
|
|
501
652
|
options,
|
|
502
653
|
initialValue,
|
|
503
654
|
}));
|
|
504
655
|
if (choice === "keep")
|
|
505
|
-
return cloneLlmConfig(
|
|
656
|
+
return cloneLlmConfig(currentLlm);
|
|
506
657
|
if (choice === "none")
|
|
507
658
|
return undefined;
|
|
508
659
|
let llm;
|
|
@@ -635,7 +786,7 @@ export async function stepRegistries(current) {
|
|
|
635
786
|
* @internal Exported for testing only.
|
|
636
787
|
*/
|
|
637
788
|
export async function stepAddSources(current, options) {
|
|
638
|
-
const existingSources = [...(current.sources ??
|
|
789
|
+
const existingSources = [...(current.sources ?? [])];
|
|
639
790
|
const sources = [];
|
|
640
791
|
if (existingSources.length > 0) {
|
|
641
792
|
p.note(renderConfiguredSourceList(existingSources), "Configured stash sources");
|
|
@@ -705,7 +856,7 @@ async function stepAgentPlatforms(current) {
|
|
|
705
856
|
p.log.info("No agent platform configurations detected.");
|
|
706
857
|
return [];
|
|
707
858
|
}
|
|
708
|
-
const existingPaths = new Set((current.sources ??
|
|
859
|
+
const existingPaths = new Set((current.sources ?? []).map((s) => s.path));
|
|
709
860
|
// Filter out platforms already configured
|
|
710
861
|
const newPlatforms = platforms.filter((pl) => !existingPaths.has(pl.path));
|
|
711
862
|
if (newPlatforms.length === 0) {
|
|
@@ -734,12 +885,374 @@ async function stepAgentPlatforms(current) {
|
|
|
734
885
|
}
|
|
735
886
|
return entries;
|
|
736
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* Step 1/2: Configure the small model connection used for metadata and bounded LLM features.
|
|
890
|
+
*
|
|
891
|
+
* Detects Ollama automatically and pre-selects it. The user may also choose
|
|
892
|
+
* OpenAI, LM Studio, a custom endpoint, or skip the step entirely.
|
|
893
|
+
*/
|
|
894
|
+
export async function stepSmallModelConnection(current) {
|
|
895
|
+
p.log.step("Step 1/2: Configure your small model connection");
|
|
896
|
+
p.note([
|
|
897
|
+
"This connection is used for background processing:",
|
|
898
|
+
" • akm index (metadata enhancement)",
|
|
899
|
+
" • akm distill (lesson distillation)",
|
|
900
|
+
" • akm remember --enrich (memory compression)",
|
|
901
|
+
" • akm curate --rerank (search reranking)",
|
|
902
|
+
].join("\n"));
|
|
903
|
+
// Probe for Ollama in the background while showing the note.
|
|
904
|
+
const spin = p.spinner();
|
|
905
|
+
spin.start("Detecting local services...");
|
|
906
|
+
const ollama = await detectOllama();
|
|
907
|
+
spin.stop(ollama.available ? `Ollama detected at ${ollama.endpoint}` : "No local services detected");
|
|
908
|
+
const ollamaEndpoint = ollama.available ? ollama.endpoint : undefined;
|
|
909
|
+
const providerOptions = [];
|
|
910
|
+
if (ollama.available) {
|
|
911
|
+
providerOptions.push({
|
|
912
|
+
value: "ollama",
|
|
913
|
+
label: "Ollama (local)",
|
|
914
|
+
hint: `detected at ${ollama.endpoint}`,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
providerOptions.push({ value: "openai", label: "OpenAI", hint: "requires AKM_LLM_API_KEY" }, { value: "lmstudio", label: "LM Studio / local server", hint: "http://localhost:1234" }, { value: "custom", label: "Custom OpenAI-compatible endpoint" }, { value: "skip", label: "Skip — disable enrichment features" });
|
|
918
|
+
const currentLlmSmall = getCurrentLlm(current);
|
|
919
|
+
if (currentLlmSmall) {
|
|
920
|
+
providerOptions.push({
|
|
921
|
+
value: "keep",
|
|
922
|
+
label: `Keep current: ${currentLlmSmall.provider ?? currentLlmSmall.endpoint}`,
|
|
923
|
+
hint: currentLlmSmall.model,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
const initialValue = currentLlmSmall ? "keep" : ollama.available ? "ollama" : "openai";
|
|
927
|
+
const providerChoice = await prompt(() => p.select({
|
|
928
|
+
message: "Provider:",
|
|
929
|
+
options: providerOptions,
|
|
930
|
+
initialValue,
|
|
931
|
+
}));
|
|
932
|
+
if (providerChoice === "keep") {
|
|
933
|
+
return { llm: cloneLlmConfig(currentLlmSmall), skipped: false, ollamaEndpoint };
|
|
934
|
+
}
|
|
935
|
+
if (providerChoice === "skip") {
|
|
936
|
+
p.note([
|
|
937
|
+
"Enrichment features disabled:",
|
|
938
|
+
" • akm index — metadata enhancement disabled",
|
|
939
|
+
" • akm distill — lesson generation",
|
|
940
|
+
" • akm remember --enrich",
|
|
941
|
+
" • akm curate --rerank",
|
|
942
|
+
"",
|
|
943
|
+
"You can configure this later with `akm setup`.",
|
|
944
|
+
].join("\n"), "Warning");
|
|
945
|
+
return { llm: undefined, skipped: true, ollamaEndpoint };
|
|
946
|
+
}
|
|
947
|
+
let llm;
|
|
948
|
+
if (providerChoice === "ollama") {
|
|
949
|
+
const ollamaChatModels = ollama.models.filter((m) => !m.includes("embed") && !m.includes("nomic") && !m.includes("minilm") && !m.includes("bge"));
|
|
950
|
+
let model;
|
|
951
|
+
if (ollamaChatModels.length > 0) {
|
|
952
|
+
model = await prompt(() => p.select({
|
|
953
|
+
message: "Model name:",
|
|
954
|
+
options: [
|
|
955
|
+
...ollamaChatModels.map((m) => ({ value: m, label: m })),
|
|
956
|
+
{ value: "__custom__", label: "Enter a model name manually..." },
|
|
957
|
+
],
|
|
958
|
+
initialValue: ollamaChatModels[0],
|
|
959
|
+
}));
|
|
960
|
+
if (model === "__custom__") {
|
|
961
|
+
model = await prompt(() => p.text({
|
|
962
|
+
message: "Model name:",
|
|
963
|
+
placeholder: "llama3.2",
|
|
964
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
965
|
+
}));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
model = await prompt(() => p.text({
|
|
970
|
+
message: "Model name (e.g. llama3.2):",
|
|
971
|
+
placeholder: "llama3.2",
|
|
972
|
+
defaultValue: "llama3.2",
|
|
973
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
974
|
+
}));
|
|
975
|
+
}
|
|
976
|
+
llm = {
|
|
977
|
+
provider: "ollama",
|
|
978
|
+
endpoint: `${ollama.endpoint}/v1/chat/completions`,
|
|
979
|
+
model: model.trim(),
|
|
980
|
+
temperature: 0.3,
|
|
981
|
+
maxTokens: 1024,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
else if (providerChoice === "openai") {
|
|
985
|
+
const model = await prompt(() => p.text({
|
|
986
|
+
message: "Model name:",
|
|
987
|
+
placeholder: "gpt-4o-mini",
|
|
988
|
+
defaultValue: "gpt-4o-mini",
|
|
989
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
990
|
+
}));
|
|
991
|
+
if (!process.env.AKM_LLM_API_KEY) {
|
|
992
|
+
p.log.info("Set AKM_LLM_API_KEY in your shell before running `akm index`.");
|
|
993
|
+
}
|
|
994
|
+
llm = {
|
|
995
|
+
provider: "openai",
|
|
996
|
+
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
997
|
+
model: model.trim() || "gpt-4o-mini",
|
|
998
|
+
temperature: 0.3,
|
|
999
|
+
maxTokens: 1024,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
else if (providerChoice === "lmstudio") {
|
|
1003
|
+
const endpoint = await prompt(() => p.text({
|
|
1004
|
+
message: "Endpoint URL:",
|
|
1005
|
+
placeholder: "http://localhost:1234/v1/chat/completions",
|
|
1006
|
+
defaultValue: "http://localhost:1234/v1/chat/completions",
|
|
1007
|
+
validate: (v) => {
|
|
1008
|
+
if (!v?.trim())
|
|
1009
|
+
return "Endpoint cannot be empty";
|
|
1010
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
1011
|
+
return "Must start with http:// or https://";
|
|
1012
|
+
},
|
|
1013
|
+
}));
|
|
1014
|
+
const model = await prompt(() => p.text({
|
|
1015
|
+
message: "Model name:",
|
|
1016
|
+
placeholder: "local-model",
|
|
1017
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1018
|
+
}));
|
|
1019
|
+
llm = {
|
|
1020
|
+
provider: "lmstudio",
|
|
1021
|
+
endpoint: endpoint.trim(),
|
|
1022
|
+
model: model.trim(),
|
|
1023
|
+
temperature: 0.3,
|
|
1024
|
+
maxTokens: 1024,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
// custom
|
|
1029
|
+
const endpoint = await prompt(() => p.text({
|
|
1030
|
+
message: "OpenAI-compatible chat completions endpoint:",
|
|
1031
|
+
placeholder: "https://your-host/v1/chat/completions",
|
|
1032
|
+
validate: (v) => {
|
|
1033
|
+
if (!v?.trim())
|
|
1034
|
+
return "Endpoint cannot be empty";
|
|
1035
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
1036
|
+
return "Must start with http:// or https://";
|
|
1037
|
+
},
|
|
1038
|
+
}));
|
|
1039
|
+
const model = await prompt(() => p.text({
|
|
1040
|
+
message: "Model name:",
|
|
1041
|
+
placeholder: "gpt-4o-mini",
|
|
1042
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1043
|
+
}));
|
|
1044
|
+
const apiKeyInput = await promptOrBack(() => p.text({
|
|
1045
|
+
message: "API key (optional — press Enter to skip):",
|
|
1046
|
+
placeholder: "",
|
|
1047
|
+
}));
|
|
1048
|
+
llm = {
|
|
1049
|
+
provider: "custom",
|
|
1050
|
+
endpoint: endpoint.trim(),
|
|
1051
|
+
model: model.trim(),
|
|
1052
|
+
temperature: 0.3,
|
|
1053
|
+
maxTokens: 1024,
|
|
1054
|
+
...(apiKeyInput?.trim() ? { apiKey: apiKeyInput.trim() } : {}),
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
// Best-effort probe — never blocks setup.
|
|
1058
|
+
const probeSpin = p.spinner();
|
|
1059
|
+
probeSpin.start("Probing LLM (structured-output round-trip)...");
|
|
1060
|
+
const probe = await probeLlmCapabilities(llm);
|
|
1061
|
+
if (probe.reachable && probe.structuredOutput) {
|
|
1062
|
+
probeSpin.stop("LLM reachable; structured output verified.");
|
|
1063
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: true };
|
|
1064
|
+
}
|
|
1065
|
+
else if (probe.reachable) {
|
|
1066
|
+
probeSpin.stop("LLM reachable but structured-output probe failed.");
|
|
1067
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: false };
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
probeSpin.stop("LLM not reachable.");
|
|
1071
|
+
p.log.warn(`Could not reach the LLM endpoint${probe.error ? ` (${probe.error})` : ""}. Configuration was saved; verify your endpoint and API key, then retry.`);
|
|
1072
|
+
}
|
|
1073
|
+
return { llm, skipped: false, ollamaEndpoint };
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Step 2/2: Configure the agent connection used for agentic features.
|
|
1077
|
+
*
|
|
1078
|
+
* Options depend on whether Step 1 was completed or skipped.
|
|
1079
|
+
*/
|
|
1080
|
+
export async function stepAgentConnection(current, smallModel) {
|
|
1081
|
+
p.log.step("Step 2/2: Configure your agent connection");
|
|
1082
|
+
p.note([
|
|
1083
|
+
"This connection is used for agentic commands:",
|
|
1084
|
+
" • akm propose (generate improvement proposals)",
|
|
1085
|
+
" • akm improve (run the reflect/distill/consolidate self-improvement pipeline)",
|
|
1086
|
+
" • akm tasks run (run automated task prompts)",
|
|
1087
|
+
].join("\n"));
|
|
1088
|
+
// Detect available CLI agents.
|
|
1089
|
+
const detections = detectAgentCliProfiles(current);
|
|
1090
|
+
const currentAgentBlock = getCurrentAgentBlock(current);
|
|
1091
|
+
const availableClis = detections.filter((d) => d.available);
|
|
1092
|
+
const agentOptions = [];
|
|
1093
|
+
if (!smallModel.skipped && smallModel.llm) {
|
|
1094
|
+
agentOptions.push({
|
|
1095
|
+
value: "same-connection",
|
|
1096
|
+
label: "Same connection, select model",
|
|
1097
|
+
hint: `uses ${smallModel.llm.endpoint.replace("/v1/chat/completions", "")}`,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
agentOptions.push({ value: "new-connection", label: "New connection (different endpoint)" });
|
|
1101
|
+
if (availableClis.length > 0) {
|
|
1102
|
+
agentOptions.push({
|
|
1103
|
+
value: "cli-agent",
|
|
1104
|
+
label: "Installed CLI agent",
|
|
1105
|
+
hint: `${availableClis.map((d) => d.name).join(", ")} detected`,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
agentOptions.push({ value: "none", label: "None — disable agentic features" });
|
|
1109
|
+
if (currentAgentBlock) {
|
|
1110
|
+
const currentDesc = currentAgentBlock.default
|
|
1111
|
+
? `CLI: ${currentAgentBlock.default}`
|
|
1112
|
+
: currentAgentBlock.profiles?.default?.model
|
|
1113
|
+
? `SDK: ${currentAgentBlock.profiles.default.model}`
|
|
1114
|
+
: "configured";
|
|
1115
|
+
agentOptions.push({ value: "keep", label: `Keep current: ${currentDesc}` });
|
|
1116
|
+
}
|
|
1117
|
+
const initialAgentValue = currentAgentBlock
|
|
1118
|
+
? "keep"
|
|
1119
|
+
: availableClis.length > 0 && smallModel.skipped
|
|
1120
|
+
? "cli-agent"
|
|
1121
|
+
: !smallModel.skipped && smallModel.llm
|
|
1122
|
+
? "same-connection"
|
|
1123
|
+
: availableClis.length > 0
|
|
1124
|
+
? "cli-agent"
|
|
1125
|
+
: "none";
|
|
1126
|
+
const agentChoice = await prompt(() => p.select({
|
|
1127
|
+
message: "How do you want to run agent commands?",
|
|
1128
|
+
options: agentOptions,
|
|
1129
|
+
initialValue: initialAgentValue,
|
|
1130
|
+
}));
|
|
1131
|
+
if (agentChoice === "keep") {
|
|
1132
|
+
return currentAgentBlock;
|
|
1133
|
+
}
|
|
1134
|
+
if (agentChoice === "none") {
|
|
1135
|
+
p.note([
|
|
1136
|
+
"Agentic features disabled:",
|
|
1137
|
+
' • akm propose — will show "no agent configured" error',
|
|
1138
|
+
' • akm improve — will show "no agent configured" error',
|
|
1139
|
+
' • akm tasks run — will show "no agent configured" error',
|
|
1140
|
+
"",
|
|
1141
|
+
"You can configure this later with `akm setup`.",
|
|
1142
|
+
].join("\n"), "Warning");
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
if (agentChoice === "same-connection") {
|
|
1146
|
+
if (smallModel.skipped || !smallModel.llm) {
|
|
1147
|
+
p.log.warn("You skipped the small model connection. Configure one to use the same connection. Falling back to 'new connection'.");
|
|
1148
|
+
// Fall through to new-connection flow
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
const baseEndpoint = smallModel.llm.endpoint.replace("/v1/chat/completions", "");
|
|
1152
|
+
p.log.info(`Endpoint: ${baseEndpoint} (from Step 1)`);
|
|
1153
|
+
const agentModel = await prompt(() => p.text({
|
|
1154
|
+
message: "Model to use for agent tasks (same model is fine, larger models work better):",
|
|
1155
|
+
placeholder: "qwen2.5-coder:32b",
|
|
1156
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1157
|
+
}));
|
|
1158
|
+
const profileName = smallModel.llm.provider ?? "default";
|
|
1159
|
+
return {
|
|
1160
|
+
...(currentAgentBlock ?? {}),
|
|
1161
|
+
profiles: {
|
|
1162
|
+
...(currentAgentBlock?.profiles ?? {}),
|
|
1163
|
+
[profileName]: {
|
|
1164
|
+
...(currentAgentBlock?.profiles?.[profileName] ?? {}),
|
|
1165
|
+
sdkMode: true,
|
|
1166
|
+
model: agentModel.trim(),
|
|
1167
|
+
endpoint: smallModel.llm.endpoint,
|
|
1168
|
+
},
|
|
1169
|
+
},
|
|
1170
|
+
default: profileName,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (agentChoice === "cli-agent") {
|
|
1175
|
+
if (availableClis.length === 0) {
|
|
1176
|
+
p.log.warn("No agent CLIs detected on PATH.");
|
|
1177
|
+
return currentAgentBlock;
|
|
1178
|
+
}
|
|
1179
|
+
const initialCli = pickDefaultAgentProfile(detections, currentAgentBlock?.default) ?? availableClis[0]?.name;
|
|
1180
|
+
const selectedCli = await prompt(() => p.select({
|
|
1181
|
+
message: "Which CLI agent?",
|
|
1182
|
+
options: availableClis.map((d) => ({
|
|
1183
|
+
value: d.name,
|
|
1184
|
+
label: d.name,
|
|
1185
|
+
hint: d.resolvedPath ?? d.bin,
|
|
1186
|
+
})),
|
|
1187
|
+
initialValue: initialCli,
|
|
1188
|
+
}));
|
|
1189
|
+
return {
|
|
1190
|
+
...(currentAgentBlock ?? {}),
|
|
1191
|
+
default: selectedCli,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
// "new-connection" (also fall-through from "same-provider" when Step 1 was skipped)
|
|
1195
|
+
const newEndpoint = await prompt(() => p.text({
|
|
1196
|
+
message: "OpenAI-compatible chat completions endpoint:",
|
|
1197
|
+
placeholder: "https://your-host/v1/chat/completions",
|
|
1198
|
+
validate: (v) => {
|
|
1199
|
+
if (!v?.trim())
|
|
1200
|
+
return "Endpoint cannot be empty";
|
|
1201
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
1202
|
+
return "Must start with http:// or https://";
|
|
1203
|
+
},
|
|
1204
|
+
}));
|
|
1205
|
+
const newApiKeyInput = await promptOrBack(() => p.text({
|
|
1206
|
+
message: "API key (optional — press Enter to skip):",
|
|
1207
|
+
placeholder: "",
|
|
1208
|
+
}));
|
|
1209
|
+
const newModel = await prompt(() => p.text({
|
|
1210
|
+
message: "Model name (larger is better, e.g. gpt-4o):",
|
|
1211
|
+
placeholder: "gpt-4o",
|
|
1212
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1213
|
+
}));
|
|
1214
|
+
const customProfile = {
|
|
1215
|
+
sdkMode: true,
|
|
1216
|
+
endpoint: newEndpoint.trim(),
|
|
1217
|
+
model: newModel.trim(),
|
|
1218
|
+
...(newApiKeyInput?.trim() ? { apiKey: newApiKeyInput.trim() } : {}),
|
|
1219
|
+
};
|
|
1220
|
+
return {
|
|
1221
|
+
...(currentAgentBlock ?? {}),
|
|
1222
|
+
profiles: {
|
|
1223
|
+
...(currentAgentBlock?.profiles ?? {}),
|
|
1224
|
+
custom: customProfile,
|
|
1225
|
+
},
|
|
1226
|
+
default: "custom",
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Print a feature capability summary after both connection steps are complete.
|
|
1231
|
+
*/
|
|
1232
|
+
function printCapabilitySummary(smallModelSkipped, agentConfigured) {
|
|
1233
|
+
const lines = ["Setup complete. Here's what's enabled:", ""];
|
|
1234
|
+
lines.push(" ✓ akm search, akm curate, akm show — always available");
|
|
1235
|
+
if (!smallModelSkipped) {
|
|
1236
|
+
lines.push(" ✓ akm index, akm distill, akm remember — small model configured");
|
|
1237
|
+
}
|
|
1238
|
+
else {
|
|
1239
|
+
lines.push(" ✗ akm index, akm distill, akm remember — run `akm setup` to enable");
|
|
1240
|
+
}
|
|
1241
|
+
if (agentConfigured) {
|
|
1242
|
+
lines.push(" ✓ akm propose, akm improve, akm tasks — agent configured");
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
lines.push(" ✗ akm propose, akm improve, akm tasks — run `akm setup` to enable");
|
|
1246
|
+
}
|
|
1247
|
+
p.note(lines.join("\n"), "Feature Summary");
|
|
1248
|
+
}
|
|
737
1249
|
export async function stepAgentSelection(current, detections) {
|
|
1250
|
+
const currentAgentBlock = getCurrentAgentBlock(current);
|
|
738
1251
|
const available = detections.filter((d) => d.available);
|
|
739
1252
|
if (available.length === 0) {
|
|
740
|
-
return
|
|
1253
|
+
return currentAgentBlock;
|
|
741
1254
|
}
|
|
742
|
-
const initialValue = pickDefaultAgentProfile(detections,
|
|
1255
|
+
const initialValue = pickDefaultAgentProfile(detections, currentAgentBlock?.default) ?? available[0]?.name;
|
|
743
1256
|
const selectedDefault = await prompt(() => p.select({
|
|
744
1257
|
message: "Which detected agent CLI should be the default?",
|
|
745
1258
|
options: [
|
|
@@ -753,16 +1266,16 @@ export async function stepAgentSelection(current, detections) {
|
|
|
753
1266
|
initialValue,
|
|
754
1267
|
}));
|
|
755
1268
|
if (selectedDefault === "disabled") {
|
|
756
|
-
if (!
|
|
1269
|
+
if (!currentAgentBlock?.profiles && !currentAgentBlock?.timeoutMs) {
|
|
757
1270
|
return undefined;
|
|
758
1271
|
}
|
|
759
1272
|
return {
|
|
760
|
-
...(
|
|
1273
|
+
...(currentAgentBlock ?? {}),
|
|
761
1274
|
default: undefined,
|
|
762
1275
|
};
|
|
763
1276
|
}
|
|
764
1277
|
return {
|
|
765
|
-
...(
|
|
1278
|
+
...(currentAgentBlock ?? {}),
|
|
766
1279
|
default: selectedDefault,
|
|
767
1280
|
};
|
|
768
1281
|
}
|
|
@@ -799,14 +1312,15 @@ export async function stepOutputConfig(current) {
|
|
|
799
1312
|
* @internal Exported for testing only.
|
|
800
1313
|
*/
|
|
801
1314
|
export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
|
|
802
|
-
const detections = detectFn(current
|
|
803
|
-
const
|
|
1315
|
+
const detections = detectFn(current);
|
|
1316
|
+
const currentAgentBlock = getCurrentAgentBlock(current);
|
|
1317
|
+
const defaultName = pickDefaultAgentProfile(detections, currentAgentBlock?.default);
|
|
804
1318
|
// No installed agents found and no existing config → leave block absent.
|
|
805
|
-
if (!defaultName && !
|
|
1319
|
+
if (!defaultName && !currentAgentBlock) {
|
|
806
1320
|
return { detections };
|
|
807
1321
|
}
|
|
808
1322
|
const agent = {
|
|
809
|
-
...(
|
|
1323
|
+
...(currentAgentBlock ?? {}),
|
|
810
1324
|
...(defaultName ? { default: defaultName } : {}),
|
|
811
1325
|
};
|
|
812
1326
|
return { agent, detections };
|
|
@@ -832,7 +1346,10 @@ export function buildSetupSteps(options) {
|
|
|
832
1346
|
label: "Stash Directory",
|
|
833
1347
|
nonInteractive: true,
|
|
834
1348
|
async run(ctx) {
|
|
835
|
-
const stashDir = await stepStashDir(ctx.config
|
|
1349
|
+
const stashDir = await stepStashDir(ctx.config, {
|
|
1350
|
+
nonInteractive: ctx.nonInteractive,
|
|
1351
|
+
preferredDir: options.preferredStashDir,
|
|
1352
|
+
});
|
|
836
1353
|
ctx.apply({ stashDir });
|
|
837
1354
|
},
|
|
838
1355
|
},
|
|
@@ -855,11 +1372,10 @@ export function buildSetupSteps(options) {
|
|
|
855
1372
|
label: "LLM Provider",
|
|
856
1373
|
async run(ctx) {
|
|
857
1374
|
if (!options.online) {
|
|
858
|
-
ctx.apply({ llm: ctx.config.llm });
|
|
859
1375
|
return;
|
|
860
1376
|
}
|
|
861
1377
|
const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels);
|
|
862
|
-
ctx.apply(
|
|
1378
|
+
ctx.apply(applyLegacyLlm(ctx.config, llm));
|
|
863
1379
|
},
|
|
864
1380
|
},
|
|
865
1381
|
{
|
|
@@ -907,8 +1423,11 @@ export function buildSetupSteps(options) {
|
|
|
907
1423
|
else {
|
|
908
1424
|
p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
|
|
909
1425
|
}
|
|
910
|
-
|
|
911
|
-
|
|
1426
|
+
// Inject the detected agent block into a synthetic AkmConfig so
|
|
1427
|
+
// stepAgentSelection can read it via getCurrentAgentBlock().
|
|
1428
|
+
const synthConfig = { ...ctx.config, ...applyLegacyAgent(ctx.config, result.agent) };
|
|
1429
|
+
const agent = await stepAgentSelection(synthConfig, result.detections);
|
|
1430
|
+
ctx.apply(applyLegacyAgent(ctx.config, agent));
|
|
912
1431
|
},
|
|
913
1432
|
},
|
|
914
1433
|
{
|
|
@@ -922,10 +1441,21 @@ export function buildSetupSteps(options) {
|
|
|
922
1441
|
];
|
|
923
1442
|
return { steps, outcome };
|
|
924
1443
|
}
|
|
925
|
-
export async function runSetupWizard() {
|
|
1444
|
+
export async function runSetupWizard(opts) {
|
|
926
1445
|
p.intro("akm setup");
|
|
927
1446
|
const current = loadUserConfig();
|
|
928
1447
|
const configPath = getConfigPath();
|
|
1448
|
+
// Resolve stash directory early so akmInit can run before any prompts
|
|
1449
|
+
const resolvedStashDir = opts?.dir ? path.resolve(opts.dir) : (current.stashDir ?? getDefaultStashDir());
|
|
1450
|
+
// Refuse explicit --dir /tmp/... before doing any work — protects the host
|
|
1451
|
+
// config from being clobbered with a stashDir that the OS may reap.
|
|
1452
|
+
assertSetupSandbox(resolvedStashDir, opts?.dir != null);
|
|
1453
|
+
applyStashIsolationToEnv(resolvedStashDir, opts?.dir != null);
|
|
1454
|
+
// Bootstrap directory structure before any prompts so the stash exists
|
|
1455
|
+
// even if the wizard is interrupted after this point.
|
|
1456
|
+
if (!opts?.noInit) {
|
|
1457
|
+
await akmInit({ dir: resolvedStashDir });
|
|
1458
|
+
}
|
|
929
1459
|
// Quick connectivity check — skip network-dependent steps when offline
|
|
930
1460
|
const online = await isOnline();
|
|
931
1461
|
if (!online) {
|
|
@@ -936,6 +1466,7 @@ export async function runSetupWizard() {
|
|
|
936
1466
|
const { steps, outcome } = buildSetupSteps({
|
|
937
1467
|
online,
|
|
938
1468
|
semanticSearchOutcome: { mode: current.semanticSearchMode, prepareAssets: false },
|
|
1469
|
+
preferredStashDir: resolvedStashDir,
|
|
939
1470
|
});
|
|
940
1471
|
// Wrap each step with a `p.log.step()` header so the wizard UI is
|
|
941
1472
|
// unchanged. The canonical `runSetupSteps()` runner is used directly by
|
|
@@ -948,6 +1479,15 @@ export async function runSetupWizard() {
|
|
|
948
1479
|
},
|
|
949
1480
|
}));
|
|
950
1481
|
await runSetupSteps(labeledSteps, ctx);
|
|
1482
|
+
// ── Two-step connection configuration ──────────────────────────────────────
|
|
1483
|
+
// Step 1/2: Small model connection (for enrichment features)
|
|
1484
|
+
const smallModelResult = await stepSmallModelConnection(ctx.config);
|
|
1485
|
+
if (!smallModelResult.skipped) {
|
|
1486
|
+
ctx.apply(applyLegacyLlm(ctx.config, smallModelResult.llm));
|
|
1487
|
+
}
|
|
1488
|
+
// Step 2/2: Agent connection (for agentic features)
|
|
1489
|
+
const agentConfig = await stepAgentConnection(ctx.config, smallModelResult);
|
|
1490
|
+
ctx.apply(applyLegacyAgent(ctx.config, agentConfig));
|
|
951
1491
|
const newConfig = {
|
|
952
1492
|
...ctx.config,
|
|
953
1493
|
// Preserve fields the steps don't manage explicitly.
|
|
@@ -956,9 +1496,12 @@ export async function runSetupWizard() {
|
|
|
956
1496
|
const semanticSearchMode = outcome.semantic;
|
|
957
1497
|
const stashDir = newConfig.stashDir ?? current.stashDir ?? getDefaultStashDir();
|
|
958
1498
|
const embedding = newConfig.embedding;
|
|
959
|
-
const llm = newConfig
|
|
1499
|
+
const llm = getDefaultLlmConfig(newConfig);
|
|
960
1500
|
const registries = newConfig.registries;
|
|
961
|
-
const allStashes = newConfig.sources ??
|
|
1501
|
+
const allStashes = newConfig.sources ?? [];
|
|
1502
|
+
// Feature capability summary
|
|
1503
|
+
const agentConfigured = Boolean(agentConfig);
|
|
1504
|
+
printCapabilitySummary(smallModelResult.skipped, agentConfigured);
|
|
962
1505
|
// Confirm before saving
|
|
963
1506
|
const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
964
1507
|
p.note([
|
|
@@ -968,7 +1511,7 @@ export async function runSetupWizard() {
|
|
|
968
1511
|
`Semantic search: ${semanticSearchMode.mode}`,
|
|
969
1512
|
`Registries: ${effectiveRegistries.filter((r) => r.enabled !== false).length} enabled`,
|
|
970
1513
|
`Stash sources: ${allStashes.length}`,
|
|
971
|
-
`Agent default: ${newConfig.agent
|
|
1514
|
+
`Agent default: ${newConfig.defaults?.agent ?? "disabled"}`,
|
|
972
1515
|
`Output: ${newConfig.output?.format ?? "json"} / ${newConfig.output?.detail ?? "brief"}`,
|
|
973
1516
|
].join("\n"), "Configuration Summary");
|
|
974
1517
|
const shouldSave = await prompt(() => p.confirm({
|
|
@@ -979,8 +1522,6 @@ export async function runSetupWizard() {
|
|
|
979
1522
|
bail();
|
|
980
1523
|
// Save config
|
|
981
1524
|
saveConfig(newConfig);
|
|
982
|
-
// Initialize stash directory
|
|
983
|
-
await akmInit({ dir: stashDir });
|
|
984
1525
|
if (semanticSearchMode.mode === "off") {
|
|
985
1526
|
clearSemanticStatus();
|
|
986
1527
|
}
|
|
@@ -1062,3 +1603,200 @@ export async function runSetupWizard() {
|
|
|
1062
1603
|
}
|
|
1063
1604
|
p.outro(`Configuration saved to ${configPath}`);
|
|
1064
1605
|
}
|
|
1606
|
+
// ── Non-interactive / scripting entry points ─────────────────────────────────
|
|
1607
|
+
/**
|
|
1608
|
+
* Run setup in non-interactive mode, applying all defaults.
|
|
1609
|
+
* Safe to call from CI or scripts. Idempotent — re-running produces the same result.
|
|
1610
|
+
*/
|
|
1611
|
+
export async function runSetupWithDefaults(opts) {
|
|
1612
|
+
const current = loadUserConfig();
|
|
1613
|
+
const stashDir = opts.dir ? path.resolve(opts.dir) : (current.stashDir ?? getDefaultStashDir());
|
|
1614
|
+
assertSetupSandbox(stashDir, opts.dir != null);
|
|
1615
|
+
applyStashIsolationToEnv(stashDir, opts.dir != null);
|
|
1616
|
+
// Bootstrap directory structure first
|
|
1617
|
+
let initResult;
|
|
1618
|
+
if (!opts.noInit) {
|
|
1619
|
+
initResult = await akmInit({ dir: stashDir });
|
|
1620
|
+
}
|
|
1621
|
+
// Run steps in non-interactive mode (applies defaults, skips prompts)
|
|
1622
|
+
const ctx = createSetupContext(current, { nonInteractive: true });
|
|
1623
|
+
const { steps } = buildSetupSteps({
|
|
1624
|
+
online: false,
|
|
1625
|
+
semanticSearchOutcome: { mode: current.semanticSearchMode, prepareAssets: false },
|
|
1626
|
+
preferredStashDir: stashDir,
|
|
1627
|
+
});
|
|
1628
|
+
await runSetupSteps(steps, ctx);
|
|
1629
|
+
// Ensure stashDir is set
|
|
1630
|
+
if (!ctx.config.stashDir)
|
|
1631
|
+
ctx.apply({ stashDir });
|
|
1632
|
+
// Auto-detect agent CLI if not already configured
|
|
1633
|
+
if (!ctx.config.defaults?.agent) {
|
|
1634
|
+
const detected = detectAgentCliProfiles(undefined);
|
|
1635
|
+
const defaultProfile = pickDefaultAgentProfile(detected, undefined);
|
|
1636
|
+
if (defaultProfile) {
|
|
1637
|
+
ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
saveConfig(ctx.config);
|
|
1641
|
+
return {
|
|
1642
|
+
configPath: getConfigPath(),
|
|
1643
|
+
stashDir,
|
|
1644
|
+
stashCreated: initResult?.created ?? false,
|
|
1645
|
+
written: true,
|
|
1646
|
+
fields: Object.keys(ctx.config).filter((k) => ctx.config[k] !== undefined),
|
|
1647
|
+
ripgrep: initResult?.ripgrep,
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Apply a JSON config blob non-interactively, merging it with the current config.
|
|
1652
|
+
* Validates required sub-fields and strips unknown/restricted keys.
|
|
1653
|
+
*/
|
|
1654
|
+
export async function runSetupFromConfig(opts) {
|
|
1655
|
+
let incoming;
|
|
1656
|
+
try {
|
|
1657
|
+
incoming = JSON.parse(opts.configJson);
|
|
1658
|
+
}
|
|
1659
|
+
catch (e) {
|
|
1660
|
+
throw new Error(`Invalid JSON in --config: ${e.message}`);
|
|
1661
|
+
}
|
|
1662
|
+
// Phase 2: Validate — only allow safe top-level keys
|
|
1663
|
+
const ALLOWED_KEYS = new Set([
|
|
1664
|
+
"stashDir",
|
|
1665
|
+
"llm",
|
|
1666
|
+
"embedding",
|
|
1667
|
+
"agent",
|
|
1668
|
+
"semanticSearchMode",
|
|
1669
|
+
"output",
|
|
1670
|
+
"profiles",
|
|
1671
|
+
"defaults",
|
|
1672
|
+
]);
|
|
1673
|
+
for (const key of Object.keys(incoming)) {
|
|
1674
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
1675
|
+
warn(`[akm setup] Ignoring unknown or restricted config key: "${key}"`);
|
|
1676
|
+
delete incoming[key];
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
// Validate required sub-fields
|
|
1680
|
+
if (incoming.llm) {
|
|
1681
|
+
if (!incoming.llm.endpoint?.trim())
|
|
1682
|
+
throw new Error("llm.endpoint is required when llm is provided");
|
|
1683
|
+
if (!incoming.llm.model?.trim())
|
|
1684
|
+
throw new Error("llm.model is required when llm is provided");
|
|
1685
|
+
}
|
|
1686
|
+
if (incoming.embedding) {
|
|
1687
|
+
if (!incoming.embedding.endpoint?.trim())
|
|
1688
|
+
throw new Error("embedding.endpoint is required when embedding is provided");
|
|
1689
|
+
if (!incoming.embedding.model?.trim())
|
|
1690
|
+
throw new Error("embedding.model is required when embedding is provided");
|
|
1691
|
+
}
|
|
1692
|
+
// Phase 3: Merge with existing config
|
|
1693
|
+
const current = loadUserConfig();
|
|
1694
|
+
const stashDir = opts.dir
|
|
1695
|
+
? path.resolve(opts.dir)
|
|
1696
|
+
: incoming.stashDir
|
|
1697
|
+
? path.resolve(incoming.stashDir)
|
|
1698
|
+
: (current.stashDir ?? getDefaultStashDir());
|
|
1699
|
+
const stashDirExplicit = opts.dir != null || incoming.stashDir != null;
|
|
1700
|
+
assertSetupSandbox(stashDir, stashDirExplicit);
|
|
1701
|
+
applyStashIsolationToEnv(stashDir, stashDirExplicit);
|
|
1702
|
+
let merged = { ...current, stashDir };
|
|
1703
|
+
// Apply non-llm/agent keys directly.
|
|
1704
|
+
const mergedRec = merged;
|
|
1705
|
+
for (const key of Object.keys(incoming)) {
|
|
1706
|
+
if (key === "llm" || key === "agent")
|
|
1707
|
+
continue;
|
|
1708
|
+
mergedRec[key] = incoming[key];
|
|
1709
|
+
}
|
|
1710
|
+
// Translate legacy llm/agent inputs into the new shape.
|
|
1711
|
+
if (incoming.llm) {
|
|
1712
|
+
merged = { ...merged, ...applyLegacyLlm(merged, incoming.llm) };
|
|
1713
|
+
}
|
|
1714
|
+
if (incoming.agent) {
|
|
1715
|
+
merged = { ...merged, ...applyLegacyAgent(merged, incoming.agent) };
|
|
1716
|
+
}
|
|
1717
|
+
// Bootstrap directory structure
|
|
1718
|
+
let initResult;
|
|
1719
|
+
if (!opts.noInit) {
|
|
1720
|
+
initResult = await akmInit({ dir: stashDir });
|
|
1721
|
+
}
|
|
1722
|
+
// Optional probe
|
|
1723
|
+
const mergedLlm = getDefaultLlmConfig(merged);
|
|
1724
|
+
if (opts.probe && mergedLlm) {
|
|
1725
|
+
try {
|
|
1726
|
+
const caps = await probeLlmCapabilities(mergedLlm);
|
|
1727
|
+
if (caps.reachable) {
|
|
1728
|
+
merged = {
|
|
1729
|
+
...merged,
|
|
1730
|
+
...applyLegacyLlm(merged, {
|
|
1731
|
+
...mergedLlm,
|
|
1732
|
+
capabilities: { structuredOutput: caps.structuredOutput ?? false },
|
|
1733
|
+
}),
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
catch {
|
|
1738
|
+
// Non-fatal: probe failure is informational only
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
saveConfig(merged);
|
|
1742
|
+
return {
|
|
1743
|
+
configPath: getConfigPath(),
|
|
1744
|
+
stashDir,
|
|
1745
|
+
stashCreated: initResult?.created ?? false,
|
|
1746
|
+
written: true,
|
|
1747
|
+
fields: Object.keys(incoming).filter((k) => incoming[k] !== undefined),
|
|
1748
|
+
ripgrep: initResult?.ripgrep,
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
// ── Setup --from <file> bootstrap helper ────────────────────────────────────
|
|
1752
|
+
/**
|
|
1753
|
+
* Resolve a `--from <file>` argument to a JSON-encoded config payload suitable
|
|
1754
|
+
* for `runSetupFromConfig({ configJson })`. Used by the CLI to bootstrap from
|
|
1755
|
+
* a JSON or YAML file on disk; extracted as a standalone function so its
|
|
1756
|
+
* filesystem and parser behaviour can be unit-tested directly.
|
|
1757
|
+
*
|
|
1758
|
+
* - Expands a leading `~` to the current user's home directory.
|
|
1759
|
+
* - Resolves the path against `cwd ?? process.cwd()` for relative inputs.
|
|
1760
|
+
* - Detects YAML vs JSON via the file extension (`.yml`/`.yaml` → YAML;
|
|
1761
|
+
* anything else, including `.json`, parses as JSON).
|
|
1762
|
+
* - Throws `ConfigError("INVALID_CONFIG_FILE")` when the file does not exist,
|
|
1763
|
+
* cannot be read, cannot be parsed, or contains a non-object top level.
|
|
1764
|
+
*
|
|
1765
|
+
* Returns `{ configJson, resolvedPath, format }` so callers can log which
|
|
1766
|
+
* file was actually loaded and which parser was used.
|
|
1767
|
+
*/
|
|
1768
|
+
export async function loadSetupConfigFromFile(filePath, opts) {
|
|
1769
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
1770
|
+
const homeDir = opts?.homeDir ?? os.homedir();
|
|
1771
|
+
const expanded = filePath.startsWith("~") ? path.join(homeDir, filePath.slice(1)) : filePath;
|
|
1772
|
+
const resolvedPath = path.resolve(cwd, expanded);
|
|
1773
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
1774
|
+
throw new ConfigError(`Config file not found: ${resolvedPath}`, "INVALID_CONFIG_FILE");
|
|
1775
|
+
}
|
|
1776
|
+
let raw;
|
|
1777
|
+
try {
|
|
1778
|
+
raw = fs.readFileSync(resolvedPath, "utf8");
|
|
1779
|
+
}
|
|
1780
|
+
catch (err) {
|
|
1781
|
+
throw new ConfigError(`Failed to read config file ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
|
|
1782
|
+
}
|
|
1783
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
1784
|
+
const format = ext === ".yml" || ext === ".yaml" ? "yaml" : "json";
|
|
1785
|
+
let parsed;
|
|
1786
|
+
try {
|
|
1787
|
+
if (format === "yaml") {
|
|
1788
|
+
const { parse: yamlParse } = await import("yaml");
|
|
1789
|
+
parsed = yamlParse(raw);
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
parsed = JSON.parse(raw);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
catch (err) {
|
|
1796
|
+
throw new ConfigError(`Failed to parse ${format.toUpperCase()} config file ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_CONFIG_FILE");
|
|
1797
|
+
}
|
|
1798
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1799
|
+
throw new ConfigError(`Config file ${resolvedPath} must contain a top-level object, got ${Array.isArray(parsed) ? "array" : typeof parsed}.`, "INVALID_CONFIG_FILE");
|
|
1800
|
+
}
|
|
1801
|
+
return { configJson: JSON.stringify(parsed), resolvedPath, format };
|
|
1802
|
+
}
|