rovecode 0.3.3 → 0.4.0-beta.2

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 (491) hide show
  1. package/CHANGELOG.md +512 -0
  2. package/README.md +33 -48
  3. package/bin/rovecode.js +24 -0
  4. package/dist/cli/app-dybnr56b.js +2 -0
  5. package/dist/cli/ask-user-p8hq4xgj.js +2 -0
  6. package/dist/cli/auth-login-ewpgw5sm.js +2 -0
  7. package/dist/cli/auth-m8p9grty.js +2 -0
  8. package/dist/cli/bench-xv3ypwev.js +9 -0
  9. package/dist/cli/catalog-737wb2s0.js +2 -0
  10. package/dist/cli/cli-arhg40m0.js +2 -0
  11. package/dist/cli/client-cf2pxx8q.js +2 -0
  12. package/dist/cli/commands-3p7e4xxs.js +2 -0
  13. package/dist/cli/connect-3q93d7cb.js +2 -0
  14. package/dist/cli/context-cmd-eqxmxhzq.js +2 -0
  15. package/dist/cli/context-report-hbw9zfes.js +2 -0
  16. package/dist/cli/count-remote-mby98cd0.js +2 -0
  17. package/dist/cli/design-122y0axd.js +2 -0
  18. package/dist/cli/dispatch-b4egzvvh.js +2 -0
  19. package/dist/cli/doctor-x4jkv72e.js +3 -0
  20. package/dist/cli/executor-ftvg6tsy.js +2 -0
  21. package/dist/cli/export-pdgdhkch.js +2 -0
  22. package/dist/cli/files-cez9a96p.js +2 -0
  23. package/dist/cli/gauntlet-r3xxaszc.js +2 -0
  24. package/dist/cli/gauntlet-runner-r515m7kk.js +10 -0
  25. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +5 -0
  26. package/dist/cli/gauntlet-wave4-acs9s60q.js +14 -0
  27. package/dist/cli/hashline-ewg5hbe3.js +2 -0
  28. package/dist/cli/http-n0kehsk8.js +5 -0
  29. package/dist/cli/index-z5qt1s76.js +2 -0
  30. package/dist/cli/install-80mp63kx.js +2 -0
  31. package/dist/cli/loop-12twjcat.js +2 -0
  32. package/dist/cli/main-01pv9206.js +4 -0
  33. package/dist/cli/main-0jys2ccn.js +3 -0
  34. package/dist/cli/main-1ztz6fkj.js +10 -0
  35. package/dist/cli/main-23q7cmww.js +9 -0
  36. package/dist/cli/main-2rzbexn2.js +3 -0
  37. package/dist/cli/main-2wyax8k9.js +9 -0
  38. package/dist/cli/main-2yeveeve.js +6 -0
  39. package/dist/cli/main-2z3dek0b.js +3 -0
  40. package/dist/cli/main-2zgsknth.js +3 -0
  41. package/dist/cli/main-45ejth3a.js +4 -0
  42. package/dist/cli/main-45rn3trk.js +22 -0
  43. package/dist/cli/main-4p4e2w7x.js +4 -0
  44. package/dist/cli/main-4y0tnfpa.js +16 -0
  45. package/dist/cli/main-5py0rkmc.js +4 -0
  46. package/dist/cli/main-6dtqmbt6.js +7 -0
  47. package/dist/cli/main-6h9x282m.js +4 -0
  48. package/dist/cli/main-6vjeds42.js +3 -0
  49. package/dist/cli/main-78gq4bt9.js +6 -0
  50. package/dist/cli/main-7jd5vh3x.js +4 -0
  51. package/dist/cli/main-7kt6r53y.js +4 -0
  52. package/dist/cli/main-8c1tbazx.js +58 -0
  53. package/dist/cli/main-9a9rnh47.js +19 -0
  54. package/dist/cli/main-9ht36z12.js +3 -0
  55. package/dist/cli/main-a2yfvcy9.js +7 -0
  56. package/dist/cli/main-a3f51n0x.js +5 -0
  57. package/dist/cli/main-b8zq261k.js +3 -0
  58. package/dist/cli/main-bxtvnf6d.js +13 -0
  59. package/dist/cli/main-edxc3yzt.js +4 -0
  60. package/dist/cli/main-evgz4mp5.js +21 -0
  61. package/dist/cli/main-f33fc5je.js +9 -0
  62. package/dist/cli/main-fvnpq46y.js +12 -0
  63. package/dist/cli/main-gbbty4d4.js +3 -0
  64. package/dist/cli/main-gth53dnt.js +25 -0
  65. package/dist/cli/main-hqbz10aw.js +9 -0
  66. package/dist/cli/main-hrrvcfan.js +38 -0
  67. package/dist/cli/main-hzwtsb2m.js +5 -0
  68. package/dist/cli/main-j7ttv0sd.js +34 -0
  69. package/dist/cli/main-jak598k9.js +5 -0
  70. package/dist/cli/main-kba6zeyd.js +6 -0
  71. package/dist/cli/main-kwwsz6rq.js +3 -0
  72. package/dist/cli/main-m8vm17zq.js +3 -0
  73. package/dist/cli/main-mg4f96e1.js +3 -0
  74. package/dist/cli/main-mg9b20ac.js +18 -0
  75. package/dist/cli/main-mgb9ccnx.js +3 -0
  76. package/dist/cli/main-mjt2p7aj.js +3 -0
  77. package/dist/cli/main-n6qrdbmy.js +3 -0
  78. package/dist/cli/main-na7wse0x.js +5 -0
  79. package/dist/cli/main-nqveez48.js +4 -0
  80. package/dist/cli/main-ntqef02r.js +10 -0
  81. package/dist/cli/main-nvc3yjay.js +136 -0
  82. package/dist/cli/main-p0cfn6nr.js +16 -0
  83. package/dist/cli/main-qj2djy17.js +19 -0
  84. package/dist/cli/main-qsevpgsv.js +3 -0
  85. package/dist/cli/main-qvarybsp.js +3 -0
  86. package/dist/cli/main-rebtt91r.js +5 -0
  87. package/dist/cli/main-rpg7h8mb.js +3 -0
  88. package/dist/cli/main-rsy72qmw.js +15 -0
  89. package/dist/cli/main-rvetps99.js +18 -0
  90. package/dist/cli/main-s4bb0jav.js +3 -0
  91. package/dist/cli/main-s9v8k74e.js +3 -0
  92. package/dist/cli/main-tjvwmscs.js +3 -0
  93. package/dist/cli/main-tkgarpjj.js +4 -0
  94. package/dist/cli/main-v8y60bb2.js +3 -0
  95. package/dist/cli/main-vhrrq337.js +3 -0
  96. package/dist/cli/main-vp2dfb7s.js +4 -0
  97. package/dist/cli/main-vqbr22sz.js +8 -0
  98. package/dist/cli/main-vxnwe5xx.js +18 -0
  99. package/dist/cli/main-wgph00xf.js +5 -0
  100. package/dist/cli/main-wk2csfnj.js +5 -0
  101. package/dist/cli/main-wm997zjx.js +3 -0
  102. package/dist/cli/main-wpkyraxh.js +3 -0
  103. package/dist/cli/main-wqt32p5x.js +4 -0
  104. package/dist/cli/main-x9ct6y1a.js +3 -0
  105. package/dist/cli/main-xfekqh9m.js +7 -0
  106. package/dist/cli/main-xt9zc3n6.js +7 -0
  107. package/dist/cli/main-xx2z3zh5.js +4 -0
  108. package/dist/cli/main-y5c82rxr.js +3 -0
  109. package/dist/cli/main-yrjt2sqt.js +14 -0
  110. package/dist/cli/main-ys6zj3yr.js +3 -0
  111. package/dist/cli/main-ywbxshqc.js +8 -0
  112. package/dist/cli/main-z13755t8.js +25 -0
  113. package/dist/cli/main-zc7pyrbj.js +4 -0
  114. package/dist/cli/main.js +279 -0
  115. package/dist/cli/market-cmd-bm5xvn9f.js +5 -0
  116. package/dist/cli/mcp-login-bthtfpt7.js +2 -0
  117. package/dist/cli/mcp-market-cmd-mbeshfyd.js +2 -0
  118. package/dist/cli/notify-54v5z9dz.js +2 -0
  119. package/dist/cli/oauth-g5gme95c.js +2 -0
  120. package/dist/cli/output-satndjap.js +16 -0
  121. package/dist/cli/profiles-sfhpbq3m.js +2 -0
  122. package/dist/cli/provider-config-hv3xtdt4.js +2 -0
  123. package/dist/cli/provider-kwzq6g84.js +2 -0
  124. package/dist/cli/registry-fh0hdnyn.js +2 -0
  125. package/dist/cli/registry-y1y8e94r.js +2 -0
  126. package/dist/cli/repl-t4z03mqq.js +11 -0
  127. package/dist/cli/resume-fqt4chg8.js +2 -0
  128. package/dist/cli/run-flags-rysbag9t.js +2 -0
  129. package/dist/cli/runtime-j19fjbsa.js +2 -0
  130. package/dist/cli/sandbox-config-g4qxd7y5.js +2 -0
  131. package/dist/cli/server-r0b6bksk.js +5 -0
  132. package/dist/cli/session-arg-txmn5g4x.js +2 -0
  133. package/dist/cli/session-ed250d9j.js +2 -0
  134. package/dist/cli/sessions-cmd-adw7svfn.js +7 -0
  135. package/dist/cli/settings-y9rzcqx8.js +2 -0
  136. package/dist/cli/setup-jmbr11j0.js +2 -0
  137. package/dist/cli/sextant-smoke-tcth0vea.js +5 -0
  138. package/dist/cli/skills-cmd-zbdy99v6.js +2 -0
  139. package/dist/cli/smoke-1bg937kx.js +8 -0
  140. package/dist/cli/start-chat-p01cdks3.js +12 -0
  141. package/dist/cli/stream-4wmyaypz.js +2 -0
  142. package/dist/cli/task-eg4s093s.js +2 -0
  143. package/dist/cli/tasks-12v9rr9k.js +2 -0
  144. package/dist/cli/thinking-a5ngvqyh.js +2 -0
  145. package/dist/cli/todo-1wxpcecx.js +2 -0
  146. package/dist/cli/tools-2ftsya7w.js +2 -0
  147. package/dist/cli/tools-x1tj4fxm.js +2 -0
  148. package/dist/cli/trust-cmd-hccxehzb.js +2 -0
  149. package/dist/cli/update-check-ygt3vd7m.js +2 -0
  150. package/dist/cli/update-cmd-v23qhr8c.js +2 -0
  151. package/dist/cli/voice-g1gtck92.js +2 -0
  152. package/dist/cli/webfetch-0nnrjgb5.js +2 -0
  153. package/dist/cli/websearch-f0vr2p7d.js +2 -0
  154. package/dist/cli/workspace-9rq1w4ta.js +2 -0
  155. package/dist/lib/index.js +62 -0
  156. package/dist/lib/models-index.json +1 -0
  157. package/dist/lib/plugins.js +6 -0
  158. package/dist/lib/providers.js +17 -0
  159. package/dist/lib/public-api.js +20 -0
  160. package/dist/rovecode.exe +4 -0
  161. package/package.json +83 -72
  162. package/bin/rovecode.ts +0 -21
  163. package/src/acp/server.ts +0 -374
  164. package/src/cli/auth-login.ts +0 -122
  165. package/src/cli/connect.ts +0 -244
  166. package/src/cli/context-cmd.ts +0 -199
  167. package/src/cli/dispatch.ts +0 -82
  168. package/src/cli/doctor.ts +0 -362
  169. package/src/cli/export.ts +0 -276
  170. package/src/cli/help.ts +0 -293
  171. package/src/cli/is-tui-invocation.ts +0 -8
  172. package/src/cli/main.ts +0 -583
  173. package/src/cli/market-cmd.ts +0 -658
  174. package/src/cli/mcp-login.ts +0 -141
  175. package/src/cli/mcp-market-cmd.ts +0 -302
  176. package/src/cli/output.ts +0 -382
  177. package/src/cli/repl.ts +0 -250
  178. package/src/cli/repomap-root.ts +0 -14
  179. package/src/cli/resume.ts +0 -57
  180. package/src/cli/run-flags.ts +0 -43
  181. package/src/cli/run-limits.ts +0 -78
  182. package/src/cli/runtime.ts +0 -931
  183. package/src/cli/session-arg.ts +0 -30
  184. package/src/cli/sessions-cmd.ts +0 -145
  185. package/src/cli/setup.ts +0 -153
  186. package/src/cli/skills-cmd.ts +0 -194
  187. package/src/cli/start-chat.ts +0 -65
  188. package/src/cli/trust-cmd.ts +0 -52
  189. package/src/coding/bash.ts +0 -148
  190. package/src/coding/checkpoints.ts +0 -327
  191. package/src/coding/diff.ts +0 -138
  192. package/src/coding/files.ts +0 -341
  193. package/src/coding/hashline.ts +0 -274
  194. package/src/coding/lsp-gate.ts +0 -254
  195. package/src/coding/lsp-servers.ts +0 -147
  196. package/src/coding/lsp.ts +0 -283
  197. package/src/coding/repomap-cache.ts +0 -99
  198. package/src/coding/repomap-files.ts +0 -192
  199. package/src/coding/repomap.ts +0 -481
  200. package/src/core/agents.ts +0 -255
  201. package/src/core/compaction.ts +0 -259
  202. package/src/core/config.ts +0 -289
  203. package/src/core/context-report.ts +0 -228
  204. package/src/core/context.ts +0 -60
  205. package/src/core/count-remote.ts +0 -107
  206. package/src/core/execpolicy-rules.ts +0 -196
  207. package/src/core/execpolicy.ts +0 -385
  208. package/src/core/executor.ts +0 -454
  209. package/src/core/guardrails.ts +0 -400
  210. package/src/core/hooks.ts +0 -411
  211. package/src/core/images.ts +0 -230
  212. package/src/core/intro.ts +0 -266
  213. package/src/core/loop.ts +0 -567
  214. package/src/core/modes.ts +0 -372
  215. package/src/core/orchestrator.ts +0 -245
  216. package/src/core/proc-group.ts +0 -48
  217. package/src/core/project-trust.ts +0 -98
  218. package/src/core/reflection.ts +0 -165
  219. package/src/core/sandbox-config.ts +0 -186
  220. package/src/core/session-id.ts +0 -24
  221. package/src/core/session-images.ts +0 -73
  222. package/src/core/session-ops.ts +0 -183
  223. package/src/core/session-text.ts +0 -29
  224. package/src/core/session.ts +0 -469
  225. package/src/core/settings.ts +0 -170
  226. package/src/core/tasks.ts +0 -646
  227. package/src/core/token-scale.ts +0 -108
  228. package/src/core/tools.ts +0 -309
  229. package/src/core/trust.ts +0 -104
  230. package/src/core/types.ts +0 -330
  231. package/src/core/update-check.ts +0 -171
  232. package/src/core/usage.ts +0 -204
  233. package/src/core/validate.ts +0 -121
  234. package/src/core/verify-gate.ts +0 -159
  235. package/src/core/verify.ts +0 -236
  236. package/src/core/voice.ts +0 -158
  237. package/src/core/win-job.ts +0 -183
  238. package/src/core/workspace.ts +0 -184
  239. package/src/design/audit.ts +0 -797
  240. package/src/design/direction.ts +0 -190
  241. package/src/design/rules.ts +0 -157
  242. package/src/eval/bench.ts +0 -150
  243. package/src/eval/gauntlet-runner.ts +0 -215
  244. package/src/eval/gauntlet-support.ts +0 -84
  245. package/src/eval/gauntlet-wave3.ts +0 -269
  246. package/src/eval/gauntlet-wave4.ts +0 -217
  247. package/src/eval/gauntlet.ts +0 -253
  248. package/src/index.ts +0 -17
  249. package/src/lanes/agy.ts +0 -95
  250. package/src/lanes/approval.ts +0 -24
  251. package/src/lanes/claude.ts +0 -129
  252. package/src/lanes/codex.ts +0 -127
  253. package/src/lanes/events.ts +0 -130
  254. package/src/lanes/job.ts +0 -142
  255. package/src/lanes/opencode.ts +0 -122
  256. package/src/lanes/process.ts +0 -184
  257. package/src/lanes/progress.ts +0 -183
  258. package/src/lanes/registry.ts +0 -178
  259. package/src/lanes/runner.ts +0 -124
  260. package/src/lanes/types.ts +0 -112
  261. package/src/market/catalogs/mcp-docs.json +0 -111
  262. package/src/market/catalogs/plugins.json +0 -111
  263. package/src/market/catalogs/skills.json +0 -478
  264. package/src/market/clone.ts +0 -72
  265. package/src/market/context-cost.ts +0 -121
  266. package/src/market/digest.ts +0 -106
  267. package/src/market/index.ts +0 -22
  268. package/src/market/install.ts +0 -578
  269. package/src/market/manifest.ts +0 -187
  270. package/src/market/prereq.ts +0 -145
  271. package/src/market/registry.ts +0 -363
  272. package/src/market/resolve.ts +0 -111
  273. package/src/market/types.ts +0 -236
  274. package/src/market/validate.ts +0 -227
  275. package/src/mcp/client.ts +0 -449
  276. package/src/mcp/config.ts +0 -252
  277. package/src/mcp/local-package.ts +0 -211
  278. package/src/mcp/market-catalog.ts +0 -84
  279. package/src/mcp/market-install.ts +0 -289
  280. package/src/mcp/market.ts +0 -362
  281. package/src/mcp/oauth.ts +0 -251
  282. package/src/mcp/prompts-resources.ts +0 -249
  283. package/src/mcp/shared.ts +0 -149
  284. package/src/mcp/status.ts +0 -67
  285. package/src/mcp/tools.ts +0 -275
  286. package/src/mcp/transport.ts +0 -122
  287. package/src/mcp/trust.ts +0 -25
  288. package/src/memory/blocks.ts +0 -278
  289. package/src/memory/recall.ts +0 -355
  290. package/src/memory/scope.ts +0 -182
  291. package/src/memory/store.ts +0 -105
  292. package/src/memory/tools.ts +0 -99
  293. package/src/plugins/cli.ts +0 -119
  294. package/src/plugins/discover.ts +0 -108
  295. package/src/plugins/index.ts +0 -50
  296. package/src/plugins/install.ts +0 -184
  297. package/src/plugins/load.ts +0 -124
  298. package/src/plugins/manifest.ts +0 -92
  299. package/src/plugins/state.ts +0 -83
  300. package/src/providers/auth.ts +0 -408
  301. package/src/providers/cache.ts +0 -223
  302. package/src/providers/catalog-local.ts +0 -160
  303. package/src/providers/catalog.ts +0 -421
  304. package/src/providers/middleware-context.ts +0 -86
  305. package/src/providers/middleware.ts +0 -373
  306. package/src/providers/model-list.ts +0 -23
  307. package/src/providers/oauth/common.ts +0 -105
  308. package/src/providers/oauth/device-code.ts +0 -107
  309. package/src/providers/oauth/github-copilot.ts +0 -146
  310. package/src/providers/oauth/loopback.ts +0 -158
  311. package/src/providers/oauth/openai.ts +0 -163
  312. package/src/providers/oauth/openrouter.ts +0 -89
  313. package/src/providers/oauth/pkce.ts +0 -45
  314. package/src/providers/oauth/registry.ts +0 -39
  315. package/src/providers/oauth/seam.ts +0 -89
  316. package/src/providers/profile-glm53.ts +0 -111
  317. package/src/providers/profile-sonnet5-persona.ts +0 -65
  318. package/src/providers/profile-sonnet5-voice.ts +0 -23
  319. package/src/providers/profiles.ts +0 -156
  320. package/src/providers/provider-config.ts +0 -311
  321. package/src/providers/registry.ts +0 -333
  322. package/src/providers/responses.ts +0 -209
  323. package/src/providers/retry.ts +0 -234
  324. package/src/providers/router.ts +0 -294
  325. package/src/providers/sse.ts +0 -26
  326. package/src/providers/stream-errors.ts +0 -117
  327. package/src/providers/stream.ts +0 -566
  328. package/src/providers/thinking.ts +0 -189
  329. package/src/providers/wire-messages.ts +0 -129
  330. package/src/providers/wire-responses.ts +0 -79
  331. package/src/providers/wire-select.ts +0 -53
  332. package/src/server/http.ts +0 -291
  333. package/src/server/openapi.ts +0 -246
  334. package/src/sextant/card-hits.ts +0 -102
  335. package/src/sextant/card-keys.ts +0 -55
  336. package/src/sextant/context-source.ts +0 -157
  337. package/src/sextant/crew-cards.ts +0 -350
  338. package/src/sextant/draw-agents.ts +0 -273
  339. package/src/sextant/draw-code.ts +0 -388
  340. package/src/sextant/draw-context.ts +0 -222
  341. package/src/sextant/draw-frame.ts +0 -164
  342. package/src/sextant/draw-market.ts +0 -573
  343. package/src/sextant/draw-messages.ts +0 -386
  344. package/src/sextant/draw-pet.ts +0 -230
  345. package/src/sextant/draw-plan.ts +0 -187
  346. package/src/sextant/draw-tabs.ts +0 -85
  347. package/src/sextant/draw-util.ts +0 -65
  348. package/src/sextant/draw-wizard.ts +0 -378
  349. package/src/sextant/engine.ts +0 -230
  350. package/src/sextant/frame-hits.ts +0 -25
  351. package/src/sextant/frame.ts +0 -101
  352. package/src/sextant/git-status.ts +0 -197
  353. package/src/sextant/grid.ts +0 -59
  354. package/src/sextant/input.ts +0 -119
  355. package/src/sextant/keys.ts +0 -521
  356. package/src/sextant/layout.ts +0 -86
  357. package/src/sextant/local-commands.ts +0 -169
  358. package/src/sextant/market-source.ts +0 -287
  359. package/src/sextant/mentions.ts +0 -200
  360. package/src/sextant/message-hits.ts +0 -26
  361. package/src/sextant/model.ts +0 -387
  362. package/src/sextant/overlays.ts +0 -456
  363. package/src/sextant/panel-hits.ts +0 -38
  364. package/src/sextant/pet.ts +0 -399
  365. package/src/sextant/screen.ts +0 -324
  366. package/src/sextant/scroll-hits.ts +0 -66
  367. package/src/sextant/scrollbar.ts +0 -82
  368. package/src/sextant/sextant-bridge.ts +0 -174
  369. package/src/sextant/sextant-cards.ts +0 -142
  370. package/src/sextant/sextant-diff-base.ts +0 -63
  371. package/src/sextant/sextant-files.ts +0 -154
  372. package/src/sextant/sextant-frame-loop.ts +0 -335
  373. package/src/sextant/sextant-renderer.ts +0 -574
  374. package/src/sextant/sextant-repo.ts +0 -140
  375. package/src/sextant/theme.ts +0 -66
  376. package/src/sextant/tool-rows.ts +0 -189
  377. package/src/sextant/types.ts +0 -493
  378. package/src/skills/index.ts +0 -387
  379. package/src/skills/pack.ts +0 -220
  380. package/src/skills/spec.ts +0 -162
  381. package/src/skills/tools.ts +0 -69
  382. package/src/skills/versioned.ts +0 -227
  383. package/src/telemetry/otel-export.ts +0 -122
  384. package/src/telemetry/otel-lanes.ts +0 -89
  385. package/src/telemetry/otel-logs.ts +0 -131
  386. package/src/telemetry/otel-metrics.ts +0 -136
  387. package/src/telemetry/otel.ts +0 -397
  388. package/src/telemetry/otlp.ts +0 -76
  389. package/src/tools/ask-user.ts +0 -156
  390. package/src/tools/bash-bg.ts +0 -94
  391. package/src/tools/bash-jobs.ts +0 -237
  392. package/src/tools/design.ts +0 -151
  393. package/src/tools/evalcell.ts +0 -338
  394. package/src/tools/html-text.ts +0 -139
  395. package/src/tools/provider.ts +0 -149
  396. package/src/tools/task.ts +0 -250
  397. package/src/tools/todo.ts +0 -320
  398. package/src/tools/webfetch.ts +0 -332
  399. package/src/tools/websearch.ts +0 -359
  400. package/src/tui/agents-cmd.ts +0 -41
  401. package/src/tui/app.ts +0 -749
  402. package/src/tui/attach.ts +0 -127
  403. package/src/tui/boot-notes.ts +0 -41
  404. package/src/tui/builtin-prompts.ts +0 -59
  405. package/src/tui/checkpoints-cmd.ts +0 -70
  406. package/src/tui/clipboard-image.ts +0 -81
  407. package/src/tui/clipboard.ts +0 -78
  408. package/src/tui/commands.ts +0 -283
  409. package/src/tui/config-view.ts +0 -53
  410. package/src/tui/context-cmds.ts +0 -282
  411. package/src/tui/cost.ts +0 -108
  412. package/src/tui/crash-guard.ts +0 -173
  413. package/src/tui/focus-terminal.ts +0 -34
  414. package/src/tui/git-cmds.ts +0 -273
  415. package/src/tui/git-plain.ts +0 -58
  416. package/src/tui/info-cmd.ts +0 -150
  417. package/src/tui/input-plain.ts +0 -76
  418. package/src/tui/mcp-cmd.ts +0 -128
  419. package/src/tui/memory-note.ts +0 -77
  420. package/src/tui/modes-cmd.ts +0 -45
  421. package/src/tui/notify-seq.ts +0 -100
  422. package/src/tui/notify.ts +0 -318
  423. package/src/tui/overlays.ts +0 -97
  424. package/src/tui/pi-renderer.ts +0 -428
  425. package/src/tui/providers-cmd.ts +0 -377
  426. package/src/tui/reasoning-view.ts +0 -56
  427. package/src/tui/renderer.ts +0 -128
  428. package/src/tui/replay-marker.ts +0 -29
  429. package/src/tui/session-cmd.ts +0 -148
  430. package/src/tui/session-manage.ts +0 -95
  431. package/src/tui/sextant-attach.ts +0 -102
  432. package/src/tui/sextant-io.ts +0 -202
  433. package/src/tui/sextant-smoke.ts +0 -110
  434. package/src/tui/shell-cmd.ts +0 -158
  435. package/src/tui/smoke.ts +0 -72
  436. package/src/tui/staged-terminal.ts +0 -50
  437. package/src/tui/startup.ts +0 -12
  438. package/src/tui/theme.ts +0 -59
  439. package/src/tui/todo-label.ts +0 -7
  440. package/src/tui/trust-card.ts +0 -107
  441. package/src/tui/tui-commands.ts +0 -87
  442. package/tsconfig.json +0 -30
  443. package/vendor/pi-tui/LICENSE +0 -21
  444. package/vendor/pi-tui/PATCHES.md +0 -12
  445. package/vendor/pi-tui/PROVENANCE.md +0 -12
  446. package/vendor/pi-tui/README.upstream.md +0 -854
  447. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  448. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  449. package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
  450. package/vendor/pi-tui/src/autocomplete.ts +0 -827
  451. package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
  452. package/vendor/pi-tui/src/components/box.ts +0 -138
  453. package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
  454. package/vendor/pi-tui/src/components/editor.ts +0 -2364
  455. package/vendor/pi-tui/src/components/h-stack.ts +0 -45
  456. package/vendor/pi-tui/src/components/image.ts +0 -128
  457. package/vendor/pi-tui/src/components/input.ts +0 -448
  458. package/vendor/pi-tui/src/components/loader.ts +0 -93
  459. package/vendor/pi-tui/src/components/markdown.ts +0 -1016
  460. package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
  461. package/vendor/pi-tui/src/components/select-list.ts +0 -230
  462. package/vendor/pi-tui/src/components/settings-list.ts +0 -277
  463. package/vendor/pi-tui/src/components/spacer.ts +0 -29
  464. package/vendor/pi-tui/src/components/stack.ts +0 -155
  465. package/vendor/pi-tui/src/components/text.ts +0 -108
  466. package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
  467. package/vendor/pi-tui/src/components/v-stack.ts +0 -34
  468. package/vendor/pi-tui/src/editor-component.ts +0 -75
  469. package/vendor/pi-tui/src/fuzzy.ts +0 -138
  470. package/vendor/pi-tui/src/index.ts +0 -149
  471. package/vendor/pi-tui/src/keybindings.ts +0 -321
  472. package/vendor/pi-tui/src/keys.ts +0 -1402
  473. package/vendor/pi-tui/src/kill-ring.ts +0 -47
  474. package/vendor/pi-tui/src/latex.ts +0 -1381
  475. package/vendor/pi-tui/src/layout-node.ts +0 -52
  476. package/vendor/pi-tui/src/layout.ts +0 -411
  477. package/vendor/pi-tui/src/native-modifiers.ts +0 -60
  478. package/vendor/pi-tui/src/native-module-path.ts +0 -32
  479. package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
  480. package/vendor/pi-tui/src/terminal-colors.ts +0 -74
  481. package/vendor/pi-tui/src/terminal-image.ts +0 -701
  482. package/vendor/pi-tui/src/terminal.ts +0 -554
  483. package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
  484. package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
  485. package/vendor/pi-tui/src/tui.ts +0 -1264
  486. package/vendor/pi-tui/src/undo-stack.ts +0 -29
  487. package/vendor/pi-tui/src/utils.ts +0 -1327
  488. package/vendor/pi-tui/src/word-navigation.ts +0 -118
  489. package/vendor/pi-tui/test/test-themes.ts +0 -39
  490. package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
  491. /package/{src/providers → dist/cli}/models-index.json +0 -0
@@ -1,387 +0,0 @@
1
- /** Skills module (hermes pattern): SKILL.md files with frontmatter index,
2
- * mtime+size manifest invalidation, usage sidecars, deterministic staleness.
3
- * No LLM consolidation — lifecycle is a pure function of usage age.
4
- *
5
- * agentskills.io shape (port #72): the frontmatter block is still read by parseFrontmatter below — which folds
6
- * `>`/`|` block scalars, the form 3 of the 19 anthropics/skills descriptions are written in — and the FIELDS are
7
- * then validated by spec.ts validateSkillMeta in LENIENT mode: an unreadable file, a missing block and an
8
- * empty/missing description are INVALID (not loaded); every other spec deviation — a description over 1024 chars,
9
- * a missing or non-conforming name, name ≠ directory, compatibility over 500, a non-string metadata value, an
10
- * unknown key — LOADS with a line in ScanResult.warnings. Before that, such a file either loaded silently wrong or
11
- * vanished from the index with nothing said. A missing `name:` falls back to the directory name.
12
- *
13
- * A directory holding SKILL.md is a LEAF: find() does not descend into it, so a skill's own
14
- * `references/example/SKILL.md` is not a second indexed skill (it used to be, which let a skill that merely
15
- * documents itself inflate the prompt index). Dot-dirs are skipped, which is also what keeps the installer's
16
- * `.install-<token>` / `.old-<token>` staging invisible to a concurrent scan (pack.ts).
17
- *
18
- * Trust, stated as a DECISION and not an omission: a project `.rovecode/skills/` is walked whether or not the
19
- * checkout is trusted. This repository's trust gate (core/trust.ts) exists to stop a cloned repo from making us
20
- * RUN code — hooks, sandbox.json, the command-bearing settings keys, project MCP servers — and a skill is prompt
21
- * text, which is a different class of problem. What is still open, and should be written down rather than implied:
22
- * a cloned repo's skill DESCRIPTIONS do reach the system prompt through the index, so this is a prompt-injection
23
- * path, and `.rovecode/memory/MEMORY.md` is the same path by another route. Both want ONE answer (withhold from the
24
- * prompt with a boot note, self-trusting our own writes) rather than two half-answers in one binary, so the seam is
25
- * here — SkillStoreOptions.walkProject — and deliberately not wired to anything yet. `allowed-tools` is likewise
26
- * parsed and carried as DATA: nothing enforces it, and skills-spec.test.ts pins that, because a field that reads
27
- * like a permission is one refactor away from being a security claim we never implemented. */
28
-
29
- import {
30
- existsSync, readFileSync, readdirSync, renameSync, statSync, utimesSync, writeFileSync,
31
- } from "node:fs";
32
- import { rovecodeHome } from "../providers/auth.ts";
33
- import { basename, dirname, join, resolve } from "node:path";
34
- import { parseSkillFrontmatter, validateSkillMeta, type SkillFrontmatter } from "./spec.ts";
35
-
36
- /** The spec view of a file parseFrontmatter already read: its flat fields (block scalars folded) with the
37
- * shapes and the `metadata:` map that only the spec walker can see. The two readers agree on every flat
38
- * key; where they differ — an indented `metadata:` map, a list — the spec walker is the one that knows. */
39
- function specViewOf(fm: Record<string, string>, raw: string): SkillFrontmatter {
40
- const spec = parseSkillFrontmatter(raw);
41
- const fields = { ...(spec?.fields ?? {}), ...fm }; // parseFrontmatter wins on flat values (its block-scalar folding)
42
- return {
43
- fields,
44
- shapes: spec?.shapes ?? Object.fromEntries(Object.keys(fm).map((k) => [k, "scalar" as const])),
45
- metadata: spec?.metadata ?? {},
46
- metadataNonString: spec?.metadataNonString ?? [],
47
- body: spec?.body ?? "",
48
- };
49
- }
50
-
51
- export const SKILL_FILE = "SKILL.md";
52
- /** the two per-skill sidecars (usage below; the versioned.ts edit ledger) — never resources, never packed */
53
- export const SIDECAR_SUFFIXES: readonly string[] = [".usage.json", ".versions.jsonl"];
54
- export const isSidecarName = (name: string): boolean => SIDECAR_SUFFIXES.some((x) => name.endsWith(x));
55
- export const MAX_DESCRIPTION_CHARS = 60;
56
- export const STALE_AFTER_MS = 1000 * 60 * 60 * 24 * 90; // 90 days
57
-
58
- export type SkillScope = "project" | "global";
59
-
60
- export interface Skill {
61
- name: string;
62
- /** one line for the system-prompt index: clipped to MAX_DESCRIPTION_CHARS on a word boundary */
63
- description: string;
64
- /** the description as written, however long — what `skill_view` and the market show */
65
- fullDescription: string;
66
- /** top-level `version`, else `metadata.version`, else "0.0.0" */
67
- version: string;
68
- /** absolute path of the SKILL.md */
69
- path: string;
70
- /** absolute skill directory — relative paths inside the skill resolve against it */
71
- dir: string;
72
- /** content after the frontmatter block */
73
- body: string;
74
- scope: SkillScope;
75
- mtimeMs: number;
76
- size: number;
77
- license?: string;
78
- compatibility?: string;
79
- /** the `metadata:` map (string → string) */
80
- metadata: Record<string, string>;
81
- /** `allowed-tools` whitespace-split — DATA ONLY: nothing enforces it (see the header) */
82
- allowedTools: string[];
83
- }
84
-
85
- export interface InvalidSkill { path: string; reason: string }
86
- /** a skill that LOADED with a spec deviation (lenient mode) */
87
- export interface SkillWarning { path: string; reason: string }
88
-
89
- export interface SkillUsage {
90
- viewCount: number;
91
- lastViewedAt: number;
92
- }
93
-
94
- export interface ScanChanges {
95
- added: string[];
96
- removed: string[];
97
- changed: string[];
98
- }
99
-
100
- const IGNORED_DIRS: Record<string, true> = { node_modules: true, ".git": true };
101
- export interface ScanResult {
102
- skills: Skill[];
103
- invalid: InvalidSkill[];
104
- /** loaded-with-warning files of this scan (added / changed only, like `invalid`) */
105
- warnings: SkillWarning[];
106
- changes: ScanChanges;
107
- }
108
-
109
- /** The skills dir of a scope: `<cwd>/.rovecode/skills` or `<user home>/skills` (ROVECODE_HOME honoured).
110
- * The ONE place the two joins live, so the CLI and the store cannot disagree (mcpConfigPath precedent). */
111
- export function skillsDir(cwd: string, scope: SkillScope): string {
112
- return scope === "project" ? join(cwd, ".rovecode", "skills") : join(rovecodeHome(), "skills");
113
- }
114
-
115
- export interface SkillStoreOptions {
116
- /** project skills dir (default: <cwd>/.rovecode/skills) */
117
- projectDir?: string;
118
- /** global skills dir; null disables (default: ~/.rovecode/skills) */
119
- globalDir?: string | null;
120
- /** max directory depth when walking for SKILL.md (default 3) */
121
- maxDepth?: number;
122
- /** the seam named in the header: false leaves the project dir out of every scan. Nothing sets it today —
123
- * skills are prompt text, and the trust gate is about running code. Default true. */
124
- walkProject?: boolean;
125
- /** more roots — a plugin's skills folder (src/plugins) — walked like the two above, tagged with the
126
- * scope the plugin itself has (a project plugin's skills win a name clash as the project's own would) */
127
- extraDirs?: readonly { dir: string; scope: SkillScope }[];
128
- }
129
-
130
- // ---------- Frontmatter ----------
131
-
132
- /** Split `---\nkey: value` frontmatter from body. Returns null when no block.
133
- *
134
- * Values may be a plain scalar, or a YAML block scalar — `key: >` / `|` / `>-` / `|-` followed by
135
- * more-indented lines, which real SKILL.md files use for anything longer than a phrase. Measured on
136
- * anthropics/skills (2026-09-05): 3 of 19 skills write their description that way, and reading only
137
- * the header line gave `description: ">"` — not an error, just a skill whose one-line summary is a
138
- * punctuation mark, with its continuation lines mistaken for keys of their own. That is worse than
139
- * rejecting it, because nothing says anything went wrong. Folded (`>`) joins its lines with a space,
140
- * literal (`|`) keeps the newlines, a trailing `-` strips the final newline. The rest of YAML —
141
- * anchors, nested maps, lists — is still out of scope: this is a frontmatter reader, not a parser. */
142
- export function parseFrontmatter(text: string): { fm: Record<string, string>; body: string } | null {
143
- if (!text.startsWith("---")) return null;
144
- const end = text.indexOf("\n---", 3);
145
- if (end === -1) return null;
146
- const closeEnd = end + 1 + text.slice(end + 1).indexOf("\n") + 1; // past the closing `---` line
147
- const block = text.slice(4, end); // skip leading `---\n`
148
- const body = text.slice(closeEnd).replace(/^\r?\n/, "");
149
- const fm: Record<string, string> = {};
150
- const lines = block.split(/\r?\n/);
151
- for (let i = 0; i < lines.length; i++) {
152
- const line = lines[i] as string;
153
- if (/^\s/.test(line)) continue; // an orphan continuation is not a key of its own
154
- const c = line.indexOf(":");
155
- if (c === -1) continue;
156
- const k = line.slice(0, c).trim();
157
- if (!k) continue;
158
- const rest = line.slice(c + 1).trim();
159
- const m = /^([|>])([+-]?)$/.exec(rest);
160
- if (m === null) { fm[k] = rest.replace(/^["']|["']$/g, ""); continue; }
161
- const folded = m[1] === ">";
162
- const owned: string[] = [];
163
- while (i + 1 < lines.length) {
164
- const next = lines[i + 1] as string;
165
- if (next.trim() !== "" && !/^\s/.test(next)) break; // back to column 0: the block ended
166
- owned.push(next.replace(/^\s+/, ""));
167
- i++;
168
- }
169
- while (owned.length > 0 && owned[owned.length - 1] === "") owned.pop();
170
- fm[k] = folded ? owned.join(" ").replace(/\s+/g, " ").trim() : owned.join("\n");
171
- }
172
- return { fm, body };
173
- }
174
-
175
- /** Clip to the index budget on a word boundary, with an ellipsis so a reader can tell it was cut. */
176
- export function clipDescription(text: string): string {
177
- const one = text.replace(/\s+/g, " ").trim();
178
- if (one.length <= MAX_DESCRIPTION_CHARS) return one;
179
- const cut = one.slice(0, MAX_DESCRIPTION_CHARS - 1);
180
- const sp = cut.lastIndexOf(" ");
181
- return `${(sp > MAX_DESCRIPTION_CHARS / 2 ? cut.slice(0, sp) : cut).replace(/[,;:.]$/, "")}…`;
182
- }
183
-
184
- /** Lenient load: `invalid` only for an unreadable file, a missing block or an empty description; every other
185
- * spec deviation is a warning and the skill loads (see the header). */
186
- function parseSkillFile(path: string, scope: SkillScope): { skill: Skill; warnings: string[] } | { invalid: InvalidSkill } {
187
- let text: string;
188
- try {
189
- text = readFileSync(path, "utf8");
190
- } catch (e) {
191
- return { invalid: { path, reason: `unreadable: ${(e as Error).message}` } };
192
- }
193
- const parsed = parseFrontmatter(text);
194
- if (!parsed) return { invalid: { path, reason: "missing frontmatter block" } };
195
- const dir = dirname(path);
196
- // the spec field set over the SAME reader as before: parseFrontmatter's flat map plus its block scalars,
197
- // spec.ts's `metadata:` walk for the nested map, then the lenient checks
198
- const v = validateSkillMeta(specViewOf(parsed.fm, text), basename(dir), "lenient");
199
- if (v.errors.length > 0) return { invalid: { path, reason: v.errors.join("; ") } };
200
- const description = v.meta.description;
201
- // A long description is CLIPPED for the index, never a reason to reject the skill. The cap exists to
202
- // protect the system prompt (50 skills x a 950-char description is 47 KB of prefix), and clipping
203
- // protects it just as well while a hard reject throws away a working skill for being wordy. Measured
204
- // on anthropics/skills (2026-09-05): 16 of 19 real skills are over the cap, median 319 chars, longest
205
- // 950 — the whole public corpus would have been unusable. The two other places rovecode reads a
206
- // description already clip (plugins/manifest.ts, tui/commands.ts); this was the outlier.
207
- const st = statSync(path);
208
- return {
209
- skill: {
210
- name: v.meta.name,
211
- description: clipDescription(description),
212
- fullDescription: description,
213
- version: v.meta.version || "0.0.0",
214
- path, dir, body: parsed.body, scope,
215
- mtimeMs: st.mtimeMs, size: st.size,
216
- ...(v.meta.license !== undefined ? { license: v.meta.license } : {}),
217
- ...(v.meta.compatibility !== undefined ? { compatibility: v.meta.compatibility } : {}),
218
- metadata: v.meta.metadata,
219
- allowedTools: v.meta.allowedTools,
220
- },
221
- warnings: v.warnings,
222
- };
223
- }
224
-
225
- // ---------- Usage sidecar ----------
226
-
227
- export function usagePath(skillFile: string): string {
228
- return skillFile + ".usage.json";
229
- }
230
-
231
- export function readUsage(skillFile: string): SkillUsage | undefined {
232
- const p = usagePath(skillFile);
233
- if (!existsSync(p)) return undefined;
234
- try {
235
- const raw = JSON.parse(readFileSync(p, "utf8")) as Partial<SkillUsage>;
236
- return {
237
- viewCount: typeof raw.viewCount === "number" ? raw.viewCount : 0,
238
- lastViewedAt: typeof raw.lastViewedAt === "number" ? raw.lastViewedAt : 0,
239
- };
240
- } catch {
241
- return undefined; // corrupt sidecar → treat as never used
242
- }
243
- }
244
-
245
- /** Atomic write: temp file + rename, so a concurrent reader never sees a partial sidecar. */
246
- export function writeUsageAtomic(skillFile: string, usage: SkillUsage): void {
247
- const p = usagePath(skillFile);
248
- const tmp = p + ".tmp";
249
- writeFileSync(tmp, JSON.stringify(usage, null, 2));
250
- renameSync(tmp, p);
251
- }
252
-
253
- // ---------- Lifecycle ----------
254
-
255
- /** Deterministic: stale when last view (or, never viewed, file mtime) is older than 90d. */
256
- export function skillLifecycle(
257
- skill: Pick<Skill, "mtimeMs">,
258
- usage: SkillUsage | undefined,
259
- now: number,
260
- ): "active" | "stale" {
261
- const lastActivity = usage && usage.viewCount > 0 ? usage.lastViewedAt : skill.mtimeMs;
262
- return now - lastActivity > STALE_AFTER_MS ? "stale" : "active";
263
- }
264
-
265
- // ---------- Store ----------
266
-
267
- export class SkillStore {
268
- private readonly projectDir: string;
269
- private readonly globalDir: string | null;
270
- private readonly maxDepth: number;
271
- /** path → parsed skill (cache valid while manifest matches) */
272
- private cache = new Map<string, Skill>();
273
- /** path → `mtimeMs:size` (hermes manifest invalidation) */
274
- private manifest = new Map<string, string>();
275
-
276
- constructor(readonly cwd: string, opts: SkillStoreOptions = {}) {
277
- this.projectDir = resolve(cwd, opts.projectDir ?? join(".rovecode", "skills"));
278
- this.globalDir = opts.globalDir === null
279
- ? null
280
- // rovecodeHome(), not homedir(): ROVECODE_HOME is how every other part of rovecode finds its
281
- // config — hooks, settings, plugins, credentials — and skills were the one that ignored it. So a
282
- // user who set it read skills from a directory nothing else used, and every test that isolates
283
- // itself with a scratch home silently indexed the DEVELOPER's real skills instead. That is how
284
- // this was found: installing one skill into a real home broke a plugin test that never touches
285
- // skills, and the failure named a skill the test had never heard of.
286
- : resolve(opts.globalDir ?? join(rovecodeHome(), "skills"));
287
- this.maxDepth = opts.maxDepth ?? 3;
288
- this.walkProject = opts.walkProject !== false;
289
- this.extraDirs = (opts.extraDirs ?? []).map(({ dir, scope }) => [resolve(cwd, dir), scope]);
290
- }
291
- private readonly extraDirs: Array<[string, SkillScope]>;
292
- private readonly walkProject: boolean;
293
-
294
- private dirs(): Array<[string, SkillScope]> {
295
- const d: Array<[string, SkillScope]> = this.walkProject ? [[this.projectDir, "project"]] : [];
296
- if (this.globalDir && existsSync(this.globalDir)) d.push([this.globalDir, "global"]);
297
- for (const [dir, scope] of this.extraDirs) if (existsSync(dir)) d.push([dir, scope]);
298
- return d;
299
- }
300
-
301
- private find(root: string, dir: string = root, depth: number = 0): string[] {
302
- let entries;
303
- try {
304
- entries = readdirSync(dir, { withFileTypes: true });
305
- } catch {
306
- return [];
307
- }
308
- // A dir holding SKILL.md is a LEAF: its sub-dirs are that skill's own resources, never skills of their own.
309
- if (entries.some((e) => e.isFile() && e.name === SKILL_FILE)) return [join(dir, SKILL_FILE)];
310
- const found: string[] = [];
311
- for (const e of entries) {
312
- const p = join(dir, e.name);
313
- if (e.isFile() && e.name === SKILL_FILE) found.push(p);
314
- else if (e.isDirectory() && depth < this.maxDepth && !IGNORED_DIRS[e.name] && !e.name.startsWith(".")) {
315
- found.push(...this.find(root, p, depth + 1));
316
- }
317
- }
318
- return found;
319
- }
320
-
321
- /** Rescan: stat every SKILL.md, diff the manifest, reparse only added/changed files. */
322
- scan(): ScanResult {
323
- const next = new Map<string, string>();
324
- const scopeOf = new Map<string, SkillScope>(); // by root, not by path prefix: a plugin's project skills live outside <cwd>/.rovecode/skills
325
- for (const [dir, scope] of this.dirs()) {
326
- for (const p of this.find(dir)) {
327
- try {
328
- const st = statSync(p);
329
- next.set(p, `${st.mtimeMs}:${st.size}`);
330
- if (!scopeOf.has(p)) scopeOf.set(p, scope);
331
- } catch { /* vanished between readdir and stat */ }
332
- }
333
- }
334
- const changes: ScanChanges = { added: [], removed: [], changed: [] };
335
- for (const [p, sig] of next) {
336
- if (!this.manifest.has(p)) changes.added.push(p);
337
- else if (this.manifest.get(p) !== sig) changes.changed.push(p);
338
- }
339
- for (const p of this.manifest.keys()) if (!next.has(p)) changes.removed.push(p);
340
-
341
- const invalid: InvalidSkill[] = [];
342
- const warnings: SkillWarning[] = [];
343
- for (const p of [...changes.added, ...changes.changed]) {
344
- const scope = scopeOf.get(p) ?? (p.startsWith(this.projectDir) ? "project" : "global");
345
- const r = parseSkillFile(p, scope);
346
- if ("skill" in r) {
347
- this.cache.set(p, r.skill);
348
- for (const reason of r.warnings) warnings.push({ path: p, reason });
349
- } else { this.cache.delete(p); invalid.push(r.invalid); }
350
- }
351
- for (const p of changes.removed) this.cache.delete(p);
352
- this.manifest = next;
353
- return { skills: this.list(), invalid, warnings, changes };
354
- }
355
-
356
- /** Project scope wins on name collisions with the global dir. */
357
- list(): Skill[] {
358
- const byName = new Map<string, Skill>();
359
- for (const s of this.cache.values()) {
360
- const prev = byName.get(s.name);
361
- if (!prev || (prev.scope === "global" && s.scope === "project")) byName.set(s.name, s);
362
- }
363
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
364
- }
365
-
366
- get(name: string): Skill | undefined {
367
- return this.list().find((s) => s.name === name);
368
- }
369
-
370
- usage(name: string): SkillUsage | undefined {
371
- const s = this.get(name);
372
- return s ? readUsage(s.path) : undefined;
373
- }
374
-
375
- /** Record one view. Returns the updated sidecar contents. */
376
- bumpUsage(skill: Skill, now: number = Date.now()): SkillUsage {
377
- const prev = readUsage(skill.path);
378
- const usage: SkillUsage = { viewCount: (prev?.viewCount ?? 0) + 1, lastViewedAt: now };
379
- writeUsageAtomic(skill.path, usage);
380
- return usage;
381
- }
382
- }
383
-
384
- /** Touch a skill file's mtime (same size) so manifest invalidation is observable in tests. */
385
- export function touchSkillFile(path: string, at: Date = new Date()): void {
386
- utimesSync(path, at, at);
387
- }
@@ -1,220 +0,0 @@
1
- /** Skill packaging + install (port #72): `<name>.tar.gz` through the Bun.Archive built-in (no tar / zip dependency)
2
- * and a staged install finished by a rename swap. Pattern sources (no code copied): opencode-2026
3
- * skill/discovery.ts:94-124 (MIT — stage into a hidden `.tmp-<token>` dir, validate, rename the old skill aside,
4
- * rename staging in, remove the old; on failure put the old back) and the house writeUsageAtomic temp + rename
5
- * idiom (index.ts). The agentskills.io specification defines NO archive format: a tar.gz holding ONE top-level
6
- * directory named after the skill is rovecode's convention (`.skill` zips are neither produced nor consumed).
7
- *
8
- * Safety: every archive entry is checked BEFORE anything is written — the `files()` keys (checkArchiveEntries: `..`,
9
- * absolute, drive letter, backslash, empty segment, archive shape) AND the raw tar headers (findLinkMember: a symlink,
10
- * hard-link, device or fifo member is refused — `files()` lists regular files only, so those members are invisible to
11
- * the key check). The staged tree is then written file-by-file from that validated Map (materialise); libarchive's
12
- * `extract` is never called: on POSIX it would create a symlink member and write the next entry THROUGH it, outside
13
- * the staging dir, and Bun 1.3.14's `extract` also silently flattens `../x` INTO the target and drops `C:/x` (both
14
- * probed). The observable pin is "exit 2 and nothing under the skills dir changed". Staging is a dot-dir inside the
15
- * skills dir (`.install-<token>`: invisible to the scanner, same volume as the target so the rename is atomic); the
16
- * staged SKILL.md must load through the SAME lenient loader as a scan with zero invalid files before anything is renamed.
17
- * File bytes go into the archive via readFileSync — Bun issue #28459 (open): `Bun.file()` inputs yield EMPTY entries.
18
- * Pure fs + archive: cli/skills-cmd.ts owns argv, io and exit codes (SkillCmdError.code: 1 refused · 2 io / unsafe). */
19
-
20
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
21
- import { basename, dirname, join, resolve } from "node:path";
22
- import { gunzipSync } from "node:zlib";
23
- import { isSidecarName, SKILL_FILE, SkillStore } from "./index.ts";
24
-
25
- export const MAX_ARCHIVE_ENTRIES = 2000;
26
- export const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
27
- /** gunzip output cap for the header walk: a 64 MiB skill plus its headers fits, a header-only gzip bomb does not */
28
- export const INFLATE_CAP = 2 * MAX_ARCHIVE_BYTES;
29
- const SKIP_DIRS = new Set([".git", "node_modules"]);
30
-
31
- /** a refused (1) or failed (2) skills command; the CLI prints the message and exits with `code` */
32
- export class SkillCmdError extends Error {
33
- constructor(message: string, readonly code: 1 | 2) {
34
- super(message);
35
- }
36
- }
37
-
38
- /** Every regular file under `dir` as sorted posix-relative paths; usage / version sidecars, .git and node_modules
39
- * skipped; a symlink anywhere → SkillCmdError(1) (an archive must not carry links, a copy must not follow them). */
40
- export function collectSkillFiles(dir: string): string[] {
41
- const out: string[] = [];
42
- const walk = (d: string, rel: string): void => {
43
- for (const e of readdirSync(d, { withFileTypes: true })) {
44
- const r = rel === "" ? e.name : `${rel}/${e.name}`;
45
- const st = lstatSync(join(d, e.name));
46
- if (st.isSymbolicLink()) throw new SkillCmdError(`symlink refused: ${r}`, 1);
47
- if (st.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(join(d, e.name), r); }
48
- else if (st.isFile() && !isSidecarName(e.name)) out.push(r);
49
- }
50
- };
51
- walk(dir, "");
52
- return out.sort();
53
- }
54
-
55
- /** gzip tarball bytes: `<name>/<rel>` for every collected file, keys sorted (posix), contents via readFileSync. */
56
- export async function packSkill(dir: string, name: string): Promise<Uint8Array> {
57
- const entries: Record<string, Uint8Array> = {};
58
- for (const rel of collectSkillFiles(dir)) entries[`${name}/${rel}`] = readFileSync(join(dir, rel));
59
- return new Bun.Archive(entries, { compress: "gzip" }).bytes();
60
- }
61
-
62
- /** The pre-write rules over `files()` keys: the reason the archive is refused, or null when it may be unpacked. */
63
- export function checkArchiveEntries(keys: readonly string[]): string | null {
64
- if (keys.length === 0) return "archive is empty";
65
- if (keys.length > MAX_ARCHIVE_ENTRIES) return `archive has ${keys.length} entries (max ${MAX_ARCHIVE_ENTRIES})`;
66
- const tops = new Set<string>();
67
- for (const key of keys) {
68
- const shown = JSON.stringify(key);
69
- if (key.includes("\\")) return `unsafe entry ${shown}: backslash`;
70
- if (key.startsWith("/")) return `unsafe entry ${shown}: absolute path`;
71
- if (/^[A-Za-z]:/.test(key)) return `unsafe entry ${shown}: drive letter`;
72
- const segs = key.split("/");
73
- if (segs.some((s) => s === "")) return `unsafe entry ${shown}: empty path segment`;
74
- if (segs.some((s) => s === ".." || s === ".")) return `unsafe entry ${shown}: relative segment`;
75
- if (segs.length < 2) return `entry ${shown} is not inside a top-level directory`;
76
- tops.add(segs[0]!);
77
- }
78
- if (tops.size !== 1) return `archive must hold exactly one top-level directory (found ${[...tops].sort().join(", ")})`;
79
- const top = [...tops][0]!;
80
- if (!keys.includes(`${top}/${SKILL_FILE}`)) return `top-level directory ${JSON.stringify(top)} has no ${SKILL_FILE}`;
81
- return null;
82
- }
83
-
84
- /** Member types a skill archive may not carry — `files()` never lists them, so an install would silently drop a hard
85
- * link and (on POSIX, under `extract`) follow a symlink out of the staging dir. */
86
- const LINK_TYPES: Record<string, string> = { "1": "hard link", "2": "symlink", "3": "character device", "4": "block device", "6": "fifo" };
87
-
88
- /** Walk the raw ustar headers and name the first link / device / fifo member, or null. gzip (1f 8b) is inflated under
89
- * INFLATE_CAP (past it → a refusal); a body that is not ustar at its first header, or a size field the walk cannot
90
- * read, returns null — `files()` already accepted the archive and materialise() writes only its regular-file keys, so
91
- * the walk is what NAMES a malformed archive, not what keeps a missed member harmless. */
92
- export function findLinkMember(bytes: Uint8Array): string | null {
93
- let tar: Uint8Array = bytes;
94
- if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
95
- try { tar = gunzipSync(bytes, { maxOutputLength: INFLATE_CAP }); }
96
- catch (e) { return (e as NodeJS.ErrnoException).code === "ERR_BUFFER_TOO_LARGE" ? `archive inflates past ${INFLATE_CAP} bytes` : null; }
97
- }
98
- const text = (off: number, len: number): string => new TextDecoder().decode(tar.subarray(off, off + len)).replace(/\0[\s\S]*$/, "");
99
- if (text(257, 5) !== "ustar") return null;
100
- for (let off = 0; off + 512 <= tar.length && tar[off] !== 0; ) { // an all-zero block (empty name) ends the archive
101
- const size = parseInt(text(off + 124, 12).trim() || "0", 8);
102
- if (!Number.isFinite(size)) return null; // base-256 size or garbage: leave it to files()
103
- const kind = LINK_TYPES[String.fromCharCode(tar[off + 156]!)];
104
- if (kind !== undefined) {
105
- const prefix = text(off + 345, 155);
106
- return `unsafe entry ${JSON.stringify(prefix === "" ? text(off, 100) : `${prefix}/${text(off, 100)}`)}: ${kind} member`;
107
- }
108
- off += 512 + Math.ceil(size / 512) * 512;
109
- }
110
- return null;
111
- }
112
-
113
- /** Write the validated regular-file entries under `dir` — these keys are the ONLY thing that ever reaches the disk
114
- * (no `extract`: no link members, no directory members, no mode bits from the archive). */
115
- async function materialise(files: Map<string, File>, dir: string): Promise<void> {
116
- for (const [key, file] of files) {
117
- const path = join(dir, key);
118
- mkdirSync(dirname(path), { recursive: true });
119
- writeFileSync(path, new Uint8Array(await file.arrayBuffer()));
120
- }
121
- }
122
-
123
- export interface InstallOptions {
124
- /** replace an existing `<skills dir>/<name>` (rename-swap; the old copy is restored when the swap fails) */
125
- force?: boolean;
126
- /** test seam: the per-file copy used while staging a directory (default copyFileSync) */
127
- copyFile?: (src: string, dst: string) => void;
128
- /** test seam: runs after the old skill was moved aside and before staging moves in — a throw exercises the rollback */
129
- onSwap?: () => void;
130
- }
131
- export interface InstallResult { name: string; version: string; path: string; warnings: string[] }
132
-
133
- const token = (): string => Math.random().toString(36).slice(2, 10);
134
-
135
- function stagingDir(skillsDir: string): string {
136
- mkdirSync(skillsDir, { recursive: true });
137
- const dir = join(skillsDir, `.install-${token()}`);
138
- mkdirSync(dir);
139
- return dir;
140
- }
141
-
142
- function removeDir(dir: string): void {
143
- for (let attempt = 1; ; attempt++) {
144
- try { rmSync(dir, { recursive: true, force: true }); return; }
145
- catch (e) { if (attempt >= 5) throw e; Bun.sleepSync(40 * attempt); } // Windows: a closing handle inside the tree
146
- }
147
- }
148
-
149
- /** Copy a skill directory (sidecars excluded) into the skills dir under its LOADED name. */
150
- export function installFromDir(src: string, skillsDir: string, opts: InstallOptions = {}): InstallResult {
151
- const from = resolve(src);
152
- if (!existsSync(join(from, SKILL_FILE))) throw new SkillCmdError(`${from} has no ${SKILL_FILE}`, 2);
153
- const files = collectSkillFiles(from);
154
- const staging = stagingDir(skillsDir);
155
- try {
156
- const dest = join(staging, basename(from)); // the source basename, so a missing `name:` falls back to the right dir
157
- const copy = opts.copyFile ?? copyFileSync;
158
- for (const rel of files) {
159
- const to = join(dest, rel);
160
- mkdirSync(dirname(to), { recursive: true });
161
- copy(join(from, rel), to);
162
- }
163
- return finish(staging, skillsDir, opts);
164
- } catch (e) {
165
- removeDir(staging);
166
- throw e;
167
- }
168
- }
169
-
170
- /** Unpack a tar.gz body: keys AND tar headers checked BEFORE anything is written, the tree written from the validated
171
- * Map (never `extract`), then the same loader as a scan, then the rename swap. */
172
- export async function installFromBytes(bytes: Uint8Array, skillsDir: string, opts: InstallOptions = {}): Promise<InstallResult> {
173
- if (bytes.length > MAX_ARCHIVE_BYTES) throw new SkillCmdError(`archive is ${bytes.length} bytes (max ${MAX_ARCHIVE_BYTES})`, 2);
174
- let files: Map<string, File>;
175
- try { files = await new Bun.Archive(bytes).files(); }
176
- catch (e) { throw new SkillCmdError(`not a tar.gz archive: ${e instanceof Error ? e.message : String(e)}`, 2); }
177
- const reason = checkArchiveEntries([...files.keys()]) ?? findLinkMember(bytes);
178
- if (reason !== null) throw new SkillCmdError(reason, 2);
179
- let total = 0;
180
- for (const f of files.values()) total += f.size;
181
- if (total > MAX_ARCHIVE_BYTES) throw new SkillCmdError(`archive unpacks to ${total} bytes (max ${MAX_ARCHIVE_BYTES})`, 2);
182
- const staging = stagingDir(skillsDir);
183
- try {
184
- await materialise(files, staging);
185
- return finish(staging, skillsDir, opts);
186
- } catch (e) {
187
- removeDir(staging);
188
- throw e;
189
- }
190
- }
191
-
192
- /** Validate the staged tree with the runtime's loader, then swap it into place. Throws with staging still present
193
- * (the callers remove it); after a failed swap the old skill is back and no `.old-*` dir remains. */
194
- function finish(staging: string, skillsDir: string, opts: InstallOptions): InstallResult {
195
- const r = new SkillStore(skillsDir, { projectDir: staging, globalDir: null }).scan();
196
- if (r.invalid.length > 0) throw new SkillCmdError(`staged skill is invalid: ${r.invalid.map((i) => i.reason).join("; ")}`, 2);
197
- if (r.skills.length !== 1) throw new SkillCmdError(`expected exactly one skill, found ${r.skills.length}`, 2);
198
- const skill = r.skills[0]!;
199
- if (!/^[^/\\]+$/.test(skill.name) || skill.name === "." || skill.name === ".." || skill.name.startsWith(".")) {
200
- throw new SkillCmdError(`refusing skill name ${JSON.stringify(skill.name)}`, 2);
201
- }
202
- const target = join(skillsDir, skill.name);
203
- const warnings = r.warnings.map((w) => w.reason);
204
- if (existsSync(target)) {
205
- if (!opts.force) throw new SkillCmdError(`${target} already exists (use --force to replace it)`, 1);
206
- const old = join(skillsDir, `.old-${token()}`);
207
- renameSync(target, old);
208
- try {
209
- opts.onSwap?.();
210
- renameSync(skill.dir, target);
211
- } catch (e) {
212
- rmSync(target, { recursive: true, force: true });
213
- renameSync(old, target);
214
- throw e;
215
- }
216
- removeDir(old);
217
- } else renameSync(skill.dir, target);
218
- removeDir(staging);
219
- return { name: skill.name, version: skill.version, path: target, warnings };
220
- }