rovecode 0.4.0-beta.2 → 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 (428) hide show
  1. package/README.md +67 -69
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -37
  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 -512
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-dybnr56b.js +0 -2
  272. package/dist/cli/ask-user-p8hq4xgj.js +0 -2
  273. package/dist/cli/auth-login-ewpgw5sm.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-xv3ypwev.js +0 -9
  276. package/dist/cli/catalog-737wb2s0.js +0 -2
  277. package/dist/cli/cli-arhg40m0.js +0 -2
  278. package/dist/cli/client-cf2pxx8q.js +0 -2
  279. package/dist/cli/commands-3p7e4xxs.js +0 -2
  280. package/dist/cli/connect-3q93d7cb.js +0 -2
  281. package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
  282. package/dist/cli/context-report-hbw9zfes.js +0 -2
  283. package/dist/cli/count-remote-mby98cd0.js +0 -2
  284. package/dist/cli/design-122y0axd.js +0 -2
  285. package/dist/cli/dispatch-b4egzvvh.js +0 -2
  286. package/dist/cli/doctor-x4jkv72e.js +0 -3
  287. package/dist/cli/executor-ftvg6tsy.js +0 -2
  288. package/dist/cli/export-pdgdhkch.js +0 -2
  289. package/dist/cli/files-cez9a96p.js +0 -2
  290. package/dist/cli/gauntlet-r3xxaszc.js +0 -2
  291. package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
  292. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
  293. package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
  294. package/dist/cli/hashline-ewg5hbe3.js +0 -2
  295. package/dist/cli/http-n0kehsk8.js +0 -5
  296. package/dist/cli/index-z5qt1s76.js +0 -2
  297. package/dist/cli/install-80mp63kx.js +0 -2
  298. package/dist/cli/loop-12twjcat.js +0 -2
  299. package/dist/cli/main-01pv9206.js +0 -4
  300. package/dist/cli/main-0jys2ccn.js +0 -3
  301. package/dist/cli/main-1ztz6fkj.js +0 -10
  302. package/dist/cli/main-23q7cmww.js +0 -9
  303. package/dist/cli/main-2rzbexn2.js +0 -3
  304. package/dist/cli/main-2wyax8k9.js +0 -9
  305. package/dist/cli/main-2yeveeve.js +0 -6
  306. package/dist/cli/main-2z3dek0b.js +0 -3
  307. package/dist/cli/main-2zgsknth.js +0 -3
  308. package/dist/cli/main-45ejth3a.js +0 -4
  309. package/dist/cli/main-45rn3trk.js +0 -22
  310. package/dist/cli/main-4p4e2w7x.js +0 -4
  311. package/dist/cli/main-4y0tnfpa.js +0 -16
  312. package/dist/cli/main-5py0rkmc.js +0 -4
  313. package/dist/cli/main-6dtqmbt6.js +0 -7
  314. package/dist/cli/main-6h9x282m.js +0 -4
  315. package/dist/cli/main-6vjeds42.js +0 -3
  316. package/dist/cli/main-78gq4bt9.js +0 -6
  317. package/dist/cli/main-7jd5vh3x.js +0 -4
  318. package/dist/cli/main-7kt6r53y.js +0 -4
  319. package/dist/cli/main-8c1tbazx.js +0 -58
  320. package/dist/cli/main-9a9rnh47.js +0 -19
  321. package/dist/cli/main-9ht36z12.js +0 -3
  322. package/dist/cli/main-a2yfvcy9.js +0 -7
  323. package/dist/cli/main-a3f51n0x.js +0 -5
  324. package/dist/cli/main-b8zq261k.js +0 -3
  325. package/dist/cli/main-bxtvnf6d.js +0 -13
  326. package/dist/cli/main-edxc3yzt.js +0 -4
  327. package/dist/cli/main-evgz4mp5.js +0 -21
  328. package/dist/cli/main-f33fc5je.js +0 -9
  329. package/dist/cli/main-fvnpq46y.js +0 -12
  330. package/dist/cli/main-gbbty4d4.js +0 -3
  331. package/dist/cli/main-gth53dnt.js +0 -25
  332. package/dist/cli/main-hqbz10aw.js +0 -9
  333. package/dist/cli/main-hrrvcfan.js +0 -38
  334. package/dist/cli/main-hzwtsb2m.js +0 -5
  335. package/dist/cli/main-j7ttv0sd.js +0 -34
  336. package/dist/cli/main-jak598k9.js +0 -5
  337. package/dist/cli/main-kba6zeyd.js +0 -6
  338. package/dist/cli/main-kwwsz6rq.js +0 -3
  339. package/dist/cli/main-m8vm17zq.js +0 -3
  340. package/dist/cli/main-mg4f96e1.js +0 -3
  341. package/dist/cli/main-mg9b20ac.js +0 -18
  342. package/dist/cli/main-mgb9ccnx.js +0 -3
  343. package/dist/cli/main-mjt2p7aj.js +0 -3
  344. package/dist/cli/main-n6qrdbmy.js +0 -3
  345. package/dist/cli/main-na7wse0x.js +0 -5
  346. package/dist/cli/main-nqveez48.js +0 -4
  347. package/dist/cli/main-ntqef02r.js +0 -10
  348. package/dist/cli/main-nvc3yjay.js +0 -136
  349. package/dist/cli/main-p0cfn6nr.js +0 -16
  350. package/dist/cli/main-qj2djy17.js +0 -19
  351. package/dist/cli/main-qsevpgsv.js +0 -3
  352. package/dist/cli/main-qvarybsp.js +0 -3
  353. package/dist/cli/main-rebtt91r.js +0 -5
  354. package/dist/cli/main-rpg7h8mb.js +0 -3
  355. package/dist/cli/main-rsy72qmw.js +0 -15
  356. package/dist/cli/main-rvetps99.js +0 -18
  357. package/dist/cli/main-s4bb0jav.js +0 -3
  358. package/dist/cli/main-s9v8k74e.js +0 -3
  359. package/dist/cli/main-tjvwmscs.js +0 -3
  360. package/dist/cli/main-tkgarpjj.js +0 -4
  361. package/dist/cli/main-v8y60bb2.js +0 -3
  362. package/dist/cli/main-vhrrq337.js +0 -3
  363. package/dist/cli/main-vp2dfb7s.js +0 -4
  364. package/dist/cli/main-vqbr22sz.js +0 -8
  365. package/dist/cli/main-vxnwe5xx.js +0 -18
  366. package/dist/cli/main-wgph00xf.js +0 -5
  367. package/dist/cli/main-wk2csfnj.js +0 -5
  368. package/dist/cli/main-wm997zjx.js +0 -3
  369. package/dist/cli/main-wpkyraxh.js +0 -3
  370. package/dist/cli/main-wqt32p5x.js +0 -4
  371. package/dist/cli/main-x9ct6y1a.js +0 -3
  372. package/dist/cli/main-xfekqh9m.js +0 -7
  373. package/dist/cli/main-xt9zc3n6.js +0 -7
  374. package/dist/cli/main-xx2z3zh5.js +0 -4
  375. package/dist/cli/main-y5c82rxr.js +0 -3
  376. package/dist/cli/main-yrjt2sqt.js +0 -14
  377. package/dist/cli/main-ys6zj3yr.js +0 -3
  378. package/dist/cli/main-ywbxshqc.js +0 -8
  379. package/dist/cli/main-z13755t8.js +0 -25
  380. package/dist/cli/main-zc7pyrbj.js +0 -4
  381. package/dist/cli/main.js +0 -279
  382. package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
  383. package/dist/cli/mcp-login-bthtfpt7.js +0 -2
  384. package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
  385. package/dist/cli/notify-54v5z9dz.js +0 -2
  386. package/dist/cli/oauth-g5gme95c.js +0 -2
  387. package/dist/cli/output-satndjap.js +0 -16
  388. package/dist/cli/profiles-sfhpbq3m.js +0 -2
  389. package/dist/cli/provider-config-hv3xtdt4.js +0 -2
  390. package/dist/cli/provider-kwzq6g84.js +0 -2
  391. package/dist/cli/registry-fh0hdnyn.js +0 -2
  392. package/dist/cli/registry-y1y8e94r.js +0 -2
  393. package/dist/cli/repl-t4z03mqq.js +0 -11
  394. package/dist/cli/resume-fqt4chg8.js +0 -2
  395. package/dist/cli/run-flags-rysbag9t.js +0 -2
  396. package/dist/cli/runtime-j19fjbsa.js +0 -2
  397. package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
  398. package/dist/cli/server-r0b6bksk.js +0 -5
  399. package/dist/cli/session-arg-txmn5g4x.js +0 -2
  400. package/dist/cli/session-ed250d9j.js +0 -2
  401. package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
  402. package/dist/cli/settings-y9rzcqx8.js +0 -2
  403. package/dist/cli/setup-jmbr11j0.js +0 -2
  404. package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
  405. package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
  406. package/dist/cli/smoke-1bg937kx.js +0 -8
  407. package/dist/cli/start-chat-p01cdks3.js +0 -12
  408. package/dist/cli/stream-4wmyaypz.js +0 -2
  409. package/dist/cli/task-eg4s093s.js +0 -2
  410. package/dist/cli/tasks-12v9rr9k.js +0 -2
  411. package/dist/cli/thinking-a5ngvqyh.js +0 -2
  412. package/dist/cli/todo-1wxpcecx.js +0 -2
  413. package/dist/cli/tools-2ftsya7w.js +0 -2
  414. package/dist/cli/tools-x1tj4fxm.js +0 -2
  415. package/dist/cli/trust-cmd-hccxehzb.js +0 -2
  416. package/dist/cli/update-check-ygt3vd7m.js +0 -2
  417. package/dist/cli/update-cmd-v23qhr8c.js +0 -2
  418. package/dist/cli/voice-g1gtck92.js +0 -2
  419. package/dist/cli/webfetch-0nnrjgb5.js +0 -2
  420. package/dist/cli/websearch-f0vr2p7d.js +0 -2
  421. package/dist/cli/workspace-9rq1w4ta.js +0 -2
  422. package/dist/lib/index.js +0 -62
  423. package/dist/lib/models-index.json +0 -1
  424. package/dist/lib/plugins.js +0 -6
  425. package/dist/lib/providers.js +0 -17
  426. package/dist/lib/public-api.js +0 -20
  427. package/dist/rovecode.exe +0 -4
  428. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,156 @@
1
+ /** Sextant renderer-local commands (port #43): the slash lines and palette actions the renderer
2
+ * answers itself — /help /theme /open /diff /focus /agents — the /undo /permissions /mode notes
3
+ * (ALIAS_NOTE) and the state-then-hook effect helpers keys.ts shares with them. Ported from the
4
+ * user's own sextant v0.4.0 prototype app.js:1024-1029 (runSlash) and :944 (openFile); split out
5
+ * of keys.ts for the line budget — also the home for #44's further renderer-local commands.
6
+ * Pure over (state, ctx): no clock, no timers, no process access. A state field is written BEFORE
7
+ * the matching KeyCtx.local hook fires, so the renderer only loads content / rebuilds the palette.
8
+ * Everything that is not renderer-local (built-ins, custom commands, `!cmd`, free text, unknown
9
+ * `/x`) reaches ctx.hooks.onSubmit unchanged — app.ts handleSlash owns the rest. */
10
+
11
+ import type { KeyCtx } from "./keys.ts";
12
+ import { parseInput, resolveFile } from "./overlays.ts";
13
+ import { expandMentions } from "./mentions.ts";
14
+ import { THEME_ORDER, type CodeMode, type Focus, type SextantState, type ThemeName } from "./types.ts";
15
+ import { openHelp, openNotices } from "./overlays.ts";
16
+
17
+ /** prototype commands with an rovecode equivalent: a toast instead of a submission — unless
18
+ * setCommands lists a custom command of that name, which then runs like any other */
19
+ export const ALIAS_NOTE: Record<string, string> = {
20
+ undo: "/undo → use /checkpoints + /restore",
21
+ permissions: "/permissions → use /yolo",
22
+ mode: "/mode → use /plan or /act",
23
+ };
24
+
25
+ const isTheme = (v: string): v is ThemeName => (THEME_ORDER as readonly string[]).includes(v);
26
+
27
+ // ------------------------------------------------------------------ effects (state first, then the hook)
28
+
29
+ export function setTheme(s: SextantState, ctx: KeyCtx, name: ThemeName): void {
30
+ s.theme = name;
31
+ ctx.local.setTheme(name);
32
+ }
33
+
34
+ export function setMode(s: SextantState, ctx: KeyCtx, mode: CodeMode): void {
35
+ s.code.mode = mode;
36
+ s.code.scroll = 0;
37
+ ctx.local.setMode(mode);
38
+ }
39
+
40
+ /** the crew board in the code panel (⌃a, /agents): lane closed, focus follows */
41
+ export function showAgents(s: SextantState, ctx: KeyCtx): void {
42
+ setMode(s, ctx, "agents");
43
+ s.code.laneOpen = false;
44
+ s.focus = "code";
45
+ }
46
+
47
+ /** app.js:944 — parent dirs expand, the code panel shows the file; focus is the caller's call */
48
+ export function openFile(s: SextantState, ctx: KeyCtx, path: string): void {
49
+ const parts = path.split("/");
50
+ for (let i = 1; i < parts.length; i++) s.files.expanded.add(parts.slice(0, i).join("/"));
51
+ s.code.file = path;
52
+ s.code.mode = "code";
53
+ s.code.scroll = 0;
54
+ ctx.local.openFile(path);
55
+ }
56
+
57
+ /** the files panel only takes focus while the layout shows it (≥ 140 columns) */
58
+ export function setFocus(s: SextantState, ctx: KeyCtx, f: Focus): void {
59
+ // a narrow terminal has no files column: page files into the main slot (draw-tabs.ts) instead of
60
+ // refusing — "the files panel needs ≥ 140 columns" was the old answer, and it left files unreachable
61
+ if (f === "files" && !ctx.layout.files) s.page = "files";
62
+ s.focus = f;
63
+ }
64
+
65
+ // ------------------------------------------------------------------ dispatch
66
+
67
+ /** renderer-local slash commands run here; everything else (built-ins, custom, `!cmd`, free
68
+ * text, unknown `/x`) reaches onSubmit unchanged — app.ts handleSlash owns the rest */
69
+ export function dispatch(s: SextantState, text: string, ctx: KeyCtx): void {
70
+ const p = parseInput(text);
71
+ // `@file` in free text: the file goes with the message as a `read` result (mentions.ts) — this is the one
72
+ // consumer of parseInput's `mentions`, and what the footer's "@ mentions attach files" has meant since
73
+ if (p.kind === "text" && p.mentions.length > 0) {
74
+ const r = expandMentions(text, { cwd: s.cwd, mentions: p.mentions, resolve: (m) => resolveFile(m, s.files.paths) });
75
+ for (const n of r.notes) ctx.local.toast(n);
76
+ ctx.hooks.onSubmit(r.text);
77
+ return;
78
+ }
79
+ if (p.kind !== "slash" || !runLocal(s, p.cmd ?? "", p.arg ?? "", ctx)) ctx.hooks.onSubmit(text);
80
+ }
81
+
82
+ /** true when the line was answered here and must not reach onSubmit */
83
+ export function runLocal(s: SextantState, cmd: string, arg: string, ctx: KeyCtx): boolean {
84
+ switch (cmd) {
85
+ case "help":
86
+ openHelp(s);
87
+ return false; // the card opens AND /help reaches the transcript
88
+ case "notices":
89
+ openNotices(s);
90
+ return true;
91
+ case "market":
92
+ // the overlay opens empty with a "loading" status; the renderer fills it when the catalog answers
93
+ ctx.local.openMarket();
94
+ return true;
95
+ case "context":
96
+ // counting is synchronous but not free on a long transcript, so the renderer does it off the frame
97
+ ctx.local.openContext();
98
+ return true;
99
+ case "theme":
100
+ if (isTheme(arg)) setTheme(s, ctx, arg);
101
+ else ctx.local.toast(`unknown theme "${arg}" · night, ember or contrast`);
102
+ return true;
103
+ case "open": {
104
+ const p = resolveFile(arg, s.files.paths, ctx.fuzzy);
105
+ if (!p) { ctx.local.toast(arg ? `no file matches "${arg}"` : "usage: /open <file>"); return true; }
106
+ openFile(s, ctx, p);
107
+ s.focus = "code";
108
+ return true;
109
+ }
110
+ case "diff": {
111
+ if (arg) {
112
+ const p = resolveFile(arg, s.files.paths, ctx.fuzzy);
113
+ if (!p) { ctx.local.toast(`no file matches "${arg}"`); return true; }
114
+ s.code.file = p;
115
+ }
116
+ setMode(s, ctx, "diff");
117
+ return true;
118
+ }
119
+ case "focus":
120
+ if (arg === "messages" || arg === "code" || arg === "files") setFocus(s, ctx, arg);
121
+ else ctx.local.toast("focus is messages, code or files");
122
+ return true;
123
+ case "agents":
124
+ showAgents(s, ctx);
125
+ return true;
126
+ default: {
127
+ const note = ALIAS_NOTE[cmd];
128
+ // a custom command of that name (listed by setCommands) wins over the note
129
+ if (note && !s.commands.some((c) => c.name === cmd)) { ctx.local.toast(note); return true; }
130
+ return false;
131
+ }
132
+ }
133
+ }
134
+
135
+ /** palette actions (overlays.ts paletteItems): a slash line, or theme: / mode: / focus: / open: */
136
+ export function runAction(s: SextantState, action: string, ctx: KeyCtx): void {
137
+ if (action.startsWith("/")) {
138
+ const p = parseInput(action), needsArg = ["theme", "open", "focus"].includes(p.cmd ?? "");
139
+ if (needsArg && !p.arg) { // the argument is still missing: park the line in the prompt, options open
140
+ s.input.text = action + " ";
141
+ s.input.cur = s.input.text.length;
142
+ s.input.sgSel = 0;
143
+ s.focus = "messages";
144
+ return;
145
+ }
146
+ dispatch(s, action, ctx);
147
+ return;
148
+ }
149
+ const i = action.indexOf(":"), kind = action.slice(0, i), rest = action.slice(i + 1);
150
+ if (kind === "theme" && isTheme(rest)) setTheme(s, ctx, rest);
151
+ else if (kind === "mode") {
152
+ setMode(s, ctx, rest as CodeMode);
153
+ if (rest === "agents") s.code.laneOpen = false;
154
+ } else if (kind === "focus") setFocus(s, ctx, rest as Focus);
155
+ else if (kind === "open") { openFile(s, ctx, rest); s.focus = "code"; }
156
+ }
@@ -0,0 +1,287 @@
1
+ /** The seam between the market overlay and the market module.
2
+ *
3
+ * draw-market.ts is pure over (state, screen): it takes rows and a status and never fetches. This file is
4
+ * the only place that knows src/market/ exists — it lazily imports the module (the boot rule: nothing in
5
+ * the market's dependency tree is loaded until someone opens /market), maps src/market/types.ts onto the
6
+ * overlay's flattened view rows, and turns every failure into a status the overlay can draw.
7
+ *
8
+ * Nothing here throws for a data problem, because the module it wraps does not either: a source that fails
9
+ * comes back as `SourceStatus {ok:false, reason}` and is shown as the error state, a source answered from
10
+ * the cache is shown as the offline state with its age, and an install that fails is a `{ok:false, error}`
11
+ * outcome printed on the plan card. The one thing this file DOES guard is the module not being there yet:
12
+ * while src/market/ is being written (types.ts landed first), an absent registry.ts must read as "the
13
+ * market module is not wired yet", not as a crash in the middle of a frame. */
14
+
15
+ import type { MarketDocLine, MarketPlan, MarketStatus, MarketViewRow } from "./draw-market.ts";
16
+ import { npxPackage } from "../mcp/local-package.ts";
17
+ import type { MarketInstall } from "../mcp/market.ts";
18
+
19
+ /** what the overlay needs to open: the rows, why they are what they are, and anything worth saying once */
20
+ export interface MarketLoad {
21
+ rows: MarketViewRow[];
22
+ status: MarketStatus;
23
+ notes: string[];
24
+ }
25
+
26
+ /** src/market/types.ts, structurally — declared here so this file compiles before the module lands and so
27
+ * the overlay never imports the market's types directly */
28
+ interface Env { name: string; description?: string; required: boolean; secret: boolean; default?: string }
29
+ interface Item {
30
+ id: string; kind: "mcp" | "skill" | "plugin"; title: string; publisher: string; description: string;
31
+ version?: string; repository?: string; homepage?: string; tags: string[]; status?: string;
32
+ env: Env[]; install: unknown; planNote?: string[];
33
+ /** the item's own documentation, as the catalog carries it (nimbus-24's writer): third-party markdown */
34
+ docs?: { source: string; format: string; bytes: number; truncated: boolean; body: string };
35
+ }
36
+ interface Row extends Item {
37
+ installed?: { path: string; scope: "user" | "project"; version?: string; updateAvailable?: boolean; trusted?: boolean };
38
+ }
39
+ type Status =
40
+ | { ok: true; from: "live" }
41
+ | { ok: true; from: "cache"; ageMs: number }
42
+ | { ok: true; from: "curated" }
43
+ /** the source was not consulted at all (an empty query never asks the registry, --offline skips it) */
44
+ | { ok: true; from: "skipped"; why: string }
45
+ | { ok: false; reason: string };
46
+ interface Result { items: Item[]; sources: Record<string, Status>; notes: string[] }
47
+ interface PlanView {
48
+ item: Item; target: string; scope: "user" | "project"; preview: string[];
49
+ asks: Env[]; pending: string[]; replaces?: string;
50
+ }
51
+ type Outcome =
52
+ | { ok: true; item: Item; target: string; scope: "user" | "project"; envNames: string[]; trusted?: boolean; next?: string;
53
+ /** install-once: what npm put on disk (src/market/types.ts InstallOutcome.package) */
54
+ package?: { name: string; version: string; prefix: string; integrity?: string; missing?: string[] } }
55
+ | { ok: false; error: string };
56
+ /** the plan/install options the overlay passes through; `local` is the chooser's answer (mcp/local-package.ts) */
57
+ interface Ctx { scope: "user" | "project"; cwd: string; home: string; local?: boolean }
58
+
59
+ /** the module's public surface, as much of it as the overlay uses (src/market/registry.ts + install.ts;
60
+ * there is no barrel file, so the two are imported separately) */
61
+ interface RegistryModule {
62
+ searchMarket(query: string, deps?: { offline?: boolean; withDocs?: boolean }): Promise<Result>;
63
+ /** one item WITH its documentation body: search and list deliberately leave the bodies out (they are
64
+ * hundreds of kilobytes the list never reads), so the docs pane asks for the row it is about to show */
65
+ findItem(kind: "mcp" | "skill" | "plugin", id: string, deps?: { offline?: boolean }): Promise<{ item?: Item; notes: string[] }>;
66
+ }
67
+ interface InstallModule {
68
+ planInstall(item: Item, opts: Ctx): PlanView | { error: string };
69
+ runInstall(plan: PlanView, answers: Record<string, string>, opts: Ctx, deps?: unknown): Promise<Outcome>;
70
+ withInstalled(items: readonly Item[], cwd: string, home: string): Row[];
71
+ }
72
+
73
+ /** "the market module is not wired yet" rather than an exception mid-frame */
74
+ const NOT_WIRED = "the market module is not available in this build (src/market/ is still landing)";
75
+
76
+ async function load(): Promise<{ registry: RegistryModule; install: InstallModule } | null> {
77
+ try {
78
+ // two lazy imports, resolved at call time so a missing file is a null, not a boot failure
79
+ const [registry, install] = await Promise.all([
80
+ import("../market/registry.ts") as Promise<Partial<RegistryModule>>,
81
+ import("../market/install.ts") as Promise<Partial<InstallModule>>,
82
+ ]);
83
+ if (typeof registry.searchMarket !== "function" || typeof install.planInstall !== "function") return null;
84
+ return { registry: registry as RegistryModule, install: install as InstallModule };
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /** what an item runs once installed, in one line — the install spec's own words, per arm */
91
+ function runsLine(item: Item): string {
92
+ const spec = item.install as { kind?: string; entry?: { installs?: { kind?: string; url?: string; command?: string; args?: string[]; runtime?: string }[] }; source?: unknown; git?: boolean; files?: { path: string }[] } | undefined;
93
+ if (!spec) return "";
94
+ if (spec.kind === "mcp") {
95
+ const first = spec.entry?.installs?.[0];
96
+ if (!first) return "";
97
+ return first.kind === "http" ? `remote ${first.url ?? ""}` : [first.command, ...(first.args ?? [])].join(" ");
98
+ }
99
+ if (spec.kind === "plugin") return `${spec.git ? "clone" : "copy"} ${String(spec.source ?? "")}`;
100
+ if (spec.kind === "skill") {
101
+ if (spec.files?.length) return `${spec.files.length} file(s), written verbatim — runs nothing`;
102
+ const src = spec.source as { git?: string; subfolder?: string } | undefined;
103
+ return src?.git ? `clone ${src.git}${src.subfolder ? ` (${src.subfolder})` : ""}` : "a SKILL.md the model reads when it matches";
104
+ }
105
+ return "";
106
+ }
107
+
108
+ /** the other ways in an MCP entry offers (a remote endpoint beside a local runtime) */
109
+ function alternatives(item: Item): string[] {
110
+ const spec = item.install as { kind?: string; entry?: { installs?: { kind?: string; url?: string; command?: string; args?: string[]; runtime?: string }[] } } | undefined;
111
+ if (spec?.kind !== "mcp") return [];
112
+ return (spec.entry?.installs ?? []).slice(1).map((i) => (i.kind === "http" ? `remote ${i.url ?? ""}` : `${i.runtime ?? "stdio"}: ${[i.command, ...(i.args ?? [])].join(" ")}`));
113
+ }
114
+
115
+ /** the npm package an mcp row's first form would run through npx — the install-once offer's subject */
116
+ function localOfferOf(item: Item): string | undefined {
117
+ const spec = item.install as { kind?: string; entry?: { installs?: unknown[] } } | undefined;
118
+ if (spec?.kind !== "mcp") return undefined;
119
+ const first = spec.entry?.installs?.[0];
120
+ if (first === undefined || typeof first !== "object" || first === null) return undefined;
121
+ return npxPackage(first as MarketInstall)?.spec;
122
+ }
123
+
124
+ export function toViewRow(row: Row): MarketViewRow {
125
+ const localOffer = localOfferOf(row);
126
+ return {
127
+ ...(localOffer !== undefined ? { localOffer } : {}),
128
+ id: row.id,
129
+ kind: row.kind,
130
+ title: row.title,
131
+ publisher: row.publisher,
132
+ description: row.description,
133
+ ...(row.version !== undefined ? { version: row.version } : {}),
134
+ runs: runsLine(row),
135
+ env: row.env.map((v) => ({ name: v.name, required: v.required, secret: v.secret, ...(v.description !== undefined ? { description: v.description } : {}) })),
136
+ alternatives: alternatives(row),
137
+ pending: row.planNote ?? [],
138
+ ...(row.installed ? { installed: row.installed } : {}),
139
+ // search carries the metadata, not the body: the pane fills `lines` from docsFor() when it opens
140
+ ...(row.docs ? { docs: { source: row.docs.source, truncated: row.docs.truncated === true, lines: typeof row.docs.body === "string" && row.docs.body !== "" ? docLines(row.docs.body) : [] } } : {}),
141
+ };
142
+ }
143
+
144
+ /** ESC and the rest of C0 (tab and newline excepted), DEL, and the C1 range some terminals still read
145
+ * as CSI — a document must not be able to move the cursor, change a colour or clear the screen */
146
+ // eslint-disable-next-line no-control-regex
147
+ const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
148
+
149
+ /** A document, flattened to lines a cockpit can draw and stripped of everything a terminal would obey.
150
+ * The body is third-party text, so escape sequences and control characters go before anything else; then
151
+ * markdown is flattened rather than rendered — headings and fenced code keep their shape, and the rest is
152
+ * prose wrapped to a column. A README must not be able to move the cursor or repaint the screen. */
153
+ export function docLines(body: string, width = 96): MarketDocLine[] {
154
+ const safe = body
155
+ .replace(/\r\n?/g, "\n")
156
+ .replace(/<(script|style|iframe|object|embed|template|noscript)\b[\s\S]*?<\/\1\s*>/gi, "")
157
+ .replace(/<\/?[a-zA-Z][^>]*>/g, "")
158
+ .replace(CONTROL, "")
159
+ .replace(/\t/g, " ")
160
+ .replace(/^---\n[\s\S]*?\n---\n/, "");
161
+ const out: MarketDocLine[] = [];
162
+ let fence = false;
163
+ for (const raw of safe.split("\n")) {
164
+ if (/^\s*```/.test(raw)) { fence = !fence; out.push({ kind: "rule", text: "" }); continue; }
165
+ if (fence) { out.push({ kind: "code", text: raw.slice(0, width) }); continue; }
166
+ const h = /^(#{1,6})\s+(.*)$/.exec(raw);
167
+ if (h) { out.push({ kind: "head", text: h[2]!.replace(/[`*]/g, "").slice(0, width) }); continue; }
168
+ const text = raw.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[`*]/g, "");
169
+ if (text.trim() === "") { out.push({ kind: "blank", text: "" }); continue; }
170
+ let line = "";
171
+ for (const word of text.split(/\s+/).filter(Boolean)) {
172
+ const piece = word.length > width ? word.slice(0, width) : word;
173
+ if (!line) { line = piece; continue; }
174
+ if (line.length + 1 + piece.length <= width) line += ` ${piece}`;
175
+ else { out.push({ kind: "text", text: line }); line = piece; }
176
+ }
177
+ if (line) out.push({ kind: "text", text: line });
178
+ }
179
+ return out;
180
+ }
181
+
182
+ /** the three drawn states, decided from the sources the module consulted — never from an empty list */
183
+ export function statusFrom(sources: Record<string, Status>): MarketStatus {
184
+ const failed = Object.entries(sources).filter(([, v]) => !v.ok) as [string, { ok: false; reason: string }][];
185
+ if (failed.length) {
186
+ const [name, s] = failed[0]!;
187
+ return { kind: "error", reason: failed.length === 1 ? `${name}: ${s.reason}` : `${name}: ${s.reason} (+${failed.length - 1} more)` };
188
+ }
189
+ const cached = Object.values(sources).find((v) => v.ok && v.from === "cache") as { ok: true; from: "cache"; ageMs: number } | undefined;
190
+ if (cached) {
191
+ const mins = Math.round(cached.ageMs / 60000);
192
+ return { kind: "offline", note: `showing cached results${mins > 0 ? `, ${mins}m old` : ""}` };
193
+ }
194
+ // "skipped" is not a failure and not staleness: the source was left out on purpose, and `why` says so.
195
+ // It is worth one line because a reader who does not see a registry row deserves to know it was not asked.
196
+ const skipped = Object.values(sources).find((v) => v.ok && v.from === "skipped") as { ok: true; from: "skipped"; why: string } | undefined;
197
+ if (skipped) return { kind: "offline", note: skipped.why };
198
+ return { kind: "ready" };
199
+ }
200
+
201
+ /** Everything the overlay opens with. `offline` forces the no-network path (the curated shelf and the
202
+ * repository catalogs), which is also what a failed network falls back to. */
203
+ export async function loadMarket(cwd: string, home: string, opts: { offline?: boolean } = {}): Promise<MarketLoad> {
204
+ const mod = await load();
205
+ if (!mod) return { rows: [], status: { kind: "error", reason: NOT_WIRED }, notes: [] };
206
+ const result = await mod.registry.searchMarket("", opts.offline === true ? { offline: true } : {});
207
+ // installed state is disk truth, joined onto the catalog by withInstalled (never read from a catalog)
208
+ let rows: Row[] = result.items as Row[];
209
+ try { rows = mod.install.withInstalled(result.items, cwd, home); } catch { /* disk unreadable: the rows still list */ }
210
+ return { rows: rows.map(toViewRow), status: statusFrom(result.sources), notes: result.notes };
211
+ }
212
+
213
+ /** The documentation for one row, fetched when the reader asks for it.
214
+ *
215
+ * `searchMarket` carries docs METADATA but not the bodies — they are ~400 KB nobody reads while browsing —
216
+ * so the list cannot fill this in advance. `findItem` reads the one row with its body, and the body is
217
+ * flattened to terminal-safe lines here, once, at the moment the pane opens. */
218
+ export async function docsFor(row: MarketViewRow): Promise<{ source: string; truncated: boolean; lines: MarketDocLine[] } | null> {
219
+ const mod = await load();
220
+ if (!mod || typeof mod.registry.findItem !== "function") return null;
221
+ const { item } = await mod.registry.findItem(row.kind, row.id, { offline: true });
222
+ const docs = item?.docs;
223
+ if (!docs || typeof docs.body !== "string" || docs.body.trim() === "") return null;
224
+ return { source: docs.source, truncated: docs.truncated === true, lines: docLines(docs.body) };
225
+ }
226
+
227
+ /** the plan for one row, or the reason there is none. Writes nothing. `local` is the chooser's answer for an npx
228
+ * row: true draws the install-once plan (`node <bin>`, the `installs`/`records` rows), false or absent the npx
229
+ * line as today — and the plan remembers it, so the install runs the plan that was approved. */
230
+ export async function planFor(row: MarketViewRow, ctx: Ctx, local?: boolean): Promise<MarketPlan | { error: string }> {
231
+ const mod = await load();
232
+ if (!mod) return { error: NOT_WIRED };
233
+ const result = await mod.registry.searchMarket(row.id, { offline: true });
234
+ const item = result.items.find((i) => i.kind === row.kind && i.id === row.id);
235
+ if (!item) return { error: `${row.kind}:${row.id} is not in the catalog any more` };
236
+ const plan = mod.install.planInstall(item, { ...ctx, ...(local === true ? { local: true } : {}) });
237
+ if ("error" in plan) return { error: plan.error };
238
+ return {
239
+ row,
240
+ title: `${plan.item.kind}:${plan.item.id} — ${plan.item.title}`,
241
+ target: plan.target,
242
+ scope: plan.scope,
243
+ preview: plan.preview,
244
+ asks: plan.asks.map((a) => ({ name: a.name, required: a.required, secret: a.secret })),
245
+ pending: plan.pending,
246
+ ...(plan.replaces !== undefined ? { replaces: plan.replaces } : {}),
247
+ ...(local === true ? { local: true } : {}),
248
+ };
249
+ }
250
+
251
+ /** Run a plan the human has just confirmed. The overlay only ever sees the outcome sentence. `opts.local` must be
252
+ * the approved plan's own `local` — npm runs here, and only here, after the card said yes. `opts.deps` is the
253
+ * installer's seam (a fake `npm` in tests). */
254
+ export async function install(row: MarketViewRow, ctx: Ctx, opts: { local?: boolean; deps?: unknown } = {}): Promise<{ ok: boolean; text: string }> {
255
+ const mod = await load();
256
+ if (!mod) return { ok: false, text: NOT_WIRED };
257
+ const result = await mod.registry.searchMarket(row.id, { offline: true });
258
+ const item = result.items.find((i) => i.kind === row.kind && i.id === row.id);
259
+ if (!item) return { ok: false, text: `${row.kind}:${row.id} is not in the catalog any more` };
260
+ const withLocal: Ctx = { ...ctx, ...(opts.local === true ? { local: true } : {}) };
261
+ const plan = mod.install.planInstall(item, withLocal);
262
+ if ("error" in plan) return { ok: false, text: plan.error };
263
+ // A SECRET cannot be finished inside the overlay: it has to be typed on a shell, masked, not into a
264
+ // query line that echoes. The overlay says so and hands the exact command over.
265
+ //
266
+ // `pending` is not that, and treating it as if it were made six of the sixteen curated MCP servers
267
+ // uninstallable from the cockpit — including `filesystem`, which is the first one anybody tries.
268
+ // Nothing is asked for a pending value: it is a placeholder the installer writes into the config
269
+ // (`<directory the server may touch>`) for the human to replace afterwards, and the CLI installs it
270
+ // exactly that way. Refusing here was the overlay inventing a requirement the installer does not have.
271
+ if (plan.asks.length > 0) {
272
+ const what = plan.asks.map((a) => a.name).join(", ");
273
+ return { ok: false, text: `${what} must be typed where it can be masked — run: rovecode market install ${item.kind}:${item.id}` };
274
+ }
275
+ const outcome = await mod.install.runInstall(plan, {}, withLocal, opts.deps);
276
+ if (!outcome.ok) return { ok: false, text: outcome.error };
277
+ const bits = [`installed into ${outcome.target}`];
278
+ if (outcome.package) {
279
+ bits.push(`${outcome.package.name} ${outcome.package.version} installed once → ${outcome.package.prefix}${outcome.package.integrity !== undefined ? " (integrity recorded)" : ""}`);
280
+ if (outcome.package.missing?.length) bits.push(`record incomplete: ${outcome.package.missing.join("; ")}`);
281
+ }
282
+ if (outcome.envNames.length) bits.push(`export ${outcome.envNames.join(", ")}`);
283
+ // the placeholder is the one thing between this install and a working server, so it leads
284
+ for (const p of plan.pending) bits.push(`fill in ${p}`);
285
+ if (outcome.next) bits.push(outcome.next);
286
+ return { ok: true, text: bits.join(" · ") };
287
+ }
@@ -0,0 +1,141 @@
1
+ /** `@file` mentions, made true. overlays.ts has parsed `@path` into `mentions[]` (with a fuzzy resolveFile) since
2
+ * the port, and the input footer promised "@ mentions attach files" — but nothing ever read `.mentions`, so the
3
+ * promise did nothing. This module is the consumer: on submit, each mention that resolves to a workspace file is
4
+ * appended to the message exactly as the `read` tool would return it (hashline `path#TAG` header, `N#hash|text`
5
+ * lines), so the model has the contents — and valid edit anchors — without spending a tool round-trip.
6
+ *
7
+ * Why here and not in tui/commands.ts: that file's note ("no `!shell` / `@file` injection … the template reaches
8
+ * the model as plain text") is about CUSTOM COMMAND TEMPLATES — files a repo or a plugin ships, where injecting
9
+ * files or shell output from a template is a way for a project to read the user's disk. That reason stands and
10
+ * is untouched. A `@file` the human types into their own prompt is the human choosing to show a file; the only
11
+ * risk is cost, so the caps below exist and every cap is SAID, in the message and as a toast.
12
+ *
13
+ * What is refused, and said: a mention no workspace file matches; a directory; a file outside the workspace
14
+ * (resolveFile only knows the scanned list, and the absolute path is checked against cwd again); a binary; a
15
+ * file past MAX_BYTES. What is capped, and said: lines per file (MAX_LINES — the footer names the offset that
16
+ * continues), files per message (MAX_FILES), characters per message (MAX_CHARS). Never silent: `@` must not be a
17
+ * way to spend a context window without knowing. */
18
+
19
+ import { readFileSync, statSync } from "node:fs";
20
+ import { resolve, sep } from "node:path";
21
+ import { fileTag, lineHash, readAnchored, renderAnchored, type AnchoredFile } from "../coding/hashline.ts";
22
+
23
+ /** `@path` tokens in free text: at the start or after whitespace, so `me@example.com` is not one. The one
24
+ * regex both surfaces and overlays.ts parseInput read — this module imports nothing from the sextant, so the
25
+ * classic renderer can use it without loading the cockpit's module graph. */
26
+ export const MENTION_RE = /(?:^|\s)@([\w./-]+)/g;
27
+ /** the mentions of a typed line — none for a `/command` or a `!shell` line, which are never expanded */
28
+ export function mentionsIn(text: string): string[] {
29
+ const t = text.trim();
30
+ if (t[0] === "/" || t[0] === "!") return [];
31
+ return [...t.matchAll(MENTION_RE)].map((m) => m[1]!);
32
+ }
33
+
34
+ /** the most lines one mention contributes — the rest is one `read` with an offset away */
35
+ export const MENTION_MAX_LINES = 400;
36
+ /** mentions expanded per message; the rest are named and left for the model to read */
37
+ export const MENTION_MAX_FILES = 8;
38
+ /** characters all expansions together may add to one message (~15k tokens) */
39
+ export const MENTION_MAX_CHARS = 60_000;
40
+ /** a file larger than this is not read at all — name it, let the model ask for a window */
41
+ export const MENTION_MAX_BYTES = 2 * 1024 * 1024;
42
+ /** with less than this left of the character budget, a further file is named rather than squeezed to a
43
+ * line or two — a three-line fragment of a file is worse context than "read it yourself" */
44
+ export const MENTION_MIN_BLOCK = 500;
45
+
46
+ /** the line that opens the attached section — what userRow cuts the transcript at */
47
+ export const MENTION_FRAME = "(files attached by @mention — each block is what `read` returns for the file; its edit anchors are valid)";
48
+ /** one per attached file, right above its read block: `[@src/x.ts — attached: 120 lines]` */
49
+ export const MENTION_HEAD = /^\[@(\S+) — attached: (\d+)(?: of (\d+))? lines(, capped[^\]]*)?\]$/;
50
+
51
+ export interface AttachedFile { path: string; shown: number; total: number; capped: boolean }
52
+ export interface MentionExpansion {
53
+ /** the message as submitted: the typed text, then the attached section (unchanged when nothing attached) */
54
+ text: string;
55
+ attached: AttachedFile[];
56
+ /** what was refused or capped, one sentence each — the renderer toasts them */
57
+ notes: string[];
58
+ }
59
+
60
+ export interface ExpandOptions {
61
+ cwd: string;
62
+ /** a mention → the cwd-relative posix path it names, or null. The sextant ranks over its scanned file list
63
+ * (overlays.ts resolveFile: exact, unique basename, fuzzy); the classic renderer, which has no list, takes
64
+ * the exact path only — what its own `@` autocomplete inserts. */
65
+ resolve: (mention: string) => string | null;
66
+ /** already parsed mentions (default: mentionsIn(text)) */
67
+ mentions?: readonly string[];
68
+ /** seams for tests */
69
+ stat?: (abs: string) => { isFile(): boolean; size: number };
70
+ read?: (abs: string) => string;
71
+ }
72
+
73
+ const BINARY_PROBE = 8 * 1024;
74
+
75
+ /** Expand every `@mention` in a typed line. Pure apart from the reads; never throws — a file that cannot be
76
+ * read becomes a note and the message goes out without it. */
77
+ export function expandMentions(text: string, opts: ExpandOptions): MentionExpansion {
78
+ const mentions = [...new Set(opts.mentions ?? mentionsIn(text))];
79
+ const notes: string[] = [];
80
+ const attached: AttachedFile[] = [];
81
+ const blocks: string[] = [];
82
+ if (mentions.length === 0) return { text, attached, notes };
83
+ const root = resolve(opts.cwd);
84
+ const stat = opts.stat ?? ((p: string) => statSync(p));
85
+ let budget = MENTION_MAX_CHARS;
86
+ for (const m of mentions) {
87
+ if (attached.length >= MENTION_MAX_FILES) { notes.push(`@${m}: not attached — ${MENTION_MAX_FILES} files per message is the cap; ask me to read it`); continue; }
88
+ const rel = opts.resolve(m);
89
+ if (rel === null) { notes.push(`@${m}: no file in the workspace matches`); continue; }
90
+ const abs = resolve(root, rel);
91
+ if (abs !== root && !abs.startsWith(root + sep)) { notes.push(`@${rel}: outside the workspace — not attached`); continue; }
92
+ let st: { isFile(): boolean; size: number };
93
+ try { st = stat(abs); } catch { notes.push(`@${rel}: cannot be read — not attached`); continue; }
94
+ if (!st.isFile()) { notes.push(`@${rel}: a directory — name a file in it`); continue; }
95
+ if (st.size > MENTION_MAX_BYTES) { notes.push(`@${rel}: ${(st.size / 1048576).toFixed(1)} MB is too large to attach — ask me to read a window of it`); continue; }
96
+ let content: string;
97
+ try { content = opts.read ? opts.read(abs) : readFileSync(abs, "utf8"); }
98
+ catch { notes.push(`@${rel}: cannot be read — not attached`); continue; }
99
+ if (content.slice(0, BINARY_PROBE).includes("\0")) { notes.push(`@${rel}: a binary file — not attached`); continue; }
100
+ if (budget < MENTION_MIN_BLOCK) { notes.push(`@${rel}: not attached — this message already carries ${MENTION_MAX_CHARS.toLocaleString()} characters of files; ask me to read it`); continue; }
101
+ const file = opts.read ? anchoredFrom(abs, content) : readAnchored(abs);
102
+ const total = file.lines.length;
103
+ // the line cap first, then the character budget: whichever is hit, the footer says where to continue
104
+ let shown = Math.min(total, MENTION_MAX_LINES);
105
+ let body = renderAnchored({ ...file, lines: file.lines.slice(0, shown) }).replace(/\n$/, "");
106
+ while (body.length > budget && shown > 1) {
107
+ shown = Math.max(1, Math.floor(shown * budget / body.length));
108
+ body = renderAnchored({ ...file, lines: file.lines.slice(0, shown) }).replace(/\n$/, "");
109
+ }
110
+ const capped = shown < total;
111
+ budget -= body.length;
112
+ const head = capped
113
+ ? `[@${rel} — attached: ${shown} of ${total} lines, capped: read it with offset ${shown + 1} for the rest]`
114
+ : `[@${rel} — attached: ${total} lines]`;
115
+ blocks.push(`${head}\n${body}\n(showing lines ${total === 0 ? 0 : 1}-${shown} of ${total})`);
116
+ attached.push({ path: rel, shown, total, capped });
117
+ if (capped) notes.push(`@${rel}: ${total} lines — attached the first ${shown}; the rest is a read away`);
118
+ }
119
+ if (blocks.length === 0) return { text, attached, notes };
120
+ return { text: `${text.trimEnd()}\n\n${MENTION_FRAME}\n\n${blocks.join("\n\n")}`, attached, notes };
121
+ }
122
+
123
+ /** an AnchoredFile from content already in hand (the `read` seam), split and hashed exactly as readAnchored does —
124
+ * including the empty last line a trailing newline yields, so the counts match what the read tool reports */
125
+ function anchoredFrom(abs: string, content: string): AnchoredFile {
126
+ return { path: abs, tag: fileTag(content), lines: content.split("\n").map((t, i) => ({ n: i + 1, hash: lineHash(t), text: t })) };
127
+ }
128
+
129
+ /** The transcript's view of a submitted line: the typed text, and one chip per attached file — never the file
130
+ * bodies, which would flood the messages panel (they are in the session, where the model reads them). */
131
+ export function splitAttached(text: string): { text: string; files: string[] } {
132
+ const lines = text.split("\n");
133
+ const at = lines.indexOf(MENTION_FRAME);
134
+ if (at < 0) return { text, files: [] };
135
+ const files: string[] = [];
136
+ for (const l of lines.slice(at + 1)) {
137
+ const m = MENTION_HEAD.exec(l);
138
+ if (m) files.push(m[3] !== undefined ? `${m[1]} · ${m[2]}/${m[3]} lines, capped` : `${m[1]} · ${m[2]} lines`);
139
+ }
140
+ return { text: lines.slice(0, at).join("\n").trimEnd(), files };
141
+ }
@@ -0,0 +1,26 @@
1
+ /** Click zones on the transcript: a tool row that names a file (`~ edit src/a.ts`, `◆ read …`) opens
2
+ * that file in the code panel — what `/open <path>` does. The rows come from the same cached build the
3
+ * painter used (draw-messages.ts cachedBuildRows), at the same scroll offset, so a zone is exactly the
4
+ * painted row. Rows under a card, or off the visible window, get no zone. */
5
+ import { areas, cachedBuildRows, messagesScroll } from "./draw-messages.ts";
6
+ import { inner } from "./draw-util.ts";
7
+ import type { Rect, SextantState, Theme } from "./types.ts";
8
+
9
+ export interface MessageHit { rect: Rect; path: string }
10
+
11
+ export function messageRowHits(rect: Rect, s: SextantState, theme: Theme, now: number): MessageHit[] {
12
+ const B = inner(rect);
13
+ if (B.w < 3 || B.h < 1) return [];
14
+ const { msgH } = areas(B, s);
15
+ if (msgH <= 0) return [];
16
+ const rows = cachedBuildRows(s, B.w, theme, now);
17
+ const { offset } = messagesScroll(rect, s, theme, now);
18
+ const out: MessageHit[] = [];
19
+ for (let i = 0; i < msgH; i++) {
20
+ const r = rows[offset + i];
21
+ if (!r?.path) continue;
22
+ const x = B.x + (r.indent ?? 0);
23
+ out.push({ rect: { x, y: B.y + i, w: Math.max(1, B.w - (r.indent ?? 0) - 1), h: 1 }, path: r.path }); // -1: the scrollbar column
24
+ }
25
+ return out;
26
+ }