rovecode 0.3.3 → 0.4.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +512 -0
- package/README.md +33 -48
- package/bin/rovecode.js +24 -0
- package/dist/cli/app-dybnr56b.js +2 -0
- package/dist/cli/ask-user-p8hq4xgj.js +2 -0
- package/dist/cli/auth-login-ewpgw5sm.js +2 -0
- package/dist/cli/auth-m8p9grty.js +2 -0
- package/dist/cli/bench-xv3ypwev.js +9 -0
- package/dist/cli/catalog-737wb2s0.js +2 -0
- package/dist/cli/cli-arhg40m0.js +2 -0
- package/dist/cli/client-cf2pxx8q.js +2 -0
- package/dist/cli/commands-3p7e4xxs.js +2 -0
- package/dist/cli/connect-3q93d7cb.js +2 -0
- package/dist/cli/context-cmd-eqxmxhzq.js +2 -0
- package/dist/cli/context-report-hbw9zfes.js +2 -0
- package/dist/cli/count-remote-mby98cd0.js +2 -0
- package/dist/cli/design-122y0axd.js +2 -0
- package/dist/cli/dispatch-b4egzvvh.js +2 -0
- package/dist/cli/doctor-x4jkv72e.js +3 -0
- package/dist/cli/executor-ftvg6tsy.js +2 -0
- package/dist/cli/export-pdgdhkch.js +2 -0
- package/dist/cli/files-cez9a96p.js +2 -0
- package/dist/cli/gauntlet-r3xxaszc.js +2 -0
- package/dist/cli/gauntlet-runner-r515m7kk.js +10 -0
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +5 -0
- package/dist/cli/gauntlet-wave4-acs9s60q.js +14 -0
- package/dist/cli/hashline-ewg5hbe3.js +2 -0
- package/dist/cli/http-n0kehsk8.js +5 -0
- package/dist/cli/index-z5qt1s76.js +2 -0
- package/dist/cli/install-80mp63kx.js +2 -0
- package/dist/cli/loop-12twjcat.js +2 -0
- package/dist/cli/main-01pv9206.js +4 -0
- package/dist/cli/main-0jys2ccn.js +3 -0
- package/dist/cli/main-1ztz6fkj.js +10 -0
- package/dist/cli/main-23q7cmww.js +9 -0
- package/dist/cli/main-2rzbexn2.js +3 -0
- package/dist/cli/main-2wyax8k9.js +9 -0
- package/dist/cli/main-2yeveeve.js +6 -0
- package/dist/cli/main-2z3dek0b.js +3 -0
- package/dist/cli/main-2zgsknth.js +3 -0
- package/dist/cli/main-45ejth3a.js +4 -0
- package/dist/cli/main-45rn3trk.js +22 -0
- package/dist/cli/main-4p4e2w7x.js +4 -0
- package/dist/cli/main-4y0tnfpa.js +16 -0
- package/dist/cli/main-5py0rkmc.js +4 -0
- package/dist/cli/main-6dtqmbt6.js +7 -0
- package/dist/cli/main-6h9x282m.js +4 -0
- package/dist/cli/main-6vjeds42.js +3 -0
- package/dist/cli/main-78gq4bt9.js +6 -0
- package/dist/cli/main-7jd5vh3x.js +4 -0
- package/dist/cli/main-7kt6r53y.js +4 -0
- package/dist/cli/main-8c1tbazx.js +58 -0
- package/dist/cli/main-9a9rnh47.js +19 -0
- package/dist/cli/main-9ht36z12.js +3 -0
- package/dist/cli/main-a2yfvcy9.js +7 -0
- package/dist/cli/main-a3f51n0x.js +5 -0
- package/dist/cli/main-b8zq261k.js +3 -0
- package/dist/cli/main-bxtvnf6d.js +13 -0
- package/dist/cli/main-edxc3yzt.js +4 -0
- package/dist/cli/main-evgz4mp5.js +21 -0
- package/dist/cli/main-f33fc5je.js +9 -0
- package/dist/cli/main-fvnpq46y.js +12 -0
- package/dist/cli/main-gbbty4d4.js +3 -0
- package/dist/cli/main-gth53dnt.js +25 -0
- package/dist/cli/main-hqbz10aw.js +9 -0
- package/dist/cli/main-hrrvcfan.js +38 -0
- package/dist/cli/main-hzwtsb2m.js +5 -0
- package/dist/cli/main-j7ttv0sd.js +34 -0
- package/dist/cli/main-jak598k9.js +5 -0
- package/dist/cli/main-kba6zeyd.js +6 -0
- package/dist/cli/main-kwwsz6rq.js +3 -0
- package/dist/cli/main-m8vm17zq.js +3 -0
- package/dist/cli/main-mg4f96e1.js +3 -0
- package/dist/cli/main-mg9b20ac.js +18 -0
- package/dist/cli/main-mgb9ccnx.js +3 -0
- package/dist/cli/main-mjt2p7aj.js +3 -0
- package/dist/cli/main-n6qrdbmy.js +3 -0
- package/dist/cli/main-na7wse0x.js +5 -0
- package/dist/cli/main-nqveez48.js +4 -0
- package/dist/cli/main-ntqef02r.js +10 -0
- package/dist/cli/main-nvc3yjay.js +136 -0
- package/dist/cli/main-p0cfn6nr.js +16 -0
- package/dist/cli/main-qj2djy17.js +19 -0
- package/dist/cli/main-qsevpgsv.js +3 -0
- package/dist/cli/main-qvarybsp.js +3 -0
- package/dist/cli/main-rebtt91r.js +5 -0
- package/dist/cli/main-rpg7h8mb.js +3 -0
- package/dist/cli/main-rsy72qmw.js +15 -0
- package/dist/cli/main-rvetps99.js +18 -0
- package/dist/cli/main-s4bb0jav.js +3 -0
- package/dist/cli/main-s9v8k74e.js +3 -0
- package/dist/cli/main-tjvwmscs.js +3 -0
- package/dist/cli/main-tkgarpjj.js +4 -0
- package/dist/cli/main-v8y60bb2.js +3 -0
- package/dist/cli/main-vhrrq337.js +3 -0
- package/dist/cli/main-vp2dfb7s.js +4 -0
- package/dist/cli/main-vqbr22sz.js +8 -0
- package/dist/cli/main-vxnwe5xx.js +18 -0
- package/dist/cli/main-wgph00xf.js +5 -0
- package/dist/cli/main-wk2csfnj.js +5 -0
- package/dist/cli/main-wm997zjx.js +3 -0
- package/dist/cli/main-wpkyraxh.js +3 -0
- package/dist/cli/main-wqt32p5x.js +4 -0
- package/dist/cli/main-x9ct6y1a.js +3 -0
- package/dist/cli/main-xfekqh9m.js +7 -0
- package/dist/cli/main-xt9zc3n6.js +7 -0
- package/dist/cli/main-xx2z3zh5.js +4 -0
- package/dist/cli/main-y5c82rxr.js +3 -0
- package/dist/cli/main-yrjt2sqt.js +14 -0
- package/dist/cli/main-ys6zj3yr.js +3 -0
- package/dist/cli/main-ywbxshqc.js +8 -0
- package/dist/cli/main-z13755t8.js +25 -0
- package/dist/cli/main-zc7pyrbj.js +4 -0
- package/dist/cli/main.js +279 -0
- package/dist/cli/market-cmd-bm5xvn9f.js +5 -0
- package/dist/cli/mcp-login-bthtfpt7.js +2 -0
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +2 -0
- package/dist/cli/notify-54v5z9dz.js +2 -0
- package/dist/cli/oauth-g5gme95c.js +2 -0
- package/dist/cli/output-satndjap.js +16 -0
- package/dist/cli/profiles-sfhpbq3m.js +2 -0
- package/dist/cli/provider-config-hv3xtdt4.js +2 -0
- package/dist/cli/provider-kwzq6g84.js +2 -0
- package/dist/cli/registry-fh0hdnyn.js +2 -0
- package/dist/cli/registry-y1y8e94r.js +2 -0
- package/dist/cli/repl-t4z03mqq.js +11 -0
- package/dist/cli/resume-fqt4chg8.js +2 -0
- package/dist/cli/run-flags-rysbag9t.js +2 -0
- package/dist/cli/runtime-j19fjbsa.js +2 -0
- package/dist/cli/sandbox-config-g4qxd7y5.js +2 -0
- package/dist/cli/server-r0b6bksk.js +5 -0
- package/dist/cli/session-arg-txmn5g4x.js +2 -0
- package/dist/cli/session-ed250d9j.js +2 -0
- package/dist/cli/sessions-cmd-adw7svfn.js +7 -0
- package/dist/cli/settings-y9rzcqx8.js +2 -0
- package/dist/cli/setup-jmbr11j0.js +2 -0
- package/dist/cli/sextant-smoke-tcth0vea.js +5 -0
- package/dist/cli/skills-cmd-zbdy99v6.js +2 -0
- package/dist/cli/smoke-1bg937kx.js +8 -0
- package/dist/cli/start-chat-p01cdks3.js +12 -0
- package/dist/cli/stream-4wmyaypz.js +2 -0
- package/dist/cli/task-eg4s093s.js +2 -0
- package/dist/cli/tasks-12v9rr9k.js +2 -0
- package/dist/cli/thinking-a5ngvqyh.js +2 -0
- package/dist/cli/todo-1wxpcecx.js +2 -0
- package/dist/cli/tools-2ftsya7w.js +2 -0
- package/dist/cli/tools-x1tj4fxm.js +2 -0
- package/dist/cli/trust-cmd-hccxehzb.js +2 -0
- package/dist/cli/update-check-ygt3vd7m.js +2 -0
- package/dist/cli/update-cmd-v23qhr8c.js +2 -0
- package/dist/cli/voice-g1gtck92.js +2 -0
- package/dist/cli/webfetch-0nnrjgb5.js +2 -0
- package/dist/cli/websearch-f0vr2p7d.js +2 -0
- package/dist/cli/workspace-9rq1w4ta.js +2 -0
- package/dist/lib/index.js +62 -0
- package/dist/lib/models-index.json +1 -0
- package/dist/lib/plugins.js +6 -0
- package/dist/lib/providers.js +17 -0
- package/dist/lib/public-api.js +20 -0
- package/dist/rovecode.exe +4 -0
- package/package.json +83 -72
- package/bin/rovecode.ts +0 -21
- package/src/acp/server.ts +0 -374
- package/src/cli/auth-login.ts +0 -122
- package/src/cli/connect.ts +0 -244
- package/src/cli/context-cmd.ts +0 -199
- package/src/cli/dispatch.ts +0 -82
- package/src/cli/doctor.ts +0 -362
- package/src/cli/export.ts +0 -276
- package/src/cli/help.ts +0 -293
- package/src/cli/is-tui-invocation.ts +0 -8
- package/src/cli/main.ts +0 -583
- package/src/cli/market-cmd.ts +0 -658
- package/src/cli/mcp-login.ts +0 -141
- package/src/cli/mcp-market-cmd.ts +0 -302
- package/src/cli/output.ts +0 -382
- package/src/cli/repl.ts +0 -250
- package/src/cli/repomap-root.ts +0 -14
- package/src/cli/resume.ts +0 -57
- package/src/cli/run-flags.ts +0 -43
- package/src/cli/run-limits.ts +0 -78
- package/src/cli/runtime.ts +0 -931
- package/src/cli/session-arg.ts +0 -30
- package/src/cli/sessions-cmd.ts +0 -145
- package/src/cli/setup.ts +0 -153
- package/src/cli/skills-cmd.ts +0 -194
- package/src/cli/start-chat.ts +0 -65
- package/src/cli/trust-cmd.ts +0 -52
- package/src/coding/bash.ts +0 -148
- package/src/coding/checkpoints.ts +0 -327
- package/src/coding/diff.ts +0 -138
- package/src/coding/files.ts +0 -341
- package/src/coding/hashline.ts +0 -274
- package/src/coding/lsp-gate.ts +0 -254
- package/src/coding/lsp-servers.ts +0 -147
- package/src/coding/lsp.ts +0 -283
- package/src/coding/repomap-cache.ts +0 -99
- package/src/coding/repomap-files.ts +0 -192
- package/src/coding/repomap.ts +0 -481
- package/src/core/agents.ts +0 -255
- package/src/core/compaction.ts +0 -259
- package/src/core/config.ts +0 -289
- package/src/core/context-report.ts +0 -228
- package/src/core/context.ts +0 -60
- package/src/core/count-remote.ts +0 -107
- package/src/core/execpolicy-rules.ts +0 -196
- package/src/core/execpolicy.ts +0 -385
- package/src/core/executor.ts +0 -454
- package/src/core/guardrails.ts +0 -400
- package/src/core/hooks.ts +0 -411
- package/src/core/images.ts +0 -230
- package/src/core/intro.ts +0 -266
- package/src/core/loop.ts +0 -567
- package/src/core/modes.ts +0 -372
- package/src/core/orchestrator.ts +0 -245
- package/src/core/proc-group.ts +0 -48
- package/src/core/project-trust.ts +0 -98
- package/src/core/reflection.ts +0 -165
- package/src/core/sandbox-config.ts +0 -186
- package/src/core/session-id.ts +0 -24
- package/src/core/session-images.ts +0 -73
- package/src/core/session-ops.ts +0 -183
- package/src/core/session-text.ts +0 -29
- package/src/core/session.ts +0 -469
- package/src/core/settings.ts +0 -170
- package/src/core/tasks.ts +0 -646
- package/src/core/token-scale.ts +0 -108
- package/src/core/tools.ts +0 -309
- package/src/core/trust.ts +0 -104
- package/src/core/types.ts +0 -330
- package/src/core/update-check.ts +0 -171
- package/src/core/usage.ts +0 -204
- package/src/core/validate.ts +0 -121
- package/src/core/verify-gate.ts +0 -159
- package/src/core/verify.ts +0 -236
- package/src/core/voice.ts +0 -158
- package/src/core/win-job.ts +0 -183
- package/src/core/workspace.ts +0 -184
- package/src/design/audit.ts +0 -797
- package/src/design/direction.ts +0 -190
- package/src/design/rules.ts +0 -157
- package/src/eval/bench.ts +0 -150
- package/src/eval/gauntlet-runner.ts +0 -215
- package/src/eval/gauntlet-support.ts +0 -84
- package/src/eval/gauntlet-wave3.ts +0 -269
- package/src/eval/gauntlet-wave4.ts +0 -217
- package/src/eval/gauntlet.ts +0 -253
- package/src/index.ts +0 -17
- package/src/lanes/agy.ts +0 -95
- package/src/lanes/approval.ts +0 -24
- package/src/lanes/claude.ts +0 -129
- package/src/lanes/codex.ts +0 -127
- package/src/lanes/events.ts +0 -130
- package/src/lanes/job.ts +0 -142
- package/src/lanes/opencode.ts +0 -122
- package/src/lanes/process.ts +0 -184
- package/src/lanes/progress.ts +0 -183
- package/src/lanes/registry.ts +0 -178
- package/src/lanes/runner.ts +0 -124
- package/src/lanes/types.ts +0 -112
- package/src/market/catalogs/mcp-docs.json +0 -111
- package/src/market/catalogs/plugins.json +0 -111
- package/src/market/catalogs/skills.json +0 -478
- package/src/market/clone.ts +0 -72
- package/src/market/context-cost.ts +0 -121
- package/src/market/digest.ts +0 -106
- package/src/market/index.ts +0 -22
- package/src/market/install.ts +0 -578
- package/src/market/manifest.ts +0 -187
- package/src/market/prereq.ts +0 -145
- package/src/market/registry.ts +0 -363
- package/src/market/resolve.ts +0 -111
- package/src/market/types.ts +0 -236
- package/src/market/validate.ts +0 -227
- package/src/mcp/client.ts +0 -449
- package/src/mcp/config.ts +0 -252
- package/src/mcp/local-package.ts +0 -211
- package/src/mcp/market-catalog.ts +0 -84
- package/src/mcp/market-install.ts +0 -289
- package/src/mcp/market.ts +0 -362
- package/src/mcp/oauth.ts +0 -251
- package/src/mcp/prompts-resources.ts +0 -249
- package/src/mcp/shared.ts +0 -149
- package/src/mcp/status.ts +0 -67
- package/src/mcp/tools.ts +0 -275
- package/src/mcp/transport.ts +0 -122
- package/src/mcp/trust.ts +0 -25
- package/src/memory/blocks.ts +0 -278
- package/src/memory/recall.ts +0 -355
- package/src/memory/scope.ts +0 -182
- package/src/memory/store.ts +0 -105
- package/src/memory/tools.ts +0 -99
- package/src/plugins/cli.ts +0 -119
- package/src/plugins/discover.ts +0 -108
- package/src/plugins/index.ts +0 -50
- package/src/plugins/install.ts +0 -184
- package/src/plugins/load.ts +0 -124
- package/src/plugins/manifest.ts +0 -92
- package/src/plugins/state.ts +0 -83
- package/src/providers/auth.ts +0 -408
- package/src/providers/cache.ts +0 -223
- package/src/providers/catalog-local.ts +0 -160
- package/src/providers/catalog.ts +0 -421
- package/src/providers/middleware-context.ts +0 -86
- package/src/providers/middleware.ts +0 -373
- package/src/providers/model-list.ts +0 -23
- package/src/providers/oauth/common.ts +0 -105
- package/src/providers/oauth/device-code.ts +0 -107
- package/src/providers/oauth/github-copilot.ts +0 -146
- package/src/providers/oauth/loopback.ts +0 -158
- package/src/providers/oauth/openai.ts +0 -163
- package/src/providers/oauth/openrouter.ts +0 -89
- package/src/providers/oauth/pkce.ts +0 -45
- package/src/providers/oauth/registry.ts +0 -39
- package/src/providers/oauth/seam.ts +0 -89
- package/src/providers/profile-glm53.ts +0 -111
- package/src/providers/profile-sonnet5-persona.ts +0 -65
- package/src/providers/profile-sonnet5-voice.ts +0 -23
- package/src/providers/profiles.ts +0 -156
- package/src/providers/provider-config.ts +0 -311
- package/src/providers/registry.ts +0 -333
- package/src/providers/responses.ts +0 -209
- package/src/providers/retry.ts +0 -234
- package/src/providers/router.ts +0 -294
- package/src/providers/sse.ts +0 -26
- package/src/providers/stream-errors.ts +0 -117
- package/src/providers/stream.ts +0 -566
- package/src/providers/thinking.ts +0 -189
- package/src/providers/wire-messages.ts +0 -129
- package/src/providers/wire-responses.ts +0 -79
- package/src/providers/wire-select.ts +0 -53
- package/src/server/http.ts +0 -291
- package/src/server/openapi.ts +0 -246
- package/src/sextant/card-hits.ts +0 -102
- package/src/sextant/card-keys.ts +0 -55
- package/src/sextant/context-source.ts +0 -157
- package/src/sextant/crew-cards.ts +0 -350
- package/src/sextant/draw-agents.ts +0 -273
- package/src/sextant/draw-code.ts +0 -388
- package/src/sextant/draw-context.ts +0 -222
- package/src/sextant/draw-frame.ts +0 -164
- package/src/sextant/draw-market.ts +0 -573
- package/src/sextant/draw-messages.ts +0 -386
- package/src/sextant/draw-pet.ts +0 -230
- package/src/sextant/draw-plan.ts +0 -187
- package/src/sextant/draw-tabs.ts +0 -85
- package/src/sextant/draw-util.ts +0 -65
- package/src/sextant/draw-wizard.ts +0 -378
- package/src/sextant/engine.ts +0 -230
- package/src/sextant/frame-hits.ts +0 -25
- package/src/sextant/frame.ts +0 -101
- package/src/sextant/git-status.ts +0 -197
- package/src/sextant/grid.ts +0 -59
- package/src/sextant/input.ts +0 -119
- package/src/sextant/keys.ts +0 -521
- package/src/sextant/layout.ts +0 -86
- package/src/sextant/local-commands.ts +0 -169
- package/src/sextant/market-source.ts +0 -287
- package/src/sextant/mentions.ts +0 -200
- package/src/sextant/message-hits.ts +0 -26
- package/src/sextant/model.ts +0 -387
- package/src/sextant/overlays.ts +0 -456
- package/src/sextant/panel-hits.ts +0 -38
- package/src/sextant/pet.ts +0 -399
- package/src/sextant/screen.ts +0 -324
- package/src/sextant/scroll-hits.ts +0 -66
- package/src/sextant/scrollbar.ts +0 -82
- package/src/sextant/sextant-bridge.ts +0 -174
- package/src/sextant/sextant-cards.ts +0 -142
- package/src/sextant/sextant-diff-base.ts +0 -63
- package/src/sextant/sextant-files.ts +0 -154
- package/src/sextant/sextant-frame-loop.ts +0 -335
- package/src/sextant/sextant-renderer.ts +0 -574
- package/src/sextant/sextant-repo.ts +0 -140
- package/src/sextant/theme.ts +0 -66
- package/src/sextant/tool-rows.ts +0 -189
- package/src/sextant/types.ts +0 -493
- package/src/skills/index.ts +0 -387
- package/src/skills/pack.ts +0 -220
- package/src/skills/spec.ts +0 -162
- package/src/skills/tools.ts +0 -69
- package/src/skills/versioned.ts +0 -227
- package/src/telemetry/otel-export.ts +0 -122
- package/src/telemetry/otel-lanes.ts +0 -89
- package/src/telemetry/otel-logs.ts +0 -131
- package/src/telemetry/otel-metrics.ts +0 -136
- package/src/telemetry/otel.ts +0 -397
- package/src/telemetry/otlp.ts +0 -76
- package/src/tools/ask-user.ts +0 -156
- package/src/tools/bash-bg.ts +0 -94
- package/src/tools/bash-jobs.ts +0 -237
- package/src/tools/design.ts +0 -151
- package/src/tools/evalcell.ts +0 -338
- package/src/tools/html-text.ts +0 -139
- package/src/tools/provider.ts +0 -149
- package/src/tools/task.ts +0 -250
- package/src/tools/todo.ts +0 -320
- package/src/tools/webfetch.ts +0 -332
- package/src/tools/websearch.ts +0 -359
- package/src/tui/agents-cmd.ts +0 -41
- package/src/tui/app.ts +0 -749
- package/src/tui/attach.ts +0 -127
- package/src/tui/boot-notes.ts +0 -41
- package/src/tui/builtin-prompts.ts +0 -59
- package/src/tui/checkpoints-cmd.ts +0 -70
- package/src/tui/clipboard-image.ts +0 -81
- package/src/tui/clipboard.ts +0 -78
- package/src/tui/commands.ts +0 -283
- package/src/tui/config-view.ts +0 -53
- package/src/tui/context-cmds.ts +0 -282
- package/src/tui/cost.ts +0 -108
- package/src/tui/crash-guard.ts +0 -173
- package/src/tui/focus-terminal.ts +0 -34
- package/src/tui/git-cmds.ts +0 -273
- package/src/tui/git-plain.ts +0 -58
- package/src/tui/info-cmd.ts +0 -150
- package/src/tui/input-plain.ts +0 -76
- package/src/tui/mcp-cmd.ts +0 -128
- package/src/tui/memory-note.ts +0 -77
- package/src/tui/modes-cmd.ts +0 -45
- package/src/tui/notify-seq.ts +0 -100
- package/src/tui/notify.ts +0 -318
- package/src/tui/overlays.ts +0 -97
- package/src/tui/pi-renderer.ts +0 -428
- package/src/tui/providers-cmd.ts +0 -377
- package/src/tui/reasoning-view.ts +0 -56
- package/src/tui/renderer.ts +0 -128
- package/src/tui/replay-marker.ts +0 -29
- package/src/tui/session-cmd.ts +0 -148
- package/src/tui/session-manage.ts +0 -95
- package/src/tui/sextant-attach.ts +0 -102
- package/src/tui/sextant-io.ts +0 -202
- package/src/tui/sextant-smoke.ts +0 -110
- package/src/tui/shell-cmd.ts +0 -158
- package/src/tui/smoke.ts +0 -72
- package/src/tui/staged-terminal.ts +0 -50
- package/src/tui/startup.ts +0 -12
- package/src/tui/theme.ts +0 -59
- package/src/tui/todo-label.ts +0 -7
- package/src/tui/trust-card.ts +0 -107
- package/src/tui/tui-commands.ts +0 -87
- package/tsconfig.json +0 -30
- package/vendor/pi-tui/LICENSE +0 -21
- package/vendor/pi-tui/PATCHES.md +0 -12
- package/vendor/pi-tui/PROVENANCE.md +0 -12
- package/vendor/pi-tui/README.upstream.md +0 -854
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +0 -158
- package/vendor/pi-tui/src/autocomplete.ts +0 -827
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
- package/vendor/pi-tui/src/components/box.ts +0 -138
- package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
- package/vendor/pi-tui/src/components/editor.ts +0 -2364
- package/vendor/pi-tui/src/components/h-stack.ts +0 -45
- package/vendor/pi-tui/src/components/image.ts +0 -128
- package/vendor/pi-tui/src/components/input.ts +0 -448
- package/vendor/pi-tui/src/components/loader.ts +0 -93
- package/vendor/pi-tui/src/components/markdown.ts +0 -1016
- package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
- package/vendor/pi-tui/src/components/select-list.ts +0 -230
- package/vendor/pi-tui/src/components/settings-list.ts +0 -277
- package/vendor/pi-tui/src/components/spacer.ts +0 -29
- package/vendor/pi-tui/src/components/stack.ts +0 -155
- package/vendor/pi-tui/src/components/text.ts +0 -108
- package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
- package/vendor/pi-tui/src/components/v-stack.ts +0 -34
- package/vendor/pi-tui/src/editor-component.ts +0 -75
- package/vendor/pi-tui/src/fuzzy.ts +0 -138
- package/vendor/pi-tui/src/index.ts +0 -149
- package/vendor/pi-tui/src/keybindings.ts +0 -321
- package/vendor/pi-tui/src/keys.ts +0 -1402
- package/vendor/pi-tui/src/kill-ring.ts +0 -47
- package/vendor/pi-tui/src/latex.ts +0 -1381
- package/vendor/pi-tui/src/layout-node.ts +0 -52
- package/vendor/pi-tui/src/layout.ts +0 -411
- package/vendor/pi-tui/src/native-modifiers.ts +0 -60
- package/vendor/pi-tui/src/native-module-path.ts +0 -32
- package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
- package/vendor/pi-tui/src/terminal-colors.ts +0 -74
- package/vendor/pi-tui/src/terminal-image.ts +0 -701
- package/vendor/pi-tui/src/terminal.ts +0 -554
- package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
- package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
- package/vendor/pi-tui/src/tui.ts +0 -1264
- package/vendor/pi-tui/src/undo-stack.ts +0 -29
- package/vendor/pi-tui/src/utils.ts +0 -1327
- package/vendor/pi-tui/src/word-navigation.ts +0 -118
- package/vendor/pi-tui/test/test-themes.ts +0 -39
- package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
- /package/{src/providers → dist/cli}/models-index.json +0 -0
package/src/core/hooks.ts
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
/** Hooks v2 (port #29): a small TYPED, file-loadable hook set — ≤10 hooks, versioned (ADR-013:
|
|
2
|
-
* "no 1.8k-line extension API surface — small typed hook set, versioned").
|
|
3
|
-
*
|
|
4
|
-
* Contract (HOOKS_API_VERSION 1): nine hooks, every one optional, sync or async —
|
|
5
|
-
* pre_run(ctx) · post_run(ctx, {status, summary}) · pre_tool(ctx, call) → void | {deny}
|
|
6
|
-
* post_tool(ctx, call, result) → void | {output?} · approval(ctx, req) → void | "allow" | "deny"
|
|
7
|
-
* compaction(ctx, event) · session_open(ctx) · session_close(ctx) · on_event(ctx, ev)
|
|
8
|
-
* ctx = {cwd, sessionId, runId?} and nothing else (no registry/store handles) — the surface stays
|
|
9
|
-
* tiny on purpose; a tenth hook is the budget's last slot, not a target.
|
|
10
|
-
*
|
|
11
|
-
* Loading: `.rovecode/hooks.ts` or `.rovecode/hooks.js` in the project plus `~/.rovecode/hooks.{ts,js}`
|
|
12
|
-
* (ROVECODE_HOME idiom, providers/auth.ts rovecodeHome), each `export default { version: 1, hooks: {…} }`,
|
|
13
|
-
* imported with a plain `await import()` — Bun runs TypeScript natively, so there is no build step
|
|
14
|
-
* and no loader dependency. User scope runs first, project second. Wrong/missing version → skipped
|
|
15
|
-
* with a warning (version gate); a failing import → warning, never a throw. Loaded ONCE per
|
|
16
|
-
* process (Bun's module cache; a `?query` does not bust it on 1.3.14): restart rovecode to pick up
|
|
17
|
-
* edits — the projectContext rule. ROVECODE_NO_HOOKS=1 skips the files (programmatic add() still works).
|
|
18
|
-
*
|
|
19
|
-
* Running: every hook call is timeout-bounded (ROVECODE_HOOK_TIMEOUT_MS, default 5000, on a REF'D
|
|
20
|
-
* timer) and isolated — a throwing or hanging hook records one bounded warning note and the run
|
|
21
|
-
* continues as if the hook had returned void. Results are validated and bounded (deny reason
|
|
22
|
-
* ≤ MAX_DENY_REASON_CHARS; post_tool may grow the tool's output by ≤ MAX_POST_TOOL_GROWTH_CHARS).
|
|
23
|
-
* pre_tool / post_tool ride core/tools.ts dispatch at the existing hook seams; approval rides the
|
|
24
|
-
* ApprovalFn chain as approver() (composed in cli/runtime.ts buildCfg — see Authority); the
|
|
25
|
-
* run-level hooks ride the event stream via observer() in core/loop.ts agentLoop (pre_run /
|
|
26
|
-
* compaction / post_run awaited in order, on_event a fire-and-forget tap — port #39 OTel builds on
|
|
27
|
-
* it); session_open/close are the runtime's lifetime (cli/runtime.ts + the surfaces' close paths).
|
|
28
|
-
*
|
|
29
|
-
* Authority: POLICY WINS. Permission rules (deny-default, last-match) are evaluated BEFORE pre_tool,
|
|
30
|
-
* so a hook never sees — and can never "un-deny" — a rule-rejected call; pre_tool can only deny,
|
|
31
|
-
* and its deny applies in every mode including yolo (a hook is the user's own stricter layer). The
|
|
32
|
-
* approval hook stands in for the HUMAN — literally: approver(human) is an ApprovalFn that buildCfg
|
|
33
|
-
* composes INSIDE execPolicyApprover, so the order is permission rules → execpolicy argv
|
|
34
|
-
* classification (port #9: forbidden → deny before any hook or human; allow-listed → runs, nobody
|
|
35
|
-
* asked) → approval hook → human. Consulted only on a policy "prompt" with nothing cached; "allow"
|
|
36
|
-
* is a one-shot yes (never cached), "deny" denies (the chain's deny shape, as execpolicy's), void →
|
|
37
|
-
* the human, or fail closed when there is none (headless). A hook "allow" is exactly as strong as
|
|
38
|
-
* the human's "once" — never stronger than policy or execpolicy. A failing/timed-out hook cannot
|
|
39
|
-
* deny (fail-open to policy, which already ran).
|
|
40
|
-
*
|
|
41
|
-
* TRUST (gated since 2026-09-07, core/trust.ts): hooks are code executed in-process with the user's
|
|
42
|
-
* privileges — the worst thing a checkout can carry, because there is no spawn to classify and no
|
|
43
|
-
* execpolicy to consult. The HOME file is the user's own and always loads. The PROJECT file loads only
|
|
44
|
-
* when this machine approved its current bytes (`rovecode trust`; the same digest store as project
|
|
45
|
-
* mcp.json and plugins); the gate is asked BEFORE the import, on the whole file, so a refusal is never a
|
|
46
|
-
* partial load — one warning names the file and stays off. .rovecode/commands/*.md are prompt text, not
|
|
47
|
-
* executables: a different class of gate, not this one.
|
|
48
|
-
*
|
|
49
|
-
* Sources (pattern references, no code copied; both MIT — covered by the generic MIT credit in the
|
|
50
|
-
* THIRD_PARTY_NOTICES.md preamble, no per-port entry as MIT sources are credited in module headers):
|
|
51
|
-
* - pi @ 853a80d packages/coding-agent/src/core/extensions — loader.ts:498-510 imports the
|
|
52
|
-
* extension module (jiti there) and reads its default export; runner.ts:851-884 emit() runs
|
|
53
|
-
* handlers in registration order with a per-handler try/catch → emitError (isolation);
|
|
54
|
-
* runner.ts:982-1004 emitToolCall: the first `block` result wins; types.ts:1125-1131
|
|
55
|
-
* ToolCallEventResult {block, reason}; types.ts:1144-1149 ToolResultEventResult replaces the
|
|
56
|
-
* result content; types.ts:1247 a handler may be sync or async.
|
|
57
|
-
* - opencode @ ebece6e packages/opencode/src/plugin — loader.ts:135-145 load() is a plain
|
|
58
|
-
* `await import(entry)` returning {ok:false, error} instead of throwing; loader.ts:203-236 failed
|
|
59
|
-
* entries are dropped and the successful order preserved; index.ts:284-297 trigger() runs each
|
|
60
|
-
* plugin's hook sequentially and later hooks see earlier mutations; index.ts:226-243 a plugin
|
|
61
|
-
* that fails to apply is logged and skipped.
|
|
62
|
-
* Deviations: an explicit `version` gate (upstream file plugins have none — opencode's compatibility
|
|
63
|
-
* check is npm-only, loader.ts:123-131); a per-call timeout (neither upstream bounds a hook);
|
|
64
|
-
* validated + bounded results; nine hooks instead of pi's ~45 events / opencode's open Hooks map. */
|
|
65
|
-
|
|
66
|
-
import { existsSync } from "node:fs";
|
|
67
|
-
import { join, resolve } from "node:path";
|
|
68
|
-
import { pathToFileURL } from "node:url";
|
|
69
|
-
import type { ApprovalFn, ApprovalRequest, RunEvent, ToolOutput } from "./types.ts";
|
|
70
|
-
import { rovecodeHome } from "../providers/auth.ts";
|
|
71
|
-
import { isTrustedFile, untrustedFileNote } from "./trust.ts";
|
|
72
|
-
|
|
73
|
-
export const HOOKS_API_VERSION = 1;
|
|
74
|
-
export const DEFAULT_HOOK_TIMEOUT_MS = 5000;
|
|
75
|
-
export const MAX_DENY_REASON_CHARS = 400;
|
|
76
|
-
export const MAX_POST_TOOL_GROWTH_CHARS = 16_000;
|
|
77
|
-
const MAX_WARNINGS = 50;
|
|
78
|
-
const MAX_WARNING_CHARS = 300;
|
|
79
|
-
|
|
80
|
-
// ---------- contract ----------
|
|
81
|
-
|
|
82
|
-
export interface HookCtx { cwd: string; sessionId: string; runId?: string }
|
|
83
|
-
export interface HookToolCall { id: string; tool: string; args: unknown }
|
|
84
|
-
export interface RunResult { status: "done" | "stopped" | "error" | "budget"; summary: string }
|
|
85
|
-
export type CompactionEvent = Extract<RunEvent, { type: "compaction" }>;
|
|
86
|
-
type Hook<A extends unknown[], R = void> = (ctx: HookCtx, ...args: A) => Promise<R | void> | R | void;
|
|
87
|
-
|
|
88
|
-
/** The whole surface. Adding a member here is an API version bump (HOOKS_API_VERSION). */
|
|
89
|
-
export interface HookSet {
|
|
90
|
-
/** run start, after run_start is produced (ctx.runId set) and before the first model turn */
|
|
91
|
-
pre_run?: Hook<[]>;
|
|
92
|
-
/** run end, awaited BEFORE run_end reaches the consumer (so it lands before a cmdRun exit); a run whose
|
|
93
|
-
* CONSUMER closed the generator first (serve disconnect, ACP cancel, TUI Esc) gets it once as {stopped, "run aborted"} */
|
|
94
|
-
post_run?: Hook<[result: RunResult]>;
|
|
95
|
-
/** before a tool executes, after policy allowed it; {deny} fails the call with this reason */
|
|
96
|
-
pre_tool?: Hook<[call: HookToolCall], { deny: string }>;
|
|
97
|
-
/** after a tool ran; {output} replaces what the model sees (growth-bounded), {} = unchanged */
|
|
98
|
-
post_tool?: Hook<[call: HookToolCall, result: ToolOutput], { output?: string }>;
|
|
99
|
-
/** policy said prompt, execpolicy left it to a human, nothing is cached: pre-answer instead of the
|
|
100
|
-
* human ("allow" one-shot / "deny"), or void to ask them — ctx is the runtime's (no runId) */
|
|
101
|
-
approval?: Hook<[req: ApprovalRequest], "allow" | "deny">;
|
|
102
|
-
/** after a history compaction (the event as yielded: strategy, trigger, token counts) */
|
|
103
|
-
compaction?: Hook<[event: CompactionEvent]>;
|
|
104
|
-
/** once per runtime, after the hook files loaded */
|
|
105
|
-
session_open?: Hook<[]>;
|
|
106
|
-
/** once per runtime, at surface teardown, after in-flight on_event taps settled */
|
|
107
|
-
session_close?: Hook<[]>;
|
|
108
|
-
/** every RunEvent, in order, fire-and-forget (not awaited — keep it cheap; port #39 OTel tap) */
|
|
109
|
-
on_event?: Hook<[ev: RunEvent]>;
|
|
110
|
-
}
|
|
111
|
-
export type HookName = keyof HookSet;
|
|
112
|
-
/** exhaustive by construction (`satisfies Record<HookName, 0>` rejects a missing or extra key) */
|
|
113
|
-
export const HOOK_NAMES = Object.keys({
|
|
114
|
-
pre_run: 0, post_run: 0, pre_tool: 0, post_tool: 0, approval: 0, compaction: 0, session_open: 0, session_close: 0, on_event: 0,
|
|
115
|
-
} satisfies Record<HookName, 0>) as HookName[];
|
|
116
|
-
/** shape of a hooks file's default export */
|
|
117
|
-
export interface HookModule { version: number; hooks: HookSet }
|
|
118
|
-
export type HookArgs<K extends HookName> = Parameters<NonNullable<HookSet[K]>>;
|
|
119
|
-
/** what a hook may decide (its non-void return), validated + bounded by the runner */
|
|
120
|
-
export type HookDecision<K extends HookName> = Exclude<Awaited<ReturnType<NonNullable<HookSet[K]>>>, void>;
|
|
121
|
-
|
|
122
|
-
// ---------- loader ----------
|
|
123
|
-
|
|
124
|
-
export interface LoadedHooks { hooks: HookSet[]; sources: string[]; warnings: string[] }
|
|
125
|
-
|
|
126
|
-
/** Load `<home>/hooks.{ts,js}` (user, first) and `<cwd>/.rovecode/hooks.{ts,js}` (project, second).
|
|
127
|
-
* Never throws: every failure is a warning line naming the file. hooks[i] came from sources[i]. */
|
|
128
|
-
export async function loadHooks(cwd: string, opts: { home?: string; timeoutMs?: number; trusted?: (file: string) => boolean } = {}): Promise<LoadedHooks> {
|
|
129
|
-
const out: LoadedHooks = { hooks: [], sources: [], warnings: [] };
|
|
130
|
-
if (process.env.ROVECODE_NO_HOOKS === "1") return out;
|
|
131
|
-
const home = opts.home ?? rovecodeHome();
|
|
132
|
-
// THE TRUST GATE (core/trust.ts, 2026-09-07): the project file is code from a checkout, imported in-process with the
|
|
133
|
-
// person's privileges — asked BEFORE the import, on the whole file, so a refusal is never a partial load. The home
|
|
134
|
-
// file is the person's own and is never gated.
|
|
135
|
-
const trusted = opts.trusted ?? ((file: string) => isTrustedFile(home, file));
|
|
136
|
-
const seen = new Set<string>();
|
|
137
|
-
for (const [scope, dir] of [["user", home], ["project", join(cwd, ".rovecode")]] as const) {
|
|
138
|
-
const file = pickHookFile(dir, out.warnings);
|
|
139
|
-
if (file === null || seen.has(resolve(file))) continue; // home inside cwd/.rovecode: one load
|
|
140
|
-
seen.add(resolve(file));
|
|
141
|
-
if (scope === "project" && !trusted(file)) {
|
|
142
|
-
out.warnings.push(untrustedFileNote(file, "its hooks stay off (they would run code from this repo in-process, with your privileges)"));
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
const set = await importHookSet(file, opts.timeoutMs ?? hookTimeoutMs(), out.warnings);
|
|
146
|
-
if (set) { out.hooks.push(set); out.sources.push(file); }
|
|
147
|
-
}
|
|
148
|
-
return out;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function pickHookFile(dir: string, warnings: string[]): string | null {
|
|
152
|
-
const ts = join(dir, "hooks.ts"), js = join(dir, "hooks.js");
|
|
153
|
-
const hasTs = existsSync(ts), hasJs = existsSync(js);
|
|
154
|
-
if (hasTs && hasJs) warnings.push(`${js}: ignored — ${ts} takes precedence`);
|
|
155
|
-
return hasTs ? ts : hasJs ? js : null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function importHookSet(file: string, timeoutMs: number, warnings: string[]): Promise<HookSet | null> {
|
|
159
|
-
let mod: unknown;
|
|
160
|
-
try {
|
|
161
|
-
mod = await withTimeout(import(pathToFileURL(file).href), timeoutMs);
|
|
162
|
-
} catch (e) {
|
|
163
|
-
warnings.push(`${file}: failed to load — ${errText(e)}`);
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
if (mod === TIMED_OUT) { warnings.push(`${file}: load timed out after ${timeoutMs}ms (top-level await?) — skipped`); return null; }
|
|
167
|
-
return validateModule(file, isRecord(mod) ? mod["default"] : undefined, warnings);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** default export → HookSet; the version gate lives here. Unknown/non-function members are
|
|
171
|
-
* dropped with a warning, the rest kept (a typo must not silently disable the whole file). */
|
|
172
|
-
function validateModule(file: string, dflt: unknown, warnings: string[]): HookSet | null {
|
|
173
|
-
if (!isRecord(dflt)) { warnings.push(`${file}: default export must be { version: ${HOOKS_API_VERSION}, hooks: {…} } — skipped`); return null; }
|
|
174
|
-
if (dflt["version"] !== HOOKS_API_VERSION) {
|
|
175
|
-
const v = dflt["version"]; // a string "1" is shown quoted, never disguised as the supported number
|
|
176
|
-
const shown = v === undefined ? "missing" : typeof v === "string" ? JSON.stringify(v) : String(v);
|
|
177
|
-
warnings.push(`${file}: hooks API version ${shown} is not supported (this rovecode speaks ${HOOKS_API_VERSION}) — skipped`);
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
if (!isRecord(dflt["hooks"])) { warnings.push(`${file}: "hooks" must be an object of hook functions — skipped`); return null; }
|
|
181
|
-
const set: Record<string, unknown> = {};
|
|
182
|
-
for (const [name, fn] of Object.entries(dflt["hooks"])) {
|
|
183
|
-
if (!(HOOK_NAMES as string[]).includes(name)) { warnings.push(`${file}: unknown hook "${name}" ignored (known: ${HOOK_NAMES.join(", ")})`); continue; }
|
|
184
|
-
if (typeof fn !== "function") { warnings.push(`${file}: hook "${name}" is not a function — ignored`); continue; }
|
|
185
|
-
set[name] = fn;
|
|
186
|
-
}
|
|
187
|
-
return set as HookSet;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** ROVECODE_HOOK_TIMEOUT_MS: blank/invalid/< 1 → default. */
|
|
191
|
-
export function hookTimeoutMs(env: Record<string, string | undefined> = process.env): number {
|
|
192
|
-
const v = Number(env["ROVECODE_HOOK_TIMEOUT_MS"] ?? "");
|
|
193
|
-
return Number.isFinite(v) && v >= 1 ? Math.floor(v) : DEFAULT_HOOK_TIMEOUT_MS;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ---------- runner ----------
|
|
197
|
-
|
|
198
|
-
/** the loop's per-run view; close() = the run ended WITHOUT a run_end (consumer-closed generator) */
|
|
199
|
-
export interface RunObserver { observe(ev: RunEvent): Promise<void>; close(): Promise<void> }
|
|
200
|
-
export interface HookRunnerOptions { timeoutMs?: number; onWarning?: (note: string) => void }
|
|
201
|
-
|
|
202
|
-
/** Holds the hook sets of one runtime and runs them: sets in attach order, per call a ref'd timeout
|
|
203
|
-
* + try/catch isolation, results validated. Decision hooks: the first decisive result wins
|
|
204
|
-
* (pi emitToolCall), except post_tool which chains (each set sees the previous output). */
|
|
205
|
-
export class HookRunner {
|
|
206
|
-
/** bounded log of load/runtime notes (also streamed to onWarning) */
|
|
207
|
-
readonly warnings: string[] = [];
|
|
208
|
-
readonly timeoutMs: number;
|
|
209
|
-
private readonly entries: { source: string; set: HookSet }[] = [];
|
|
210
|
-
private loading: Promise<void> | null = null;
|
|
211
|
-
private opened = false;
|
|
212
|
-
private closing: Promise<void> | null = null;
|
|
213
|
-
private readonly pending = new Set<Promise<void>>();
|
|
214
|
-
private listener: ((note: string) => void) | undefined;
|
|
215
|
-
|
|
216
|
-
constructor(private readonly base: HookCtx, opts: HookRunnerOptions = {}) {
|
|
217
|
-
this.timeoutMs = opts.timeoutMs ?? hookTimeoutMs();
|
|
218
|
-
this.listener = opts.onWarning;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** attach a set programmatically (port #39 OTel) — before the first run; session_open is open()'s */
|
|
222
|
-
add(set: HookSet, source = "programmatic"): void { this.entries.push({ source, set }); }
|
|
223
|
-
get size(): number { return this.entries.length; }
|
|
224
|
-
has(name: HookName): boolean { return this.entries.some((e) => typeof e.set[name] === "function"); }
|
|
225
|
-
/** settles when the hook files are attached and session_open has run; run() waits for it */
|
|
226
|
-
get ready(): Promise<void> { return this.loading ?? Promise.resolve(); }
|
|
227
|
-
|
|
228
|
-
/** load the hook files (background import) then fire session_open ONCE; idempotent */
|
|
229
|
-
open(cwd = this.base.cwd, opts: { home?: string } = {}): Promise<void> {
|
|
230
|
-
if (this.opened) return this.ready;
|
|
231
|
-
this.opened = true;
|
|
232
|
-
const load = loadHooks(cwd, { ...opts, timeoutMs: this.timeoutMs }).then(async (loaded) => {
|
|
233
|
-
loaded.hooks.forEach((set, i) => this.add(set, loaded.sources[i] ?? "file"));
|
|
234
|
-
for (const w of loaded.warnings) this.warn(w);
|
|
235
|
-
await this.dispatch("session_open", [this.base]);
|
|
236
|
-
}).then(() => { this.loading = null; });
|
|
237
|
-
this.loading = load;
|
|
238
|
-
return load;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** run one hook across all sets (waits for open() first, so no caller can race the load) */
|
|
242
|
-
async run<K extends HookName>(name: K, ...args: HookArgs<K>): Promise<HookDecision<K> | undefined> {
|
|
243
|
-
if (this.loading) await this.loading;
|
|
244
|
-
return this.dispatch(name, args);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private async dispatch<K extends HookName>(name: K, args: HookArgs<K>): Promise<HookDecision<K> | undefined> {
|
|
248
|
-
let decision: HookDecision<K> | undefined;
|
|
249
|
-
for (const entry of this.entries) {
|
|
250
|
-
const fn = entry.set[name];
|
|
251
|
-
if (typeof fn !== "function") continue;
|
|
252
|
-
const raw = await this.invoke(entry.source, name, fn as (...a: unknown[]) => unknown, args);
|
|
253
|
-
const d = normalize(name, raw, args, (w) => this.warn(`${entry.source}: ${w}`));
|
|
254
|
-
if (d === undefined) continue;
|
|
255
|
-
if (name !== "post_tool") return d; // first decisive result wins
|
|
256
|
-
decision = d; // post_tool chains: the next set sees this set's output
|
|
257
|
-
const cur = args as unknown as [HookCtx, HookToolCall, ToolOutput];
|
|
258
|
-
const next = (d as { output?: string }).output;
|
|
259
|
-
if (next !== undefined) cur[2] = { ...cur[2], output: next };
|
|
260
|
-
}
|
|
261
|
-
return decision;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** one hook function: sync throw, async rejection and timeout all become `undefined` + a note */
|
|
265
|
-
private invoke(source: string, name: HookName, fn: (...a: unknown[]) => unknown, args: readonly unknown[]): Promise<unknown> {
|
|
266
|
-
let p: Promise<unknown>;
|
|
267
|
-
try { p = Promise.resolve(fn(...args)); }
|
|
268
|
-
catch (e) { this.warn(`${source}: ${name} hook threw: ${errText(e)} — ignored, run continues`); return Promise.resolve(undefined); }
|
|
269
|
-
return withTimeout(p, this.timeoutMs).then(
|
|
270
|
-
(v) => {
|
|
271
|
-
if (v !== TIMED_OUT) return v;
|
|
272
|
-
this.warn(`${source}: ${name} hook timed out after ${this.timeoutMs}ms — ignored, run continues`);
|
|
273
|
-
return undefined;
|
|
274
|
-
},
|
|
275
|
-
(e) => { this.warn(`${source}: ${name} hook threw: ${errText(e)} — ignored, run continues`); return undefined; },
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** per-run view for the loop: maps the event stream onto pre_run / compaction / post_run (awaited, in
|
|
280
|
-
* order) and taps on_event for every event (fire-and-forget). close() is the loop's teardown seam
|
|
281
|
-
* (fix-wave 4, #39 MED-1): a consumer that .return()s the generator before run_end still ended the
|
|
282
|
-
* run, so post_run fires ONCE with the abort shape — after a pre_run only, never after a yielded
|
|
283
|
-
* run_end — while on_event gets nothing synthesized (it mirrors the consumer's stream exactly). */
|
|
284
|
-
observer(base: { cwd: string; sessionId: string }): RunObserver {
|
|
285
|
-
let ctx: HookCtx = { cwd: base.cwd, sessionId: base.sessionId };
|
|
286
|
-
let ended = false;
|
|
287
|
-
return {
|
|
288
|
-
observe: async (ev) => {
|
|
289
|
-
if (ev.type === "run_start") ctx = { ...ctx, runId: ev.runId };
|
|
290
|
-
this.tap(ctx, ev);
|
|
291
|
-
if (ev.type === "run_start") await this.run("pre_run", ctx);
|
|
292
|
-
else if (ev.type === "compaction") await this.run("compaction", ctx, ev);
|
|
293
|
-
else if (ev.type === "run_end") { ended = true; await this.run("post_run", ctx, { status: ev.status, summary: ev.summary }); }
|
|
294
|
-
},
|
|
295
|
-
close: async () => {
|
|
296
|
-
if (ended || ctx.runId === undefined) return;
|
|
297
|
-
ended = true; await this.run("post_run", ctx, { status: "stopped", summary: "run aborted" });
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** The approval hook as an ApprovalFn for the approver chain — cli/runtime.ts buildCfg composes
|
|
303
|
-
* execPolicyApprover(hooks.approver(human)), so this runs only for prompt-classified calls that
|
|
304
|
-
* execpolicy did not settle (header: Authority). "allow" → "once" (dispatch never caches once),
|
|
305
|
-
* "deny" → "deny", void → the human; no human (headless) → fail closed like execpolicy's prompt
|
|
306
|
-
* arm. ctx is the runtime's {cwd, sessionId}: the chain is composed per config, before any run. */
|
|
307
|
-
approver(human?: ApprovalFn): ApprovalFn {
|
|
308
|
-
return async (req) => {
|
|
309
|
-
const pre = await this.run("approval", this.base, { ...req, args: cloneForHook(req.args), revisedArgs: cloneForHook(req.revisedArgs) });
|
|
310
|
-
if (pre === "allow") return "once";
|
|
311
|
-
if (pre === "deny") return "deny";
|
|
312
|
-
return human ? human(req) : "deny";
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private tap(ctx: HookCtx, ev: RunEvent): void {
|
|
317
|
-
if (!this.loading && !this.has("on_event")) return; // zero cost without an on_event hook
|
|
318
|
-
const p: Promise<void> = this.run("on_event", ctx, ev).then(() => undefined, () => undefined);
|
|
319
|
-
this.pending.add(p);
|
|
320
|
-
void p.then(() => { this.pending.delete(p); });
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** wait for in-flight on_event taps (each bounded by the timeout) */
|
|
324
|
-
async settle(): Promise<void> { await Promise.all([...this.pending]); }
|
|
325
|
-
|
|
326
|
-
/** fire session_close ONCE (after the load and in-flight taps settle); later calls share the promise */
|
|
327
|
-
close(): Promise<void> {
|
|
328
|
-
this.closing ??= (async () => {
|
|
329
|
-
if (this.loading) await this.loading;
|
|
330
|
-
await this.settle();
|
|
331
|
-
await this.dispatch("session_close", [this.base]);
|
|
332
|
-
})();
|
|
333
|
-
return this.closing;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** subscribe a surface (cmdRun → stderr, TUI → note); buffered notes are replayed first */
|
|
337
|
-
onWarning(fn: (note: string) => void): void { this.listener = fn; for (const w of this.warnings) fn(w); }
|
|
338
|
-
drainWarnings(): string[] { return this.warnings.splice(0); }
|
|
339
|
-
private warn(note: string): void {
|
|
340
|
-
const n = clip(note, MAX_WARNING_CHARS);
|
|
341
|
-
this.warnings.push(n);
|
|
342
|
-
if (this.warnings.length > MAX_WARNINGS) this.warnings.shift();
|
|
343
|
-
this.listener?.(n);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// ---------- result validation + helpers ----------
|
|
348
|
-
|
|
349
|
-
/** Hooks get COPIES of a call's args and a tool's result: a hook that mutates its argument must not
|
|
350
|
-
* re-aim a call policy already evaluated, rewrite the persisted tool_call, or dodge the post_tool
|
|
351
|
-
* growth bound (which applies to the RETURNED {output} only). JSON-derived values clone; anything
|
|
352
|
-
* structuredClone rejects (never off the wire) passes through as is rather than failing the call. */
|
|
353
|
-
export function cloneForHook<T>(v: T): T { try { return structuredClone(v); } catch { return v; } }
|
|
354
|
-
|
|
355
|
-
function normalize<K extends HookName>(name: K, raw: unknown, args: HookArgs<K>, warn: (w: string) => void): HookDecision<K> | undefined {
|
|
356
|
-
if (raw === undefined || raw === null) return undefined;
|
|
357
|
-
switch (name) {
|
|
358
|
-
case "pre_tool": {
|
|
359
|
-
const deny = isRecord(raw) ? raw["deny"] : undefined;
|
|
360
|
-
if (typeof deny === "string" && deny.trim().length > 0) return { deny: clip(deny.trim(), MAX_DENY_REASON_CHARS) } as HookDecision<K>;
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
case "approval":
|
|
364
|
-
if (raw === "allow" || raw === "deny") return raw as HookDecision<K>;
|
|
365
|
-
break;
|
|
366
|
-
case "post_tool": {
|
|
367
|
-
if (!isRecord(raw)) break;
|
|
368
|
-
if (raw["output"] === undefined) return undefined; // {} = leave the output alone
|
|
369
|
-
if (typeof raw["output"] !== "string") break;
|
|
370
|
-
const original = (args as unknown as [HookCtx, HookToolCall, ToolOutput])[2].output;
|
|
371
|
-
return { output: boundGrowth(raw["output"], original.length) } as HookDecision<K>;
|
|
372
|
-
}
|
|
373
|
-
default:
|
|
374
|
-
return undefined; // void hooks: a stray return value is not an error
|
|
375
|
-
}
|
|
376
|
-
warn(`${name} hook returned an invalid result (${describe(raw)}) — ignored`);
|
|
377
|
-
return undefined;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** a hook may replace the output outright, but may not GROW it past the original by more than the cap */
|
|
381
|
-
function boundGrowth(text: string, originalLen: number): string {
|
|
382
|
-
const cap = originalLen + MAX_POST_TOOL_GROWTH_CHARS;
|
|
383
|
-
if (text.length <= cap) return text;
|
|
384
|
-
return text.slice(0, cap) + `\n… [post_tool output truncated: hooks may add at most ${MAX_POST_TOOL_GROWTH_CHARS} chars]`;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const TIMED_OUT: unique symbol = Symbol("rovecode.hook.timeout");
|
|
388
|
-
/** resolves TIMED_OUT after ms on a REF'D timer (Bun unrefs AbortSignal.timeout — providers/retry.ts
|
|
389
|
-
* sleepMs / tools/webfetch.ts idiom); the original promise's later settle is ignored */
|
|
390
|
-
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T | typeof TIMED_OUT> {
|
|
391
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
392
|
-
const timer = setTimeout(() => resolvePromise(TIMED_OUT), ms);
|
|
393
|
-
(timer as unknown as { ref?: () => void }).ref?.();
|
|
394
|
-
p.then((v) => { clearTimeout(timer); resolvePromise(v); }, (e: unknown) => { clearTimeout(timer); rejectPromise(e); });
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function isRecord(v: unknown): v is Record<string, unknown> { return typeof v === "object" && v !== null && !Array.isArray(v); }
|
|
399
|
-
function clip(s: string, max: number): string { return s.length <= max ? s : s.slice(0, max) + "…"; }
|
|
400
|
-
/** Bun's BuildMessage/ResolveMessage are not Error instances but carry .message */
|
|
401
|
-
function errText(e: unknown): string {
|
|
402
|
-
if (e instanceof Error) return e.message;
|
|
403
|
-
if (isRecord(e) && typeof e["message"] === "string") return e["message"];
|
|
404
|
-
return String(e);
|
|
405
|
-
}
|
|
406
|
-
function describe(v: unknown): string {
|
|
407
|
-
if (typeof v === "string") return JSON.stringify(clip(v, 40));
|
|
408
|
-
if (Array.isArray(v)) return "array";
|
|
409
|
-
if (isRecord(v)) return `object with keys ${Object.keys(v).slice(0, 5).join(",") || "(none)"}`;
|
|
410
|
-
return typeof v;
|
|
411
|
-
}
|
package/src/core/images.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/** Image attachments (port #34): file → sniff → caps → ImagePart; the wire blocks both provider
|
|
2
|
-
* adapters emit; the text stand-in for models without vision.
|
|
3
|
-
*
|
|
4
|
-
* Ported from opencode @ ebece6e (MIT), packages/opencode/src unless noted:
|
|
5
|
-
* - util/media.ts:15-26 sniffAttachmentMime — the MIME comes from magic bytes, never the file
|
|
6
|
-
* extension (png 89 50 4E 47.., jpeg FF D8 FF, gif "GIF8", webp "RIFF"…"WEBP"); bmp/pdf are
|
|
7
|
-
* recognised there but not accepted here (neither wire protocol takes them as images);
|
|
8
|
-
* - image/image.ts:10 MAX_BASE64_BYTES = 5 MiB (their cap is on the base64 form and they resize
|
|
9
|
-
* past it with photon; here the cap is on the decoded file, ROVECODE_IMAGE_MAX_BYTES, and an
|
|
10
|
-
* oversize image is an error the user fixes — never a silent re-encode);
|
|
11
|
-
* - session/prompt.ts:66-71 SUPPORTED_MCP_RESOURCE_ATTACHMENT_MIMES — the same four types;
|
|
12
|
-
* - session/message-v2.ts:213-217 `[Attached <mime>: <filename>]` text stand-in for stripped
|
|
13
|
-
* media and provider/transform.ts:409-441 unsupportedParts (image → text when the model's
|
|
14
|
-
* input modalities lack "image") — the placeholder below is rovecode's own wording.
|
|
15
|
-
* Pattern reference only, no code copied: cline @ 8eb5f3d sdk/packages/shared/src/llms/media.ts
|
|
16
|
-
* SUPPORTED_IMAGE_MEDIA_TYPES (:73-78), DEFAULT_MAX_IMAGE_BASE64_BYTES 5 MiB (:80),
|
|
17
|
-
* IMAGE_UNSUPPORTED_PLACEHOLDER (:6-12: the stored history keeps the real image, only the
|
|
18
|
-
* request is substituted); apps/vscode/src/shared/messages/content.ts:140-153 base64 source ↔
|
|
19
|
-
* `data:<mime>;base64,<data>` URL.
|
|
20
|
-
* Wire shapes: Anthropic `{type:"image", source:{type:"base64", media_type, data}}`; OpenAI
|
|
21
|
-
* `{type:"image_url", image_url:{url:"data:<mime>;base64,<data>", detail}}`.
|
|
22
|
-
* Dimensions are read from container headers only (PNG IHDR, GIF logical screen, JPEG SOFn,
|
|
23
|
-
* WebP VP8/VP8L/VP8X) — no decoding; a header we cannot parse just leaves width/height unset. */
|
|
24
|
-
|
|
25
|
-
import { readFileSync, statSync } from "node:fs";
|
|
26
|
-
import { basename, isAbsolute } from "node:path";
|
|
27
|
-
import type { ImageMime, ImagePart } from "./types.ts";
|
|
28
|
-
|
|
29
|
-
/** decoded-file cap per image (bytes); ROVECODE_IMAGE_MAX_BYTES overrides */
|
|
30
|
-
export const IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
31
|
-
export const IMAGE_MAX_BYTES_ENV = "ROVECODE_IMAGE_MAX_BYTES";
|
|
32
|
-
/** cap per user message — the TUI's /attach queue and the ACP prompt path both check it */
|
|
33
|
-
export const MAX_IMAGES_PER_MESSAGE = 8;
|
|
34
|
-
export const IMAGE_MIMES: readonly ImageMime[] = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
35
|
-
|
|
36
|
-
export interface ImageLoadOptions {
|
|
37
|
-
/** per-image byte cap; default ROVECODE_IMAGE_MAX_BYTES, else IMAGE_MAX_BYTES */
|
|
38
|
-
maxBytes?: number;
|
|
39
|
-
/** env the cap is read from (tests); default process.env */
|
|
40
|
-
env?: Record<string, string | undefined>;
|
|
41
|
-
/** display name; loadImageAttachment uses the file's basename */
|
|
42
|
-
name?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type ImageLoadResult = ImagePart | { error: string };
|
|
46
|
-
|
|
47
|
-
/** The effective per-image cap: a positive integer ROVECODE_IMAGE_MAX_BYTES wins, else the default. */
|
|
48
|
-
export function imageMaxBytes(env: Record<string, string | undefined> = process.env): number {
|
|
49
|
-
const raw = env[IMAGE_MAX_BYTES_ENV]?.trim();
|
|
50
|
-
if (!raw) return IMAGE_MAX_BYTES;
|
|
51
|
-
const n = Number(raw);
|
|
52
|
-
return Number.isInteger(n) && n > 0 ? n : IMAGE_MAX_BYTES;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---------- sniffing + dimensions ----------
|
|
56
|
-
|
|
57
|
-
const startsWith = (b: Uint8Array, at: number, prefix: number[]): boolean =>
|
|
58
|
-
b.length >= at + prefix.length && prefix.every((v, i) => b[at + i] === v);
|
|
59
|
-
|
|
60
|
-
/** MIME by magic bytes (opencode media.ts:15-26 minus bmp/pdf); undefined = not an image we accept. */
|
|
61
|
-
export function sniffImageMime(bytes: Uint8Array): ImageMime | undefined {
|
|
62
|
-
if (startsWith(bytes, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "image/png";
|
|
63
|
-
if (startsWith(bytes, 0, [0xff, 0xd8, 0xff])) return "image/jpeg";
|
|
64
|
-
if (startsWith(bytes, 0, [0x47, 0x49, 0x46, 0x38])) return "image/gif";
|
|
65
|
-
if (startsWith(bytes, 0, [0x52, 0x49, 0x46, 0x46]) && startsWith(bytes, 8, [0x57, 0x45, 0x42, 0x50])) return "image/webp";
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const be16 = (b: Uint8Array, i: number): number => (b[i]! << 8) | b[i + 1]!;
|
|
70
|
-
const be32 = (b: Uint8Array, i: number): number => ((b[i]! << 24) | (b[i + 1]! << 16) | (b[i + 2]! << 8) | b[i + 3]!) >>> 0;
|
|
71
|
-
const le16 = (b: Uint8Array, i: number): number => b[i]! | (b[i + 1]! << 8);
|
|
72
|
-
const le24 = (b: Uint8Array, i: number): number => b[i]! | (b[i + 1]! << 8) | (b[i + 2]! << 16);
|
|
73
|
-
|
|
74
|
-
/** Header-only width/height; undefined when the header is truncated or unrecognised. */
|
|
75
|
-
export function imageDimensions(bytes: Uint8Array, mime: ImageMime): { width: number; height: number } | undefined {
|
|
76
|
-
const dims = (width: number, height: number) => (width > 0 && height > 0 ? { width, height } : undefined);
|
|
77
|
-
switch (mime) {
|
|
78
|
-
case "image/png": // IHDR is always the first chunk: length(4) "IHDR"(4) width(4) height(4)
|
|
79
|
-
return bytes.length >= 24 && startsWith(bytes, 12, [0x49, 0x48, 0x44, 0x52]) ? dims(be32(bytes, 16), be32(bytes, 20)) : undefined;
|
|
80
|
-
case "image/gif": // logical screen descriptor right after "GIF89a"/"GIF87a"
|
|
81
|
-
return bytes.length >= 10 ? dims(le16(bytes, 6), le16(bytes, 8)) : undefined;
|
|
82
|
-
case "image/jpeg": return jpegDimensions(bytes);
|
|
83
|
-
case "image/webp": return webpDimensions(bytes);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Walk the marker segments to the first SOFn (C0-CF except C4 DHT, C8 JPG, CC DAC): height, width. */
|
|
88
|
-
function jpegDimensions(b: Uint8Array): { width: number; height: number } | undefined {
|
|
89
|
-
let i = 2;
|
|
90
|
-
while (i + 3 < b.length) {
|
|
91
|
-
if (b[i] !== 0xff) return undefined;
|
|
92
|
-
const marker = b[i + 1]!;
|
|
93
|
-
if (marker === 0xff) { i += 1; continue; } // fill byte
|
|
94
|
-
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { i += 2; continue; } // standalone
|
|
95
|
-
if (marker === 0xd9 || marker === 0xda) return undefined; // EOI / SOS before any SOF
|
|
96
|
-
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
97
|
-
return i + 8 < b.length ? { height: be16(b, i + 5), width: be16(b, i + 7) } : undefined;
|
|
98
|
-
}
|
|
99
|
-
i += 2 + be16(b, i + 2);
|
|
100
|
-
}
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** First chunk after "WEBP": VP8 (lossy frame header), VP8L (lossless 14-bit fields), VP8X (extended canvas). */
|
|
105
|
-
function webpDimensions(b: Uint8Array): { width: number; height: number } | undefined {
|
|
106
|
-
if (b.length < 30) return undefined;
|
|
107
|
-
const fourcc = String.fromCharCode(b[12]!, b[13]!, b[14]!, b[15]!);
|
|
108
|
-
if (fourcc === "VP8 ") return { width: le16(b, 26) & 0x3fff, height: le16(b, 28) & 0x3fff };
|
|
109
|
-
if (fourcc === "VP8L" && b[20] === 0x2f) {
|
|
110
|
-
return { width: 1 + (b[21]! | ((b[22]! & 0x3f) << 8)), height: 1 + ((b[22]! >> 6) | (b[23]! << 2) | ((b[24]! & 0x0f) << 10)) };
|
|
111
|
-
}
|
|
112
|
-
if (fourcc === "VP8X") return { width: 1 + le24(b, 24), height: 1 + le24(b, 27) };
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ---------- loading ----------
|
|
117
|
-
|
|
118
|
-
/** `70 B`, `5121 KB` — the unit both the caps' errors and the placeholder use */
|
|
119
|
-
const fmtBytes = (n: number): string => (n < 1024 ? `${n} B` : `${Math.ceil(n / 1024)} KB`);
|
|
120
|
-
|
|
121
|
-
/** Bytes already in memory → ImagePart (base64 inline). Rejects by magic bytes and by the cap. */
|
|
122
|
-
export function imageFromBytes(bytes: Uint8Array, opts: ImageLoadOptions = {}): ImageLoadResult {
|
|
123
|
-
const label = opts.name ?? "image";
|
|
124
|
-
const mime = sniffImageMime(bytes);
|
|
125
|
-
if (!mime) {
|
|
126
|
-
const head = [...bytes.subarray(0, 4)].map((x) => x.toString(16).padStart(2, "0")).join(" ");
|
|
127
|
-
return { error: `${label}: not a png/jpeg/gif/webp image (magic bytes: ${head || "empty file"})` };
|
|
128
|
-
}
|
|
129
|
-
const max = opts.maxBytes ?? imageMaxBytes(opts.env);
|
|
130
|
-
if (bytes.byteLength > max) return { error: `${label}: ${fmtBytes(bytes.byteLength)} is over the ${fmtBytes(max)} per-image cap (${IMAGE_MAX_BYTES_ENV})` };
|
|
131
|
-
const part: ImagePart = { kind: "image", mime, bytes: Buffer.from(bytes).toString("base64") };
|
|
132
|
-
const d = imageDimensions(bytes, mime);
|
|
133
|
-
if (d) { part.width = d.width; part.height = d.height; }
|
|
134
|
-
if (opts.name !== undefined) part.name = opts.name;
|
|
135
|
-
return part;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** A file on disk → ImagePart. Size is checked from stat BEFORE reading (a 2 GB "image" is
|
|
139
|
-
* rejected without touching its bytes); the type comes from the bytes, never the extension.
|
|
140
|
-
* Never throws: every failure is `{ error }`. */
|
|
141
|
-
export function loadImageAttachment(filePath: string, opts: ImageLoadOptions = {}): ImageLoadResult {
|
|
142
|
-
const name = opts.name ?? basename(filePath);
|
|
143
|
-
let size: number;
|
|
144
|
-
try {
|
|
145
|
-
const st = statSync(filePath);
|
|
146
|
-
if (!st.isFile()) return { error: `${name}: not a file` };
|
|
147
|
-
size = st.size;
|
|
148
|
-
} catch (e) {
|
|
149
|
-
return { error: `${name}: cannot read (${e instanceof Error ? e.message : String(e)})` };
|
|
150
|
-
}
|
|
151
|
-
const max = opts.maxBytes ?? imageMaxBytes(opts.env);
|
|
152
|
-
if (size > max) return { error: `${name}: ${fmtBytes(size)} is over the ${fmtBytes(max)} per-image cap (${IMAGE_MAX_BYTES_ENV})` };
|
|
153
|
-
try {
|
|
154
|
-
return imageFromBytes(readFileSync(filePath), { ...opts, maxBytes: max, name });
|
|
155
|
-
} catch (e) {
|
|
156
|
-
return { error: `${name}: cannot read (${e instanceof Error ? e.message : String(e)})` };
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Transport form (ACP image blocks, HTTP bodies): base64 + declared mime → ImagePart. The bytes
|
|
161
|
-
* decide the type; a declared mime that disagrees is an error (cline media.ts media_type_mismatch). */
|
|
162
|
-
export function imageFromBase64(data: string, declaredMime: string | undefined, opts: ImageLoadOptions = {}): ImageLoadResult {
|
|
163
|
-
const bytes = Buffer.from(data, "base64");
|
|
164
|
-
const res = imageFromBytes(bytes, opts);
|
|
165
|
-
if ("error" in res) return res;
|
|
166
|
-
if (declaredMime !== undefined && declaredMime !== res.mime) return { error: `${opts.name ?? "image"}: declared ${declaredMime} but the bytes are ${res.mime}` };
|
|
167
|
-
return res;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Per-message image cap: the error to show when `count` images would ride on one message. */
|
|
171
|
-
export function checkImageCount(count: number, max = MAX_IMAGES_PER_MESSAGE): string | undefined {
|
|
172
|
-
return count > max ? `at most ${max} images per message (${count} attached)` : undefined;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---------- reading back (wire + display) ----------
|
|
176
|
-
|
|
177
|
-
/** The base64 payload: inline bytes, else the sidecar file (absolute path — the session store
|
|
178
|
-
* resolves relative ones on load). undefined when unreadable — callers fall back to text. */
|
|
179
|
-
export function imageData(part: ImagePart): string | undefined {
|
|
180
|
-
if (part.bytes !== undefined) return part.bytes;
|
|
181
|
-
if (part.path === undefined || !isAbsolute(part.path)) return undefined;
|
|
182
|
-
try { return readFileSync(part.path).toString("base64"); } catch { return undefined; }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Decoded byte size when knowable without reading a sidecar (inline: from the base64 length).
|
|
186
|
-
* Same path rule as imageData(): only an absolute (store-hydrated) path is stat'ed — a persisted
|
|
187
|
-
* relative path must never touch the process cwd (session.ts F2 confinement). */
|
|
188
|
-
export function imageByteSize(part: ImagePart): number | undefined {
|
|
189
|
-
if (part.bytes !== undefined) {
|
|
190
|
-
const pad = part.bytes.endsWith("==") ? 2 : part.bytes.endsWith("=") ? 1 : 0;
|
|
191
|
-
return Math.floor((part.bytes.length * 3) / 4) - pad;
|
|
192
|
-
}
|
|
193
|
-
if (part.path === undefined || !isAbsolute(part.path)) return undefined;
|
|
194
|
-
try { return statSync(part.path).size; } catch { return undefined; }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function imageExt(mime: ImageMime): "png" | "jpg" | "gif" | "webp" {
|
|
198
|
-
return mime === "image/png" ? "png" : mime === "image/jpeg" ? "jpg" : mime === "image/gif" ? "gif" : "webp";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** `name, WxH, N KB` — whatever is known, in that order. */
|
|
202
|
-
export function describeImage(part: ImagePart): string {
|
|
203
|
-
const bits = [part.name ?? part.mime];
|
|
204
|
-
if (part.width !== undefined && part.height !== undefined) bits.push(`${part.width}x${part.height}`);
|
|
205
|
-
const size = imageByteSize(part);
|
|
206
|
-
if (size !== undefined) bits.push(fmtBytes(size));
|
|
207
|
-
return bits.join(", ");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Text the adapters send in place of an image the model cannot see (or a sidecar that is gone). */
|
|
211
|
-
export function imagePlaceholder(part: ImagePart, reason = "model has no vision"): string {
|
|
212
|
-
return `[image: ${describeImage(part)} — ${reason}]`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Transcript chip for a user message's attachment. */
|
|
216
|
-
export function imageChip(part: ImagePart): string {
|
|
217
|
-
return `[image: ${part.name ?? part.mime}]`;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Anthropic Messages content block; undefined when the bytes are unavailable. */
|
|
221
|
-
export function anthropicImageBlock(part: ImagePart): Record<string, unknown> | undefined {
|
|
222
|
-
const data = imageData(part);
|
|
223
|
-
return data === undefined ? undefined : { type: "image", source: { type: "base64", media_type: part.mime, data } };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** OpenAI chat-completions content part (data URL); undefined when the bytes are unavailable. */
|
|
227
|
-
export function openaiImageBlock(part: ImagePart, detail: "auto" | "low" | "high" = "auto"): Record<string, unknown> | undefined {
|
|
228
|
-
const data = imageData(part);
|
|
229
|
-
return data === undefined ? undefined : { type: "image_url", image_url: { url: `data:${part.mime};base64,${data}`, detail } };
|
|
230
|
-
}
|