rovecode 0.4.0-beta.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Project context inheritance (port #8): auto-import instruction files from
3
+ * other coding-agent "harnesses" (Claude, Gemini, Cursor, Copilot, plain
4
+ * AGENTS.md) so rovecode projects don't need to duplicate repo conventions.
5
+ *
6
+ * Pattern + precedence order modeled on oh-my-pi's context-file discovery
7
+ * (research/source_snapshots/can1357-oh-my-pi):
8
+ * - docs/context-files.md, "Other supported context conventions" and
9
+ * "Load order and shadowing" tables — the provider-priority idea
10
+ * (higher priority wins at a shared scope) and the per-tool path
11
+ * conventions: AGENTS.md, CLAUDE.md / .claude/CLAUDE.md, GEMINI.md,
12
+ * .cursor/rules/*.mdc + legacy .cursorrules, .github/copilot-instructions.md.
13
+ * - packages/coding-agent/src/discovery/builtin.ts `getAncestorDirs` — the
14
+ * cwd-upward ancestor walk with an optional inclusive stop directory.
15
+ * - packages/coding-agent/src/discovery/cursor.ts — `.cursor/rules/*.mdc`
16
+ * carries MDC frontmatter that must be separated from the rule body.
17
+ *
18
+ * Discovery + precedence (rovecode's documented spec):
19
+ * - Ancestor walk: cwd UPWARD via dirname until parent === current (the
20
+ * filesystem root). The walk additionally stops — INCLUSIVELY — at the
21
+ * first directory containing `.git` (file or directory; worktrees use a
22
+ * file), so a repository never inherits context from outside itself.
23
+ * `opts.stopAt` bounds the walk at an explicit dir (also inclusive).
24
+ * - Precedence: NEARER directories first (nearest wins); within one
25
+ * directory, family order rovecode > agents > claude > gemini > cursor >
26
+ * copilot (the harvest list in `buildCandidates`). Earlier position wins
27
+ * dedupe and total-cap priority.
28
+ * - Shadowing (dedupe by depth): the same relative path (e.g. `AGENTS.md`)
29
+ * found in a nearer directory completely shadows the farther one — the
30
+ * farther file is never read and never listed.
31
+ * - Blank files (empty or whitespace-only, after MDC frontmatter stripping)
32
+ * are skipped entirely: no section, no dedupe registration, no shadowing.
33
+ * - Byte-identical content across surviving candidates is included once;
34
+ * the earlier (higher-precedence) occurrence wins and later duplicates
35
+ * never reach `sources` — unlike total-cap drops, which keep a stub.
36
+ *
37
+ * Budgets (rovecode-specific; OMP has no cap on context-file loading):
38
+ * - `maxPerFileChars` caps each file's content. Truncation is fence-safe:
39
+ * cut at the last newline inside the window and close an odd ``` fence
40
+ * count so the following sections aren't swallowed by an open code block.
41
+ * - `maxTotalChars` budgets the FULL rendered section string — the
42
+ * `## From <path>` header included, so `text.length` never exceeds it.
43
+ * - `maxFiles` bounds how many files are included; further existing
44
+ * candidate files are counted in `skippedFiles` but never read.
45
+ *
46
+ * Freshness: callers snapshot the result once per runtime (cli/runtime.ts)
47
+ * so the system prompt stays byte-stable for prompt caching (port #5).
48
+ * Mid-session edits to config files are intentionally not picked up —
49
+ * restart rovecode (a new runtime) to refresh.
50
+ *
51
+ * Remaining deliberate deviations from OMP: no provider-priority shadowing
52
+ * table (content dedupe + path shadowing instead); `.cursor/rules/*.mdc`
53
+ * harvested unconditionally as plain text (frontmatter stripped, never
54
+ * parsed/acted on) and restricted to `*.mdc`; the character/file budgets are
55
+ * rovecode's own.
56
+ */
57
+
58
+ import { readFileSync, readdirSync, existsSync } from "node:fs";
59
+ import { join, dirname, resolve } from "node:path";
60
+
61
+ export interface ContextSource {
62
+ /** Display path relative to cwd, "/"-separated with one "../" segment per
63
+ * ancestor level — used verbatim in the rendered "## From <path>" header. */
64
+ path: string;
65
+ family: "rovecode" | "agents" | "claude" | "gemini" | "cursor" | "copilot";
66
+ chars: number;
67
+ truncated: boolean;
68
+ }
69
+
70
+ export interface ProjectContext {
71
+ text: string;
72
+ sources: ContextSource[];
73
+ /** Existing candidate files that were NOT read because the `maxFiles`
74
+ * bound was already reached (surfaced in /status, never silent). */
75
+ skippedFiles: number;
76
+ }
77
+
78
+ export interface LoadOptions {
79
+ /** Per-file cap in characters. Default 8000. */
80
+ maxPerFileChars?: number;
81
+ /** Total cap on the rendered text (headers included). Default 24000. */
82
+ maxTotalChars?: number;
83
+ /** Max files included across the walk. Default 24. */
84
+ maxFiles?: number;
85
+ /** Inclusive upper bound for the ancestor walk (tests/embedders). */
86
+ stopAt?: string;
87
+ }
88
+
89
+ const DEFAULT_MAX_PER_FILE_CHARS = 8000;
90
+ const DEFAULT_MAX_TOTAL_CHARS = 24000;
91
+ const DEFAULT_MAX_FILES = 24;
92
+ const TRUNCATION_MARKER = "…[truncated]";
93
+
94
+ type Family = ContextSource["family"];
95
+
96
+ interface Candidate {
97
+ /** Path relative to its directory, "/"-separated regardless of host OS. */
98
+ relPath: string;
99
+ family: Family;
100
+ /** Strip a leading MDC frontmatter block before treating this as content. */
101
+ mdc: boolean;
102
+ }
103
+
104
+ /** Harvest list for ONE directory, in family precedence order (first =
105
+ * highest): rovecode > agents > claude > gemini > cursor > copilot. */
106
+ function buildCandidates(dir: string): Candidate[] {
107
+ const candidates: Candidate[] = [
108
+ { relPath: ".rovecode/ROVECODE.md", family: "rovecode", mdc: false },
109
+ { relPath: "ROVECODE.md", family: "rovecode", mdc: false },
110
+ { relPath: "AGENTS.md", family: "agents", mdc: false },
111
+ { relPath: "CLAUDE.md", family: "claude", mdc: false },
112
+ { relPath: ".claude/CLAUDE.md", family: "claude", mdc: false },
113
+ { relPath: "GEMINI.md", family: "gemini", mdc: false },
114
+ { relPath: ".cursorrules", family: "cursor", mdc: false },
115
+ ];
116
+ for (const name of listCursorRuleFiles(dir)) {
117
+ candidates.push({ relPath: `.cursor/rules/${name}`, family: "cursor", mdc: true });
118
+ }
119
+ candidates.push({ relPath: ".github/copilot-instructions.md", family: "copilot", mdc: false });
120
+ return candidates;
121
+ }
122
+
123
+ /** cwd upward: dirname until parent === current (filesystem root), stopping
124
+ * inclusively at `stopAt` or at the first dir containing `.git` (file or
125
+ * directory — git worktrees use a file). OMP `getAncestorDirs` pattern. */
126
+ function ancestorDirs(cwd: string, stopAt?: string): string[] {
127
+ const dirs: string[] = [];
128
+ const stop = stopAt === undefined ? null : resolve(stopAt);
129
+ let current = resolve(cwd);
130
+ for (;;) {
131
+ dirs.push(current);
132
+ if (stop !== null && current === stop) break;
133
+ if (hasGitMarker(current)) break; // repo root — inclusive, never above
134
+ const parent = dirname(current);
135
+ if (parent === current) break; // filesystem root
136
+ current = parent;
137
+ }
138
+ return dirs;
139
+ }
140
+
141
+ /** `.git` presence, or false on any fs error (permissions, bad path, …). */
142
+ function hasGitMarker(dir: string): boolean {
143
+ try {
144
+ return existsSync(join(dir, ".git"));
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /** `.cursor/rules/*.mdc`, sorted by filename. A missing/unreadable directory
151
+ * yields no entries — silent skip, never a throw. */
152
+ function listCursorRuleFiles(dir: string): string[] {
153
+ try {
154
+ const entries = readdirSync(join(dir, ".cursor", "rules"), { withFileTypes: true });
155
+ return entries
156
+ .filter((e) => e.isFile() && e.name.endsWith(".mdc"))
157
+ .map((e) => e.name)
158
+ .sort();
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ /** Read a file's content, or null on any failure (missing, unreadable, is a
165
+ * directory, …) — every failure mode is a silent skip, never a throw. */
166
+ function tryReadFile(absPath: string): string | null {
167
+ try {
168
+ return readFileSync(absPath, "utf8");
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /** existsSync that can never throw (used only for `skippedFiles` counting). */
175
+ function fileExists(absPath: string): boolean {
176
+ try {
177
+ return existsSync(absPath);
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ /** Strip a leading `---\n...\n---` MDC frontmatter block, if present. */
184
+ function stripMdcFrontmatter(content: string): string {
185
+ const lines = content.split("\n");
186
+ if ((lines[0] ?? "").trim() !== "---") return content;
187
+ let endIdx = -1;
188
+ for (let i = 1; i < lines.length; i++) {
189
+ if ((lines[i] ?? "").trim() === "---") {
190
+ endIdx = i;
191
+ break;
192
+ }
193
+ }
194
+ if (endIdx === -1) return content;
195
+ return lines.slice(endIdx + 1).join("\n").replace(/^\n+/, "");
196
+ }
197
+
198
+ /** Fence-safe truncation: cut at the last newline inside the window (whole
199
+ * lines only; hard cut when the window has no interior newline), close an
200
+ * odd ``` fence count so following sections aren't swallowed by an open
201
+ * code block, then append the marker. */
202
+ function truncateSafely(full: string, maxChars: number): string {
203
+ const window = full.slice(0, maxChars);
204
+ const nl = window.lastIndexOf("\n");
205
+ const cut = nl > 0 ? window.slice(0, nl) : window;
206
+ const fences = cut.split("\n").filter((l) => l.trimStart().startsWith("```")).length;
207
+ return fences % 2 === 1 ? `${cut}${TRUNCATION_MARKER}\n\`\`\`` : cut + TRUNCATION_MARKER;
208
+ }
209
+
210
+ interface KeptFile {
211
+ /** cwd-relative display path: "../" per ancestor level + relPath. */
212
+ displayPath: string;
213
+ family: Family;
214
+ /** Content after per-file truncation (includes the marker when truncated). */
215
+ content: string;
216
+ truncated: boolean;
217
+ }
218
+
219
+ /**
220
+ * Load and merge instruction files from every supported harness convention
221
+ * found in `cwd` and its ancestors (see module doc for the walk, precedence,
222
+ * shadowing, and budget rules). Deterministic: identical tree contents always
223
+ * produce the same `text` and `sources`, in the same order.
224
+ */
225
+ export function loadProjectContext(cwd: string, opts?: LoadOptions): ProjectContext {
226
+ const maxPerFileChars = opts?.maxPerFileChars ?? DEFAULT_MAX_PER_FILE_CHARS;
227
+ const maxTotalChars = opts?.maxTotalChars ?? DEFAULT_MAX_TOTAL_CHARS;
228
+ const maxFiles = opts?.maxFiles ?? DEFAULT_MAX_FILES;
229
+
230
+ const seenContent = new Set<string>();
231
+ const shadowed = new Set<string>(); // relPaths claimed by a nearer non-blank file
232
+ const kept: KeptFile[] = [];
233
+ let skippedFiles = 0;
234
+
235
+ const dirs = ancestorDirs(cwd, opts?.stopAt);
236
+ for (let depth = 0; depth < dirs.length; depth++) {
237
+ const dir = dirs[depth]!;
238
+ for (const candidate of buildCandidates(dir)) {
239
+ if (shadowed.has(candidate.relPath)) continue; // nearest wins (dedupe by depth)
240
+ const absPath = join(dir, candidate.relPath);
241
+ if (kept.length >= maxFiles) {
242
+ // file-count bound (HIGH-3): count existing candidates, never read them
243
+ if (fileExists(absPath)) skippedFiles++;
244
+ continue;
245
+ }
246
+ const raw = tryReadFile(absPath);
247
+ if (raw === null) continue; // missing or unreadable — skip silently
248
+
249
+ const full = candidate.mdc ? stripMdcFrontmatter(raw) : raw;
250
+ if (full.trim() === "") continue; // blank: no section/dedupe/shadow (LOW-6)
251
+ shadowed.add(candidate.relPath);
252
+
253
+ if (seenContent.has(full)) continue; // byte-identical dupe: earlier one won
254
+ seenContent.add(full);
255
+
256
+ let content = full;
257
+ let truncated = false;
258
+ if (full.length > maxPerFileChars) {
259
+ content = truncateSafely(full, maxPerFileChars);
260
+ truncated = true;
261
+ }
262
+ kept.push({
263
+ displayPath: "../".repeat(depth) + candidate.relPath,
264
+ family: candidate.family, content, truncated,
265
+ });
266
+ }
267
+ }
268
+
269
+ // Total cap enforced in precedence order over the FULL section string —
270
+ // header included, so `text.length <= maxTotalChars` always holds. A file
271
+ // that would exceed the budget is dropped from `text` but stays listed in
272
+ // `sources` as a chars:0/truncated:true stub so callers can see what was
273
+ // cut, and why the surviving text is short.
274
+ const sources: ContextSource[] = [];
275
+ const sections: string[] = [];
276
+ let total = 0;
277
+ for (const file of kept) {
278
+ const section = `\n\n## From ${file.displayPath}\n${file.content}`;
279
+ if (total + section.length <= maxTotalChars) {
280
+ sources.push({ path: file.displayPath, family: file.family, chars: file.content.length, truncated: file.truncated });
281
+ sections.push(section);
282
+ total += section.length;
283
+ } else {
284
+ sources.push({ path: file.displayPath, family: file.family, chars: 0, truncated: true });
285
+ }
286
+ }
287
+
288
+ return { text: sections.join(""), sources, skippedFiles };
289
+ }
@@ -0,0 +1,228 @@
1
+ /** What is actually in the context window, item by item — and how far our arithmetic is from what
2
+ * the provider says it charged for.
3
+ *
4
+ * Two numbers exist for every turn and they are NOT the same thing:
5
+ * - the ESTIMATE: o200k over every part we are about to send. Ours, synchronous, available before
6
+ * a request and for a session that never ran. An estimate for anything that is not an OpenAI
7
+ * tokenizer — Anthropic's is not public, and its 4.7-generation tokenizer produces materially
8
+ * more tokens for the same text, so the estimate reads LOW there.
9
+ * - the REPORTED prompt: what the provider itself counted, i.e. `input + cacheRead + cacheWrite`
10
+ * of a turn's usage. Cache reads and writes are part of the prompt the model saw; leaving them
11
+ * out is the most common way a context meter reads far too low on an agentic session, where
12
+ * almost the whole prompt is a cache read.
13
+ * drift() compares them at the same point in the transcript, so "how wrong is our meter for this
14
+ * provider" becomes a measured number instead of a belief. Beyond DRIFT_TOLERANCE it is worth
15
+ * saying out loud: it means compaction fires at the wrong time.
16
+ *
17
+ * Nothing here reads the network or the disk, and nothing throws: a transcript with no usage at all
18
+ * still produces a report, with `drift` simply absent. */
19
+
20
+ import { partsTokenText } from "./loop.ts";
21
+ import type { Message, MessagePart } from "./types.ts";
22
+ import { tokenScaleFor } from "./token-scale.ts";
23
+ import { contextHealth, costUsdTiered, countTokens, type NormalizedUsage, type PricingRow } from "./usage.ts";
24
+ import { ratesFor } from "../providers/catalog.ts";
25
+ import type { PriceTier } from "../providers/catalog-local.ts";
26
+
27
+ /** past this the estimate is misleading enough to name — the compaction trigger reads the estimate */
28
+ export const DRIFT_TOLERANCE = 0.05;
29
+
30
+ /** the budget for a model whose window we do not know — the old flat value, kept as the fallback */
31
+ export const DEFAULT_CONTEXT_BUDGET = 200_000;
32
+ /** never plan for less history than this, however small the window says it is */
33
+ export const MIN_CONTEXT_BUDGET = 32_000;
34
+ /** room left beside the history for the system prompt, the tool schemas and the indexes */
35
+ export const PROMPT_OVERHEAD_TOKENS = 24_000;
36
+ /** assumed answer room when the catalog states no output limit */
37
+ const ASSUMED_OUTPUT = 32_000;
38
+
39
+ /** How much history a run may carry before compaction. A flat 200k spends a fifth of a 1M window and
40
+ * overflows a 128k one, so it is derived: the window minus what the answer and the fixed prompt need.
41
+ * An explicit override wins (ROVECODE_CONTEXT_BUDGET), an unknown window keeps the old default, and a
42
+ * window too small to hold the floor gets a proportional share rather than a budget larger than itself. */
43
+ export function contextBudgetFor(opts: { window?: number; maxOutput?: number; override?: number; scale?: number }): number {
44
+ const { window, maxOutput, override } = opts;
45
+ if (override !== undefined && Number.isFinite(override) && override > 0) return Math.floor(override);
46
+ if (!window || !Number.isFinite(window) || window <= 0) return DEFAULT_CONTEXT_BUDGET;
47
+ const reserve = (Number.isFinite(maxOutput) && (maxOutput ?? 0) > 0 ? (maxOutput as number) : ASSUMED_OUTPUT) + PROMPT_OVERHEAD_TOKENS;
48
+ const room = window - reserve;
49
+ const raw = room < MIN_CONTEXT_BUDGET ? Math.max(1, Math.floor(window * 0.6)) : Math.floor(room);
50
+ // The budget is compared against an estimate, so a model whose tokenizer counts more than the
51
+ // estimator must get a smaller budget — dividing here is exactly equivalent to inflating every
52
+ // estimate at every call site, and there is one of it. See core/token-scale.ts for the measurements.
53
+ const scale = opts.scale !== undefined && Number.isFinite(opts.scale) && opts.scale > 0 ? opts.scale : 1;
54
+ return Math.max(1, Math.floor(raw / scale));
55
+ }
56
+
57
+ export interface ContextSlice {
58
+ label: string;
59
+ tokens: number;
60
+ /** of the estimate, 0..1 — 0 when the estimate is 0 */
61
+ share: number;
62
+ /** what the reader should know about this row, when a number alone would mislead */
63
+ note?: string;
64
+ }
65
+
66
+ export interface ContextDrift {
67
+ /** our estimate of the prompt at the last turn that reported usage */
68
+ estimated: number;
69
+ /** what that turn says it was given: input + cacheRead + cacheWrite */
70
+ reported: number;
71
+ /** reported − estimated; positive means we are UNDER-counting the real window */
72
+ delta: number;
73
+ /** |delta| / reported, 0 when reported is 0 */
74
+ fraction: number;
75
+ beyondTolerance: boolean;
76
+ }
77
+
78
+ export interface ContextReport {
79
+ model: { provider: string; model: string };
80
+ /** the catalog's context window for the current model, when it knows one */
81
+ window?: number;
82
+ /** o200k over the whole transcript — what the next request would carry */
83
+ estimated: number;
84
+ /** the estimate corrected towards this model's own tokenizer; equals `estimated` when unmeasured.
85
+ * The window rows below are computed from THIS, because it is the number the provider will use. */
86
+ corrected: number;
87
+ /** the correction that was applied, and where its number came from */
88
+ scale: { factor: number; measured: boolean; note: string };
89
+ slices: ContextSlice[];
90
+ /** window − corrected, floored at 0; absent when the window is unknown */
91
+ remaining?: number;
92
+ /** corrected / window; absent when the window is unknown */
93
+ fraction?: number;
94
+ nearLimit?: boolean;
95
+ drift?: ContextDrift;
96
+ /** summed over every turn that reported usage */
97
+ totals: NormalizedUsage;
98
+ /** USD over the turns that could be priced, and how many could not */
99
+ costUsd?: number;
100
+ unpricedTurns: number;
101
+ /** images carry tokens we do not estimate — say how many rather than pretend they are free */
102
+ images: number;
103
+ }
104
+
105
+ export interface ReportInput {
106
+ messages: readonly Message[];
107
+ /** the current model, used for the window and for turns with no origin */
108
+ current: { provider: string; model: string };
109
+ /** window + pricing for a model, however the caller gets them (catalog, overlay, a test double) */
110
+ lookup: (ref: { provider: string; model: string }) => { contextWindow?: number; pricing?: PricingRow; tier?: PriceTier } | undefined;
111
+ /** the system prompt that will be sent, when the caller has it */
112
+ system?: string;
113
+ /** the serialized tool schemas that will be sent, when the caller has them */
114
+ toolSchemas?: string;
115
+ }
116
+
117
+ const tokensOf = (text: string): number => (text ? countTokens(text) : 0);
118
+
119
+ function partTokens(parts: readonly MessagePart[], kind: MessagePart["kind"]): number {
120
+ const only = parts.filter((p) => p.kind === kind);
121
+ return only.length === 0 ? 0 : tokensOf(partsTokenText(only as MessagePart[]));
122
+ }
123
+
124
+ /** Roles are grouped the way a reader thinks about them, not the way the wire does: what I sent, what
125
+ * the model said, what the tools were asked, what they answered. Tool traffic dominates an agentic
126
+ * session and hiding it inside "assistant" is what makes a context meter useless. */
127
+ export function contextReport(input: ReportInput): ContextReport {
128
+ const { messages, current, lookup } = input;
129
+ const info = lookup(current);
130
+
131
+ const userText = messages.filter((m) => m.role === "user");
132
+ const assistantText = messages.filter((m) => m.role === "assistant");
133
+ const systemMsgs = messages.filter((m) => m.role === "system");
134
+
135
+ const slices: ContextSlice[] = [];
136
+ const push = (label: string, tokens: number, note?: string) => {
137
+ if (tokens > 0) slices.push({ label, tokens, share: 0, ...(note ? { note } : {}) });
138
+ };
139
+
140
+ push("system prompt", tokensOf(input.system ?? "") + partTokens(systemMsgs.flatMap((m) => m.parts), "text"));
141
+ push("tool schemas", tokensOf(input.toolSchemas ?? ""), input.toolSchemas ? undefined : "not supplied");
142
+ push("your messages", partTokens(userText.flatMap((m) => m.parts), "text"));
143
+ push("assistant replies", partTokens(assistantText.flatMap((m) => m.parts), "text"));
144
+ const allParts = messages.flatMap((m) => m.parts);
145
+ push("tool calls", partTokens(allParts, "tool_call"));
146
+ push("tool results", partTokens(allParts, "tool_result"));
147
+
148
+ const estimated = slices.reduce((n, s) => n + s.tokens, 0);
149
+ for (const s of slices) s.share = estimated > 0 ? s.tokens / estimated : 0;
150
+
151
+ const totals: NormalizedUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
152
+ let cost = 0;
153
+ let priced = 0;
154
+ let unpricedTurns = 0;
155
+ for (const m of messages) {
156
+ const u = m.usage;
157
+ if (!u) continue;
158
+ const n: NormalizedUsage = { input: u.input, output: u.output, cacheRead: u.cacheRead ?? 0, cacheWrite: u.cacheWrite ?? 0 };
159
+ totals.input += n.input; totals.output += n.output; totals.cacheRead += n.cacheRead; totals.cacheWrite += n.cacheWrite;
160
+ if (n.input === 0 && n.output === 0 && n.cacheRead === 0 && n.cacheWrite === 0) continue;
161
+ const info = lookup(m.origin ?? current);
162
+ // The prompt this turn carried decides the rate on a tiered model: xAI and Google bill a prompt
163
+ // over 200k at the upper rate, and xAI applies it to the whole request. Pricing a tiered turn at
164
+ // the base rate printed roughly HALF the real cost, with no caveat saying so — the TUI's /cost had
165
+ // this right and this report did not, which is the worst arrangement of the two.
166
+ const c = info?.pricing
167
+ ? costUsdTiered(n, ratesFor({ ...(m.origin ?? current), pricing: info.pricing, ...(info.tier ? { tier: info.tier } : {}) }, n.input + n.cacheRead + n.cacheWrite))
168
+ : undefined;
169
+ if (c === undefined) unpricedTurns += 1;
170
+ else { cost += c; priced += 1; }
171
+ }
172
+
173
+ // The window rows are what a reader acts on, so they are computed from the corrected estimate: on
174
+ // Claude 5 o200k reads up to 1.58x low, and a meter that says 60% of a window already at 95% is worse
175
+ // than no meter. `estimated` stays raw beside it so the correction is visible, never silent.
176
+ const sc = tokenScaleFor(current);
177
+ const corrected = Math.ceil(estimated * sc.scale);
178
+ const report: ContextReport = {
179
+ model: current,
180
+ estimated,
181
+ corrected,
182
+ scale: { factor: sc.scale, measured: sc.measured, note: sc.note },
183
+ slices,
184
+ totals,
185
+ unpricedTurns,
186
+ images: allParts.filter((p) => p.kind === "image").length,
187
+ ...(priced > 0 ? { costUsd: cost } : {}),
188
+ };
189
+ if (info?.contextWindow) {
190
+ const health = contextHealth(corrected, info.contextWindow);
191
+ report.window = info.contextWindow;
192
+ report.fraction = health.fraction;
193
+ report.nearLimit = health.nearLimit;
194
+ report.remaining = Math.max(0, info.contextWindow - corrected);
195
+ }
196
+ // the two rows the provider counted and the transcript never stored — see drift()
197
+ const fixedTokens = (slices.find((x) => x.label === "system prompt")?.tokens ?? 0) + (slices.find((x) => x.label === "tool schemas")?.tokens ?? 0);
198
+ const d = drift(messages, fixedTokens);
199
+ if (d) report.drift = d;
200
+ return report;
201
+ }
202
+
203
+ /** Our estimate against the provider's own count, measured at the last turn that reported one.
204
+ * The comparison point matters: a turn's usage describes the prompt BEFORE that turn, so the
205
+ * estimate is taken over everything up to it, exclusive. Returns undefined when no turn reported
206
+ * a prompt (a fresh session, or a provider that sends no usage).
207
+ *
208
+ * `fixedTokens` is the system prompt plus the tool schemas. They must be included or the comparison
209
+ * is not a comparison: neither is ever stored in a transcript — the system message is appended to the
210
+ * wire payload and never to history, and tool schemas are a separate wire field entirely — while the
211
+ * provider's `reported` count is of a request that always carried both. Leaving them out made every
212
+ * session look like it drifted by roughly the size of the fixed prompt, which on a fresh session is
213
+ * most of it, and did so even for OpenAI models where the estimator is exact by construction. That is
214
+ * a false signal on the one line whose whole job is to say whether the meter can be trusted. */
215
+ export function drift(messages: readonly Message[], fixedTokens = 0): ContextDrift | undefined {
216
+ for (let i = messages.length - 1; i >= 0; i--) {
217
+ const u = messages[i]?.usage;
218
+ if (!u) continue;
219
+ const reported = u.input + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
220
+ if (reported <= 0) continue;
221
+ const before = messages.slice(0, i);
222
+ const estimated = fixedTokens + (before.length === 0 ? 0 : tokensOf(before.map((m) => partsTokenText(m.parts)).join("\n")));
223
+ const delta = reported - estimated;
224
+ const fraction = reported > 0 ? Math.abs(delta) / reported : 0;
225
+ return { estimated, reported, delta, fraction, beyondTolerance: fraction > DRIFT_TOLERANCE };
226
+ }
227
+ return undefined;
228
+ }
@@ -0,0 +1,60 @@
1
+ /** Token accounting + ordered context assembly (ADR-007). */
2
+
3
+ export interface ContextChunk {
4
+ name: string; // "system" | "skills" | "repo-map" | "files" | "history" | "reminder"
5
+ text: string;
6
+ /** drop first under pressure; history is compacted not dropped */
7
+ priority: number; // higher = keep longer
8
+ tokens: number;
9
+ }
10
+
11
+ /** Cheap estimator: ~4 chars/token, clamped. Accurate enough for budgeting;
12
+ * providers report exact usage which we feed back via usage accounting. */
13
+ export function estimateTokens(text: string): number {
14
+ return Math.ceil(text.length / 4);
15
+ }
16
+
17
+ export interface AssemblyResult {
18
+ chunks: ContextChunk[]; // kept, in prompt order
19
+ dropped: ContextChunk[]; // non-history chunks evicted to fit budget (lowest priority first)
20
+ totalTokens: number; // tokens of kept chunks
21
+ overBudget: boolean; // true when even kept chunks exceed budget
22
+ }
23
+
24
+ /** Order: system > files > repo-map > skills > history > reminder (aider ChatChunks ordering).
25
+ * History is never dropped here (compaction owns it); non-history chunks are
26
+ * evicted lowest-priority-first until the budget holds — system last-dropped. */
27
+ export function assembleContext(chunks: ContextChunk[], budgetTokens: number): AssemblyResult {
28
+ const ordered = [...chunks].sort((a, b) => b.priority - a.priority);
29
+ const nonHistory = ordered.filter((c) => c.name !== "history");
30
+ const dropped: ContextChunk[] = [];
31
+ const kept = [...nonHistory];
32
+ const hist = ordered.find((c) => c.name === "history");
33
+ const histTokens = hist?.tokens ?? 0;
34
+ while (kept.length > 0 && kept.reduce((n, c) => n + c.tokens, 0) + histTokens > budgetTokens) {
35
+ // evict lowest-priority kept chunk (tail after priority sort = system last)
36
+ const victim = kept.pop()!;
37
+ dropped.push(victim);
38
+ }
39
+ const total = kept.reduce((n, c) => n + c.tokens, 0) + histTokens;
40
+ const chunksOut = [...kept]; // prompt order: high priority first
41
+ if (hist) chunksOut.push(hist);
42
+ return { chunks: chunksOut, dropped, totalTokens: total, overBudget: total > budgetTokens };
43
+ }
44
+
45
+ /** Head/tail summarization plan (aider history.py:41): keep recent tail under
46
+ * half budget, summarize older head. Pure — callers do the LLM summarize. */
47
+ export function planCompaction(history: Message4Plan[], budgetTokens: number): { keep: Message4Plan[]; summarize: Message4Plan[] } {
48
+ const total = history.reduce((n, m) => n + m.tokens, 0);
49
+ if (total <= budgetTokens * 0.8) return { keep: history, summarize: [] };
50
+ const half = Math.floor(budgetTokens / 2);
51
+ let acc = 0; let cut = history.length;
52
+ for (let i = history.length - 1; i >= 0; i--) {
53
+ acc += history[i]!.tokens;
54
+ if (acc > half) { cut = i + 1; break; }
55
+ cut = i;
56
+ }
57
+ return { keep: history.slice(cut), summarize: history.slice(0, cut) };
58
+ }
59
+
60
+ export interface Message4Plan { id: string; tokens: number; text: string }