rovecode 0.4.0-beta.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -69
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -37
- 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 -512
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-dybnr56b.js +0 -2
- package/dist/cli/ask-user-p8hq4xgj.js +0 -2
- package/dist/cli/auth-login-ewpgw5sm.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-xv3ypwev.js +0 -9
- package/dist/cli/catalog-737wb2s0.js +0 -2
- package/dist/cli/cli-arhg40m0.js +0 -2
- package/dist/cli/client-cf2pxx8q.js +0 -2
- package/dist/cli/commands-3p7e4xxs.js +0 -2
- package/dist/cli/connect-3q93d7cb.js +0 -2
- package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
- package/dist/cli/context-report-hbw9zfes.js +0 -2
- package/dist/cli/count-remote-mby98cd0.js +0 -2
- package/dist/cli/design-122y0axd.js +0 -2
- package/dist/cli/dispatch-b4egzvvh.js +0 -2
- package/dist/cli/doctor-x4jkv72e.js +0 -3
- package/dist/cli/executor-ftvg6tsy.js +0 -2
- package/dist/cli/export-pdgdhkch.js +0 -2
- package/dist/cli/files-cez9a96p.js +0 -2
- package/dist/cli/gauntlet-r3xxaszc.js +0 -2
- package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
- package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
- package/dist/cli/hashline-ewg5hbe3.js +0 -2
- package/dist/cli/http-n0kehsk8.js +0 -5
- package/dist/cli/index-z5qt1s76.js +0 -2
- package/dist/cli/install-80mp63kx.js +0 -2
- package/dist/cli/loop-12twjcat.js +0 -2
- package/dist/cli/main-01pv9206.js +0 -4
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-1ztz6fkj.js +0 -10
- package/dist/cli/main-23q7cmww.js +0 -9
- package/dist/cli/main-2rzbexn2.js +0 -3
- package/dist/cli/main-2wyax8k9.js +0 -9
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2z3dek0b.js +0 -3
- package/dist/cli/main-2zgsknth.js +0 -3
- package/dist/cli/main-45ejth3a.js +0 -4
- package/dist/cli/main-45rn3trk.js +0 -22
- package/dist/cli/main-4p4e2w7x.js +0 -4
- package/dist/cli/main-4y0tnfpa.js +0 -16
- package/dist/cli/main-5py0rkmc.js +0 -4
- package/dist/cli/main-6dtqmbt6.js +0 -7
- package/dist/cli/main-6h9x282m.js +0 -4
- package/dist/cli/main-6vjeds42.js +0 -3
- package/dist/cli/main-78gq4bt9.js +0 -6
- package/dist/cli/main-7jd5vh3x.js +0 -4
- package/dist/cli/main-7kt6r53y.js +0 -4
- package/dist/cli/main-8c1tbazx.js +0 -58
- package/dist/cli/main-9a9rnh47.js +0 -19
- package/dist/cli/main-9ht36z12.js +0 -3
- package/dist/cli/main-a2yfvcy9.js +0 -7
- package/dist/cli/main-a3f51n0x.js +0 -5
- package/dist/cli/main-b8zq261k.js +0 -3
- package/dist/cli/main-bxtvnf6d.js +0 -13
- package/dist/cli/main-edxc3yzt.js +0 -4
- package/dist/cli/main-evgz4mp5.js +0 -21
- package/dist/cli/main-f33fc5je.js +0 -9
- package/dist/cli/main-fvnpq46y.js +0 -12
- package/dist/cli/main-gbbty4d4.js +0 -3
- package/dist/cli/main-gth53dnt.js +0 -25
- package/dist/cli/main-hqbz10aw.js +0 -9
- package/dist/cli/main-hrrvcfan.js +0 -38
- package/dist/cli/main-hzwtsb2m.js +0 -5
- package/dist/cli/main-j7ttv0sd.js +0 -34
- package/dist/cli/main-jak598k9.js +0 -5
- package/dist/cli/main-kba6zeyd.js +0 -6
- package/dist/cli/main-kwwsz6rq.js +0 -3
- package/dist/cli/main-m8vm17zq.js +0 -3
- package/dist/cli/main-mg4f96e1.js +0 -3
- package/dist/cli/main-mg9b20ac.js +0 -18
- package/dist/cli/main-mgb9ccnx.js +0 -3
- package/dist/cli/main-mjt2p7aj.js +0 -3
- package/dist/cli/main-n6qrdbmy.js +0 -3
- package/dist/cli/main-na7wse0x.js +0 -5
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-ntqef02r.js +0 -10
- package/dist/cli/main-nvc3yjay.js +0 -136
- package/dist/cli/main-p0cfn6nr.js +0 -16
- package/dist/cli/main-qj2djy17.js +0 -19
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-qvarybsp.js +0 -3
- package/dist/cli/main-rebtt91r.js +0 -5
- package/dist/cli/main-rpg7h8mb.js +0 -3
- package/dist/cli/main-rsy72qmw.js +0 -15
- package/dist/cli/main-rvetps99.js +0 -18
- package/dist/cli/main-s4bb0jav.js +0 -3
- package/dist/cli/main-s9v8k74e.js +0 -3
- package/dist/cli/main-tjvwmscs.js +0 -3
- package/dist/cli/main-tkgarpjj.js +0 -4
- package/dist/cli/main-v8y60bb2.js +0 -3
- package/dist/cli/main-vhrrq337.js +0 -3
- package/dist/cli/main-vp2dfb7s.js +0 -4
- package/dist/cli/main-vqbr22sz.js +0 -8
- package/dist/cli/main-vxnwe5xx.js +0 -18
- package/dist/cli/main-wgph00xf.js +0 -5
- package/dist/cli/main-wk2csfnj.js +0 -5
- package/dist/cli/main-wm997zjx.js +0 -3
- package/dist/cli/main-wpkyraxh.js +0 -3
- package/dist/cli/main-wqt32p5x.js +0 -4
- package/dist/cli/main-x9ct6y1a.js +0 -3
- package/dist/cli/main-xfekqh9m.js +0 -7
- package/dist/cli/main-xt9zc3n6.js +0 -7
- package/dist/cli/main-xx2z3zh5.js +0 -4
- package/dist/cli/main-y5c82rxr.js +0 -3
- package/dist/cli/main-yrjt2sqt.js +0 -14
- package/dist/cli/main-ys6zj3yr.js +0 -3
- package/dist/cli/main-ywbxshqc.js +0 -8
- package/dist/cli/main-z13755t8.js +0 -25
- package/dist/cli/main-zc7pyrbj.js +0 -4
- package/dist/cli/main.js +0 -279
- package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
- package/dist/cli/mcp-login-bthtfpt7.js +0 -2
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
- package/dist/cli/notify-54v5z9dz.js +0 -2
- package/dist/cli/oauth-g5gme95c.js +0 -2
- package/dist/cli/output-satndjap.js +0 -16
- package/dist/cli/profiles-sfhpbq3m.js +0 -2
- package/dist/cli/provider-config-hv3xtdt4.js +0 -2
- package/dist/cli/provider-kwzq6g84.js +0 -2
- package/dist/cli/registry-fh0hdnyn.js +0 -2
- package/dist/cli/registry-y1y8e94r.js +0 -2
- package/dist/cli/repl-t4z03mqq.js +0 -11
- package/dist/cli/resume-fqt4chg8.js +0 -2
- package/dist/cli/run-flags-rysbag9t.js +0 -2
- package/dist/cli/runtime-j19fjbsa.js +0 -2
- package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
- package/dist/cli/server-r0b6bksk.js +0 -5
- package/dist/cli/session-arg-txmn5g4x.js +0 -2
- package/dist/cli/session-ed250d9j.js +0 -2
- package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
- package/dist/cli/settings-y9rzcqx8.js +0 -2
- package/dist/cli/setup-jmbr11j0.js +0 -2
- package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
- package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
- package/dist/cli/smoke-1bg937kx.js +0 -8
- package/dist/cli/start-chat-p01cdks3.js +0 -12
- package/dist/cli/stream-4wmyaypz.js +0 -2
- package/dist/cli/task-eg4s093s.js +0 -2
- package/dist/cli/tasks-12v9rr9k.js +0 -2
- package/dist/cli/thinking-a5ngvqyh.js +0 -2
- package/dist/cli/todo-1wxpcecx.js +0 -2
- package/dist/cli/tools-2ftsya7w.js +0 -2
- package/dist/cli/tools-x1tj4fxm.js +0 -2
- package/dist/cli/trust-cmd-hccxehzb.js +0 -2
- package/dist/cli/update-check-ygt3vd7m.js +0 -2
- package/dist/cli/update-cmd-v23qhr8c.js +0 -2
- package/dist/cli/voice-g1gtck92.js +0 -2
- package/dist/cli/webfetch-0nnrjgb5.js +0 -2
- package/dist/cli/websearch-f0vr2p7d.js +0 -2
- package/dist/cli/workspace-9rq1w4ta.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -6
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/rovecode.exe +0 -4
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** design_audit / design_direction — the agent's half of the design protocol (design/rules.ts).
|
|
2
|
+
*
|
|
3
|
+
* Split like provider_list/provider_edit (and task/task_status) for the same reason: reading must
|
|
4
|
+
* never cost a prompt, writing must be visible.
|
|
5
|
+
* - design_audit is kind "read" -> policy action file.read, auto-allowed under the gated rules and
|
|
6
|
+
* in plan mode. Checking your own work has to be free, or it will not happen.
|
|
7
|
+
* - design_direction is kind "custom" -> action tool.design_direction, PROMPT under the gated rules
|
|
8
|
+
* (cli/runtime.ts buildCfg), denied in plan mode by the blanket tool.* rule, allowed under yolo.
|
|
9
|
+
* It writes the project's design identity, and the approval card is where the human sees exactly
|
|
10
|
+
* what is being recorded on their behalf. It is asked ONCE per project, which is the whole design
|
|
11
|
+
* of the feature -- so the one prompt is the point, not friction.
|
|
12
|
+
*
|
|
13
|
+
* design_direction deliberately cannot invent a direction: `set` records what the human chose. The
|
|
14
|
+
* model is expected to have asked first (the prompt section says so). Nothing here can enforce that
|
|
15
|
+
* -- what it can do is keep the record honest and put the write in front of a human once. */
|
|
16
|
+
|
|
17
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
18
|
+
import type { Tool, ToolOutput } from "../core/types.ts";
|
|
19
|
+
import { auditFiles, auditProject, formatFindings, type Finding } from "../design/audit.ts";
|
|
20
|
+
import {
|
|
21
|
+
designPath, loadDirection, parseDirection, renderDirection, saveDirection,
|
|
22
|
+
type DesignDirection,
|
|
23
|
+
} from "../design/direction.ts";
|
|
24
|
+
|
|
25
|
+
/** Resolve a caller-supplied path against the run's cwd and refuse to leave it: an audit is about
|
|
26
|
+
* THIS project's files, and a read tool that wanders is a read tool nobody can reason about. */
|
|
27
|
+
function insideCwd(cwd: string, p: string): string | null {
|
|
28
|
+
const abs = isAbsolute(p) ? resolve(p) : resolve(cwd, p);
|
|
29
|
+
const rel = relative(resolve(cwd), abs);
|
|
30
|
+
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? null : abs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function designAuditTool(): Tool {
|
|
34
|
+
return {
|
|
35
|
+
schema: {
|
|
36
|
+
name: "design_audit",
|
|
37
|
+
description:
|
|
38
|
+
"Check interface code for the patterns that arrive when nobody decided (a webfont loaded without a choice, amber doing accent duty, the three-up feature grid, a centred full-viewport hero, the violet-to-blue gradient, nothing but square corners) and for drift from the direction this project recorded in .rovecode/design.json. Run it on the SOURCE TREE, not on a fetched page: a built page measures the framework's output, not the design. Pass the page files you touched along with the components they import, because density and centring are scored per page and 'no rounded corner anywhere' is scored per project. It reports evidence, not verdicts: each finding says what it counted, so you can fix it or explain why it is wrong here. Never prompts, never writes.",
|
|
39
|
+
args: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
files: { type: "array", items: { type: "string" }, description: "paths inside the project to audit (html/css/jsx/tsx/vue/svelte)" },
|
|
43
|
+
source: { type: "string", description: "audit this markup/CSS directly instead of reading files" },
|
|
44
|
+
ignore: { type: "array", items: { type: "string" }, description: "rule ids to skip (the full table is in docs/design.md): cliche-font, font-deviation, font-named-not-loaded, cliche-accent-amber, accent-deviation, template-grid, template-icons, reflex-hero, cliche-gradient, off-palette, decoration-density, rule-line-density, everything-centered, all-square" },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
kind: "read",
|
|
49
|
+
sequential: false,
|
|
50
|
+
async execute(args, ctx): Promise<ToolOutput> {
|
|
51
|
+
const a = (args && typeof args === "object" ? args : {}) as { files?: string[]; source?: string; ignore?: string[] };
|
|
52
|
+
const direction = loadDirection(ctx.cwd);
|
|
53
|
+
const ignore = a.ignore ?? [];
|
|
54
|
+
const findings: Finding[] = [];
|
|
55
|
+
|
|
56
|
+
// `source` is one pathless page: auditProject treats a lone file as a page so pasted markup still
|
|
57
|
+
// gets the page-scoped density and centring checks (auditSource alone is file-scope by design).
|
|
58
|
+
if (typeof a.source === "string" && a.source.length > 0) {
|
|
59
|
+
findings.push(...auditProject([{ path: "source", text: a.source }], { direction, ignore }));
|
|
60
|
+
}
|
|
61
|
+
const paths = a.files ?? [];
|
|
62
|
+
if (paths.length > 0) {
|
|
63
|
+
const resolved: string[] = [];
|
|
64
|
+
const outside: string[] = [];
|
|
65
|
+
for (const p of paths) {
|
|
66
|
+
const abs = insideCwd(ctx.cwd, p);
|
|
67
|
+
if (abs === null) outside.push(p); else resolved.push(abs);
|
|
68
|
+
}
|
|
69
|
+
if (outside.length > 0) {
|
|
70
|
+
return { ok: false, output: `design_audit only reads files inside the project. Outside: ${outside.join(", ")}` };
|
|
71
|
+
}
|
|
72
|
+
findings.push(...auditFiles(resolved, { direction, ignore }).map((f) => ({
|
|
73
|
+
...f, ...(f.file !== undefined ? { file: relative(resolve(ctx.cwd), f.file) || f.file } : {}),
|
|
74
|
+
})));
|
|
75
|
+
}
|
|
76
|
+
if (paths.length === 0 && (a.source === undefined || a.source.length === 0)) {
|
|
77
|
+
return { ok: false, output: "design_audit needs `files` (paths inside the project) or `source` (markup/CSS text)." };
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, output: formatFindings(findings, direction), data: { findings, direction } };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function designDirectionTool(): Tool {
|
|
85
|
+
return {
|
|
86
|
+
schema: {
|
|
87
|
+
name: "design_direction",
|
|
88
|
+
description:
|
|
89
|
+
"Read or record this project's design direction (.rovecode/design.json). `get` returns the recorded direction, or says none is recorded. `set` records the direction THE HUMAN CHOSE after you proposed three — do not call it with a direction you picked yourself. The one exception is a headless run where nothing can ask a human: then build ONE direction and record it with provisional:true and the other two in `alternatives`, so the next interactive session asks before more UI is built. Recording it is what makes the choice a once-per-project question and lets design_audit check later screens for consistency.",
|
|
90
|
+
args: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
action: { type: "string", enum: ["get", "set"] },
|
|
94
|
+
name: { type: "string", description: "short name of the chosen direction, as presented to the human" },
|
|
95
|
+
rationale: { type: "string", description: "one line: why it suits this product" },
|
|
96
|
+
palette: { type: "object", description: "chosen colours, e.g. {\"ink\":\"#0b1a2e\",\"accent\":\"#c2410c\"}" },
|
|
97
|
+
typeface: { type: "object", description: "{\"display\":\"...\",\"text\":\"...\"}" },
|
|
98
|
+
corners: { type: "string", enum: ["sharp", "soft", "round"] },
|
|
99
|
+
layout: { type: "string", enum: ["centered", "left", "asymmetric", "grid"] },
|
|
100
|
+
density: { type: "string", enum: ["tight", "regular", "airy"] },
|
|
101
|
+
sectionOrder: { type: "array", items: { type: "string" }, description: "the page's blocks in order, e.g. [\"hero\",\"proof\",\"pricing\",\"faq\"]" },
|
|
102
|
+
heroPattern: { type: "string", description: "the hero pattern by name, e.g. \"command-first\" or \"product-forward\"" },
|
|
103
|
+
typeScale: { type: "array", items: { type: "number" }, description: "the chosen type steps, e.g. [14,16,20,28,44]" },
|
|
104
|
+
motion: { type: "string", description: "the motion rule in the human's words, e.g. \"one entrance per section, no loops\"" },
|
|
105
|
+
copyRegister: { type: "string", description: "how the copy reads, e.g. \"technical, names and numbers, no marketing verbs\"" },
|
|
106
|
+
notes: { type: "string", description: "what the audit cannot infer: imagery, what to avoid here" },
|
|
107
|
+
provisional: { type: "boolean", description: "true ONLY when no human could be asked (a headless run): you built to one direction to get unblocked. Never true when a human answered." },
|
|
108
|
+
alternatives: { type: "array", items: { type: "string" }, description: "with provisional: the names of the directions you did NOT build, so the next interactive session can offer them" },
|
|
109
|
+
},
|
|
110
|
+
required: ["action"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
kind: "custom",
|
|
114
|
+
sequential: true,
|
|
115
|
+
// `get` reads .rovecode/design.json and writes nothing; `set` records the project's design
|
|
116
|
+
// identity. Only the second is worth a human's attention, so the two get their own policy
|
|
117
|
+
// resource (types.ts Tool.resource) instead of sharing one rule under the tool name. Anything
|
|
118
|
+
// that is not the literal "get" is treated as the write: an unknown action must not read as
|
|
119
|
+
// the safer of the two.
|
|
120
|
+
resource(args: unknown): string {
|
|
121
|
+
const a = args && typeof args === "object" ? (args as Record<string, unknown>) : undefined;
|
|
122
|
+
return a?.["action"] === "get" ? "get" : "set";
|
|
123
|
+
},
|
|
124
|
+
async execute(args, ctx): Promise<ToolOutput> {
|
|
125
|
+
const a = (args && typeof args === "object" ? args : {}) as Record<string, unknown>;
|
|
126
|
+
if (a["action"] === "get") {
|
|
127
|
+
const d = loadDirection(ctx.cwd);
|
|
128
|
+
return d === null
|
|
129
|
+
? { ok: true, output: `No design direction recorded for this project (${designPath(ctx.cwd)} does not exist). Propose three distinct directions and let the human choose before writing UI. If nothing here can ask a human, build ONE and record it with provisional: true.` }
|
|
130
|
+
: { ok: true, output: renderDirection(d), data: d };
|
|
131
|
+
}
|
|
132
|
+
if (a["action"] !== "set") return { ok: false, output: "design_direction: action must be \"get\" or \"set\"." };
|
|
133
|
+
|
|
134
|
+
const parsed: DesignDirection | null = parseDirection(a);
|
|
135
|
+
if (parsed === null) {
|
|
136
|
+
return { ok: false, output: "design_direction set needs at least `name` — the short name of the direction the human chose." };
|
|
137
|
+
}
|
|
138
|
+
// Who chose is DERIVED, never taken from the call: `provisional` means the agent picked, its
|
|
139
|
+
// absence means a human did. Trusting the model with both fields lets one contradict the other,
|
|
140
|
+
// and the whole point of the field is that it can be believed. Stamped on every write, so a
|
|
141
|
+
// record always says who — absent is only ever a file written before the field existed.
|
|
142
|
+
parsed.chosenBy = parsed.provisional === true ? "agent" : "human";
|
|
143
|
+
const path = saveDirection(ctx.cwd, parsed);
|
|
144
|
+
if (parsed.provisional === true) {
|
|
145
|
+
const alts = parsed.alternatives === undefined ? "" : ` The alternatives (${parsed.alternatives.join(", ")}) are recorded with it.`;
|
|
146
|
+
return { ok: true, output: `Recorded "${parsed.name}" in ${path} as PROVISIONAL — you chose it, not the human.${alts} Build to it and say so in your final summary, naming what you did not build. The next interactive session asks before more UI is written.`, data: parsed };
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, output: `Recorded "${parsed.name}" in ${path}. Later UI in this project is built to it, and design_audit checks against it.`, data: parsed };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PORT #18 — persistent eval cell, feature-flagged (ROVECODE_EVAL_CELL=1).
|
|
3
|
+
*
|
|
4
|
+
* Concept sources (research/source_snapshots):
|
|
5
|
+
* - OMP (can1357-oh-my-pi), MIT — persistent eval cells:
|
|
6
|
+
* docs/tools/eval.md:3 ("one tool call is one cell; state survives later calls"),
|
|
7
|
+
* :108 ("Persistent worker VM keyed by js:${sessionId}"; reset recreates the VM),
|
|
8
|
+
* :179 ("Cancellation is destructive when needed: JS terminates its worker"),
|
|
9
|
+
* :185 (output window byte budget), :65 ("(no output)" placeholder).
|
|
10
|
+
* packages/coding-agent/src/eval/js/shared/indirect-eval.ts:13-23 — user code runs
|
|
11
|
+
* through *indirect* `globalThis.eval` (global scope), deliberately NOT node:vm:
|
|
12
|
+
* Bun SIGTRAPs when Worker.terminate() fires mid-vm.runInContext — and terminate
|
|
13
|
+
* is exactly our timeout path.
|
|
14
|
+
* packages/coding-agent/src/eval/js/worker-core.ts:279-308 — run/result message
|
|
15
|
+
* protocol; execution errors become result payloads, never worker throws.
|
|
16
|
+
* packages/coding-agent/src/eval/js/shared/rewrite-imports.ts:531-550 — TS strip +
|
|
17
|
+
* async wrapping (docs/tools/eval.md:110). Simplified here: Bun.Transpiler with
|
|
18
|
+
* deadCodeElimination:false (keeps the trailing expression), thenable completion
|
|
19
|
+
* values awaited, and one async-IIFE retry at whichever stage rejects bare
|
|
20
|
+
* top-level forms (enables top-level await / bare return; declarations in the
|
|
21
|
+
* wrapped path are cell-local):
|
|
22
|
+
* - transpile stage: the transpiler parses cells as ES modules, so bare
|
|
23
|
+
* `return` is a BuildMessage error there — retry transpiling the ORIGINAL
|
|
24
|
+
* source wrapped (types still strip); if that also fails, rethrow the first
|
|
25
|
+
* error so diagnostics point at the unwrapped code;
|
|
26
|
+
* - eval stage: top-level `await` is module-legal but eval-illegal
|
|
27
|
+
* (SyntaxError) — retry evaluating the transpiled js wrapped.
|
|
28
|
+
* - prime-agent RLM single-REPL concept: prime-agent-runtime/src/rlm/repl.md:3-5
|
|
29
|
+
* (cells execute "in one persistent __main__ namespace" per session), :53-55
|
|
30
|
+
* (result = repr of the trailing expression), repl.py. BLUEPRINT rejects the
|
|
31
|
+
* Python kernel, so this is the TS-only analog: one Bun Worker per session.
|
|
32
|
+
*
|
|
33
|
+
* Ouroboros rule — what cell code CAN see:
|
|
34
|
+
* - Standard Bun Worker realm globals: Bun, fetch, process (including the inherited
|
|
35
|
+
* process.env — same trust level as the bash tool's child shell), timers, require.
|
|
36
|
+
* This is NOT a security sandbox; the "execute" policy gate (deny-default → prompt)
|
|
37
|
+
* is the control, exactly like bash.
|
|
38
|
+
* - Bootstrap additions to the global scope: the six console methods are replaced
|
|
39
|
+
* with capture stubs, and `self.onmessage` holds the (secret-free) runner.
|
|
40
|
+
* Everything else lives in the bootstrap IIFE's closure, unreachable from cell
|
|
41
|
+
* code, which evaluates via indirect eval in the worker's global scope.
|
|
42
|
+
* What is NOT pre-exposed (a non-exposure claim, NOT unreachability): nothing from
|
|
43
|
+
* the rovecode module graph is imported into or handed to the worker — it loads only
|
|
44
|
+
* this inline bootstrap blob, so the tool registry (v1 deliberately has NO tool
|
|
45
|
+
* re-entry bridge — scope note), session stores, permission/approval machinery,
|
|
46
|
+
* and the graders under src/eval/* stay live only in the host process, and the
|
|
47
|
+
* only traffic across the seam is {id, code} in and {id, ok, stdout, value, error}
|
|
48
|
+
* out via postMessage. Cell code CAN still `await import()` or Bun.file-read any
|
|
49
|
+
* file on disk (src/** included) exactly like the bash tool can run `bun -e` —
|
|
50
|
+
* the execute-kind policy gate is the control there, not the worker boundary.
|
|
51
|
+
*
|
|
52
|
+
* Lifecycle: worker per ctx.sessionId, created lazily, unref'd so it never holds the
|
|
53
|
+
* host process open. Cell errors keep the worker (state survives); timeout/abort/reset
|
|
54
|
+
* terminate it and the session's cell state is lost (reported in the output). An
|
|
55
|
+
* uncaught BACKGROUND error (timer throw / unhandled rejection firing the worker
|
|
56
|
+
* "error" event, possibly with no call in flight) also kills the worker; the reason is
|
|
57
|
+
* parked in `crashed` and the next call's output is prefixed with a crash note, so a
|
|
58
|
+
* fresh cell never silently contradicts "state survives". A cell that kills its own
|
|
59
|
+
* worker cleanly (e.g. process.exit()) fires no error event and surfaces as a timeout
|
|
60
|
+
* on a later call.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
import type { Tool, ToolContext, ToolOutput } from "../core/types.ts";
|
|
64
|
+
|
|
65
|
+
export const EVAL_CELL_FLAG = "ROVECODE_EVAL_CELL";
|
|
66
|
+
|
|
67
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
68
|
+
const MIN_TIMEOUT_MS = 50;
|
|
69
|
+
const MAX_TIMEOUT_MS = 120_000;
|
|
70
|
+
const DEFAULT_OUTPUT_BYTES = 16_384;
|
|
71
|
+
const MIN_OUTPUT_BYTES = 256;
|
|
72
|
+
const MAX_OUTPUT_BYTES = 65_536;
|
|
73
|
+
|
|
74
|
+
/** Self-contained worker bootstrap (plain JS, no imports — see ouroboros note above).
|
|
75
|
+
* Hard caps inside the worker bound postMessage payloads; the model-facing byte
|
|
76
|
+
* budget is enforced host-side by truncateToBudget. */
|
|
77
|
+
const BOOTSTRAP = `
|
|
78
|
+
(() => {
|
|
79
|
+
"use strict";
|
|
80
|
+
const MAX_CAPTURE = 1048576;
|
|
81
|
+
const MAX_VALUE = 262144;
|
|
82
|
+
const transpiler = new Bun.Transpiler({ loader: "ts", deadCodeElimination: false });
|
|
83
|
+
let buf = [];
|
|
84
|
+
let bufLen = 0;
|
|
85
|
+
let capped = false;
|
|
86
|
+
const fmt = (v) => (typeof v === "string" ? v : Bun.inspect(v));
|
|
87
|
+
const capture = (...parts) => {
|
|
88
|
+
if (capped) return;
|
|
89
|
+
const line = parts.map(fmt).join(" ") + "\\n";
|
|
90
|
+
bufLen += line.length;
|
|
91
|
+
if (bufLen > MAX_CAPTURE) { capped = true; buf.push("[console capture cap reached]\\n"); return; }
|
|
92
|
+
buf.push(line);
|
|
93
|
+
};
|
|
94
|
+
for (const k of ["log", "info", "warn", "error", "debug", "trace"]) console[k] = capture;
|
|
95
|
+
self.onmessage = async (ev) => {
|
|
96
|
+
const { id, code } = ev.data;
|
|
97
|
+
buf = []; bufLen = 0; capped = false;
|
|
98
|
+
let ok = true; let value = ""; let error = "";
|
|
99
|
+
try {
|
|
100
|
+
// Bare top-level return is illegal in a module, so it fails at TRANSPILE time
|
|
101
|
+
// (BuildMessage), before eval — retry with the original source async-wrapped.
|
|
102
|
+
// If the wrapped transpile fails too, the code is genuinely broken: rethrow the
|
|
103
|
+
// FIRST error so diagnostics describe the unwrapped source.
|
|
104
|
+
let js; let preWrapped = false;
|
|
105
|
+
try {
|
|
106
|
+
js = transpiler.transformSync(code);
|
|
107
|
+
} catch (e1) {
|
|
108
|
+
try { js = transpiler.transformSync("(async () => {\\n" + code + "\\n})()"); preWrapped = true; }
|
|
109
|
+
catch { throw e1; }
|
|
110
|
+
}
|
|
111
|
+
let v;
|
|
112
|
+
try {
|
|
113
|
+
v = (0, eval)(js);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// Top-level await is module-legal but a SyntaxError in plain eval; retry
|
|
116
|
+
// wrapped (OMP docs/tools/eval.md:110). Declarations in wrapped paths
|
|
117
|
+
// (either stage) are cell-local.
|
|
118
|
+
if (!preWrapped && e instanceof SyntaxError) v = (0, eval)("(async () => {\\n" + js + "\\n})()");
|
|
119
|
+
else throw e;
|
|
120
|
+
}
|
|
121
|
+
if (v && (typeof v === "object" || typeof v === "function") && typeof v.then === "function") v = await v;
|
|
122
|
+
if (v !== undefined) {
|
|
123
|
+
value = Bun.inspect(v);
|
|
124
|
+
if (value.length > MAX_VALUE) value = value.slice(0, MAX_VALUE) + "...[value capped]";
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
ok = false;
|
|
128
|
+
error = e instanceof Error ? e.name + ": " + e.message : String(e);
|
|
129
|
+
}
|
|
130
|
+
postMessage({ id, ok, stdout: buf.join(""), value, error });
|
|
131
|
+
};
|
|
132
|
+
postMessage({ type: "ready" });
|
|
133
|
+
})();
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
interface WorkerResult { id: number; ok: boolean; stdout: string; value: string; error: string }
|
|
137
|
+
|
|
138
|
+
interface CellRuntime {
|
|
139
|
+
worker: Worker;
|
|
140
|
+
pending: Map<number, (r: WorkerResult) => void>;
|
|
141
|
+
ready: Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const cells = new Map<string, CellRuntime>();
|
|
145
|
+
/** Reasons from worker "error" events (background timer throws / unhandled
|
|
146
|
+
* rejections), parked per session: with no call in flight destroyCell has nobody
|
|
147
|
+
* to tell, so the NEXT call consumes the reason and explains its fresh cell. */
|
|
148
|
+
const crashed = new Map<string, string>();
|
|
149
|
+
let callSeq = 0;
|
|
150
|
+
let bootstrapUrl: string | null = null;
|
|
151
|
+
|
|
152
|
+
/** Bun's ErrorEvent.message is a multi-line source excerpt + stack; condense to the
|
|
153
|
+
* `error: …` line (or a flattened whole) so crash notes stay one line. */
|
|
154
|
+
function crashReason(raw: string): string {
|
|
155
|
+
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
156
|
+
const flat = (lines.find((l) => /^\w*[Ee]rror:/.test(l)) ?? lines.join(" ")).replace(/\s+/g, " ");
|
|
157
|
+
return flat === "" ? "unknown worker error" : flat.length > 200 ? flat.slice(0, 200) + "…" : flat;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function urlForBootstrap(): string {
|
|
161
|
+
if (bootstrapUrl === null) {
|
|
162
|
+
bootstrapUrl = URL.createObjectURL(new Blob([BOOTSTRAP], { type: "application/javascript" }));
|
|
163
|
+
}
|
|
164
|
+
return bootstrapUrl;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function spawnCell(sessionId: string): CellRuntime {
|
|
168
|
+
const worker = new Worker(urlForBootstrap());
|
|
169
|
+
const pending = new Map<number, (r: WorkerResult) => void>();
|
|
170
|
+
let readyResolve: () => void = () => {};
|
|
171
|
+
const ready = new Promise<void>((res) => { readyResolve = res; });
|
|
172
|
+
const runtime: CellRuntime = { worker, pending, ready };
|
|
173
|
+
worker.addEventListener("message", (ev: MessageEvent) => {
|
|
174
|
+
const d = ev.data as { type?: string } & Partial<WorkerResult>;
|
|
175
|
+
if (d.type === "ready") { readyResolve(); return; }
|
|
176
|
+
if (typeof d.id !== "number") return;
|
|
177
|
+
const cb = pending.get(d.id);
|
|
178
|
+
if (cb) {
|
|
179
|
+
pending.delete(d.id);
|
|
180
|
+
cb({ id: d.id, ok: d.ok === true, stdout: d.stdout ?? "", value: d.value ?? "", error: d.error ?? "" });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
worker.addEventListener("error", (ev) => {
|
|
184
|
+
const msg = crashReason((ev as ErrorEvent).message || "unknown worker error");
|
|
185
|
+
// Park the reason BEFORE destroying: a background crash (no call in flight)
|
|
186
|
+
// otherwise leaves the next call a silently-fresh cell, contradicting the
|
|
187
|
+
// "state survives later calls" contract. (preventDefault() does NOT stop Bun
|
|
188
|
+
// from tearing the worker down — verified — so report-on-next-call it is.)
|
|
189
|
+
crashed.set(sessionId, msg);
|
|
190
|
+
destroyCell(sessionId, runtime, `worker crashed: ${msg}`);
|
|
191
|
+
});
|
|
192
|
+
// Never hold the host process open on account of an idle cell.
|
|
193
|
+
(worker as unknown as { unref?: () => void }).unref?.();
|
|
194
|
+
return runtime;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Terminate a runtime and fail its in-flight runs. Guarded against stale handles:
|
|
198
|
+
* only unmaps when this runtime is still the session's current one. */
|
|
199
|
+
function destroyCell(sessionId: string, runtime: CellRuntime, reason: string): void {
|
|
200
|
+
if (cells.get(sessionId) === runtime) cells.delete(sessionId);
|
|
201
|
+
for (const [id, cb] of [...runtime.pending]) {
|
|
202
|
+
runtime.pending.delete(id);
|
|
203
|
+
cb({ id, ok: false, stdout: "", value: "", error: reason });
|
|
204
|
+
}
|
|
205
|
+
try { runtime.worker.terminate(); } catch { /* already dead */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Kill every session's cell worker (test teardown / host shutdown). */
|
|
209
|
+
export async function disposeEvalCells(): Promise<void> {
|
|
210
|
+
for (const [sid, rt] of [...cells]) destroyCell(sid, rt, "eval cells disposed");
|
|
211
|
+
crashed.clear();
|
|
212
|
+
if (bootstrapUrl !== null) { URL.revokeObjectURL(bootstrapUrl); bootstrapUrl = null; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Byte-budget truncation (OMP output window, docs/tools/eval.md:185). UTF-8 safe:
|
|
216
|
+
* a torn trailing multibyte sequence is dropped, and a marker reports kept/total. */
|
|
217
|
+
export function truncateToBudget(text: string, budgetBytes: number): string {
|
|
218
|
+
const enc = new TextEncoder();
|
|
219
|
+
const bytes = enc.encode(text);
|
|
220
|
+
if (bytes.length <= budgetBytes) return text;
|
|
221
|
+
const kept = new TextDecoder("utf-8", { fatal: false })
|
|
222
|
+
.decode(bytes.slice(0, budgetBytes))
|
|
223
|
+
.replace(/�+$/, "");
|
|
224
|
+
return `${kept}\n[output truncated: sent ${enc.encode(kept).length} of ${bytes.length} bytes]`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function clampInt(v: unknown, min: number, max: number, dflt: number): number {
|
|
228
|
+
const n = typeof v === "number" && Number.isFinite(v) ? Math.floor(v) : dflt;
|
|
229
|
+
return Math.min(max, Math.max(min, n));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function runCell(
|
|
233
|
+
sessionId: string, code: string, timeoutMs: number, budgetBytes: number,
|
|
234
|
+
signal: AbortSignal, reset: boolean,
|
|
235
|
+
): Promise<ToolOutput> {
|
|
236
|
+
let existing = cells.get(sessionId);
|
|
237
|
+
if (reset && existing) { destroyCell(sessionId, existing, "cell reset"); existing = undefined; }
|
|
238
|
+
// A spawn that replaces a crashed worker must SAY so: consume the parked reason
|
|
239
|
+
// exactly once and prefix this call's output (outside the byte budget, like the
|
|
240
|
+
// truncation marker — host metadata is never silently truncated away).
|
|
241
|
+
let note = "";
|
|
242
|
+
if (!existing) {
|
|
243
|
+
const reason = crashed.get(sessionId);
|
|
244
|
+
if (reason !== undefined) {
|
|
245
|
+
crashed.delete(sessionId);
|
|
246
|
+
note = `note: previous cell worker crashed (${reason}); state was reset\n`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const cell = existing ?? spawnCell(sessionId);
|
|
250
|
+
if (!existing) cells.set(sessionId, cell);
|
|
251
|
+
|
|
252
|
+
const id = ++callSeq;
|
|
253
|
+
const result = new Promise<WorkerResult>((res) => { cell.pending.set(id, res); });
|
|
254
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
255
|
+
const timedOut = new Promise<"timeout">((res) => { timer = setTimeout(() => res("timeout"), timeoutMs); });
|
|
256
|
+
let onAbort: (() => void) | undefined;
|
|
257
|
+
const aborted = new Promise<"aborted">((res) => {
|
|
258
|
+
if (signal.aborted) { res("aborted"); return; }
|
|
259
|
+
onAbort = () => res("aborted");
|
|
260
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const up = await Promise.race([cell.ready.then(() => "ready" as const), timedOut, aborted]);
|
|
265
|
+
if (up === "ready") cell.worker.postMessage({ id, code });
|
|
266
|
+
const winner = up === "ready" ? await Promise.race([result, timedOut, aborted]) : up;
|
|
267
|
+
if (winner === "timeout" || winner === "aborted") {
|
|
268
|
+
// Destructive cancellation (OMP docs/tools/eval.md:179): a busy synchronous cell
|
|
269
|
+
// can only be stopped by killing its worker; retained state dies with it.
|
|
270
|
+
destroyCell(sessionId, cell, winner);
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
output: note + (winner === "timeout"
|
|
274
|
+
? `Error: eval cell timed out after ${timeoutMs}ms — worker killed; this session's cell state was reset`
|
|
275
|
+
: "Error: eval cell aborted — worker killed; this session's cell state was reset"),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const parts: string[] = [];
|
|
279
|
+
if (winner.stdout !== "") parts.push(winner.stdout.replace(/\n$/, ""));
|
|
280
|
+
if (winner.ok && winner.value !== "") parts.push(`=> ${winner.value}`);
|
|
281
|
+
if (!winner.ok) parts.push(`Error: ${winner.error}`);
|
|
282
|
+
const text = parts.length > 0 ? parts.join("\n") : "(no output)";
|
|
283
|
+
return { ok: winner.ok, output: note + truncateToBudget(text, budgetBytes) };
|
|
284
|
+
} finally {
|
|
285
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
286
|
+
if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
|
|
287
|
+
cell.pending.delete(id);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const evalCellTool: Tool = {
|
|
292
|
+
schema: {
|
|
293
|
+
name: "eval_cell",
|
|
294
|
+
description:
|
|
295
|
+
"Execute JavaScript/TypeScript in this session's persistent eval cell (a Bun worker). " +
|
|
296
|
+
"State survives across calls: `var`, function declarations, and `globalThis.*` assignments " +
|
|
297
|
+
"persist; top-level `let`/`const` are cell-local. Console output is captured and the final " +
|
|
298
|
+
"expression's value is returned as `=> value`. Cells using top-level `await` or bare `return` " +
|
|
299
|
+
"run wrapped in an async function — persist state via `globalThis` there. No rovecode tool access " +
|
|
300
|
+
"from inside the cell (v1 scope). Output is truncated to a byte budget. On timeout — or if a " +
|
|
301
|
+
"background error crashes the worker between calls — cell state resets, and the next call " +
|
|
302
|
+
"says so in a `note:` prefix. NOT a sandbox: gated by the same execute policy as bash.",
|
|
303
|
+
args: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
code: { type: "string", description: "JS/TS source of one cell" },
|
|
307
|
+
timeout_ms: { type: "integer", description: `kill budget in ms (default ${DEFAULT_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS})` },
|
|
308
|
+
max_output_bytes: { type: "integer", description: `output byte budget (default ${DEFAULT_OUTPUT_BYTES}, max ${MAX_OUTPUT_BYTES})` },
|
|
309
|
+
reset: { type: "boolean", description: "discard this session's cell state before running (OMP reset semantics)" },
|
|
310
|
+
},
|
|
311
|
+
required: ["code"],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
kind: "execute", // → action "shell.exec": deny-default rules prompt for it like bash
|
|
315
|
+
sequential: true,
|
|
316
|
+
async execute(args: unknown, ctx: ToolContext): Promise<ToolOutput> {
|
|
317
|
+
const a = (args ?? {}) as { code?: unknown; timeout_ms?: unknown; max_output_bytes?: unknown; reset?: unknown };
|
|
318
|
+
if (typeof a.code !== "string" || a.code.trim() === "") {
|
|
319
|
+
return { ok: false, output: "Error: eval_cell requires a non-empty string `code` argument" };
|
|
320
|
+
}
|
|
321
|
+
const timeoutMs = clampInt(a.timeout_ms, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
|
|
322
|
+
const budget = clampInt(a.max_output_bytes, MIN_OUTPUT_BYTES, MAX_OUTPUT_BYTES, DEFAULT_OUTPUT_BYTES);
|
|
323
|
+
try {
|
|
324
|
+
return await runCell(ctx.sessionId, a.code, timeoutMs, budget, ctx.signal, a.reset === true);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
// Never throw across the tool seam (ADR-005): host failures become typed output.
|
|
327
|
+
return { ok: false, output: `Error: eval cell host failure: ${e instanceof Error ? e.message : String(e)}` };
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/** Flag-gated factory: the ONLY door to the tool. Returns null unless ROVECODE_EVAL_CELL=1,
|
|
333
|
+
* so flag-off runs register nothing (bar: OFF by default and unregistered when off). */
|
|
334
|
+
export function createEvalCellTool(
|
|
335
|
+
env: Record<string, string | undefined> = process.env,
|
|
336
|
+
): Tool | null {
|
|
337
|
+
return env[EVAL_CELL_FLAG] === "1" ? evalCellTool : null;
|
|
338
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** Compact HTML → readable text (PORT #31). Stands in for the html-to-text
|
|
2
|
+
* dependency gemini-cli uses (Apache-2.0, 0bd1d43, packages/core/src/tools/
|
|
3
|
+
* web-fetch.ts:704-709: links keep their href, images are skipped) and for
|
|
4
|
+
* opencode's htmlparser2 skip-depth extractor (MIT, ebece6e, packages/opencode/
|
|
5
|
+
* src/tool/webfetch.ts:158-180: script/style/noscript/iframe/object subtrees
|
|
6
|
+
* dropped; <embed> is void, so unlike opencode it is not depth-tracked). One
|
|
7
|
+
* regex tokenizer pass: raw-text elements are jumped over wholesale, dropped
|
|
8
|
+
* subtrees are depth-tracked, block elements become line breaks, list items
|
|
9
|
+
* get "- ", links render as `text (href)` resolved against the page URL,
|
|
10
|
+
* entities are decoded, whitespace collapses except inside <pre>. Not a spec
|
|
11
|
+
* parser (no tree, no adoption agency) but the tokenizer follows the HTML5
|
|
12
|
+
* tag and attribute states, so quoted attribute values may contain "<" and
|
|
13
|
+
* ">". Every scan is bounded (see tagRx) so a hostile body parses in O(n):
|
|
14
|
+
* the tool's timeout cannot interrupt a synchronous parse, so the parse must
|
|
15
|
+
* never be the slow part. */
|
|
16
|
+
|
|
17
|
+
/** Elements whose CONTENT is raw text up to the matching close tag. <title> is
|
|
18
|
+
* RCDATA and never rendered, so a page that omits <head> still loses its title. */
|
|
19
|
+
const RAW = new Set(["script", "style", "noscript", "template", "title"]);
|
|
20
|
+
/** Elements dropped with their whole (markup) subtree. <head> also ends at the
|
|
21
|
+
* first start tag outside HEAD_ONLY: an omitted </head> — even an omitted
|
|
22
|
+
* <body> — is legal HTML, common in minified pages. */
|
|
23
|
+
const DROP = new Set(["head", "svg", "iframe", "object", "canvas", "audio", "video", "select"]);
|
|
24
|
+
/** Start tags the HTML5 "in head" insertion mode keeps inside <head>; any other
|
|
25
|
+
* start tag implies </head> (and <body>). RAW members are jumped before the
|
|
26
|
+
* check ever runs — listed so the set reads as the spec's. */
|
|
27
|
+
const HEAD_ONLY = new Set(["head", "meta", "link", "base", "basefont", "bgsound", "noframes", "title", "style", "script", "noscript", "template"]);
|
|
28
|
+
/** Elements that break lines; PARA members separate with a blank line. */
|
|
29
|
+
const BLOCK = new Set([
|
|
30
|
+
"address", "article", "aside", "blockquote", "body", "caption", "center", "dd", "details", "dialog", "div", "dl", "dt",
|
|
31
|
+
"fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "html",
|
|
32
|
+
"legend", "main", "menu", "nav", "ol", "p", "pre", "section", "summary", "table", "tbody", "tfoot", "thead", "tr", "ul",
|
|
33
|
+
]);
|
|
34
|
+
const PARA = new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "ul", "ol", "table", "figure"]);
|
|
35
|
+
|
|
36
|
+
const ENTITIES: Record<string, string> = {
|
|
37
|
+
amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", shy: "", copy: "©", reg: "®", trade: "™",
|
|
38
|
+
hellip: "…", mdash: "—", ndash: "–", lsquo: "‘", rsquo: "’", ldquo: "“", rdquo: "”", laquo: "«", raquo: "»",
|
|
39
|
+
bull: "•", middot: "·", deg: "°", times: "×", divide: "÷", plusmn: "±", euro: "€", pound: "£", yen: "¥", cent: "¢",
|
|
40
|
+
sect: "§", para: "¶", larr: "←", rarr: "→", uarr: "↑", darr: "↓", harr: "↔", frac12: "½", frac14: "¼", frac34: "¾",
|
|
41
|
+
sup2: "²", sup3: "³", micro: "µ", iexcl: "¡", iquest: "¿",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Named (common subset), decimal and hex character references; unknown ones are kept verbatim. */
|
|
45
|
+
export function decodeEntities(s: string): string {
|
|
46
|
+
return s.replace(/&(#[xX][0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]*);/g, (m, body: string) => {
|
|
47
|
+
if (body[0] === "#") {
|
|
48
|
+
const cp = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
49
|
+
return cp > 0 && cp <= 0x10ffff && (cp < 0xd800 || cp > 0xdfff) ? String.fromCodePoint(cp) : m;
|
|
50
|
+
}
|
|
51
|
+
return ENTITIES[body] ?? m;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function attr(attrs: string, key: string): string | undefined {
|
|
56
|
+
const m = new RegExp(`(?:^|\\s)${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "i").exec(attrs);
|
|
57
|
+
return m ? decodeEntities(m[1] ?? m[2] ?? m[3] ?? "").trim() : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveHref(href: string, baseUrl: string | undefined): string {
|
|
61
|
+
try { return new URL(href, baseUrl).href; } catch { return href; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Readable text for an HTML document. `baseUrl` resolves relative link hrefs. */
|
|
65
|
+
export function htmlToText(html: string, baseUrl?: string): string {
|
|
66
|
+
const out: string[] = [];
|
|
67
|
+
let skip = 0; // depth inside DROP subtrees
|
|
68
|
+
let pre = 0; // depth inside <pre>
|
|
69
|
+
let inHead = false; // between <head> and </head>, explicit or implied
|
|
70
|
+
let link: { href: string; start: number } | null = null;
|
|
71
|
+
// Linear-time tokenizer (MED-3): every alternative is deterministic, so each
|
|
72
|
+
// character is examined once. Comment/CDATA run lazily to their close or EOF
|
|
73
|
+
// (unterminated swallows the rest, as browsers do). A tag, once opened, never
|
|
74
|
+
// backs out: the name runs to the next blank, "/" or ">" (HTML5 tag-name state —
|
|
75
|
+
// `<a<a<a…` is ONE element, as in a browser), the attribute part must START with
|
|
76
|
+
// a blank or "/" so the name and attribute quantifiers can never trade characters
|
|
77
|
+
// (that nesting made `<aaaa…` quadratic), and inside it a quoted value consumes
|
|
78
|
+
// "<" and ">" while an unterminated quote runs to EOF ("eof-in-tag"), so raw-text
|
|
79
|
+
// jumps and subtree drops engage on legal markup like <script data-x="<">.
|
|
80
|
+
const tagRx = /<!--[\s\S]*?(?:-->|$)|<!\[CDATA\[[\s\S]*?(?:\]\]>|$)|<[!?][^<>]*>|<\/?([a-zA-Z][^\s/>]*)((?:[\s/](?:[^>"']|"[^"]*(?:"|$)|'[^']*(?:'|$))*)?)(?:>|$)/g;
|
|
81
|
+
let pos = 0;
|
|
82
|
+
|
|
83
|
+
const text = (raw: string): void => {
|
|
84
|
+
if (skip > 0 || raw === "") return;
|
|
85
|
+
let s = decodeEntities(raw);
|
|
86
|
+
if (pre === 0) {
|
|
87
|
+
s = s.replace(/\s+/g, " ");
|
|
88
|
+
const last = out.length > 0 ? out[out.length - 1]! : "\n";
|
|
89
|
+
if (last.endsWith("\n") || last.endsWith("- ")) s = s.trimStart();
|
|
90
|
+
}
|
|
91
|
+
if (s) out.push(s);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let m: RegExpExecArray | null;
|
|
95
|
+
while ((m = tagRx.exec(html)) !== null) {
|
|
96
|
+
text(html.slice(pos, m.index));
|
|
97
|
+
pos = tagRx.lastIndex;
|
|
98
|
+
const name = m[1]?.toLowerCase();
|
|
99
|
+
if (!name) continue; // comment, doctype, CDATA, processing instruction
|
|
100
|
+
const closing = m[0].startsWith("</");
|
|
101
|
+
if (RAW.has(name)) {
|
|
102
|
+
if (closing) continue;
|
|
103
|
+
const end = html.slice(pos).search(new RegExp(`</${name}\\s*>`, "i"));
|
|
104
|
+
pos = end < 0 ? html.length : pos + end; // the close tag itself is tokenized next and ignored above
|
|
105
|
+
tagRx.lastIndex = pos;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Browser "in head" insertion mode (MED-4): the first start tag that is not a
|
|
109
|
+
// head-only element — <body>, but also a bare <p> — closes an unclosed <head>.
|
|
110
|
+
if (inHead && !closing && !HEAD_ONLY.has(name)) { inHead = false; skip = 0; }
|
|
111
|
+
if (name === "head") inHead = !closing;
|
|
112
|
+
if (DROP.has(name)) { skip = closing ? Math.max(0, skip - 1) : skip + 1; continue; }
|
|
113
|
+
if (skip > 0) continue;
|
|
114
|
+
if (name === "a") {
|
|
115
|
+
if (!closing) {
|
|
116
|
+
// same-page fragments are judged on the RAW href: resolved against a base they never start with "#"
|
|
117
|
+
const href = attr(m[2] ?? "", "href") ?? "";
|
|
118
|
+
link = { href: href === "" || href.startsWith("#") ? "" : resolveHref(href, baseUrl), start: out.length };
|
|
119
|
+
} else if (link) {
|
|
120
|
+
const label = out.slice(link.start).join("").trim();
|
|
121
|
+
const h = link.href;
|
|
122
|
+
if (h && label && label !== h && !/^(javascript|data|mailto|tel):/i.test(h)) out.push(` (${h})`);
|
|
123
|
+
link = null;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (name === "br") { out.push("\n"); continue; }
|
|
128
|
+
if (name === "li") { if (!closing) out.push("\n- "); continue; }
|
|
129
|
+
const last = out.length > 0 ? out[out.length - 1]! : "\n";
|
|
130
|
+
if (name === "td" || name === "th") { if (!closing && !last.endsWith("\n")) out.push(" "); continue; }
|
|
131
|
+
if (name === "pre") pre = Math.max(0, pre + (closing ? -1 : 1));
|
|
132
|
+
if (BLOCK.has(name)) { const sep = PARA.has(name) ? "\n\n" : "\n"; if (!last.endsWith(sep)) out.push(sep); }
|
|
133
|
+
}
|
|
134
|
+
text(html.slice(pos));
|
|
135
|
+
// Trailing blanks before a newline: the lookbehind anchors each run at its
|
|
136
|
+
// first blank so a <pre> full of spaces is scanned once (bare `[ \t]+\n`
|
|
137
|
+
// re-scanned the run from every blank — quadratic, minutes on 512KB).
|
|
138
|
+
return out.join("").replace(/(?<=^|[^ \t])[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
139
|
+
}
|