akm-cli 0.6.0 → 0.7.0-rc1
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 +66 -0
- package/dist/{cli.js → src/cli.js} +672 -29
- package/dist/{commands → src/commands}/config-cli.js +5 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/{commands → src/commands}/installed-stashes.js +28 -2
- package/dist/src/commands/proposal.js +119 -0
- package/dist/src/commands/propose.js +171 -0
- package/dist/src/commands/reflect.js +193 -0
- package/dist/{commands → src/commands}/registry-search.js +2 -1
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +74 -1
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +67 -2
- package/dist/{core → src/core}/asset-ref.js +5 -5
- package/dist/{core → src/core}/asset-spec.js +12 -0
- package/dist/{core → src/core}/common.js +1 -1
- package/dist/{core → src/core}/config.js +175 -121
- package/dist/{core → src/core}/errors.js +4 -0
- package/dist/src/core/events.js +239 -0
- package/dist/src/core/lesson-lint.js +86 -0
- package/dist/src/core/proposals.js +406 -0
- package/dist/src/core/warn.js +72 -0
- package/dist/{core → src/core}/write-source.js +80 -5
- package/dist/{indexer → src/indexer}/db-search.js +119 -27
- package/dist/{indexer → src/indexer}/db.js +76 -23
- package/dist/{indexer → src/indexer}/file-context.js +0 -3
- package/dist/src/indexer/graph-boost.js +179 -0
- package/dist/src/indexer/graph-extraction.js +212 -0
- package/dist/{indexer → src/indexer}/indexer.js +73 -6
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +114 -11
- package/dist/src/integrations/agent/config.js +292 -0
- package/dist/src/integrations/agent/detect.js +94 -0
- package/dist/src/integrations/agent/index.js +17 -0
- package/dist/src/integrations/agent/profiles.js +65 -0
- package/dist/src/integrations/agent/prompts.js +167 -0
- package/dist/src/integrations/agent/spawn.js +221 -0
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/src/llm/feature-gate.js +108 -0
- package/dist/src/llm/graph-extract.js +107 -0
- package/dist/src/llm/index-passes.js +35 -0
- package/dist/src/llm/memory-infer.js +86 -0
- package/dist/{output → src/output}/renderers.js +60 -1
- package/dist/src/output/shapes.js +516 -0
- package/dist/{output → src/output}/text.js +447 -4
- package/dist/{registry → src/registry}/build-index.js +14 -4
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +3 -2
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +43 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/{wiki → src/wiki}/wiki.js +9 -11
- package/dist/tests/add-website-source.test.js +119 -0
- package/dist/tests/agent/agent-config-loader.test.js +70 -0
- package/dist/tests/agent/agent-config.test.js +221 -0
- package/dist/tests/agent/agent-detect.test.js +100 -0
- package/dist/tests/agent/agent-spawn.test.js +234 -0
- package/dist/tests/agent-output.test.js +186 -0
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
- package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
- package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
- package/dist/tests/asset-ref.test.js +192 -0
- package/dist/tests/asset-registry.test.js +103 -0
- package/dist/tests/asset-spec.test.js +241 -0
- package/dist/tests/bench/attribution.test.js +995 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +203 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +683 -0
- package/dist/tests/bench/cli.test.js +177 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +314 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/driver.js +346 -0
- package/dist/tests/bench/driver.test.js +443 -0
- package/dist/tests/bench/evolve-metrics.js +179 -0
- package/dist/tests/bench/evolve-metrics.test.js +187 -0
- package/dist/tests/bench/evolve.js +580 -0
- package/dist/tests/bench/evolve.test.js +616 -0
- package/dist/tests/bench/failure-modes.test.js +300 -0
- package/dist/tests/bench/feedback-integrity.test.js +456 -0
- package/dist/tests/bench/leakage.test.js +125 -0
- package/dist/tests/bench/learning-curve.test.js +133 -0
- package/dist/tests/bench/metrics.js +2319 -0
- package/dist/tests/bench/metrics.test.js +1144 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/report.js +1821 -0
- package/dist/tests/bench/report.test.js +989 -0
- package/dist/tests/bench/runner.js +536 -0
- package/dist/tests/bench/runner.test.js +958 -0
- package/dist/tests/bench/search-bridge.test.js +331 -0
- package/dist/tests/bench/tmp.js +41 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +109 -0
- package/dist/tests/bench/verifier.test.js +118 -0
- package/dist/tests/bench/workflow-evaluator.js +557 -0
- package/dist/tests/bench/workflow-evaluator.test.js +421 -0
- package/dist/tests/bench/workflow-spec.js +358 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +438 -0
- package/dist/tests/bench/workflow-trace.test.js +254 -0
- package/dist/tests/benchmark-search-quality.js +536 -0
- package/dist/tests/benchmark-suite.js +1441 -0
- package/dist/tests/capture-cli.test.js +112 -0
- package/dist/tests/cli-errors.test.js +203 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +223 -0
- package/dist/tests/commands/import.test.js +103 -0
- package/dist/tests/commands/proposal-cli.test.js +209 -0
- package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
- package/dist/tests/commands/remember.test.js +97 -0
- package/dist/tests/commands/scope-flags.test.js +300 -0
- package/dist/tests/commands/search.test.js +537 -0
- package/dist/tests/commands/show-indexer-parity.test.js +117 -0
- package/dist/tests/commands/show.test.js +294 -0
- package/dist/tests/common.test.js +266 -0
- package/dist/tests/completions.test.js +142 -0
- package/dist/tests/config-cli.test.js +193 -0
- package/dist/tests/config-llm-features.test.js +139 -0
- package/dist/tests/config.test.js +544 -0
- package/dist/tests/contracts/migration-baseline.test.js +43 -0
- package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
- package/dist/tests/contracts/spec-helpers.js +46 -0
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
- package/dist/tests/core/write-source.test.js +366 -0
- package/dist/tests/curate-command.test.js +87 -0
- package/dist/tests/db-scoring.test.js +201 -0
- package/dist/tests/db.test.js +654 -0
- package/dist/tests/distill-cli-flag.test.js +208 -0
- package/dist/tests/distill.test.js +515 -0
- package/dist/tests/docker-install.test.js +120 -0
- package/dist/tests/e2e.test.js +1398 -0
- package/dist/tests/embedder.test.js +340 -0
- package/dist/tests/embedding-model-config.test.js +379 -0
- package/dist/tests/feedback-command.test.js +172 -0
- package/dist/tests/file-context.test.js +552 -0
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
- package/dist/tests/fixtures/stashes/load.js +166 -0
- package/dist/tests/fixtures/stashes/load.test.js +88 -0
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
- package/dist/tests/frontmatter.test.js +190 -0
- package/dist/tests/fts-field-weighting.test.js +254 -0
- package/dist/tests/fuzzy-search.test.js +230 -0
- package/dist/tests/git-provider-clone.test.js +45 -0
- package/dist/tests/github.test.js +161 -0
- package/dist/tests/graph-boost-ranking.test.js +305 -0
- package/dist/tests/graph-extraction.test.js +282 -0
- package/dist/tests/helpers/usage-events.js +8 -0
- package/dist/tests/index-pass-llm.test.js +161 -0
- package/dist/tests/indexer.test.js +559 -0
- package/dist/tests/info-command.test.js +166 -0
- package/dist/tests/init.test.js +69 -0
- package/dist/tests/install-script.test.js +246 -0
- package/dist/tests/integration/agent-real-profile.test.js +94 -0
- package/dist/tests/issue-36-repro.test.js +304 -0
- package/dist/tests/issues-191-194.test.js +160 -0
- package/dist/tests/lesson-lint.test.js +111 -0
- package/dist/tests/llm-client.test.js +115 -0
- package/dist/tests/llm-feature-gate.test.js +151 -0
- package/dist/tests/llm.test.js +139 -0
- package/dist/tests/lockfile.test.js +216 -0
- package/dist/tests/manifest.test.js +205 -0
- package/dist/tests/markdown.test.js +126 -0
- package/dist/tests/matchers-unit.test.js +189 -0
- package/dist/tests/memory-inference.test.js +299 -0
- package/dist/tests/merge-scoring.test.js +136 -0
- package/dist/tests/metadata.test.js +313 -0
- package/dist/tests/migration-help.test.js +89 -0
- package/dist/tests/origin-resolve.test.js +124 -0
- package/dist/tests/output-baseline.test.js +217 -0
- package/dist/tests/output-shapes-unit.test.js +476 -0
- package/dist/tests/parallel-search.test.js +272 -0
- package/dist/tests/parameter-metadata.test.js +365 -0
- package/dist/tests/paths.test.js +177 -0
- package/dist/tests/progressive-disclosure.test.js +280 -0
- package/dist/tests/proposals.test.js +279 -0
- package/dist/tests/proposed-quality.test.js +271 -0
- package/dist/tests/provider-registry.test.js +32 -0
- package/dist/tests/ranking-regression.test.js +548 -0
- package/dist/tests/reflect-propose.test.js +455 -0
- package/dist/tests/registry-build-index.test.js +378 -0
- package/dist/tests/registry-cli.test.js +290 -0
- package/dist/tests/registry-index-v2.test.js +430 -0
- package/dist/tests/registry-install.test.js +728 -0
- package/dist/tests/registry-providers/parity.test.js +189 -0
- package/dist/tests/registry-providers/skills-sh.test.js +309 -0
- package/dist/tests/registry-providers/static-index.test.js +204 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +723 -0
- package/dist/tests/remember-frontmatter.test.js +380 -0
- package/dist/tests/remember-unit.test.js +123 -0
- package/dist/tests/ripgrep-install.test.js +251 -0
- package/dist/tests/ripgrep-resolve.test.js +108 -0
- package/dist/tests/ripgrep.test.js +163 -0
- package/dist/tests/save-command.test.js +94 -0
- package/dist/tests/save-trust-qa-fixes.test.js +270 -0
- package/dist/tests/scoring-pipeline.test.js +648 -0
- package/dist/tests/search-include-proposed-cli.test.js +118 -0
- package/dist/tests/self-update.test.js +442 -0
- package/dist/tests/semantic-search-e2e.test.js +512 -0
- package/dist/tests/semantic-status.test.js +471 -0
- package/dist/tests/setup-run.integration.js +877 -0
- package/dist/tests/setup-wizard.test.js +198 -0
- package/dist/tests/setup.test.js +131 -0
- package/dist/tests/source-add.test.js +11 -0
- package/dist/tests/source-clone.test.js +254 -0
- package/dist/tests/source-manage.test.js +366 -0
- package/dist/tests/source-providers/filesystem.test.js +82 -0
- package/dist/tests/source-providers/git.test.js +252 -0
- package/dist/tests/source-providers/website.test.js +128 -0
- package/dist/tests/source-qa-fixes.test.js +268 -0
- package/dist/tests/source-registry.test.js +350 -0
- package/dist/tests/source-resolve.test.js +100 -0
- package/dist/tests/source-source.test.js +221 -0
- package/dist/tests/source.test.js +533 -0
- package/dist/tests/tar-utils-scan.test.js +73 -0
- package/dist/tests/toggle-components.test.js +73 -0
- package/dist/tests/usage-telemetry.test.js +265 -0
- package/dist/tests/utility-scoring.test.js +558 -0
- package/dist/tests/vault-load-error.test.js +78 -0
- package/dist/tests/vault-qa-fixes.test.js +194 -0
- package/dist/tests/vault.test.js +429 -0
- package/dist/tests/vector-search.test.js +608 -0
- package/dist/tests/walker.test.js +252 -0
- package/dist/tests/wave2-cluster-bc.test.js +228 -0
- package/dist/tests/wave2-cluster-d.test.js +180 -0
- package/dist/tests/wave2-cluster-e.test.js +179 -0
- package/dist/tests/wiki-qa-fixes.test.js +270 -0
- package/dist/tests/wiki.test.js +529 -0
- package/dist/tests/workflow-cli.test.js +271 -0
- package/dist/tests/workflow-markdown.test.js +171 -0
- package/dist/tests/workflow-path-escape.test.js +132 -0
- package/dist/tests/workflow-qa-fixes.test.js +377 -0
- package/dist/tests/workflows/indexer-rejection.test.js +213 -0
- package/docs/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +244 -0
- package/package.json +2 -2
- package/dist/core/warn.js +0 -27
- package/dist/output/shapes.js +0 -212
- /package/dist/{commands → src/commands}/completions.js +0 -0
- /package/dist/{commands → src/commands}/curate.js +0 -0
- /package/dist/{commands → src/commands}/info.js +0 -0
- /package/dist/{commands → src/commands}/init.js +0 -0
- /package/dist/{commands → src/commands}/install-audit.js +0 -0
- /package/dist/{commands → src/commands}/migration-help.js +0 -0
- /package/dist/{commands → src/commands}/source-add.js +0 -0
- /package/dist/{commands → src/commands}/source-clone.js +0 -0
- /package/dist/{commands → src/commands}/source-manage.js +0 -0
- /package/dist/{commands → src/commands}/vault.js +0 -0
- /package/dist/{core → src/core}/asset-registry.js +0 -0
- /package/dist/{core → src/core}/frontmatter.js +0 -0
- /package/dist/{core → src/core}/markdown.js +0 -0
- /package/dist/{core → src/core}/paths.js +0 -0
- /package/dist/{indexer → src/indexer}/manifest.js +0 -0
- /package/dist/{indexer → src/indexer}/matchers.js +0 -0
- /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
- /package/dist/{indexer → src/indexer}/search-source.js +0 -0
- /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
- /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
- /package/dist/{indexer → src/indexer}/walker.js +0 -0
- /package/dist/{integrations → src/integrations}/github.js +0 -0
- /package/dist/{llm → src/llm}/embedder.js +0 -0
- /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
- /package/dist/{llm → src/llm}/embedders/local.js +0 -0
- /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
- /package/dist/{llm → src/llm}/embedders/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
- /package/dist/{output → src/output}/cli-hints.js +0 -0
- /package/dist/{output → src/output}/context.js +0 -0
- /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
- /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
- /package/dist/{registry → src/registry}/providers/index.js +0 -0
- /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
- /package/dist/{registry → src/registry}/providers/types.js +0 -0
- /package/dist/{registry → src/registry}/types.js +0 -0
- /package/dist/{setup → src/setup}/detect.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
- /package/dist/{setup → src/setup}/steps.js +0 -0
- /package/dist/{sources → src/sources}/include.js +0 -0
- /package/dist/{sources → src/sources}/provider-factory.js +0 -0
- /package/dist/{sources → src/sources}/provider.js +0 -0
- /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
- /package/dist/{sources → src/sources}/providers/index.js +0 -0
- /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
- /package/dist/{sources → src/sources}/providers/npm.js +0 -0
- /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/website.js +0 -0
- /package/dist/{sources → src/sources}/resolve.js +0 -0
- /package/dist/{sources → src/sources}/types.js +0 -0
- /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
- /package/dist/{version.js → src/version.js} +0 -0
- /package/dist/{workflows → src/workflows}/authoring.js +0 -0
- /package/dist/{workflows → src/workflows}/cli.js +0 -0
- /package/dist/{workflows → src/workflows}/db.js +0 -0
- /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
- /package/dist/{workflows → src/workflows}/parser.js +0 -0
- /package/dist/{workflows → src/workflows}/renderer.js +0 -0
- /package/dist/{workflows → src/workflows}/runs.js +0 -0
- /package/dist/{workflows → src/workflows}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -14,6 +14,7 @@ import { getDefaultStashDir } from "../core/paths";
|
|
|
14
14
|
import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db";
|
|
15
15
|
import { akmIndex } from "../indexer/indexer";
|
|
16
16
|
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/semantic-status";
|
|
17
|
+
import { detectAgentCliProfiles, pickDefaultAgentProfile, } from "../integrations/agent";
|
|
17
18
|
import { probeLlmCapabilities } from "../llm/client";
|
|
18
19
|
import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
|
|
19
20
|
import { detectAgentPlatforms, detectOllama } from "./detect";
|
|
@@ -284,6 +285,7 @@ async function stepOllama(current) {
|
|
|
284
285
|
bge: 384,
|
|
285
286
|
};
|
|
286
287
|
const guessedDim = Object.entries(knownDims).find(([k]) => embChoice.includes(k))?.[1] ?? 384;
|
|
288
|
+
p.note("Embedding dimension must match the model. Common values: 384 (BGE small), 768 (BGE base), 1024 (BGE large). Press Enter to accept the detected default.", "Embedding dimension");
|
|
287
289
|
const dimChoice = await prompt(() => p.text({
|
|
288
290
|
message: `Embedding dimension for ${embChoice}:`,
|
|
289
291
|
placeholder: String(guessedDim),
|
|
@@ -313,7 +315,6 @@ const LLM_PRESETS = [
|
|
|
313
315
|
endpoint: "https://api.anthropic.com/v1/chat/completions",
|
|
314
316
|
defaultModel: "claude-sonnet-4-5",
|
|
315
317
|
hint: "beta OpenAI-compat layer; set AKM_LLM_API_KEY; override the model if the default is unavailable",
|
|
316
|
-
contextWindow: 200_000,
|
|
317
318
|
},
|
|
318
319
|
{
|
|
319
320
|
value: "openai",
|
|
@@ -321,7 +322,6 @@ const LLM_PRESETS = [
|
|
|
321
322
|
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
322
323
|
defaultModel: "gpt-4o-mini",
|
|
323
324
|
hint: "AKM_LLM_API_KEY required",
|
|
324
|
-
contextWindow: 128_000,
|
|
325
325
|
},
|
|
326
326
|
{
|
|
327
327
|
value: "google",
|
|
@@ -329,7 +329,6 @@ const LLM_PRESETS = [
|
|
|
329
329
|
endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
|
|
330
330
|
defaultModel: "gemini-2.0-flash",
|
|
331
331
|
hint: "OpenAI-compat endpoint, AKM_LLM_API_KEY required",
|
|
332
|
-
contextWindow: 1_000_000,
|
|
333
332
|
},
|
|
334
333
|
];
|
|
335
334
|
/**
|
|
@@ -431,7 +430,6 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
|
431
430
|
model: model.trim() || preset.defaultModel,
|
|
432
431
|
temperature: 0.3,
|
|
433
432
|
maxTokens: 1024,
|
|
434
|
-
contextWindow: preset.contextWindow,
|
|
435
433
|
};
|
|
436
434
|
}
|
|
437
435
|
// Remind the user about API key placement. We do not offer a "store in config"
|
|
@@ -649,6 +647,29 @@ async function stepAgentPlatforms(current) {
|
|
|
649
647
|
}
|
|
650
648
|
return entries;
|
|
651
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Detect installed agent CLIs and produce an updated `agent` config block
|
|
652
|
+
* with a sensible `default` (the first detected profile that the user has
|
|
653
|
+
* not already overridden).
|
|
654
|
+
*
|
|
655
|
+
* Pure-ish: file system / PATH probes are routed through `detectFn` so
|
|
656
|
+
* tests can drive the branches without touching the real PATH.
|
|
657
|
+
*
|
|
658
|
+
* @internal Exported for testing only.
|
|
659
|
+
*/
|
|
660
|
+
export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
|
|
661
|
+
const detections = detectFn(current.agent);
|
|
662
|
+
const defaultName = pickDefaultAgentProfile(detections, current.agent?.default);
|
|
663
|
+
// No installed agents found and no existing config → leave block absent.
|
|
664
|
+
if (!defaultName && !current.agent) {
|
|
665
|
+
return { detections };
|
|
666
|
+
}
|
|
667
|
+
const agent = {
|
|
668
|
+
...(current.agent ?? {}),
|
|
669
|
+
...(defaultName ? { default: defaultName } : {}),
|
|
670
|
+
};
|
|
671
|
+
return { agent, detections };
|
|
672
|
+
}
|
|
652
673
|
// ── Main Wizard ─────────────────────────────────────────────────────────────
|
|
653
674
|
/**
|
|
654
675
|
* Build the canonical list of `SetupStep`s for the interactive wizard.
|
|
@@ -731,6 +752,23 @@ export function buildSetupSteps(options) {
|
|
|
731
752
|
ctx.apply({ sources: merged.length > 0 ? merged : undefined });
|
|
732
753
|
},
|
|
733
754
|
},
|
|
755
|
+
{
|
|
756
|
+
id: "agent-cli",
|
|
757
|
+
label: "Agent CLI",
|
|
758
|
+
nonInteractive: true,
|
|
759
|
+
async run(ctx) {
|
|
760
|
+
const result = stepAgentCliDetection(ctx.config);
|
|
761
|
+
const detected = result.detections.filter((d) => d.available);
|
|
762
|
+
if (detected.length > 0) {
|
|
763
|
+
p.log.info(`Detected agent CLIs: ${detected.map((d) => d.name).join(", ")}.` +
|
|
764
|
+
(result.agent?.default ? ` Default profile: ${result.agent.default}.` : ""));
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
|
|
768
|
+
}
|
|
769
|
+
ctx.apply({ agent: result.agent });
|
|
770
|
+
},
|
|
771
|
+
},
|
|
734
772
|
];
|
|
735
773
|
return { steps, outcome };
|
|
736
774
|
}
|
|
@@ -771,7 +809,7 @@ export async function runSetupWizard() {
|
|
|
771
809
|
const embedding = newConfig.embedding;
|
|
772
810
|
const llm = newConfig.llm;
|
|
773
811
|
const registries = newConfig.registries;
|
|
774
|
-
const allStashes = newConfig.stashes ?? [];
|
|
812
|
+
const allStashes = newConfig.sources ?? newConfig.stashes ?? [];
|
|
775
813
|
// Confirm before saving
|
|
776
814
|
const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
777
815
|
p.note([
|
|
@@ -6,6 +6,7 @@ import { resolveStashDir } from "../../core/common";
|
|
|
6
6
|
import { loadConfig } from "../../core/config";
|
|
7
7
|
import { ConfigError, UsageError } from "../../core/errors";
|
|
8
8
|
import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../../core/paths";
|
|
9
|
+
import { sanitizeCommitMessage } from "../../core/write-source";
|
|
9
10
|
import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve";
|
|
10
11
|
import { registerSourceProvider } from "../provider-factory";
|
|
11
12
|
import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, detectStashRoot, isDirectory, isExpired, sanitizeString, } from "./provider-utils";
|
|
@@ -77,20 +78,6 @@ function getCachePaths(repoUrl) {
|
|
|
77
78
|
const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
78
79
|
const cacheRoot = getRegistryIndexCacheDir();
|
|
79
80
|
const rootDir = path.join(cacheRoot, `git-${key}`);
|
|
80
|
-
// One-time silent migration: legacy `context-hub-${key}` directories were
|
|
81
|
-
// created for ALL git stashes (not just the andrewyng/context-hub repo). If
|
|
82
|
-
// the new path doesn't yet exist but the legacy one does, rename it in place
|
|
83
|
-
// so existing clones aren't silently invalidated. Failures are non-fatal —
|
|
84
|
-
// worst case the repo is re-cloned on the next refresh.
|
|
85
|
-
try {
|
|
86
|
-
const legacyRootDir = path.join(cacheRoot, `context-hub-${key}`);
|
|
87
|
-
if (!fs.existsSync(rootDir) && fs.existsSync(legacyRootDir)) {
|
|
88
|
-
fs.renameSync(legacyRootDir, rootDir);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
/* migration is best-effort */
|
|
93
|
-
}
|
|
94
81
|
return {
|
|
95
82
|
rootDir,
|
|
96
83
|
repoDir: path.join(rootDir, "repo"),
|
|
@@ -398,7 +385,12 @@ function parseGitRepoUrl(rawUrl) {
|
|
|
398
385
|
*/
|
|
399
386
|
export function saveGitStash(name, message, writableOverride) {
|
|
400
387
|
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
401
|
-
|
|
388
|
+
// Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
|
|
389
|
+
// clamp length. An attacker can otherwise pass `--message "subject\n\n\
|
|
390
|
+
// Co-Authored-By: someone-else"` and forge trailers in the commit log.
|
|
391
|
+
// Empty result falls back to the timestamped default.
|
|
392
|
+
const sanitized = message ? sanitizeCommitMessage(message) : "";
|
|
393
|
+
const commitMessage = sanitized || `akm save ${timestamp}`;
|
|
402
394
|
let repoDir;
|
|
403
395
|
let writable = false;
|
|
404
396
|
if (name) {
|
|
@@ -99,8 +99,9 @@ export function ensureWikiNameAvailable(stashDir, name) {
|
|
|
99
99
|
* Walk a wiki directory and bucket files into pages vs raws.
|
|
100
100
|
*
|
|
101
101
|
* "Pages" are any `.md` files under the wiki root EXCEPT `schema.md`,
|
|
102
|
-
* `index.md`, `log.md
|
|
103
|
-
*
|
|
102
|
+
* `index.md`, or `log.md`. Raw sources are bucketed separately so callers can
|
|
103
|
+
* distinguish authored pages from ingested source material while still
|
|
104
|
+
* surfacing both.
|
|
104
105
|
*
|
|
105
106
|
* Returns two mtime signals:
|
|
106
107
|
* - `lastModifiedMs` — newest across all .md files. Used for the `show` /
|
|
@@ -495,15 +496,16 @@ function readPageFrontmatter(absPath) {
|
|
|
495
496
|
return out;
|
|
496
497
|
}
|
|
497
498
|
/**
|
|
498
|
-
* List the
|
|
499
|
-
*
|
|
500
|
-
*
|
|
499
|
+
* List the addressable markdown entries in a wiki, excluding only the
|
|
500
|
+
* infrastructure files `schema.md`, `index.md`, and `log.md`. This includes
|
|
501
|
+
* both authored pages and `raw/` sources so `wiki pages` can inventory content
|
|
502
|
+
* written via `akm wiki stash`.
|
|
501
503
|
*/
|
|
502
504
|
export function listPages(stashDir, name) {
|
|
503
505
|
const wikiDir = resolveWikiSource(stashDir, name).path;
|
|
504
|
-
const { pages } = scanWikiFiles(wikiDir);
|
|
506
|
+
const { pages, raws } = scanWikiFiles(wikiDir);
|
|
505
507
|
const result = [];
|
|
506
|
-
for (const abs of pages) {
|
|
508
|
+
for (const abs of [...pages, ...raws]) {
|
|
507
509
|
const pageName = pageNameFromPath(wikiDir, abs);
|
|
508
510
|
const ref = `wiki:${name}/${pageName}`;
|
|
509
511
|
const fm = readPageFrontmatter(abs);
|
|
@@ -540,7 +542,6 @@ export async function searchInWiki(input) {
|
|
|
540
542
|
}
|
|
541
543
|
throw err;
|
|
542
544
|
}
|
|
543
|
-
const rawDir = path.join(wikiDir, RAW_SUBDIR);
|
|
544
545
|
const filtered = [];
|
|
545
546
|
for (const hit of response.hits) {
|
|
546
547
|
// hits can be SourceSearchHit or RegistrySearchResultHit (union); filter
|
|
@@ -556,9 +557,6 @@ export async function searchInWiki(input) {
|
|
|
556
557
|
const basename = path.basename(stashHit.path);
|
|
557
558
|
if (WIKI_SPECIAL_FILES.has(basename) && path.dirname(stashHit.path) === wikiDir)
|
|
558
559
|
continue;
|
|
559
|
-
// Exclude anything under raw/
|
|
560
|
-
if (isWithin(stashHit.path, rawDir))
|
|
561
|
-
continue;
|
|
562
560
|
filtered.push(stashHit);
|
|
563
561
|
}
|
|
564
562
|
return { ...response, hits: filtered, registryHits: undefined };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const CLI = path.join(__dirname, "..", "src", "cli.ts");
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
const servers = [];
|
|
9
|
+
const CLI_TIMEOUT_MS = 30_000;
|
|
10
|
+
const TEST_TIMEOUT_MS = 60_000;
|
|
11
|
+
function makeTempDir(prefix) {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
13
|
+
tempDirs.push(dir);
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
function createWorkingStash() {
|
|
17
|
+
const dir = makeTempDir("akm-add-website-stash-");
|
|
18
|
+
for (const sub of ["skills", "commands", "agents", "knowledge", "scripts"]) {
|
|
19
|
+
fs.mkdirSync(path.join(dir, sub), { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
function serveWebsite() {
|
|
24
|
+
const server = Bun.serve({
|
|
25
|
+
port: 0,
|
|
26
|
+
fetch(request) {
|
|
27
|
+
const url = new URL(request.url);
|
|
28
|
+
if (url.pathname === "/") {
|
|
29
|
+
return new Response("<html><head><title>Example Docs</title></head><body><h1>Example Docs</h1><p>Welcome to the docs.</p><a href='/getting-started'>Getting started</a></body></html>", {
|
|
30
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (url.pathname === "/getting-started") {
|
|
34
|
+
return new Response("<html><body><h1>Getting started</h1><p>Run setup first.</p></body></html>", {
|
|
35
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return new Response("not found", { status: 404 });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
servers.push(server);
|
|
42
|
+
return `http://127.0.0.1:${server.port}`;
|
|
43
|
+
}
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
for (const server of servers.splice(0)) {
|
|
46
|
+
server.stop(true);
|
|
47
|
+
}
|
|
48
|
+
for (const dir of tempDirs.splice(0)) {
|
|
49
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
for (const server of servers.splice(0)) {
|
|
54
|
+
server.stop(true);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
describe("akm add website", () => {
|
|
58
|
+
test("adds a website stash source, caches markdown, and indexes it", async () => {
|
|
59
|
+
const stashDir = createWorkingStash();
|
|
60
|
+
const xdgCache = makeTempDir("akm-add-website-cache-");
|
|
61
|
+
const xdgConfig = makeTempDir("akm-add-website-config-");
|
|
62
|
+
const websiteUrl = serveWebsite();
|
|
63
|
+
const configDir = path.join(xdgConfig, "akm");
|
|
64
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
65
|
+
fs.writeFileSync(path.join(configDir, "config.json"), `${JSON.stringify({ semanticSearchMode: "off" }, null, 2)}\n`);
|
|
66
|
+
const child = spawn("bun", [CLI, "add", websiteUrl, "--name", "docs-site", "--format=json"], {
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
AKM_STASH_DIR: stashDir,
|
|
71
|
+
XDG_CACHE_HOME: xdgCache,
|
|
72
|
+
XDG_CONFIG_HOME: xdgConfig,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
let stdout = "";
|
|
76
|
+
let stderr = "";
|
|
77
|
+
child.stdout.on("data", (chunk) => {
|
|
78
|
+
stdout += String(chunk);
|
|
79
|
+
});
|
|
80
|
+
child.stderr.on("data", (chunk) => {
|
|
81
|
+
stderr += String(chunk);
|
|
82
|
+
});
|
|
83
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
child.kill("SIGKILL");
|
|
86
|
+
reject(new Error("CLI website add timed out"));
|
|
87
|
+
}, CLI_TIMEOUT_MS);
|
|
88
|
+
child.on("error", (err) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(err);
|
|
91
|
+
});
|
|
92
|
+
child.on("close", (code) => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve(code ?? 1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
expect(exitCode).toBe(0);
|
|
98
|
+
expect(stderr.trim()).toBe("");
|
|
99
|
+
const parsed = JSON.parse(stdout.trim());
|
|
100
|
+
const normalizedWebsiteUrl = `${websiteUrl}/`;
|
|
101
|
+
expect(parsed.sourceAdded).toBeDefined();
|
|
102
|
+
expect(parsed.sourceAdded?.type).toBe("website");
|
|
103
|
+
expect(parsed.sourceAdded?.url).toBe(normalizedWebsiteUrl);
|
|
104
|
+
expect(parsed.sourceAdded?.name).toBe("docs-site");
|
|
105
|
+
expect(parsed.index?.totalEntries).toBeGreaterThanOrEqual(2);
|
|
106
|
+
const configPath = path.join(xdgConfig, "akm", "config.json");
|
|
107
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
108
|
+
expect(config.sources).toContainEqual({
|
|
109
|
+
type: "website",
|
|
110
|
+
url: normalizedWebsiteUrl,
|
|
111
|
+
name: "docs-site",
|
|
112
|
+
});
|
|
113
|
+
expect(parsed.sourceAdded?.stashRoot).toBeDefined();
|
|
114
|
+
const knowledgeFiles = fs.readdirSync(path.join(parsed.sourceAdded?.stashRoot, "knowledge")).sort();
|
|
115
|
+
expect(knowledgeFiles).toEqual(["getting-started.md", "index.md"]);
|
|
116
|
+
const homeDoc = fs.readFileSync(path.join(parsed.sourceAdded?.stashRoot, "knowledge", "index.md"), "utf8");
|
|
117
|
+
expect(homeDoc).toContain("Example Docs");
|
|
118
|
+
}, { timeout: TEST_TIMEOUT_MS });
|
|
119
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: the AkmConfig loader propagates the `agent` block
|
|
3
|
+
* through `loadConfig()` (and through the on-disk JSONC parser, exercising
|
|
4
|
+
* the `pickKnownKeys` path).
|
|
5
|
+
*
|
|
6
|
+
* The acceptance criterion "config schema accepts an optional agent block"
|
|
7
|
+
* lives at the loader boundary, not just the parser; this test pins the
|
|
8
|
+
* end-to-end shape.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let originalHome;
|
|
16
|
+
let originalXdg;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-agent-cfg-"));
|
|
19
|
+
originalHome = process.env.HOME;
|
|
20
|
+
originalXdg = process.env.XDG_CONFIG_HOME;
|
|
21
|
+
process.env.HOME = tmpHome;
|
|
22
|
+
process.env.XDG_CONFIG_HOME = path.join(tmpHome, ".config");
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (originalHome === undefined)
|
|
26
|
+
delete process.env.HOME;
|
|
27
|
+
else
|
|
28
|
+
process.env.HOME = originalHome;
|
|
29
|
+
if (originalXdg === undefined)
|
|
30
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
31
|
+
else
|
|
32
|
+
process.env.XDG_CONFIG_HOME = originalXdg;
|
|
33
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
describe("AkmConfig loader — agent block", () => {
|
|
36
|
+
test("loads agent.default + agent.profiles from disk", async () => {
|
|
37
|
+
const { getConfigPath, loadUserConfig, resetConfigCache } = await import("../../src/core/config");
|
|
38
|
+
const cfgPath = getConfigPath();
|
|
39
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
40
|
+
fs.writeFileSync(cfgPath, JSON.stringify({
|
|
41
|
+
semanticSearchMode: "auto",
|
|
42
|
+
agent: {
|
|
43
|
+
default: "claude",
|
|
44
|
+
timeoutMs: 45000,
|
|
45
|
+
profiles: {
|
|
46
|
+
claude: { args: ["--print"] },
|
|
47
|
+
rover: { bin: "rover-cli", parseOutput: "json" },
|
|
48
|
+
},
|
|
49
|
+
// Unknown key — must not throw at load.
|
|
50
|
+
mystery: 1,
|
|
51
|
+
},
|
|
52
|
+
}, null, 2));
|
|
53
|
+
resetConfigCache();
|
|
54
|
+
const cfg = loadUserConfig();
|
|
55
|
+
expect(cfg.agent?.default).toBe("claude");
|
|
56
|
+
expect(cfg.agent?.timeoutMs).toBe(45000);
|
|
57
|
+
expect(cfg.agent?.profiles?.claude?.args).toEqual(["--print"]);
|
|
58
|
+
expect(cfg.agent?.profiles?.rover?.bin).toBe("rover-cli");
|
|
59
|
+
expect(cfg.agent?.profiles?.rover?.parseOutput).toBe("json");
|
|
60
|
+
});
|
|
61
|
+
test("agent block absent → cfg.agent is undefined → requireAgentProfile throws", async () => {
|
|
62
|
+
const { loadUserConfig, resetConfigCache } = await import("../../src/core/config");
|
|
63
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
64
|
+
const { ConfigError } = await import("../../src/core/errors");
|
|
65
|
+
resetConfigCache();
|
|
66
|
+
const cfg = loadUserConfig();
|
|
67
|
+
expect(cfg.agent).toBeUndefined();
|
|
68
|
+
expect(() => requireAgentProfile(cfg.agent)).toThrow(ConfigError);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `agent.*` config block parser and profile resolver.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance coverage:
|
|
5
|
+
* • Parser accepts the documented shape.
|
|
6
|
+
* • Unknown keys are warn-and-ignored (no throw).
|
|
7
|
+
* • Built-in profiles resolve for opencode, claude, codex, gemini, aider.
|
|
8
|
+
* • Missing block surfaces a stable ConfigError via requireAgentProfile.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
const warnings = [];
|
|
12
|
+
// NOTE: `mock.module` in Bun is process-global — once installed it persists
|
|
13
|
+
// across test files run in the same `bun test` invocation. So this mock has
|
|
14
|
+
// to remain a faithful drop-in for the real `src/core/warn` module:
|
|
15
|
+
//
|
|
16
|
+
// 1. Every export that the real module ships must be represented here,
|
|
17
|
+
// otherwise tests in other files that import a missing export get
|
|
18
|
+
// `undefined` and silently break (issue #273).
|
|
19
|
+
// 2. `warn()` must also forward to `console.warn` so other test files that
|
|
20
|
+
// capture stderr (e.g. the noise-gate tests in
|
|
21
|
+
// tests/workflows/indexer-rejection.test.ts) continue to see the calls.
|
|
22
|
+
// We push to the local `warnings[]` so this file's own assertions still
|
|
23
|
+
// work, AND forward to `console.warn` so callers that intercept it
|
|
24
|
+
// still observe what was emitted.
|
|
25
|
+
let mockedQuiet = false;
|
|
26
|
+
let mockedVerbose = false;
|
|
27
|
+
mock.module("../../src/core/warn", () => ({
|
|
28
|
+
warn: (...args) => {
|
|
29
|
+
warnings.push(args.join(" "));
|
|
30
|
+
if (!mockedQuiet)
|
|
31
|
+
console.warn(...args);
|
|
32
|
+
},
|
|
33
|
+
warnVerbose: (...args) => {
|
|
34
|
+
if (!mockedVerbose)
|
|
35
|
+
return;
|
|
36
|
+
warnings.push(args.join(" "));
|
|
37
|
+
if (!mockedQuiet)
|
|
38
|
+
console.warn(...args);
|
|
39
|
+
},
|
|
40
|
+
setQuiet: (value) => {
|
|
41
|
+
mockedQuiet = value;
|
|
42
|
+
},
|
|
43
|
+
resetQuiet: () => {
|
|
44
|
+
mockedQuiet = false;
|
|
45
|
+
},
|
|
46
|
+
isQuiet: () => mockedQuiet,
|
|
47
|
+
setVerbose: (value) => {
|
|
48
|
+
mockedVerbose = value;
|
|
49
|
+
},
|
|
50
|
+
resetVerbose: () => {
|
|
51
|
+
mockedVerbose = false;
|
|
52
|
+
},
|
|
53
|
+
isVerbose: () => {
|
|
54
|
+
const env = process.env.AKM_VERBOSE?.trim().toLowerCase();
|
|
55
|
+
if (env === "1" || env === "true" || env === "yes" || env === "on")
|
|
56
|
+
return true;
|
|
57
|
+
if (env === "0" || env === "false" || env === "no" || env === "off")
|
|
58
|
+
return false;
|
|
59
|
+
return mockedVerbose;
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
warnings.length = 0;
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
warnings.length = 0;
|
|
67
|
+
});
|
|
68
|
+
describe("parseAgentConfig", () => {
|
|
69
|
+
test("returns undefined when block is absent", async () => {
|
|
70
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
71
|
+
expect(parseAgentConfig(undefined)).toBeUndefined();
|
|
72
|
+
expect(warnings).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
test("warns and returns undefined for non-object root", async () => {
|
|
75
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
76
|
+
expect(parseAgentConfig("oops")).toBeUndefined();
|
|
77
|
+
expect(warnings.some((w) => w.includes('"agent"'))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
test("accepts the documented shape", async () => {
|
|
80
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
81
|
+
const parsed = parseAgentConfig({
|
|
82
|
+
default: "opencode",
|
|
83
|
+
timeoutMs: 30000,
|
|
84
|
+
profiles: {
|
|
85
|
+
opencode: { bin: "opencode", args: ["--non-interactive"], stdio: "captured" },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
expect(parsed?.default).toBe("opencode");
|
|
89
|
+
expect(parsed?.timeoutMs).toBe(30000);
|
|
90
|
+
expect(parsed?.profiles?.opencode).toEqual({
|
|
91
|
+
bin: "opencode",
|
|
92
|
+
args: ["--non-interactive"],
|
|
93
|
+
stdio: "captured",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
test("warn-and-ignore unknown top-level keys (no throw)", async () => {
|
|
97
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
98
|
+
const parsed = parseAgentConfig({
|
|
99
|
+
default: "claude",
|
|
100
|
+
moonRoutingTable: { foo: "bar" }, // unknown
|
|
101
|
+
});
|
|
102
|
+
expect(parsed?.default).toBe("claude");
|
|
103
|
+
expect(warnings.some((w) => w.includes("moonRoutingTable"))).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
test("warn-and-ignore unknown per-profile keys", async () => {
|
|
106
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
107
|
+
const parsed = parseAgentConfig({
|
|
108
|
+
profiles: {
|
|
109
|
+
custom: { bin: "ok", quirks: "nope" },
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
expect(parsed?.profiles?.custom?.bin).toBe("ok");
|
|
113
|
+
expect(warnings.some((w) => w.includes("quirks"))).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
test("warn-and-ignore malformed timeoutMs", async () => {
|
|
116
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
117
|
+
const parsed = parseAgentConfig({ timeoutMs: "60s" });
|
|
118
|
+
expect(parsed?.timeoutMs).toBeUndefined();
|
|
119
|
+
expect(warnings.some((w) => w.includes("timeoutMs"))).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
test("rejects non-string args entries", async () => {
|
|
122
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
123
|
+
const parsed = parseAgentConfig({
|
|
124
|
+
profiles: { opencode: { args: ["--ok", 5, "--also-ok"] } },
|
|
125
|
+
});
|
|
126
|
+
expect(parsed?.profiles?.opencode?.args).toEqual(["--ok", "--also-ok"]);
|
|
127
|
+
expect(warnings.some((w) => w.includes("args"))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
test("rejects bad stdio mode", async () => {
|
|
130
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
131
|
+
const parsed = parseAgentConfig({
|
|
132
|
+
profiles: { opencode: { stdio: "weird" } },
|
|
133
|
+
});
|
|
134
|
+
expect(parsed?.profiles?.opencode?.stdio).toBeUndefined();
|
|
135
|
+
expect(warnings.some((w) => w.includes("stdio"))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("built-in profile resolution", () => {
|
|
139
|
+
test("resolves opencode, claude, codex, gemini, aider out of the box", async () => {
|
|
140
|
+
const { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile } = await import("../../src/integrations/agent/profiles");
|
|
141
|
+
expect(BUILTIN_AGENT_PROFILE_NAMES).toEqual(["aider", "claude", "codex", "gemini", "opencode"]);
|
|
142
|
+
for (const name of ["opencode", "claude", "codex", "gemini", "aider"]) {
|
|
143
|
+
const profile = getBuiltinAgentProfile(name);
|
|
144
|
+
expect(profile).toBeDefined();
|
|
145
|
+
expect(profile?.bin).toBeTruthy();
|
|
146
|
+
expect(profile?.envPassthrough).toContain("PATH");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
test("user override merges on top of built-in", async () => {
|
|
150
|
+
const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
|
|
151
|
+
const merged = resolveAgentProfile("opencode", { args: ["--scripted"], stdio: "captured" });
|
|
152
|
+
expect(merged?.bin).toBe("opencode"); // built-in default
|
|
153
|
+
expect(merged?.args).toEqual(["--scripted"]); // override
|
|
154
|
+
expect(merged?.stdio).toBe("captured"); // override
|
|
155
|
+
expect(merged?.envPassthrough).toContain("PATH"); // built-in retained
|
|
156
|
+
});
|
|
157
|
+
test("user-defined profile (no built-in) requires bin", async () => {
|
|
158
|
+
const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
|
|
159
|
+
expect(resolveAgentProfile("rover", undefined)).toBeUndefined();
|
|
160
|
+
expect(resolveAgentProfile("rover", {})).toBeUndefined();
|
|
161
|
+
const ok = resolveAgentProfile("rover", { bin: "rover-cli", args: ["--silent"] });
|
|
162
|
+
expect(ok?.bin).toBe("rover-cli");
|
|
163
|
+
expect(ok?.args).toEqual(["--silent"]);
|
|
164
|
+
expect(ok?.stdio).toBe("captured");
|
|
165
|
+
});
|
|
166
|
+
test("envPassthrough merges base + override", async () => {
|
|
167
|
+
const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
|
|
168
|
+
const merged = resolveAgentProfile("opencode", { envPassthrough: ["MY_TOKEN"] });
|
|
169
|
+
expect(merged?.envPassthrough).toContain("PATH"); // from built-in
|
|
170
|
+
expect(merged?.envPassthrough).toContain("MY_TOKEN"); // from override
|
|
171
|
+
});
|
|
172
|
+
test("listAgentProfileNames includes built-ins plus user-defined", async () => {
|
|
173
|
+
const { listAgentProfileNames } = await import("../../src/integrations/agent/config");
|
|
174
|
+
const names = listAgentProfileNames({ profiles: { rover: { bin: "rover" } } });
|
|
175
|
+
expect(names).toContain("rover");
|
|
176
|
+
expect(names).toContain("opencode");
|
|
177
|
+
expect(names).toContain("claude");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe("requireAgentProfile", () => {
|
|
181
|
+
test("throws ConfigError when the agent block is missing", async () => {
|
|
182
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
183
|
+
const { ConfigError } = await import("../../src/core/errors");
|
|
184
|
+
let caught;
|
|
185
|
+
try {
|
|
186
|
+
requireAgentProfile(undefined);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
caught = err;
|
|
190
|
+
}
|
|
191
|
+
expect(caught).toBeInstanceOf(ConfigError);
|
|
192
|
+
expect(caught.message).toContain("agent commands are disabled");
|
|
193
|
+
const hint = caught.hint();
|
|
194
|
+
expect(hint).toBeTruthy();
|
|
195
|
+
expect(hint).toContain("akm setup");
|
|
196
|
+
});
|
|
197
|
+
test("throws when no default and no requested name", async () => {
|
|
198
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
199
|
+
const { ConfigError } = await import("../../src/core/errors");
|
|
200
|
+
let caught;
|
|
201
|
+
try {
|
|
202
|
+
requireAgentProfile({});
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
caught = err;
|
|
206
|
+
}
|
|
207
|
+
expect(caught).toBeInstanceOf(ConfigError);
|
|
208
|
+
expect(caught.message).toContain("require a profile");
|
|
209
|
+
});
|
|
210
|
+
test("resolves the requested profile when valid", async () => {
|
|
211
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
212
|
+
const profile = requireAgentProfile({ default: "claude" });
|
|
213
|
+
expect(profile.name).toBe("claude");
|
|
214
|
+
expect(profile.bin).toBe("claude");
|
|
215
|
+
});
|
|
216
|
+
test("explicit requested name beats config default", async () => {
|
|
217
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
218
|
+
const profile = requireAgentProfile({ default: "claude" }, "codex");
|
|
219
|
+
expect(profile.name).toBe("codex");
|
|
220
|
+
});
|
|
221
|
+
});
|