akm-cli 0.6.1 → 0.7.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 +66 -0
- package/dist/{cli.js → src/cli.js} +712 -34
- package/dist/{commands → src/commands}/config-cli.js +47 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +191 -0
- package/dist/{commands → src/commands}/installed-stashes.js +1 -1
- 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 +71 -7
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +104 -4
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +73 -0
- package/dist/{commands → src/commands}/source-add.js +5 -1
- package/dist/{commands → src/commands}/source-manage.js +7 -1
- 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 +203 -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 +114 -24
- 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 +88 -7
- package/dist/{indexer → src/indexer}/matchers.js +1 -1
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +111 -3
- package/dist/{indexer → src/indexer}/search-source.js +4 -2
- 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 +272 -0
- package/dist/{integrations → src/integrations}/github.js +9 -3
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/{llm → src/llm}/embedders/remote.js +37 -3
- 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}/cli-hints.js +15 -2
- package/dist/{output → src/output}/renderers.js +63 -2
- package/dist/src/output/shapes.js +523 -0
- package/dist/src/output/text.js +1116 -0
- package/dist/{registry → src/registry}/build-index.js +19 -8
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +6 -3
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +52 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/{wiki → src/wiki}/wiki.js +54 -6
- package/dist/{workflows → src/workflows}/runs.js +37 -3
- 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 +996 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +234 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +1018 -0
- package/dist/tests/bench/cli.test.js +445 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +317 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/doctor.js +525 -0
- package/dist/tests/bench/driver.js +401 -0
- package/dist/tests/bench/driver.test.js +584 -0
- package/dist/tests/bench/environment.js +233 -0
- package/dist/tests/bench/environment.test.js +199 -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 +647 -0
- package/dist/tests/bench/evolve.test.js +624 -0
- package/dist/tests/bench/failure-modes.test.js +349 -0
- package/dist/tests/bench/feedback-integrity.test.js +457 -0
- package/dist/tests/bench/leakage.test.js +228 -0
- package/dist/tests/bench/learning-curve.test.js +134 -0
- package/dist/tests/bench/metrics.js +2395 -0
- package/dist/tests/bench/metrics.test.js +1150 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/opencode-config.js +194 -0
- package/dist/tests/bench/opencode-config.test.js +370 -0
- package/dist/tests/bench/report.js +1885 -0
- package/dist/tests/bench/report.test.js +1038 -0
- package/dist/tests/bench/run-config.js +355 -0
- package/dist/tests/bench/run-config.test.js +298 -0
- package/dist/tests/bench/run-curate-test.js +32 -0
- package/dist/tests/bench/run-failing-tasks.js +56 -0
- package/dist/tests/bench/run-full-bench.js +51 -0
- package/dist/tests/bench/run-items36-targeted.js +69 -0
- package/dist/tests/bench/run-nano-quick.js +42 -0
- package/dist/tests/bench/run-waveg-targeted.js +62 -0
- package/dist/tests/bench/runner.js +699 -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 +131 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +114 -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 +345 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +472 -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 +204 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +418 -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 +569 -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 +1419 -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 +97 -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 +570 -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 +218 -0
- package/dist/tests/output-shapes-unit.test.js +478 -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 +394 -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 +238 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +923 -0
- package/dist/tests/remember-frontmatter.test.js +378 -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 +286 -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 +281 -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 +395 -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/output/text.js +0 -520
- /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-clone.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}/search-fields.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/{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/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.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}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prompt builders for proposal-producing agent commands (#226).
|
|
3
|
+
*
|
|
4
|
+
* `akm reflect` and `akm propose` both shell out to the configured agent CLI
|
|
5
|
+
* (via {@link runAgent}) and ask it for a structured proposal payload. The
|
|
6
|
+
* prompts are intentionally similar — both ask the agent to return a single
|
|
7
|
+
* JSON object containing `ref`, `content`, and (optionally) `frontmatter` —
|
|
8
|
+
* so we share the construction here. Keeping the prompt builders in
|
|
9
|
+
* `src/integrations/agent/` rather than `src/llm/` is deliberate: these are
|
|
10
|
+
* shell-out prompts targeting an agent CLI, not in-tree LLM API calls.
|
|
11
|
+
*
|
|
12
|
+
* The output the agent must produce is a *strict* JSON object:
|
|
13
|
+
*
|
|
14
|
+
* ```json
|
|
15
|
+
* {
|
|
16
|
+
* "ref": "lesson:my-lesson",
|
|
17
|
+
* "content": "---\ndescription: ...\nwhen_to_use: ...\n---\n\nbody",
|
|
18
|
+
* "frontmatter": { "description": "...", "when_to_use": "..." }
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* `frontmatter` is optional — the proposal queue parses it from `content`
|
|
23
|
+
* during validation. We carry it through if the agent supplies it.
|
|
24
|
+
*/
|
|
25
|
+
import { TYPE_DIRS } from "../../core/asset-spec";
|
|
26
|
+
/**
|
|
27
|
+
* Per-asset-type frontmatter / authoring hints surfaced in the prompt so
|
|
28
|
+
* the agent can produce content that passes proposal validation. Kept tiny:
|
|
29
|
+
* full schema docs live in `docs/` — these are nudges, not contracts.
|
|
30
|
+
*/
|
|
31
|
+
const TYPE_HINTS = {
|
|
32
|
+
lesson: "lesson assets MUST start with frontmatter containing `description` and `when_to_use` keys (both non-empty). Body should be 1–3 short paragraphs of practical guidance.",
|
|
33
|
+
skill: "skill assets are stored as `skills/<name>/SKILL.md`. Frontmatter typically includes `name`, `description`, and `when_to_use`.",
|
|
34
|
+
command: "command assets are markdown with optional frontmatter (`name`, `description`). The body is the prompt template the user invokes.",
|
|
35
|
+
agent: "agent assets are markdown with frontmatter describing the agent role (`name`, `description`, optional `tools`, `model`).",
|
|
36
|
+
knowledge: "knowledge assets are reference markdown documents. Include a top-level `# Title` and concise sections.",
|
|
37
|
+
memory: "memory assets are short factual notes the user wants persisted across sessions. Frontmatter usually includes `description`.",
|
|
38
|
+
workflow: "workflow assets are markdown describing a multi-step process. Include `# <Title>` and ordered `## Step N` sections.",
|
|
39
|
+
script: "script assets are executable text files. Include a shebang and minimal usage comment.",
|
|
40
|
+
vault: "vault assets store environment variables (KEY=VALUE pairs). Comments use `#`. Never echo secret values back to the user.",
|
|
41
|
+
wiki: "wiki assets are markdown reference pages with `# Title` and structured headings.",
|
|
42
|
+
};
|
|
43
|
+
function hintForType(type) {
|
|
44
|
+
return TYPE_HINTS[type] ?? `assets of type "${type}" — produce sensible markdown with optional frontmatter.`;
|
|
45
|
+
}
|
|
46
|
+
function knownTypeList() {
|
|
47
|
+
return Object.keys(TYPE_DIRS).sort().join(", ");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Common envelope every prompt asks the agent to honour. The wrapper code
|
|
51
|
+
* uses `JSON.parse(stdout)` to extract the payload — anything outside the
|
|
52
|
+
* JSON object will be treated as a parse error.
|
|
53
|
+
*/
|
|
54
|
+
const RESPONSE_CONTRACT = [
|
|
55
|
+
"Respond ONLY with a single JSON object. No prose before or after.",
|
|
56
|
+
'Shape: {"ref": "<type>:<name>", "content": "<full file contents>", "frontmatter": {...}}',
|
|
57
|
+
"`content` is the full file body that will be written if accepted.",
|
|
58
|
+
"`frontmatter` is optional — include it if `content` starts with `---` so reviewers can sanity-check the keys.",
|
|
59
|
+
].join("\n");
|
|
60
|
+
/**
|
|
61
|
+
* Build the prompt for `akm reflect [ref]`. Asks the agent to review an
|
|
62
|
+
* existing asset (plus any negative feedback / lint findings) and propose
|
|
63
|
+
* an improved version. Returns a single string — the agent runtime will
|
|
64
|
+
* forward it as the trailing positional arg.
|
|
65
|
+
*/
|
|
66
|
+
export function buildReflectPrompt(input) {
|
|
67
|
+
const sections = [];
|
|
68
|
+
if (input.ref && input.type && input.name) {
|
|
69
|
+
sections.push(`You are reviewing an akm stash asset (${input.type}) called "${input.name}" and proposing an improved version.`);
|
|
70
|
+
sections.push(`Target ref: ${input.ref}`);
|
|
71
|
+
sections.push(`Asset-type guidance: ${hintForType(input.type)}`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
sections.push("You are reviewing recent akm feedback and proposing a single improved asset revision.");
|
|
75
|
+
sections.push("No target ref was supplied. Choose the best target from the feedback below and return it in `ref`.");
|
|
76
|
+
sections.push(`Known asset types: ${knownTypeList()}.`);
|
|
77
|
+
}
|
|
78
|
+
if (input.task?.trim()) {
|
|
79
|
+
sections.push(`Task / focus: ${input.task.trim()}`);
|
|
80
|
+
}
|
|
81
|
+
if (input.assetContent?.trim()) {
|
|
82
|
+
sections.push("Current asset content (verbatim):");
|
|
83
|
+
sections.push("```");
|
|
84
|
+
sections.push(input.assetContent.trimEnd());
|
|
85
|
+
sections.push("```");
|
|
86
|
+
}
|
|
87
|
+
else if (input.ref) {
|
|
88
|
+
sections.push("(No existing content — propose a fresh asset that fits the ref.)");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
sections.push("(No existing asset content was supplied.)");
|
|
92
|
+
}
|
|
93
|
+
if (input.feedback && input.feedback.length > 0) {
|
|
94
|
+
sections.push("Recent feedback / signals:");
|
|
95
|
+
for (const line of input.feedback)
|
|
96
|
+
sections.push(`- ${line}`);
|
|
97
|
+
}
|
|
98
|
+
else if (!input.ref) {
|
|
99
|
+
sections.push("Recent feedback / signals:");
|
|
100
|
+
sections.push("- (no feedback events recorded)");
|
|
101
|
+
}
|
|
102
|
+
if (input.schemaHints && input.schemaHints.length > 0) {
|
|
103
|
+
sections.push("Schema / lint hints to address:");
|
|
104
|
+
for (const line of input.schemaHints)
|
|
105
|
+
sections.push(`- ${line}`);
|
|
106
|
+
}
|
|
107
|
+
sections.push("Produce a single proposal that addresses the feedback and respects the asset-type contract.");
|
|
108
|
+
sections.push(RESPONSE_CONTRACT);
|
|
109
|
+
return sections.join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the prompt for `akm propose <type> <name> --task ...`. Asks the
|
|
113
|
+
* agent to author a brand-new asset of the given type fulfilling `task`.
|
|
114
|
+
*/
|
|
115
|
+
export function buildProposePrompt(input) {
|
|
116
|
+
const sections = [];
|
|
117
|
+
sections.push(`Author a new akm stash asset of type "${input.type}" named "${input.name}".`);
|
|
118
|
+
sections.push(`Task: ${input.task}`);
|
|
119
|
+
sections.push(`Asset-type guidance: ${hintForType(input.type)}`);
|
|
120
|
+
sections.push(`(Known asset types: ${knownTypeList()}.)`);
|
|
121
|
+
if (input.schemaHints && input.schemaHints.length > 0) {
|
|
122
|
+
sections.push("Schema / lint hints:");
|
|
123
|
+
for (const line of input.schemaHints)
|
|
124
|
+
sections.push(`- ${line}`);
|
|
125
|
+
}
|
|
126
|
+
sections.push("Produce a single proposal that, if accepted, would land as the asset described above.");
|
|
127
|
+
sections.push(RESPONSE_CONTRACT);
|
|
128
|
+
return sections.join("\n\n");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Parse agent stdout into a proposal payload. The agent contract requires a
|
|
132
|
+
* single JSON object; anything else is reported as a parse error so callers
|
|
133
|
+
* can map to {@link AgentFailureReason} `parse_error`.
|
|
134
|
+
*/
|
|
135
|
+
export function parseAgentProposalPayload(stdout) {
|
|
136
|
+
const trimmed = stripJsonFences(stdout).trim();
|
|
137
|
+
if (!trimmed)
|
|
138
|
+
throw new Error("agent produced empty output");
|
|
139
|
+
const parsed = JSON.parse(trimmed);
|
|
140
|
+
if (typeof parsed.ref !== "string" || !parsed.ref.trim()) {
|
|
141
|
+
throw new Error('agent response missing required string field "ref"');
|
|
142
|
+
}
|
|
143
|
+
if (typeof parsed.content !== "string" || !parsed.content.trim()) {
|
|
144
|
+
throw new Error('agent response missing required string field "content"');
|
|
145
|
+
}
|
|
146
|
+
const out = {
|
|
147
|
+
ref: parsed.ref.trim(),
|
|
148
|
+
content: parsed.content,
|
|
149
|
+
};
|
|
150
|
+
if (parsed.frontmatter && typeof parsed.frontmatter === "object" && !Array.isArray(parsed.frontmatter)) {
|
|
151
|
+
out.frontmatter = parsed.frontmatter;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Strip `\`\`\`json … \`\`\`` fences if the agent wrapped its JSON output.
|
|
157
|
+
* Mirrors the same helper in `src/llm/client.ts` but kept local here so
|
|
158
|
+
* `agent/` does not import from `llm/` (the boundary is one-way per
|
|
159
|
+
* v1 spec §9.7 — agents are shell-out only).
|
|
160
|
+
*/
|
|
161
|
+
export function stripJsonFences(text) {
|
|
162
|
+
const trimmed = text.trim();
|
|
163
|
+
const fenced = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/);
|
|
164
|
+
if (fenced)
|
|
165
|
+
return fenced[1] ?? trimmed;
|
|
166
|
+
return trimmed;
|
|
167
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent CLI spawn wrapper (v1 spec §12.2).
|
|
3
|
+
*
|
|
4
|
+
* Single helper that owns:
|
|
5
|
+
* • Process spawn (Bun's subprocess API).
|
|
6
|
+
* • Captured vs interactive stdio.
|
|
7
|
+
* • Hard timeout (per-call override or profile default).
|
|
8
|
+
* • Structured failure reasons — `timeout`, `spawn_failed`,
|
|
9
|
+
* `non_zero_exit`, `parse_error`.
|
|
10
|
+
*
|
|
11
|
+
* NEVER imports an LLM SDK. Agents are reachable only via shell-out;
|
|
12
|
+
* this is a pre-emptive guarantee against the #222 invariant.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
|
|
15
|
+
/**
|
|
16
|
+
* Kill the process group of `proc` with `signal`, falling back to
|
|
17
|
+
* `proc.kill(signal)` when `proc.pid` is unavailable (e.g. test fakes).
|
|
18
|
+
*
|
|
19
|
+
* Passing a negative PID to `process.kill` targets the entire process
|
|
20
|
+
* group, so opencode's child processes (the .opencode binary, etc.) are
|
|
21
|
+
* reaped alongside the node wrapper. The fallback keeps test fakes working
|
|
22
|
+
* without modification.
|
|
23
|
+
*/
|
|
24
|
+
export function killGroup(proc, signal) {
|
|
25
|
+
if (typeof proc.pid === "number") {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(-proc.pid, signal);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Process may have already exited; fall through to direct kill.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
proc.kill(signal);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const DEFAULT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_MS;
|
|
42
|
+
function resolveSpawnFn(options) {
|
|
43
|
+
if (options.spawn)
|
|
44
|
+
return options.spawn;
|
|
45
|
+
// Pull from globalThis so tests that swap it out at module level are honoured.
|
|
46
|
+
const bun = globalThis.Bun;
|
|
47
|
+
if (!bun?.spawn) {
|
|
48
|
+
throw new Error("Bun.spawn is unavailable; pass options.spawn for non-Bun environments.");
|
|
49
|
+
}
|
|
50
|
+
return bun.spawn.bind(bun);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the child env. Starts empty and copies through:
|
|
54
|
+
* • Every name in `profile.envPassthrough`.
|
|
55
|
+
* • Every entry in `profile.env`.
|
|
56
|
+
* • Every entry in `options.env` (highest precedence).
|
|
57
|
+
*/
|
|
58
|
+
function buildChildEnv(profile, options) {
|
|
59
|
+
const source = options.envSource ?? process.env;
|
|
60
|
+
const env = {};
|
|
61
|
+
for (const name of profile.envPassthrough) {
|
|
62
|
+
const value = source[name];
|
|
63
|
+
if (value !== undefined)
|
|
64
|
+
env[name] = value;
|
|
65
|
+
}
|
|
66
|
+
if (profile.env) {
|
|
67
|
+
for (const [k, v] of Object.entries(profile.env))
|
|
68
|
+
env[k] = v;
|
|
69
|
+
}
|
|
70
|
+
if (options.env) {
|
|
71
|
+
for (const [k, v] of Object.entries(options.env))
|
|
72
|
+
env[k] = v;
|
|
73
|
+
}
|
|
74
|
+
return env;
|
|
75
|
+
}
|
|
76
|
+
async function readStream(stream, opts) {
|
|
77
|
+
if (!stream)
|
|
78
|
+
return "";
|
|
79
|
+
const readPromise = new Response(stream).text().catch(() => "");
|
|
80
|
+
if (!opts?.timeoutMs)
|
|
81
|
+
return readPromise;
|
|
82
|
+
// Race the stream read against a timeout so a process that is killed via
|
|
83
|
+
// SIGTERM/SIGKILL but whose pipe endpoints stay open (e.g. background
|
|
84
|
+
// threads still holding the fd) cannot block the caller indefinitely.
|
|
85
|
+
// On timeout we return whatever we received so far (empty string here since
|
|
86
|
+
// `readPromise` is all-or-nothing with `Response.text()`).
|
|
87
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
88
|
+
setTimeout(() => resolve(""), opts.timeoutMs);
|
|
89
|
+
});
|
|
90
|
+
return Promise.race([readPromise, timeoutPromise]);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Spawn the agent CLI described by `profile` with `prompt` (forwarded as
|
|
94
|
+
* the last positional arg by default) and return a structured result.
|
|
95
|
+
*
|
|
96
|
+
* The `prompt` argument is appended to `profile.args` (and `options.args`)
|
|
97
|
+
* unless it is `undefined`. Pass `prompt = ""` to forward an explicit
|
|
98
|
+
* empty positional, or pass extra args via `options.args`.
|
|
99
|
+
*
|
|
100
|
+
* Failure modes (see {@link AgentFailureReason}):
|
|
101
|
+
*
|
|
102
|
+
* • `spawn_failed` — `Bun.spawn` threw synchronously.
|
|
103
|
+
* • `timeout` — exceeded the resolved timeout.
|
|
104
|
+
* • `non_zero_exit` — child exited with a non-zero code.
|
|
105
|
+
* • `parse_error` — `parseOutput === "json"` and stdout was not JSON.
|
|
106
|
+
*
|
|
107
|
+
* `ok === true` requires exit code 0 and (if `parseOutput === "json"`)
|
|
108
|
+
* a successful `JSON.parse`.
|
|
109
|
+
*/
|
|
110
|
+
export async function runAgent(profile, prompt, options = {}) {
|
|
111
|
+
const stdioMode = options.stdio ?? profile.stdio;
|
|
112
|
+
const timeoutMs = options.timeoutMs ?? profile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
113
|
+
const parseOutput = options.parseOutput ?? profile.parseOutput;
|
|
114
|
+
const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
|
|
115
|
+
const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
|
|
116
|
+
const args = [...profile.args, ...(options.args ?? [])];
|
|
117
|
+
if (prompt !== undefined)
|
|
118
|
+
args.push(prompt);
|
|
119
|
+
const env = buildChildEnv(profile, options);
|
|
120
|
+
const start = Date.now();
|
|
121
|
+
let proc;
|
|
122
|
+
try {
|
|
123
|
+
const spawnFn = resolveSpawnFn(options);
|
|
124
|
+
proc = spawnFn([profile.bin, ...args], {
|
|
125
|
+
stdin: stdioMode === "captured" ? (options.stdin !== undefined ? "pipe" : "ignore") : "inherit",
|
|
126
|
+
stdout: stdioMode === "captured" ? "pipe" : "inherit",
|
|
127
|
+
stderr: stdioMode === "captured" ? "pipe" : "inherit",
|
|
128
|
+
env,
|
|
129
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
130
|
+
// Spawn in its own process group so killGroup(-pid, signal) reaches all
|
|
131
|
+
// descendants (e.g. the .opencode binary that opencode's node wrapper forks).
|
|
132
|
+
// Only applied in captured mode — interactive mode inherits the parent
|
|
133
|
+
// terminal's process group intentionally.
|
|
134
|
+
...(stdioMode === "captured" ? { detached: true } : {}),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const durationMs = Date.now() - start;
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
exitCode: null,
|
|
142
|
+
stdout: "",
|
|
143
|
+
stderr: "",
|
|
144
|
+
durationMs,
|
|
145
|
+
reason: "spawn_failed",
|
|
146
|
+
error: err instanceof Error ? err.message : String(err),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Hard timeout. We prefer SIGTERM, then SIGKILL if SIGTERM is ignored,
|
|
150
|
+
// but Bun.spawn only exposes a single .kill() — one signal is enough
|
|
151
|
+
// for the structured-failure contract.
|
|
152
|
+
//
|
|
153
|
+
// BUG-M3: only flag `timedOut` when the child has not already exited. A
|
|
154
|
+
// timer firing in the same microtask as `proc.exited` resolving could
|
|
155
|
+
// otherwise label a clean exit as a timeout.
|
|
156
|
+
let timedOut = false;
|
|
157
|
+
const timer = setTimeoutImpl(() => {
|
|
158
|
+
if (proc.exitCode !== null)
|
|
159
|
+
return;
|
|
160
|
+
timedOut = true;
|
|
161
|
+
killGroup(proc, "SIGTERM");
|
|
162
|
+
// Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
|
|
163
|
+
setTimeoutImpl(() => {
|
|
164
|
+
if (proc.exitCode !== null)
|
|
165
|
+
return;
|
|
166
|
+
killGroup(proc, "SIGKILL");
|
|
167
|
+
}, 5000);
|
|
168
|
+
}, timeoutMs);
|
|
169
|
+
// Stream-drain timeout: the overall wall-clock budget plus a 2 s grace
|
|
170
|
+
// period. When a process is killed via SIGTERM/SIGKILL (from our timeout
|
|
171
|
+
// handler or from outside) some runtimes keep the pipe write-end open in
|
|
172
|
+
// background threads, which would cause `Response.text()` to block forever.
|
|
173
|
+
// Capping stream draining at `timeoutMs + 2 000 ms` ensures the caller
|
|
174
|
+
// never hangs past the wall budget regardless of subprocess pipe behaviour.
|
|
175
|
+
const streamDrainTimeoutMs = timeoutMs + 2_000;
|
|
176
|
+
const stdoutPromise = stdioMode === "captured"
|
|
177
|
+
? readStream(proc.stdout ?? null, { timeoutMs: streamDrainTimeoutMs })
|
|
178
|
+
: Promise.resolve("");
|
|
179
|
+
const stderrPromise = stdioMode === "captured"
|
|
180
|
+
? readStream(proc.stderr ?? null, { timeoutMs: streamDrainTimeoutMs })
|
|
181
|
+
: Promise.resolve("");
|
|
182
|
+
// Optional stdin payload (captured mode only).
|
|
183
|
+
//
|
|
184
|
+
// BUG-H1: race the stdin write/close against `proc.exited` and the
|
|
185
|
+
// timeout timer. If the child never drains stdin, an unraced
|
|
186
|
+
// `await writer.write()` would block forever and prevent `runAgent`
|
|
187
|
+
// from ever returning.
|
|
188
|
+
if (options.stdin !== undefined && stdioMode === "captured" && proc.stdin) {
|
|
189
|
+
const stdinPayload = options.stdin;
|
|
190
|
+
const stdinStream = proc.stdin;
|
|
191
|
+
const stdinDone = (async () => {
|
|
192
|
+
try {
|
|
193
|
+
const writer = stdinStream.getWriter();
|
|
194
|
+
const bytes = new TextEncoder().encode(stdinPayload);
|
|
195
|
+
await writer.write(bytes);
|
|
196
|
+
await writer.close();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Best-effort: ignore stdin write failures, the child will get EOF.
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
// Resolve as soon as either the write completes or the child exits.
|
|
203
|
+
// We don't await the result — only that one of the two has settled —
|
|
204
|
+
// so a stuck writer cannot keep us pinned past the timeout.
|
|
205
|
+
await Promise.race([stdinDone, proc.exited.catch(() => undefined)]);
|
|
206
|
+
}
|
|
207
|
+
let exitCode = null;
|
|
208
|
+
try {
|
|
209
|
+
exitCode = await proc.exited;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
clearTimeoutImpl(timer);
|
|
213
|
+
// BUG-H2: drain stream readers before the early return so they don't
|
|
214
|
+
// surface as unhandled rejections after the function resolves.
|
|
215
|
+
// The streams already carry a built-in drain timeout so this allSettled
|
|
216
|
+
// will not block indefinitely.
|
|
217
|
+
await Promise.allSettled([stdoutPromise, stderrPromise]);
|
|
218
|
+
const durationMs = Date.now() - start;
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
exitCode: null,
|
|
222
|
+
stdout: "",
|
|
223
|
+
stderr: "",
|
|
224
|
+
durationMs,
|
|
225
|
+
reason: "spawn_failed",
|
|
226
|
+
error: err instanceof Error ? err.message : String(err),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
clearTimeoutImpl(timer);
|
|
230
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
231
|
+
const durationMs = Date.now() - start;
|
|
232
|
+
if (timedOut) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
exitCode,
|
|
236
|
+
stdout,
|
|
237
|
+
stderr,
|
|
238
|
+
durationMs,
|
|
239
|
+
reason: "timeout",
|
|
240
|
+
error: `agent CLI "${profile.name}" timed out after ${timeoutMs}ms`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (exitCode !== 0) {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
exitCode,
|
|
247
|
+
stdout,
|
|
248
|
+
stderr,
|
|
249
|
+
durationMs,
|
|
250
|
+
reason: "non_zero_exit",
|
|
251
|
+
error: `agent CLI "${profile.name}" exited with code ${exitCode}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (parseOutput === "json" && stdioMode === "captured") {
|
|
255
|
+
try {
|
|
256
|
+
const parsed = JSON.parse(stdout);
|
|
257
|
+
return { ok: true, exitCode, stdout, stderr, durationMs, parsed };
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
exitCode,
|
|
263
|
+
stdout,
|
|
264
|
+
stderr,
|
|
265
|
+
durationMs,
|
|
266
|
+
reason: "parse_error",
|
|
267
|
+
error: err instanceof Error ? err.message : String(err),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { ok: true, exitCode, stdout, stderr, durationMs };
|
|
272
|
+
}
|
|
@@ -2,8 +2,13 @@ import * as childProcess from "node:child_process";
|
|
|
2
2
|
export const GITHUB_API_BASE = "https://api.github.com";
|
|
3
3
|
const GITHUB_TOKEN_DOMAINS = new Set(["api.github.com", "github.com", "uploads.github.com"]);
|
|
4
4
|
function readGithubTokenFromEnv() {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
if (process.env.GITHUB_TOKEN !== undefined) {
|
|
6
|
+
return process.env.GITHUB_TOKEN.trim();
|
|
7
|
+
}
|
|
8
|
+
if (process.env.GH_TOKEN !== undefined) {
|
|
9
|
+
return process.env.GH_TOKEN.trim();
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
7
12
|
}
|
|
8
13
|
function readGithubTokenFromGhCli() {
|
|
9
14
|
const result = childProcess.spawnSync("gh", ["auth", "token"], {
|
|
@@ -17,7 +22,8 @@ function readGithubTokenFromGhCli() {
|
|
|
17
22
|
return token || undefined;
|
|
18
23
|
}
|
|
19
24
|
function resolveGithubToken() {
|
|
20
|
-
|
|
25
|
+
const token = readGithubTokenFromEnv();
|
|
26
|
+
return token !== undefined ? token || undefined : readGithubTokenFromGhCli();
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Build headers for GitHub API requests.
|
|
@@ -3,34 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { getConfigDir } from "../core/config";
|
|
4
4
|
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
5
5
|
const LOCKFILE_NAME = "akm.lock";
|
|
6
|
-
const LEGACY_LOCKFILE_NAME = "stash.lock";
|
|
7
6
|
function getLockfilePath() {
|
|
8
7
|
return path.join(getConfigDir(), LOCKFILE_NAME);
|
|
9
8
|
}
|
|
10
|
-
function getLegacyLockfilePath() {
|
|
11
|
-
return path.join(getConfigDir(), LEGACY_LOCKFILE_NAME);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* One-time migration: if the new `akm.lock` does not exist but the legacy
|
|
15
|
-
* `stash.lock` does, copy it across so installed-stash tracking survives the
|
|
16
|
-
* rename. Best-effort; failures are silent because the lockfile loader treats
|
|
17
|
-
* a missing file as an empty lockfile.
|
|
18
|
-
*/
|
|
19
|
-
function migrateLegacyLockfileIfNeeded() {
|
|
20
|
-
const newPath = getLockfilePath();
|
|
21
|
-
const legacyPath = getLegacyLockfilePath();
|
|
22
|
-
try {
|
|
23
|
-
if (fs.existsSync(newPath))
|
|
24
|
-
return;
|
|
25
|
-
if (!fs.existsSync(legacyPath))
|
|
26
|
-
return;
|
|
27
|
-
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
28
|
-
fs.copyFileSync(legacyPath, newPath);
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
/* best-effort — fall through to empty lockfile */
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
9
|
// ── Lock sentinel ────────────────────────────────────────────────────────────
|
|
35
10
|
const LOCK_MAX_RETRIES = 3;
|
|
36
11
|
const LOCK_RETRY_DELAY_MS = 100;
|
|
@@ -100,7 +75,6 @@ function releaseLockSentinel() {
|
|
|
100
75
|
}
|
|
101
76
|
// ── Read / Write ────────────────────────────────────────────────────────────
|
|
102
77
|
export function readLockfile() {
|
|
103
|
-
migrateLegacyLockfileIfNeeded();
|
|
104
78
|
const lockfilePath = getLockfilePath();
|
|
105
79
|
try {
|
|
106
80
|
const raw = JSON.parse(fs.readFileSync(lockfilePath, "utf8"));
|
|
@@ -8,6 +8,36 @@
|
|
|
8
8
|
* `llm.ts` re-exports everything from this module for backward compatibility.
|
|
9
9
|
*/
|
|
10
10
|
import { fetchWithTimeout } from "../core/common";
|
|
11
|
+
/** Maximum length of an LLM error response body included in thrown errors. */
|
|
12
|
+
const ERROR_BODY_MAX_LEN = 200;
|
|
13
|
+
/**
|
|
14
|
+
* Redact credential-shaped substrings from an upstream error body before
|
|
15
|
+
* including it in a thrown Error. The body is also trimmed to a fixed length
|
|
16
|
+
* so that a verbose provider response cannot leak large amounts of context.
|
|
17
|
+
*
|
|
18
|
+
* Targets:
|
|
19
|
+
* - `Bearer <token>` headers echoed back by the provider
|
|
20
|
+
* - `sk-…` / `sk_…` style API keys (OpenAI / Anthropic-shaped)
|
|
21
|
+
* - `key-…` / `key_…` shorthand keys
|
|
22
|
+
* - `"api_key": "…"` / `"apiKey": "…"` JSON fields
|
|
23
|
+
*/
|
|
24
|
+
export function redactErrorBody(input) {
|
|
25
|
+
if (!input)
|
|
26
|
+
return "";
|
|
27
|
+
let out = input
|
|
28
|
+
// Bearer tokens (case-insensitive)
|
|
29
|
+
.replace(/\bBearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [REDACTED]")
|
|
30
|
+
// sk-/sk_ style keys
|
|
31
|
+
.replace(/\bsk[-_][A-Za-z0-9._-]{6,}/g, "[REDACTED]")
|
|
32
|
+
// key-/key_ shorthand keys
|
|
33
|
+
.replace(/\bkey[-_][A-Za-z0-9._-]{6,}/g, "[REDACTED]")
|
|
34
|
+
// JSON-style "api_key": "...", "apiKey": "...", "api-key": "..."
|
|
35
|
+
.replace(/("(?:api[_-]?key|apiKey|authorization|token)"\s*:\s*")([^"]*)(")/gi, "$1[REDACTED]$3");
|
|
36
|
+
if (out.length > ERROR_BODY_MAX_LEN) {
|
|
37
|
+
out = `${out.slice(0, ERROR_BODY_MAX_LEN)}…`;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
11
41
|
export async function chatCompletion(config, messages, options) {
|
|
12
42
|
const headers = { "Content-Type": "application/json" };
|
|
13
43
|
if (config.apiKey) {
|
|
@@ -24,8 +54,9 @@ export async function chatCompletion(config, messages, options) {
|
|
|
24
54
|
}),
|
|
25
55
|
});
|
|
26
56
|
if (!response.ok) {
|
|
27
|
-
const
|
|
28
|
-
|
|
57
|
+
const rawBody = await response.text().catch(() => "");
|
|
58
|
+
const safeBody = redactErrorBody(rawBody);
|
|
59
|
+
throw new Error(`LLM request failed (${response.status}) ${config.endpoint}: ${safeBody}`);
|
|
29
60
|
}
|
|
30
61
|
const json = (await response.json());
|
|
31
62
|
return json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* vectors so the scoring pipeline's L2-to-cosine conversion is correct.
|
|
6
6
|
*/
|
|
7
7
|
import { fetchWithTimeout, isHttpUrl } from "../../core/common";
|
|
8
|
-
const
|
|
8
|
+
const DEFAULT_REMOTE_BATCH_SIZE = 100;
|
|
9
|
+
/** Cheap token estimator: 4 chars ≈ 1 token. Used in verbose logging and error messages. */
|
|
10
|
+
export function estimateTokenCount(text) {
|
|
11
|
+
return Math.round(text.length / 4);
|
|
12
|
+
}
|
|
9
13
|
export class RemoteEmbedder {
|
|
10
14
|
config;
|
|
11
15
|
constructor(config) {
|
|
@@ -20,6 +24,10 @@ export class RemoteEmbedder {
|
|
|
20
24
|
if (this.config.dimension) {
|
|
21
25
|
body.dimensions = this.config.dimension;
|
|
22
26
|
}
|
|
27
|
+
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
28
|
+
if (ollamaOpts) {
|
|
29
|
+
body.options = ollamaOpts;
|
|
30
|
+
}
|
|
23
31
|
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.config.endpoint), {
|
|
24
32
|
method: "POST",
|
|
25
33
|
headers,
|
|
@@ -40,8 +48,10 @@ export class RemoteEmbedder {
|
|
|
40
48
|
return [];
|
|
41
49
|
const results = [];
|
|
42
50
|
const headers = this.buildHeaders();
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
const ollamaOpts = resolveOllamaOptions(this.config);
|
|
52
|
+
const batchSize = this.config.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
|
|
53
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
54
|
+
const batch = texts.slice(i, i + batchSize);
|
|
45
55
|
const body = {
|
|
46
56
|
input: batch,
|
|
47
57
|
model: this.config.model,
|
|
@@ -49,6 +59,9 @@ export class RemoteEmbedder {
|
|
|
49
59
|
if (this.config.dimension) {
|
|
50
60
|
body.dimensions = this.config.dimension;
|
|
51
61
|
}
|
|
62
|
+
if (ollamaOpts) {
|
|
63
|
+
body.options = ollamaOpts;
|
|
64
|
+
}
|
|
52
65
|
const response = await fetchWithTimeout(normalizeEmbeddingEndpoint(this.config.endpoint), {
|
|
53
66
|
method: "POST",
|
|
54
67
|
headers,
|
|
@@ -115,6 +128,27 @@ function embeddingEndpointPathHint(endpoint) {
|
|
|
115
128
|
}
|
|
116
129
|
return "";
|
|
117
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve Ollama-native `options` from the embedding config.
|
|
133
|
+
*
|
|
134
|
+
* Resolution order:
|
|
135
|
+
* 1. `ollamaOptions` — forwarded verbatim (explicit opt-in, takes precedence).
|
|
136
|
+
* 2. `contextLength` — wrapped as `{ num_ctx: contextLength }`.
|
|
137
|
+
* 3. Neither set → returns `undefined` (no `options` field in the request body).
|
|
138
|
+
*
|
|
139
|
+
* These options are only meaningful for Ollama's native `/api/embed` endpoint.
|
|
140
|
+
* OpenAI-compatible endpoints ignore unknown request fields, so passing them to
|
|
141
|
+
* other providers is harmless but has no effect.
|
|
142
|
+
*/
|
|
143
|
+
function resolveOllamaOptions(config) {
|
|
144
|
+
if (config.ollamaOptions && Object.keys(config.ollamaOptions).length > 0) {
|
|
145
|
+
return config.ollamaOptions;
|
|
146
|
+
}
|
|
147
|
+
if (config.contextLength) {
|
|
148
|
+
return { num_ctx: config.contextLength };
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
118
152
|
/** Check whether an EmbeddingConnectionConfig has a valid remote endpoint. */
|
|
119
153
|
export function hasRemoteEndpoint(config) {
|
|
120
154
|
return isHttpUrl(config.endpoint);
|