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
package/lib/budget.mjs ADDED
@@ -0,0 +1,37 @@
1
+ // Session accounting is deliberately independent of delivery-plan revisions.
2
+ export function budgetLimits(getFlag) {
3
+ const limits = {};
4
+ for (const [key, flag] of [['tools', 'tools'], ['seconds', 'seconds'], ['repairs', 'repairs']]) {
5
+ const raw = getFlag(`delivery-max-${flag}`) ?? '0';
6
+ if (!/^\d+$/.test(String(raw)) || !Number.isSafeInteger(Number(raw)) || Number(raw) > 1000000) throw Error(`delivery-max-${flag} must be an integer from 0 to 1000000`);
7
+ limits[key] = Number(raw);
8
+ }
9
+ return limits;
10
+ }
11
+ export function newBudget(limits, now = Date.now()) {
12
+ return { startedAt: now, limits, tools: 0, repairs: 0, stopped: null };
13
+ }
14
+ export function budgetReason(budget, action, now = Date.now()) {
15
+ if (!budget) return null;
16
+ if (budget.stopped) return budget.stopped;
17
+ if (budget.limits.seconds && now - budget.startedAt >= budget.limits.seconds * 1000) return 'elapsed-time';
18
+ if (action === 'tool' && budget.limits.tools && budget.tools >= budget.limits.tools) return 'tool-calls';
19
+ if (action === 'repair' && budget.limits.repairs && budget.repairs >= budget.limits.repairs) return 'repair-rounds';
20
+ return null;
21
+ }
22
+
23
+ export function validateBudgetOptions(opts, { allowWithoutDelivery = false } = {}) {
24
+ const keys = ['maxTools', 'maxSeconds', 'maxRepairs'];
25
+ if (!allowWithoutDelivery && opts.delivery === false && keys.some(key => opts[key] !== undefined)) {
26
+ throw Error('Budget options require the delivery extension');
27
+ }
28
+ budgetLimits(flag => opts[{ 'delivery-max-tools': 'maxTools', 'delivery-max-seconds': 'maxSeconds', 'delivery-max-repairs': 'maxRepairs' }[flag]]);
29
+ }
30
+ export function budgetSnapshot(budget, now = Date.now()) {
31
+ if (!budget) return { enabled: false };
32
+ return { ...budget, remaining: {
33
+ tools: budget.limits.tools ? Math.max(0, budget.limits.tools - budget.tools) : null,
34
+ repairs: budget.limits.repairs ? Math.max(0, budget.limits.repairs - budget.repairs) : null,
35
+ seconds: budget.limits.seconds ? Math.max(0, Math.ceil((budget.startedAt + budget.limits.seconds * 1000 - now) / 1000)) : null
36
+ }, reason: budgetReason(budget, undefined, now) };
37
+ }
package/lib/cdp.mjs ADDED
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Minimal Chrome DevTools Protocol client for the bundled headless browser.
3
+ * Replaces the geckodriver/WebDriver tier: chrome-headless-shell is launched
4
+ * with --remote-debugging-port=0 and driven over the DevTools websocket —
5
+ * navigation, DOM assertions, clicks, screenshots and console-error capture
6
+ * with a single binary and zero npm deps.
7
+ */
8
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { spawn } from 'node:child_process';
11
+ import { tmpdir } from 'node:os';
12
+ import { findChromium, ensureBundledBrowser, isSandboxFailure } from './browser-bin.mjs';
13
+
14
+ const MAX_ERRORS = 50;
15
+
16
+ /** Spawn the headless shell once and return its browser-level DevTools ws URL. */
17
+ function spawnShell(binary, extraArgs, { timeoutMs = 30000 } = {}) {
18
+ const profile = mkdtempSync(join(tmpdir(), 'carthagent-chrome-'));
19
+ const child = spawn(binary, [
20
+ ...extraArgs,
21
+ '--remote-debugging-port=0', '--user-data-dir=' + profile,
22
+ '--no-first-run', '--no-default-browser-check', '--disable-extensions',
23
+ '--mute-audio', 'about:blank',
24
+ ], { stdio: ['ignore', 'ignore', 'pipe'] });
25
+ let stderr = '';
26
+ child.stderr.on('data', d => { stderr += d; });
27
+ return new Promise((resolve, reject) => {
28
+ const fail = e => { child.kill(); rmSync(profile, { recursive: true, force: true }); reject(e); };
29
+ const timer = setTimeout(() => fail(Error(`browser did not expose DevTools within ${timeoutMs}ms: ${stderr.slice(-500)}`)), timeoutMs);
30
+ child.stderr.on('data', () => {
31
+ const m = stderr.match(/DevTools listening on (ws:\/\/\S+)/);
32
+ if (m) {
33
+ clearTimeout(timer);
34
+ child.removeAllListeners('error');
35
+ child.removeAllListeners('close');
36
+ resolve({ child, wsUrl: m[1], profile, stderr: () => stderr });
37
+ }
38
+ });
39
+ child.on('error', e => { clearTimeout(timer); fail(e); });
40
+ child.on('close', () => { clearTimeout(timer); fail(Error(`browser exited before DevTools opened: ${stderr.slice(-500)}`)); });
41
+ });
42
+ }
43
+
44
+ /** Spawn the shell, retrying once with --no-sandbox when the environment
45
+ * blocks Chromium's process sandbox (Ubuntu >=23.10 AppArmor, containers). */
46
+ async function launchBrowser(options = {}) {
47
+ let binary = findChromium();
48
+ if (!binary) binary = await ensureBundledBrowser();
49
+ try {
50
+ return await spawnShell(binary, [], options);
51
+ } catch (e) {
52
+ if (e?.message && isSandboxFailure(e.message)) {
53
+ return await spawnShell(binary, ['--no-sandbox'], options);
54
+ }
55
+ throw e;
56
+ }
57
+ }
58
+
59
+ /** Minimal CDP session over one websocket, scoped to a page target. */
60
+ async function connectSession(wsUrl, { timeoutMs = 15000 } = {}) {
61
+ const socket = new WebSocket(wsUrl);
62
+ const pending = new Map();
63
+ const errors = [];
64
+ let dropped = 0;
65
+ let sequence = 0;
66
+ let sessionId;
67
+ let closed = false;
68
+ socket.addEventListener('close', () => { closed = true; });
69
+ socket.addEventListener('message', event => {
70
+ let message;
71
+ try { message = JSON.parse(event.data); } catch { return; }
72
+ if (message.id !== undefined) {
73
+ const handler = pending.get(message.id);
74
+ if (handler) { pending.delete(message.id); handler(message); }
75
+ return;
76
+ }
77
+ const { method, params } = message;
78
+ let entry;
79
+ if (method === 'Runtime.consoleAPICalled' && params.type === 'error') {
80
+ entry = { type: 'console', text: (params.args || []).map(a => a.value ?? a.description ?? '').join(' ').slice(0, 2000) };
81
+ } else if (method === 'Runtime.exceptionThrown') {
82
+ const d = params.exceptionDetails || {};
83
+ entry = { type: 'exception', text: String(d.exception?.description || d.text || 'uncaught exception').slice(0, 2000),
84
+ stack: (d.stackTrace?.callFrames || []).slice(0, 8).map(f => ({ url: String(f.url).slice(0, 500), lineNumber: f.lineNumber, columnNumber: f.columnNumber })) };
85
+ } else if (method === 'Log.entryAdded' && params.entry.level === 'error') {
86
+ entry = { type: params.entry.source, text: String(params.entry.text ?? '').slice(0, 2000) };
87
+ }
88
+ if (!entry) return;
89
+ if (errors.length === MAX_ERRORS) { dropped++; return; }
90
+ errors.push(entry);
91
+ });
92
+ await new Promise((resolve, reject) => {
93
+ const timer = setTimeout(() => reject(Error('DevTools connection timed out')), timeoutMs);
94
+ socket.addEventListener('open', () => { clearTimeout(timer); resolve(); }, { once: true });
95
+ socket.addEventListener('error', () => { clearTimeout(timer); reject(Error('DevTools connection failed')); }, { once: true });
96
+ });
97
+ function send(method, params = {}, session) {
98
+ if (closed) return Promise.reject(Error('DevTools connection closed'));
99
+ const id = ++sequence;
100
+ return new Promise((resolve, reject) => {
101
+ const timer = setTimeout(() => { pending.delete(id); reject(Error(`CDP ${method} timed out`)); }, timeoutMs);
102
+ pending.set(id, message => {
103
+ clearTimeout(timer);
104
+ if (message.error) reject(Error(`CDP ${method}: ${message.error.message}`));
105
+ else resolve(message.result);
106
+ });
107
+ socket.send(JSON.stringify(session ? { id, method, params, sessionId: session } : { id, method, params }));
108
+ });
109
+ }
110
+ const { targetId } = await send('Target.createTarget', { url: 'about:blank' });
111
+ const attached = await send('Target.attachToTarget', { targetId, flatten: true });
112
+ sessionId = attached.sessionId;
113
+ const cmd = (method, params) => send(method, params, sessionId);
114
+ await Promise.all([cmd('Page.enable'), cmd('Runtime.enable'), cmd('Log.enable'), cmd('Page.setLifecycleEventsEnabled', { enabled: true })]);
115
+ return {
116
+ send, cmd, sessionId, targetId,
117
+ errors, dropped: () => dropped,
118
+ navigate(url) {
119
+ // Resolve on loadEventFired; caller applies the navigation timeout.
120
+ const loaded = new Promise(resolve => {
121
+ const listener = event => {
122
+ let message;
123
+ try { message = JSON.parse(event.data); } catch { return; }
124
+ if (message.method === 'Page.loadEventFired' && message.sessionId === sessionId) {
125
+ socket.removeEventListener('message', listener);
126
+ resolve();
127
+ }
128
+ };
129
+ socket.addEventListener('message', listener);
130
+ });
131
+ return { loaded, navigating: cmd('Page.navigate', { url }) };
132
+ },
133
+ close() { socket.close(); },
134
+ };
135
+ }
136
+
137
+ function evaluateExpression(cmd, expression) {
138
+ return cmd('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: false })
139
+ .then(r => {
140
+ if (r.exceptionDetails) throw Object.assign(Error(`page eval failed: ${r.exceptionDetails.text}`), { webdriverCode: 'eval error' });
141
+ return r.result?.value;
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Real-browser check over CDP — mirrors the webdriverCheck contract:
147
+ * navigate, poll selector/text assertions, click once, screenshot, then fail on
148
+ * captured console/page errors. Categories: launch, capture, navigation,
149
+ * assertion, interaction, screenshot, cleanup.
150
+ */
151
+ export async function cdpCheck({ url, steps = [], screenshot, waitMs = 5000 }) {
152
+ if (!Number.isInteger(waitMs) || waitMs < 1 || waitMs > 60000) throw Object.assign(Error('waitMs must be an integer from 1 to 60000'), { category: 'configuration' });
153
+ let phase = 'launch';
154
+ let stepIndex;
155
+ let currentStep;
156
+ let failure;
157
+ let browser;
158
+ try {
159
+ browser = await launchBrowser();
160
+ } catch (error) {
161
+ throw Object.assign(error, { category: 'launch' });
162
+ }
163
+ let session;
164
+ try {
165
+ phase = 'capture';
166
+ session = await connectSession(browser.wsUrl);
167
+ phase = 'navigation';
168
+ const { loaded, navigating } = session.navigate(url);
169
+ await Promise.race([
170
+ Promise.all([navigating, loaded]),
171
+ new Promise((_, reject) => setTimeout(() => reject(Error('page load timed out')), 15000)),
172
+ ]);
173
+ for (const [index, step] of steps.entries()) {
174
+ phase = 'assertion'; stepIndex = index; currentStep = step;
175
+ const deadline = Date.now() + waitMs;
176
+ const sel = JSON.stringify(step.selector);
177
+ for (;;) {
178
+ try {
179
+ if (step.text !== undefined) {
180
+ const text = await evaluateExpression(session.cmd, `document.querySelector(${sel})?.innerText ?? null`);
181
+ if (text == null) throw Object.assign(Error(`${step.selector}: not found`), { webdriverCode: 'no such element' });
182
+ if (!String(text).includes(step.text)) throw Object.assign(Error(`${step.selector}: expected ${JSON.stringify(step.text)}, got ${JSON.stringify(String(text).slice(0, 200))}`), { webdriverCode: 'text mismatch' });
183
+ } else {
184
+ const found = await evaluateExpression(session.cmd, `!!document.querySelector(${sel})`);
185
+ if (!found) throw Object.assign(Error(`${step.selector}: not found`), { webdriverCode: 'no such element' });
186
+ }
187
+ break;
188
+ } catch (error) {
189
+ if (!['no such element', 'text mismatch', 'eval error'].includes(error.webdriverCode) || Date.now() >= deadline) throw error;
190
+ await new Promise(r => setTimeout(r, Math.min(100, Math.max(1, deadline - Date.now()))));
191
+ }
192
+ }
193
+ if (step.action === 'click') {
194
+ phase = 'interaction';
195
+ await evaluateExpression(session.cmd, `document.querySelector(${sel})?.click()`);
196
+ }
197
+ }
198
+ stepIndex = undefined; currentStep = undefined; phase = 'screenshot';
199
+ if (screenshot) {
200
+ const shot = await session.cmd('Page.captureScreenshot', { format: 'png' });
201
+ mkdirSync(dirname(screenshot), { recursive: true });
202
+ writeFileSync(screenshot, Buffer.from(shot.data, 'base64'));
203
+ }
204
+ } catch (error) {
205
+ failure = Object.assign(error, { category: phase, stepIndex, selector: currentStep?.selector, expected: currentStep?.text });
206
+ }
207
+ if (session) {
208
+ try {
209
+ // Round trip on the same channel before consuming the observed log.
210
+ await session.cmd('Runtime.evaluate', { expression: 'true' });
211
+ if (session.errors.length) failure ||= Object.assign(Error('Unexpected browser errors'), { category: 'browser-errors' });
212
+ } catch (error) { failure ||= Object.assign(error, { category: 'capture' }); }
213
+ if (failure && session.errors.length) Object.assign(failure, { browserErrors: [...session.errors], droppedBrowserErrors: session.dropped() });
214
+ }
215
+ phase = 'cleanup';
216
+ try { session?.close(); } catch {}
217
+ try { browser.child.kill('SIGKILL'); } catch {}
218
+ try { rmSync(browser.profile, { recursive: true, force: true }); } catch {}
219
+ if (failure) throw failure;
220
+ return { pass: true, engine: 'chrome-headless-shell', screenshot: screenshot || null, consoleChecked: true };
221
+ }
@@ -0,0 +1,275 @@
1
+ const DEFAULT_CLOUD_URL = 'https://api.carthagent.xyz';
2
+ const DEFAULT_POLL_INTERVAL_MS = 5_000;
3
+ const MAX_ERROR_DETAIL = 240;
4
+
5
+ let spawnProcess = null;
6
+
7
+ export const CARTHAGENT_CLOUD_URL_ENV = 'CARTHAGENT_CLOUD_URL';
8
+
9
+ export function cloudControlUrl(env = process.env) {
10
+ const override = env?.[CARTHAGENT_CLOUD_URL_ENV]?.trim();
11
+ return (override || DEFAULT_CLOUD_URL).replace(/\/+$/, '');
12
+ }
13
+
14
+ function abortError(signal) {
15
+ return signal?.reason instanceof Error ? signal.reason : new Error('cancelled');
16
+ }
17
+
18
+ export async function openBrowser(url, { platform = process.platform, env = process.env, spawnImpl } = {}) {
19
+ const target = String(url || '').trim();
20
+ if (!/^https?:\/\//i.test(target)) return false;
21
+ if (!spawnImpl) {
22
+ spawnProcess ||= import('node:child_process').then(module => module.spawn);
23
+ spawnImpl = await spawnProcess;
24
+ }
25
+ let command;
26
+ let args;
27
+ if (platform === 'darwin') [command, args] = ['open', [target]];
28
+ else if (platform === 'win32') [command, args] = ['cmd.exe', ['/d', '/s', '/c', 'start', '', target.replace(/&/g, '^&')]];
29
+ else if (env?.WSL_DISTRO_NAME || env?.WSL_INTEROP) [command, args] = ['cmd.exe', ['/d', '/s', '/c', 'start', '', target.replace(/&/g, '^&')]];
30
+ else [command, args] = ['xdg-open', [target]];
31
+ return new Promise(resolve => {
32
+ try {
33
+ const child = spawnImpl(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
34
+ let settled = false;
35
+ const done = value => { if (!settled) { settled = true; resolve(value); } };
36
+ child.once?.('error', () => done(false));
37
+ child.once?.('spawn', () => { child.unref?.(); done(true); });
38
+ setTimeout(() => done(true), 100).unref?.();
39
+ } catch { resolve(false); }
40
+ });
41
+ }
42
+
43
+ export function delay(ms, signal) {
44
+ if (signal?.aborted) return Promise.reject(abortError(signal));
45
+ return new Promise((resolve, reject) => {
46
+ const timer = setTimeout(done, Math.max(0, ms));
47
+ function done() { signal?.removeEventListener('abort', cancelled); resolve(); }
48
+ function cancelled() { clearTimeout(timer); signal?.removeEventListener('abort', cancelled); reject(abortError(signal)); }
49
+ signal?.addEventListener('abort', cancelled, { once: true });
50
+ });
51
+ }
52
+
53
+ function errorCode(payload, fallback) {
54
+ if (typeof payload?.error === 'string' && payload.error) return payload.error;
55
+ if (typeof payload?.error?.code === 'string' && payload.error.code) return payload.error.code;
56
+ if (typeof payload?.error?.message === 'string' && payload.error.message) return payload.error.message;
57
+ return fallback;
58
+ }
59
+
60
+ function errorWithCode(code, status, detail) {
61
+ const error = new Error(code || `cloud_http_${status}`);
62
+ error.code = code || `cloud_http_${status}`;
63
+ error.status = status;
64
+ if (detail) error.detail = detail;
65
+ return error;
66
+ }
67
+
68
+ export class CloudClient {
69
+ constructor({ baseUrl = cloudControlUrl(), fetchImpl = globalThis.fetch, sleep = delay } = {}) {
70
+ this.baseUrl = String(baseUrl).replace(/\/+$/, '');
71
+ this.fetch = fetchImpl;
72
+ this.sleep = sleep;
73
+ }
74
+
75
+ async request(path, { method = 'GET', accessToken, body, signal } = {}) {
76
+ if (typeof this.fetch !== 'function') throw new Error('Carthagent Ship requires fetch support.');
77
+ const headers = { accept: 'application/json' };
78
+ if (body !== undefined) headers['content-type'] = 'application/json';
79
+ if (accessToken) headers.authorization = `Bearer ${accessToken}`;
80
+ const response = await this.fetch(`${this.baseUrl}${path}`, {
81
+ method, headers, signal,
82
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
83
+ });
84
+ const text = await response.text();
85
+ let payload = null;
86
+ if (text) {
87
+ try { payload = JSON.parse(text); } catch { payload = null; }
88
+ }
89
+ if (!response.ok) {
90
+ const fallback = `cloud_http_${response.status}`;
91
+ throw errorWithCode(errorCode(payload, fallback), response.status, text.slice(0, MAX_ERROR_DETAIL));
92
+ }
93
+ return payload ?? {};
94
+ }
95
+
96
+ createDeviceAuthorization({ clientName = 'Carthagent CLI', installId = null, signal } = {}) {
97
+ const body = { clientName };
98
+ if (installId) body.installId = installId;
99
+ return this.request('/v1/device/authorizations', { method: 'POST', body, signal });
100
+ }
101
+
102
+ pollDeviceToken({ deviceCode, signal } = {}) {
103
+ return this.request('/v1/device/token', { method: 'POST', body: { deviceCode }, signal });
104
+ }
105
+
106
+ refresh({ refreshToken, signal } = {}) {
107
+ return this.request('/v1/token/refresh', { method: 'POST', body: { refreshToken }, signal });
108
+ }
109
+
110
+ account({ accessToken, signal } = {}) {
111
+ return this.request('/v1/account', { accessToken, signal });
112
+ }
113
+
114
+ usage({ accessToken, limit = 50, signal } = {}) {
115
+ const bounded = Number.isSafeInteger(limit) ? Math.max(1, Math.min(200, limit)) : 50;
116
+ return this.request(`/v1/account/usage?limit=${bounded}`, { accessToken, signal });
117
+ }
118
+
119
+ revokeCurrent({ accessToken, signal } = {}) {
120
+ return this.request('/v1/token/revoke', { method: 'POST', accessToken, signal });
121
+ }
122
+
123
+ revokeSession({ accessToken, sessionId, signal } = {}) {
124
+ return this.request(`/v1/account/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE', accessToken, signal });
125
+ }
126
+
127
+ checkout({ accessToken, successUrl, cancelUrl, priceId, signal } = {}) {
128
+ return this.request('/v1/billing/checkout', { method: 'POST', accessToken, body: { successUrl, cancelUrl, priceId }, signal });
129
+ }
130
+
131
+ portal({ accessToken, returnUrl, signal } = {}) {
132
+ return this.request('/v1/billing/portal', { method: 'POST', accessToken, body: { returnUrl }, signal });
133
+ }
134
+
135
+ async authorizeDevice({ clientName = 'Carthagent CLI', installId = null, notify = () => {}, signal } = {}) {
136
+ const authorization = await this.createDeviceAuthorization({ clientName, installId, signal });
137
+ const verificationUri = authorization.verificationUriComplete || authorization.verificationUri;
138
+ notify({
139
+ type: 'device_code',
140
+ userCode: authorization.userCode,
141
+ verificationUri,
142
+ intervalSeconds: authorization.interval,
143
+ expiresInSeconds: authorization.expiresIn,
144
+ });
145
+ notify({ type: 'auth_url', url: verificationUri, instructions: `Approve device code ${authorization.userCode} in your browser.` });
146
+ const deadline = Date.now() + Number(authorization.expiresIn || 600) * 1000;
147
+ const interval = Math.max(1_000, Number(authorization.interval || DEFAULT_POLL_INTERVAL_MS / 1000) * 1000);
148
+ while (Date.now() < deadline) {
149
+ if (signal?.aborted) throw abortError(signal);
150
+ try {
151
+ return await this.pollDeviceToken({ deviceCode: authorization.deviceCode, signal });
152
+ } catch (error) {
153
+ if (error.code !== 'authorization_pending') throw error;
154
+ }
155
+ notify({ type: 'progress', message: 'Waiting for browser approval…' });
156
+ await this.sleep(interval, signal);
157
+ }
158
+ throw errorWithCode('expired_token', 410);
159
+ }
160
+ }
161
+
162
+ export function oauthCredentials(tokens, now = Date.now()) {
163
+ if (!tokens?.accessToken || !tokens?.refreshToken || !Number.isFinite(Number(tokens.expiresIn))) throw new Error('invalid_token_response');
164
+ return {
165
+ access: tokens.accessToken,
166
+ refresh: tokens.refreshToken,
167
+ expires: now + Math.max(1, Number(tokens.expiresIn)) * 1000,
168
+ };
169
+ }
170
+
171
+ export function formatNanoUsd(value) {
172
+ if (!Number.isSafeInteger(value)) return 'unavailable';
173
+ const sign = value < 0 ? '-' : '';
174
+ const absolute = Math.abs(value);
175
+ const dollars = Math.floor(absolute / 1_000_000_000);
176
+ const cents = Math.floor((absolute % 1_000_000_000) / 10_000_000);
177
+ return `${sign}$${dollars}.${String(cents).padStart(2, '0')}`;
178
+ }
179
+
180
+ function formatEpochSeconds(value) {
181
+ const seconds = Number(value);
182
+ return Number.isFinite(seconds) ? new Date(seconds * 1000).toISOString() : null;
183
+ }
184
+
185
+ export function cloudManagedUsageRecovery(value) {
186
+ const text = String(value?.code || value?.message || value || '');
187
+ if (/daily_limit_reached|managed daily usage limit/i.test(text)) return 'Managed daily usage is exhausted. It resets at 00:00 UTC. Use /account for headroom or /login for BYOK.';
188
+ if (/monthly_allowance_exhausted|managed monthly allowance/i.test(text)) return 'Managed monthly usage is exhausted. Use /billing portal to manage the plan or /login for BYOK.';
189
+ if (/subscription_inactive|managed subscription is not active/i.test(text)) return 'The managed subscription is inactive. Use /billing portal or /login for BYOK.';
190
+ if (/insufficient_quota|insufficient.credit|credit (?:is )?exhausted/i.test(text)) return 'Carthagent Ship credit is exhausted. Use /billing checkout where available or /login for BYOK.';
191
+ return null;
192
+ }
193
+
194
+ const METER_WIDTH = 16;
195
+ const meterBar = ratio => {
196
+ const bounded = Math.min(1, Math.max(0, Number.isFinite(ratio) ? ratio : 0));
197
+ return '\u2593'.repeat(Math.round(bounded * METER_WIDTH)).padEnd(METER_WIDTH, '\u2591');
198
+ };
199
+ const meterPercent = ratio => `${Math.round(Math.min(1, Math.max(0, Number.isFinite(ratio) ? ratio : 0)) * 100)}%`;
200
+ const managedRatio = bucket => {
201
+ const allowance = Number(bucket?.allowanceNanoUsd ?? bucket?.limitNanoUsd);
202
+ const available = Number(bucket?.availableNanoUsd);
203
+ return Number.isFinite(allowance) && allowance > 0 ? (allowance - Math.max(0, available)) / allowance : 0;
204
+ };
205
+ const shortUtcDate = epochSeconds => {
206
+ const date = epochSeconds ? new Date(Number(epochSeconds) * 1000) : null;
207
+ return date && Number.isFinite(date.getTime())
208
+ ? date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }) : null;
209
+ };
210
+ const utcClock = value => {
211
+ const date = value ? new Date(value) : null;
212
+ return date && Number.isFinite(date.getTime()) ? `${date.toISOString().slice(11, 16)} UTC` : null;
213
+ };
214
+ const compactTokens = value => {
215
+ if (!Number.isSafeInteger(value)) return '?';
216
+ return Math.abs(value) >= 1000 ? `${(value / 1000).toFixed(1).replace(/\.0$/, '')}k` : String(value);
217
+ };
218
+
219
+ export function managedUsageMeterLines(managed, { indent = '' } = {}) {
220
+ const lines = [];
221
+ if (managed?.monthly) {
222
+ const ratio = managedRatio(managed.monthly);
223
+ const renews = shortUtcDate(managed.periodEnd);
224
+ lines.push(`${indent}monthly ${meterPercent(ratio).padStart(4)} ${meterBar(ratio)}${renews ? ` renews ${renews}` : ''}`);
225
+ }
226
+ if (managed?.daily) {
227
+ const ratio = managedRatio(managed.daily);
228
+ const resets = utcClock(managed.daily.resetsAt);
229
+ lines.push(`${indent}daily ${meterPercent(ratio).padStart(4)} ${meterBar(ratio)}${resets ? ` resets ${resets}` : ''}`);
230
+ }
231
+ return lines;
232
+ }
233
+
234
+ export function formatCloudUsage(payload, { account, byok = false } = {}) {
235
+ const usage = Array.isArray(payload?.data) ? payload.data : [];
236
+ const managed = account?.managedUsage;
237
+ const lines = [];
238
+ lines.push(`\u{10900} ${managed?.planName ? managed.planName.toUpperCase() : 'SHIP'} \u00b7 USAGE`);
239
+ lines.push(...managedUsageMeterLines(managed, { indent: ' ' }));
240
+ if (byok) lines.push(' this session uses an external provider \u2014 not counted here');
241
+ if (!usage.length) {
242
+ lines.push(' no settled requests yet');
243
+ return lines.join('\n');
244
+ }
245
+ lines.push(' ' + '\u2500'.repeat(44));
246
+ for (const [index, item] of usage.slice(0, 5).entries()) {
247
+ const label = index === 0 ? 'last 5 ' : ' ';
248
+ const status = item?.status && item.status !== 'completed' ? ` \u00b7 ${item.status}` : '';
249
+ lines.push(` ${label}${item?.alias || 'Ship model'} \u00b7 ${compactTokens(item?.inputTokens)} in / ${compactTokens(item?.outputTokens)} out${status}`);
250
+ }
251
+ return lines.join('\n');
252
+ }
253
+
254
+ export function formatCloudAccount(account, { usage } = {}) {
255
+ const subscription = account?.subscription || {};
256
+ const managed = account?.managedUsage;
257
+ const planName = managed?.planName || 'plan';
258
+ const lines = [
259
+ `Carthagent Ship account ${account?.accountId || 'unknown'}`,
260
+ `Credit: ${formatNanoUsd(account?.balance?.availableNanoUsd)} available`,
261
+ `Plan: ${subscription.status && subscription.status !== 'none' ? `${planName} (${subscription.status})` : 'Free'}`,
262
+ ];
263
+ const subscriptionEnd = formatEpochSeconds(subscription.current_period_end);
264
+ if (subscriptionEnd) lines.push(`Renews/ends: ${subscriptionEnd}`);
265
+ if (managed) {
266
+ lines.push(...managedUsageMeterLines(managed));
267
+ } else if (subscription.status && subscription.status !== 'none' && ['active', 'trialing'].includes(subscription.status)) {
268
+ lines.push('Managed usage: unavailable for the current billing period');
269
+ }
270
+ if (usage) lines.push(formatCloudUsage(usage));
271
+ const sessions = Array.isArray(account?.sessions) ? account.sessions : [];
272
+ const active = sessions.filter(session => !session.revoked_at).length;
273
+ lines.push(`CLI sessions: ${active} active, ${sessions.length - active} revoked`);
274
+ return lines.join('\n');
275
+ }