rovecode 0.3.3 → 0.4.0-beta.3
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.
- package/CHANGELOG.md +527 -0
- package/README.md +46 -48
- package/bin/rovecode.js +24 -0
- package/dist/cli/app-j6gn14w3.js +2 -0
- package/dist/cli/ask-user-cwstt8fz.js +2 -0
- package/dist/cli/auth-login-9bbp9915.js +2 -0
- package/dist/cli/auth-m8p9grty.js +2 -0
- package/dist/cli/bench-16zqdms5.js +9 -0
- package/dist/cli/catalog-1xchffa4.js +2 -0
- package/dist/cli/cli-1n1zb64f.js +2 -0
- package/dist/cli/client-2t9gjkck.js +2 -0
- package/dist/cli/commands-exafvm2b.js +2 -0
- package/dist/cli/connect-6zde0kn3.js +2 -0
- package/dist/cli/context-cmd-5t43wgqt.js +2 -0
- package/dist/cli/context-report-kt01pw8y.js +2 -0
- package/dist/cli/count-remote-ap7x3vh6.js +2 -0
- package/dist/cli/design-ne5zszyh.js +2 -0
- package/dist/cli/dispatch-2r5myxye.js +2 -0
- package/dist/cli/doctor-ws4fh4tn.js +3 -0
- package/dist/cli/executor-bdrjn634.js +2 -0
- package/dist/cli/export-1mxb9g5p.js +2 -0
- package/dist/cli/files-g104xghh.js +2 -0
- package/dist/cli/gauntlet-07xrjpj7.js +2 -0
- package/dist/cli/gauntlet-runner-xvy64436.js +10 -0
- package/dist/cli/gauntlet-wave3-jm91yt5w.js +5 -0
- package/dist/cli/gauntlet-wave4-r13py7p1.js +14 -0
- package/dist/cli/hashline-znvrat11.js +2 -0
- package/dist/cli/http-xafw6fsh.js +143 -0
- package/dist/cli/index-1sgjm25y.js +2 -0
- package/dist/cli/init-g2m0tn4m.js +51 -0
- package/dist/cli/install-avaqjjqq.js +2 -0
- package/dist/cli/loop-mmpfft01.js +2 -0
- package/dist/cli/main-0904f6ps.js +5 -0
- package/dist/cli/main-0ab9fc26.js +9 -0
- package/dist/cli/main-0jys2ccn.js +3 -0
- package/dist/cli/main-0mtcdbs7.js +3 -0
- package/dist/cli/main-0z1w2zsg.js +3 -0
- package/dist/cli/main-1dchs7xv.js +18 -0
- package/dist/cli/main-1ereejm1.js +3 -0
- package/dist/cli/main-1k1kw6b5.js +3 -0
- package/dist/cli/main-27y4sm2k.js +38 -0
- package/dist/cli/main-2wwjex5j.js +58 -0
- package/dist/cli/main-2yeveeve.js +6 -0
- package/dist/cli/main-2yfck9b5.js +3 -0
- package/dist/cli/main-2zmzgkwh.js +3 -0
- package/dist/cli/main-351pz3z7.js +7 -0
- package/dist/cli/main-3gjqfh7a.js +6 -0
- package/dist/cli/main-3nf3kgve.js +3 -0
- package/dist/cli/main-3pjrb2hd.js +3 -0
- package/dist/cli/main-3rxcvgna.js +19 -0
- package/dist/cli/main-4b3jgy66.js +19 -0
- package/dist/cli/main-4wndhjdc.js +7 -0
- package/dist/cli/main-4xcmvxnk.js +3 -0
- package/dist/cli/main-5tbz0wbz.js +4 -0
- package/dist/cli/main-5ywnwthm.js +3 -0
- package/dist/cli/main-6b62vkz0.js +14 -0
- package/dist/cli/main-6dnk69vp.js +3 -0
- package/dist/cli/main-6genrmhs.js +136 -0
- package/dist/cli/main-73g7eff4.js +15 -0
- package/dist/cli/main-7c5thhjd.js +5 -0
- package/dist/cli/main-7rn6bqje.js +3 -0
- package/dist/cli/main-80haw7qk.js +4 -0
- package/dist/cli/main-875s60s2.js +4 -0
- package/dist/cli/main-8kjxbpw4.js +8 -0
- package/dist/cli/main-90ds1z4e.js +10 -0
- package/dist/cli/main-9etavkew.js +3 -0
- package/dist/cli/main-a9njrkk1.js +3 -0
- package/dist/cli/main-aecrjq2d.js +12 -0
- package/dist/cli/main-ck9asesq.js +9 -0
- package/dist/cli/main-cta9racd.js +4 -0
- package/dist/cli/main-ddv7j2ag.js +3 -0
- package/dist/cli/main-dfreez27.js +10 -0
- package/dist/cli/main-f7rw7des.js +3 -0
- package/dist/cli/main-ggcn7rd7.js +5 -0
- package/dist/cli/main-gzkmycnv.js +3 -0
- package/dist/cli/main-hq51jg8v.js +18 -0
- package/dist/cli/main-jft389w9.js +8 -0
- package/dist/cli/main-k1eqkg83.js +3 -0
- package/dist/cli/main-k2y8a2aw.js +9 -0
- package/dist/cli/main-kcpbykxz.js +4 -0
- package/dist/cli/main-kd488vje.js +22 -0
- package/dist/cli/main-kh32yvgk.js +5 -0
- package/dist/cli/main-kqxnqjnv.js +25 -0
- package/dist/cli/main-kyn0xnsg.js +3 -0
- package/dist/cli/main-m1kk6fp5.js +21 -0
- package/dist/cli/main-mv40pcr2.js +4 -0
- package/dist/cli/main-n0t3973w.js +3 -0
- package/dist/cli/main-nqveez48.js +4 -0
- package/dist/cli/main-pknhvrmj.js +3 -0
- package/dist/cli/main-pn1w7a7j.js +3 -0
- package/dist/cli/main-prxxs70n.js +4 -0
- package/dist/cli/main-q3vsesf9.js +3 -0
- package/dist/cli/main-qsevpgsv.js +3 -0
- package/dist/cli/main-rdgdw24b.js +25 -0
- package/dist/cli/main-rfth4tbm.js +16 -0
- package/dist/cli/main-rg0wn0xf.js +5 -0
- package/dist/cli/main-sdmxhtv8.js +4 -0
- package/dist/cli/main-skbp13js.js +18 -0
- package/dist/cli/main-t4xnd213.js +7 -0
- package/dist/cli/main-vqak588n.js +4 -0
- package/dist/cli/main-w2n1303f.js +9 -0
- package/dist/cli/main-wbrdspr2.js +5 -0
- package/dist/cli/main-wsrg79c1.js +7 -0
- package/dist/cli/main-x4r0fne4.js +5 -0
- package/dist/cli/main-xea2f3tn.js +6 -0
- package/dist/cli/main-xg704a3c.js +3 -0
- package/dist/cli/main-xvnrabfp.js +16 -0
- package/dist/cli/main-xy53xf0r.js +4 -0
- package/dist/cli/main-y1fqy60y.js +3 -0
- package/dist/cli/main-yn8cd281.js +34 -0
- package/dist/cli/main-yr0ksc0h.js +4 -0
- package/dist/cli/main-z2ex2vyf.js +4 -0
- package/dist/cli/main-z3aayzvq.js +3 -0
- package/dist/cli/main-zaqh35jg.js +3 -0
- package/dist/cli/main-zc2e8e46.js +4 -0
- package/dist/cli/main-zzrfw6cf.js +13 -0
- package/dist/cli/main.js +280 -0
- package/dist/cli/market-cmd-e14kmx9n.js +5 -0
- package/dist/cli/mcp-login-wq7ktdek.js +2 -0
- package/dist/cli/mcp-market-cmd-9mg3jecy.js +2 -0
- package/dist/cli/notify-b7qc0cjb.js +2 -0
- package/dist/cli/oauth-z8whcgfx.js +2 -0
- package/dist/cli/output-b3ewj3ps.js +16 -0
- package/dist/cli/profiles-6mr5he5e.js +2 -0
- package/dist/cli/provider-config-g7j42q8x.js +2 -0
- package/dist/cli/provider-jr1y8vvm.js +2 -0
- package/dist/cli/registry-s8yk86g0.js +2 -0
- package/dist/cli/registry-t6p8d4mn.js +2 -0
- package/dist/cli/repl-bajwe1mh.js +11 -0
- package/dist/cli/resume-rwn9nz7y.js +2 -0
- package/dist/cli/run-flags-nah7ndpt.js +2 -0
- package/dist/cli/runtime-n7gafzhb.js +2 -0
- package/dist/cli/sandbox-config-emdy18x4.js +2 -0
- package/dist/cli/server-b0nvs2bn.js +5 -0
- package/dist/cli/session-arg-y75wd4kj.js +2 -0
- package/dist/cli/session-j62evmjq.js +2 -0
- package/dist/cli/sessions-cmd-tsnwz0ns.js +7 -0
- package/dist/cli/settings-df10wfez.js +2 -0
- package/dist/cli/setup-jzvv72fg.js +2 -0
- package/dist/cli/sextant-smoke-37m81ke6.js +5 -0
- package/dist/cli/skills-cmd-gjxnxnhx.js +2 -0
- package/dist/cli/smoke-p7748apt.js +8 -0
- package/dist/cli/start-chat-s4st3mm0.js +12 -0
- package/dist/cli/stream-gmeyewds.js +2 -0
- package/dist/cli/task-gh0kkp3n.js +2 -0
- package/dist/cli/tasks-z1kfpe8e.js +2 -0
- package/dist/cli/thinking-0eqkrz6t.js +2 -0
- package/dist/cli/todo-5brcrt9m.js +2 -0
- package/dist/cli/tools-7pzm0vj9.js +2 -0
- package/dist/cli/tools-s635p6s8.js +2 -0
- package/dist/cli/trust-cmd-cjav8zgm.js +2 -0
- package/dist/cli/update-check-pt31bm2f.js +2 -0
- package/dist/cli/update-cmd-tk131s9t.js +2 -0
- package/dist/cli/voice-56nabd8d.js +2 -0
- package/dist/cli/webfetch-xd8q596m.js +2 -0
- package/dist/cli/websearch-5hkf98k1.js +2 -0
- package/dist/cli/workflow-cmd-cy3cvzjp.js +4 -0
- package/dist/cli/workspace-q10g5z3e.js +2 -0
- package/dist/lib/index.js +62 -0
- package/dist/lib/models-index.json +1 -0
- package/dist/lib/plugins.js +55 -0
- package/dist/lib/providers.js +17 -0
- package/dist/lib/public-api.js +20 -0
- package/dist/lib/sdk.js +360 -0
- package/package.json +84 -72
- package/bin/rovecode.ts +0 -21
- package/src/acp/server.ts +0 -374
- package/src/cli/auth-login.ts +0 -122
- package/src/cli/connect.ts +0 -244
- package/src/cli/context-cmd.ts +0 -199
- package/src/cli/dispatch.ts +0 -82
- package/src/cli/doctor.ts +0 -362
- package/src/cli/export.ts +0 -276
- package/src/cli/help.ts +0 -293
- package/src/cli/is-tui-invocation.ts +0 -8
- package/src/cli/main.ts +0 -583
- package/src/cli/market-cmd.ts +0 -658
- package/src/cli/mcp-login.ts +0 -141
- package/src/cli/mcp-market-cmd.ts +0 -302
- package/src/cli/output.ts +0 -382
- package/src/cli/repl.ts +0 -250
- package/src/cli/repomap-root.ts +0 -14
- package/src/cli/resume.ts +0 -57
- package/src/cli/run-flags.ts +0 -43
- package/src/cli/run-limits.ts +0 -78
- package/src/cli/runtime.ts +0 -931
- package/src/cli/session-arg.ts +0 -30
- package/src/cli/sessions-cmd.ts +0 -145
- package/src/cli/setup.ts +0 -153
- package/src/cli/skills-cmd.ts +0 -194
- package/src/cli/start-chat.ts +0 -65
- package/src/cli/trust-cmd.ts +0 -52
- package/src/coding/bash.ts +0 -148
- package/src/coding/checkpoints.ts +0 -327
- package/src/coding/diff.ts +0 -138
- package/src/coding/files.ts +0 -341
- package/src/coding/hashline.ts +0 -274
- package/src/coding/lsp-gate.ts +0 -254
- package/src/coding/lsp-servers.ts +0 -147
- package/src/coding/lsp.ts +0 -283
- package/src/coding/repomap-cache.ts +0 -99
- package/src/coding/repomap-files.ts +0 -192
- package/src/coding/repomap.ts +0 -481
- package/src/core/agents.ts +0 -255
- package/src/core/compaction.ts +0 -259
- package/src/core/config.ts +0 -289
- package/src/core/context-report.ts +0 -228
- package/src/core/context.ts +0 -60
- package/src/core/count-remote.ts +0 -107
- package/src/core/execpolicy-rules.ts +0 -196
- package/src/core/execpolicy.ts +0 -385
- package/src/core/executor.ts +0 -454
- package/src/core/guardrails.ts +0 -400
- package/src/core/hooks.ts +0 -411
- package/src/core/images.ts +0 -230
- package/src/core/intro.ts +0 -266
- package/src/core/loop.ts +0 -567
- package/src/core/modes.ts +0 -372
- package/src/core/orchestrator.ts +0 -245
- package/src/core/proc-group.ts +0 -48
- package/src/core/project-trust.ts +0 -98
- package/src/core/reflection.ts +0 -165
- package/src/core/sandbox-config.ts +0 -186
- package/src/core/session-id.ts +0 -24
- package/src/core/session-images.ts +0 -73
- package/src/core/session-ops.ts +0 -183
- package/src/core/session-text.ts +0 -29
- package/src/core/session.ts +0 -469
- package/src/core/settings.ts +0 -170
- package/src/core/tasks.ts +0 -646
- package/src/core/token-scale.ts +0 -108
- package/src/core/tools.ts +0 -309
- package/src/core/trust.ts +0 -104
- package/src/core/types.ts +0 -330
- package/src/core/update-check.ts +0 -171
- package/src/core/usage.ts +0 -204
- package/src/core/validate.ts +0 -121
- package/src/core/verify-gate.ts +0 -159
- package/src/core/verify.ts +0 -236
- package/src/core/voice.ts +0 -158
- package/src/core/win-job.ts +0 -183
- package/src/core/workspace.ts +0 -184
- package/src/design/audit.ts +0 -797
- package/src/design/direction.ts +0 -190
- package/src/design/rules.ts +0 -157
- package/src/eval/bench.ts +0 -150
- package/src/eval/gauntlet-runner.ts +0 -215
- package/src/eval/gauntlet-support.ts +0 -84
- package/src/eval/gauntlet-wave3.ts +0 -269
- package/src/eval/gauntlet-wave4.ts +0 -217
- package/src/eval/gauntlet.ts +0 -253
- package/src/index.ts +0 -17
- package/src/lanes/agy.ts +0 -95
- package/src/lanes/approval.ts +0 -24
- package/src/lanes/claude.ts +0 -129
- package/src/lanes/codex.ts +0 -127
- package/src/lanes/events.ts +0 -130
- package/src/lanes/job.ts +0 -142
- package/src/lanes/opencode.ts +0 -122
- package/src/lanes/process.ts +0 -184
- package/src/lanes/progress.ts +0 -183
- package/src/lanes/registry.ts +0 -178
- package/src/lanes/runner.ts +0 -124
- package/src/lanes/types.ts +0 -112
- package/src/market/catalogs/mcp-docs.json +0 -111
- package/src/market/catalogs/plugins.json +0 -111
- package/src/market/catalogs/skills.json +0 -478
- package/src/market/clone.ts +0 -72
- package/src/market/context-cost.ts +0 -121
- package/src/market/digest.ts +0 -106
- package/src/market/index.ts +0 -22
- package/src/market/install.ts +0 -578
- package/src/market/manifest.ts +0 -187
- package/src/market/prereq.ts +0 -145
- package/src/market/registry.ts +0 -363
- package/src/market/resolve.ts +0 -111
- package/src/market/types.ts +0 -236
- package/src/market/validate.ts +0 -227
- package/src/mcp/client.ts +0 -449
- package/src/mcp/config.ts +0 -252
- package/src/mcp/local-package.ts +0 -211
- package/src/mcp/market-catalog.ts +0 -84
- package/src/mcp/market-install.ts +0 -289
- package/src/mcp/market.ts +0 -362
- package/src/mcp/oauth.ts +0 -251
- package/src/mcp/prompts-resources.ts +0 -249
- package/src/mcp/shared.ts +0 -149
- package/src/mcp/status.ts +0 -67
- package/src/mcp/tools.ts +0 -275
- package/src/mcp/transport.ts +0 -122
- package/src/mcp/trust.ts +0 -25
- package/src/memory/blocks.ts +0 -278
- package/src/memory/recall.ts +0 -355
- package/src/memory/scope.ts +0 -182
- package/src/memory/store.ts +0 -105
- package/src/memory/tools.ts +0 -99
- package/src/plugins/cli.ts +0 -119
- package/src/plugins/discover.ts +0 -108
- package/src/plugins/index.ts +0 -50
- package/src/plugins/install.ts +0 -184
- package/src/plugins/load.ts +0 -124
- package/src/plugins/manifest.ts +0 -92
- package/src/plugins/state.ts +0 -83
- package/src/providers/auth.ts +0 -408
- package/src/providers/cache.ts +0 -223
- package/src/providers/catalog-local.ts +0 -160
- package/src/providers/catalog.ts +0 -421
- package/src/providers/middleware-context.ts +0 -86
- package/src/providers/middleware.ts +0 -373
- package/src/providers/model-list.ts +0 -23
- package/src/providers/oauth/common.ts +0 -105
- package/src/providers/oauth/device-code.ts +0 -107
- package/src/providers/oauth/github-copilot.ts +0 -146
- package/src/providers/oauth/loopback.ts +0 -158
- package/src/providers/oauth/openai.ts +0 -163
- package/src/providers/oauth/openrouter.ts +0 -89
- package/src/providers/oauth/pkce.ts +0 -45
- package/src/providers/oauth/registry.ts +0 -39
- package/src/providers/oauth/seam.ts +0 -89
- package/src/providers/profile-glm53.ts +0 -111
- package/src/providers/profile-sonnet5-persona.ts +0 -65
- package/src/providers/profile-sonnet5-voice.ts +0 -23
- package/src/providers/profiles.ts +0 -156
- package/src/providers/provider-config.ts +0 -311
- package/src/providers/registry.ts +0 -333
- package/src/providers/responses.ts +0 -209
- package/src/providers/retry.ts +0 -234
- package/src/providers/router.ts +0 -294
- package/src/providers/sse.ts +0 -26
- package/src/providers/stream-errors.ts +0 -117
- package/src/providers/stream.ts +0 -566
- package/src/providers/thinking.ts +0 -189
- package/src/providers/wire-messages.ts +0 -129
- package/src/providers/wire-responses.ts +0 -79
- package/src/providers/wire-select.ts +0 -53
- package/src/server/http.ts +0 -291
- package/src/server/openapi.ts +0 -246
- package/src/sextant/card-hits.ts +0 -102
- package/src/sextant/card-keys.ts +0 -55
- package/src/sextant/context-source.ts +0 -157
- package/src/sextant/crew-cards.ts +0 -350
- package/src/sextant/draw-agents.ts +0 -273
- package/src/sextant/draw-code.ts +0 -388
- package/src/sextant/draw-context.ts +0 -222
- package/src/sextant/draw-frame.ts +0 -164
- package/src/sextant/draw-market.ts +0 -573
- package/src/sextant/draw-messages.ts +0 -386
- package/src/sextant/draw-pet.ts +0 -230
- package/src/sextant/draw-plan.ts +0 -187
- package/src/sextant/draw-tabs.ts +0 -85
- package/src/sextant/draw-util.ts +0 -65
- package/src/sextant/draw-wizard.ts +0 -378
- package/src/sextant/engine.ts +0 -230
- package/src/sextant/frame-hits.ts +0 -25
- package/src/sextant/frame.ts +0 -101
- package/src/sextant/git-status.ts +0 -197
- package/src/sextant/grid.ts +0 -59
- package/src/sextant/input.ts +0 -119
- package/src/sextant/keys.ts +0 -521
- package/src/sextant/layout.ts +0 -86
- package/src/sextant/local-commands.ts +0 -169
- package/src/sextant/market-source.ts +0 -287
- package/src/sextant/mentions.ts +0 -200
- package/src/sextant/message-hits.ts +0 -26
- package/src/sextant/model.ts +0 -387
- package/src/sextant/overlays.ts +0 -456
- package/src/sextant/panel-hits.ts +0 -38
- package/src/sextant/pet.ts +0 -399
- package/src/sextant/screen.ts +0 -324
- package/src/sextant/scroll-hits.ts +0 -66
- package/src/sextant/scrollbar.ts +0 -82
- package/src/sextant/sextant-bridge.ts +0 -174
- package/src/sextant/sextant-cards.ts +0 -142
- package/src/sextant/sextant-diff-base.ts +0 -63
- package/src/sextant/sextant-files.ts +0 -154
- package/src/sextant/sextant-frame-loop.ts +0 -335
- package/src/sextant/sextant-renderer.ts +0 -574
- package/src/sextant/sextant-repo.ts +0 -140
- package/src/sextant/theme.ts +0 -66
- package/src/sextant/tool-rows.ts +0 -189
- package/src/sextant/types.ts +0 -493
- package/src/skills/index.ts +0 -387
- package/src/skills/pack.ts +0 -220
- package/src/skills/spec.ts +0 -162
- package/src/skills/tools.ts +0 -69
- package/src/skills/versioned.ts +0 -227
- package/src/telemetry/otel-export.ts +0 -122
- package/src/telemetry/otel-lanes.ts +0 -89
- package/src/telemetry/otel-logs.ts +0 -131
- package/src/telemetry/otel-metrics.ts +0 -136
- package/src/telemetry/otel.ts +0 -397
- package/src/telemetry/otlp.ts +0 -76
- package/src/tools/ask-user.ts +0 -156
- package/src/tools/bash-bg.ts +0 -94
- package/src/tools/bash-jobs.ts +0 -237
- package/src/tools/design.ts +0 -151
- package/src/tools/evalcell.ts +0 -338
- package/src/tools/html-text.ts +0 -139
- package/src/tools/provider.ts +0 -149
- package/src/tools/task.ts +0 -250
- package/src/tools/todo.ts +0 -320
- package/src/tools/webfetch.ts +0 -332
- package/src/tools/websearch.ts +0 -359
- package/src/tui/agents-cmd.ts +0 -41
- package/src/tui/app.ts +0 -749
- package/src/tui/attach.ts +0 -127
- package/src/tui/boot-notes.ts +0 -41
- package/src/tui/builtin-prompts.ts +0 -59
- package/src/tui/checkpoints-cmd.ts +0 -70
- package/src/tui/clipboard-image.ts +0 -81
- package/src/tui/clipboard.ts +0 -78
- package/src/tui/commands.ts +0 -283
- package/src/tui/config-view.ts +0 -53
- package/src/tui/context-cmds.ts +0 -282
- package/src/tui/cost.ts +0 -108
- package/src/tui/crash-guard.ts +0 -173
- package/src/tui/focus-terminal.ts +0 -34
- package/src/tui/git-cmds.ts +0 -273
- package/src/tui/git-plain.ts +0 -58
- package/src/tui/info-cmd.ts +0 -150
- package/src/tui/input-plain.ts +0 -76
- package/src/tui/mcp-cmd.ts +0 -128
- package/src/tui/memory-note.ts +0 -77
- package/src/tui/modes-cmd.ts +0 -45
- package/src/tui/notify-seq.ts +0 -100
- package/src/tui/notify.ts +0 -318
- package/src/tui/overlays.ts +0 -97
- package/src/tui/pi-renderer.ts +0 -428
- package/src/tui/providers-cmd.ts +0 -377
- package/src/tui/reasoning-view.ts +0 -56
- package/src/tui/renderer.ts +0 -128
- package/src/tui/replay-marker.ts +0 -29
- package/src/tui/session-cmd.ts +0 -148
- package/src/tui/session-manage.ts +0 -95
- package/src/tui/sextant-attach.ts +0 -102
- package/src/tui/sextant-io.ts +0 -202
- package/src/tui/sextant-smoke.ts +0 -110
- package/src/tui/shell-cmd.ts +0 -158
- package/src/tui/smoke.ts +0 -72
- package/src/tui/staged-terminal.ts +0 -50
- package/src/tui/startup.ts +0 -12
- package/src/tui/theme.ts +0 -59
- package/src/tui/todo-label.ts +0 -7
- package/src/tui/trust-card.ts +0 -107
- package/src/tui/tui-commands.ts +0 -87
- package/tsconfig.json +0 -30
- package/vendor/pi-tui/LICENSE +0 -21
- package/vendor/pi-tui/PATCHES.md +0 -12
- package/vendor/pi-tui/PROVENANCE.md +0 -12
- package/vendor/pi-tui/README.upstream.md +0 -854
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
- package/vendor/pi-tui/src/autocomplete.ts +0 -827
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
- package/vendor/pi-tui/src/components/box.ts +0 -138
- package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
- package/vendor/pi-tui/src/components/editor.ts +0 -2364
- package/vendor/pi-tui/src/components/h-stack.ts +0 -45
- package/vendor/pi-tui/src/components/image.ts +0 -128
- package/vendor/pi-tui/src/components/input.ts +0 -448
- package/vendor/pi-tui/src/components/loader.ts +0 -93
- package/vendor/pi-tui/src/components/markdown.ts +0 -1016
- package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
- package/vendor/pi-tui/src/components/select-list.ts +0 -230
- package/vendor/pi-tui/src/components/settings-list.ts +0 -277
- package/vendor/pi-tui/src/components/spacer.ts +0 -29
- package/vendor/pi-tui/src/components/stack.ts +0 -155
- package/vendor/pi-tui/src/components/text.ts +0 -108
- package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
- package/vendor/pi-tui/src/components/v-stack.ts +0 -34
- package/vendor/pi-tui/src/editor-component.ts +0 -75
- package/vendor/pi-tui/src/fuzzy.ts +0 -138
- package/vendor/pi-tui/src/index.ts +0 -149
- package/vendor/pi-tui/src/keybindings.ts +0 -321
- package/vendor/pi-tui/src/keys.ts +0 -1402
- package/vendor/pi-tui/src/kill-ring.ts +0 -47
- package/vendor/pi-tui/src/latex.ts +0 -1381
- package/vendor/pi-tui/src/layout-node.ts +0 -52
- package/vendor/pi-tui/src/layout.ts +0 -411
- package/vendor/pi-tui/src/native-modifiers.ts +0 -60
- package/vendor/pi-tui/src/native-module-path.ts +0 -32
- package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
- package/vendor/pi-tui/src/terminal-colors.ts +0 -74
- package/vendor/pi-tui/src/terminal-image.ts +0 -701
- package/vendor/pi-tui/src/terminal.ts +0 -554
- package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
- package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
- package/vendor/pi-tui/src/tui.ts +0 -1264
- package/vendor/pi-tui/src/undo-stack.ts +0 -29
- package/vendor/pi-tui/src/utils.ts +0 -1327
- package/vendor/pi-tui/src/word-navigation.ts +0 -118
- package/vendor/pi-tui/test/test-themes.ts +0 -39
- package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
- /package/{src/providers → dist/cli}/models-index.json +0 -0
package/src/skills/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/skills/pack.ts
DELETED
|
@@ -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
|
-
}
|