xcoder-dna 0.0.0-stage → 1.3.1
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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/agents/ai-chat-debate.d.ts +36 -0
- package/dist/agents/ai-chat-debate.js +300 -0
- package/dist/agents/ai-chat-report-writer.d.ts +7 -0
- package/dist/agents/ai-chat-report-writer.js +100 -0
- package/dist/agents/duplicate-detector.d.ts +41 -0
- package/dist/agents/duplicate-detector.js +65 -0
- package/dist/agents/genesis-agent.d.ts +61 -0
- package/dist/agents/genesis-agent.js +340 -0
- package/dist/agents/samurai-agent.d.ts +53 -0
- package/dist/agents/samurai-agent.js +279 -0
- package/dist/agents/sub-agent.d.ts +56 -0
- package/dist/agents/sub-agent.js +353 -0
- package/dist/backlog/backlog-requirement-validator.d.ts +10 -0
- package/dist/backlog/backlog-requirement-validator.js +60 -0
- package/dist/backlog/backlog-store.d.ts +29 -0
- package/dist/backlog/backlog-store.js +148 -0
- package/dist/backlog/po-agent.d.ts +16 -0
- package/dist/backlog/po-agent.js +121 -0
- package/dist/backlog/types.d.ts +25 -0
- package/dist/backlog/types.js +4 -0
- package/dist/bench/run.d.ts +11 -0
- package/dist/bench/run.js +185 -0
- package/dist/bench/types.d.ts +50 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/swarm.d.ts +10 -0
- package/dist/cli/swarm.js +100 -0
- package/dist/cli/task.d.ts +14 -0
- package/dist/cli/task.js +121 -0
- package/dist/cli/wbc-arg.d.ts +6 -0
- package/dist/cli/wbc-arg.js +40 -0
- package/dist/cli/wbc.d.ts +45 -0
- package/dist/cli/wbc.js +98 -0
- package/dist/commands/license.d.ts +9 -0
- package/dist/commands/license.js +140 -0
- package/dist/commands/release.d.ts +44 -0
- package/dist/commands/release.js +180 -0
- package/dist/commands/test.d.ts +50 -0
- package/dist/commands/test.js +385 -0
- package/dist/config/loadConfig.d.ts +82 -0
- package/dist/config/loadConfig.js +240 -0
- package/dist/config/secure-mode.d.ts +29 -0
- package/dist/config/secure-mode.js +63 -0
- package/dist/config/user-config.d.ts +39 -0
- package/dist/config/user-config.js +202 -0
- package/dist/engine/reasoning-engine.d.ts +21 -0
- package/dist/engine/reasoning-engine.js +59 -0
- package/dist/events/bus.d.ts +22 -0
- package/dist/events/bus.js +18 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +77 -0
- package/dist/indexing/ignoreRules.d.ts +1 -0
- package/dist/indexing/ignoreRules.js +53 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.d.ts +57 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.js +204 -0
- package/dist/instance-swarm/protocol-name-resolver.d.ts +23 -0
- package/dist/instance-swarm/protocol-name-resolver.js +64 -0
- package/dist/license/gate.d.ts +31 -0
- package/dist/license/gate.js +188 -0
- package/dist/license/http-client.d.ts +43 -0
- package/dist/license/http-client.js +195 -0
- package/dist/license/manager.d.ts +39 -0
- package/dist/license/manager.js +175 -0
- package/dist/license/receipt.d.ts +27 -0
- package/dist/license/receipt.js +108 -0
- package/dist/license/state-codec.d.ts +72 -0
- package/dist/license/state-codec.js +218 -0
- package/dist/license/state-paths.d.ts +17 -0
- package/dist/license/state-paths.js +64 -0
- package/dist/license/store.d.ts +44 -0
- package/dist/license/store.js +223 -0
- package/dist/license/trust-anchor.d.ts +24 -0
- package/dist/license/trust-anchor.js +74 -0
- package/dist/license/types.d.ts +34 -0
- package/dist/license/types.js +19 -0
- package/dist/llm/budget.d.ts +23 -0
- package/dist/llm/budget.js +58 -0
- package/dist/llm/client.d.ts +53 -0
- package/dist/llm/client.js +122 -0
- package/dist/llm/factory.d.ts +14 -0
- package/dist/llm/factory.js +32 -0
- package/dist/orchestrator/defect-logger.d.ts +31 -0
- package/dist/orchestrator/defect-logger.js +101 -0
- package/dist/orchestrator/defect-manager.d.ts +118 -0
- package/dist/orchestrator/defect-manager.js +338 -0
- package/dist/orchestrator/defect-migration.d.ts +14 -0
- package/dist/orchestrator/defect-migration.js +174 -0
- package/dist/orchestrator/defect-store.d.ts +48 -0
- package/dist/orchestrator/defect-store.js +76 -0
- package/dist/orchestrator/healing.d.ts +14 -0
- package/dist/orchestrator/healing.js +11 -0
- package/dist/orchestrator/interactive-steering.d.ts +64 -0
- package/dist/orchestrator/interactive-steering.js +107 -0
- package/dist/orchestrator/interrupt-handler.d.ts +30 -0
- package/dist/orchestrator/interrupt-handler.js +70 -0
- package/dist/orchestrator/lesson-manager.d.ts +25 -0
- package/dist/orchestrator/lesson-manager.js +122 -0
- package/dist/orchestrator/progress-summary.d.ts +15 -0
- package/dist/orchestrator/progress-summary.js +106 -0
- package/dist/orchestrator/reasoning-score.d.ts +49 -0
- package/dist/orchestrator/reasoning-score.js +117 -0
- package/dist/orchestrator/swarm.d.ts +36 -0
- package/dist/orchestrator/swarm.js +77 -0
- package/dist/orchestrator/task-factory.d.ts +15 -0
- package/dist/orchestrator/task-factory.js +176 -0
- package/dist/orchestrator/validation-gate.d.ts +23 -0
- package/dist/orchestrator/validation-gate.js +155 -0
- package/dist/protocol/react-contract.d.ts +136 -0
- package/dist/protocol/react-contract.js +698 -0
- package/dist/protocol/wks-protocol-manager.d.ts +22 -0
- package/dist/protocol/wks-protocol-manager.js +72 -0
- package/dist/sdlc-runtime/conversation-runner.d.ts +8 -0
- package/dist/sdlc-runtime/conversation-runner.js +64 -0
- package/dist/sdlc-runtime/dag-utils.d.ts +15 -0
- package/dist/sdlc-runtime/dag-utils.js +67 -0
- package/dist/sdlc-runtime/defect-integration.d.ts +7 -0
- package/dist/sdlc-runtime/defect-integration.js +99 -0
- package/dist/sdlc-runtime/execution-loop.d.ts +38 -0
- package/dist/sdlc-runtime/execution-loop.js +274 -0
- package/dist/sdlc-runtime/lesson-recorder.d.ts +9 -0
- package/dist/sdlc-runtime/lesson-recorder.js +27 -0
- package/dist/sdlc-runtime/plan-initializer.d.ts +47 -0
- package/dist/sdlc-runtime/plan-initializer.js +135 -0
- package/dist/sdlc-runtime/progress-persister.d.ts +10 -0
- package/dist/sdlc-runtime/progress-persister.js +34 -0
- package/dist/sdlc-runtime/protocol-loader.d.ts +10 -0
- package/dist/sdlc-runtime/protocol-loader.js +21 -0
- package/dist/sdlc-runtime/reporting.d.ts +24 -0
- package/dist/sdlc-runtime/reporting.js +68 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.d.ts +59 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.js +170 -0
- package/dist/sdlc-runtime/sdlc-skill.d.ts +44 -0
- package/dist/sdlc-runtime/sdlc-skill.js +148 -0
- package/dist/sdlc-runtime/steering-handler.d.ts +14 -0
- package/dist/sdlc-runtime/steering-handler.js +51 -0
- package/dist/sdlc-runtime/trace-recorder.d.ts +10 -0
- package/dist/sdlc-runtime/trace-recorder.js +7 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +12 -0
- package/dist/security/input-screen.d.ts +30 -0
- package/dist/security/input-screen.js +75 -0
- package/dist/security/tool-call-guard.d.ts +16 -0
- package/dist/security/tool-call-guard.js +163 -0
- package/dist/security/untrusted-content.d.ts +34 -0
- package/dist/security/untrusted-content.js +63 -0
- package/dist/skills/advanced-dfir/SKILL.md +85 -0
- package/dist/skills/advanced-pentest-lab/SKILL.md +88 -0
- package/dist/skills/analyst/SKILL.md +55 -0
- package/dist/skills/architect/SKILL.md +54 -0
- package/dist/skills/aws/SKILL.md +92 -0
- package/dist/skills/azure/SKILL.md +90 -0
- package/dist/skills/bylaw-context.d.ts +45 -0
- package/dist/skills/bylaw-context.js +61 -0
- package/dist/skills/bylaw-review.d.ts +20 -0
- package/dist/skills/bylaw-review.js +101 -0
- package/dist/skills/conversation/SKILL.md +29 -0
- package/dist/skills/design-patterns/SKILL.md +190 -0
- package/dist/skills/devops/SKILL.md +55 -0
- package/dist/skills/directives.d.ts +20 -0
- package/dist/skills/directives.js +313 -0
- package/dist/skills/docker/SKILL.md +76 -0
- package/dist/skills/docker-expert/SKILL.md +56 -0
- package/dist/skills/enterprise-systems-analyst/SKILL.md +83 -0
- package/dist/skills/enterprise-systems-analyst/references/crm.md +41 -0
- package/dist/skills/enterprise-systems-analyst/references/erp.md +40 -0
- package/dist/skills/enterprise-systems-analyst/references/financial-systems.md +35 -0
- package/dist/skills/enterprise-systems-analyst/references/hcm.md +38 -0
- package/dist/skills/enterprise-systems-analyst/references/mrp.md +37 -0
- package/dist/skills/enterprise-systems-analyst/references/scm.md +32 -0
- package/dist/skills/ethical-hacking-methodology/SKILL.md +119 -0
- package/dist/skills/externalSkillLoader.d.ts +25 -0
- package/dist/skills/externalSkillLoader.js +65 -0
- package/dist/skills/externalSkillRegistry.d.ts +33 -0
- package/dist/skills/externalSkillRegistry.js +94 -0
- package/dist/skills/filesystem-management/SKILL.md +61 -0
- package/dist/skills/first-run-seed.d.ts +26 -0
- package/dist/skills/first-run-seed.js +75 -0
- package/dist/skills/git-vcs/SKILL.md +84 -0
- package/dist/skills/kafka/SKILL.md +89 -0
- package/dist/skills/kubernetes/SKILL.md +93 -0
- package/dist/skills/kubernetes-expert/SKILL.md +54 -0
- package/dist/skills/openshift/SKILL.md +93 -0
- package/dist/skills/pentester/SKILL.md +59 -0
- package/dist/skills/performance-tester/SKILL.md +57 -0
- package/dist/skills/playwright-ui-tester/SKILL.md +67 -0
- package/dist/skills/programmer/SKILL.md +58 -0
- package/dist/skills/project-scaffolding/SKILL.md +133 -0
- package/dist/skills/qa-engineer/SKILL.md +91 -0
- package/dist/skills/rca/SKILL.md +58 -0
- package/dist/skills/redhat/SKILL.md +92 -0
- package/dist/skills/rosa/SKILL.md +80 -0
- package/dist/skills/scrum-framework/SKILL.md +74 -0
- package/dist/skills/scrum-framework/references/artifacts_and_quality.md +79 -0
- package/dist/skills/scrum-framework/references/roles_and_events.md +105 -0
- package/dist/skills/scrum-master-agent/SKILL.md +74 -0
- package/dist/skills/scrum-master-agent/references/behaviors.md +56 -0
- package/dist/skills/sdlc-skill.d.ts +1 -0
- package/dist/skills/sdlc-skill.js +4 -0
- package/dist/skills/secops/SKILL.md +52 -0
- package/dist/skills/secure-architect.bylaws.d.ts +68 -0
- package/dist/skills/secure-architect.bylaws.js +525 -0
- package/dist/skills/seed-skills.d.ts +48 -0
- package/dist/skills/seed-skills.js +213 -0
- package/dist/skills/skill-authoring/SKILL.md +123 -0
- package/dist/skills/skill-header.d.ts +51 -0
- package/dist/skills/skill-header.js +163 -0
- package/dist/skills/skill-path-guard.d.ts +41 -0
- package/dist/skills/skill-path-guard.js +95 -0
- package/dist/skills/skillRouter.d.ts +51 -0
- package/dist/skills/skillRouter.js +90 -0
- package/dist/skills/skills-root.d.ts +64 -0
- package/dist/skills/skills-root.js +168 -0
- package/dist/skills/software-architect/SKILL.md +77 -0
- package/dist/skills/software-engineer/SKILL.md +74 -0
- package/dist/skills/solid-principles/SKILL.md +94 -0
- package/dist/skills/task-planning/SKILL.md +115 -0
- package/dist/skills/tester/SKILL.md +54 -0
- package/dist/skills/ubuntu/SKILL.md +87 -0
- package/dist/skills/ui-ux-design/SKILL.md +114 -0
- package/dist/skills/workspace-context/SKILL.md +103 -0
- package/dist/sprint/controller.d.ts +37 -0
- package/dist/sprint/controller.js +220 -0
- package/dist/sprint/dag-builder.d.ts +14 -0
- package/dist/sprint/dag-builder.js +105 -0
- package/dist/sprint/reporter.d.ts +11 -0
- package/dist/sprint/reporter.js +97 -0
- package/dist/sprint/state-store.d.ts +10 -0
- package/dist/sprint/state-store.js +71 -0
- package/dist/sprint/types.d.ts +24 -0
- package/dist/sprint/types.js +4 -0
- package/dist/store/file-version-store.d.ts +24 -0
- package/dist/store/file-version-store.js +73 -0
- package/dist/store/task-store.d.ts +90 -0
- package/dist/store/task-store.js +166 -0
- package/dist/task-ingest/architect-detailer.d.ts +43 -0
- package/dist/task-ingest/architect-detailer.js +141 -0
- package/dist/task-ingest/detail-schema.d.ts +9 -0
- package/dist/task-ingest/detail-schema.js +195 -0
- package/dist/task-ingest/detailed-run.d.ts +36 -0
- package/dist/task-ingest/detailed-run.js +108 -0
- package/dist/task-ingest/extract-id.d.ts +9 -0
- package/dist/task-ingest/extract-id.js +24 -0
- package/dist/task-ingest/index.d.ts +23 -0
- package/dist/task-ingest/index.js +14 -0
- package/dist/task-ingest/ingest.d.ts +45 -0
- package/dist/task-ingest/ingest.js +163 -0
- package/dist/task-ingest/list-ingested.d.ts +13 -0
- package/dist/task-ingest/list-ingested.js +30 -0
- package/dist/task-ingest/pending-filter.d.ts +30 -0
- package/dist/task-ingest/pending-filter.js +59 -0
- package/dist/task-ingest/scanner.d.ts +56 -0
- package/dist/task-ingest/scanner.js +216 -0
- package/dist/task-ingest/source-reader.d.ts +25 -0
- package/dist/task-ingest/source-reader.js +93 -0
- package/dist/task-ingest/spec-builder.d.ts +34 -0
- package/dist/task-ingest/spec-builder.js +89 -0
- package/dist/task-ingest/wbs-extract.d.ts +50 -0
- package/dist/task-ingest/wbs-extract.js +226 -0
- package/dist/task-ingest/wbs-parser.d.ts +36 -0
- package/dist/task-ingest/wbs-parser.js +170 -0
- package/dist/tools/apiTestTool.d.ts +37 -0
- package/dist/tools/apiTestTool.js +128 -0
- package/dist/tools/ast-tools.d.ts +183 -0
- package/dist/tools/ast-tools.js +853 -0
- package/dist/tools/browse-tool.d.ts +105 -0
- package/dist/tools/browse-tool.js +394 -0
- package/dist/tools/credential.d.ts +17 -0
- package/dist/tools/credential.js +32 -0
- package/dist/tools/dependency-graph.d.ts +23 -0
- package/dist/tools/dependency-graph.js +164 -0
- package/dist/tools/edit-tool.d.ts +34 -0
- package/dist/tools/edit-tool.js +95 -0
- package/dist/tools/file-header-manager.d.ts +27 -0
- package/dist/tools/file-header-manager.js +153 -0
- package/dist/tools/fs-tools.d.ts +154 -0
- package/dist/tools/fs-tools.js +337 -0
- package/dist/tools/git-tools.d.ts +79 -0
- package/dist/tools/git-tools.js +348 -0
- package/dist/tools/github-tool.d.ts +48 -0
- package/dist/tools/github-tool.js +147 -0
- package/dist/tools/globTool.d.ts +20 -0
- package/dist/tools/globTool.js +34 -0
- package/dist/tools/grepTool.d.ts +19 -0
- package/dist/tools/grepTool.js +44 -0
- package/dist/tools/index-freshness.d.ts +58 -0
- package/dist/tools/index-freshness.js +190 -0
- package/dist/tools/index-glob.d.ts +48 -0
- package/dist/tools/index-glob.js +175 -0
- package/dist/tools/load-skill-tool.d.ts +15 -0
- package/dist/tools/load-skill-tool.js +114 -0
- package/dist/tools/patch-tools.d.ts +58 -0
- package/dist/tools/patch-tools.js +153 -0
- package/dist/tools/playwright-tool.d.ts +66 -0
- package/dist/tools/playwright-tool.js +121 -0
- package/dist/tools/project-detect.d.ts +20 -0
- package/dist/tools/project-detect.js +87 -0
- package/dist/tools/prompt-guidance.d.ts +22 -0
- package/dist/tools/prompt-guidance.js +23 -0
- package/dist/tools/scheduleTool.d.ts +19 -0
- package/dist/tools/scheduleTool.js +181 -0
- package/dist/tools/search-tools.d.ts +27 -0
- package/dist/tools/search-tools.js +78 -0
- package/dist/tools/sed-tool.d.ts +66 -0
- package/dist/tools/sed-tool.js +402 -0
- package/dist/tools/shell-result.d.ts +11 -0
- package/dist/tools/shell-result.js +1 -0
- package/dist/tools/shell-tool.d.ts +45 -0
- package/dist/tools/shell-tool.js +141 -0
- package/dist/tools/siteCrawlerTool.d.ts +32 -0
- package/dist/tools/siteCrawlerTool.js +205 -0
- package/dist/tools/tool-registry.d.ts +67 -0
- package/dist/tools/tool-registry.js +596 -0
- package/dist/tools/validate-tool.d.ts +23 -0
- package/dist/tools/validate-tool.js +201 -0
- package/dist/tools/workspace-index.d.ts +46 -0
- package/dist/tools/workspace-index.js +173 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +7 -0
- package/dist/updater/index.d.ts +14 -0
- package/dist/updater/index.js +120 -0
- package/dist/util/banner.d.ts +25 -0
- package/dist/util/banner.js +140 -0
- package/dist/util/command-suggester.d.ts +6 -0
- package/dist/util/command-suggester.js +25 -0
- package/dist/util/confirm-interrupt.d.ts +10 -0
- package/dist/util/confirm-interrupt.js +31 -0
- package/dist/util/continue-trigger.d.ts +11 -0
- package/dist/util/continue-trigger.js +20 -0
- package/dist/util/env-info.d.ts +18 -0
- package/dist/util/env-info.js +37 -0
- package/dist/util/file-access-log.d.ts +35 -0
- package/dist/util/file-access-log.js +140 -0
- package/dist/util/footer-manager.d.ts +124 -0
- package/dist/util/footer-manager.js +195 -0
- package/dist/util/footer-renderer.d.ts +67 -0
- package/dist/util/footer-renderer.js +319 -0
- package/dist/util/footer-state.d.ts +34 -0
- package/dist/util/footer-state.js +18 -0
- package/dist/util/footer-text.d.ts +51 -0
- package/dist/util/footer-text.js +74 -0
- package/dist/util/ids.d.ts +1 -0
- package/dist/util/ids.js +4 -0
- package/dist/util/levenshtein.d.ts +6 -0
- package/dist/util/levenshtein.js +34 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.js +41 -0
- package/dist/util/logs.d.ts +26 -0
- package/dist/util/logs.js +88 -0
- package/dist/util/output-handler.d.ts +89 -0
- package/dist/util/output-handler.js +193 -0
- package/dist/util/output-interceptor.d.ts +18 -0
- package/dist/util/output-interceptor.js +49 -0
- package/dist/util/output-paths.d.ts +43 -0
- package/dist/util/output-paths.js +121 -0
- package/dist/util/package-json.d.ts +6 -0
- package/dist/util/package-json.js +36 -0
- package/dist/util/process-kill.d.ts +27 -0
- package/dist/util/process-kill.js +148 -0
- package/dist/util/protocol-error-logger.d.ts +33 -0
- package/dist/util/protocol-error-logger.js +73 -0
- package/dist/util/redact.d.ts +25 -0
- package/dist/util/redact.js +103 -0
- package/dist/util/scroll-region-guard.d.ts +33 -0
- package/dist/util/scroll-region-guard.js +63 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.js +49 -0
- package/dist/util/terminal-output.d.ts +34 -0
- package/dist/util/terminal-output.js +1 -0
- package/dist/util/terminal-silent.d.ts +6 -0
- package/dist/util/terminal-silent.js +20 -0
- package/dist/util/terminal.d.ts +56 -0
- package/dist/util/terminal.js +124 -0
- package/dist/util/text-sanitize.d.ts +8 -0
- package/dist/util/text-sanitize.js +45 -0
- package/dist/util/thinking-spinner.d.ts +27 -0
- package/dist/util/thinking-spinner.js +189 -0
- package/dist/util/tool-error-logger.d.ts +33 -0
- package/dist/util/tool-error-logger.js +77 -0
- package/dist/util/tty.d.ts +13 -0
- package/dist/util/tty.js +16 -0
- package/dist/util/xcoder-paths.d.ts +56 -0
- package/dist/util/xcoder-paths.js +63 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/worker-agent/to-subtask-node.d.ts +35 -0
- package/dist/worker-agent/to-subtask-node.js +41 -0
- package/dist/worker-agent/worker-orchestrator.d.ts +104 -0
- package/dist/worker-agent/worker-orchestrator.js +201 -0
- package/package.json +38 -3
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// xcoder:version 8 | xcoder:task task-689517 | xcoder:updated 2026-11-01T08:21:37.747Z | xcoder:subtask code-st-828150
|
|
2
|
+
// Atomic per-user license state persistence.
|
|
3
|
+
//
|
|
4
|
+
// Canonical location: <homedir>/.xcoder/license/activation.json — per-user,
|
|
5
|
+
// next to config.json, NOT inside the workspace. A license belongs to the
|
|
6
|
+
// person/machine, so a fresh `git clone` must not silently consume another
|
|
7
|
+
// seat.
|
|
8
|
+
//
|
|
9
|
+
// SHARED STATE: the canonical file is the SAME file v3 and ronin read and
|
|
10
|
+
// write. One activation therefore covers every xcoder binary on the machine —
|
|
11
|
+
// the bug was never config.json (already shared) but the receipt living at
|
|
12
|
+
// different paths in different schemas. Reads walk the discovery list, writes
|
|
13
|
+
// land on the primary path in the superset shape all readers accept.
|
|
14
|
+
//
|
|
15
|
+
// Contains no secrets by construction: the serial key is never written to
|
|
16
|
+
// disk. `receipt` is a public, signed artifact whose only use is offline
|
|
17
|
+
// verification against the build-pinned key.
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { resolveStateCandidates } from "./state-paths.js";
|
|
23
|
+
import { decodeStateFile, recordFromClaims, writeStateFile, } from "./state-codec.js";
|
|
24
|
+
const DIR_MODE = 0o700;
|
|
25
|
+
const FILE_MODE = 0o600;
|
|
26
|
+
export function defaultLicenseDir() {
|
|
27
|
+
return path.join(os.homedir(), ".xcoder", "license");
|
|
28
|
+
}
|
|
29
|
+
function isObject(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function asString(value) {
|
|
33
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Every FILE an activation may be stored in, highest priority first.
|
|
37
|
+
*
|
|
38
|
+
* Candidates are files, not directories: $XCODER_LICENSE_FILE names an
|
|
39
|
+
* arbitrary file and the legacy host store is license.json. Collapsing them to
|
|
40
|
+
* their parent directory and re-appending a hard-coded activation.json silently
|
|
41
|
+
* ignored both, which is exactly the v1-asks-for-a-key bug that this sharing
|
|
42
|
+
* work exists to fix. Reads, writes and clears all use this one list.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveCandidateFiles(env = process.env) {
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const files = [];
|
|
47
|
+
for (const candidate of resolveStateCandidates(env)) {
|
|
48
|
+
const key = path.resolve(candidate.path).toLowerCase();
|
|
49
|
+
if (seen.has(key))
|
|
50
|
+
continue;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
files.push(candidate.path);
|
|
53
|
+
}
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Directories of the candidate files, highest priority first.
|
|
58
|
+
*
|
|
59
|
+
* Kept for callers that reason about the installation sidecar next to the
|
|
60
|
+
* activation; activation I/O must use resolveCandidateFiles().
|
|
61
|
+
*/
|
|
62
|
+
export function resolveCandidateDirs(env = process.env) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const dirs = [];
|
|
65
|
+
for (const file of resolveCandidateFiles(env)) {
|
|
66
|
+
const dir = path.dirname(file);
|
|
67
|
+
const key = path.resolve(dir).toLowerCase();
|
|
68
|
+
if (seen.has(key))
|
|
69
|
+
continue;
|
|
70
|
+
seen.add(key);
|
|
71
|
+
dirs.push(dir);
|
|
72
|
+
}
|
|
73
|
+
return dirs;
|
|
74
|
+
}
|
|
75
|
+
export function createLicenseStore(opts) {
|
|
76
|
+
const dir = opts?.dir ?? defaultLicenseDir();
|
|
77
|
+
// An explicitly injected dir is a fixture: stay inside it, so a test never
|
|
78
|
+
// reads (or is shadowed by) the developer's real activation on this machine.
|
|
79
|
+
const candidateFiles = opts?.dir ? [path.join(dir, "activation.json")] : resolveCandidateFiles();
|
|
80
|
+
// Writes follow the SAME priority list as reads (including the documented
|
|
81
|
+
// XCODER_LICENSE_FILE override). Writing to defaultLicenseDir() while reading
|
|
82
|
+
// from an override would store the activation where no subsequent read looks.
|
|
83
|
+
const activationFile = candidateFiles[0] ?? path.join(dir, "activation.json");
|
|
84
|
+
const writeDir = path.dirname(activationFile);
|
|
85
|
+
const installationFile = path.join(writeDir, "installation.json");
|
|
86
|
+
function ensureDir() {
|
|
87
|
+
fs.mkdirSync(writeDir, { recursive: true, mode: DIR_MODE });
|
|
88
|
+
}
|
|
89
|
+
/** The decoded shared record from the first candidate that has one. */
|
|
90
|
+
function readShared() {
|
|
91
|
+
for (const candidateFile of candidateFiles) {
|
|
92
|
+
const decoded = decodeStateFile(candidateFile);
|
|
93
|
+
if (decoded.ok)
|
|
94
|
+
return decoded.record;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/** Read raw JSON from one file. Symlinks are never followed. */
|
|
99
|
+
function readJson(file) {
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
const stat = fs.lstatSync(file);
|
|
103
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
104
|
+
return null;
|
|
105
|
+
raw = fs.readFileSync(file, "utf8");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null; // missing / unreadable → caller sees "nothing stored"
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(raw);
|
|
112
|
+
return isObject(parsed) ? parsed : null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Deliberately do NOT delete the file: the gate maps this to
|
|
116
|
+
// `corrupt-store` and fails closed, and a human can still inspect it.
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function readInstallation() {
|
|
121
|
+
const raw = readJson(installationFile);
|
|
122
|
+
if (raw) {
|
|
123
|
+
const installationId = asString(raw.installationId);
|
|
124
|
+
const createdAt = asString(raw.createdAt);
|
|
125
|
+
if (installationId)
|
|
126
|
+
return { installationId, createdAt: createdAt ?? new Date(0).toISOString() };
|
|
127
|
+
}
|
|
128
|
+
// A machine activated by another binary never wrote installation.json; the
|
|
129
|
+
// id lives in the shared activation instead. Without this fallback
|
|
130
|
+
// refresh() would refuse with "no installation id" on a machine that is
|
|
131
|
+
// plainly activated.
|
|
132
|
+
const shared = readShared();
|
|
133
|
+
if (!shared)
|
|
134
|
+
return null;
|
|
135
|
+
return { installationId: shared.installationId, createdAt: shared.savedAt };
|
|
136
|
+
}
|
|
137
|
+
function ensureInstallation() {
|
|
138
|
+
const existing = readInstallation();
|
|
139
|
+
if (existing) {
|
|
140
|
+
// Adopt-on-read: materialise the id in this dir so older builds keep
|
|
141
|
+
// working, but never replace an existing file.
|
|
142
|
+
if (!fs.existsSync(installationFile)) {
|
|
143
|
+
try {
|
|
144
|
+
ensureDir();
|
|
145
|
+
fs.writeFileSync(installationFile, `${JSON.stringify({ installationId: existing.installationId, createdAt: existing.createdAt }, null, 2)}\n`, { encoding: "utf8", mode: FILE_MODE, flag: "wx" });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* a concurrent writer won the race; its id is equivalent */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return existing;
|
|
152
|
+
}
|
|
153
|
+
const record = {
|
|
154
|
+
installationId: randomUUID(),
|
|
155
|
+
createdAt: new Date().toISOString(),
|
|
156
|
+
};
|
|
157
|
+
ensureDir();
|
|
158
|
+
try {
|
|
159
|
+
// `wx` fails if the file appeared between our read and this write, so a
|
|
160
|
+
// concurrent process can never clobber another installation's id.
|
|
161
|
+
fs.writeFileSync(installationFile, `${JSON.stringify(record, null, 2)}\n`, {
|
|
162
|
+
encoding: "utf8",
|
|
163
|
+
mode: FILE_MODE,
|
|
164
|
+
flag: "wx",
|
|
165
|
+
});
|
|
166
|
+
return record;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const code = err.code;
|
|
170
|
+
if (code === "EEXIST") {
|
|
171
|
+
const winner = readInstallation();
|
|
172
|
+
if (winner)
|
|
173
|
+
return winner;
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function readActivation() {
|
|
179
|
+
// First candidate wins; the file is the superset every binary shares, but
|
|
180
|
+
// this module keeps speaking the 7-field shape to its callers.
|
|
181
|
+
for (const candidateFile of candidateFiles) {
|
|
182
|
+
const decoded = decodeStateFile(candidateFile);
|
|
183
|
+
if (!decoded.ok)
|
|
184
|
+
continue;
|
|
185
|
+
const record = decoded.record;
|
|
186
|
+
return {
|
|
187
|
+
activationId: record.activationId,
|
|
188
|
+
licenseId: record.licenseId,
|
|
189
|
+
plan: record.plan,
|
|
190
|
+
receipt: record.receipt,
|
|
191
|
+
expiresAt: record.expiresAt,
|
|
192
|
+
refreshBy: record.refreshBy,
|
|
193
|
+
savedAt: record.savedAt,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function writeActivation(rec) {
|
|
199
|
+
// Replace, never append: exactly one activation per installation (AC3).
|
|
200
|
+
// Always the PRIMARY file, in the superset shape, so v3 and ronin read the
|
|
201
|
+
// very same activation without re-activating.
|
|
202
|
+
const installation = ensureInstallation();
|
|
203
|
+
const record = recordFromClaims(rec, {
|
|
204
|
+
installationId: installation.installationId,
|
|
205
|
+
receipt: rec.receipt,
|
|
206
|
+
savedAt: rec.savedAt,
|
|
207
|
+
});
|
|
208
|
+
writeStateFile(activationFile, record);
|
|
209
|
+
}
|
|
210
|
+
function clearActivation() {
|
|
211
|
+
// Clear every candidate so a released seat cannot be re-adopted from a
|
|
212
|
+
// lower-priority copy on the next read.
|
|
213
|
+
for (const candidateFile of candidateFiles) {
|
|
214
|
+
try {
|
|
215
|
+
fs.rmSync(candidateFile, { force: true });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
/* already absent — clearing is idempotent */
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { dir, readInstallation, ensureInstallation, readActivation, writeActivation, clearActivation };
|
|
223
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TrustAnchor {
|
|
2
|
+
/** SPKI PEM, Ed25519. */
|
|
3
|
+
readonly publicKeyPem: string;
|
|
4
|
+
/** True only while the labelled development key is pinned. */
|
|
5
|
+
readonly isDevelopment: boolean;
|
|
6
|
+
/** Set by scripts/fetch-public-key.mjs during release prep. */
|
|
7
|
+
readonly source: "release" | "development";
|
|
8
|
+
}
|
|
9
|
+
/** The `iss` claim the service signs with. */
|
|
10
|
+
export declare const LICENSE_ISSUER = "https://license.coderph.dev";
|
|
11
|
+
/** The `aud` claim; also the product ID sent at activation. */
|
|
12
|
+
export declare const LICENSE_PRODUCT_ID: "xcoder-sdlc-cli";
|
|
13
|
+
/**
|
|
14
|
+
* Default activation API base: exactly `<LICENSE_ISSUER>/activation/v1`.
|
|
15
|
+
*
|
|
16
|
+
* Asserted at import time: a mismatch means the binary would activate against
|
|
17
|
+
* a surface the Key System does not serve, which fails for every user at
|
|
18
|
+
* runtime and is invisible in a unit test that injects its own base URL. A
|
|
19
|
+
* throw here turns that into an immediate, unmissable build/test failure.
|
|
20
|
+
*/
|
|
21
|
+
export declare const LICENSE_API_BASE_URL: string;
|
|
22
|
+
/** The maximum offline window a receipt may claim: 7 days. */
|
|
23
|
+
export declare const RECEIPT_TTL_MS: number;
|
|
24
|
+
export declare const TRUST_ANCHOR: TrustAnchor;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// xcoder:version 2 | xcoder:task task-689517 | xcoder:updated 2026-11-01T08:22:00.385Z | xcoder:subtask code-st-828150
|
|
2
|
+
// The pinned license trust anchor — a build artifact, NOT runtime configuration.
|
|
3
|
+
//
|
|
4
|
+
// Invariants (each enforced by src/license/trust-anchor.test.ts):
|
|
5
|
+
// 1. This module contains no `process.env` reference, so neither the
|
|
6
|
+
// environment nor ~/.xcoder/config.json can replace the pinned key.
|
|
7
|
+
// 2. publicKeyPem parses via crypto.createPublicKey() as Ed25519 SPKI.
|
|
8
|
+
// 3. A release build (XCODER_RELEASE_BUILD=1) carrying the DEV-ONLY key
|
|
9
|
+
// fails the test suite — release prep must run scripts/fetch-public-key.mjs.
|
|
10
|
+
// 4. The dev key is labelled DEV-ONLY in its comment with source "development".
|
|
11
|
+
// 5. LICENSE_API_BASE_URL is exactly `<LICENSE_ISSUER>/activation/v1` — the
|
|
12
|
+
// serial-key gateway route the Key System is published behind. A mismatch
|
|
13
|
+
// throws at import time rather than silently activating against the wrong
|
|
14
|
+
// surface (v1 shipped `/api/v1`, so every valid key 404'd as bad-response).
|
|
15
|
+
//
|
|
16
|
+
// scripts/fetch-public-key.mjs rewrites everything between the BEGIN/END
|
|
17
|
+
// markers below. Never hand-edit the PEM inside those markers.
|
|
18
|
+
/** The `iss` claim the service signs with. */
|
|
19
|
+
export const LICENSE_ISSUER = "https://license.coderph.dev";
|
|
20
|
+
/** The `aud` claim; also the product ID sent at activation. */
|
|
21
|
+
export const LICENSE_PRODUCT_ID = "xcoder-sdlc-cli";
|
|
22
|
+
/**
|
|
23
|
+
* The path the serial-key gateway mounts the Key System's `/v1/` API under.
|
|
24
|
+
* Kept as a literal (not derived from the client) so the client module stays
|
|
25
|
+
* unchanged and the route is pinned in one auditable place.
|
|
26
|
+
*/
|
|
27
|
+
const GATEWAY_BASE_PATH = "/activation/v1";
|
|
28
|
+
/**
|
|
29
|
+
* The pinned activation API base — rewritten by hand, never at runtime.
|
|
30
|
+
*
|
|
31
|
+
* v1 shipped `<origin>/api/v1`, but the gateway publishes `POST
|
|
32
|
+
* <origin>/activation/v1/activations` (and /refresh, /release). Every valid
|
|
33
|
+
* serial key therefore hit a 404, surfaced to the user as `bad-response`: this
|
|
34
|
+
* is the reason v1 rejected a key v3 accepted. The value lives OUTSIDE the
|
|
35
|
+
* BEGIN/END marker block because scripts/fetch-public-key.mjs rewrites only
|
|
36
|
+
* the PEM and the key source, and a release build must not drop the route.
|
|
37
|
+
*/
|
|
38
|
+
const PINNED_API_BASE_URL = "https://license.coderph.dev/activation/v1";
|
|
39
|
+
/** Strip trailing slashes so `<origin>/` and `<origin>` compare equal. */
|
|
40
|
+
function stripTrailingSlashes(value) {
|
|
41
|
+
return value.replace(/\/+$/, "");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Default activation API base: exactly `<LICENSE_ISSUER>/activation/v1`.
|
|
45
|
+
*
|
|
46
|
+
* Asserted at import time: a mismatch means the binary would activate against
|
|
47
|
+
* a surface the Key System does not serve, which fails for every user at
|
|
48
|
+
* runtime and is invisible in a unit test that injects its own base URL. A
|
|
49
|
+
* throw here turns that into an immediate, unmissable build/test failure.
|
|
50
|
+
*/
|
|
51
|
+
export const LICENSE_API_BASE_URL = (() => {
|
|
52
|
+
const expected = `${LICENSE_ISSUER}${GATEWAY_BASE_PATH}`;
|
|
53
|
+
const pinned = stripTrailingSlashes(PINNED_API_BASE_URL);
|
|
54
|
+
if (pinned !== expected) {
|
|
55
|
+
throw new Error(`license trust anchor: PINNED_API_BASE_URL must be ${expected} (got ${pinned}) — re-pin it so it is the issuer origin plus ${GATEWAY_BASE_PATH}`);
|
|
56
|
+
}
|
|
57
|
+
return expected;
|
|
58
|
+
})();
|
|
59
|
+
/** The maximum offline window a receipt may claim: 7 days. */
|
|
60
|
+
export const RECEIPT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
61
|
+
// BEGIN XCODER LICENSE PUBLIC KEY (managed by scripts/fetch-public-key.mjs)
|
|
62
|
+
// DEV-ONLY — this Ed25519 key is published with the source tree and MUST NOT
|
|
63
|
+
// be trusted in a release. Release prep replaces it and flips `source`.
|
|
64
|
+
const PINNED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
65
|
+
MCowBQYDK2VwAyEABJbTsj5H+GLJJEpA0X5m2+UofSkDAg/y7Z0ahE6c05Y=
|
|
66
|
+
-----END PUBLIC KEY-----
|
|
67
|
+
`;
|
|
68
|
+
const PINNED_KEY_SOURCE = "development";
|
|
69
|
+
// END XCODER LICENSE PUBLIC KEY
|
|
70
|
+
export const TRUST_ANCHOR = Object.freeze({
|
|
71
|
+
publicKeyPem: PINNED_PUBLIC_KEY_PEM,
|
|
72
|
+
isDevelopment: PINNED_KEY_SOURCE === "development",
|
|
73
|
+
source: PINNED_KEY_SOURCE,
|
|
74
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The signed claim set embedded in a license receipt (segment 0). */
|
|
2
|
+
export interface LicenseClaims {
|
|
3
|
+
iss: string;
|
|
4
|
+
aud: string;
|
|
5
|
+
licenseId: string;
|
|
6
|
+
activationId: string;
|
|
7
|
+
plan: string;
|
|
8
|
+
/** ISO-8601, e.g. "2026-11-01T10:00:00.000Z" */
|
|
9
|
+
issuedAt: string;
|
|
10
|
+
/** Subscription expiry. */
|
|
11
|
+
expiresAt: string;
|
|
12
|
+
/** <= min(expiresAt, issuedAt + 7d) — the offline grace boundary. */
|
|
13
|
+
refreshBy: string;
|
|
14
|
+
}
|
|
15
|
+
/** Distinct, user-actionable denial codes. */
|
|
16
|
+
export type LicenseDenialReason = "no-activation" | "corrupt-store" | "malformed-receipt" | "bad-signature" | "untrusted-key" | "wrong-issuer" | "wrong-audience" | "expired" | "not-yet-valid" | "refresh-overdue";
|
|
17
|
+
export type VerifyResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
claims: LicenseClaims;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: LicenseDenialReason;
|
|
23
|
+
detail?: string;
|
|
24
|
+
};
|
|
25
|
+
export type LicenseGateResult = {
|
|
26
|
+
allowed: true;
|
|
27
|
+
claims: LicenseClaims;
|
|
28
|
+
} | {
|
|
29
|
+
allowed: false;
|
|
30
|
+
reason: LicenseDenialReason;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
/** Human-readable, non-leaking explanation for each denial code. */
|
|
34
|
+
export declare const DENIAL_DETAIL: Record<LicenseDenialReason, string>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// xcoder:version 1 | xcoder:task task-1d9460 | xcoder:updated 2026-10-25T17:01:22.522Z | xcoder:subtask code-st-4b0403
|
|
2
|
+
// Client licensing — shared type layer.
|
|
3
|
+
//
|
|
4
|
+
// Type-only by design: no I/O, no `node:fs`, no `process`. Every other
|
|
5
|
+
// module in src/license/ imports its vocabulary from here so the denial
|
|
6
|
+
// codes and the gate result cannot drift apart.
|
|
7
|
+
/** Human-readable, non-leaking explanation for each denial code. */
|
|
8
|
+
export const DENIAL_DETAIL = {
|
|
9
|
+
"no-activation": "no activation found on this machine",
|
|
10
|
+
"corrupt-store": "stored license state is unreadable",
|
|
11
|
+
"malformed-receipt": "the stored receipt is malformed",
|
|
12
|
+
"bad-signature": "the receipt signature does not verify",
|
|
13
|
+
"untrusted-key": "the receipt was signed by a key that is not pinned",
|
|
14
|
+
"wrong-issuer": "the receipt was issued by an unexpected issuer",
|
|
15
|
+
"wrong-audience": "the receipt is not for this product",
|
|
16
|
+
expired: "the subscription has expired",
|
|
17
|
+
"not-yet-valid": "the receipt is not valid yet",
|
|
18
|
+
"refresh-overdue": "the offline window has elapsed — refresh required",
|
|
19
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
export declare const requestScope: AsyncLocalStorage<{
|
|
3
|
+
budget?: RequestBudget;
|
|
4
|
+
recordUsage?: (entry: Record<string, unknown>) => void;
|
|
5
|
+
/** In-memory mirror prevents a successful call from disappearing if ledger I/O fails. */
|
|
6
|
+
usageEvents?: Record<string, unknown>[];
|
|
7
|
+
}>;
|
|
8
|
+
/** Shared synchronous reservations prevent concurrent requests overspending a cap.
|
|
9
|
+
* Unknown/time-out usage retains its reservation until explicit reconciliation.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RequestBudget {
|
|
12
|
+
readonly capUsd: number;
|
|
13
|
+
readonly inputPerMillion: number;
|
|
14
|
+
readonly outputPerMillion: number;
|
|
15
|
+
private committed;
|
|
16
|
+
private reserved;
|
|
17
|
+
constructor(capUsd: number, inputPerMillion: number, outputPerMillion: number);
|
|
18
|
+
reserve(input: number, output: number): (usage?: {
|
|
19
|
+
inputTokens: number;
|
|
20
|
+
outputTokens: number;
|
|
21
|
+
}) => void;
|
|
22
|
+
restore(jsonl: string): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
export const requestScope = new AsyncLocalStorage();
|
|
3
|
+
/** Shared synchronous reservations prevent concurrent requests overspending a cap.
|
|
4
|
+
* Unknown/time-out usage retains its reservation until explicit reconciliation.
|
|
5
|
+
*/
|
|
6
|
+
export class RequestBudget {
|
|
7
|
+
capUsd;
|
|
8
|
+
inputPerMillion;
|
|
9
|
+
outputPerMillion;
|
|
10
|
+
committed = 0;
|
|
11
|
+
reserved = 0;
|
|
12
|
+
constructor(capUsd, inputPerMillion, outputPerMillion) {
|
|
13
|
+
this.capUsd = capUsd;
|
|
14
|
+
this.inputPerMillion = inputPerMillion;
|
|
15
|
+
this.outputPerMillion = outputPerMillion;
|
|
16
|
+
if (![capUsd, inputPerMillion, outputPerMillion].every(n => Number.isFinite(n) && n >= 0)) {
|
|
17
|
+
throw new Error("A dollar budget requires non-negative cap and explicit input/output prices");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
reserve(input, output) {
|
|
21
|
+
if (![input, output].every(n => Number.isFinite(n) && n >= 0))
|
|
22
|
+
throw new Error("Invalid token reservation");
|
|
23
|
+
const amount = (input * this.inputPerMillion + output * this.outputPerMillion) / 1e6;
|
|
24
|
+
if (this.committed + this.reserved + amount > this.capUsd)
|
|
25
|
+
throw new Error("Request refused: task dollar budget exhausted");
|
|
26
|
+
this.reserved += amount;
|
|
27
|
+
let settled = false;
|
|
28
|
+
return usage => {
|
|
29
|
+
if (settled)
|
|
30
|
+
throw new Error("Budget reservation already settled");
|
|
31
|
+
settled = true;
|
|
32
|
+
if (!usage)
|
|
33
|
+
return; // Unknown charge stays reserved.
|
|
34
|
+
this.reserved -= amount;
|
|
35
|
+
this.committed += (usage.inputTokens * this.inputPerMillion + usage.outputTokens * this.outputPerMillion) / 1e6;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
restore(jsonl) {
|
|
39
|
+
const requests = new Map();
|
|
40
|
+
for (const line of jsonl.split("\n").filter(line => line.trim())) {
|
|
41
|
+
const entry = JSON.parse(line);
|
|
42
|
+
if (typeof entry.requestId !== "string")
|
|
43
|
+
throw new Error("Invalid usage ledger");
|
|
44
|
+
requests.set(entry.requestId, { ...requests.get(entry.requestId), ...entry });
|
|
45
|
+
}
|
|
46
|
+
for (const entry of requests.values()) {
|
|
47
|
+
if (entry.inputPrice !== this.inputPerMillion || entry.outputPrice !== this.outputPerMillion) {
|
|
48
|
+
throw new Error("Budget pricing changed or is missing from the ledger; reconcile prior charges before resume");
|
|
49
|
+
}
|
|
50
|
+
if (entry.status === "completed" && entry.usageKnown === true && entry.usage) {
|
|
51
|
+
this.reserve(entry.usage.inputTokens, entry.usage.outputTokens)(entry.usage);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.reserve(entry.estimatedInputTokens ?? NaN, entry.maxOutputTokens ?? NaN)();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type LlmProvider = "deepseek" | "openai-compatible" | "anthropic";
|
|
2
|
+
export interface LlmMessage {
|
|
3
|
+
role: "user" | "assistant";
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LlmClientOptions {
|
|
7
|
+
model?: string;
|
|
8
|
+
maxTokens?: number;
|
|
9
|
+
temperature?: number;
|
|
10
|
+
provider?: LlmProvider;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
apiKeyEnv?: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LlmCompletion {
|
|
17
|
+
text: string;
|
|
18
|
+
usage: {
|
|
19
|
+
inputTokens: number;
|
|
20
|
+
outputTokens: number;
|
|
21
|
+
};
|
|
22
|
+
/** The API's stop reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | 'length' | 'stop' | 'tool_calls' | null. */
|
|
23
|
+
stopReason: string | null;
|
|
24
|
+
/** The model name used for this completion. */
|
|
25
|
+
model: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One call per ReAct turn: system prompt + running transcript in, raw text
|
|
29
|
+
* + token usage out. Deliberately thin — the contract parsing lives in
|
|
30
|
+
* `protocol/react-contract.ts`, not here, so this file never needs to know
|
|
31
|
+
* what a valid response looks like. Callers are responsible for their own
|
|
32
|
+
* try/catch around `complete()` — a transient API error here must not be
|
|
33
|
+
* allowed to propagate all the way up and take the Master Orchestrator
|
|
34
|
+
* down with it (see `agents/sub-agent.ts`).
|
|
35
|
+
*
|
|
36
|
+
* Transport selection:
|
|
37
|
+
* - `"deepseek"` (default) — DeepSeek's Anthropic-compatible endpoint via
|
|
38
|
+
* `@anthropic-ai/sdk`; zero-config behavior is unchanged.
|
|
39
|
+
* - `"openai-compatible"` — any OpenAI `chat/completions` provider via the
|
|
40
|
+
* `openai` SDK (OpenAI, Groq, OpenRouter, Ollama, LM Studio, vLLM, ...).
|
|
41
|
+
* - `"anthropic"` — native Claude via `@anthropic-ai/sdk`.
|
|
42
|
+
*/
|
|
43
|
+
export declare class LlmClient {
|
|
44
|
+
private anthropic;
|
|
45
|
+
private openai;
|
|
46
|
+
private provider;
|
|
47
|
+
private model;
|
|
48
|
+
private maxTokens;
|
|
49
|
+
private temperature;
|
|
50
|
+
private endpoint;
|
|
51
|
+
constructor(opts?: LlmClientOptions);
|
|
52
|
+
complete(systemPrompt: string, transcript: LlmMessage[]): Promise<LlmCompletion>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// xcoder:version 1 | xcoder:task task-c49600 | xcoder:updated 2026-08-13T13:50:08.142Z | xcoder:subtask code-st-6d79ef
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import OpenAI from "openai";
|
|
4
|
+
/**
|
|
5
|
+
* One call per ReAct turn: system prompt + running transcript in, raw text
|
|
6
|
+
* + token usage out. Deliberately thin — the contract parsing lives in
|
|
7
|
+
* `protocol/react-contract.ts`, not here, so this file never needs to know
|
|
8
|
+
* what a valid response looks like. Callers are responsible for their own
|
|
9
|
+
* try/catch around `complete()` — a transient API error here must not be
|
|
10
|
+
* allowed to propagate all the way up and take the Master Orchestrator
|
|
11
|
+
* down with it (see `agents/sub-agent.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Transport selection:
|
|
14
|
+
* - `"deepseek"` (default) — DeepSeek's Anthropic-compatible endpoint via
|
|
15
|
+
* `@anthropic-ai/sdk`; zero-config behavior is unchanged.
|
|
16
|
+
* - `"openai-compatible"` — any OpenAI `chat/completions` provider via the
|
|
17
|
+
* `openai` SDK (OpenAI, Groq, OpenRouter, Ollama, LM Studio, vLLM, ...).
|
|
18
|
+
* - `"anthropic"` — native Claude via `@anthropic-ai/sdk`.
|
|
19
|
+
*/
|
|
20
|
+
export class LlmClient {
|
|
21
|
+
anthropic = null;
|
|
22
|
+
openai = null;
|
|
23
|
+
provider;
|
|
24
|
+
model;
|
|
25
|
+
maxTokens;
|
|
26
|
+
temperature;
|
|
27
|
+
endpoint;
|
|
28
|
+
constructor(opts = {}) {
|
|
29
|
+
this.provider = opts.provider ?? "deepseek";
|
|
30
|
+
this.model = opts.model ?? process.env.XCODER_MODEL ?? "deepseek-chat";
|
|
31
|
+
this.maxTokens = opts.maxTokens ?? 16384;
|
|
32
|
+
this.temperature = opts.temperature;
|
|
33
|
+
if (this.provider === "openai-compatible") {
|
|
34
|
+
let apiKey = opts.apiKey;
|
|
35
|
+
if (!apiKey && opts.apiKeyEnv) {
|
|
36
|
+
apiKey = process.env[opts.apiKeyEnv];
|
|
37
|
+
}
|
|
38
|
+
// Keyless local providers (Ollama, LM Studio) accept any placeholder.
|
|
39
|
+
if (!apiKey)
|
|
40
|
+
apiKey = "ollama";
|
|
41
|
+
this.openai = new OpenAI({
|
|
42
|
+
apiKey,
|
|
43
|
+
baseURL: opts.baseUrl,
|
|
44
|
+
});
|
|
45
|
+
this.endpoint = opts.endpoint ?? "/chat/completions";
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// `deepseek` (the zero-config default) and native `anthropic` both speak
|
|
49
|
+
// the Anthropic Messages protocol.
|
|
50
|
+
let apiKey = opts.apiKey;
|
|
51
|
+
if (!apiKey && opts.apiKeyEnv) {
|
|
52
|
+
apiKey = process.env[opts.apiKeyEnv];
|
|
53
|
+
}
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
if (this.provider === "anthropic") {
|
|
56
|
+
apiKey = process.env.ANTHROPIC_API_KEY ?? process.env.DEEPSEEK_API_KEY;
|
|
57
|
+
if (!apiKey) {
|
|
58
|
+
throw new Error("ANTHROPIC_API_KEY (or DEEPSEEK_API_KEY) is not set. Export it before running xcoder, e.g.\n" +
|
|
59
|
+
' export ANTHROPIC_API_KEY="sk-ant-..."');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
apiKey = process.env.DEEPSEEK_API_KEY;
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
throw new Error("DEEPSEEK_API_KEY is not set. Export it before running xcoder, e.g.\n" +
|
|
66
|
+
' export DEEPSEEK_API_KEY="sk-..."');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const baseURL = opts.baseUrl ??
|
|
71
|
+
(this.provider === "anthropic"
|
|
72
|
+
? "https://api.anthropic.com"
|
|
73
|
+
: "https://api.deepseek.com/anthropic");
|
|
74
|
+
this.anthropic = new Anthropic({ apiKey, baseURL });
|
|
75
|
+
}
|
|
76
|
+
async complete(systemPrompt, transcript) {
|
|
77
|
+
if (this.provider === "openai-compatible" && this.openai) {
|
|
78
|
+
const response = await this.openai.chat.completions.create({
|
|
79
|
+
model: this.model,
|
|
80
|
+
max_tokens: this.maxTokens,
|
|
81
|
+
...(this.temperature !== undefined ? { temperature: this.temperature } : {}),
|
|
82
|
+
messages: [
|
|
83
|
+
{ role: "system", content: systemPrompt },
|
|
84
|
+
...transcript.map((m) => ({ role: m.role, content: m.content })),
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
const choice = response.choices?.[0];
|
|
88
|
+
return {
|
|
89
|
+
text: choice?.message?.content ?? "",
|
|
90
|
+
usage: {
|
|
91
|
+
inputTokens: response.usage?.prompt_tokens ?? 0,
|
|
92
|
+
outputTokens: response.usage?.completion_tokens ?? 0,
|
|
93
|
+
},
|
|
94
|
+
stopReason: choice?.finish_reason ?? null,
|
|
95
|
+
model: this.model,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (!this.anthropic) {
|
|
99
|
+
throw new Error(`LLM transport not initialized for provider "${this.provider}"`);
|
|
100
|
+
}
|
|
101
|
+
const response = await this.anthropic.messages.create({
|
|
102
|
+
model: this.model,
|
|
103
|
+
max_tokens: this.maxTokens,
|
|
104
|
+
...(this.temperature !== undefined ? { temperature: this.temperature } : {}),
|
|
105
|
+
system: systemPrompt,
|
|
106
|
+
messages: transcript.map((m) => ({ role: m.role, content: m.content })),
|
|
107
|
+
});
|
|
108
|
+
const text = response.content
|
|
109
|
+
.map((block) => (block.type === "text" ? block.text : ""))
|
|
110
|
+
.join("\n")
|
|
111
|
+
.trim();
|
|
112
|
+
return {
|
|
113
|
+
text,
|
|
114
|
+
usage: {
|
|
115
|
+
inputTokens: response.usage?.input_tokens ?? 0,
|
|
116
|
+
outputTokens: response.usage?.output_tokens ?? 0,
|
|
117
|
+
},
|
|
118
|
+
stopReason: response.stop_reason ?? null,
|
|
119
|
+
model: this.model,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LlmClient } from "./client.js";
|
|
2
|
+
import { type AgentRole } from "../config/loadConfig.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sole production constructor path for LLM clients. Resolves the effective
|
|
5
|
+
* per-role configuration from `.xcoder/config/llm.yaml` (or built-in
|
|
6
|
+
* DeepSeek defaults) and returns a transport ready for that role.
|
|
7
|
+
*
|
|
8
|
+
* Precedence in `resolveLlmConfig`: role block → `default` block → built-in
|
|
9
|
+
* DeepSeek; `XCODER_MODEL` applies to `model` only.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createLlmClient(role: AgentRole, overrides?: {
|
|
12
|
+
model?: string;
|
|
13
|
+
maxTokens?: number;
|
|
14
|
+
}): LlmClient;
|