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.
Files changed (379) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -2
  3. package/bin/ctg.mjs +736 -0
  4. package/docs/budgets.md +72 -0
  5. package/docs/compaction.md +34 -0
  6. package/docs/context-efficiency-plan.md +24 -0
  7. package/docs/context-efficiency-review.md +27 -0
  8. package/docs/context-efficiency.md +49 -0
  9. package/docs/deepseek-jev-benchmark-plan.md +70 -0
  10. package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
  11. package/docs/deepseek-jev-benchmark-restart.md +9 -0
  12. package/docs/deepseek-jev-benchmark-results.md +77 -0
  13. package/docs/deepseek-jev-benchmark-running.md +60 -0
  14. package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
  15. package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
  16. package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
  17. package/docs/deepseek-jev-http400-diagnosis.md +45 -0
  18. package/docs/delivery-review-evidence.md +53 -0
  19. package/docs/delivery-timeout-repair-plan.md +51 -0
  20. package/docs/evaluation-readiness.md +31 -0
  21. package/docs/evaluation.md +70 -0
  22. package/docs/extending.md +354 -0
  23. package/docs/glm-pilot-execution.md +9 -0
  24. package/docs/glm-pilot-plan.md +15 -0
  25. package/docs/glm-pilot-results.md +71 -0
  26. package/docs/graph-context-optimization.md +15 -0
  27. package/docs/jev-repository-pilot-plan.md +25 -0
  28. package/docs/jev-repository-pilot-protocol.md +39 -0
  29. package/docs/jev-repository-pilot-restart.md +7 -0
  30. package/docs/jev-repository-pilot-results.md +79 -0
  31. package/docs/jev.md +32 -0
  32. package/docs/model-registry.md +25 -0
  33. package/docs/native-compatibility.md +117 -0
  34. package/docs/opencode-archive.md +9 -0
  35. package/docs/opencode-migration.md +115 -0
  36. package/docs/opencode-runtime.md +94 -0
  37. package/docs/opencode-workspace.md +31 -0
  38. package/docs/owned-runtime-workflow-registration.md +17 -0
  39. package/docs/plan-advisor-evaluation.md +38 -0
  40. package/docs/planning-startup-fix-plan.md +23 -0
  41. package/docs/planning-startup-fix-review.md +24 -0
  42. package/docs/planning.md +118 -0
  43. package/docs/production-merge-review.md +23 -0
  44. package/docs/quality.md +35 -0
  45. package/docs/repository-benchmark-design.md +111 -0
  46. package/docs/review-assurance.md +9 -0
  47. package/docs/review-experiment-results.md +59 -0
  48. package/docs/tested-planning-roadmap.md +58 -0
  49. package/docs/tested-planning.md +58 -0
  50. package/docs/tui-pickers.md +9 -0
  51. package/docs/webdriver.md +83 -0
  52. package/docs/workflow-modes.md +26 -0
  53. package/docs/workflow.d2 +17 -0
  54. package/docs/workflow.svg +149 -0
  55. package/docs/workspace-coordination.md +9 -0
  56. package/evaluate.mjs +249 -0
  57. package/extensions/codegraph.ts +36 -0
  58. package/extensions/delivery.ts +819 -0
  59. package/extensions/experiential.ts +21 -0
  60. package/extensions/subscriptions.ts +21 -0
  61. package/guidance/profiles/authenticated-web.json +24 -0
  62. package/guidance/profiles/data-visualization.json +25 -0
  63. package/guidance/profiles/database-change.json +24 -0
  64. package/guidance/profiles/external-api.json +24 -0
  65. package/guidance/profiles/transactional-data.json +25 -0
  66. package/guidance/profiles/web-application.json +26 -0
  67. package/lib/acceptance-review.mjs +4 -0
  68. package/lib/browser-bin.mjs +160 -0
  69. package/lib/browser-errors.mjs +54 -0
  70. package/lib/browser.mjs +219 -0
  71. package/lib/budget.mjs +37 -0
  72. package/lib/cdp.mjs +221 -0
  73. package/lib/cloud/client.mjs +275 -0
  74. package/lib/cloud/control-plane.mjs +350 -0
  75. package/lib/cloud/gateway.mjs +380 -0
  76. package/lib/cloud/http.mjs +135 -0
  77. package/lib/cloud/ledger.mjs +215 -0
  78. package/lib/codegraph-query.mjs +60 -0
  79. package/lib/codegraph.mjs +378 -0
  80. package/lib/context-archive.mjs +18 -0
  81. package/lib/delivery-context.mjs +132 -0
  82. package/lib/delivery-guidance.mjs +9 -0
  83. package/lib/delivery-review.mjs +94 -0
  84. package/lib/delivery.mjs +610 -0
  85. package/lib/engine.mjs +137 -0
  86. package/lib/guidance.mjs +71 -0
  87. package/lib/jev-advisor.mjs +47 -0
  88. package/lib/opencode/archive.mjs +122 -0
  89. package/lib/opencode/compatibility.mjs +19 -0
  90. package/lib/opencode/credentials.mjs +205 -0
  91. package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
  92. package/lib/opencode/evaluation.mjs +242 -0
  93. package/lib/opencode/file-lock.mjs +86 -0
  94. package/lib/opencode/headless.mjs +200 -0
  95. package/lib/opencode/review.mjs +213 -0
  96. package/lib/opencode/runtime.mjs +246 -0
  97. package/lib/opencode/service.mjs +249 -0
  98. package/lib/opencode/session-options.mjs +60 -0
  99. package/lib/opencode/workspace.mjs +133 -0
  100. package/lib/openrouter-jev.mjs +37 -0
  101. package/lib/outcome-contract.mjs +21 -0
  102. package/lib/plan-advisor.mjs +38 -0
  103. package/lib/planning-access.mjs +7 -0
  104. package/lib/planning-advice.mjs +28 -0
  105. package/lib/planning-discovery.mjs +14 -0
  106. package/lib/planning-guide.mjs +34 -0
  107. package/lib/planning-view.mjs +57 -0
  108. package/lib/planning.mjs +155 -0
  109. package/lib/probe-diagnostics.mjs +13 -0
  110. package/lib/providers/experiential.mjs +292 -0
  111. package/lib/providers/model-catalog.json +700 -0
  112. package/lib/providers/model-catalog.mjs +7 -0
  113. package/lib/providers/names.mjs +13 -0
  114. package/lib/providers/subscription-models.mjs +483 -0
  115. package/lib/reports.mjs +80 -0
  116. package/lib/review-state.mjs +49 -0
  117. package/lib/review.mjs +114 -0
  118. package/lib/scenarios.mjs +99 -0
  119. package/lib/ship-auth.mjs +44 -0
  120. package/lib/ship-jev.mjs +59 -0
  121. package/lib/ship-telemetry.mjs +183 -0
  122. package/lib/tui/ansi.mjs +255 -0
  123. package/lib/tui/app.mjs +2445 -0
  124. package/lib/tui/auth.mjs +249 -0
  125. package/lib/tui/carthage.mjs +109 -0
  126. package/lib/tui/clipboard.mjs +122 -0
  127. package/lib/tui/d2.mjs +431 -0
  128. package/lib/tui/feed.mjs +467 -0
  129. package/lib/tui/framing.mjs +80 -0
  130. package/lib/tui/glyphs.mjs +172 -0
  131. package/lib/tui/hash-worker.mjs +11 -0
  132. package/lib/tui/mock-engine.mjs +222 -0
  133. package/lib/tui/models.mjs +73 -0
  134. package/lib/tui/picker.mjs +18 -0
  135. package/lib/tui/theme.mjs +663 -0
  136. package/lib/tui/title.mjs +7 -0
  137. package/lib/update-channel.mjs +62 -0
  138. package/lib/update.mjs +230 -0
  139. package/lib/webdriver.mjs +82 -0
  140. package/lib/workflow.mjs +88 -0
  141. package/lib/workspace.mjs +39 -0
  142. package/metadata.json +8 -0
  143. package/package.json +94 -4
  144. package/plugins/carthagent/index.ts +3 -0
  145. package/plugins/carthagent/package.json +28 -0
  146. package/plugins/carthagent/src/archive-rpc.ts +46 -0
  147. package/plugins/carthagent/src/bootstrap.ts +38 -0
  148. package/plugins/carthagent/src/index.ts +62 -0
  149. package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
  150. package/plugins/carthagent/src/rpc.ts +76 -0
  151. package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
  152. package/plugins/carthagent/src/server/budgets.ts +21 -0
  153. package/plugins/carthagent/src/server/codegraph.ts +46 -0
  154. package/plugins/carthagent/src/server/delivery.ts +488 -0
  155. package/plugins/carthagent/src/server/device-auth.ts +91 -0
  156. package/plugins/carthagent/src/server/native-delivery.ts +26 -0
  157. package/plugins/carthagent/src/server/orchestration.ts +56 -0
  158. package/plugins/carthagent/src/server/policy.ts +24 -0
  159. package/plugins/carthagent/src/server/providers.ts +424 -0
  160. package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
  161. package/plugins/carthagent/src/server/session-controls.ts +82 -0
  162. package/plugins/carthagent/src/server/telemetry.ts +92 -0
  163. package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
  164. package/plugins/carthagent/src/session-rpc.ts +31 -0
  165. package/plugins/carthagent/src/testing.ts +85 -0
  166. package/plugins/carthagent/src/workspace-rpc.ts +133 -0
  167. package/plugins/carthagent/test-plugin/index.ts +4 -0
  168. package/plugins/carthagent/test-plugin/package.json +1 -0
  169. package/plugins/carthagent/tsconfig.json +19 -0
  170. package/plugins/carthagent/tui.ts +1 -0
  171. package/prompts/delivery.md +7 -0
  172. package/public/index.html +983 -0
  173. package/scenarios/cli/grade_cli.py +69 -0
  174. package/scenarios/cli/scenario.json +21 -0
  175. package/scenarios/game/baseline/minecraft.py +193 -0
  176. package/scenarios/game/context.md +1 -0
  177. package/scenarios/game/grade_minecraft_clone.py +603 -0
  178. package/scenarios/game/requirements.txt +2 -0
  179. package/scenarios/game/scenario.json +33 -0
  180. package/server.mjs +341 -0
  181. package/skills/game-development/SKILL.md +36 -0
  182. package/skills/game-development/assets/ursina_starter.py +98 -0
  183. package/skills/game-development/scripts/verify_ursina.py +80 -0
  184. package/skills/software-delivery/SKILL.md +50 -0
  185. package/skills/typescript-delivery/SKILL.md +69 -0
  186. package/tests/acceptance-review.test.mjs +13 -0
  187. package/tests/auth.test.mjs +234 -0
  188. package/tests/bounded-summary.test.mjs +68 -0
  189. package/tests/browser-bin.test.mjs +91 -0
  190. package/tests/browser.test.mjs +96 -0
  191. package/tests/budget-engine.test.mjs +110 -0
  192. package/tests/budget.test.mjs +23 -0
  193. package/tests/cli.test.mjs +245 -0
  194. package/tests/clipboard.test.mjs +79 -0
  195. package/tests/cloud-client.test.mjs +129 -0
  196. package/tests/cloud-control-plane.test.mjs +144 -0
  197. package/tests/cloud-gateway.test.mjs +147 -0
  198. package/tests/cloud-http.test.mjs +102 -0
  199. package/tests/cloud-ledger.test.mjs +75 -0
  200. package/tests/codegraph-query.test.mjs +66 -0
  201. package/tests/codegraph.test.mjs +142 -0
  202. package/tests/delivery-context.test.mjs +66 -0
  203. package/tests/delivery-review.test.mjs +139 -0
  204. package/tests/delivery.test.mjs +343 -0
  205. package/tests/engine-api.mjs +24 -0
  206. package/tests/experiential-extension.test.mjs +37 -0
  207. package/tests/experiential.test.mjs +300 -0
  208. package/tests/extension.test.mjs +921 -0
  209. package/tests/fixtures/archive-plugin/index.ts +10 -0
  210. package/tests/guidance.test.mjs +78 -0
  211. package/tests/model-registry.test.mjs +68 -0
  212. package/tests/models.test.mjs +70 -0
  213. package/tests/openai-idempotency.test.mjs +99 -0
  214. package/tests/opencode-archive-rpc.test.mjs +82 -0
  215. package/tests/opencode-delivery.test.mjs +231 -0
  216. package/tests/opencode-providers.test.mjs +414 -0
  217. package/tests/opencode-runtime.test.mjs +455 -0
  218. package/tests/opencode-session-controls.test.mjs +115 -0
  219. package/tests/opencode-session-options.test.mjs +26 -0
  220. package/tests/opencode-workspace.test.mjs +339 -0
  221. package/tests/outcome-contract.test.mjs +17 -0
  222. package/tests/picker.test.mjs +34 -0
  223. package/tests/plan-advisor.test.mjs +88 -0
  224. package/tests/plan-review-loop.test.mjs +53 -0
  225. package/tests/planning-advice.test.mjs +27 -0
  226. package/tests/planning-contract.test.mjs +65 -0
  227. package/tests/planning-startup.test.mjs +38 -0
  228. package/tests/planning-view.test.mjs +33 -0
  229. package/tests/planning.test.mjs +223 -0
  230. package/tests/probe-diagnostics.test.mjs +21 -0
  231. package/tests/pull-request-rpc.test.mjs +366 -0
  232. package/tests/review-state.test.mjs +32 -0
  233. package/tests/review.test.mjs +127 -0
  234. package/tests/server.test.mjs +252 -0
  235. package/tests/ship-jev.test.mjs +77 -0
  236. package/tests/ship-telemetry.test.mjs +333 -0
  237. package/tests/standard-workflow.test.mjs +28 -0
  238. package/tests/subscription-models.test.mjs +416 -0
  239. package/tests/subscriptions-extension.test.mjs +27 -0
  240. package/tests/terminal-title.test.mjs +13 -0
  241. package/tests/test_grade_minecraft_clone.py +199 -0
  242. package/tests/test_starter.py +78 -0
  243. package/tests/theme.test.mjs +175 -0
  244. package/tests/tui-copy-paste.test.mjs +54 -0
  245. package/tests/tui.test.mjs +808 -0
  246. package/tests/update-channels.test.mjs +129 -0
  247. package/tests/update.test.mjs +175 -0
  248. package/tests/webdriver.test.mjs +31 -0
  249. package/tests/workflow.test.mjs +47 -0
  250. package/tests/workspace-rpc.test.mjs +236 -0
  251. package/tests/workspace.test.mjs +24 -0
  252. package/tools/benchmark/advisor.test.mjs +7 -0
  253. package/tools/benchmark/advisor.ts +27 -0
  254. package/tools/benchmark/campaign.mjs +45 -0
  255. package/tools/benchmark/diagnose-request.mjs +40 -0
  256. package/tools/benchmark/eval-common.mjs +32 -0
  257. package/tools/benchmark/eval-ledger.mjs +22 -0
  258. package/tools/benchmark/eval-shop.mjs +26 -0
  259. package/tools/benchmark/eval-voxel.mjs +20 -0
  260. package/tools/benchmark/evaluate.mjs +24 -0
  261. package/tools/benchmark/evaluators.test.mjs +13 -0
  262. package/tools/benchmark/fixtures/shop/server.mjs +18 -0
  263. package/tools/benchmark/fixtures/shop/ui.html +8 -0
  264. package/tools/benchmark/freeze.mjs +30 -0
  265. package/tools/benchmark/gateway.mjs +91 -0
  266. package/tools/benchmark/gateway.test.mjs +38 -0
  267. package/tools/benchmark/generator.mjs +11 -0
  268. package/tools/benchmark/generator.test.mjs +26 -0
  269. package/tools/benchmark/guard.test.mjs +133 -0
  270. package/tools/benchmark/guard.ts +136 -0
  271. package/tools/benchmark/native-plugin/index.ts +24 -0
  272. package/tools/benchmark/planning-startup.test.mjs +47 -0
  273. package/tools/benchmark/preflight-live.mjs +31 -0
  274. package/tools/benchmark/preflight.mjs +22 -0
  275. package/tools/benchmark/replay-diagnostic.mjs +23 -0
  276. package/tools/benchmark/runner.mjs +141 -0
  277. package/tools/benchmark/runner.test.mjs +93 -0
  278. package/tools/benchmark/runtime.mjs +23 -0
  279. package/tools/benchmark/selftest.mjs +47 -0
  280. package/tools/benchmark/smoke-jev.mjs +5 -0
  281. package/tools/benchmark/tasks.mjs +20 -0
  282. package/tools/benchmark/transport.mjs +65 -0
  283. package/tools/benchmark/transport.test.mjs +44 -0
  284. package/tools/benchmark/upstream-error.mjs +18 -0
  285. package/tools/benchmark/upstream-error.test.mjs +10 -0
  286. package/tools/browser-check.mjs +109 -0
  287. package/tools/check-syntax.mjs +27 -0
  288. package/tools/evaluate-plan-advisor.mjs +20 -0
  289. package/tools/native-fixture.mjs +194 -0
  290. package/tools/native-pty-smoke.py +108 -0
  291. package/tools/native-report-plugin/index.ts +50 -0
  292. package/tools/native-report-plugin/package.json +7 -0
  293. package/tools/patch-compaction.mjs +24 -0
  294. package/tools/patch-model-catalog.mjs +12 -0
  295. package/tools/patch-print-shutdown.mjs +39 -0
  296. package/tools/repo-pilot/evaluator.cjs +32 -0
  297. package/tools/repo-pilot/reference.mjs +43 -0
  298. package/tools/repo-pilot/runner.mjs +85 -0
  299. package/tools/repo-pilot/sandbox-entry.mjs +12 -0
  300. package/tools/repo-pilot/tasks.mjs +20 -0
  301. package/tools/repo-pilot/type-evaluator.cjs +6 -0
  302. package/tools/test-native-controls.mjs +84 -0
  303. package/tools/test-native-delivery-host.mjs +307 -0
  304. package/tools/test-native-evaluation.mjs +46 -0
  305. package/tools/test-native-host.mjs +385 -0
  306. package/tools/test-native-outcomes.mjs +73 -0
  307. package/tools/test-native-package.mjs +70 -0
  308. package/tools/test-native-pty.mjs +20 -0
  309. package/tools/test-native-report.mjs +88 -0
  310. package/tools/test-native-swe-host.mjs +132 -0
  311. package/tools/test-native-swe.mjs +122 -0
  312. package/tools/test-picker-pty.py +45 -0
  313. package/tools/test-python.mjs +29 -0
  314. package/tools/test-webdriver.mjs +54 -0
  315. package/tools/verify-release-version.mjs +37 -0
  316. package/tools/webdriver-check.mjs +52 -0
  317. package/vendor/agent/bounded-summary.mjs +88 -0
  318. package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
  319. package/vendor/agent/chunks/anthropic.js +75 -0
  320. package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
  321. package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
  322. package/vendor/agent/chunks/chord-context.js +96 -0
  323. package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
  324. package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
  325. package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
  326. package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
  327. package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
  328. package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
  329. package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
  330. package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
  331. package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
  332. package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
  333. package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
  334. package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
  335. package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
  336. package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
  337. package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
  338. package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
  339. package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
  340. package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
  341. package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
  342. package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
  343. package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
  344. package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
  345. package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
  346. package/vendor/agent/chunks/github-copilot.js +2 -0
  347. package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
  348. package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
  349. package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
  350. package/vendor/agent/chunks/image-resize-worker.js +2 -0
  351. package/vendor/agent/chunks/kimi-coding.js +2 -0
  352. package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
  353. package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
  354. package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
  355. package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
  356. package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
  357. package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
  358. package/vendor/agent/chunks/openai-codex.js +75 -0
  359. package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
  360. package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
  361. package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
  362. package/vendor/agent/chunks/openrouter.js +75 -0
  363. package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
  364. package/vendor/agent/chunks/radius.js +75 -0
  365. package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
  366. package/vendor/agent/chunks/xai.js +2 -0
  367. package/vendor/agent/cli.js +3 -0
  368. package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
  369. package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
  370. package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
  371. package/vendor/agent/index.js +2 -0
  372. package/vendor/agent/modes/interactive/theme/dark.json +91 -0
  373. package/vendor/agent/modes/interactive/theme/light.json +90 -0
  374. package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
  375. package/vendor/agent/package.json +14 -0
  376. package/vendor/agent/rpc-entry.js +3 -0
  377. package/vendor/agent/theme/dark.json +91 -0
  378. package/vendor/agent/theme/light.json +90 -0
  379. package/vendor/agent/theme/theme-schema.json +356 -0
@@ -0,0 +1,350 @@
1
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { CloudLedger } from './ledger.mjs';
3
+
4
+ const TRIAL_NANO_USD = 1_000_000_000;
5
+ const BUILDER_MONTHLY_NANO_USD = 10_000_000_000;
6
+ const ACCESS_TTL_SECONDS = 15 * 60;
7
+ const REFRESH_TTL_SECONDS = 30 * 24 * 60 * 60;
8
+ const DEVICE_TTL_SECONDS = 10 * 60;
9
+ const DEVICE_INTERVAL_SECONDS = 5;
10
+
11
+ const b64url = value => Buffer.from(value).toString('base64url');
12
+ const hash = value => createHash('sha256').update(value).digest('hex');
13
+ const epoch = date => Math.floor(date.getTime() / 1000);
14
+
15
+ function secureEqual(left, right) {
16
+ const a = Buffer.from(left || '');
17
+ const b = Buffer.from(right || '');
18
+ return a.length === b.length && timingSafeEqual(a, b);
19
+ }
20
+
21
+ export function verifyStripeSignature(payload, signature, secret, { now = Date.now(), toleranceSeconds = 300 } = {}) {
22
+ if (!secret) throw new Error('Stripe webhook secret is not configured');
23
+ const parts = Object.fromEntries(String(signature || '').split(',').map(item => item.split('=', 2)));
24
+ const timestamp = Number(parts.t);
25
+ if (!Number.isFinite(timestamp) || Math.abs(Math.floor(now / 1000) - timestamp) > toleranceSeconds) throw new Error('Invalid Stripe signature timestamp');
26
+ const expected = createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex');
27
+ if (!secureEqual(expected, parts.v1)) throw new Error('Invalid Stripe signature');
28
+ return true;
29
+ }
30
+
31
+ export class CloudControlPlane {
32
+ constructor({
33
+ ledger = new CloudLedger(),
34
+ clock = () => new Date(),
35
+ id = randomUUID,
36
+ random = size => randomBytes(size),
37
+ tokenSecret = process.env.CARTHAGENT_CLOUD_TOKEN_SECRET || randomBytes(32).toString('hex'),
38
+ publicUrl = process.env.CARTHAGENT_CLOUD_URL || 'https://cloud.carthagent.xyz',
39
+ stripe = null,
40
+ stripePriceId = process.env.STRIPE_BUILDER_PRICE_ID || '',
41
+ stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET || '',
42
+ planCatalog = null,
43
+ } = {}) {
44
+ this.ledger = ledger;
45
+ this.db = ledger.db;
46
+ this.clock = clock;
47
+ this.id = id;
48
+ this.random = random;
49
+ this.tokenSecret = tokenSecret;
50
+ this.publicUrl = publicUrl.replace(/\/$/, '');
51
+ this.stripe = stripe;
52
+ this.stripePriceId = stripePriceId;
53
+ this.planCatalog = { ...(planCatalog || {}), ...(stripePriceId ? { builder: stripePriceId } : {}) };
54
+ this.stripeWebhookSecret = stripeWebhookSecret;
55
+ this.db.exec(`
56
+ CREATE TABLE IF NOT EXISTS cloud_users (
57
+ id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL
58
+ );
59
+ CREATE TABLE IF NOT EXISTS account_members (
60
+ account_id TEXT NOT NULL REFERENCES accounts(id), user_id TEXT NOT NULL REFERENCES cloud_users(id),
61
+ role TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY(account_id, user_id)
62
+ );
63
+ CREATE TABLE IF NOT EXISTS trial_claims (
64
+ account_id TEXT PRIMARY KEY REFERENCES accounts(id), claimed_at TEXT NOT NULL
65
+ );
66
+ CREATE TABLE IF NOT EXISTS device_authorizations (
67
+ id TEXT PRIMARY KEY, device_code_hash TEXT NOT NULL UNIQUE, user_code TEXT NOT NULL UNIQUE,
68
+ client_name TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('pending','approved','denied','consumed')),
69
+ account_id TEXT REFERENCES accounts(id), expires_at INTEGER NOT NULL, interval_seconds INTEGER NOT NULL,
70
+ created_at TEXT NOT NULL, approved_at TEXT
71
+ );
72
+ CREATE TABLE IF NOT EXISTS cli_sessions (
73
+ id TEXT PRIMARY KEY, account_id TEXT NOT NULL REFERENCES accounts(id), refresh_token_hash TEXT NOT NULL UNIQUE,
74
+ scopes TEXT NOT NULL, client_name TEXT NOT NULL DEFAULT 'Carthagent CLI', expires_at INTEGER NOT NULL,
75
+ revoked_at TEXT, created_at TEXT NOT NULL, last_used_at TEXT
76
+ );
77
+ CREATE TABLE IF NOT EXISTS subscriptions (
78
+ account_id TEXT PRIMARY KEY REFERENCES accounts(id), stripe_customer_id TEXT UNIQUE,
79
+ stripe_subscription_id TEXT UNIQUE, stripe_price_id TEXT, status TEXT NOT NULL,
80
+ current_period_start INTEGER, current_period_end INTEGER, updated_at TEXT NOT NULL
81
+ );
82
+ CREATE TABLE IF NOT EXISTS stripe_events (
83
+ id TEXT PRIMARY KEY, type TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, processed_at TEXT
84
+ );
85
+ `);
86
+ }
87
+
88
+ now() { return this.clock(); }
89
+ nowIso() { return this.now().toISOString(); }
90
+
91
+ createUserAccount({ email, accountId = this.id(), userId = this.id() }) {
92
+ if (!/^\S+@\S+\.\S+$/.test(email || '')) throw new Error('valid email is required');
93
+ this.db.exec('BEGIN IMMEDIATE');
94
+ try {
95
+ this.ledger.createAccount(accountId);
96
+ this.db.prepare('INSERT INTO cloud_users (id, email, created_at) VALUES (?, ?, ?)').run(userId, email.toLowerCase(), this.nowIso());
97
+ this.db.prepare('INSERT INTO account_members (account_id, user_id, role, created_at) VALUES (?, ?, ?, ?)').run(accountId, userId, 'owner', this.nowIso());
98
+ this.db.exec('COMMIT');
99
+ } catch (error) { this.db.exec('ROLLBACK'); throw error; }
100
+ return { accountId, userId, email: email.toLowerCase() };
101
+ }
102
+
103
+ claimTrial(accountId) {
104
+ const existing = this.db.prepare('SELECT * FROM trial_claims WHERE account_id = ?').get(accountId);
105
+ if (existing) return { claimed: false, availableNanoUsd: this.ledger.available(accountId) };
106
+ this.db.prepare('INSERT INTO trial_claims (account_id, claimed_at) VALUES (?, ?)').run(accountId, this.nowIso());
107
+ try {
108
+ this.ledger.grant({ accountId, amountNanoUsd: TRIAL_NANO_USD, idempotencyKey: `trial:${accountId}`, source: 'verified_trial', metadata: { grantType: 'trial' } });
109
+ } catch (error) {
110
+ this.db.prepare('DELETE FROM trial_claims WHERE account_id = ?').run(accountId);
111
+ throw error;
112
+ }
113
+ return { claimed: true, availableNanoUsd: this.ledger.available(accountId) };
114
+ }
115
+
116
+ createDeviceAuthorization({ clientName = 'Carthagent CLI' } = {}) {
117
+ const deviceCode = `ctgd_${this.random(32).toString('base64url')}`;
118
+ const raw = this.random(5).toString('hex').toUpperCase();
119
+ const userCode = `${raw.slice(0, 5)}-${raw.slice(5)}`;
120
+ const expiresAt = epoch(this.now()) + DEVICE_TTL_SECONDS;
121
+ this.db.prepare(`INSERT INTO device_authorizations
122
+ (id, device_code_hash, user_code, client_name, status, expires_at, interval_seconds, created_at)
123
+ VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)`).run(this.id(), hash(deviceCode), userCode, clientName.slice(0, 120), expiresAt, DEVICE_INTERVAL_SECONDS, this.nowIso());
124
+ return {
125
+ deviceCode, userCode,
126
+ verificationUri: `${this.publicUrl}/activate`,
127
+ verificationUriComplete: `${this.publicUrl}/activate?code=${encodeURIComponent(userCode)}`,
128
+ expiresIn: DEVICE_TTL_SECONDS,
129
+ interval: DEVICE_INTERVAL_SECONDS,
130
+ };
131
+ }
132
+
133
+ approveDeviceAuthorization({ userCode, accountId, approve = true }) {
134
+ const row = this.db.prepare('SELECT * FROM device_authorizations WHERE user_code = ?').get(String(userCode || '').toUpperCase());
135
+ if (!row) throw new Error('invalid_user_code');
136
+ if (row.expires_at <= epoch(this.now())) throw new Error('expired_token');
137
+ if (row.status !== 'pending') throw new Error(`authorization_${row.status}`);
138
+ this.db.prepare('UPDATE device_authorizations SET status = ?, account_id = ?, approved_at = ? WHERE id = ?').run(approve ? 'approved' : 'denied', approve ? accountId : null, this.nowIso(), row.id);
139
+ if (approve) this.claimTrial(accountId);
140
+ return { approved: approve, clientName: row.client_name };
141
+ }
142
+
143
+ signAccessToken(claims) {
144
+ const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
145
+ const payload = b64url(JSON.stringify(claims));
146
+ const signature = createHmac('sha256', this.tokenSecret).update(`${header}.${payload}`).digest('base64url');
147
+ return `${header}.${payload}.${signature}`;
148
+ }
149
+
150
+ verifyAccessToken(token, requiredScope) {
151
+ const [header, payload, signature] = String(token || '').split('.');
152
+ if (!header || !payload || !signature) throw new Error('invalid_token');
153
+ const expected = createHmac('sha256', this.tokenSecret).update(`${header}.${payload}`).digest('base64url');
154
+ if (!secureEqual(expected, signature)) throw new Error('invalid_token');
155
+ const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
156
+ if (claims.exp <= epoch(this.now())) throw new Error('expired_token');
157
+ const session = this.db.prepare('SELECT * FROM cli_sessions WHERE id = ?').get(claims.sid);
158
+ if (!session || session.revoked_at) throw new Error('revoked_token');
159
+ if (requiredScope && !String(claims.scope || '').split(' ').includes(requiredScope)) throw new Error('insufficient_scope');
160
+ this.db.prepare('UPDATE cli_sessions SET last_used_at = ? WHERE id = ?').run(this.nowIso(), claims.sid);
161
+ return claims;
162
+ }
163
+
164
+ issueSession(accountId, scopes = ['models:read', 'gateway:invoke', 'account:read'], clientName = 'Carthagent CLI') {
165
+ const sessionId = this.id();
166
+ const refreshToken = `ctgr_${this.random(32).toString('base64url')}`;
167
+ const now = epoch(this.now());
168
+ this.db.prepare(`INSERT INTO cli_sessions
169
+ (id, account_id, refresh_token_hash, scopes, client_name, expires_at, created_at, last_used_at)
170
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(sessionId, accountId, hash(refreshToken), scopes.join(' '), String(clientName || 'Carthagent CLI').slice(0, 120), now + REFRESH_TTL_SECONDS, this.nowIso(), this.nowIso());
171
+ return {
172
+ accessToken: this.signAccessToken({ sub: accountId, sid: sessionId, scope: scopes.join(' '), iat: now, exp: now + ACCESS_TTL_SECONDS }),
173
+ tokenType: 'Bearer', expiresIn: ACCESS_TTL_SECONDS, refreshToken, scope: scopes.join(' '),
174
+ };
175
+ }
176
+
177
+ pollDeviceToken({ deviceCode }) {
178
+ const row = this.db.prepare('SELECT * FROM device_authorizations WHERE device_code_hash = ?').get(hash(deviceCode || ''));
179
+ if (!row) throw new Error('invalid_grant');
180
+ if (row.expires_at <= epoch(this.now())) throw new Error('expired_token');
181
+ if (row.status === 'pending') throw new Error('authorization_pending');
182
+ if (row.status === 'denied') throw new Error('access_denied');
183
+ if (row.status === 'consumed') throw new Error('invalid_grant');
184
+ const tokens = this.issueSession(row.account_id, undefined, row.client_name);
185
+ this.db.prepare(`UPDATE device_authorizations SET status = 'consumed' WHERE id = ?`).run(row.id);
186
+ return tokens;
187
+ }
188
+
189
+ refreshSession({ refreshToken }) {
190
+ const row = this.db.prepare('SELECT * FROM cli_sessions WHERE refresh_token_hash = ?').get(hash(refreshToken || ''));
191
+ if (!row || row.revoked_at || row.expires_at <= epoch(this.now())) throw new Error('invalid_grant');
192
+ const replacement = `ctgr_${this.random(32).toString('base64url')}`;
193
+ const now = epoch(this.now());
194
+ this.db.prepare('UPDATE cli_sessions SET refresh_token_hash = ?, expires_at = ?, last_used_at = ? WHERE id = ?').run(hash(replacement), now + REFRESH_TTL_SECONDS, this.nowIso(), row.id);
195
+ const scopes = row.scopes.split(' ').filter(Boolean);
196
+ return { accessToken: this.signAccessToken({ sub: row.account_id, sid: row.id, scope: row.scopes, iat: now, exp: now + ACCESS_TTL_SECONDS }), tokenType: 'Bearer', expiresIn: ACCESS_TTL_SECONDS, refreshToken: replacement, scope: scopes.join(' ') };
197
+ }
198
+
199
+ revokeSession({ sessionId }) {
200
+ const result = this.db.prepare('UPDATE cli_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL').run(this.nowIso(), sessionId);
201
+ return { revoked: result.changes > 0 };
202
+ }
203
+
204
+ revokeAccountSession({ accountId, sessionId }) {
205
+ const result = this.db.prepare('UPDATE cli_sessions SET revoked_at = ? WHERE id = ? AND account_id = ? AND revoked_at IS NULL').run(this.nowIso(), sessionId, accountId);
206
+ if (!result.changes) throw new Error('session_not_found');
207
+ return { revoked: true, sessionId };
208
+ }
209
+
210
+ listSessions(accountId) {
211
+ return this.db.prepare('SELECT id, scopes, client_name, expires_at, created_at, last_used_at, revoked_at FROM cli_sessions WHERE account_id = ? ORDER BY created_at DESC').all(accountId);
212
+ }
213
+
214
+ account(accountId) {
215
+ const user = this.db.prepare(`SELECT u.email FROM cloud_users u JOIN account_members m ON m.user_id = u.id
216
+ WHERE m.account_id = ? AND m.role = 'owner' ORDER BY m.created_at LIMIT 1`).get(accountId);
217
+ return {
218
+ accountId,
219
+ email: user?.email || null,
220
+ balance: { currency: 'USD', availableNanoUsd: this.ledger.available(accountId) },
221
+ grants: this.grants(accountId),
222
+ subscription: this.subscription(accountId),
223
+ sessions: this.listSessions(accountId),
224
+ };
225
+ }
226
+
227
+ grants(accountId) {
228
+ return this.db.prepare(`SELECT id, original_nano_usd, remaining_nano_usd, expires_at, source, created_at
229
+ FROM credit_grants WHERE account_id = ? ORDER BY created_at DESC, id DESC`).all(accountId).map(row => ({
230
+ id: row.id,
231
+ originalNanoUsd: Number(row.original_nano_usd),
232
+ remainingNanoUsd: Number(row.remaining_nano_usd),
233
+ expiresAt: row.expires_at,
234
+ source: row.source,
235
+ createdAt: row.created_at,
236
+ }));
237
+ }
238
+
239
+ usage(accountId, { limit = 50 } = {}) {
240
+ const bounded = Math.max(1, Math.min(200, Number.isSafeInteger(limit) ? limit : 50));
241
+ return this.db.prepare(`SELECT amount_nano_usd, gateway_request_id, gateway_attempt_id, metadata_json, created_at
242
+ FROM ledger_entries WHERE account_id = ? AND kind = 'charge' ORDER BY created_at DESC, rowid DESC LIMIT ?`).all(accountId, bounded).map(row => {
243
+ let metadata = {};
244
+ try { metadata = JSON.parse(row.metadata_json); } catch { /* invalid historical metadata */ }
245
+ return {
246
+ createdAt: row.created_at,
247
+ alias: typeof metadata.alias === 'string' ? metadata.alias : null,
248
+ inputTokens: Number.isSafeInteger(metadata.inputTokens) ? metadata.inputTokens : null,
249
+ outputTokens: Number.isSafeInteger(metadata.outputTokens) ? metadata.outputTokens : null,
250
+ status: typeof metadata.status === 'string' ? metadata.status : null,
251
+ chargeNanoUsd: Math.abs(Number(row.amount_nano_usd)),
252
+ gatewayRequestId: row.gateway_request_id,
253
+ gatewayAttemptId: row.gateway_attempt_id,
254
+ };
255
+ });
256
+ }
257
+
258
+ subscription(accountId) {
259
+ return this.db.prepare('SELECT * FROM subscriptions WHERE account_id = ?').get(accountId) || { account_id: accountId, status: 'none' };
260
+ }
261
+
262
+ checkoutPrice(selector) {
263
+ const wanted = String(selector || '').trim();
264
+ if (!wanted) return this.stripePriceId || null;
265
+ const lowered = wanted.toLowerCase();
266
+ for (const [name, price] of Object.entries(this.planCatalog)) {
267
+ if (price === wanted || name.toLowerCase() === lowered) return price;
268
+ }
269
+ return null;
270
+ }
271
+
272
+ async createCheckout({ accountId, successUrl, cancelUrl, priceId }) {
273
+ if (!this.stripe) throw new Error('billing_not_configured');
274
+ const price = this.checkoutPrice(priceId);
275
+ if (!price) throw new Error('billing_not_configured');
276
+ const current = this.subscription(accountId);
277
+ const session = await this.stripe.createCheckoutSession({
278
+ mode: 'subscription', customer: current.stripe_customer_id || undefined,
279
+ client_reference_id: accountId, line_items: [{ price, quantity: 1 }],
280
+ success_url: successUrl, cancel_url: cancelUrl, metadata: { accountId },
281
+ });
282
+ return { url: session.url, id: session.id };
283
+ }
284
+
285
+ async createPortal({ accountId, returnUrl }) {
286
+ if (!this.stripe) throw new Error('billing_not_configured');
287
+ const current = this.subscription(accountId);
288
+ if (!current.stripe_customer_id) throw new Error('stripe_customer_not_found');
289
+ const session = await this.stripe.createPortalSession({ customer: current.stripe_customer_id, return_url: returnUrl });
290
+ return { url: session.url, id: session.id };
291
+ }
292
+
293
+ upsertSubscription(accountId, object) {
294
+ const period = object.items?.data?.[0]?.current_period_end ? object.items.data[0] : object;
295
+ this.db.prepare(`INSERT INTO subscriptions
296
+ (account_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, status, current_period_start, current_period_end, updated_at)
297
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
298
+ ON CONFLICT(account_id) DO UPDATE SET stripe_customer_id=excluded.stripe_customer_id,
299
+ stripe_subscription_id=excluded.stripe_subscription_id, stripe_price_id=excluded.stripe_price_id,
300
+ status=excluded.status, current_period_start=excluded.current_period_start,
301
+ current_period_end=excluded.current_period_end, updated_at=excluded.updated_at`).run(
302
+ accountId, object.customer || null, object.id || null, object.items?.data?.[0]?.price?.id || this.stripePriceId || null,
303
+ object.status || 'unknown', period.current_period_start || null, period.current_period_end || null, this.nowIso()
304
+ );
305
+ }
306
+
307
+ processStripeEvent(event) {
308
+ const inserted = this.db.prepare(`INSERT OR IGNORE INTO stripe_events (id, type, status, created_at) VALUES (?, ?, 'processing', ?)`).run(event.id, event.type, this.nowIso());
309
+ if (!inserted.changes) return { duplicate: true };
310
+ try {
311
+ const object = event.data?.object || {};
312
+ const accountId = object.metadata?.accountId || object.client_reference_id || object.subscription_details?.metadata?.accountId;
313
+ if (event.type === 'checkout.session.completed' && accountId) {
314
+ const subscription = { id: object.subscription, customer: object.customer, status: object.payment_status === 'paid' ? 'active' : 'incomplete' };
315
+ this.upsertSubscription(accountId, subscription);
316
+ } else if (event.type.startsWith('customer.subscription.') && accountId) {
317
+ this.upsertSubscription(accountId, object);
318
+ } else if (event.type === 'invoice.paid' && accountId) {
319
+ this.upsertSubscription(accountId, { id: object.subscription, customer: object.customer, status: 'active', current_period_start: object.period_start, current_period_end: object.period_end });
320
+ this.ledger.grant({
321
+ accountId, amountNanoUsd: BUILDER_MONTHLY_NANO_USD,
322
+ idempotencyKey: `stripe-invoice:${object.id}`, expiresAt: object.period_end ? new Date(object.period_end * 1000).toISOString() : null,
323
+ source: 'subscription', metadata: { invoiceId: object.id, subscriptionId: object.subscription || '' },
324
+ });
325
+ } else if (event.type === 'invoice.payment_failed' && accountId) {
326
+ this.upsertSubscription(accountId, { id: object.subscription, customer: object.customer, status: 'past_due' });
327
+ } else if ((event.type === 'charge.refunded' || event.type === 'charge.dispute.created') && accountId) {
328
+ const nano = Math.round(Number(object.amount_refunded || object.amount || 0) * 10_000_000);
329
+ if (nano > 0) this.ledger.appendEntry({ accountId, kind: 'refund', amountNanoUsd: -nano, idempotencyKey: `stripe-reversal:${event.id}`, metadata: { stripeObjectId: object.id, eventType: event.type } });
330
+ }
331
+ this.db.prepare(`UPDATE stripe_events SET status = 'processed', processed_at = ? WHERE id = ?`).run(this.nowIso(), event.id);
332
+ return { duplicate: false, processed: true };
333
+ } catch (error) {
334
+ this.db.prepare('DELETE FROM stripe_events WHERE id = ?').run(event.id);
335
+ throw error;
336
+ }
337
+ }
338
+
339
+ handleStripeWebhook({ payload, signature }) {
340
+ verifyStripeSignature(payload, signature, this.stripeWebhookSecret, { now: this.now().getTime() });
341
+ return this.processStripeEvent(JSON.parse(payload));
342
+ }
343
+ }
344
+
345
+ export const CLOUD_PLAN = Object.freeze({
346
+ trialNanoUsd: TRIAL_NANO_USD,
347
+ builderMonthlyNanoUsd: BUILDER_MONTHLY_NANO_USD,
348
+ accessTtlSeconds: ACCESS_TTL_SECONDS,
349
+ refreshTtlSeconds: REFRESH_TTL_SECONDS,
350
+ });