akm-cli 0.8.1 → 0.9.0-beta.0
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 +258 -0
- package/dist/assets/help/help-proposals.md +1 -2
- package/dist/assets/hints/cli-hints-full.md +34 -19
- package/dist/assets/hints/cli-hints-short.md +1 -1
- package/dist/assets/profiles/catchup.json +13 -0
- package/dist/assets/profiles/consolidate.json +13 -0
- package/dist/assets/profiles/frequent.json +13 -0
- package/dist/assets/stash-skeleton/README.md +76 -0
- package/dist/assets/tasks/core/backup.yml +4 -0
- package/dist/assets/tasks/core/extract.yml +4 -0
- package/dist/assets/tasks/core/improve.yml +4 -0
- package/dist/assets/tasks/core/index-refresh.yml +4 -0
- package/dist/assets/tasks/core/sync.yml +4 -0
- package/dist/assets/tasks/core/update-stashes.yml +4 -0
- package/dist/assets/tasks/core/version-check.yml +4 -0
- package/dist/cli/config-migrate.js +6 -6
- package/dist/cli/config-validate.js +4 -4
- package/dist/cli/confirm.js +3 -3
- package/dist/cli/parse-args.js +1 -1
- package/dist/cli/shared.js +51 -14
- package/dist/cli-node.mjs +26 -0
- package/dist/cli.js +171 -3857
- package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
- package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
- package/dist/commands/agent/contribute-cli.js +200 -0
- package/dist/commands/completions.js +1 -1
- package/dist/commands/config-cli.js +240 -3
- package/dist/commands/config-edit.js +344 -0
- package/dist/commands/db-cli.js +2 -2
- package/dist/commands/env/env-cli.js +529 -0
- package/dist/commands/env/env.js +410 -0
- package/dist/commands/env/secret-cli.js +259 -0
- package/dist/commands/{secret.js → env/secret.js} +6 -47
- package/dist/commands/events.js +4 -4
- package/dist/commands/feedback-cli.js +18 -34
- package/dist/commands/graph/graph-cli.js +132 -0
- package/dist/commands/{graph.js → graph/graph.js} +22 -16
- package/dist/commands/health/checks.js +279 -0
- package/dist/commands/health.js +101 -249
- package/dist/commands/{consolidate.js → improve/consolidate.js} +52 -40
- package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
- package/dist/commands/{distill.js → improve/distill.js} +39 -18
- package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
- package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
- package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
- package/dist/commands/{extract.js → improve/extract.js} +185 -26
- package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +4 -4
- package/dist/commands/{improve-cli.js → improve/improve-cli.js} +45 -23
- package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
- package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +10 -5
- package/dist/commands/{improve.js → improve/improve.js} +536 -248
- package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
- package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
- package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
- package/dist/commands/{reflect.js → improve/reflect.js} +33 -28
- package/dist/commands/improve/session-asset.js +248 -0
- package/dist/commands/lint/agent-linter.js +1 -1
- package/dist/commands/lint/base-linter.js +55 -37
- package/dist/commands/lint/command-linter.js +1 -1
- package/dist/commands/lint/default-linter.js +1 -1
- package/dist/commands/lint/env-key-rules.js +1 -1
- package/dist/commands/lint/index.js +19 -25
- package/dist/commands/lint/knowledge-linter.js +1 -1
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +8 -8
- package/dist/commands/lint/skill-linter.js +1 -1
- package/dist/commands/lint/task-linter.js +1 -1
- package/dist/commands/lint/workflow-linter.js +1 -1
- package/dist/commands/lint.js +1 -1
- package/dist/commands/observability-cli.js +244 -0
- package/dist/commands/{proposal-drain-policies.js → proposal/drain-policies.js} +3 -3
- package/dist/commands/{proposal-drain.js → proposal/drain.js} +15 -10
- package/dist/commands/proposal/proposal-cli.js +478 -0
- package/dist/commands/{proposal.js → proposal/proposal.js} +5 -5
- package/dist/commands/{propose.js → proposal/propose.js} +11 -11
- package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
- package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
- package/dist/{core → commands/proposal/validators}/proposals.js +13 -7
- package/dist/commands/{curate.js → read/curate.js} +7 -7
- package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
- package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
- package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
- package/dist/commands/read/search-cli.js +207 -0
- package/dist/commands/{search.js → read/search.js} +22 -27
- package/dist/commands/{show.js → read/show.js} +77 -44
- package/dist/commands/registry-cli.js +8 -8
- package/dist/commands/remember.js +8 -8
- package/dist/commands/sources/add-cli.js +293 -0
- package/dist/commands/{history.js → sources/history.js} +27 -25
- package/dist/commands/{info.js → sources/info.js} +6 -6
- package/dist/commands/{init.js → sources/init.js} +10 -5
- package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
- package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
- package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
- package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
- package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
- package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
- package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
- package/dist/commands/sources/sources-cli.js +305 -0
- package/dist/commands/sources/stash-cli.js +219 -0
- package/dist/commands/sources/stash-skeleton.js +79 -0
- package/dist/commands/tasks/default-tasks.js +173 -0
- package/dist/commands/tasks/tasks-cli.js +210 -0
- package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
- package/dist/commands/wiki-cli.js +307 -0
- package/dist/commands/workflow-cli.js +329 -0
- package/dist/core/action-contributors.js +1 -1
- package/dist/core/assert.js +40 -0
- package/dist/core/asset/asset-create.js +54 -0
- package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
- package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
- package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
- package/dist/core/{markdown.js → asset/markdown.js} +1 -1
- package/dist/core/asset/stash-meta.js +110 -0
- package/dist/core/best-effort.js +64 -0
- package/dist/core/common.js +32 -18
- package/dist/core/{config-io.js → config/config-io.js} +29 -19
- package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
- package/dist/core/{config-schema.js → config/config-schema.js} +45 -1
- package/dist/core/config/config-types.js +16 -0
- package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
- package/dist/core/{config.js → config/config.js} +10 -8
- package/dist/core/env-secret-ref.js +90 -0
- package/dist/core/errors.js +13 -3
- package/dist/core/events.js +27 -4
- package/dist/core/file-lock.js +1 -1
- package/dist/core/improve-types.js +48 -0
- package/dist/core/lesson-lint.js +2 -2
- package/dist/core/paths.js +2 -2
- package/dist/{setup/ripgrep-install.js → core/ripgrep/install.js} +2 -2
- package/dist/{setup/ripgrep-resolve.js → core/ripgrep/resolve.js} +2 -2
- package/dist/core/state-db.js +88 -46
- package/dist/core/text-truncation.js +148 -0
- package/dist/core/time.js +1 -1
- package/dist/core/write-source.js +98 -85
- package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
- package/dist/indexer/{db.js → db/db.js} +126 -116
- package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
- package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
- package/dist/indexer/ensure-index.js +4 -4
- package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
- package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
- package/dist/indexer/indexer.js +37 -30
- package/dist/indexer/init.js +54 -0
- package/dist/indexer/manifest.js +10 -10
- package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +92 -23
- package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
- package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
- package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
- package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
- package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
- package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
- package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
- package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
- package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
- package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
- package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
- package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
- package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
- package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
- package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
- package/dist/indexer/{walker.js → walk/walker.js} +4 -3
- package/dist/integrations/agent/builder-shared.js +39 -0
- package/dist/integrations/agent/builders.js +14 -81
- package/dist/integrations/agent/config.js +6 -4
- package/dist/integrations/agent/detect.js +1 -1
- package/dist/integrations/agent/index.js +23 -8
- package/dist/integrations/agent/prompts.js +2 -3
- package/dist/integrations/agent/runner.js +22 -3
- package/dist/integrations/agent/spawn.js +9 -10
- package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
- package/dist/integrations/harnesses/claude/config-import.js +70 -0
- package/dist/integrations/harnesses/claude/index.js +64 -0
- package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +16 -1
- package/dist/integrations/harnesses/index.js +144 -0
- package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
- package/dist/integrations/harnesses/opencode/config-import.js +82 -0
- package/dist/integrations/harnesses/opencode/index.js +59 -0
- package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
- package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
- package/dist/integrations/harnesses/types.js +43 -0
- package/dist/integrations/lockfile.js +7 -16
- package/dist/integrations/session-logs/index.js +82 -9
- package/dist/llm/call-ai.js +4 -4
- package/dist/llm/client.js +131 -6
- package/dist/llm/embedder.js +6 -6
- package/dist/llm/embedders/local.js +9 -22
- package/dist/llm/embedders/remote.js +2 -2
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +31 -12
- package/dist/llm/index-passes.js +1 -1
- package/dist/llm/memory-infer.js +12 -5
- package/dist/llm/metadata-enhance.js +2 -2
- package/dist/output/context.js +6 -44
- package/dist/output/renderers.js +88 -58
- package/dist/output/shapes/curate.js +7 -3
- package/dist/output/shapes/distill.js +7 -3
- package/dist/output/shapes/env-list.js +18 -16
- package/dist/output/shapes/events.js +5 -4
- package/dist/output/shapes/helpers.js +2 -4
- package/dist/output/shapes/history.js +7 -3
- package/dist/output/shapes/passthrough.js +8 -11
- package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
- package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
- package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
- package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
- package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
- package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
- package/dist/output/shapes/registry-search.js +7 -3
- package/dist/output/shapes/registry.js +12 -0
- package/dist/output/shapes/search.js +7 -3
- package/dist/output/shapes/secret-list.js +18 -16
- package/dist/output/shapes/show.js +7 -3
- package/dist/output/shapes.js +55 -30
- package/dist/output/text/add.js +2 -3
- package/dist/output/text/clone.js +2 -3
- package/dist/output/text/config.js +2 -3
- package/dist/output/text/curate.js +4 -3
- package/dist/output/text/distill.js +2 -3
- package/dist/output/text/enable-disable.js +5 -4
- package/dist/output/text/env.js +13 -0
- package/dist/output/text/events.js +5 -4
- package/dist/output/text/feedback.js +4 -3
- package/dist/output/text/helpers.js +54 -39
- package/dist/output/text/history.js +2 -3
- package/dist/output/text/import.js +2 -3
- package/dist/output/text/index.js +2 -3
- package/dist/output/text/info.js +2 -3
- package/dist/output/text/init.js +2 -3
- package/dist/output/text/list.js +2 -3
- package/dist/output/text/proposal/producer.js +9 -0
- package/dist/output/text/proposal/proposal.js +13 -0
- package/dist/output/text/registry-commands.js +8 -7
- package/dist/output/text/registry.js +12 -0
- package/dist/output/text/remember.js +4 -3
- package/dist/output/text/remove.js +2 -3
- package/dist/output/text/save.js +2 -3
- package/dist/output/text/search.js +4 -3
- package/dist/output/text/show.js +4 -3
- package/dist/output/text/update.js +2 -3
- package/dist/output/text/upgrade.js +2 -3
- package/dist/output/text/wiki.js +12 -11
- package/dist/output/text/workflow.js +12 -10
- package/dist/output/text.js +66 -32
- package/dist/registry/build-index.js +11 -10
- package/dist/registry/factory.js +1 -1
- package/dist/registry/origin-resolve.js +1 -1
- package/dist/registry/providers/index.js +2 -2
- package/dist/registry/providers/skills-sh.js +91 -72
- package/dist/registry/providers/static-index.js +75 -52
- package/dist/registry/resolve.js +3 -3
- package/dist/runtime.js +242 -0
- package/dist/scripts/migrate-storage.js +1594 -673
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +240 -166
- package/dist/setup/detect.js +338 -9
- package/dist/setup/harness-config-import.js +56 -0
- package/dist/setup/registry-stash-loader.js +99 -0
- package/dist/setup/setup.js +664 -96
- package/dist/sources/include.js +1 -1
- package/dist/sources/provider-factory.js +2 -2
- package/dist/sources/providers/filesystem.js +3 -3
- package/dist/sources/providers/git.js +9 -9
- package/dist/sources/providers/index.js +4 -4
- package/dist/sources/providers/npm.js +6 -6
- package/dist/sources/providers/provider-utils.js +13 -20
- package/dist/sources/providers/sync-from-ref.js +5 -5
- package/dist/sources/providers/tar-utils.js +2 -2
- package/dist/sources/providers/website.js +2 -2
- package/dist/sources/resolve.js +5 -5
- package/dist/sources/website-ingest.js +5 -5
- package/dist/storage/database.js +102 -0
- package/dist/storage/engines/sqlite-migrations.js +42 -0
- package/dist/storage/locations.js +25 -0
- package/dist/storage/repositories/index-db.js +43 -0
- package/dist/storage/repositories/workflow-runs-repository.js +141 -0
- package/dist/tasks/backends/cron.js +4 -4
- package/dist/tasks/backends/exec-utils.js +32 -0
- package/dist/tasks/backends/index.js +3 -3
- package/dist/tasks/backends/launchd.js +7 -14
- package/dist/tasks/backends/schtasks.js +7 -16
- package/dist/tasks/embedded.js +71 -0
- package/dist/tasks/parser.js +2 -2
- package/dist/tasks/resolveAkmBin.js +1 -1
- package/dist/tasks/runner.js +28 -15
- package/dist/tasks/schedule.js +1 -1
- package/dist/tasks/validator.js +7 -7
- package/dist/text-import-hook.mjs +51 -0
- package/dist/version.js +2 -1
- package/dist/wiki/wiki.js +7 -7
- package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
- package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
- package/dist/workflows/cli.js +1 -1
- package/dist/workflows/db.js +50 -32
- package/dist/workflows/parser.js +4 -4
- package/dist/workflows/renderer.js +5 -5
- package/dist/workflows/runtime/agent-identity.js +56 -0
- package/dist/workflows/runtime/checkin.js +57 -0
- package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
- package/dist/workflows/validate-summary.js +82 -0
- package/docs/README.md +1 -1
- package/docs/data-and-telemetry.md +6 -6
- package/package.json +16 -8
- package/dist/commands/add-cli.js +0 -279
- package/dist/commands/env.js +0 -213
- package/dist/integrations/agent/sdk-runner.js +0 -126
- package/dist/output/shapes/vault-list.js +0 -19
- package/dist/output/text/proposal-producer.js +0 -8
- package/dist/output/text/proposal.js +0 -12
- package/dist/output/text/vault.js +0 -16
- /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
- /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
- /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
- /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
- /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
- /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
- /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
- /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
package/dist/setup/setup.js
CHANGED
|
@@ -13,20 +13,31 @@ import fs from "node:fs";
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import * as p from "@clack/prompts";
|
|
16
|
-
import { akmInit } from "../commands/init";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
16
|
+
import { akmInit } from "../commands/sources/init.js";
|
|
17
|
+
import { detectServerDefault, isCiEnvironment, registerDefaultTasks } from "../commands/tasks/default-tasks.js";
|
|
18
|
+
import { akmTasksAdd, akmTasksList, akmTasksSetEnabled, akmTasksSync } from "../commands/tasks/tasks.js";
|
|
19
|
+
import { isHttpUrl } from "../core/common.js";
|
|
20
|
+
import { DEFAULT_CONFIG, getDefaultLlmConfig, getEffectiveRegistries, loadUserConfig, saveConfig, } from "../core/config/config.js";
|
|
21
|
+
import { backupExistingConfig } from "../core/config/config-io.js";
|
|
22
|
+
import { ConfigError, UsageError } from "../core/errors.js";
|
|
23
|
+
import { assertSafeStashDir, getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
|
|
24
|
+
import { warn } from "../core/warn.js";
|
|
25
|
+
import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db/db.js";
|
|
26
|
+
import { akmIndex } from "../indexer/indexer.js";
|
|
27
|
+
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/search/semantic-status.js";
|
|
28
|
+
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent/index.js";
|
|
29
|
+
import { defaultProfileName, v1ProfilePlatform } from "../integrations/harnesses/index.js";
|
|
30
|
+
import { probeLlmCapabilities } from "../llm/client.js";
|
|
31
|
+
import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder.js";
|
|
32
|
+
import { getDirname, spawn } from "../runtime.js";
|
|
33
|
+
import { saveGitStash } from "../sources/providers/git.js";
|
|
34
|
+
import { backendNameForPlatform } from "../tasks/backends/index.js";
|
|
35
|
+
import { listEmbeddedTasks } from "../tasks/embedded.js";
|
|
36
|
+
import { parseSchedule } from "../tasks/schedule.js";
|
|
37
|
+
import { detectAgentPlatforms, detectEnvironment, detectLMStudio, detectOllama, renderDetectionSummary, } from "./detect.js";
|
|
38
|
+
import { detectHarnessConfigs } from "./harness-config-import.js";
|
|
39
|
+
import { loadSetupStashes } from "./registry-stash-loader.js";
|
|
40
|
+
import { createSetupContext, runSetupSteps } from "./steps.js";
|
|
30
41
|
// ── Setup sandbox guard ─────────────────────────────────────────────────────
|
|
31
42
|
/**
|
|
32
43
|
* Refuse to persist an explicit `--dir /tmp/...` stashDir to the user's
|
|
@@ -132,11 +143,28 @@ function applyLegacyAgent(config, agent) {
|
|
|
132
143
|
}
|
|
133
144
|
const v2Profiles = { ...(config.profiles?.agent ?? {}) };
|
|
134
145
|
for (const [name, profile] of Object.entries(agent.profiles ?? {})) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
// #566: resolve the platform via the harness registry instead of the old
|
|
147
|
+
// `name.includes("claude") ? "claude" : "opencode"` heuristic, which
|
|
148
|
+
// silently mapped Cursor/Copilot/any new harness to "opencode". An explicit
|
|
149
|
+
// sdkMode flag still wins; otherwise we ask the registry. A name the
|
|
150
|
+
// registry does not recognize is surfaced (warn) rather than silently
|
|
151
|
+
// misclassified, then kept as a best-effort "opencode" profile so the user
|
|
152
|
+
// does not lose a profile they explicitly configured.
|
|
153
|
+
let platform;
|
|
154
|
+
if (profile.sdkMode) {
|
|
155
|
+
platform = "opencode-sdk";
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const resolved = v1ProfilePlatform(name);
|
|
159
|
+
if (resolved) {
|
|
160
|
+
platform = resolved;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
warn(`[akm setup] Agent profile "${name}" did not match any known harness; ` +
|
|
164
|
+
`defaulting its platform to "opencode". Set its platform explicitly in config if this is wrong.`);
|
|
165
|
+
platform = "opencode";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
140
168
|
v2Profiles[name] = {
|
|
141
169
|
platform,
|
|
142
170
|
...(profile.bin ? { bin: profile.bin } : {}),
|
|
@@ -149,22 +177,7 @@ function applyLegacyAgent(config, agent) {
|
|
|
149
177
|
defaults: { ...(config.defaults ?? {}), agent: agent.default },
|
|
150
178
|
};
|
|
151
179
|
}
|
|
152
|
-
|
|
153
|
-
* Recommended GitHub repositories shown during setup.
|
|
154
|
-
*/
|
|
155
|
-
const RECOMMENDED_GITHUB_REPOS = [
|
|
156
|
-
{
|
|
157
|
-
url: "https://github.com/itlackey/akm-stash",
|
|
158
|
-
name: "itlackey/akm-stash",
|
|
159
|
-
hint: "official onboarding stash",
|
|
160
|
-
defaultSelected: true,
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
url: "https://github.com/andrewyng/context-hub",
|
|
164
|
-
name: "andrewyng/context-hub",
|
|
165
|
-
hint: "optional community prompt and context stash",
|
|
166
|
-
},
|
|
167
|
-
];
|
|
180
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
168
181
|
// Approximate first-download sizes used in the setup note.
|
|
169
182
|
// LOCAL_MODEL_APPROX_SIZE_MB tracks the default local model (DEFAULT_LOCAL_MODEL).
|
|
170
183
|
const LOCAL_MODEL_APPROX_SIZE_MB = 130;
|
|
@@ -398,8 +411,8 @@ async function prepareSemanticSearchAssets(config) {
|
|
|
398
411
|
const spin = p.spinner();
|
|
399
412
|
spin.start("Installing @huggingface/transformers...");
|
|
400
413
|
try {
|
|
401
|
-
const pkgRoot = path.resolve(import.meta.
|
|
402
|
-
const proc =
|
|
414
|
+
const pkgRoot = path.resolve(getDirname(import.meta.url), "../..");
|
|
415
|
+
const proc = spawn(["bun", "add", "@huggingface/transformers"], {
|
|
403
416
|
cwd: pkgRoot,
|
|
404
417
|
stdout: "pipe",
|
|
405
418
|
stderr: "pipe",
|
|
@@ -622,12 +635,21 @@ const LLM_PRESETS = [
|
|
|
622
635
|
*
|
|
623
636
|
* @internal Exported for testing only.
|
|
624
637
|
*/
|
|
625
|
-
export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
638
|
+
export async function stepLlm(current, ollamaEndpoint, ollamaChatModels, lmStudio, harnessConfigs) {
|
|
639
|
+
// Build "Import from <Harness>" options and prepend them before LLM_PRESETS
|
|
640
|
+
const harnessOptions = (harnessConfigs ?? []).map((h) => ({
|
|
641
|
+
value: `harness:${h.harnessName}`,
|
|
642
|
+
label: `Import from ${h.harnessName}`,
|
|
643
|
+
hint: [h.provider, h.model].filter(Boolean).join(" / ") || "detected",
|
|
630
644
|
}));
|
|
645
|
+
const options = [
|
|
646
|
+
...harnessOptions,
|
|
647
|
+
...LLM_PRESETS.map((preset) => ({
|
|
648
|
+
value: preset.value,
|
|
649
|
+
label: preset.label,
|
|
650
|
+
hint: preset.hint,
|
|
651
|
+
})),
|
|
652
|
+
];
|
|
631
653
|
const ollamaAvailable = Boolean(ollamaEndpoint && ollamaChatModels && ollamaChatModels.length > 0);
|
|
632
654
|
if (ollamaAvailable) {
|
|
633
655
|
options.push({
|
|
@@ -636,6 +658,10 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
|
636
658
|
hint: ollamaChatModels?.[0] ?? "local",
|
|
637
659
|
});
|
|
638
660
|
}
|
|
661
|
+
const lmStudioHint = lmStudio?.available
|
|
662
|
+
? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
|
|
663
|
+
: "http://localhost:1234";
|
|
664
|
+
options.push({ value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint });
|
|
639
665
|
options.push({ value: "custom", label: "Custom OpenAI-compatible endpoint" });
|
|
640
666
|
options.push({ value: "none", label: "Skip LLM", hint: "no metadata enhancement during indexing" });
|
|
641
667
|
const currentLlm = getCurrentLlm(current);
|
|
@@ -656,6 +682,26 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
|
656
682
|
return cloneLlmConfig(currentLlm);
|
|
657
683
|
if (choice === "none")
|
|
658
684
|
return undefined;
|
|
685
|
+
// Handle "Import from <Harness>" choices
|
|
686
|
+
if (typeof choice === "string" && choice.startsWith("harness:")) {
|
|
687
|
+
const harness = (harnessConfigs ?? []).find((h) => `harness:${h.harnessName}` === choice);
|
|
688
|
+
if (!harness)
|
|
689
|
+
return undefined;
|
|
690
|
+
// Show a summary before accepting
|
|
691
|
+
p.log.info(`Importing LLM config from ${harness.harnessName}: ` +
|
|
692
|
+
[harness.provider, harness.model, harness.baseUrl].filter(Boolean).join(", "));
|
|
693
|
+
const llmConfig = {
|
|
694
|
+
endpoint: harness.baseUrl ?? "",
|
|
695
|
+
model: harness.model ?? "",
|
|
696
|
+
temperature: 0.3,
|
|
697
|
+
maxTokens: 1024,
|
|
698
|
+
};
|
|
699
|
+
if (harness.provider)
|
|
700
|
+
llmConfig.provider = harness.provider;
|
|
701
|
+
if (harness.baseUrl)
|
|
702
|
+
llmConfig.endpoint = harness.baseUrl;
|
|
703
|
+
return llmConfig;
|
|
704
|
+
}
|
|
659
705
|
let llm;
|
|
660
706
|
if (choice === "ollama") {
|
|
661
707
|
const modelChoice = await prompt(() => p.select({
|
|
@@ -671,10 +717,66 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
|
671
717
|
maxTokens: 1024,
|
|
672
718
|
};
|
|
673
719
|
}
|
|
720
|
+
else if (choice === "lmstudio") {
|
|
721
|
+
const currentLmsLlm = currentLlm?.provider === "lmstudio" ? currentLlm : undefined;
|
|
722
|
+
const defaultEndpoint = currentLmsLlm?.endpoint ??
|
|
723
|
+
(lmStudio?.endpoint ? `${lmStudio.endpoint}/v1/chat/completions` : "http://localhost:1234/v1/chat/completions");
|
|
724
|
+
const endpoint = await prompt(() => p.text({
|
|
725
|
+
message: "Endpoint URL:",
|
|
726
|
+
placeholder: defaultEndpoint,
|
|
727
|
+
defaultValue: defaultEndpoint,
|
|
728
|
+
validate: (v) => {
|
|
729
|
+
if (!v?.trim())
|
|
730
|
+
return "Endpoint cannot be empty";
|
|
731
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
732
|
+
return "Must start with http:// or https://";
|
|
733
|
+
},
|
|
734
|
+
}));
|
|
735
|
+
let model;
|
|
736
|
+
const lmsModels = lmStudio?.available && lmStudio.models.length > 0 ? lmStudio.models : [];
|
|
737
|
+
if (lmsModels.length > 0) {
|
|
738
|
+
const modelChoice = await prompt(() => p.select({
|
|
739
|
+
message: "Model name:",
|
|
740
|
+
options: [
|
|
741
|
+
...lmsModels.map((m) => ({ value: m, label: m })),
|
|
742
|
+
{ value: "__manual__", label: "Enter manually..." },
|
|
743
|
+
],
|
|
744
|
+
initialValue: currentLmsLlm?.model && lmsModels.includes(currentLmsLlm.model) ? currentLmsLlm.model : lmsModels[0],
|
|
745
|
+
}));
|
|
746
|
+
if (modelChoice === "__manual__") {
|
|
747
|
+
model = await prompt(() => p.text({
|
|
748
|
+
message: "Model name:",
|
|
749
|
+
placeholder: currentLmsLlm?.model ?? "local-model",
|
|
750
|
+
...(currentLmsLlm?.model ? { defaultValue: currentLmsLlm.model } : {}),
|
|
751
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
752
|
+
}));
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
model = modelChoice;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
model = await prompt(() => p.text({
|
|
760
|
+
message: "Model name:",
|
|
761
|
+
placeholder: currentLmsLlm?.model ?? "local-model",
|
|
762
|
+
...(currentLmsLlm?.model ? { defaultValue: currentLmsLlm.model } : {}),
|
|
763
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
766
|
+
llm = {
|
|
767
|
+
provider: "lmstudio",
|
|
768
|
+
endpoint: endpoint.trim(),
|
|
769
|
+
model: model.trim(),
|
|
770
|
+
temperature: 0.3,
|
|
771
|
+
maxTokens: 1024,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
674
774
|
else if (choice === "custom") {
|
|
775
|
+
const currentCustomLlm = currentLlm?.provider === "custom" ? currentLlm : undefined;
|
|
675
776
|
const endpoint = await prompt(() => p.text({
|
|
676
777
|
message: "OpenAI-compatible chat completions endpoint:",
|
|
677
|
-
placeholder: "https://your-host/v1/chat/completions",
|
|
778
|
+
placeholder: currentCustomLlm?.endpoint ?? "https://your-host/v1/chat/completions",
|
|
779
|
+
...(currentCustomLlm?.endpoint ? { defaultValue: currentCustomLlm.endpoint } : {}),
|
|
678
780
|
validate: (v) => {
|
|
679
781
|
if (!v?.trim())
|
|
680
782
|
return "Endpoint cannot be empty";
|
|
@@ -684,7 +786,8 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
|
684
786
|
}));
|
|
685
787
|
const model = await prompt(() => p.text({
|
|
686
788
|
message: "Model name:",
|
|
687
|
-
placeholder: "gpt-4o-mini",
|
|
789
|
+
placeholder: currentCustomLlm?.model ?? "gpt-4o-mini",
|
|
790
|
+
...(currentCustomLlm?.model ? { defaultValue: currentCustomLlm.model } : {}),
|
|
688
791
|
validate: (v) => {
|
|
689
792
|
if (!v?.trim())
|
|
690
793
|
return "Model name cannot be empty";
|
|
@@ -806,41 +909,47 @@ export async function stepAddSources(current, options) {
|
|
|
806
909
|
if ((current.installed?.length ?? 0) > 0) {
|
|
807
910
|
p.note(renderInstalledSourceList(current.installed ?? []), "Installed managed stashes (preserved)");
|
|
808
911
|
}
|
|
809
|
-
// ──
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
|
|
912
|
+
// ── Registry-driven stash recommendations ─────────────────────────────
|
|
913
|
+
// Fetch available stashes from the official registry (cached, stale-ok).
|
|
914
|
+
// Falls back to the bundled list when the registry is unreachable.
|
|
915
|
+
const registryUrl = getEffectiveRegistries(current)[0]?.url ??
|
|
916
|
+
"https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json";
|
|
917
|
+
const availableStashes = await loadSetupStashes(registryUrl);
|
|
918
|
+
if (availableStashes.length > 0) {
|
|
813
919
|
const existingUrls = new Set(sources.map((s) => s.url));
|
|
814
|
-
const
|
|
815
|
-
value:
|
|
816
|
-
label:
|
|
817
|
-
hint: existingUrls.has(
|
|
920
|
+
const stashOptions = availableStashes.map((s) => ({
|
|
921
|
+
value: s.url,
|
|
922
|
+
label: s.name,
|
|
923
|
+
hint: existingUrls.has(s.url) ? `${s.description} (already added)` : s.description || s.source,
|
|
818
924
|
}));
|
|
925
|
+
// Pre-check: already-installed stashes OR default-selected on fresh install
|
|
819
926
|
const initialValues = sources.length > 0
|
|
820
|
-
?
|
|
821
|
-
:
|
|
822
|
-
const
|
|
823
|
-
message:
|
|
824
|
-
|
|
927
|
+
? stashOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value)
|
|
928
|
+
: availableStashes.filter((s) => s.defaultSelected).map((s) => s.url);
|
|
929
|
+
const selectedUrls = await prompt(() => p.multiselect({
|
|
930
|
+
message: availableStashes[0]?.source === "registry"
|
|
931
|
+
? "Available stashes from the AKM registry — toggle to add or remove:"
|
|
932
|
+
: "Recommended stash sources — toggle to add or remove:",
|
|
933
|
+
options: stashOptions,
|
|
825
934
|
initialValues,
|
|
826
935
|
required: false,
|
|
827
936
|
}));
|
|
828
|
-
// Add newly selected
|
|
829
|
-
for (const url of
|
|
937
|
+
// Add newly selected stashes
|
|
938
|
+
for (const url of selectedUrls) {
|
|
830
939
|
if (!existingUrls.has(url)) {
|
|
831
|
-
const
|
|
832
|
-
sources.push({ type: "git", url, name:
|
|
940
|
+
const entry = availableStashes.find((s) => s.url === url);
|
|
941
|
+
sources.push({ type: "git", url, name: entry?.name });
|
|
833
942
|
existingUrls.add(url);
|
|
834
943
|
}
|
|
835
944
|
}
|
|
836
|
-
// Remove deselected
|
|
837
|
-
for (const
|
|
838
|
-
if (existingUrls.has(
|
|
839
|
-
const idx = sources.findIndex((s) => s.url ===
|
|
945
|
+
// Remove deselected stashes that were previously configured
|
|
946
|
+
for (const entry of availableStashes) {
|
|
947
|
+
if (existingUrls.has(entry.url) && !selectedUrls.includes(entry.url)) {
|
|
948
|
+
const idx = sources.findIndex((s) => s.url === entry.url);
|
|
840
949
|
if (idx !== -1) {
|
|
841
950
|
sources.splice(idx, 1);
|
|
842
|
-
existingUrls.delete(
|
|
843
|
-
p.log.info(`Removed ${
|
|
951
|
+
existingUrls.delete(entry.url);
|
|
952
|
+
p.log.info(`Removed ${entry.name}.`);
|
|
844
953
|
}
|
|
845
954
|
}
|
|
846
955
|
}
|
|
@@ -900,11 +1009,17 @@ export async function stepSmallModelConnection(current) {
|
|
|
900
1009
|
" • akm remember --enrich (memory compression)",
|
|
901
1010
|
" • akm curate --rerank (search reranking)",
|
|
902
1011
|
].join("\n"));
|
|
903
|
-
// Probe for Ollama in the background while showing the note.
|
|
1012
|
+
// Probe for Ollama and LM Studio in the background while showing the note.
|
|
904
1013
|
const spin = p.spinner();
|
|
905
1014
|
spin.start("Detecting local services...");
|
|
906
|
-
const ollama = await detectOllama();
|
|
907
|
-
|
|
1015
|
+
const [ollama, lmStudio] = await Promise.all([detectOllama(), detectLMStudio()]);
|
|
1016
|
+
const detectedServices = [
|
|
1017
|
+
ollama.available ? `Ollama at ${ollama.endpoint}` : null,
|
|
1018
|
+
lmStudio.available ? `LM Studio at ${lmStudio.endpoint}` : null,
|
|
1019
|
+
]
|
|
1020
|
+
.filter(Boolean)
|
|
1021
|
+
.join(", ");
|
|
1022
|
+
spin.stop(detectedServices ? `Detected: ${detectedServices}` : "No local services detected");
|
|
908
1023
|
const ollamaEndpoint = ollama.available ? ollama.endpoint : undefined;
|
|
909
1024
|
const providerOptions = [];
|
|
910
1025
|
if (ollama.available) {
|
|
@@ -914,7 +1029,10 @@ export async function stepSmallModelConnection(current) {
|
|
|
914
1029
|
hint: `detected at ${ollama.endpoint}`,
|
|
915
1030
|
});
|
|
916
1031
|
}
|
|
917
|
-
|
|
1032
|
+
const lmStudioHint = lmStudio.available
|
|
1033
|
+
? `${lmStudio.models.length} model${lmStudio.models.length === 1 ? "" : "s"} detected`
|
|
1034
|
+
: "http://localhost:1234";
|
|
1035
|
+
providerOptions.push({ value: "openai", label: "OpenAI", hint: "requires AKM_LLM_API_KEY" }, { value: "lmstudio", label: "LM Studio / local server", hint: lmStudioHint }, { value: "custom", label: "Custom OpenAI-compatible endpoint" }, { value: "skip", label: "Skip — disable enrichment features" });
|
|
918
1036
|
const currentLlmSmall = getCurrentLlm(current);
|
|
919
1037
|
if (currentLlmSmall) {
|
|
920
1038
|
providerOptions.push({
|
|
@@ -966,10 +1084,11 @@ export async function stepSmallModelConnection(current) {
|
|
|
966
1084
|
}
|
|
967
1085
|
}
|
|
968
1086
|
else {
|
|
1087
|
+
const currentOllamaModel = currentLlmSmall?.provider === "ollama" ? (currentLlmSmall.model ?? "llama3.2") : "llama3.2";
|
|
969
1088
|
model = await prompt(() => p.text({
|
|
970
1089
|
message: "Model name (e.g. llama3.2):",
|
|
971
|
-
placeholder:
|
|
972
|
-
defaultValue:
|
|
1090
|
+
placeholder: currentOllamaModel,
|
|
1091
|
+
defaultValue: currentOllamaModel,
|
|
973
1092
|
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
974
1093
|
}));
|
|
975
1094
|
}
|
|
@@ -982,10 +1101,11 @@ export async function stepSmallModelConnection(current) {
|
|
|
982
1101
|
};
|
|
983
1102
|
}
|
|
984
1103
|
else if (providerChoice === "openai") {
|
|
1104
|
+
const currentOpenAiModel = currentLlmSmall?.provider === "openai" ? (currentLlmSmall.model ?? "gpt-4o-mini") : "gpt-4o-mini";
|
|
985
1105
|
const model = await prompt(() => p.text({
|
|
986
1106
|
message: "Model name:",
|
|
987
|
-
placeholder:
|
|
988
|
-
defaultValue:
|
|
1107
|
+
placeholder: currentOpenAiModel,
|
|
1108
|
+
defaultValue: currentOpenAiModel,
|
|
989
1109
|
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
990
1110
|
}));
|
|
991
1111
|
if (!process.env.AKM_LLM_API_KEY) {
|
|
@@ -994,16 +1114,20 @@ export async function stepSmallModelConnection(current) {
|
|
|
994
1114
|
llm = {
|
|
995
1115
|
provider: "openai",
|
|
996
1116
|
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
997
|
-
model: model.trim() ||
|
|
1117
|
+
model: model.trim() || currentOpenAiModel,
|
|
998
1118
|
temperature: 0.3,
|
|
999
1119
|
maxTokens: 1024,
|
|
1000
1120
|
};
|
|
1001
1121
|
}
|
|
1002
1122
|
else if (providerChoice === "lmstudio") {
|
|
1123
|
+
const currentLmsEndpoint = currentLlmSmall?.provider === "lmstudio"
|
|
1124
|
+
? (currentLlmSmall.endpoint ?? `${lmStudio.endpoint}/v1/chat/completions`)
|
|
1125
|
+
: `${lmStudio.endpoint}/v1/chat/completions`;
|
|
1126
|
+
const currentLmsModel = currentLlmSmall?.provider === "lmstudio" ? currentLlmSmall.model : undefined;
|
|
1003
1127
|
const endpoint = await prompt(() => p.text({
|
|
1004
1128
|
message: "Endpoint URL:",
|
|
1005
|
-
placeholder:
|
|
1006
|
-
defaultValue:
|
|
1129
|
+
placeholder: currentLmsEndpoint,
|
|
1130
|
+
defaultValue: currentLmsEndpoint,
|
|
1007
1131
|
validate: (v) => {
|
|
1008
1132
|
if (!v?.trim())
|
|
1009
1133
|
return "Endpoint cannot be empty";
|
|
@@ -1011,11 +1135,37 @@ export async function stepSmallModelConnection(current) {
|
|
|
1011
1135
|
return "Must start with http:// or https://";
|
|
1012
1136
|
},
|
|
1013
1137
|
}));
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1138
|
+
let model;
|
|
1139
|
+
const lmsModels = lmStudio.available && lmStudio.models.length > 0 ? lmStudio.models : [];
|
|
1140
|
+
if (lmsModels.length > 0) {
|
|
1141
|
+
const modelChoice = await prompt(() => p.select({
|
|
1142
|
+
message: "Model name:",
|
|
1143
|
+
options: [
|
|
1144
|
+
...lmsModels.map((m) => ({ value: m, label: m })),
|
|
1145
|
+
{ value: "__manual__", label: "Enter manually..." },
|
|
1146
|
+
],
|
|
1147
|
+
initialValue: currentLmsModel && lmsModels.includes(currentLmsModel) ? currentLmsModel : lmsModels[0],
|
|
1148
|
+
}));
|
|
1149
|
+
if (modelChoice === "__manual__") {
|
|
1150
|
+
model = await prompt(() => p.text({
|
|
1151
|
+
message: "Model name:",
|
|
1152
|
+
placeholder: currentLmsModel ?? "local-model",
|
|
1153
|
+
...(currentLmsModel ? { defaultValue: currentLmsModel } : {}),
|
|
1154
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
model = modelChoice;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
else {
|
|
1162
|
+
model = await prompt(() => p.text({
|
|
1163
|
+
message: "Model name:",
|
|
1164
|
+
placeholder: currentLmsModel ?? "local-model",
|
|
1165
|
+
...(currentLmsModel ? { defaultValue: currentLmsModel } : {}),
|
|
1166
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1167
|
+
}));
|
|
1168
|
+
}
|
|
1019
1169
|
llm = {
|
|
1020
1170
|
provider: "lmstudio",
|
|
1021
1171
|
endpoint: endpoint.trim(),
|
|
@@ -1026,9 +1176,12 @@ export async function stepSmallModelConnection(current) {
|
|
|
1026
1176
|
}
|
|
1027
1177
|
else {
|
|
1028
1178
|
// custom
|
|
1179
|
+
const currentCustomEndpoint = currentLlmSmall?.provider === "custom" ? currentLlmSmall.endpoint : undefined;
|
|
1180
|
+
const currentCustomModel = currentLlmSmall?.provider === "custom" ? currentLlmSmall.model : undefined;
|
|
1029
1181
|
const endpoint = await prompt(() => p.text({
|
|
1030
1182
|
message: "OpenAI-compatible chat completions endpoint:",
|
|
1031
|
-
placeholder: "https://your-host/v1/chat/completions",
|
|
1183
|
+
placeholder: currentCustomEndpoint ?? "https://your-host/v1/chat/completions",
|
|
1184
|
+
...(currentCustomEndpoint ? { defaultValue: currentCustomEndpoint } : {}),
|
|
1032
1185
|
validate: (v) => {
|
|
1033
1186
|
if (!v?.trim())
|
|
1034
1187
|
return "Endpoint cannot be empty";
|
|
@@ -1038,7 +1191,8 @@ export async function stepSmallModelConnection(current) {
|
|
|
1038
1191
|
}));
|
|
1039
1192
|
const model = await prompt(() => p.text({
|
|
1040
1193
|
message: "Model name:",
|
|
1041
|
-
placeholder: "gpt-4o-mini",
|
|
1194
|
+
placeholder: currentCustomModel ?? "gpt-4o-mini",
|
|
1195
|
+
...(currentCustomModel ? { defaultValue: currentCustomModel } : {}),
|
|
1042
1196
|
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1043
1197
|
}));
|
|
1044
1198
|
const apiKeyInput = await promptOrBack(() => p.text({
|
|
@@ -1150,12 +1304,15 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
1150
1304
|
else {
|
|
1151
1305
|
const baseEndpoint = smallModel.llm.endpoint.replace("/v1/chat/completions", "");
|
|
1152
1306
|
p.log.info(`Endpoint: ${baseEndpoint} (from Step 1)`);
|
|
1307
|
+
const profileName = smallModel.llm.provider ?? "default";
|
|
1308
|
+
// Pre-populate from existing agent profile for this provider, if any.
|
|
1309
|
+
const existingAgentModel = currentAgentBlock?.profiles?.[profileName]?.model ?? smallModel.llm.model ?? undefined;
|
|
1153
1310
|
const agentModel = await prompt(() => p.text({
|
|
1154
1311
|
message: "Model to use for agent tasks (same model is fine, larger models work better):",
|
|
1155
|
-
placeholder: "qwen2.5-coder:32b",
|
|
1312
|
+
placeholder: existingAgentModel ?? "qwen2.5-coder:32b",
|
|
1313
|
+
...(existingAgentModel ? { defaultValue: existingAgentModel } : {}),
|
|
1156
1314
|
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1157
1315
|
}));
|
|
1158
|
-
const profileName = smallModel.llm.provider ?? "default";
|
|
1159
1316
|
return {
|
|
1160
1317
|
...(currentAgentBlock ?? {}),
|
|
1161
1318
|
profiles: {
|
|
@@ -1192,9 +1349,14 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
1192
1349
|
};
|
|
1193
1350
|
}
|
|
1194
1351
|
// "new-connection" (also fall-through from "same-provider" when Step 1 was skipped)
|
|
1352
|
+
// Pre-populate from current "custom" agent profile if available.
|
|
1353
|
+
const currentCustomAgentProfile = currentAgentBlock?.profiles?.custom;
|
|
1354
|
+
const currentNewEndpoint = currentCustomAgentProfile?.endpoint ?? undefined;
|
|
1355
|
+
const currentNewModel = currentCustomAgentProfile?.model ?? undefined;
|
|
1195
1356
|
const newEndpoint = await prompt(() => p.text({
|
|
1196
1357
|
message: "OpenAI-compatible chat completions endpoint:",
|
|
1197
|
-
placeholder: "https://your-host/v1/chat/completions",
|
|
1358
|
+
placeholder: currentNewEndpoint ?? "https://your-host/v1/chat/completions",
|
|
1359
|
+
...(currentNewEndpoint ? { defaultValue: currentNewEndpoint } : {}),
|
|
1198
1360
|
validate: (v) => {
|
|
1199
1361
|
if (!v?.trim())
|
|
1200
1362
|
return "Endpoint cannot be empty";
|
|
@@ -1208,7 +1370,8 @@ export async function stepAgentConnection(current, smallModel) {
|
|
|
1208
1370
|
}));
|
|
1209
1371
|
const newModel = await prompt(() => p.text({
|
|
1210
1372
|
message: "Model name (larger is better, e.g. gpt-4o):",
|
|
1211
|
-
placeholder: "gpt-4o",
|
|
1373
|
+
placeholder: currentNewModel ?? "gpt-4o",
|
|
1374
|
+
...(currentNewModel ? { defaultValue: currentNewModel } : {}),
|
|
1212
1375
|
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1213
1376
|
}));
|
|
1214
1377
|
const customProfile = {
|
|
@@ -1326,6 +1489,165 @@ export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles
|
|
|
1326
1489
|
return { agent, detections };
|
|
1327
1490
|
}
|
|
1328
1491
|
// ── Main Wizard ─────────────────────────────────────────────────────────────
|
|
1492
|
+
/**
|
|
1493
|
+
* Normalise a task id the same way `akm tasks` does (strip a trailing `.yml`
|
|
1494
|
+
* / `.md` suffix, trim) so the wizard can match embedded template ids against
|
|
1495
|
+
* the ids reported by `akmTasksList()`.
|
|
1496
|
+
*/
|
|
1497
|
+
function normaliseTaskIdForMatch(raw) {
|
|
1498
|
+
return raw.trim().replace(/\.(yml|md)$/, "");
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Interactive-only setup step: enable/disable embedded core tasks.
|
|
1502
|
+
*
|
|
1503
|
+
* Presents a multi-select of the bundled core task templates pre-checked
|
|
1504
|
+
* against the user's currently-enabled tasks. On confirm:
|
|
1505
|
+
* - newly-checked & absent → copy template (with edited schedule) into the
|
|
1506
|
+
* primary stash via `akmTasksAdd`, then `akmTasksSync`, then `akm sync`
|
|
1507
|
+
* (a no-op for non-git stashes).
|
|
1508
|
+
* - newly-checked & present-but-disabled → `akmTasksSetEnabled(id, true)`.
|
|
1509
|
+
* - previously-enabled & now unchecked → `akmTasksSetEnabled(id, false)`
|
|
1510
|
+
* (keeps the stash file, removes the scheduler entry).
|
|
1511
|
+
* - unchanged → no action.
|
|
1512
|
+
*
|
|
1513
|
+
* Exported for testing. Not registered as `nonInteractive`, so `akm init` /
|
|
1514
|
+
* `--yes` never reach it.
|
|
1515
|
+
*
|
|
1516
|
+
* The task primitives + git-sync helper are injected via `deps` (defaulting
|
|
1517
|
+
* to the real implementations) so tests can supply fakes without
|
|
1518
|
+
* `mock.module`-ing the shared `commands/tasks` / `sources/providers/git`
|
|
1519
|
+
* modules — which would leak into unrelated test files (Bun's `mock.module`
|
|
1520
|
+
* is process-global and not reverted by `mock.restore()`).
|
|
1521
|
+
*/
|
|
1522
|
+
/**
|
|
1523
|
+
* Setup sub-step (issue #552): idempotently register the default improve task
|
|
1524
|
+
* set. Asks a single "Is this a server install?" question (defaulting per
|
|
1525
|
+
* platform) to decide whether the nightly sweep is enabled, then delegates to
|
|
1526
|
+
* {@link registerDefaultTasks}, which is CI-aware and never duplicates an
|
|
1527
|
+
* existing task. Skipped entirely under CI (the registration helper short-
|
|
1528
|
+
* circuits, and we never even prompt).
|
|
1529
|
+
*
|
|
1530
|
+
* Exported for testing.
|
|
1531
|
+
*/
|
|
1532
|
+
export async function stepDefaultImproveTasks(register = registerDefaultTasks) {
|
|
1533
|
+
// CI: register nothing and don't prompt.
|
|
1534
|
+
if (isCiEnvironment()) {
|
|
1535
|
+
p.log.info("CI detected — skipping default improve task registration.");
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const platformDefault = detectServerDefault();
|
|
1539
|
+
const serverInstall = await prompt(() => p.confirm({
|
|
1540
|
+
message: "Is this a server install? (enables the nightly quality sweep at 2am)",
|
|
1541
|
+
initialValue: platformDefault,
|
|
1542
|
+
}));
|
|
1543
|
+
const result = await register({ serverInstall: serverInstall === true });
|
|
1544
|
+
if (result.skipped)
|
|
1545
|
+
return;
|
|
1546
|
+
const total = result.created.length + result.existing.length;
|
|
1547
|
+
p.log.success(`Default improve tasks registered (${result.created.length} new, ${result.existing.length} already present, ${total} total).`);
|
|
1548
|
+
}
|
|
1549
|
+
const DEFAULT_SCHEDULED_TASKS_DEPS = {
|
|
1550
|
+
list: akmTasksList,
|
|
1551
|
+
add: akmTasksAdd,
|
|
1552
|
+
setEnabled: akmTasksSetEnabled,
|
|
1553
|
+
sync: akmTasksSync,
|
|
1554
|
+
gitSync: saveGitStash,
|
|
1555
|
+
};
|
|
1556
|
+
export async function stepScheduledTasks(deps = DEFAULT_SCHEDULED_TASKS_DEPS) {
|
|
1557
|
+
const embedded = listEmbeddedTasks();
|
|
1558
|
+
if (embedded.length === 0)
|
|
1559
|
+
return;
|
|
1560
|
+
// Snapshot current state so we can diff against the user's selection.
|
|
1561
|
+
let installed = [];
|
|
1562
|
+
try {
|
|
1563
|
+
installed = (await deps.list()).tasks;
|
|
1564
|
+
}
|
|
1565
|
+
catch {
|
|
1566
|
+
// A missing/empty tasks dir is fine — treat as nothing installed.
|
|
1567
|
+
installed = [];
|
|
1568
|
+
}
|
|
1569
|
+
const byId = new Map();
|
|
1570
|
+
for (const t of installed)
|
|
1571
|
+
byId.set(normaliseTaskIdForMatch(t.id), t);
|
|
1572
|
+
// Pre-check tasks that are installed AND enabled.
|
|
1573
|
+
const preChecked = embedded.filter((e) => byId.get(e.id)?.enabled === true).map((e) => e.id);
|
|
1574
|
+
const stateLabel = (e) => {
|
|
1575
|
+
const cur = byId.get(e.id);
|
|
1576
|
+
if (!cur)
|
|
1577
|
+
return "not installed";
|
|
1578
|
+
return cur.enabled ? "enabled" : "disabled";
|
|
1579
|
+
};
|
|
1580
|
+
const selected = await prompt(() => p.multiselect({
|
|
1581
|
+
message: "Enable scheduled core tasks? (space to toggle, enter to confirm)",
|
|
1582
|
+
required: false,
|
|
1583
|
+
initialValues: preChecked,
|
|
1584
|
+
options: embedded.map((e) => ({
|
|
1585
|
+
value: e.id,
|
|
1586
|
+
label: e.label,
|
|
1587
|
+
hint: `${e.description} — ${e.schedule} [${stateLabel(e)}]`,
|
|
1588
|
+
})),
|
|
1589
|
+
}));
|
|
1590
|
+
const selectedSet = new Set(selected);
|
|
1591
|
+
// Resolve per-task schedule edits for newly-checked, not-yet-installed tasks.
|
|
1592
|
+
const scheduleFor = new Map();
|
|
1593
|
+
for (const e of embedded) {
|
|
1594
|
+
const cur = byId.get(e.id);
|
|
1595
|
+
if (selectedSet.has(e.id) && !cur) {
|
|
1596
|
+
const edited = await prompt(() => p.text({
|
|
1597
|
+
message: `Schedule for ${e.label}?`,
|
|
1598
|
+
initialValue: e.schedule,
|
|
1599
|
+
validate(value) {
|
|
1600
|
+
const candidate = (value ?? "").trim() || e.schedule;
|
|
1601
|
+
try {
|
|
1602
|
+
parseSchedule(candidate, backendNameForPlatform());
|
|
1603
|
+
}
|
|
1604
|
+
catch (err) {
|
|
1605
|
+
return err instanceof Error ? err.message : "Invalid schedule.";
|
|
1606
|
+
}
|
|
1607
|
+
return undefined;
|
|
1608
|
+
},
|
|
1609
|
+
}));
|
|
1610
|
+
const sched = (edited ?? "").trim() || e.schedule;
|
|
1611
|
+
scheduleFor.set(e.id, sched);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
let syncNeeded = false;
|
|
1615
|
+
for (const e of embedded) {
|
|
1616
|
+
const cur = byId.get(e.id);
|
|
1617
|
+
const checked = selectedSet.has(e.id);
|
|
1618
|
+
if (checked && !cur) {
|
|
1619
|
+
// New task: copy template into the primary stash + install scheduler entry.
|
|
1620
|
+
const schedule = scheduleFor.get(e.id) ?? e.schedule;
|
|
1621
|
+
await deps.add({
|
|
1622
|
+
id: e.id,
|
|
1623
|
+
schedule,
|
|
1624
|
+
command: e.command,
|
|
1625
|
+
description: e.description,
|
|
1626
|
+
});
|
|
1627
|
+
syncNeeded = true;
|
|
1628
|
+
}
|
|
1629
|
+
else if (checked && cur && !cur.enabled) {
|
|
1630
|
+
// Present but disabled → re-enable.
|
|
1631
|
+
await deps.setEnabled(e.id, true);
|
|
1632
|
+
}
|
|
1633
|
+
else if (!checked && cur?.enabled) {
|
|
1634
|
+
// Previously enabled, now unchecked → disable (keep the stash file).
|
|
1635
|
+
await deps.setEnabled(e.id, false);
|
|
1636
|
+
}
|
|
1637
|
+
// No state change → no action.
|
|
1638
|
+
}
|
|
1639
|
+
if (syncNeeded) {
|
|
1640
|
+
// Reconcile scheduler entries with on-disk YAML, then commit the new file
|
|
1641
|
+
// to git (a no-op for non-git stashes).
|
|
1642
|
+
await deps.sync();
|
|
1643
|
+
try {
|
|
1644
|
+
deps.gitSync(undefined, "akm setup: enable scheduled tasks");
|
|
1645
|
+
}
|
|
1646
|
+
catch {
|
|
1647
|
+
// Non-fatal — the task is installed regardless of git sync outcome.
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1329
1651
|
/**
|
|
1330
1652
|
* Build the canonical list of `SetupStep`s for the interactive wizard.
|
|
1331
1653
|
* Exposed (and exported) so tests and `akm init` can compose subsets.
|
|
@@ -1340,6 +1662,10 @@ export function buildSetupSteps(options) {
|
|
|
1340
1662
|
// to the LLM step. Mutable by design — `stepLlm` needs them.
|
|
1341
1663
|
let ollamaEndpoint;
|
|
1342
1664
|
let ollamaChatModels;
|
|
1665
|
+
let lmStudioResult;
|
|
1666
|
+
// Harness configs detected once and shared with the LLM step. Reuse the
|
|
1667
|
+
// aggregate detection's harness configs when available so we detect once.
|
|
1668
|
+
const harnessConfigs = options.detection?.harnessConfigs ?? detectHarnessConfigs();
|
|
1343
1669
|
const steps = [
|
|
1344
1670
|
{
|
|
1345
1671
|
id: "stash-dir",
|
|
@@ -1361,9 +1687,10 @@ export function buildSetupSteps(options) {
|
|
|
1361
1687
|
ctx.apply({ embedding: ctx.config.embedding });
|
|
1362
1688
|
return;
|
|
1363
1689
|
}
|
|
1364
|
-
const result = await stepOllama(ctx.config);
|
|
1690
|
+
const [result, lmStudio] = await Promise.all([stepOllama(ctx.config), detectLMStudio()]);
|
|
1365
1691
|
ollamaEndpoint = result.ollamaEndpoint;
|
|
1366
1692
|
ollamaChatModels = result.ollamaChatModels;
|
|
1693
|
+
lmStudioResult = lmStudio;
|
|
1367
1694
|
ctx.apply({ embedding: result.embedding });
|
|
1368
1695
|
},
|
|
1369
1696
|
},
|
|
@@ -1374,7 +1701,7 @@ export function buildSetupSteps(options) {
|
|
|
1374
1701
|
if (!options.online) {
|
|
1375
1702
|
return;
|
|
1376
1703
|
}
|
|
1377
|
-
const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels);
|
|
1704
|
+
const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels, lmStudioResult, harnessConfigs);
|
|
1378
1705
|
ctx.apply(applyLegacyLlm(ctx.config, llm));
|
|
1379
1706
|
},
|
|
1380
1707
|
},
|
|
@@ -1438,6 +1765,16 @@ export function buildSetupSteps(options) {
|
|
|
1438
1765
|
ctx.apply({ output });
|
|
1439
1766
|
},
|
|
1440
1767
|
},
|
|
1768
|
+
{
|
|
1769
|
+
id: "scheduled-tasks",
|
|
1770
|
+
label: "Scheduled Tasks",
|
|
1771
|
+
// Interactive-only: `akm init` / `--yes` skip this step so headless
|
|
1772
|
+
// runs never enable a scheduled task (see issue #512).
|
|
1773
|
+
async run() {
|
|
1774
|
+
await stepDefaultImproveTasks();
|
|
1775
|
+
await stepScheduledTasks();
|
|
1776
|
+
},
|
|
1777
|
+
},
|
|
1441
1778
|
];
|
|
1442
1779
|
return { steps, outcome };
|
|
1443
1780
|
}
|
|
@@ -1462,11 +1799,28 @@ export async function runSetupWizard(opts) {
|
|
|
1462
1799
|
p.log.warn("No network connectivity detected. Skipping Ollama detection and remote embedding checks.\n" +
|
|
1463
1800
|
"Local-only setup will continue. Re-run `akm setup` when online for full configuration.");
|
|
1464
1801
|
}
|
|
1802
|
+
// Aggregate environment detection — run once before any prompt and surface
|
|
1803
|
+
// a summary so the user sees what was auto-detected. NAMES only, never
|
|
1804
|
+
// API key values.
|
|
1805
|
+
const detection = await detectEnvironment({ existingStashDir: current.stashDir });
|
|
1806
|
+
p.note(renderDetectionSummary(detection), "Detected environment");
|
|
1807
|
+
// Interactive entry point for `--reset-recommended`: offer to apply the
|
|
1808
|
+
// opinionated, detection-derived defaults and skip the step-by-step wizard.
|
|
1809
|
+
const useRecommended = await prompt(() => p.confirm({
|
|
1810
|
+
message: "Apply recommended defaults from the detected environment (merged into your existing config)?",
|
|
1811
|
+
initialValue: false,
|
|
1812
|
+
}));
|
|
1813
|
+
if (useRecommended) {
|
|
1814
|
+
const result = await runResetRecommended({ dir: opts?.dir, noInit: opts?.noInit });
|
|
1815
|
+
p.outro(`Recommended configuration saved to ${result.configPath}`);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1465
1818
|
const ctx = createSetupContext(current, { nonInteractive: false });
|
|
1466
1819
|
const { steps, outcome } = buildSetupSteps({
|
|
1467
1820
|
online,
|
|
1468
1821
|
semanticSearchOutcome: { mode: current.semanticSearchMode, prepareAssets: false },
|
|
1469
1822
|
preferredStashDir: resolvedStashDir,
|
|
1823
|
+
detection,
|
|
1470
1824
|
});
|
|
1471
1825
|
// Wrap each step with a `p.log.step()` header so the wizard UI is
|
|
1472
1826
|
// unchanged. The canonical `runSetupSteps()` runner is used directly by
|
|
@@ -1521,6 +1875,8 @@ export async function runSetupWizard(opts) {
|
|
|
1521
1875
|
if (!shouldSave)
|
|
1522
1876
|
bail();
|
|
1523
1877
|
// Save config
|
|
1878
|
+
const cfgPath1 = getConfigPath();
|
|
1879
|
+
backupAndAnnounce(cfgPath1);
|
|
1524
1880
|
saveConfig(newConfig);
|
|
1525
1881
|
if (semanticSearchMode.mode === "off") {
|
|
1526
1882
|
clearSemanticStatus();
|
|
@@ -1604,6 +1960,20 @@ export async function runSetupWizard(opts) {
|
|
|
1604
1960
|
p.outro(`Configuration saved to ${configPath}`);
|
|
1605
1961
|
}
|
|
1606
1962
|
// ── Non-interactive / scripting entry points ─────────────────────────────────
|
|
1963
|
+
/**
|
|
1964
|
+
* Back up an existing config file and print the real, timestamped backup
|
|
1965
|
+
* location (not a generic display string). On a fresh install where there is
|
|
1966
|
+
* nothing to back up, print a "nothing to back up" notice instead.
|
|
1967
|
+
*/
|
|
1968
|
+
function backupAndAnnounce(configPath) {
|
|
1969
|
+
const result = backupExistingConfig(configPath);
|
|
1970
|
+
if (result) {
|
|
1971
|
+
p.log.info(`Config backed up to ${result.timestamped}`);
|
|
1972
|
+
}
|
|
1973
|
+
else {
|
|
1974
|
+
p.log.info("No existing config to back up.");
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1607
1977
|
/**
|
|
1608
1978
|
* Run setup in non-interactive mode, applying all defaults.
|
|
1609
1979
|
* Safe to call from CI or scripts. Idempotent — re-running produces the same result.
|
|
@@ -1629,14 +1999,41 @@ export async function runSetupWithDefaults(opts) {
|
|
|
1629
1999
|
// Ensure stashDir is set
|
|
1630
2000
|
if (!ctx.config.stashDir)
|
|
1631
2001
|
ctx.apply({ stashDir });
|
|
2002
|
+
// Aggregate environment detection — apply detected values directly.
|
|
2003
|
+
const env = await detectEnvironment({ existingStashDir: ctx.config.stashDir });
|
|
2004
|
+
// Apply a detected LLM (live local server) when the config has none yet.
|
|
2005
|
+
if (!getDefaultLlmConfig(ctx.config)) {
|
|
2006
|
+
const liveLocal = env.localServers.find((s) => s.available && s.defaultModel);
|
|
2007
|
+
if (liveLocal?.defaultModel) {
|
|
2008
|
+
const llm = {
|
|
2009
|
+
provider: "local",
|
|
2010
|
+
endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1`,
|
|
2011
|
+
model: liveLocal.defaultModel,
|
|
2012
|
+
};
|
|
2013
|
+
// A required field being unresolvable must fail loudly rather than write
|
|
2014
|
+
// a broken config (--yes acceptance criterion).
|
|
2015
|
+
if (!llm.endpoint?.trim() || !llm.model?.trim()) {
|
|
2016
|
+
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");
|
|
2017
|
+
}
|
|
2018
|
+
ctx.apply(applyLegacyLlm(ctx.config, llm));
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
1632
2021
|
// Auto-detect agent CLI if not already configured
|
|
1633
2022
|
if (!ctx.config.defaults?.agent) {
|
|
1634
|
-
|
|
1635
|
-
|
|
2023
|
+
let defaultProfile;
|
|
2024
|
+
if (env.harness !== "none") {
|
|
2025
|
+
defaultProfile = env.harness;
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
const detected = detectAgentCliProfiles(undefined);
|
|
2029
|
+
defaultProfile = pickDefaultAgentProfile(detected, undefined);
|
|
2030
|
+
}
|
|
1636
2031
|
if (defaultProfile) {
|
|
1637
2032
|
ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
|
|
1638
2033
|
}
|
|
1639
2034
|
}
|
|
2035
|
+
const cfgPath2 = getConfigPath();
|
|
2036
|
+
backupAndAnnounce(cfgPath2);
|
|
1640
2037
|
saveConfig(ctx.config);
|
|
1641
2038
|
return {
|
|
1642
2039
|
configPath: getConfigPath(),
|
|
@@ -1647,6 +2044,147 @@ export async function runSetupWithDefaults(opts) {
|
|
|
1647
2044
|
ripgrep: initResult?.ripgrep,
|
|
1648
2045
|
};
|
|
1649
2046
|
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Recursively merge `incoming` into `base`: plain objects merge key-by-key,
|
|
2049
|
+
* while arrays and scalars replace wholesale. A partial input therefore only
|
|
2050
|
+
* updates the keys it carries and never drops sibling subkeys (e.g. a file
|
|
2051
|
+
* containing `{ output: { format: "text" } }` leaves `output.detail` intact).
|
|
2052
|
+
*
|
|
2053
|
+
* `base` is treated as immutable — a fresh object graph is returned.
|
|
2054
|
+
*/
|
|
2055
|
+
function deepMergeConfig(base, incoming) {
|
|
2056
|
+
if (!isPlainObject(incoming))
|
|
2057
|
+
return incoming;
|
|
2058
|
+
const baseObj = isPlainObject(base) ? base : {};
|
|
2059
|
+
const out = { ...baseObj };
|
|
2060
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
2061
|
+
if (value === undefined)
|
|
2062
|
+
continue;
|
|
2063
|
+
if (isPlainObject(value) && isPlainObject(baseObj[key])) {
|
|
2064
|
+
out[key] = deepMergeConfig(baseObj[key], value);
|
|
2065
|
+
}
|
|
2066
|
+
else {
|
|
2067
|
+
out[key] = value;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return out;
|
|
2071
|
+
}
|
|
2072
|
+
/** True for non-null, non-array plain objects. */
|
|
2073
|
+
function isPlainObject(value) {
|
|
2074
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Run ONLY environment detection and return the typed result. Performs no
|
|
2078
|
+
* config writes and shows no prompts. Backs `akm setup --detect-only`.
|
|
2079
|
+
*
|
|
2080
|
+
* SAFETY: The returned object carries env var NAMES only — never any API key
|
|
2081
|
+
* value.
|
|
2082
|
+
*/
|
|
2083
|
+
export async function runDetectOnly() {
|
|
2084
|
+
const current = loadUserConfig();
|
|
2085
|
+
return detectEnvironment({ existingStashDir: current.stashDir });
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Derive opinionated defaults from a detection result.
|
|
2089
|
+
*
|
|
2090
|
+
* - Best harness → agent default (when a profile maps to it).
|
|
2091
|
+
* - Fastest live local model, else the first detected cloud key's provider.
|
|
2092
|
+
* - `nomic-embed-text` embeddings when a local LLM is live.
|
|
2093
|
+
* - improve task `0 2 * * *`, index task `0 4 * * *`.
|
|
2094
|
+
*
|
|
2095
|
+
* Returns a partial `AkmConfig`-shaped object plus a legacy `llm` block, ready
|
|
2096
|
+
* to merge. Never includes an API key value.
|
|
2097
|
+
*/
|
|
2098
|
+
export function deriveRecommendedConfig(env) {
|
|
2099
|
+
const result = {};
|
|
2100
|
+
// Best harness → agent default. #566: derive the default profile name from
|
|
2101
|
+
// the harness registry instead of a hardcoded if-chain, so a newly added
|
|
2102
|
+
// dispatch-capable harness gets a usable headless default (its canonical id)
|
|
2103
|
+
// automatically. "none" / unknown ids resolve to undefined (no default).
|
|
2104
|
+
const agentDefault = defaultProfileName(env.harness);
|
|
2105
|
+
if (agentDefault)
|
|
2106
|
+
result.agentDefault = agentDefault;
|
|
2107
|
+
// LLM: prefer a live local server, else a detected cloud provider key.
|
|
2108
|
+
const liveLocal = env.localServers.find((s) => s.available && s.defaultModel);
|
|
2109
|
+
if (liveLocal?.defaultModel) {
|
|
2110
|
+
result.llm = {
|
|
2111
|
+
provider: "local",
|
|
2112
|
+
endpoint: `${liveLocal.baseUrl.replace(/\/$/, "")}/v1`,
|
|
2113
|
+
model: liveLocal.defaultModel,
|
|
2114
|
+
};
|
|
2115
|
+
// Local LLM live → use a local embedding model.
|
|
2116
|
+
result.embedding = { provider: "ollama", model: "nomic-embed-text", endpoint: `${liveLocal.baseUrl}/v1` };
|
|
2117
|
+
}
|
|
2118
|
+
else {
|
|
2119
|
+
// Map a detected cloud API-key provider to an llm endpoint. NAMES only —
|
|
2120
|
+
// the value lives in the env var the user already set; we never read it.
|
|
2121
|
+
const cloud = env.providers.find((pr) => pr.kind === "apiKey");
|
|
2122
|
+
if (cloud) {
|
|
2123
|
+
const endpoint = cloudEndpointForProvider(cloud.provider);
|
|
2124
|
+
const model = cloudDefaultModelForProvider(cloud.provider);
|
|
2125
|
+
if (endpoint && model) {
|
|
2126
|
+
result.llm = { provider: cloud.provider, endpoint, model };
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
result.taskSchedules = { improve: "0 2 * * *", index: "0 4 * * *" };
|
|
2131
|
+
return result;
|
|
2132
|
+
}
|
|
2133
|
+
function cloudEndpointForProvider(provider) {
|
|
2134
|
+
switch (provider) {
|
|
2135
|
+
case "anthropic":
|
|
2136
|
+
return "https://api.anthropic.com/v1";
|
|
2137
|
+
case "openai":
|
|
2138
|
+
return "https://api.openai.com/v1";
|
|
2139
|
+
case "gemini":
|
|
2140
|
+
return "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
2141
|
+
case "groq":
|
|
2142
|
+
return "https://api.groq.com/openai/v1";
|
|
2143
|
+
default:
|
|
2144
|
+
return undefined;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function cloudDefaultModelForProvider(provider) {
|
|
2148
|
+
switch (provider) {
|
|
2149
|
+
case "anthropic":
|
|
2150
|
+
return "claude-sonnet-4-5";
|
|
2151
|
+
case "openai":
|
|
2152
|
+
return "gpt-4o-mini";
|
|
2153
|
+
case "gemini":
|
|
2154
|
+
return "gemini-1.5-flash";
|
|
2155
|
+
case "groq":
|
|
2156
|
+
return "llama-3.3-70b-versatile";
|
|
2157
|
+
default:
|
|
2158
|
+
return undefined;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* `akm setup --reset-recommended`: merge opinionated, detection-derived
|
|
2163
|
+
* defaults into the existing config WITHOUT removing pre-existing custom keys.
|
|
2164
|
+
* Uses the same merge path as {@link runSetupFromConfig} so custom keys survive
|
|
2165
|
+
* (follows #511 semantics).
|
|
2166
|
+
*/
|
|
2167
|
+
export async function runResetRecommended(opts) {
|
|
2168
|
+
const current = loadUserConfig();
|
|
2169
|
+
const env = await detectEnvironment({ existingStashDir: current.stashDir });
|
|
2170
|
+
const recommended = deriveRecommendedConfig(env);
|
|
2171
|
+
const incoming = {};
|
|
2172
|
+
if (recommended.llm)
|
|
2173
|
+
incoming.llm = recommended.llm;
|
|
2174
|
+
if (recommended.embedding)
|
|
2175
|
+
incoming.embedding = recommended.embedding;
|
|
2176
|
+
if (recommended.agentDefault)
|
|
2177
|
+
incoming.agent = { default: recommended.agentDefault };
|
|
2178
|
+
if (recommended.taskSchedules) {
|
|
2179
|
+
incoming.setup = { taskSchedules: recommended.taskSchedules };
|
|
2180
|
+
}
|
|
2181
|
+
return runSetupFromConfig({
|
|
2182
|
+
configJson: JSON.stringify(incoming),
|
|
2183
|
+
dir: opts.dir,
|
|
2184
|
+
noInit: opts.noInit,
|
|
2185
|
+
probe: opts.probe,
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
1650
2188
|
/**
|
|
1651
2189
|
* Apply a JSON config blob non-interactively, merging it with the current config.
|
|
1652
2190
|
* Validates required sub-fields and strips unknown/restricted keys.
|
|
@@ -1669,6 +2207,7 @@ export async function runSetupFromConfig(opts) {
|
|
|
1669
2207
|
"output",
|
|
1670
2208
|
"profiles",
|
|
1671
2209
|
"defaults",
|
|
2210
|
+
"setup",
|
|
1672
2211
|
]);
|
|
1673
2212
|
for (const key of Object.keys(incoming)) {
|
|
1674
2213
|
if (!ALLOWED_KEYS.has(key)) {
|
|
@@ -1700,12 +2239,16 @@ export async function runSetupFromConfig(opts) {
|
|
|
1700
2239
|
assertSetupSandbox(stashDir, stashDirExplicit);
|
|
1701
2240
|
applyStashIsolationToEnv(stashDir, stashDirExplicit);
|
|
1702
2241
|
let merged = { ...current, stashDir };
|
|
1703
|
-
//
|
|
1704
|
-
|
|
2242
|
+
// Deep-merge non-llm/agent keys: nested objects merge key-by-key so a
|
|
2243
|
+
// partial `--file` only updates the keys it carries and never drops sibling
|
|
2244
|
+
// subkeys (e.g. output.detail survives an output.format-only file). Arrays
|
|
2245
|
+
// and scalars replace wholesale.
|
|
1705
2246
|
for (const key of Object.keys(incoming)) {
|
|
1706
2247
|
if (key === "llm" || key === "agent")
|
|
1707
2248
|
continue;
|
|
1708
|
-
|
|
2249
|
+
const incomingVal = incoming[key];
|
|
2250
|
+
const mergedRec = merged;
|
|
2251
|
+
mergedRec[key] = deepMergeConfig(mergedRec[key], incomingVal);
|
|
1709
2252
|
}
|
|
1710
2253
|
// Translate legacy llm/agent inputs into the new shape.
|
|
1711
2254
|
if (incoming.llm) {
|
|
@@ -1714,6 +2257,29 @@ export async function runSetupFromConfig(opts) {
|
|
|
1714
2257
|
if (incoming.agent) {
|
|
1715
2258
|
merged = { ...merged, ...applyLegacyAgent(merged, incoming.agent) };
|
|
1716
2259
|
}
|
|
2260
|
+
// With `--yes`, fill keys still missing after the merge with non-interactive
|
|
2261
|
+
// defaults. Steps start from `merged` and their nonInteractive path only
|
|
2262
|
+
// populates absent values, so nothing the file or existing config supplied
|
|
2263
|
+
// is overwritten.
|
|
2264
|
+
if (opts.applyDefaults) {
|
|
2265
|
+
const ctx = createSetupContext(merged, { nonInteractive: true });
|
|
2266
|
+
const { steps } = buildSetupSteps({
|
|
2267
|
+
online: false,
|
|
2268
|
+
semanticSearchOutcome: { mode: merged.semanticSearchMode, prepareAssets: false },
|
|
2269
|
+
preferredStashDir: stashDir,
|
|
2270
|
+
});
|
|
2271
|
+
await runSetupSteps(steps, ctx);
|
|
2272
|
+
if (!ctx.config.stashDir)
|
|
2273
|
+
ctx.apply({ stashDir });
|
|
2274
|
+
if (!ctx.config.defaults?.agent) {
|
|
2275
|
+
const detected = detectAgentCliProfiles(undefined);
|
|
2276
|
+
const defaultProfile = pickDefaultAgentProfile(detected, undefined);
|
|
2277
|
+
if (defaultProfile) {
|
|
2278
|
+
ctx.apply(applyLegacyAgent(ctx.config, { default: defaultProfile }));
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
merged = ctx.config;
|
|
2282
|
+
}
|
|
1717
2283
|
// Bootstrap directory structure
|
|
1718
2284
|
let initResult;
|
|
1719
2285
|
if (!opts.noInit) {
|
|
@@ -1738,6 +2304,8 @@ export async function runSetupFromConfig(opts) {
|
|
|
1738
2304
|
// Non-fatal: probe failure is informational only
|
|
1739
2305
|
}
|
|
1740
2306
|
}
|
|
2307
|
+
const cfgPath3 = getConfigPath();
|
|
2308
|
+
backupAndAnnounce(cfgPath3);
|
|
1741
2309
|
saveConfig(merged);
|
|
1742
2310
|
return {
|
|
1743
2311
|
configPath: getConfigPath(),
|