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,382 @@
|
|
|
1
|
+
/** PORT #35 — machine-readable output for `rovecode run`: --output text | json | ndjson.
|
|
2
|
+
*
|
|
3
|
+
* Ported shape decisions (pi @ 853a80d, packages/coding-agent/src):
|
|
4
|
+
* - modes/print-mode.ts:108-111 — `--mode json` writes ONE JSON line per session event
|
|
5
|
+
* (`JSON.stringify(toJsonEvent(event)) + "\n"` through writeRawStdout); the RPC protocol
|
|
6
|
+
* frames its output identically (modes/rpc/rpc-mode.ts:60-62, :355-356). rovecode's ndjson mode
|
|
7
|
+
* emits every RunEvent VERBATIM that way (no reformatting: the loop's events are already
|
|
8
|
+
* deltas — pi's toJsonEvent only strips its cumulative partials, json-event.ts:40-45) and
|
|
9
|
+
* closes with one {type:"result", …} line.
|
|
10
|
+
* - core/output-guard.ts:45-70 takeOverStdout — outside the interactive TUI process.stdout.write
|
|
11
|
+
* is redirected to stderr and only the raw writer bound BEFORE the takeover reaches fd 1
|
|
12
|
+
* (installed for every appMode !== "interactive", main.ts:633-636). guardStdout below is that
|
|
13
|
+
* guard; Bun's console.log does not route through process.stdout.write, so console.log/info/
|
|
14
|
+
* debug are redirected too. stdout purity is structural, not a discipline. cmdRun installs it
|
|
15
|
+
* right after parseOutputMode — BEFORE bootRuntime, whose session_open hooks may print — and
|
|
16
|
+
* hands the sink the raw writer it bound first (fix-wave 4 MED-C); the sink's own install below
|
|
17
|
+
* serves embedders that pass process.stdout itself.
|
|
18
|
+
* - print-mode.ts:139-156 — text mode prints only the final assistant text; error/aborted →
|
|
19
|
+
* message on stderr, exit 1 (:145-147); signals exit 128+n (:57-61: 143 SIGTERM, 129 SIGHUP).
|
|
20
|
+
* rovecode keeps its text mode byte-identical to the pre-port cmdRun (progress lines + summary on
|
|
21
|
+
* stdout) and maps an ABORTED run to 130 by the same 128+signal convention: the sink's signal
|
|
22
|
+
* aborts on the first SIGINT and rides into LoopDeps.signal (port #21), so Ctrl-C ends the run
|
|
23
|
+
* "stopped" with a well-formed result instead of a hard kill (Windows: exit 0xC000013A, no output).
|
|
24
|
+
*
|
|
25
|
+
* Result schema (json: the ONLY stdout line; ndjson: the LAST line, with type:"result"):
|
|
26
|
+
* status "done" | "stopped" | "error" | "budget" — run_end.status ("error" when the loop
|
|
27
|
+
* ended without a run_end)
|
|
28
|
+
* summary run_end.summary: the final assistant text, or the error text
|
|
29
|
+
* sessionId run_start.sessionId (null if never seen) — the store under .rovecode/sessions
|
|
30
|
+
* model { provider, model } requested
|
|
31
|
+
* origin { provider, model } that SERVED the last turn (router fallback may differ), or null
|
|
32
|
+
* usage { input, output, cacheRead, cacheWrite } summed over the run's assistant messages
|
|
33
|
+
* costUsd number, or null when any usage-bearing turn has no catalog pricing (mock, unknown
|
|
34
|
+
* models) — an honest unknown, never a silent lower bound (core/usage.ts costUsd)
|
|
35
|
+
* toolCalls [{ tool, ok, ms? }] in event order, one entry per ISSUED call — keyed per
|
|
36
|
+
* `<turn>:<callId>`, so a call id a provider reuses across turns (the SSE adapter's
|
|
37
|
+
* `tc<idx>` fallback, providers/stream.ts) is one entry PER TURN, the telemetry/otel.ts
|
|
38
|
+
* rovecode.tool_calls count (LOW-B, #39); ms absent for calls that never executed
|
|
39
|
+
* (permission_denied / truncated / not_found → ok:false)
|
|
40
|
+
* durationMs sink construction → finish
|
|
41
|
+
* exitCode the process exit code below
|
|
42
|
+
* Exit codes: 0 done · 1 error / budget / no run_end · 2 usage error (bad --output value; also
|
|
43
|
+
* the startup-error class, port #27) · 130 stopped (the run was aborted: SIGINT). Text mode keeps
|
|
44
|
+
* its 0 done / 1 otherwise, plus 130 for the (newly reachable) aborted run. */
|
|
45
|
+
|
|
46
|
+
import { format } from "node:util";
|
|
47
|
+
import type { Message, ModelRef, RunEvent, RunOutstanding, StreamFn } from "../core/types.ts";
|
|
48
|
+
import { outstandingClause, type LoopDeps } from "../core/loop.ts";
|
|
49
|
+
import type { Runtime } from "./runtime.ts";
|
|
50
|
+
import { costUsd, type PricingRow } from "../core/usage.ts";
|
|
51
|
+
import { ModelCatalog } from "../providers/catalog.ts";
|
|
52
|
+
import { VALUE_FLAGS } from "./dispatch.ts";
|
|
53
|
+
|
|
54
|
+
export type OutputMode = "text" | "json" | "ndjson";
|
|
55
|
+
export const OUTPUT_MODES: readonly OutputMode[] = ["text", "json", "ndjson"];
|
|
56
|
+
export type RunEndStatus = Extract<RunEvent, { type: "run_end" }>["status"];
|
|
57
|
+
type RunEnd = { status: RunEndStatus; summary: string; outstanding?: RunOutstanding };
|
|
58
|
+
|
|
59
|
+
export interface RunResult {
|
|
60
|
+
status: RunEndStatus;
|
|
61
|
+
summary: string;
|
|
62
|
+
sessionId: string | null;
|
|
63
|
+
model: { provider: string; model: string };
|
|
64
|
+
origin: { provider: string; model: string } | null;
|
|
65
|
+
usage: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
66
|
+
costUsd: number | null;
|
|
67
|
+
toolCalls: { tool: string; ok: boolean; ms?: number }[];
|
|
68
|
+
durationMs: number;
|
|
69
|
+
exitCode: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Writer { write(chunk: string): unknown }
|
|
73
|
+
|
|
74
|
+
/** ModelCatalog.lookup's shape — tests inject fixed pricing. */
|
|
75
|
+
export interface PricingSource { lookup(provider: string, model: string): { pricing?: PricingRow } | undefined }
|
|
76
|
+
|
|
77
|
+
export interface OutputSinkOptions {
|
|
78
|
+
stdout: Writer;
|
|
79
|
+
stderr: Writer;
|
|
80
|
+
/** requested model → result.model, and the pricing fallback for origin-less messages */
|
|
81
|
+
model: ModelRef;
|
|
82
|
+
/** live view of the run's session store; read at finish (usage, origin, tool names) */
|
|
83
|
+
messages: () => Message[];
|
|
84
|
+
catalog?: PricingSource;
|
|
85
|
+
/** SIGINT hookup seam; returns the uninstaller. Default: process.once("SIGINT"). */
|
|
86
|
+
onInterrupt?: (handler: () => void) => () => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface OutputSink {
|
|
90
|
+
readonly mode: OutputMode;
|
|
91
|
+
/** aborts on the first SIGINT — thread into LoopDeps.signal so the run ends "stopped" (130) */
|
|
92
|
+
readonly signal: AbortSignal;
|
|
93
|
+
onEvent(ev: RunEvent): void;
|
|
94
|
+
/** After the loop settles: writes the text summary / the json result / the ndjson result line
|
|
95
|
+
* and returns the exit code. No `end` = the loop ended without run_end → status "error", 1. */
|
|
96
|
+
finish(end?: RunEnd): number;
|
|
97
|
+
/** Uninstalls the stdout guard (installed for json/ndjson over the REAL process.stdout; a no-op
|
|
98
|
+
* otherwise) so console.log / process.stdout.write reach fd 1 again — for embedders and tests.
|
|
99
|
+
* cmdRun never calls it: hooks.close()/mcp.close() run AFTER finish and may still print, so the
|
|
100
|
+
* guard must hold until process.exit. */
|
|
101
|
+
close(): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------- argv ----------
|
|
105
|
+
|
|
106
|
+
const USAGE = "--output <mode>: text (default) | json | ndjson";
|
|
107
|
+
|
|
108
|
+
const usageExit = (msg: string): never => {
|
|
109
|
+
process.stderr.write(`error: ${msg} — ${USAGE}\n`);
|
|
110
|
+
return process.exit(2);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function isOutputMode(v: string): v is OutputMode {
|
|
114
|
+
return (OUTPUT_MODES as readonly string[]).includes(v);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `--output <mode>` or `--output=<mode>`, anywhere in argv (dispatch.ts VALUE_FLAGS keeps the
|
|
118
|
+
* value from being taken for the command). Missing or unknown value → one-line stderr usage
|
|
119
|
+
* error, exit 2 (the usage/startup-error class; `fail` is injectable for tests). Last one wins.
|
|
120
|
+
* cmdRun calls this FIRST — before bootRuntime — so a usage error leaves no trace: no
|
|
121
|
+
* .rovecode/sessions/<id> (meta.json, memory dir), no sandbox probe, no MCP children to reap. */
|
|
122
|
+
export function parseOutputMode(argv: readonly string[], fail: (msg: string) => never = usageExit): OutputMode {
|
|
123
|
+
const args = argv.slice(2);
|
|
124
|
+
let mode: string | undefined;
|
|
125
|
+
for (let i = 0; i < args.length; i++) {
|
|
126
|
+
const a = args[i]!;
|
|
127
|
+
if (a === "--output") {
|
|
128
|
+
const v = args[++i];
|
|
129
|
+
if (v === undefined || v.startsWith("-")) return fail("--output needs a value");
|
|
130
|
+
mode = v;
|
|
131
|
+
} else if (a.startsWith("--output=")) {
|
|
132
|
+
mode = a.slice("--output=".length);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (mode === undefined) return "text";
|
|
136
|
+
return isOutputMode(mode) ? mode : fail(`unknown --output mode "${mode}"`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The one-shot prompt words for cmdRun. parseCli's `rest` keeps a POST-command value flag's
|
|
140
|
+
* value (its contract — owners drop their own, like cmdAuth's --key and export.ts's --out), so
|
|
141
|
+
* `rovecode run "hi" --output json` arrives as rest ["hi", "json"]. The token that followed --output
|
|
142
|
+
* is removed by POSITION, never by value (a prompt may legitimately contain the word "json"). A
|
|
143
|
+
* pre-command --output value never enters rest; a dangling --output is parseOutputMode's error. */
|
|
144
|
+
export function runPromptWords(cli: { cmd: string; rest: string[] }, argv: readonly string[]): string[] {
|
|
145
|
+
const words = cli.cmd === "run" ? [...cli.rest] : [cli.cmd, ...cli.rest]; // bare prompt keeps cmd as word 0
|
|
146
|
+
const args = argv.slice(2);
|
|
147
|
+
const isFlag = (a: string) => a.startsWith("-");
|
|
148
|
+
const cmdIdx = args.findIndex((a, i) => !isFlag(a) && !(i > 0 && VALUE_FLAGS.has(args[i - 1]!)));
|
|
149
|
+
if (cmdIdx === -1) return words;
|
|
150
|
+
// words = the non-flag tokens from `from` on; a value's word index = the non-flag tokens before it.
|
|
151
|
+
// EVERY value flag's value, not only --output's: `rovecode run "hi" --max-turns 1` used to send the prompt
|
|
152
|
+
// "hi 1" — the ceiling's number rode into the words because only --output dropped its own.
|
|
153
|
+
const from = cli.cmd === "run" ? cmdIdx + 1 : cmdIdx;
|
|
154
|
+
const drop: number[] = [];
|
|
155
|
+
args.forEach((a, oi) => {
|
|
156
|
+
const value = args[oi + 1];
|
|
157
|
+
if (!VALUE_FLAGS.has(a) || oi < cmdIdx || value === undefined || isFlag(value)) return;
|
|
158
|
+
drop.push(args.slice(from, oi + 1).filter((t) => !isFlag(t)).length);
|
|
159
|
+
});
|
|
160
|
+
for (const k of drop.reverse()) words.splice(k, 1); // descending: earlier indexes stay valid
|
|
161
|
+
return words;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------- piped stdin (`git diff | rovecode run "review this"`) ----------
|
|
165
|
+
|
|
166
|
+
/** What a pipe on stdin handed us, or "" — never from a terminal, and never waited on forever.
|
|
167
|
+
*
|
|
168
|
+
* The one hazard: stdin that is not a TTY and not a pipe anybody writes to — a child spawned with an
|
|
169
|
+
* inherited-but-idle handle, an agent's own bash tool running `rovecode run`. `readFileSync(0)` there
|
|
170
|
+
* blocks until something closes the handle, which may be never. So the first byte gets a deadline
|
|
171
|
+
* (default 3 s): nothing by then means nothing is coming, a note says so, and the run proceeds with the
|
|
172
|
+
* prompt alone. A producer that HAS started is read to EOF however long it takes — the deadline is on the
|
|
173
|
+
* first byte, not the whole stream. Bounded at `maxChars` (1 MB of text) with a note, because a 200 MB log
|
|
174
|
+
* piped in by accident should not become one 200 MB prompt. */
|
|
175
|
+
export async function readPipedStdin(
|
|
176
|
+
stdin: NodeJS.ReadableStream & { isTTY?: boolean; pause?: () => unknown; resume?: () => unknown },
|
|
177
|
+
opts: { firstByteMs?: number; maxChars?: number; note?: (line: string) => void } = {},
|
|
178
|
+
): Promise<string> {
|
|
179
|
+
if (stdin.isTTY === true) return "";
|
|
180
|
+
const firstByteMs = opts.firstByteMs ?? 3_000;
|
|
181
|
+
const maxChars = opts.maxChars ?? 1_000_000;
|
|
182
|
+
return new Promise<string>((resolve) => {
|
|
183
|
+
const chunks: Buffer[] = [];
|
|
184
|
+
let got = false, done = false;
|
|
185
|
+
const finish = (): void => {
|
|
186
|
+
if (done) return;
|
|
187
|
+
done = true; clearTimeout(timer);
|
|
188
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
189
|
+
if (text.length > maxChars) opts.note?.(`stdin: ${text.length.toLocaleString("en-US")} characters piped in — kept the first ${maxChars.toLocaleString("en-US")}`);
|
|
190
|
+
resolve(text.slice(0, maxChars));
|
|
191
|
+
};
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
if (got || done) return;
|
|
194
|
+
opts.note?.(`stdin: not a terminal, but nothing arrived in ${firstByteMs / 1000} s — ignored (pipe your input, or pass --no-stdin)`);
|
|
195
|
+
try { stdin.pause?.(); } catch { /* nothing to pause */ }
|
|
196
|
+
finish();
|
|
197
|
+
}, firstByteMs);
|
|
198
|
+
stdin.on("data", (c: Buffer | string) => { got = true; chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)); });
|
|
199
|
+
stdin.on("end", finish);
|
|
200
|
+
stdin.on("close", finish);
|
|
201
|
+
stdin.on("error", finish);
|
|
202
|
+
try { stdin.resume?.(); } catch { finish(); }
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The prompt with the piped text under it as a fenced block. The fence grows until it cannot occur inside the
|
|
207
|
+
* text (a diff of a markdown file carries ``` of its own). No words → "Here is the input:" introduces the
|
|
208
|
+
* block, so the model is never handed a bare fence. Empty pipe → the prompt unchanged. */
|
|
209
|
+
export function withPipedInput(prompt: string, piped: string): string {
|
|
210
|
+
const text = piped.replace(/\r\n?/g, "\n").replace(/\n+$/, "");
|
|
211
|
+
if (text.length === 0) return prompt;
|
|
212
|
+
let fence = "```";
|
|
213
|
+
while (text.includes(fence)) fence += "`";
|
|
214
|
+
const head = prompt.trim().length > 0 ? prompt.trim() : "Here is the input:";
|
|
215
|
+
return `${head}\n\n${fence}\n${text}\n${fence}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------- stdout guard (pi core/output-guard.ts:45-70) ----------
|
|
219
|
+
|
|
220
|
+
/** Redirect every stray stdout writer — console.log/info/debug (Bun writes them natively, not via
|
|
221
|
+
* process.stdout.write) and process.stdout.write itself — to `stderr`, so only a writer bound
|
|
222
|
+
* BEFORE the guard can reach fd 1. Returns the restorer. */
|
|
223
|
+
export function guardStdout(stderr: Writer): { restore(): void } {
|
|
224
|
+
const saved = { log: console.log, info: console.info, debug: console.debug, write: process.stdout.write };
|
|
225
|
+
const toErr = (...args: unknown[]): void => { stderr.write(`${format(...args)}\n`); };
|
|
226
|
+
console.log = toErr; console.info = toErr; console.debug = toErr;
|
|
227
|
+
process.stdout.write = ((chunk: string | Uint8Array): boolean => {
|
|
228
|
+
stderr.write(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
229
|
+
return true;
|
|
230
|
+
}) as typeof process.stdout.write;
|
|
231
|
+
return {
|
|
232
|
+
restore() {
|
|
233
|
+
console.log = saved.log; console.info = saved.info; console.debug = saved.debug;
|
|
234
|
+
process.stdout.write = saved.write;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------- exit codes ----------
|
|
240
|
+
|
|
241
|
+
/** 0 done · 130 stopped (loop.ts yields "stopped" ONLY on abort — :133/:219/:302) · 1 otherwise. */
|
|
242
|
+
export function exitCodeFor(status: RunEndStatus | undefined): number {
|
|
243
|
+
if (status === "done") return 0;
|
|
244
|
+
if (status === "stopped") return 130;
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------- sink ----------
|
|
249
|
+
|
|
250
|
+
const installSigint = (handler: () => void): (() => void) => {
|
|
251
|
+
// once: the first Ctrl-C aborts the run (a well-formed result follows); the second is the
|
|
252
|
+
// platform default again (hard exit), so a signal-deaf tool cannot hold the terminal hostage
|
|
253
|
+
try { process.once("SIGINT", handler); } catch { return () => {}; }
|
|
254
|
+
return () => { try { process.off("SIGINT", handler); } catch { /* already gone */ } };
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** one issued call; `id` = the provider's call id (the store's tool_call part id — the name fallback) */
|
|
258
|
+
type CallRecord = { id: string; tool?: string; ok: boolean; ms?: number };
|
|
259
|
+
|
|
260
|
+
export function createOutputSink(mode: OutputMode, opts: OutputSinkOptions): OutputSink {
|
|
261
|
+
const t0 = Date.now();
|
|
262
|
+
// bind the raw writer BEFORE guarding: the guard turns process.stdout.write into a stderr relay
|
|
263
|
+
const real = opts.stdout === process.stdout;
|
|
264
|
+
const out: Writer = real ? { write: process.stdout.write.bind(process.stdout) } : opts.stdout;
|
|
265
|
+
const guard = mode !== "text" && real ? guardStdout(opts.stderr) : null;
|
|
266
|
+
const ac = new AbortController();
|
|
267
|
+
const uninstall = (opts.onInterrupt ?? installSigint)(() => ac.abort());
|
|
268
|
+
let sessionId: string | null = null;
|
|
269
|
+
let baseline = 0; // store length at run_start — only THIS run's messages are accounted
|
|
270
|
+
let turn = 0; // the issuing turn: a turn's tool events follow its turn_end (loop.ts:232 → :295)
|
|
271
|
+
// issued calls by `<turn>:<callId>` in event order (the telemetry/otel.ts toolKey idiom): a call id a
|
|
272
|
+
// provider reuses across turns is one call PER TURN, never a merge — keyed by callId alone, `same`
|
|
273
|
+
// issued by 3 turns was ONE toolCall while rovecode.tool_calls said 3 (LOW-B, #39)
|
|
274
|
+
const calls = new Map<string, CallRecord>();
|
|
275
|
+
const call = (id: string): CallRecord => {
|
|
276
|
+
const key = `${turn}:${id}`;
|
|
277
|
+
const c = calls.get(key) ?? { id, ok: false };
|
|
278
|
+
calls.set(key, c);
|
|
279
|
+
return c;
|
|
280
|
+
};
|
|
281
|
+
// human progress: stdout in text mode (byte-identical to the pre-port console.log lines),
|
|
282
|
+
// stderr in json mode (a terminal user still sees progress; stdout stays the one object),
|
|
283
|
+
// nothing extra in ndjson mode (the event stream IS the progress)
|
|
284
|
+
const human = (line: string): void => {
|
|
285
|
+
if (mode === "text") out.write(`${line}\n`);
|
|
286
|
+
else if (mode === "json") opts.stderr.write(`${line}\n`);
|
|
287
|
+
};
|
|
288
|
+
return {
|
|
289
|
+
mode, signal: ac.signal,
|
|
290
|
+
onEvent(ev) {
|
|
291
|
+
if (mode === "ndjson") out.write(`${JSON.stringify(ev)}\n`);
|
|
292
|
+
if (ev.type === "run_start") { sessionId = ev.sessionId; baseline = opts.messages().length; }
|
|
293
|
+
else if (ev.type === "turn_start") turn = ev.turn;
|
|
294
|
+
else if (ev.type === "tool_execution_start") {
|
|
295
|
+
call(ev.callId).tool = ev.tool;
|
|
296
|
+
human(`→ ${ev.tool} ${String(JSON.stringify(ev.args)).slice(0, 100)}`);
|
|
297
|
+
} else if (ev.type === "tool_execution_end") {
|
|
298
|
+
Object.assign(call(ev.callId), { ok: ev.ok, ms: ev.durationMs });
|
|
299
|
+
human(`← ${ev.ok ? "ok" : "FAIL"} ${ev.output.slice(0, 200).replace(/\n/g, " ⏎ ")}`);
|
|
300
|
+
} else if (ev.type === "tool_call_failed") {
|
|
301
|
+
call(ev.callId).ok = false;
|
|
302
|
+
} else if (ev.type === "verify") {
|
|
303
|
+
// the verify gate, in the same two-line shape as a tool call: what runs, then how it ended
|
|
304
|
+
human(ev.state === "running" ? `→ verify ${ev.command.slice(0, 100)}` : `← ${ev.state === "passed" ? "ok" : "FAIL"} verify ${ev.detail ?? ev.state}`);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
finish(end) {
|
|
308
|
+
uninstall();
|
|
309
|
+
const exitCode = exitCodeFor(end?.status);
|
|
310
|
+
if (mode === "text") {
|
|
311
|
+
if (end) out.write(`\n${end.summary}\n`);
|
|
312
|
+
// "done" is the model's silence, not a verdict: one clause says what the transcript says was left
|
|
313
|
+
// (a failed call never recovered, an unanswered question, open todos). A run that only answered a
|
|
314
|
+
// question carries no `outstanding` at all and prints exactly the bytes it always did.
|
|
315
|
+
if (end?.status === "done" && end.outstanding) {
|
|
316
|
+
const clause = outstandingClause(end.outstanding);
|
|
317
|
+
if (clause !== null) out.write(`done · ${clause}\n`);
|
|
318
|
+
}
|
|
319
|
+
return exitCode;
|
|
320
|
+
}
|
|
321
|
+
const result = summarize(
|
|
322
|
+
end ?? { status: "error", summary: "stream ended without run_end" }, exitCode, sessionId, opts.model,
|
|
323
|
+
opts.messages().slice(baseline), [...calls.values()], opts.catalog ?? new ModelCatalog(), Date.now() - t0,
|
|
324
|
+
);
|
|
325
|
+
out.write(`${JSON.stringify(mode === "json" ? result : { type: "result", ...result })}\n`);
|
|
326
|
+
return exitCode;
|
|
327
|
+
},
|
|
328
|
+
close() { guard?.restore(); },
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------- run deps ----------
|
|
333
|
+
|
|
334
|
+
/** cmdRun's LoopDeps, built in one place so the wiring is unit-testable (LOW-2): the runtime's
|
|
335
|
+
* registry/store/tools, guard (port #4), cwd (port #26) and hooks (port #29), with the sink's
|
|
336
|
+
* SIGINT signal (port #21) — the fields agentLoop reads. Same object literal cmdRun used to inline,
|
|
337
|
+
* evaluated at the same argument position (tools listed at call time). */
|
|
338
|
+
export function buildRunDeps(rt: Pick<Runtime, "registry" | "store" | "guard" | "planReminder" | "cwd" | "hooks">, stream: StreamFn, sink: Pick<OutputSink, "signal">): LoopDeps {
|
|
339
|
+
return { stream, registry: rt.registry, store: rt.store, tools: rt.registry.list().map((t) => t.schema), guard: rt.guard, planReminder: rt.planReminder, cwd: rt.cwd, signal: sink.signal, hooks: rt.hooks };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function summarize(
|
|
343
|
+
end: RunEnd, exitCode: number, sessionId: string | null, model: ModelRef, msgs: Message[],
|
|
344
|
+
calls: CallRecord[], catalog: PricingSource, durationMs: number,
|
|
345
|
+
): RunResult {
|
|
346
|
+
const assistants = msgs.filter((m) => m.role === "assistant");
|
|
347
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
348
|
+
let cost: number | null = 0;
|
|
349
|
+
for (const m of assistants) {
|
|
350
|
+
const u = m.usage;
|
|
351
|
+
if (!u) continue;
|
|
352
|
+
const n = { input: u.input, output: u.output, cacheRead: u.cacheRead ?? 0, cacheWrite: u.cacheWrite ?? 0 };
|
|
353
|
+
usage.input += n.input; usage.output += n.output; usage.cacheRead += n.cacheRead; usage.cacheWrite += n.cacheWrite;
|
|
354
|
+
if (cost === null || (n.input === 0 && n.output === 0 && n.cacheRead === 0 && n.cacheWrite === 0)) continue;
|
|
355
|
+
// priced PER MESSAGE at the model that served it (Message.origin — the tui/cost.ts idiom)
|
|
356
|
+
const o = m.origin ?? model;
|
|
357
|
+
const pricing = catalog.lookup(o.provider, o.model)?.pricing;
|
|
358
|
+
const c = pricing ? costUsd(n, pricing) : undefined;
|
|
359
|
+
cost = c === undefined ? null : cost + c;
|
|
360
|
+
}
|
|
361
|
+
// names for calls that never started (denied/truncated/not_found) come from the tool_call parts, paired
|
|
362
|
+
// by OCCURRENCE — the n-th record with a call id ↔ the n-th part carrying it (one part per issued call),
|
|
363
|
+
// so a reused id names each turn's own call rather than the last part that mentioned the id
|
|
364
|
+
const names = new Map<string, string[]>();
|
|
365
|
+
for (const m of assistants) for (const p of m.parts) if (p.kind === "tool_call") names.set(p.id, [...(names.get(p.id) ?? []), p.tool]);
|
|
366
|
+
const nth = new Map<string, number>();
|
|
367
|
+
const served = assistants.at(-1)?.origin;
|
|
368
|
+
return {
|
|
369
|
+
status: end.status, summary: end.summary, sessionId,
|
|
370
|
+
// what "done" left behind (core/loop.ts assessOutstanding): failed tool calls in the last turn, an
|
|
371
|
+
// unanswered ask_user, successful edit/write count, open todos, whether the finish check asked once
|
|
372
|
+
...(end.outstanding ? { outstanding: end.outstanding } : {}),
|
|
373
|
+
model: { provider: model.provider, model: model.model },
|
|
374
|
+
origin: served ? { provider: served.provider, model: served.model } : null,
|
|
375
|
+
usage, costUsd: cost,
|
|
376
|
+
toolCalls: calls.map((c) => {
|
|
377
|
+
const n = nth.get(c.id) ?? 0; nth.set(c.id, n + 1);
|
|
378
|
+
return { tool: c.tool ?? names.get(c.id)?.[n] ?? "unknown", ok: c.ok, ...(c.ms !== undefined ? { ms: c.ms } : {}) };
|
|
379
|
+
}),
|
|
380
|
+
durationMs, exitCode,
|
|
381
|
+
};
|
|
382
|
+
}
|
package/src/cli/repl.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Interactive agent chat (omp/claude-code style): persistent session, streaming
|
|
2
|
+
* output, y/n/a approvals, slash commands. Bare `rovecode` drops here. */
|
|
3
|
+
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { agentLoop } from "../core/loop.ts";
|
|
6
|
+
import { resetTurnFailureCount } from "../memory/tools.ts";
|
|
7
|
+
import type { ApprovalFn, RunEvent } from "../core/types.ts";
|
|
8
|
+
import type { AskFn } from "../tools/ask-user.ts";
|
|
9
|
+
import { bootRuntime, NO_PROVIDER_HINT } from "./runtime.ts";
|
|
10
|
+
import { modeSwitchNote } from "../core/voice.ts";
|
|
11
|
+
import { SandboxConfigError, describeSandbox } from "../core/sandbox-config.ts";
|
|
12
|
+
|
|
13
|
+
export interface ReplState {
|
|
14
|
+
yolo: boolean;
|
|
15
|
+
provider: string;
|
|
16
|
+
model: string;
|
|
17
|
+
turns: number;
|
|
18
|
+
tokensIn: number;
|
|
19
|
+
tokensOut: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ask(rl: readline.Interface, q: string): Promise<string> {
|
|
23
|
+
return new Promise((res) => rl.question(q, (a) => res(a.trim().toLowerCase())));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** port #33: the `--plain` asker behind ask_user — `--plain` HAS a human (the y/n/a approvals prove
|
|
27
|
+
* it), headless surfaces leave the tool unbound and it fails closed. Numbered options plus free text
|
|
28
|
+
* when allowed: a number picks, other text is the typed answer, an empty line declines (null). Piped
|
|
29
|
+
* stdin is fine — it just consumes the next line. The run's abort resolves null AND is handed to
|
|
30
|
+
* rl.question itself (WIRE-1 LOW): an aborted question's callback is DISARMED, so the user's next
|
|
31
|
+
* line is a normal `line` event again instead of being swallowed by the dead callback. `out` is
|
|
32
|
+
* console.log; tests capture it. */
|
|
33
|
+
export function readlineAsker(rl: readline.Interface, out: (line: string) => void = console.log): AskFn {
|
|
34
|
+
return (q, signal) => new Promise((resolve) => {
|
|
35
|
+
const options = q.options ?? [];
|
|
36
|
+
const free = q.allowFreeText !== false;
|
|
37
|
+
out(`\n question: ${q.question}`);
|
|
38
|
+
options.forEach((o, i) => out(` ${i + 1}) ${o}`));
|
|
39
|
+
const hint = [options.length > 0 ? `1-${options.length}` : "", free ? "text" : ""].filter(Boolean).join(" or ");
|
|
40
|
+
const onAbort = (): void => { resolve(null); };
|
|
41
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
42
|
+
rl.question(` answer [${hint}; empty = decline]: `, { signal }, (line) => {
|
|
43
|
+
signal.removeEventListener("abort", onAbort);
|
|
44
|
+
const a = line.trim();
|
|
45
|
+
const n = Number(a);
|
|
46
|
+
if (a === "") resolve(null);
|
|
47
|
+
else if (Number.isInteger(n) && n >= 1 && n <= options.length) resolve({ choice: n - 1, label: options[n - 1] });
|
|
48
|
+
else if (free) resolve({ text: a });
|
|
49
|
+
else { out(" (not one of the options — declined)"); resolve(null); }
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runRepl( /* eslint-disable-line complexity */
|
|
55
|
+
opts: { yolo?: boolean; model?: string } = {},
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
// port #27: sandbox misconfig / unavailable configured rung → one-line startup error, exit 2
|
|
58
|
+
const rt = await bootRuntime().catch((e: unknown): never => {
|
|
59
|
+
if (e instanceof SandboxConfigError) { console.error(`error: ${e.message}`); process.exit(2); }
|
|
60
|
+
throw e;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const state: ReplState = {
|
|
64
|
+
yolo: opts.yolo ?? process.env.ROVECODE_YOLO === "1",
|
|
65
|
+
provider: "mock",
|
|
66
|
+
model: opts.model ?? process.env.ROVECODE_MODEL ?? "",
|
|
67
|
+
turns: 0, tokensIn: 0, tokensOut: 0,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// the registry is live: `rovecode provider add …` / `rovecode auth set …` from another terminal is
|
|
71
|
+
// picked up on the next line — no restart and no ad-hoc base-url prompt here
|
|
72
|
+
const stream = rt.stream ?? undefined;
|
|
73
|
+
const adoptDefault = (): void => {
|
|
74
|
+
const d = rt.providers.defaultRef();
|
|
75
|
+
if (d !== null) { state.provider = d.provider; state.model = state.model || d.model || "gpt-4o-mini"; }
|
|
76
|
+
};
|
|
77
|
+
adoptDefault();
|
|
78
|
+
if (state.provider === "mock") console.log(NO_PROVIDER_HINT);
|
|
79
|
+
rt.hooks.onWarning((w) => console.error(`hooks: ${w}`)); // port #29: load + runtime hook notes → stderr (cmdRun idiom)
|
|
80
|
+
|
|
81
|
+
console.log(`◆ rovecode here — plain chat with ${state.provider}/${state.model}`);
|
|
82
|
+
console.log(`session ${rt.sessionId.slice(0, 8)} in ${rt.cwd}`);
|
|
83
|
+
console.log(modeSwitchNote(state.yolo));
|
|
84
|
+
console.log(`commands: /exit /new /yolo /model <provider/model> /status /skills /memory`);
|
|
85
|
+
|
|
86
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "rovecode> " });
|
|
87
|
+
|
|
88
|
+
rt.setAskUser(readlineAsker(rl)); // port #33: ask_user over the same readline (readlineAsker above)
|
|
89
|
+
|
|
90
|
+
const approval: ApprovalFn = async (req) => {
|
|
91
|
+
const argPreview = JSON.stringify(req.revisedArgs).slice(0, 140);
|
|
92
|
+
console.log(`\n approval needed: ${req.tool} ${argPreview}`);
|
|
93
|
+
const a = await ask(rl, " allow? [y]es / [a]lways / [n]o: ");
|
|
94
|
+
return a === "a" ? "always" : a === "n" || a === "" ? "deny" : "once";
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
rl.prompt();
|
|
98
|
+
|
|
99
|
+
// port #21: one AbortController per run — Ctrl+C mid-run aborts the in-flight
|
|
100
|
+
// fetch/tools for real (abort first, then return() settles the generator); idle
|
|
101
|
+
// Ctrl+C keeps its old meaning (close the repl)
|
|
102
|
+
let running: { ac: AbortController; gen: AsyncGenerator<RunEvent> } | null = null;
|
|
103
|
+
rl.on("SIGINT", () => {
|
|
104
|
+
if (running) { running.ac.abort(); void running.gen.return(undefined as never); console.log("\n [interrupted]"); }
|
|
105
|
+
else rl.close();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
rl.on("line", async (line) => {
|
|
109
|
+
const text = line.trim();
|
|
110
|
+
if (!text) { rl.prompt(); return; }
|
|
111
|
+
if (text === "/exit" || text === "/quit") { rl.close(); return; }
|
|
112
|
+
if (text === "/yolo") { state.yolo = !state.yolo; console.log(modeSwitchNote(state.yolo)); rl.prompt(); return; }
|
|
113
|
+
if (text === "/status") { console.log(`provider=${state.provider} model=${state.model} turns=${state.turns} tokens=${state.tokensIn}in/${state.tokensOut}out\nsandbox: ${describeSandbox(rt.sandbox)}`); rl.prompt(); return; }
|
|
114
|
+
if (text === "/skills") { for (const s of rt.skillStore.list()) console.log(` ${s.name.padEnd(20)} ${s.description}`); rl.prompt(); return; }
|
|
115
|
+
if (text === "/memory") { console.log(rt.blockStore.renderForPrompt() || "(empty)"); rl.prompt(); return; }
|
|
116
|
+
if (text.startsWith("/model ")) {
|
|
117
|
+
const ref = rt.providers.resolveSelector(text.slice(7), state.provider);
|
|
118
|
+
if ("error" in ref) console.log(ref.error); else { state.provider = ref.provider; state.model = ref.model; console.log(`model → ${ref.provider}/${ref.model}`); }
|
|
119
|
+
rl.prompt(); return;
|
|
120
|
+
}
|
|
121
|
+
if (text === "/new") { rt.store.branch(rt.store.messages()[0]?.id ?? ""); console.log("branched to session start"); rl.prompt(); return; }
|
|
122
|
+
|
|
123
|
+
const reason = rt.noProviderReason(); // live — a provider added since boot is picked up here
|
|
124
|
+
if (!stream || reason !== null) { console.log(reason ?? "no provider stream"); rl.prompt(); return; }
|
|
125
|
+
if (state.provider === "mock") adoptDefault();
|
|
126
|
+
|
|
127
|
+
const def = rt.buildDef({ provider: state.provider, model: state.model });
|
|
128
|
+
|
|
129
|
+
const ac = new AbortController();
|
|
130
|
+
rt.tasks.bindRun(ac.signal); // port #26: Ctrl-C (ac.abort above) also cancels the background tasks this run started
|
|
131
|
+
// port #29: hooks ride the deps like every surface; port #26: the runtime's ONE steering queue (not a
|
|
132
|
+
// fresh one) so background-task completion notes — and any hook-pushed steer — reach the next turn
|
|
133
|
+
const gen = agentLoop(def, text, {}, rt.buildCfg(state.yolo, approval), { stream, registry: rt.registry, store: rt.store, tools: rt.registry.list().map((t) => t.schema), guard: rt.guard, planReminder: rt.planReminder, cwd: rt.cwd, signal: ac.signal, hooks: rt.hooks }, rt.steering);
|
|
134
|
+
running = { ac, gen };
|
|
135
|
+
try {
|
|
136
|
+
let live = "";
|
|
137
|
+
for await (const ev of gen) {
|
|
138
|
+
if (ev.type === "turn_start") { resetTurnFailureCount(); state.turns++; }
|
|
139
|
+
if (ev.type === "message_update") { process.stdout.write(ev.delta); live += ev.delta; }
|
|
140
|
+
if (ev.type === "tool_execution_start") { console.log(`\n → ${ev.tool} ${JSON.stringify(ev.args).slice(0, 120)}`); }
|
|
141
|
+
if (ev.type === "tool_execution_end") { console.log(` ← ${ev.ok ? "ok" : "FAIL"} ${ev.output.slice(0, 160).replace(/\n/g, " ⏎ ")}`); }
|
|
142
|
+
if (ev.type === "run_end") {
|
|
143
|
+
if (!live.trim()) console.log(ev.summary);
|
|
144
|
+
else console.log();
|
|
145
|
+
if (ev.status !== "done") console.log(` [${ev.status}]`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const n of rt.drainRouterNotes()) console.log(` [${n}]`); // port #14 fallback advances
|
|
149
|
+
for (const m of rt.store.messages()) if (m.usage) { state.tokensIn += m.usage.input; state.tokensOut += m.usage.output; }
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.log(`error: ${e instanceof Error ? e.message : String(e)}`);
|
|
152
|
+
} finally {
|
|
153
|
+
running = null;
|
|
154
|
+
}
|
|
155
|
+
rl.prompt();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// every quit path lands here (Ctrl+D, /exit, /quit, idle Ctrl+C → rl.close()), so this is the ONE exit
|
|
159
|
+
rl.on("close", async () => {
|
|
160
|
+
// port #29: a run still in flight dies with the surface (abort, then let its generator settle) BEFORE
|
|
161
|
+
// session_close fires once — after in-flight on_event taps drained (hooks.close() waits for them)
|
|
162
|
+
if (running) { running.ac.abort(); await running.gen.return(undefined as never).catch(() => {}); }
|
|
163
|
+
// port #26: quitting leaves no background children — their runs (and subprocess trees) die
|
|
164
|
+
// now and settle, bounded, before the process goes (same policy as cmdRun's exit())
|
|
165
|
+
rt.tasks.cancelAll();
|
|
166
|
+
await rt.tasks.drain(2_000);
|
|
167
|
+
await rt.hooks.close().catch(() => {});
|
|
168
|
+
void rt.mcp?.close().catch(() => {}); // kill MCP child processes (TUI does the same in app.ts)
|
|
169
|
+
console.log(`\nbye — session ${rt.sessionId.slice(0, 8)} saved (${state.turns} turns, ${state.tokensIn}in/${state.tokensOut}out tokens)`);
|
|
170
|
+
process.exit(0);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Which session `rovecode` reopens at boot, from argv.
|
|
2
|
+
*
|
|
3
|
+
* Three spellings, one rule — an explicit id always wins:
|
|
4
|
+
* rovecode --resume <id> that session (a unique prefix resolves in the TUI, session-cmd.ts resolveBootSession)
|
|
5
|
+
* rovecode --resume the newest session that holds something
|
|
6
|
+
* rovecode --continue the same — the flag every CLI that got this right converged on
|
|
7
|
+
* `--resume` followed by another flag (`rovecode --resume --yolo`) is `--resume` with no id, never "resume the
|
|
8
|
+
* session named --yolo". With nothing to continue from, the answer is undefined and the TUI starts fresh, as
|
|
9
|
+
* it always did; that is the honest outcome, not an error. */
|
|
10
|
+
|
|
11
|
+
import { newestSession } from "../core/session.ts";
|
|
12
|
+
|
|
13
|
+
export interface ResumeRequest {
|
|
14
|
+
/** the id or prefix the user named */
|
|
15
|
+
id?: string;
|
|
16
|
+
/** `--continue`, or `--resume` with no id: reopen the newest non-empty session */
|
|
17
|
+
newest: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseResume(argv: readonly string[]): ResumeRequest {
|
|
21
|
+
const ix = argv.indexOf("--resume");
|
|
22
|
+
const arg = ix !== -1 ? argv[ix + 1] : undefined;
|
|
23
|
+
const id = arg !== undefined && !arg.startsWith("-") ? arg : undefined;
|
|
24
|
+
return { ...(id !== undefined ? { id } : {}), newest: id === undefined && (ix !== -1 || argv.includes("--continue")) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The session id to boot with, or undefined for a fresh one. */
|
|
28
|
+
export function resolveResume(argv: readonly string[], sessionsDir: string): string | undefined {
|
|
29
|
+
const r = parseResume(argv);
|
|
30
|
+
if (r.id !== undefined) return r.id;
|
|
31
|
+
return r.newest ? newestSession(sessionsDir)?.id : undefined;
|
|
32
|
+
}
|