rovecode 0.4.0-beta.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -72
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -38
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- 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 +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -527
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-j6gn14w3.js +0 -2
- package/dist/cli/ask-user-cwstt8fz.js +0 -2
- package/dist/cli/auth-login-9bbp9915.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-16zqdms5.js +0 -9
- package/dist/cli/catalog-1xchffa4.js +0 -2
- package/dist/cli/cli-1n1zb64f.js +0 -2
- package/dist/cli/client-2t9gjkck.js +0 -2
- package/dist/cli/commands-exafvm2b.js +0 -2
- package/dist/cli/connect-6zde0kn3.js +0 -2
- package/dist/cli/context-cmd-5t43wgqt.js +0 -2
- package/dist/cli/context-report-kt01pw8y.js +0 -2
- package/dist/cli/count-remote-ap7x3vh6.js +0 -2
- package/dist/cli/design-ne5zszyh.js +0 -2
- package/dist/cli/dispatch-2r5myxye.js +0 -2
- package/dist/cli/doctor-ws4fh4tn.js +0 -3
- package/dist/cli/executor-bdrjn634.js +0 -2
- package/dist/cli/export-1mxb9g5p.js +0 -2
- package/dist/cli/files-g104xghh.js +0 -2
- package/dist/cli/gauntlet-07xrjpj7.js +0 -2
- package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
- package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
- package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
- package/dist/cli/hashline-znvrat11.js +0 -2
- package/dist/cli/http-xafw6fsh.js +0 -143
- package/dist/cli/index-1sgjm25y.js +0 -2
- package/dist/cli/init-g2m0tn4m.js +0 -51
- package/dist/cli/install-avaqjjqq.js +0 -2
- package/dist/cli/loop-mmpfft01.js +0 -2
- package/dist/cli/main-0904f6ps.js +0 -5
- package/dist/cli/main-0ab9fc26.js +0 -9
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-0mtcdbs7.js +0 -3
- package/dist/cli/main-0z1w2zsg.js +0 -3
- package/dist/cli/main-1dchs7xv.js +0 -18
- package/dist/cli/main-1ereejm1.js +0 -3
- package/dist/cli/main-1k1kw6b5.js +0 -3
- package/dist/cli/main-27y4sm2k.js +0 -38
- package/dist/cli/main-2wwjex5j.js +0 -58
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2yfck9b5.js +0 -3
- package/dist/cli/main-2zmzgkwh.js +0 -3
- package/dist/cli/main-351pz3z7.js +0 -7
- package/dist/cli/main-3gjqfh7a.js +0 -6
- package/dist/cli/main-3nf3kgve.js +0 -3
- package/dist/cli/main-3pjrb2hd.js +0 -3
- package/dist/cli/main-3rxcvgna.js +0 -19
- package/dist/cli/main-4b3jgy66.js +0 -19
- package/dist/cli/main-4wndhjdc.js +0 -7
- package/dist/cli/main-4xcmvxnk.js +0 -3
- package/dist/cli/main-5tbz0wbz.js +0 -4
- package/dist/cli/main-5ywnwthm.js +0 -3
- package/dist/cli/main-6b62vkz0.js +0 -14
- package/dist/cli/main-6dnk69vp.js +0 -3
- package/dist/cli/main-6genrmhs.js +0 -136
- package/dist/cli/main-73g7eff4.js +0 -15
- package/dist/cli/main-7c5thhjd.js +0 -5
- package/dist/cli/main-7rn6bqje.js +0 -3
- package/dist/cli/main-80haw7qk.js +0 -4
- package/dist/cli/main-875s60s2.js +0 -4
- package/dist/cli/main-8kjxbpw4.js +0 -8
- package/dist/cli/main-90ds1z4e.js +0 -10
- package/dist/cli/main-9etavkew.js +0 -3
- package/dist/cli/main-a9njrkk1.js +0 -3
- package/dist/cli/main-aecrjq2d.js +0 -12
- package/dist/cli/main-ck9asesq.js +0 -9
- package/dist/cli/main-cta9racd.js +0 -4
- package/dist/cli/main-ddv7j2ag.js +0 -3
- package/dist/cli/main-dfreez27.js +0 -10
- package/dist/cli/main-f7rw7des.js +0 -3
- package/dist/cli/main-ggcn7rd7.js +0 -5
- package/dist/cli/main-gzkmycnv.js +0 -3
- package/dist/cli/main-hq51jg8v.js +0 -18
- package/dist/cli/main-jft389w9.js +0 -8
- package/dist/cli/main-k1eqkg83.js +0 -3
- package/dist/cli/main-k2y8a2aw.js +0 -9
- package/dist/cli/main-kcpbykxz.js +0 -4
- package/dist/cli/main-kd488vje.js +0 -22
- package/dist/cli/main-kh32yvgk.js +0 -5
- package/dist/cli/main-kqxnqjnv.js +0 -25
- package/dist/cli/main-kyn0xnsg.js +0 -3
- package/dist/cli/main-m1kk6fp5.js +0 -21
- package/dist/cli/main-mv40pcr2.js +0 -4
- package/dist/cli/main-n0t3973w.js +0 -3
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-pknhvrmj.js +0 -3
- package/dist/cli/main-pn1w7a7j.js +0 -3
- package/dist/cli/main-prxxs70n.js +0 -4
- package/dist/cli/main-q3vsesf9.js +0 -3
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-rdgdw24b.js +0 -25
- package/dist/cli/main-rfth4tbm.js +0 -16
- package/dist/cli/main-rg0wn0xf.js +0 -5
- package/dist/cli/main-sdmxhtv8.js +0 -4
- package/dist/cli/main-skbp13js.js +0 -18
- package/dist/cli/main-t4xnd213.js +0 -7
- package/dist/cli/main-vqak588n.js +0 -4
- package/dist/cli/main-w2n1303f.js +0 -9
- package/dist/cli/main-wbrdspr2.js +0 -5
- package/dist/cli/main-wsrg79c1.js +0 -7
- package/dist/cli/main-x4r0fne4.js +0 -5
- package/dist/cli/main-xea2f3tn.js +0 -6
- package/dist/cli/main-xg704a3c.js +0 -3
- package/dist/cli/main-xvnrabfp.js +0 -16
- package/dist/cli/main-xy53xf0r.js +0 -4
- package/dist/cli/main-y1fqy60y.js +0 -3
- package/dist/cli/main-yn8cd281.js +0 -34
- package/dist/cli/main-yr0ksc0h.js +0 -4
- package/dist/cli/main-z2ex2vyf.js +0 -4
- package/dist/cli/main-z3aayzvq.js +0 -3
- package/dist/cli/main-zaqh35jg.js +0 -3
- package/dist/cli/main-zc2e8e46.js +0 -4
- package/dist/cli/main-zzrfw6cf.js +0 -13
- package/dist/cli/main.js +0 -280
- package/dist/cli/market-cmd-e14kmx9n.js +0 -5
- package/dist/cli/mcp-login-wq7ktdek.js +0 -2
- package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
- package/dist/cli/notify-b7qc0cjb.js +0 -2
- package/dist/cli/oauth-z8whcgfx.js +0 -2
- package/dist/cli/output-b3ewj3ps.js +0 -16
- package/dist/cli/profiles-6mr5he5e.js +0 -2
- package/dist/cli/provider-config-g7j42q8x.js +0 -2
- package/dist/cli/provider-jr1y8vvm.js +0 -2
- package/dist/cli/registry-s8yk86g0.js +0 -2
- package/dist/cli/registry-t6p8d4mn.js +0 -2
- package/dist/cli/repl-bajwe1mh.js +0 -11
- package/dist/cli/resume-rwn9nz7y.js +0 -2
- package/dist/cli/run-flags-nah7ndpt.js +0 -2
- package/dist/cli/runtime-n7gafzhb.js +0 -2
- package/dist/cli/sandbox-config-emdy18x4.js +0 -2
- package/dist/cli/server-b0nvs2bn.js +0 -5
- package/dist/cli/session-arg-y75wd4kj.js +0 -2
- package/dist/cli/session-j62evmjq.js +0 -2
- package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
- package/dist/cli/settings-df10wfez.js +0 -2
- package/dist/cli/setup-jzvv72fg.js +0 -2
- package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
- package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
- package/dist/cli/smoke-p7748apt.js +0 -8
- package/dist/cli/start-chat-s4st3mm0.js +0 -12
- package/dist/cli/stream-gmeyewds.js +0 -2
- package/dist/cli/task-gh0kkp3n.js +0 -2
- package/dist/cli/tasks-z1kfpe8e.js +0 -2
- package/dist/cli/thinking-0eqkrz6t.js +0 -2
- package/dist/cli/todo-5brcrt9m.js +0 -2
- package/dist/cli/tools-7pzm0vj9.js +0 -2
- package/dist/cli/tools-s635p6s8.js +0 -2
- package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
- package/dist/cli/update-check-pt31bm2f.js +0 -2
- package/dist/cli/update-cmd-tk131s9t.js +0 -2
- package/dist/cli/voice-56nabd8d.js +0 -2
- package/dist/cli/webfetch-xd8q596m.js +0 -2
- package/dist/cli/websearch-5hkf98k1.js +0 -2
- package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
- package/dist/cli/workspace-q10g5z3e.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -55
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/lib/sdk.js +0 -360
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Port #34 TUI wiring for image input: `/attach` stages image parts for the next user message,
|
|
2
|
+
* user turns echo (and replay) with image chips, and the stage survives a session switch.
|
|
3
|
+
*
|
|
4
|
+
* The stage is the SessionStore's own (stageAttachments / stagedAttachments): the store folds it
|
|
5
|
+
* into the NEXT role:"user" entry that lands in append() — the run's goal message when idle
|
|
6
|
+
* (loop.ts userMsg), the queued steer when the loop drains it mid-run (loop.ts steering drain) —
|
|
7
|
+
* so the ONE agentLoop (ADR-003) needs no attach change and the TUI keeps no second copy of the
|
|
8
|
+
* list. Consequences, documented rather than hidden:
|
|
9
|
+
* - a steer queued BEFORE /attach is the next user entry and takes the images instead; if the
|
|
10
|
+
* run ends before any steer is drained, the images ride on the next run's goal message and the
|
|
11
|
+
* steer text follows it in the same request — nothing is ever dropped;
|
|
12
|
+
* - /sessions, /resume and a root /rewind replace the store INSTANCE: switchSession carries the
|
|
13
|
+
* stage across with a visible note (carryOverAttachments); same-store /new and /rewind move
|
|
14
|
+
* only the leaf, so the stage stays put;
|
|
15
|
+
* - no image-only messages: the editor drops empty input (pi-renderer.ts onSubmit), so text is
|
|
16
|
+
* required and every attach note says so — no "(see attached image)" text is invented on the
|
|
17
|
+
* user's behalf.
|
|
18
|
+
* Vision check at attach time (catalog.supportsImages on the current mode's model): false → warn
|
|
19
|
+
* (wire-messages.ts will send a text placeholder), undefined → info (unknown model: sent as-is,
|
|
20
|
+
* the provider may reject). Paths resolve against the runtime cwd; the loader sniffs magic bytes
|
|
21
|
+
* (core/images.ts), never the extension. */
|
|
22
|
+
|
|
23
|
+
import { resolve } from "node:path";
|
|
24
|
+
import { checkImageCount, describeImage, imageChip, imageFromBytes, loadImageAttachment, MAX_IMAGES_PER_MESSAGE } from "../core/images.ts";
|
|
25
|
+
import { clipboardImageName, readClipboardImage } from "./clipboard-image.ts";
|
|
26
|
+
import type { SessionStore } from "../core/session.ts";
|
|
27
|
+
import type { ImagePart, MessagePart } from "../core/types.ts";
|
|
28
|
+
import { supportsImages } from "../providers/catalog.ts";
|
|
29
|
+
import type { Renderer, SlashCommand } from "./renderer.ts";
|
|
30
|
+
|
|
31
|
+
/** The TUI_COMMANDS entry (/help, palette, custom-command reserved names). */
|
|
32
|
+
export const ATTACH_COMMAND: SlashCommand = {
|
|
33
|
+
name: "attach",
|
|
34
|
+
description: "Attach an image to your next message (text required): /attach <path> · /attach = list · /attach clear · or drag an image file onto the terminal",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface AttachCtx {
|
|
38
|
+
renderer: Renderer;
|
|
39
|
+
/** runtime cwd — relative paths resolve against it */
|
|
40
|
+
cwd: string;
|
|
41
|
+
/** the ACTIVE store, read live (/sessions and a root /rewind swap it) */
|
|
42
|
+
store(): SessionStore;
|
|
43
|
+
/** the current mode's model — the vision check */
|
|
44
|
+
modelRef(): { provider: string; model: string };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const plural = (n: number): string => `${n} image${n === 1 ? "" : "s"}`;
|
|
48
|
+
|
|
49
|
+
/** `/attach <path>` stages one image · `/attach` lists the stage · `/attach clear` empties it.
|
|
50
|
+
* Never throws: a bad file, an oversize image or the 9th image is a note, nothing staged. */
|
|
51
|
+
export function cmdAttach(ctx: AttachCtx, arg: string): void {
|
|
52
|
+
const { renderer } = ctx;
|
|
53
|
+
const store = ctx.store();
|
|
54
|
+
const staged = store.stagedAttachments;
|
|
55
|
+
if (arg === "") {
|
|
56
|
+
renderer.addSystemNote(staged.length === 0
|
|
57
|
+
? "no images attached — /attach <path>"
|
|
58
|
+
: `attached (${staged.length}/${MAX_IMAGES_PER_MESSAGE}):\n${staged.map((p, i) => `${i + 1}. ${describeImage(p)}`).join("\n")}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (arg === "clear") {
|
|
62
|
+
store.stageAttachments([]);
|
|
63
|
+
renderer.addSystemNote(staged.length === 0 ? "no images attached" : `attachments cleared (${plural(staged.length)} removed)`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const path = arg.replace(/^(["'])(.*)\1$/, "$2"); // a quoted path keeps its spaces
|
|
67
|
+
const loaded = loadImageAttachment(resolve(ctx.cwd, path));
|
|
68
|
+
if ("error" in loaded) { renderer.addSystemNote(loaded.error, "error"); return; }
|
|
69
|
+
const over = checkImageCount(staged.length + 1);
|
|
70
|
+
if (over !== undefined) { renderer.addSystemNote(over, "error"); return; }
|
|
71
|
+
store.stageAttachments([...staged, loaded]); // replaces the stage; folds into the next user entry
|
|
72
|
+
renderer.addSystemNote(`attached ${describeImage(loaded)} (${staged.length + 1}/${MAX_IMAGES_PER_MESSAGE}) — type your message and press Enter to send it`);
|
|
73
|
+
const ref = ctx.modelRef();
|
|
74
|
+
const vision = supportsImages(ref);
|
|
75
|
+
if (vision === false) renderer.addSystemNote(`model ${ref.provider}/${ref.model} has no image input — it will be sent as a text placeholder`, "warn");
|
|
76
|
+
else if (vision === undefined) renderer.addSystemNote(`unknown model ${ref.provider}/${ref.model}; image sent as-is (provider may reject)`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `/paste` — the clipboard image as an attachment (⌃v in the sextant surface lands here). */
|
|
80
|
+
export const PASTE_COMMAND: SlashCommand = {
|
|
81
|
+
name: "paste",
|
|
82
|
+
description: "attach the image on the clipboard (⌃v)",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Stage the image currently on the system clipboard. Berkay: "gorsel gonderebilme olayini yapalim" —
|
|
86
|
+
* /attach <path> existed, but the thing everyone tries first is copying a screenshot and pressing ⌃v,
|
|
87
|
+
* and a terminal delivers only TEXT pastes, so that did nothing. tui/clipboard-image.ts asks the OS.
|
|
88
|
+
* Same rules as /attach: magic-byte sniff, size cap, the 8-image cap, the vision note; never throws.
|
|
89
|
+
* `read` is injectable (tests never touch a real clipboard). */
|
|
90
|
+
export function cmdPasteImage(ctx: AttachCtx, read: () => Uint8Array | null = () => readClipboardImage(), name = clipboardImageName()): void {
|
|
91
|
+
const { renderer } = ctx;
|
|
92
|
+
const bytes = read();
|
|
93
|
+
if (bytes === null) { renderer.addSystemNote("no image on the clipboard — copy a screenshot or picture first, or /attach <path>"); return; }
|
|
94
|
+
const loaded = imageFromBytes(bytes, { name });
|
|
95
|
+
if ("error" in loaded) { renderer.addSystemNote(loaded.error, "error"); return; }
|
|
96
|
+
const store = ctx.store();
|
|
97
|
+
const staged = store.stagedAttachments;
|
|
98
|
+
const over = checkImageCount(staged.length + 1);
|
|
99
|
+
if (over !== undefined) { renderer.addSystemNote(over, "error"); return; }
|
|
100
|
+
store.stageAttachments([...staged, loaded]);
|
|
101
|
+
renderer.addSystemNote(`attached ${describeImage(loaded)} from the clipboard (${staged.length + 1}/${MAX_IMAGES_PER_MESSAGE}) — type your message and press Enter to send it`);
|
|
102
|
+
const ref = ctx.modelRef();
|
|
103
|
+
const vision = supportsImages(ref);
|
|
104
|
+
if (vision === false) renderer.addSystemNote(`model ${ref.provider}/${ref.model} has no image input — it will be sent as a text placeholder`, "warn");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Transcript line for a user turn: the text, then one chip per image on a second line; an
|
|
108
|
+
* image-only turn is just its chips; "" when there is nothing to show. Used by the submit echo
|
|
109
|
+
* (with the stage about to fold) and by replay (with the persisted parts). */
|
|
110
|
+
export function userTurnLine(text: string, parts: readonly MessagePart[]): string {
|
|
111
|
+
const chips = parts.filter((p): p is ImagePart => p.kind === "image").map(imageChip).join(" ");
|
|
112
|
+
return text && chips ? `${text}\n${chips}` : text || chips;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Suffix for the busy-submit note: what the queued steer will carry when the loop drains it. */
|
|
116
|
+
export function queuedAttachNote(store: SessionStore): string {
|
|
117
|
+
const n = store.stagedAttachments.length;
|
|
118
|
+
return n === 0 ? "" : ` — ${plural(n)} attached to your queued message`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** After a store swap (/sessions, /resume, root /rewind): re-stage the images the OLD instance held
|
|
122
|
+
* on the new one and say so — the switch must never lose them silently. */
|
|
123
|
+
export function carryOverAttachments(ctx: AttachCtx, pending: readonly ImagePart[]): void {
|
|
124
|
+
if (pending.length === 0) return;
|
|
125
|
+
ctx.store().stageAttachments(pending);
|
|
126
|
+
ctx.renderer.addSystemNote(`${plural(pending.length)} still attached — carried over to this session, rides on your next message`);
|
|
127
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** TUI surface for shadow-git checkpoints (port #11, cline): /checkpoints lists the
|
|
2
|
+
* session's snapshots in a pickOne overlay; /restore <ref> [files|conversation|both]
|
|
3
|
+
* replays cline's three restore modes. Conversation/both restores branch the session
|
|
4
|
+
* tree to the checkpoint's recorded entryId (port #2 leaf machinery) — the CALLER
|
|
5
|
+
* owns the store, so branching + replay run through the injected ctx. */
|
|
6
|
+
|
|
7
|
+
import type { RestoreMode } from "../coding/checkpoints.ts";
|
|
8
|
+
import type { Checkpoints } from "../coding/checkpoints.ts";
|
|
9
|
+
import type { Renderer } from "./renderer.ts";
|
|
10
|
+
|
|
11
|
+
export interface CheckpointCmdCtx {
|
|
12
|
+
renderer: Renderer;
|
|
13
|
+
busy(): boolean;
|
|
14
|
+
/** active session id (checkpoints are per-session) */
|
|
15
|
+
sessionId(): string;
|
|
16
|
+
checkpointsFor(sessionId: string): Promise<Checkpoints | null>;
|
|
17
|
+
/** branch the ACTIVE session store to entryId; false when the entry is unknown */
|
|
18
|
+
branchTo(entryId: string): boolean;
|
|
19
|
+
/** re-render transcript + usage after a conversation restore */
|
|
20
|
+
replayAndRefresh(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const MODES: readonly RestoreMode[] = ["files", "conversation", "both"];
|
|
24
|
+
|
|
25
|
+
/** /checkpoints — list snapshots newest-first in the overlay; picking one prefills nothing
|
|
26
|
+
* and just prints the /restore hint (restore is deliberately an explicit second step). */
|
|
27
|
+
export async function cmdCheckpoints(ctx: CheckpointCmdCtx): Promise<void> {
|
|
28
|
+
if (ctx.busy()) { ctx.renderer.addSystemNote("finish or interrupt the run first (Esc)", "warn"); return; }
|
|
29
|
+
const cp = await ctx.checkpointsFor(ctx.sessionId());
|
|
30
|
+
if (!cp) { ctx.renderer.addSystemNote("checkpoints unavailable (git missing or ROVECODE_NO_CHECKPOINTS=1)", "warn"); return; }
|
|
31
|
+
const log = cp.list();
|
|
32
|
+
if (log.length === 0) { ctx.renderer.addSystemNote("no checkpoints yet — snapshots land after each mutating tool call"); return; }
|
|
33
|
+
const items = [...log].reverse().slice(0, 20).map((c, i) => ({
|
|
34
|
+
value: c.hash,
|
|
35
|
+
label: `#${log.length - i} ${c.label} · ${c.hash.slice(0, 8)}`,
|
|
36
|
+
description: `${new Date(c.createdAt).toLocaleString()}${c.entryId ? " · conversation anchor" : ""}`,
|
|
37
|
+
}));
|
|
38
|
+
const picked = await ctx.renderer.pickOne(items, "checkpoints (Enter = show restore hint, Esc = close)");
|
|
39
|
+
ctx.replayAndRefresh(); // clear the overlay title note
|
|
40
|
+
if (picked) ctx.renderer.addSystemNote(`restore with: /restore ${picked.slice(0, 8)} [files|conversation|both]`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** /restore <hash-prefix> [mode] — files (workspace only), conversation (session branch
|
|
44
|
+
* only), or both. Cline's three modes, checkpoint-restore semantics. */
|
|
45
|
+
export async function cmdRestore(ctx: CheckpointCmdCtx, arg: string): Promise<void> {
|
|
46
|
+
if (ctx.busy()) { ctx.renderer.addSystemNote("finish or interrupt the run first (Esc)", "warn"); return; }
|
|
47
|
+
const [ref, modeArg] = arg.split(/\s+/);
|
|
48
|
+
if (!ref) { ctx.renderer.addSystemNote("usage: /restore <checkpoint> [files|conversation|both] — list with /checkpoints", "warn"); return; }
|
|
49
|
+
const mode = (modeArg || "files") as RestoreMode;
|
|
50
|
+
if (!MODES.includes(mode)) { ctx.renderer.addSystemNote(`unknown restore mode "${modeArg}" (files|conversation|both)`, "warn"); return; }
|
|
51
|
+
const cp = await ctx.checkpointsFor(ctx.sessionId());
|
|
52
|
+
if (!cp) { ctx.renderer.addSystemNote("checkpoints unavailable (git missing or ROVECODE_NO_CHECKPOINTS=1)", "warn"); return; }
|
|
53
|
+
// dedupe candidates BY HASH (identical content re-snapshotted shares one): counting
|
|
54
|
+
// entries made even the full 40-char hash "match 2 checkpoints" forever. Latest entry
|
|
55
|
+
// per hash wins, mirroring the module's own pick.
|
|
56
|
+
const match = [...new Map(cp.list().filter((c) => c.hash.startsWith(ref)).map((c) => [c.hash, c])).values()];
|
|
57
|
+
if (match.length !== 1) {
|
|
58
|
+
ctx.renderer.addSystemNote(match.length === 0 ? `no checkpoint matching "${ref}"` : `"${ref}" matches ${match.length} checkpoints — be more specific`, "warn");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const res = await cp.restore(match[0]!.hash, mode);
|
|
62
|
+
if (!res.ok) { ctx.renderer.addSystemNote(`restore failed: ${res.error}`, "error"); return; }
|
|
63
|
+
let convNote = "";
|
|
64
|
+
if (mode === "conversation" || mode === "both") {
|
|
65
|
+
if (res.entryId && ctx.branchTo(res.entryId)) { ctx.replayAndRefresh(); convNote = " · conversation branched to the checkpoint's turn"; }
|
|
66
|
+
else convNote = " · conversation anchor unavailable in this session tree (files unaffected by that)";
|
|
67
|
+
}
|
|
68
|
+
const fileNote = mode === "conversation" ? "" : "workspace rolled back";
|
|
69
|
+
ctx.renderer.addSystemNote(`restored ${res.checkpoint.hash.slice(0, 8)} (${mode}) — ${fileNote}${convNote}; snapshots keep history`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Read an IMAGE from the system clipboard (⌃v / `/paste`).
|
|
2
|
+
*
|
|
3
|
+
* A terminal only ever hands a program TEXT from the clipboard (bracketed paste). When the clipboard
|
|
4
|
+
* holds an image — a screenshot, a copied picture — the terminal sends nothing useful, and users who
|
|
5
|
+
* press ⌃v expecting the image to land (every chat app taught them that) get silence. So the surface
|
|
6
|
+
* asks the OS itself, through the one tool each platform has for it, and converts to PNG:
|
|
7
|
+
* win32 PowerShell + System.Windows.Forms.Clipboard.GetImage() → PNG → base64 on stdout
|
|
8
|
+
* darwin osascript `the clipboard as «class PNGf»` → hex («data PNGf…»)
|
|
9
|
+
* linux wl-paste -t image/png, else xclip -selection clipboard -t image/png -o
|
|
10
|
+
*
|
|
11
|
+
* The command runner is injected so tests never touch a real clipboard; every failure (no image on
|
|
12
|
+
* the clipboard, tool missing, odd output) is `null`, never a throw — the caller says "no image on the
|
|
13
|
+
* clipboard" and moves on. Nothing here writes a file: the bytes go straight to core/images.ts
|
|
14
|
+
* imageFromBytes, which sniffs the magic bytes and enforces the size cap like any other attachment. */
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
export interface ClipboardRunner {
|
|
19
|
+
/** run a command, return stdout decoded as `encoding` (utf8 for text tools, latin1 when the tool
|
|
20
|
+
* prints raw image bytes — utf8 decoding would corrupt them), or null when it failed / is missing */
|
|
21
|
+
(cmd: string, args: readonly string[], encoding?: "utf8" | "latin1"): string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TIMEOUT_MS = 5_000;
|
|
25
|
+
const MAX_OUT = 64 * 1024 * 1024; // a 5 MiB image is ~7 MiB of base64; leave room, never unbounded
|
|
26
|
+
|
|
27
|
+
/** child_process.spawnSync under the contract above: non-zero exit, signal, timeout or ENOENT → null */
|
|
28
|
+
export const spawnRunner: ClipboardRunner = (cmd, args, encoding = "utf8") => {
|
|
29
|
+
try {
|
|
30
|
+
const r = spawnSync(cmd, args, { encoding, timeout: TIMEOUT_MS, maxBuffer: MAX_OUT, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
31
|
+
if (r.error || r.status !== 0 || r.signal) return null;
|
|
32
|
+
return r.stdout;
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** PowerShell: the image as PNG base64, or nothing when the clipboard holds no image */
|
|
37
|
+
const PS_SCRIPT = [
|
|
38
|
+
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing",
|
|
39
|
+
"$i = [System.Windows.Forms.Clipboard]::GetImage()",
|
|
40
|
+
"if ($i -eq $null) { exit 3 }",
|
|
41
|
+
"$ms = New-Object System.IO.MemoryStream",
|
|
42
|
+
"$i.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)",
|
|
43
|
+
"[Console]::Out.Write([Convert]::ToBase64String($ms.ToArray()))",
|
|
44
|
+
].join("; ");
|
|
45
|
+
|
|
46
|
+
function fromBase64(s: string | null): Uint8Array | null {
|
|
47
|
+
const t = s?.trim() ?? "";
|
|
48
|
+
if (t.length < 16 || !/^[A-Za-z0-9+/=\r\n]+$/.test(t)) return null;
|
|
49
|
+
try { return new Uint8Array(Buffer.from(t.replace(/\s+/g, ""), "base64")); } catch { return null; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** osascript prints `«data PNGf89504E47…»`; take the hex between the marker and the closing » */
|
|
53
|
+
function fromAppleScriptHex(s: string | null): Uint8Array | null {
|
|
54
|
+
const m = /«data PNGf([0-9A-Fa-f]+)»/.exec(s ?? "");
|
|
55
|
+
if (!m || m[1]!.length < 32 || m[1]!.length % 2 !== 0) return null;
|
|
56
|
+
return new Uint8Array(Buffer.from(m[1]!, "hex"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The clipboard image as raw bytes (PNG on win32/darwin, whatever the tool yields on linux — the
|
|
60
|
+
* magic-byte sniff downstream decides), or null when there is none. `platform` defaults to the process's. */
|
|
61
|
+
export function readClipboardImage(run: ClipboardRunner = spawnRunner, platform: NodeJS.Platform = process.platform): Uint8Array | null {
|
|
62
|
+
if (platform === "win32") {
|
|
63
|
+
return fromBase64(run("powershell", ["-NoProfile", "-NonInteractive", "-STA", "-Command", PS_SCRIPT]));
|
|
64
|
+
}
|
|
65
|
+
if (platform === "darwin") {
|
|
66
|
+
return fromAppleScriptHex(run("osascript", ["-e", "the clipboard as «class PNGf»"]));
|
|
67
|
+
}
|
|
68
|
+
// linux and the rest: wayland first, then X11; both print the raw PNG, which the runner returns as a
|
|
69
|
+
// utf8 string — decode it back to bytes with latin1 so no byte is altered
|
|
70
|
+
for (const [cmd, args] of [["wl-paste", ["-t", "image/png"]], ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]] as const) {
|
|
71
|
+
const out = run(cmd, args, "latin1");
|
|
72
|
+
if (out !== null && out.length > 8) return new Uint8Array(Buffer.from(out, "latin1"));
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** what a pasted image is called in the transcript chip and the sidecar name: clipboard-HHMMSS.png */
|
|
78
|
+
export function clipboardImageName(now: Date = new Date()): string {
|
|
79
|
+
const p = (n: number): string => String(n).padStart(2, "0");
|
|
80
|
+
return `clipboard-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}.png`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/** Custom slash commands (port #30): `.rovecode/commands/*.md` (project) and `~/.rovecode/commands/*.md`
|
|
2
|
+
* (user scope, ROVECODE_HOME-aware) become `/name` commands in the TUI — autocomplete, /help,
|
|
3
|
+
* and dispatch through the same user-turn path a typed prompt takes.
|
|
4
|
+
*
|
|
5
|
+
* Ported from opencode @ ebece6e (MIT), packages/opencode/src unless noted:
|
|
6
|
+
* - discovery: markdown files under a `command(s)/` dir, name = path sans extension
|
|
7
|
+
* (config/command.ts:13-39 load, config/entry-name.ts:15-19 configEntryNameFromPath);
|
|
8
|
+
* the frontmatter is the command's metadata and the markdown body its template
|
|
9
|
+
* (config/command.ts:26-30 `{ name, ...md.data, template: md.content.trim() }`); fields
|
|
10
|
+
* description/model (packages/core/src/v1/config/command.ts:5-12 — agent/variant/subtask
|
|
11
|
+
* have no rovecode counterpart; rovecode adds `mode` for plan/act, port #20).
|
|
12
|
+
* - precedence: the global config dir is loaded before the project's and later entries
|
|
13
|
+
* replace earlier ones (config/config.ts:473 mergeDeep) → project shadows user here.
|
|
14
|
+
* - templating (session/prompt.ts:1372-1395): quote-aware argument split (argsRegex :1594,
|
|
15
|
+
* quoteTrimRegex :1596), `$N` positional (placeholderRegex :1595), `$ARGUMENTS` = the whole
|
|
16
|
+
* argument string (:1391), and a template with no placeholder gets the arguments appended
|
|
17
|
+
* after a blank line (:1393-1395); `hints` (command/index.ts:36-44) lists the placeholders.
|
|
18
|
+
* - a per-command `model` overrides the session model for THAT prompt only (:1411-1412).
|
|
19
|
+
* Pattern reference only, no code ported: gemini-cli @ 0bd1d43 packages/cli/src/services/
|
|
20
|
+
* FileCommandLoader.ts — user dir first, project dir second, "last wins" (:85-90, :204-228),
|
|
21
|
+
* invalid files skipped with a report, never fatal (:277-298).
|
|
22
|
+
*
|
|
23
|
+
* Deviations: flat directory (no subdirectory namespaces); names are lowercased and must
|
|
24
|
+
* match [a-z0-9_-]+; YAML-lite frontmatter (`key: value` lines — skills/index.ts
|
|
25
|
+
* parseFrontmatter, reused) instead of gray-matter, so an unterminated block, a `mode`
|
|
26
|
+
* outside plan|act, or an empty body skips the file with a boot warning (never a throw);
|
|
27
|
+
* substitution is ONE pass, so substituted text is never re-scanned, `$$` is a literal `$`,
|
|
28
|
+
* and a positional `$N` is exactly one token (upstream's "last placeholder absorbs the rest",
|
|
29
|
+
* prompt.ts:1387, is not ported); no `!\`shell\`` / `@file` injection (prompt.ts:1397-1408) —
|
|
30
|
+
* the template reaches the model as plain text; a built-in TUI command always beats a custom
|
|
31
|
+
* one of the same name (warned at boot); a symlinked *.md counts when it points at a file (a
|
|
32
|
+
* dangling link is reported unreadable); boot-warning echoes and descriptions are bounded.
|
|
33
|
+
*
|
|
34
|
+
* TUI semantics (runCustomCommand): `mode` switches through the /plan-/act path and STAYS
|
|
35
|
+
* switched after the run — the switch is a durable session entry (port #20), and silently
|
|
36
|
+
* flipping back would desynchronize the status line from the session record; `model` is set
|
|
37
|
+
* the way /model does and restored after the run unless the user re-pointed it mid-run
|
|
38
|
+
* (/model is not busy-gated). With planActSeparateModels off, /model mirrors writes into
|
|
39
|
+
* both slots, so the restore does too — same as typing /model <prev> by hand. */
|
|
40
|
+
|
|
41
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
42
|
+
import { join, resolve } from "node:path";
|
|
43
|
+
import type { AgentMode, ModeManager } from "../core/modes.ts";
|
|
44
|
+
import { rovecodeHome } from "../providers/auth.ts";
|
|
45
|
+
import { parseFrontmatter } from "../skills/index.ts";
|
|
46
|
+
import { togglePlanAct, type ModeStateSlice } from "./modes-cmd.ts";
|
|
47
|
+
import type { Renderer, SlashCommand } from "./renderer.ts";
|
|
48
|
+
|
|
49
|
+
export type CommandScope = "project" | "user";
|
|
50
|
+
|
|
51
|
+
export interface CustomCommand {
|
|
52
|
+
/** `/name`: the filename sans `.md`, lowercased; must match [a-z0-9_-]+ */
|
|
53
|
+
name: string;
|
|
54
|
+
description: string;
|
|
55
|
+
/** frontmatter `model:` — per-run override, applied the way /model does */
|
|
56
|
+
model?: string;
|
|
57
|
+
/** frontmatter `mode:` — the run happens in this plan/act mode */
|
|
58
|
+
mode?: AgentMode;
|
|
59
|
+
/** the markdown body: the prompt template */
|
|
60
|
+
body: string;
|
|
61
|
+
/** placeholders the template uses, for /help — opencode hints(): "$1 $2 $ARGUMENTS" */
|
|
62
|
+
hints: string[];
|
|
63
|
+
path: string;
|
|
64
|
+
scope: CommandScope;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ParsedCommandFile { description?: string; model?: string; mode?: AgentMode; body: string }
|
|
68
|
+
|
|
69
|
+
const NAME_RE = /^[a-z0-9_-]+$/;
|
|
70
|
+
/** opencode prompt.ts:1594 minus its `[Image N]` alternative (no image parts reach this path). */
|
|
71
|
+
const ARGS_RE = /(?:"[^"]*"|'[^']*'|[^\s"']+)/g;
|
|
72
|
+
const QUOTE_TRIM_RE = /^["']|["']$/g;
|
|
73
|
+
/** one pass: `$$` (literal), `$ARGUMENTS`, `$1`..`$9` */
|
|
74
|
+
const PLACEHOLDER_RE = /\$(\$|ARGUMENTS|[1-9])/g;
|
|
75
|
+
/** /help + palette rows stay one line; a 20k-char frontmatter field must not become one */
|
|
76
|
+
const MAX_DESCRIPTION_CHARS = 200;
|
|
77
|
+
|
|
78
|
+
/** Bounded echo (todo.ts `show` idiom): file content quoted back in a warning is clipped. */
|
|
79
|
+
function clip(s: string, max: number): string {
|
|
80
|
+
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------- file format ----------
|
|
84
|
+
|
|
85
|
+
/** Optional `---` frontmatter + body → command fields. `{ error }` names why a file must be
|
|
86
|
+
* skipped (unterminated block, `mode` outside plan|act, empty body). Never throws. */
|
|
87
|
+
export function parseCommandFile(text: string): ParsedCommandFile | { error: string } {
|
|
88
|
+
let src = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text; // drop a UTF-8 BOM
|
|
89
|
+
let fm: Record<string, string> = {};
|
|
90
|
+
let body = src;
|
|
91
|
+
if (src.startsWith("---")) {
|
|
92
|
+
if (!src.endsWith("\n")) src += "\n"; // parseFrontmatter finds the closing fence by its line end
|
|
93
|
+
const parsed = parseFrontmatter(src);
|
|
94
|
+
if (!parsed) return { error: "unterminated frontmatter (no closing ---)" };
|
|
95
|
+
fm = parsed.fm; body = parsed.body;
|
|
96
|
+
}
|
|
97
|
+
body = body.trim();
|
|
98
|
+
if (body === "") return { error: "empty command body (nothing to send)" };
|
|
99
|
+
const field = (k: string): string | undefined => { const v = fm[k]?.trim(); return v ? v : undefined; };
|
|
100
|
+
const mode = field("mode");
|
|
101
|
+
if (mode !== undefined && mode !== "plan" && mode !== "act") return { error: `mode must be "plan" or "act" (got "${clip(mode, 40)}")` }; // LOW-2: bounded echo
|
|
102
|
+
return { description: field("description"), model: field("model"), mode, body };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Placeholders a template uses: `$1..$9` sorted, then `$ARGUMENTS` (command/index.ts:36-44).
|
|
106
|
+
* `$$` escapes are dropped first so `$$1` does not count. */
|
|
107
|
+
export function hints(body: string): string[] {
|
|
108
|
+
const src = body.replace(/\$\$/g, "");
|
|
109
|
+
const out = [...new Set(src.match(/\$[1-9]/g) ?? [])].sort();
|
|
110
|
+
if (src.includes("$ARGUMENTS")) out.push("$ARGUMENTS");
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------- discovery ----------
|
|
115
|
+
|
|
116
|
+
export interface DiscoverOptions {
|
|
117
|
+
/** user-scope rovecode dir; default rovecodeHome() (ROVECODE_HOME → ~/.rovecode). Commands: `<home>/commands` */
|
|
118
|
+
home?: string;
|
|
119
|
+
/** built-in command names — a custom command with one of these names is dropped, with a warning */
|
|
120
|
+
reserved?: readonly string[];
|
|
121
|
+
/** more command folders — a plugin's (src/plugins) — scanned AFTER the folder of the same scope, so
|
|
122
|
+
* your own `~/.rovecode/commands/x.md` or `.rovecode/commands/x.md` keeps `/x` over a plugin's */
|
|
123
|
+
extraDirs?: readonly { dir: string; scope: CommandScope }[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DiscoveredCommands { commands: CustomCommand[]; warnings: string[] }
|
|
127
|
+
|
|
128
|
+
/** Scan `<home>/commands` then `<cwd>/.rovecode/commands`; a later (project) entry replaces an
|
|
129
|
+
* earlier (user) one of the same name. Missing dirs are silent (the common case); every
|
|
130
|
+
* other problem is a warning for the boot transcript, never a throw. Sorted by name. */
|
|
131
|
+
export function discoverCommands(cwd: string, opts: DiscoverOptions = {}): DiscoveredCommands {
|
|
132
|
+
const warnings: string[] = [];
|
|
133
|
+
const reserved = new Set(opts.reserved ?? []);
|
|
134
|
+
const userDir = join(opts.home ?? rovecodeHome(), "commands");
|
|
135
|
+
const projectDir = join(cwd, ".rovecode", "commands");
|
|
136
|
+
const extra = (scope: CommandScope): [CommandScope, string][] => (opts.extraDirs ?? []).filter((e) => e.scope === scope).map((e) => [scope, e.dir]);
|
|
137
|
+
// cwd IS the rovecode home: one directory, scanned once (gemini-cli FileCommandLoader.ts:221-228)
|
|
138
|
+
const dirs: [CommandScope, string][] = resolve(userDir) === resolve(projectDir)
|
|
139
|
+
? [["project", projectDir], ...extra("user"), ...extra("project")]
|
|
140
|
+
: [["user", userDir], ...extra("user"), ["project", projectDir], ...extra("project")];
|
|
141
|
+
const byName = new Map<string, CustomCommand>();
|
|
142
|
+
for (const [scope, dir] of dirs) {
|
|
143
|
+
for (const cmd of scanCommandDir(dir, scope, warnings)) {
|
|
144
|
+
// the verdict first, the path last: an 80-column terminal wraps a long path mid-line and the
|
|
145
|
+
// sentence must survive at the line start
|
|
146
|
+
if (reserved.has(cmd.name)) { warnings.push(`/${cmd.name} is a built-in command — built-in kept (${cmd.path})`); continue; }
|
|
147
|
+
const prev = byName.get(cmd.name);
|
|
148
|
+
if (prev?.scope === scope) { warnings.push(`${cmd.path}: /${cmd.name} already defined by ${prev.path} — first kept`); continue; }
|
|
149
|
+
byName.set(cmd.name, cmd); // project (scanned second) shadows user
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { commands: [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)), warnings };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** LOW-3: a symlinked `*.md` counts when its target is a file (a link to a directory is skipped
|
|
156
|
+
* like any directory); a DANGLING link stays in the list so the read below reports it unreadable. */
|
|
157
|
+
function linksToFile(path: string): boolean {
|
|
158
|
+
try { return statSync(path).isFile(); } catch { return true; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** One directory's `*.md` files (top level only, sorted), each parsed or warned about. */
|
|
162
|
+
function scanCommandDir(dir: string, scope: CommandScope, warnings: string[]): CustomCommand[] {
|
|
163
|
+
let files: string[];
|
|
164
|
+
try {
|
|
165
|
+
files = readdirSync(dir, { withFileTypes: true })
|
|
166
|
+
.filter((e) => /\.md$/i.test(e.name) && (e.isFile() || (e.isSymbolicLink() && linksToFile(join(dir, e.name)))))
|
|
167
|
+
.map((e) => e.name).sort();
|
|
168
|
+
} catch {
|
|
169
|
+
return []; // no such directory — silent, like the skills store
|
|
170
|
+
}
|
|
171
|
+
const out: CustomCommand[] = [];
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const path = join(dir, file);
|
|
174
|
+
const name = file.slice(0, -3).toLowerCase();
|
|
175
|
+
if (!NAME_RE.test(name)) { warnings.push(`${path}: skipped — command name "${name}" must match [a-z0-9_-]+`); continue; }
|
|
176
|
+
let text: string;
|
|
177
|
+
try {
|
|
178
|
+
text = readFileSync(path, "utf8");
|
|
179
|
+
} catch (e) {
|
|
180
|
+
warnings.push(`${path}: skipped — unreadable (${e instanceof Error ? e.message : String(e)})`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseCommandFile(text);
|
|
184
|
+
if ("error" in parsed) { warnings.push(`${path}: skipped — ${parsed.error}`); continue; }
|
|
185
|
+
out.push({
|
|
186
|
+
name, description: clip(parsed.description ?? `custom command (${file})`, MAX_DESCRIPTION_CHARS), model: parsed.model, mode: parsed.mode,
|
|
187
|
+
body: parsed.body, hints: hints(parsed.body), path, scope,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------- templating ----------
|
|
194
|
+
|
|
195
|
+
/** Template → prompt. `$ARGUMENTS` = the whole argument string, `$1..$9` = one quote-aware token
|
|
196
|
+
* each ("" when missing), `$$` = a literal `$`. One pass, so substituted text is never
|
|
197
|
+
* re-scanned. A template without placeholders gets the arguments appended after a blank line
|
|
198
|
+
* (prompt.ts:1393-1395) so nothing the user typed is silently dropped. */
|
|
199
|
+
export function renderCommand(cmd: Pick<CustomCommand, "body">, argString: string): string {
|
|
200
|
+
const raw = argString.trim();
|
|
201
|
+
const args = (raw.match(ARGS_RE) ?? []).map((a) => a.replace(QUOTE_TRIM_RE, ""));
|
|
202
|
+
let placeholder = false;
|
|
203
|
+
const out = cmd.body.replace(PLACEHOLDER_RE, (_m, tok: string) => {
|
|
204
|
+
if (tok === "$") return "$";
|
|
205
|
+
placeholder = true;
|
|
206
|
+
return tok === "ARGUMENTS" ? raw : (args[Number(tok) - 1] ?? "");
|
|
207
|
+
});
|
|
208
|
+
return placeholder || raw === "" ? out : `${out}\n\n${raw}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** `rovecode run "/name args"` hook (main.ts call sites — see the port notes): a leading `/name`
|
|
212
|
+
* that names a discovered command renders to its prompt; anything else passes through
|
|
213
|
+
* verbatim. `model`/`mode` are TUI semantics and are not applied here. */
|
|
214
|
+
export function expandSlashPrompt(prompt: string, cwd: string, opts: DiscoverOptions = {}): string {
|
|
215
|
+
const m = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(prompt.trim());
|
|
216
|
+
if (!m) return prompt;
|
|
217
|
+
const cmd = discoverCommands(cwd, opts).commands.find((c) => c.name === m[1]!.toLowerCase());
|
|
218
|
+
return cmd ? renderCommand(cmd, m[2] ?? "") : prompt;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------- TUI surface ----------
|
|
222
|
+
|
|
223
|
+
/** Entries for Renderer.setCommands (editor autocomplete) — appended after the built-ins. */
|
|
224
|
+
export function commandsForPalette(list: readonly CustomCommand[]): SlashCommand[] {
|
|
225
|
+
return list.map((c) => ({ name: c.name, description: c.description }));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** `/help` tail: "" when there are none, else a "custom:" section (name, hints, description, scope). */
|
|
229
|
+
export function helpForCommands(list: readonly CustomCommand[]): string {
|
|
230
|
+
if (list.length === 0) return "";
|
|
231
|
+
return "\ncustom:\n" + list
|
|
232
|
+
.map((c) => `/${c.name}${c.hints.length > 0 ? " " + c.hints.join(" ") : ""} — ${c.description} (${c.scope})`)
|
|
233
|
+
.join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** What the TUI hands the dispatcher (app.ts builds it once; state is read live). */
|
|
237
|
+
export interface CustomCommandCtx {
|
|
238
|
+
renderer: Renderer;
|
|
239
|
+
modes: ModeManager;
|
|
240
|
+
state: ModeStateSlice;
|
|
241
|
+
pushStatus: () => void;
|
|
242
|
+
/** the plain user-turn path (app.ts `submit`): echo, flush a pending mode switch, start the run */
|
|
243
|
+
submit: (text: string) => Promise<void>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** handleSlash `default:` hook: false when `name` is not a custom command (the caller reports
|
|
247
|
+
* "unknown command"); true after kicking off the run — fire-and-forget, the same shape as the
|
|
248
|
+
* other async slash commands. */
|
|
249
|
+
export function dispatchCustomCommand(ctx: CustomCommandCtx, list: readonly CustomCommand[], name: string, arg: string): boolean {
|
|
250
|
+
const cmd = list.find((c) => c.name === name);
|
|
251
|
+
if (!cmd) return false;
|
|
252
|
+
void runCustomCommand(ctx, cmd, arg);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Busy-gated like /plan and /checkpoints — a rendered prompt with a model/mode attached cannot
|
|
257
|
+
* be queued as steering. `mode` → togglePlanAct (stays); `model` → setModel like /model,
|
|
258
|
+
* restored after the run unless the user re-pointed it meanwhile (header: TUI semantics). */
|
|
259
|
+
export async function runCustomCommand(ctx: CustomCommandCtx, cmd: CustomCommand, arg: string): Promise<void> {
|
|
260
|
+
if (ctx.state.busy) { ctx.renderer.addSystemNote("finish or interrupt the run first (Esc)", "warn"); return; }
|
|
261
|
+
if (cmd.mode !== undefined && cmd.mode !== ctx.modes.mode) togglePlanAct(ctx.modes, cmd.mode, ctx.state, ctx.renderer, ctx.pushStatus);
|
|
262
|
+
const prev = ctx.modes.modelFor().model;
|
|
263
|
+
const override = cmd.model !== undefined && cmd.model !== prev;
|
|
264
|
+
if (override) {
|
|
265
|
+
ctx.modes.setModel({ model: cmd.model });
|
|
266
|
+
ctx.state.model = ctx.modes.modelFor().model;
|
|
267
|
+
ctx.renderer.addSystemNote(`model → ${cmd.model} for /${cmd.name} (restored after the run)`);
|
|
268
|
+
ctx.pushStatus();
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await ctx.submit(renderCommand(cmd, arg));
|
|
272
|
+
} finally {
|
|
273
|
+
if (override && ctx.modes.modelFor().model === cmd.model) {
|
|
274
|
+
ctx.modes.setModel({ model: prev }); ctx.state.model = prev; ctx.pushStatus();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|