carthagent 0.0.0-stage → 1.0.7
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 +219 -2
- package/bin/ctg.mjs +736 -0
- package/docs/budgets.md +72 -0
- package/docs/compaction.md +34 -0
- package/docs/context-efficiency-plan.md +24 -0
- package/docs/context-efficiency-review.md +27 -0
- package/docs/context-efficiency.md +49 -0
- package/docs/deepseek-jev-benchmark-plan.md +70 -0
- package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
- package/docs/deepseek-jev-benchmark-restart.md +9 -0
- package/docs/deepseek-jev-benchmark-results.md +77 -0
- package/docs/deepseek-jev-benchmark-running.md +60 -0
- package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
- package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
- package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
- package/docs/deepseek-jev-http400-diagnosis.md +45 -0
- package/docs/delivery-review-evidence.md +53 -0
- package/docs/delivery-timeout-repair-plan.md +51 -0
- package/docs/evaluation-readiness.md +31 -0
- package/docs/evaluation.md +70 -0
- package/docs/extending.md +354 -0
- package/docs/glm-pilot-execution.md +9 -0
- package/docs/glm-pilot-plan.md +15 -0
- package/docs/glm-pilot-results.md +71 -0
- package/docs/graph-context-optimization.md +15 -0
- package/docs/jev-repository-pilot-plan.md +25 -0
- package/docs/jev-repository-pilot-protocol.md +39 -0
- package/docs/jev-repository-pilot-restart.md +7 -0
- package/docs/jev-repository-pilot-results.md +79 -0
- package/docs/jev.md +32 -0
- package/docs/model-registry.md +25 -0
- package/docs/native-compatibility.md +117 -0
- package/docs/opencode-archive.md +9 -0
- package/docs/opencode-migration.md +115 -0
- package/docs/opencode-runtime.md +94 -0
- package/docs/opencode-workspace.md +31 -0
- package/docs/owned-runtime-workflow-registration.md +17 -0
- package/docs/plan-advisor-evaluation.md +38 -0
- package/docs/planning-startup-fix-plan.md +23 -0
- package/docs/planning-startup-fix-review.md +24 -0
- package/docs/planning.md +118 -0
- package/docs/production-merge-review.md +23 -0
- package/docs/quality.md +35 -0
- package/docs/repository-benchmark-design.md +111 -0
- package/docs/review-assurance.md +9 -0
- package/docs/review-experiment-results.md +59 -0
- package/docs/tested-planning-roadmap.md +58 -0
- package/docs/tested-planning.md +58 -0
- package/docs/tui-pickers.md +9 -0
- package/docs/webdriver.md +83 -0
- package/docs/workflow-modes.md +26 -0
- package/docs/workflow.d2 +17 -0
- package/docs/workflow.svg +149 -0
- package/docs/workspace-coordination.md +9 -0
- package/evaluate.mjs +249 -0
- package/extensions/codegraph.ts +36 -0
- package/extensions/delivery.ts +819 -0
- package/extensions/experiential.ts +21 -0
- package/extensions/subscriptions.ts +21 -0
- package/guidance/profiles/authenticated-web.json +24 -0
- package/guidance/profiles/data-visualization.json +25 -0
- package/guidance/profiles/database-change.json +24 -0
- package/guidance/profiles/external-api.json +24 -0
- package/guidance/profiles/transactional-data.json +25 -0
- package/guidance/profiles/web-application.json +26 -0
- package/lib/acceptance-review.mjs +4 -0
- package/lib/browser-bin.mjs +160 -0
- package/lib/browser-errors.mjs +54 -0
- package/lib/browser.mjs +219 -0
- package/lib/budget.mjs +37 -0
- package/lib/cdp.mjs +221 -0
- package/lib/cloud/client.mjs +275 -0
- package/lib/cloud/control-plane.mjs +350 -0
- package/lib/cloud/gateway.mjs +380 -0
- package/lib/cloud/http.mjs +135 -0
- package/lib/cloud/ledger.mjs +215 -0
- package/lib/codegraph-query.mjs +60 -0
- package/lib/codegraph.mjs +378 -0
- package/lib/context-archive.mjs +18 -0
- package/lib/delivery-context.mjs +132 -0
- package/lib/delivery-guidance.mjs +9 -0
- package/lib/delivery-review.mjs +94 -0
- package/lib/delivery.mjs +610 -0
- package/lib/engine.mjs +137 -0
- package/lib/guidance.mjs +71 -0
- package/lib/jev-advisor.mjs +47 -0
- package/lib/opencode/archive.mjs +122 -0
- package/lib/opencode/compatibility.mjs +19 -0
- package/lib/opencode/credentials.mjs +205 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
- package/lib/opencode/evaluation.mjs +242 -0
- package/lib/opencode/file-lock.mjs +86 -0
- package/lib/opencode/headless.mjs +200 -0
- package/lib/opencode/review.mjs +213 -0
- package/lib/opencode/runtime.mjs +246 -0
- package/lib/opencode/service.mjs +249 -0
- package/lib/opencode/session-options.mjs +60 -0
- package/lib/opencode/workspace.mjs +133 -0
- package/lib/openrouter-jev.mjs +37 -0
- package/lib/outcome-contract.mjs +21 -0
- package/lib/plan-advisor.mjs +38 -0
- package/lib/planning-access.mjs +7 -0
- package/lib/planning-advice.mjs +28 -0
- package/lib/planning-discovery.mjs +14 -0
- package/lib/planning-guide.mjs +34 -0
- package/lib/planning-view.mjs +57 -0
- package/lib/planning.mjs +155 -0
- package/lib/probe-diagnostics.mjs +13 -0
- package/lib/providers/experiential.mjs +292 -0
- package/lib/providers/model-catalog.json +700 -0
- package/lib/providers/model-catalog.mjs +7 -0
- package/lib/providers/names.mjs +13 -0
- package/lib/providers/subscription-models.mjs +483 -0
- package/lib/reports.mjs +80 -0
- package/lib/review-state.mjs +49 -0
- package/lib/review.mjs +114 -0
- package/lib/scenarios.mjs +99 -0
- package/lib/ship-auth.mjs +44 -0
- package/lib/ship-jev.mjs +59 -0
- package/lib/ship-telemetry.mjs +183 -0
- package/lib/tui/ansi.mjs +255 -0
- package/lib/tui/app.mjs +2445 -0
- package/lib/tui/auth.mjs +249 -0
- package/lib/tui/carthage.mjs +109 -0
- package/lib/tui/clipboard.mjs +122 -0
- package/lib/tui/d2.mjs +431 -0
- package/lib/tui/feed.mjs +467 -0
- package/lib/tui/framing.mjs +80 -0
- package/lib/tui/glyphs.mjs +172 -0
- package/lib/tui/hash-worker.mjs +11 -0
- package/lib/tui/mock-engine.mjs +222 -0
- package/lib/tui/models.mjs +73 -0
- package/lib/tui/picker.mjs +18 -0
- package/lib/tui/theme.mjs +663 -0
- package/lib/tui/title.mjs +7 -0
- package/lib/update-channel.mjs +62 -0
- package/lib/update.mjs +230 -0
- package/lib/webdriver.mjs +82 -0
- package/lib/workflow.mjs +88 -0
- package/lib/workspace.mjs +39 -0
- package/metadata.json +8 -0
- package/package.json +94 -4
- package/plugins/carthagent/index.ts +3 -0
- package/plugins/carthagent/package.json +28 -0
- package/plugins/carthagent/src/archive-rpc.ts +46 -0
- package/plugins/carthagent/src/bootstrap.ts +38 -0
- package/plugins/carthagent/src/index.ts +62 -0
- package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
- package/plugins/carthagent/src/rpc.ts +76 -0
- package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
- package/plugins/carthagent/src/server/budgets.ts +21 -0
- package/plugins/carthagent/src/server/codegraph.ts +46 -0
- package/plugins/carthagent/src/server/delivery.ts +488 -0
- package/plugins/carthagent/src/server/device-auth.ts +91 -0
- package/plugins/carthagent/src/server/native-delivery.ts +26 -0
- package/plugins/carthagent/src/server/orchestration.ts +56 -0
- package/plugins/carthagent/src/server/policy.ts +24 -0
- package/plugins/carthagent/src/server/providers.ts +424 -0
- package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
- package/plugins/carthagent/src/server/session-controls.ts +82 -0
- package/plugins/carthagent/src/server/telemetry.ts +92 -0
- package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
- package/plugins/carthagent/src/session-rpc.ts +31 -0
- package/plugins/carthagent/src/testing.ts +85 -0
- package/plugins/carthagent/src/workspace-rpc.ts +133 -0
- package/plugins/carthagent/test-plugin/index.ts +4 -0
- package/plugins/carthagent/test-plugin/package.json +1 -0
- package/plugins/carthagent/tsconfig.json +19 -0
- package/plugins/carthagent/tui.ts +1 -0
- package/prompts/delivery.md +7 -0
- package/public/index.html +983 -0
- package/scenarios/cli/grade_cli.py +69 -0
- package/scenarios/cli/scenario.json +21 -0
- package/scenarios/game/baseline/minecraft.py +193 -0
- package/scenarios/game/context.md +1 -0
- package/scenarios/game/grade_minecraft_clone.py +603 -0
- package/scenarios/game/requirements.txt +2 -0
- package/scenarios/game/scenario.json +33 -0
- package/server.mjs +341 -0
- package/skills/game-development/SKILL.md +36 -0
- package/skills/game-development/assets/ursina_starter.py +98 -0
- package/skills/game-development/scripts/verify_ursina.py +80 -0
- package/skills/software-delivery/SKILL.md +50 -0
- package/skills/typescript-delivery/SKILL.md +69 -0
- package/tests/acceptance-review.test.mjs +13 -0
- package/tests/auth.test.mjs +234 -0
- package/tests/bounded-summary.test.mjs +68 -0
- package/tests/browser-bin.test.mjs +91 -0
- package/tests/browser.test.mjs +96 -0
- package/tests/budget-engine.test.mjs +110 -0
- package/tests/budget.test.mjs +23 -0
- package/tests/cli.test.mjs +245 -0
- package/tests/clipboard.test.mjs +79 -0
- package/tests/cloud-client.test.mjs +129 -0
- package/tests/cloud-control-plane.test.mjs +144 -0
- package/tests/cloud-gateway.test.mjs +147 -0
- package/tests/cloud-http.test.mjs +102 -0
- package/tests/cloud-ledger.test.mjs +75 -0
- package/tests/codegraph-query.test.mjs +66 -0
- package/tests/codegraph.test.mjs +142 -0
- package/tests/delivery-context.test.mjs +66 -0
- package/tests/delivery-review.test.mjs +139 -0
- package/tests/delivery.test.mjs +343 -0
- package/tests/engine-api.mjs +24 -0
- package/tests/experiential-extension.test.mjs +37 -0
- package/tests/experiential.test.mjs +300 -0
- package/tests/extension.test.mjs +921 -0
- package/tests/fixtures/archive-plugin/index.ts +10 -0
- package/tests/guidance.test.mjs +78 -0
- package/tests/model-registry.test.mjs +68 -0
- package/tests/models.test.mjs +70 -0
- package/tests/openai-idempotency.test.mjs +99 -0
- package/tests/opencode-archive-rpc.test.mjs +82 -0
- package/tests/opencode-delivery.test.mjs +231 -0
- package/tests/opencode-providers.test.mjs +414 -0
- package/tests/opencode-runtime.test.mjs +455 -0
- package/tests/opencode-session-controls.test.mjs +115 -0
- package/tests/opencode-session-options.test.mjs +26 -0
- package/tests/opencode-workspace.test.mjs +339 -0
- package/tests/outcome-contract.test.mjs +17 -0
- package/tests/picker.test.mjs +34 -0
- package/tests/plan-advisor.test.mjs +88 -0
- package/tests/plan-review-loop.test.mjs +53 -0
- package/tests/planning-advice.test.mjs +27 -0
- package/tests/planning-contract.test.mjs +65 -0
- package/tests/planning-startup.test.mjs +38 -0
- package/tests/planning-view.test.mjs +33 -0
- package/tests/planning.test.mjs +223 -0
- package/tests/probe-diagnostics.test.mjs +21 -0
- package/tests/pull-request-rpc.test.mjs +366 -0
- package/tests/review-state.test.mjs +32 -0
- package/tests/review.test.mjs +127 -0
- package/tests/server.test.mjs +252 -0
- package/tests/ship-jev.test.mjs +77 -0
- package/tests/ship-telemetry.test.mjs +333 -0
- package/tests/standard-workflow.test.mjs +28 -0
- package/tests/subscription-models.test.mjs +416 -0
- package/tests/subscriptions-extension.test.mjs +27 -0
- package/tests/terminal-title.test.mjs +13 -0
- package/tests/test_grade_minecraft_clone.py +199 -0
- package/tests/test_starter.py +78 -0
- package/tests/theme.test.mjs +175 -0
- package/tests/tui-copy-paste.test.mjs +54 -0
- package/tests/tui.test.mjs +808 -0
- package/tests/update-channels.test.mjs +129 -0
- package/tests/update.test.mjs +175 -0
- package/tests/webdriver.test.mjs +31 -0
- package/tests/workflow.test.mjs +47 -0
- package/tests/workspace-rpc.test.mjs +236 -0
- package/tests/workspace.test.mjs +24 -0
- package/tools/benchmark/advisor.test.mjs +7 -0
- package/tools/benchmark/advisor.ts +27 -0
- package/tools/benchmark/campaign.mjs +45 -0
- package/tools/benchmark/diagnose-request.mjs +40 -0
- package/tools/benchmark/eval-common.mjs +32 -0
- package/tools/benchmark/eval-ledger.mjs +22 -0
- package/tools/benchmark/eval-shop.mjs +26 -0
- package/tools/benchmark/eval-voxel.mjs +20 -0
- package/tools/benchmark/evaluate.mjs +24 -0
- package/tools/benchmark/evaluators.test.mjs +13 -0
- package/tools/benchmark/fixtures/shop/server.mjs +18 -0
- package/tools/benchmark/fixtures/shop/ui.html +8 -0
- package/tools/benchmark/freeze.mjs +30 -0
- package/tools/benchmark/gateway.mjs +91 -0
- package/tools/benchmark/gateway.test.mjs +38 -0
- package/tools/benchmark/generator.mjs +11 -0
- package/tools/benchmark/generator.test.mjs +26 -0
- package/tools/benchmark/guard.test.mjs +133 -0
- package/tools/benchmark/guard.ts +136 -0
- package/tools/benchmark/native-plugin/index.ts +24 -0
- package/tools/benchmark/planning-startup.test.mjs +47 -0
- package/tools/benchmark/preflight-live.mjs +31 -0
- package/tools/benchmark/preflight.mjs +22 -0
- package/tools/benchmark/replay-diagnostic.mjs +23 -0
- package/tools/benchmark/runner.mjs +141 -0
- package/tools/benchmark/runner.test.mjs +93 -0
- package/tools/benchmark/runtime.mjs +23 -0
- package/tools/benchmark/selftest.mjs +47 -0
- package/tools/benchmark/smoke-jev.mjs +5 -0
- package/tools/benchmark/tasks.mjs +20 -0
- package/tools/benchmark/transport.mjs +65 -0
- package/tools/benchmark/transport.test.mjs +44 -0
- package/tools/benchmark/upstream-error.mjs +18 -0
- package/tools/benchmark/upstream-error.test.mjs +10 -0
- package/tools/browser-check.mjs +109 -0
- package/tools/check-syntax.mjs +27 -0
- package/tools/evaluate-plan-advisor.mjs +20 -0
- package/tools/native-fixture.mjs +194 -0
- package/tools/native-pty-smoke.py +108 -0
- package/tools/native-report-plugin/index.ts +50 -0
- package/tools/native-report-plugin/package.json +7 -0
- package/tools/patch-compaction.mjs +24 -0
- package/tools/patch-model-catalog.mjs +12 -0
- package/tools/patch-print-shutdown.mjs +39 -0
- package/tools/repo-pilot/evaluator.cjs +32 -0
- package/tools/repo-pilot/reference.mjs +43 -0
- package/tools/repo-pilot/runner.mjs +85 -0
- package/tools/repo-pilot/sandbox-entry.mjs +12 -0
- package/tools/repo-pilot/tasks.mjs +20 -0
- package/tools/repo-pilot/type-evaluator.cjs +6 -0
- package/tools/test-native-controls.mjs +84 -0
- package/tools/test-native-delivery-host.mjs +307 -0
- package/tools/test-native-evaluation.mjs +46 -0
- package/tools/test-native-host.mjs +385 -0
- package/tools/test-native-outcomes.mjs +73 -0
- package/tools/test-native-package.mjs +70 -0
- package/tools/test-native-pty.mjs +20 -0
- package/tools/test-native-report.mjs +88 -0
- package/tools/test-native-swe-host.mjs +132 -0
- package/tools/test-native-swe.mjs +122 -0
- package/tools/test-picker-pty.py +45 -0
- package/tools/test-python.mjs +29 -0
- package/tools/test-webdriver.mjs +54 -0
- package/tools/verify-release-version.mjs +37 -0
- package/tools/webdriver-check.mjs +52 -0
- package/vendor/agent/bounded-summary.mjs +88 -0
- package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
- package/vendor/agent/chunks/anthropic.js +75 -0
- package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
- package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
- package/vendor/agent/chunks/chord-context.js +96 -0
- package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
- package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
- package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
- package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
- package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
- package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
- package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
- package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
- package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
- package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
- package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
- package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
- package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
- package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
- package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
- package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
- package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
- package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
- package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
- package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
- package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
- package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
- package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
- package/vendor/agent/chunks/github-copilot.js +2 -0
- package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
- package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
- package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
- package/vendor/agent/chunks/image-resize-worker.js +2 -0
- package/vendor/agent/chunks/kimi-coding.js +2 -0
- package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
- package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
- package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
- package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
- package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
- package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
- package/vendor/agent/chunks/openai-codex.js +75 -0
- package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
- package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
- package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
- package/vendor/agent/chunks/openrouter.js +75 -0
- package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
- package/vendor/agent/chunks/radius.js +75 -0
- package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
- package/vendor/agent/chunks/xai.js +2 -0
- package/vendor/agent/cli.js +3 -0
- package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/index.js +2 -0
- package/vendor/agent/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/package.json +14 -0
- package/vendor/agent/rpc-entry.js +3 -0
- package/vendor/agent/theme/dark.json +91 -0
- package/vendor/agent/theme/light.json +90 -0
- package/vendor/agent/theme/theme-schema.json +356 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Context } from "@opencode/plugin/promise/plugin"
|
|
2
|
+
import { registerCodegraph } from "./codegraph.ts"
|
|
3
|
+
import { registerDelivery } from "./delivery.ts"
|
|
4
|
+
|
|
5
|
+
export { registerCodegraph, registerDelivery }
|
|
6
|
+
|
|
7
|
+
/** Optional production composition surface for a parent-owned plugin entrypoint. */
|
|
8
|
+
export async function registerNativeDelivery(ctx: Context, options: Parameters<typeof registerDelivery>[1] = {}) {
|
|
9
|
+
const cleanups: Array<() => Promise<void>> = []
|
|
10
|
+
try {
|
|
11
|
+
cleanups.push(await registerDelivery(ctx, options))
|
|
12
|
+
cleanups.push(await registerCodegraph(ctx, { maxFiles: typeof options.codegraphMaxFiles === "number" ? options.codegraphMaxFiles : undefined }))
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const failures = await Promise.allSettled(cleanups.reverse().map(cleanup => cleanup()))
|
|
15
|
+
const cleanupErrors = failures.filter((result): result is PromiseRejectedResult => result.status === "rejected").map(result => result.reason)
|
|
16
|
+
if (cleanupErrors.length) throw new AggregateError([error, ...cleanupErrors], "Native delivery registration failed and cleanup was incomplete.")
|
|
17
|
+
throw error
|
|
18
|
+
}
|
|
19
|
+
return async () => {
|
|
20
|
+
const failures: unknown[] = []
|
|
21
|
+
for (const cleanup of cleanups.reverse()) {
|
|
22
|
+
try { await cleanup() } catch (error) { failures.push(error) }
|
|
23
|
+
}
|
|
24
|
+
if (failures.length) throw new AggregateError(failures, "Native delivery cleanup was incomplete.")
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createTask, listNativeSessions, listNativeWorktrees, listTasks, reconcileTasks } from "../../../../lib/opencode/workspace.mjs"
|
|
2
|
+
import { exportLegacyTranscript, listLegacyArchive, readLegacyTranscript, searchLegacyArchive } from "../../../../lib/opencode/archive.mjs"
|
|
3
|
+
import { startReview, startPullRequestReview, verifyPullRequestHeads } from "../../../../lib/opencode/review.mjs"
|
|
4
|
+
import type { Context as PluginContext } from "@opencode/plugin/promise/plugin"
|
|
5
|
+
|
|
6
|
+
export type Orchestration = ReturnType<typeof registerOrchestration>
|
|
7
|
+
|
|
8
|
+
export type OrchestrationOptions = {
|
|
9
|
+
legacyRoots?: string[]
|
|
10
|
+
reviewModel?: { providerID: string; id: string; variant?: string }
|
|
11
|
+
onInvalidate?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Domain surface used by the CTG native RPC registration; event streams only invalidate caches. */
|
|
15
|
+
export function registerOrchestration(ctx: PluginContext, options: OrchestrationOptions = {}) {
|
|
16
|
+
const canonical = ctx.location.project.canonical
|
|
17
|
+
const projectID = ctx.location.project.id
|
|
18
|
+
if (!canonical || !projectID) throw new Error("CTG orchestration requires the canonical project location and projectID")
|
|
19
|
+
const legacyRoots = options.legacyRoots ?? []
|
|
20
|
+
return {
|
|
21
|
+
createTask: (input: { task: string; from?: string; branch?: string; model?: { providerID: string; id: string; variant?: string }; agent?: string; parentTaskID?: string | null }) => createTask({ ctx, canonical, projectID, ...input }),
|
|
22
|
+
listTasks: (input?: { includeOperations?: boolean }) => listTasks({ canonical, ...input }),
|
|
23
|
+
async snapshot() {
|
|
24
|
+
const [sessions, worktrees] = await Promise.all([
|
|
25
|
+
listNativeSessions({ ctx, canonical, projectID }),
|
|
26
|
+
listNativeWorktrees({ ctx, canonical, projectID }),
|
|
27
|
+
])
|
|
28
|
+
return { projectID, canonical, tasks: listTasks({ canonical }), sessions, worktrees }
|
|
29
|
+
},
|
|
30
|
+
async reconcile() {
|
|
31
|
+
const [sessions, worktrees] = await Promise.all([
|
|
32
|
+
listNativeSessions({ ctx, canonical, projectID }),
|
|
33
|
+
listNativeWorktrees({ ctx, canonical, projectID }),
|
|
34
|
+
])
|
|
35
|
+
return reconcileTasks({
|
|
36
|
+
canonical, projectID,
|
|
37
|
+
sessionIDs: sessions.map(session => session.id),
|
|
38
|
+
worktreeDirectories: worktrees.map((worktree: { directory: string }) => worktree.directory),
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
archive: {
|
|
42
|
+
list: () => listLegacyArchive({ roots: legacyRoots }),
|
|
43
|
+
search: (query: string) => searchLegacyArchive({ roots: legacyRoots, query }),
|
|
44
|
+
transcript: (path: string) => readLegacyTranscript({ path, roots: legacyRoots }),
|
|
45
|
+
export: (path: string, format?: "json" | "jsonl") => exportLegacyTranscript({ path, format, roots: legacyRoots }),
|
|
46
|
+
},
|
|
47
|
+
startReview: (input: { cwd: string; identity?: string; agent?: string; timeoutMs?: number }, signal?: AbortSignal) => startReview({ ctx, model: options.reviewModel, ...input, signal }),
|
|
48
|
+
startPullRequestReview: (input: { cwd: string; repository: string; prNumber: number; agent?: string; timeoutMs?: number }, signal?: AbortSignal) => startPullRequestReview({ ctx, model: options.reviewModel, ...input, signal }),
|
|
49
|
+
verifyPullRequestHeads,
|
|
50
|
+
async watch(signal?: AbortSignal) {
|
|
51
|
+
for await (const event of ctx.event.subscribe({ signal })) {
|
|
52
|
+
if (["session.updated", "session.created", "session.deleted", "worktree.updated"].includes(event.type)) options.onInvalidate?.()
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Delivery's participating-tool policy. This deliberately is not an OS sandbox. */
|
|
2
|
+
import { needsImplementation } from "../../../../lib/planning-access.mjs"
|
|
3
|
+
import { startImplementation } from "../../../../lib/planning.mjs"
|
|
4
|
+
import { fingerprint } from "../../../../lib/delivery.mjs"
|
|
5
|
+
|
|
6
|
+
type DeliveryState = Parameters<typeof startImplementation>[0]
|
|
7
|
+
|
|
8
|
+
export const READ_ONLY_TOOLS = new Set(["read", "ls", "find", "grep", "code_nav", "delivery_probe"])
|
|
9
|
+
|
|
10
|
+
export function requiresPlanApproval(tool: string) {
|
|
11
|
+
return needsImplementation(tool)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function enforceToolPolicy(tool: string, state: DeliveryState, cwd: string) {
|
|
15
|
+
if (!requiresPlanApproval(tool)) return
|
|
16
|
+
if (!state) throw Error(`Implementation locked: ${tool} requires a delivery_plan first.`)
|
|
17
|
+
startImplementation(state, fingerprint(cwd, ["."]))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function policyDescription(workflow: "standard" | "strict") {
|
|
21
|
+
return workflow === "standard"
|
|
22
|
+
? "Standard workflow: a valid required delivery_plan permits implementation; all declared checks, final review and delivery_finish gates remain mandatory."
|
|
23
|
+
: "Strict workflow: implementation, tests and commands remain locked until the current plan revision has passed design validation, inspection, adversarial review and approval."
|
|
24
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { Context } from '@opencode/plugin/promise/plugin';
|
|
2
|
+
import type { ConnectionInfo } from '@opencode/client';
|
|
3
|
+
import type { Credential } from '@opencode/schema/credential';
|
|
4
|
+
import type { SessionModelRequest } from '@opencode/plugin/promise/session';
|
|
5
|
+
import type { Form } from '@opencode/schema/form';
|
|
6
|
+
import { Model } from '@opencode/schema/model';
|
|
7
|
+
import { Provider } from '@opencode/schema/provider';
|
|
8
|
+
import { Integration as IntegrationSchema } from '@opencode/schema/integration';
|
|
9
|
+
import { Money } from '@opencode/schema/money';
|
|
10
|
+
import {
|
|
11
|
+
CloudClient, cloudControlUrl, cloudManagedUsageRecovery, oauthCredentials, openBrowser,
|
|
12
|
+
} from '../../../../lib/cloud/client.mjs';
|
|
13
|
+
import {
|
|
14
|
+
parseExperientialModels, experientialModelDefinition, cloudGatewayUrl,
|
|
15
|
+
EXPERIENTIAL_API_KEY_ENV, EXPERIENTIAL_GATEWAY_URL_ENV,
|
|
16
|
+
EXPERIENTIAL_PROVIDER_ID, EXPERIENTIAL_PROVIDER_NAME,
|
|
17
|
+
experientialBaseUrl,
|
|
18
|
+
} from '../../../../lib/providers/experiential.mjs';
|
|
19
|
+
import {
|
|
20
|
+
ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID,
|
|
21
|
+
anthropicModelDefinition, codexModelDefinition,
|
|
22
|
+
fetchAnthropicSubscriptionModels, fetchCodexSubscriptionModels,
|
|
23
|
+
parseAnthropicModels, parseCodexModels,
|
|
24
|
+
} from '../../../../lib/providers/subscription-models.mjs';
|
|
25
|
+
import { catalogModels } from '../../../../lib/providers/model-catalog.mjs';
|
|
26
|
+
import { shipAgentDir } from '../../../../lib/ship-auth.mjs';
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
import { legacyOAuthForOpenCode, nativeCredentialFile, refreshNativeCredential, refreshSingleFlight } from '../../../../lib/opencode/credentials.mjs';
|
|
29
|
+
import { createDeviceAuthController } from './device-auth.ts';
|
|
30
|
+
import { createSessionIdentity, isManagedShipCredential, shipTelemetryHeaders } from './telemetry.ts';
|
|
31
|
+
|
|
32
|
+
const OAUTH_METHOD = 'carthagent-device';
|
|
33
|
+
const SHIP_INTEGRATION_ID = EXPERIENTIAL_PROVIDER_ID;
|
|
34
|
+
const DISCOVERY_TIMEOUT = 10_000;
|
|
35
|
+
const PROFILE_CAP = 200;
|
|
36
|
+
const CACHE_KEY = 'ctg.models/subscriptions/';
|
|
37
|
+
const RUN_MODE = new Set(['interactive', 'print', 'rpc']);
|
|
38
|
+
|
|
39
|
+
function signalWithTimeout(signal: AbortSignal | undefined, timeout: number) {
|
|
40
|
+
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
41
|
+
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function providerInfo(id: string, name: string, pkg: string, integrationID?: string, settings: Record<string, unknown> = {}): Provider.Info {
|
|
45
|
+
return {
|
|
46
|
+
...Provider.Info.empty(Provider.ID.make(id)),
|
|
47
|
+
name,
|
|
48
|
+
activation: 'enabled',
|
|
49
|
+
package: pkg,
|
|
50
|
+
settings,
|
|
51
|
+
...(integrationID ? { integrationID: IntegrationSchema.ID.make(integrationID) } : {}),
|
|
52
|
+
} as Provider.Info;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CatalogModel {
|
|
56
|
+
id: string;
|
|
57
|
+
modelID?: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
input?: string[];
|
|
60
|
+
output?: string[];
|
|
61
|
+
cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; tiers?: Array<{ input: number; output: number; inputTokensAbove: number; cacheRead?: number; cacheWrite?: number }> };
|
|
62
|
+
contextWindow?: number;
|
|
63
|
+
maxTokens?: number;
|
|
64
|
+
temperature?: boolean;
|
|
65
|
+
baseUrl?: string;
|
|
66
|
+
compat?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function modelInfo(providerID: string, profile: CatalogModel, defaultBaseURL: string): Model.Info {
|
|
70
|
+
const input = profile.input ?? ['text'];
|
|
71
|
+
const output = profile.output ?? ['text'];
|
|
72
|
+
const id = Model.ID.make(String(profile.id));
|
|
73
|
+
const price = profile.cost;
|
|
74
|
+
const cost: Model.Cost[] = price && Number.isFinite(price.input) && Number.isFinite(price.output)
|
|
75
|
+
? [{ input: Money.USDPerMillionTokens.make(price.input!), output: Money.USDPerMillionTokens.make(price.output!),
|
|
76
|
+
cache: { read: Money.USDPerMillionTokens.make(price.cacheRead ?? 0), write: Money.USDPerMillionTokens.make(price.cacheWrite ?? 0) } }]
|
|
77
|
+
: [];
|
|
78
|
+
for (const tier of price?.tiers ?? []) {
|
|
79
|
+
if (!Number.isFinite(tier.input) || !Number.isFinite(tier.output) || !Number.isFinite(tier.inputTokensAbove)) continue;
|
|
80
|
+
cost.push({ tier: { type: 'context', size: tier.inputTokensAbove }, input: Money.USDPerMillionTokens.make(tier.input), output: Money.USDPerMillionTokens.make(tier.output),
|
|
81
|
+
cache: { read: Money.USDPerMillionTokens.make(tier.cacheRead ?? 0), write: Money.USDPerMillionTokens.make(tier.cacheWrite ?? 0) } });
|
|
82
|
+
}
|
|
83
|
+
const base = Model.Info.default(Provider.ID.make(providerID), id);
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
id,
|
|
87
|
+
modelID: Model.ID.make(String(profile.modelID || id)),
|
|
88
|
+
name: profile.name || String(id),
|
|
89
|
+
family: Model.Family.make(String(id).split(/[-/]/)[0]),
|
|
90
|
+
capabilities: { tools: true, input, output, ...(typeof profile.temperature === 'boolean' ? { temperature: profile.temperature } : {}) },
|
|
91
|
+
cost,
|
|
92
|
+
limit: { context: Math.trunc(profile.contextWindow ?? 128_000), output: Math.trunc(profile.maxTokens ?? 16_384) },
|
|
93
|
+
settings: profile.baseUrl || defaultBaseURL ? { baseURL: profile.baseUrl || defaultBaseURL } : {},
|
|
94
|
+
...(profile.compat ? { compatibility: mapCompatibility(profile.compat) } : {}),
|
|
95
|
+
} as Model.Info;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function mapCompatibility(compat: Record<string, unknown>): NonNullable<Model.Info['compatibility']> {
|
|
99
|
+
const keys = [
|
|
100
|
+
'reasoningField', 'requireReasoning', 'maxTokensField', 'requireFinishReason',
|
|
101
|
+
'requireAssistantAfterTool', 'supportsPromptCacheKey', 'cacheFormat',
|
|
102
|
+
'supportsThinkingBlockBinding', 'supportsEffortUpdates',
|
|
103
|
+
] as const;
|
|
104
|
+
return Object.fromEntries(keys.flatMap(key => key in compat ? [[key, compat[key]]] : [])) as NonNullable<Model.Info['compatibility']>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function oauthMethod({ env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow, rereadCredential, credentialFile }: {
|
|
108
|
+
env: NodeJS.ProcessEnv;
|
|
109
|
+
fetchImpl: typeof fetch;
|
|
110
|
+
sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
111
|
+
now: () => number;
|
|
112
|
+
openBrowserImpl: typeof openBrowser;
|
|
113
|
+
agentDir: string;
|
|
114
|
+
startDeviceFlow: () => Promise<{ authorization: { userCode: string; verificationUri: string; verificationUriComplete?: string; expiresIn: number; interval: number; deviceCode: string }; callback: Promise<Credential.OAuth> }>;
|
|
115
|
+
rereadCredential: () => Promise<Credential.OAuth | undefined>;
|
|
116
|
+
credentialFile: string;
|
|
117
|
+
}) {
|
|
118
|
+
const client = new CloudClient({ baseUrl: cloudControlUrl(env), fetchImpl, sleep: ms => sleep(ms) });
|
|
119
|
+
return {
|
|
120
|
+
integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID),
|
|
121
|
+
method: { id: OAUTH_METHOD, type: 'oauth' as const, label: 'Sign in with Carthagent Ship' },
|
|
122
|
+
authorize: async (_answer: Form.Answer) => {
|
|
123
|
+
const legacy = await legacyOAuthForOpenCode({ agentDir });
|
|
124
|
+
if (legacy) {
|
|
125
|
+
return {
|
|
126
|
+
mode: 'auto' as const,
|
|
127
|
+
url: 'https://api.carthagent.xyz',
|
|
128
|
+
instructions: 'A legacy Carthagent Ship credential was found and will be migrated into OpenCode integration storage.',
|
|
129
|
+
callback: Promise.resolve({
|
|
130
|
+
type: 'oauth' as const,
|
|
131
|
+
methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
|
|
132
|
+
access: legacy.access,
|
|
133
|
+
refresh: legacy.refresh,
|
|
134
|
+
expires: Math.trunc(legacy.expires),
|
|
135
|
+
} as Credential.OAuth),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const { authorization: response, callback } = await startDeviceFlow();
|
|
139
|
+
const url = response.verificationUriComplete || response.verificationUri;
|
|
140
|
+
Promise.resolve(openBrowserImpl(url)).catch(() => false);
|
|
141
|
+
return {
|
|
142
|
+
mode: 'auto' as const, url,
|
|
143
|
+
instructions: `Approve device code ${response.userCode} in your browser.`,
|
|
144
|
+
expiresAt: now() + Number(response.expiresIn || 600) * 1000,
|
|
145
|
+
callback,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
refresh: (credential: Credential.OAuth) => refreshSingleFlight(
|
|
149
|
+
`${credential.methodID}:${credential.refresh}`,
|
|
150
|
+
() => refreshNativeCredential({
|
|
151
|
+
credential,
|
|
152
|
+
credentialFile,
|
|
153
|
+
reread: rereadCredential,
|
|
154
|
+
refresh: async (current: Credential.OAuth) => ({ type: 'oauth' as const, methodID: IntegrationSchema.MethodID.make(OAUTH_METHOD),
|
|
155
|
+
...oauthCredentials(await client.refresh({ refreshToken: current.refresh }), now()),
|
|
156
|
+
...(current.metadata ? { metadata: current.metadata } : {}) }),
|
|
157
|
+
}),
|
|
158
|
+
),
|
|
159
|
+
label: () => 'Carthagent Ship',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function integrationDefinitions(editor: Parameters<Context['integration']['transform']>[0] extends (editor: infer E) => void ? E : never, options: Parameters<typeof oauthMethod>[0]) {
|
|
164
|
+
editor.method.update(oauthMethod(options));
|
|
165
|
+
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'key', label: 'Use an Experiential API key' } });
|
|
166
|
+
editor.method.update({ integrationID: IntegrationSchema.ID.make(SHIP_INTEGRATION_ID), method: { type: 'env', names: [EXPERIENTIAL_API_KEY_ENV] } });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function asModelInfo(providerID: string, model: { id: string }, baseURL: string) {
|
|
170
|
+
const fixed = experientialModelDefinition(model);
|
|
171
|
+
return modelInfo(providerID, fixed, baseURL);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function loadShipModels({ ctx, fetchImpl, env, baseURL, signal }: {
|
|
175
|
+
ctx: Context; fetchImpl: typeof fetch; env: NodeJS.ProcessEnv; baseURL: string; signal?: AbortSignal;
|
|
176
|
+
}): Promise<{ models: ReturnType<typeof parseExperientialModels>; connection: ConnectionInfo | undefined; managed: boolean }> {
|
|
177
|
+
const connection = await ctx.integration.connection.active(SHIP_INTEGRATION_ID);
|
|
178
|
+
if (!connection) return { models: [], connection: undefined, managed: false };
|
|
179
|
+
const credential = await ctx.integration.connection.resolve(connection);
|
|
180
|
+
const key = credential?.type === 'key' ? credential.key : credential?.type === 'oauth' ? credential.access : '';
|
|
181
|
+
if (!key) return { models: [], connection, managed: false };
|
|
182
|
+
const managed = isManagedShipCredential(credential);
|
|
183
|
+
const response = await fetchImpl(`${managed ? cloudGatewayUrl(env) : baseURL}/models`, {
|
|
184
|
+
headers: { authorization: `Bearer ${key}` }, signal: signalWithTimeout(signal, DISCOVERY_TIMEOUT),
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok) throw new Error(`Carthagent Ship model discovery failed (${response.status}).`);
|
|
187
|
+
const models = parseExperientialModels(await response.json()).slice(0, PROFILE_CAP);
|
|
188
|
+
if (connection && managed) return { connection, models, managed };
|
|
189
|
+
return { connection, models, managed };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Register CTG Ship plus subscription inventory on OpenCode's native provider
|
|
193
|
+
* engine. OpenCode owns all inference transport; hooks only add metadata and
|
|
194
|
+
* idempotency identity to native HTTP requests. */
|
|
195
|
+
interface ProviderRegistrationOptions {
|
|
196
|
+
env?: NodeJS.ProcessEnv;
|
|
197
|
+
fetchImpl?: typeof fetch;
|
|
198
|
+
now?: () => number;
|
|
199
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
200
|
+
agentDir?: string;
|
|
201
|
+
credentialFile?: string;
|
|
202
|
+
openBrowser?: typeof openBrowser;
|
|
203
|
+
telemetryIdentity?: ReturnType<typeof createSessionIdentity>;
|
|
204
|
+
sessionIdentity?: (event: SessionModelRequest) => string;
|
|
205
|
+
requestSecret?: string;
|
|
206
|
+
onRecovery?: (recovery: unknown, context: { sessionID: string; kind: string }) => void;
|
|
207
|
+
deviceAuth?: ReturnType<typeof import('./device-auth').createDeviceAuthController>;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function registerProviders(ctx: Context, options: ProviderRegistrationOptions = {}) {
|
|
211
|
+
const env = options.env ?? process.env;
|
|
212
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
213
|
+
const now = options.now ?? Date.now;
|
|
214
|
+
const sleep = options.sleep ?? ((ms: number) => new Promise(resolve => setTimeout(resolve, ms)));
|
|
215
|
+
const agentDir = options.agentDir ?? shipAgentDir(env);
|
|
216
|
+
const openBrowserImpl = options.openBrowser ?? openBrowser;
|
|
217
|
+
const deviceAuth = options.deviceAuth ?? createDeviceAuthController({ env, fetchImpl, agentDir, now, sleep });
|
|
218
|
+
const rereadCredential = async (): Promise<Credential.OAuth | undefined> => {
|
|
219
|
+
const active = await ctx.integration.connection.active(SHIP_INTEGRATION_ID);
|
|
220
|
+
if (!active) return undefined;
|
|
221
|
+
const value = await ctx.integration.connection.resolve(active);
|
|
222
|
+
return value?.type === 'oauth' && value.methodID === OAUTH_METHOD ? value : undefined;
|
|
223
|
+
};
|
|
224
|
+
const requestIdentity = options.sessionIdentity;
|
|
225
|
+
const identity = options.telemetryIdentity ?? createSessionIdentity();
|
|
226
|
+
const requestSecret = options.requestSecret ?? `${ctx.location?.project?.id || ctx.location?.directory || 'ctg'}:request-v1`;
|
|
227
|
+
const registrations: Array<{ dispose: () => Promise<void> }> = [];
|
|
228
|
+
const controller = new AbortController();
|
|
229
|
+
let eventTask: Promise<void> | undefined;
|
|
230
|
+
const disposeRegistrations = async () => {
|
|
231
|
+
const failures: unknown[] = [];
|
|
232
|
+
for (const registration of registrations.splice(0).reverse()) {
|
|
233
|
+
try { await registration.dispose?.(); }
|
|
234
|
+
catch (error) { failures.push(error); }
|
|
235
|
+
}
|
|
236
|
+
return failures;
|
|
237
|
+
};
|
|
238
|
+
try {
|
|
239
|
+
const integration = await ctx.integration.transform(editor => {
|
|
240
|
+
integrationDefinitions(editor, {
|
|
241
|
+
env, fetchImpl, sleep, now, openBrowserImpl, agentDir, startDeviceFlow: deviceAuth.start,
|
|
242
|
+
rereadCredential,
|
|
243
|
+
credentialFile: options.credentialFile ?? nativeCredentialFile(env),
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
registrations.push(integration);
|
|
247
|
+
|
|
248
|
+
const baseURL = experientialBaseUrl(env);
|
|
249
|
+
const shipModels: Model.Info[] = [];
|
|
250
|
+
const subscriptions: Record<string, Model.Info[]> = { [ANTHROPIC_PROVIDER_ID]: [], [OPENAI_CODEX_PROVIDER_ID]: [] };
|
|
251
|
+
const subscriptionConnections: Partial<Record<string, ConnectionInfo>> = {};
|
|
252
|
+
const providerRegistration = await ctx.provider.transform(editor => {
|
|
253
|
+
editor.add({ info: providerInfo(EXPERIENTIAL_PROVIDER_ID, EXPERIENTIAL_PROVIDER_NAME,
|
|
254
|
+
'@opencode/ai/providers/openai-compatible', SHIP_INTEGRATION_ID,
|
|
255
|
+
{ baseURL, apiKey: `$${EXPERIENTIAL_API_KEY_ENV}` }), models: shipModels });
|
|
256
|
+
// Subscription IDs already exist in the native catalog. Overlay only the
|
|
257
|
+
// definitions and retain catalog cost/capability metadata.
|
|
258
|
+
for (const [providerID, baseURL] of [
|
|
259
|
+
[ANTHROPIC_PROVIDER_ID, 'https://api.anthropic.com'],
|
|
260
|
+
[OPENAI_CODEX_PROVIDER_ID, 'https://chatgpt.com/backend-api'],
|
|
261
|
+
] as const) {
|
|
262
|
+
const existing = editor.get(providerID);
|
|
263
|
+
const models: readonly Model.Info[] = subscriptions[providerID].length
|
|
264
|
+
? subscriptions[providerID]
|
|
265
|
+
: (existing ? [...existing.models.values()] : catalogModels(providerID).map((model: CatalogModel) => modelInfo(providerID, model, baseURL)));
|
|
266
|
+
if (existing && subscriptionConnections[providerID]) {
|
|
267
|
+
editor.add({ info: existing.provider, models, sourceConnection: subscriptionConnections[providerID] });
|
|
268
|
+
} else if (subscriptions[providerID].length) {
|
|
269
|
+
editor.models.set(providerID, models);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
registrations.push(providerRegistration);
|
|
274
|
+
|
|
275
|
+
const providerHook = await ctx.session.hook('http.request', async event => {
|
|
276
|
+
if (event.model.providerID !== EXPERIENTIAL_PROVIDER_ID) return;
|
|
277
|
+
let credential;
|
|
278
|
+
try {
|
|
279
|
+
const active = await ctx.integration.connection.active(SHIP_INTEGRATION_ID);
|
|
280
|
+
if (active) credential = await ctx.integration.connection.resolve(active);
|
|
281
|
+
} catch { return; }
|
|
282
|
+
if (!isManagedShipCredential(credential)) return;
|
|
283
|
+
const body = await event.request.clone().text().catch(() => '');
|
|
284
|
+
const headers = new Headers(event.request.headers);
|
|
285
|
+
const telemetry = shipTelemetryHeaders({
|
|
286
|
+
sessionID: event.sessionID, kind: event.kind, body, env, agentDir,
|
|
287
|
+
identity,
|
|
288
|
+
requestID: event.request.headers.get('x-carthagent-request-id') || identity.request(event.sessionID, event.kind),
|
|
289
|
+
requestSecret,
|
|
290
|
+
});
|
|
291
|
+
for (const [name, value] of Object.entries(telemetry)) headers.set(name, String(value));
|
|
292
|
+
headers.set('Idempotency-Key', telemetry['x-carthagent-request-id']);
|
|
293
|
+
headers.set('X-Client-Request-Id', telemetry['x-carthagent-request-id']);
|
|
294
|
+
event.request = new Request(event.request, { headers });
|
|
295
|
+
}, { providerID: EXPERIENTIAL_PROVIDER_ID });
|
|
296
|
+
registrations.push(providerHook);
|
|
297
|
+
|
|
298
|
+
const routeHook = await ctx.session.hook('model.request', async event => {
|
|
299
|
+
if (event.model.providerID !== EXPERIENTIAL_PROVIDER_ID) return;
|
|
300
|
+
event.headers['x-carthagent-request-id'] = requestIdentity
|
|
301
|
+
? requestIdentity(event)
|
|
302
|
+
: identity.request(event.sessionID, event.kind);
|
|
303
|
+
const active = await ctx.integration.connection.active(SHIP_INTEGRATION_ID).catch(() => undefined);
|
|
304
|
+
if (!active) return;
|
|
305
|
+
const credential = await ctx.integration.connection.resolve(active).catch(() => undefined);
|
|
306
|
+
if (isManagedShipCredential(credential)) event.baseURL = cloudGatewayUrl(env);
|
|
307
|
+
}, { providerID: EXPERIENTIAL_PROVIDER_ID });
|
|
308
|
+
registrations.push(routeHook);
|
|
309
|
+
|
|
310
|
+
const noUnsafeRetry = await ctx.session.hook('retry', async event => {
|
|
311
|
+
if (event.model.providerID !== EXPERIENTIAL_PROVIDER_ID) return;
|
|
312
|
+
const active = await ctx.integration.connection.active(SHIP_INTEGRATION_ID).catch(() => undefined);
|
|
313
|
+
const credential = active ? await ctx.integration.connection.resolve(active).catch(() => undefined) : undefined;
|
|
314
|
+
if (!isManagedShipCredential(credential)) return;
|
|
315
|
+
// Gateway idempotency currently reserves before dispatch and cannot replay
|
|
316
|
+
// an existing operation, so retries must not duplicate managed usage.
|
|
317
|
+
event.decision = { retry: false };
|
|
318
|
+
}, { providerID: EXPERIENTIAL_PROVIDER_ID });
|
|
319
|
+
registrations.push(noUnsafeRetry);
|
|
320
|
+
|
|
321
|
+
const recoveryHook = await ctx.session.hook('http.response', async event => {
|
|
322
|
+
if (event.model.providerID !== EXPERIENTIAL_PROVIDER_ID || event.response.ok) return;
|
|
323
|
+
const body = await event.response.clone().text().catch(() => '');
|
|
324
|
+
const recovery = cloudManagedUsageRecovery(body);
|
|
325
|
+
if (recovery) options.onRecovery?.(recovery, { sessionID: event.sessionID, kind: event.kind });
|
|
326
|
+
}, { providerID: EXPERIENTIAL_PROVIDER_ID });
|
|
327
|
+
registrations.push(recoveryHook);
|
|
328
|
+
|
|
329
|
+
let disposed = false;
|
|
330
|
+
const refresh = async (signal?: AbortSignal): Promise<void> => {
|
|
331
|
+
const inventory = await loadShipModels({ ctx, fetchImpl, env, baseURL, signal });
|
|
332
|
+
if (inventory?.models) {
|
|
333
|
+
shipModels.splice(0, shipModels.length, ...inventory.models.map(model => asModelInfo(EXPERIENTIAL_PROVIDER_ID, model,
|
|
334
|
+
inventory.managed ? cloudGatewayUrl(env) : baseURL)));
|
|
335
|
+
await ctx.provider.reload();
|
|
336
|
+
}
|
|
337
|
+
for (const providerID of [ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID]) {
|
|
338
|
+
if (signal?.aborted) return;
|
|
339
|
+
const active = await ctx.integration.connection.active(providerID);
|
|
340
|
+
if (!active) {
|
|
341
|
+
if (subscriptionConnections[providerID]) {
|
|
342
|
+
delete subscriptionConnections[providerID];
|
|
343
|
+
subscriptions[providerID].splice(0, subscriptions[providerID].length);
|
|
344
|
+
await ctx.provider.reload();
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const credential = await ctx.integration.connection.resolve(active);
|
|
349
|
+
const result = providerID === ANTHROPIC_PROVIDER_ID
|
|
350
|
+
? await fetchAnthropicSubscriptionModels(credential, { fetchImpl })
|
|
351
|
+
: await fetchCodexSubscriptionModels(credential, { fetchImpl });
|
|
352
|
+
const discovered = result.models.slice(0, PROFILE_CAP);
|
|
353
|
+
const current = await ctx.integration.connection.active(providerID);
|
|
354
|
+
if (!current || !('id' in active) || !('id' in current) || current.id !== active.id) continue;
|
|
355
|
+
const cacheKey = `${CACHE_KEY}${providerID}`;
|
|
356
|
+
const prior = await ctx.storage.get(cacheKey) as { account?: string; models?: CatalogModel[] } | undefined;
|
|
357
|
+
const accountMetadata = credential && 'metadata' in credential ? credential.metadata as Record<string, unknown> | undefined : undefined;
|
|
358
|
+
const accountKey = createHash('sha256').update(String(('id' in active ? active.id : '') || accountMetadata?.accountId || (credential?.type === 'oauth' ? credential.access : credential?.type === 'key' ? credential.key : '') || '')).digest('hex');
|
|
359
|
+
if (prior?.account !== accountKey) {
|
|
360
|
+
subscriptions[providerID].splice(0, subscriptions[providerID].length);
|
|
361
|
+
}
|
|
362
|
+
const known = new Map([
|
|
363
|
+
...catalogModels(providerID).map((model: CatalogModel) => [model.id, model] as const),
|
|
364
|
+
...(Array.isArray(prior?.models) ? prior.models : []).map(model => [model.id, model] as const),
|
|
365
|
+
]);
|
|
366
|
+
const converted = discovered.map(model => {
|
|
367
|
+
const base = known.get(model.id);
|
|
368
|
+
return modelInfo(providerID, providerID === ANTHROPIC_PROVIDER_ID
|
|
369
|
+
? anthropicModelDefinition(model, base) : codexModelDefinition(model, base),
|
|
370
|
+
providerID === ANTHROPIC_PROVIDER_ID ? 'https://api.anthropic.com' : 'https://chatgpt.com/backend-api');
|
|
371
|
+
});
|
|
372
|
+
subscriptions[providerID].splice(0, subscriptions[providerID].length, ...converted);
|
|
373
|
+
subscriptionConnections[providerID] = active;
|
|
374
|
+
await ctx.storage.set(cacheKey, { account: accountKey, models: discovered, checkedAt: now() });
|
|
375
|
+
await ctx.provider.reload();
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
void refresh(controller.signal).catch(() => {});
|
|
379
|
+
const onConnection = async (event: { type: string }) => {
|
|
380
|
+
if (event.type === 'integration.updated' || event.type === 'credential.updated' || event.type === 'credential.switched') {
|
|
381
|
+
void refresh(controller.signal).catch(() => {});
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
eventTask = (async () => {
|
|
385
|
+
try { for await (const event of ctx.event.subscribe({ signal: controller.signal })) await onConnection(event); }
|
|
386
|
+
catch { /* abort/disconnect ends the watcher */ }
|
|
387
|
+
})();
|
|
388
|
+
|
|
389
|
+
return async () => {
|
|
390
|
+
if (disposed) return;
|
|
391
|
+
disposed = true;
|
|
392
|
+
controller.abort();
|
|
393
|
+
const failures: unknown[] = [];
|
|
394
|
+
try { await eventTask; } catch (error) { failures.push(error); }
|
|
395
|
+
try { await deviceAuth.cleanup(); } catch (error) { failures.push(error); }
|
|
396
|
+
failures.push(...await disposeRegistrations());
|
|
397
|
+
if (failures.length) throw new AggregateError(failures, 'Carthagent provider cleanup did not complete cleanly.');
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
controller.abort();
|
|
401
|
+
const failures: unknown[] = [];
|
|
402
|
+
if (eventTask) {
|
|
403
|
+
try { await eventTask; } catch (cleanupError) { failures.push(cleanupError); }
|
|
404
|
+
}
|
|
405
|
+
try { await deviceAuth.cleanup(); } catch (cleanupError) { failures.push(cleanupError); }
|
|
406
|
+
failures.push(...await disposeRegistrations());
|
|
407
|
+
if (failures.length) throw new AggregateError([error, ...failures], 'Carthagent provider registration failed and rollback was incomplete.', { cause: error });
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export const providerConstants = Object.freeze({
|
|
413
|
+
SHIP_PROVIDER_ID: EXPERIENTIAL_PROVIDER_ID,
|
|
414
|
+
SHIP_PROVIDER_NAME: EXPERIENTIAL_PROVIDER_NAME,
|
|
415
|
+
SHIP_INTEGRATION_ID,
|
|
416
|
+
ANTHROPIC_PROVIDER_ID,
|
|
417
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
418
|
+
OAUTH_METHOD,
|
|
419
|
+
EXPERIENTIAL_GATEWAY_URL_ENV,
|
|
420
|
+
RUN_MODES: [...RUN_MODE],
|
|
421
|
+
parseExperientialModels,
|
|
422
|
+
parseAnthropicModels,
|
|
423
|
+
parseCodexModels,
|
|
424
|
+
});
|