@sma1lboy/rove 0.9.276 → 0.9.277-next.1
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/dist/cli/{chunk-mkfc9a09.js → chunk-0a65j1zj.js} +1 -1
- package/dist/cli/{chunk-xb3p11g0.js → chunk-0rvydb9q.js} +1 -1
- package/dist/cli/chunk-1g6vhgfe.js +14 -0
- package/dist/cli/{chunk-eq7vey2j.js → chunk-1vxtd8rh.js} +2 -2
- package/dist/cli/chunk-1wwqf6kx.js +3 -0
- package/dist/cli/{chunk-1jjqk7sq.js → chunk-2kbzb24j.js} +1 -1
- package/dist/cli/chunk-2kt0mfvf.js +2 -0
- package/dist/cli/{chunk-mz1ffkqb.js → chunk-2vph030z.js} +3 -3
- package/dist/cli/{chunk-nr7e3e82.js → chunk-2y70y8na.js} +1 -1
- package/dist/cli/{chunk-jckx79gk.js → chunk-36eyrv4r.js} +3 -3
- package/dist/cli/chunk-37fas9aj.js +2 -0
- package/dist/cli/chunk-3dhv5n19.js +2 -0
- package/dist/cli/{chunk-zkp6nty3.js → chunk-3x5g06mw.js} +1 -1
- package/dist/cli/{chunk-c6fy8xew.js → chunk-3y0z478f.js} +1 -1
- package/dist/cli/{chunk-91tcwxph.js → chunk-4w4tpsqx.js} +1 -1
- package/dist/cli/chunk-5apxck3d.js +97 -0
- package/dist/cli/chunk-5vrg6h71.js +3 -0
- package/dist/cli/{chunk-k1pwp155.js → chunk-5xpvd182.js} +1 -1
- package/dist/cli/{chunk-8dd60adw.js → chunk-5yh5trgy.js} +1 -1
- package/dist/cli/{chunk-r7ntkd6y.js → chunk-61cn88e7.js} +1 -1
- package/dist/cli/{chunk-e3cd4qd2.js → chunk-671ak750.js} +2 -2
- package/dist/cli/{chunk-82apkcrk.js → chunk-676f3tva.js} +1 -1
- package/dist/cli/chunk-6hcyvg0m.js +3 -0
- package/dist/cli/{chunk-h2gznz46.js → chunk-6mg5zp8y.js} +1 -1
- package/dist/cli/chunk-6q4br0pt.js +2 -0
- package/dist/cli/chunk-6wfhq8xd.js +4 -0
- package/dist/cli/{chunk-3xkhcfet.js → chunk-748656h7.js} +3 -3
- package/dist/cli/{chunk-2kqxz9a4.js → chunk-7bakcyes.js} +1 -1
- package/dist/cli/{chunk-yk9b8zgn.js → chunk-7kgcy89w.js} +1 -1
- package/dist/cli/{chunk-e3t1zbv1.js → chunk-8qkyd589.js} +1 -1
- package/dist/cli/chunk-9ckvdy0j.js +3 -0
- package/dist/cli/{chunk-5e9y8aw5.js → chunk-9fdy8vne.js} +1 -1
- package/dist/cli/chunk-9x2xd1rz.js +2 -0
- package/dist/cli/{chunk-je80r59s.js → chunk-9zswhjkv.js} +1 -1
- package/dist/cli/{chunk-c7mz2aq0.js → chunk-aq4j8ar4.js} +1 -1
- package/dist/cli/{chunk-dt676ks9.js → chunk-aqwckba5.js} +1 -1
- package/dist/cli/chunk-atg13xab.js +3 -0
- package/dist/cli/{chunk-wvqn1t55.js → chunk-bagwgxsg.js} +1 -1
- package/dist/cli/{chunk-syn0tfwy.js → chunk-bcyex84w.js} +1 -1
- package/dist/cli/chunk-bv050ssb.js +8 -0
- package/dist/cli/chunk-bykqhbvy.js +2 -0
- package/dist/cli/{chunk-81thm1eb.js → chunk-ca4x8s96.js} +3 -3
- package/dist/cli/{chunk-syfs63r2.js → chunk-cakyme4s.js} +1 -1
- package/dist/cli/{chunk-63cwjceg.js → chunk-ccmke5sx.js} +1 -1
- package/dist/cli/{chunk-5nvg3ek8.js → chunk-cf3vpsm5.js} +1 -1
- package/dist/cli/{chunk-zat16a83.js → chunk-d552cvb2.js} +1 -1
- package/dist/cli/{chunk-awh7np6x.js → chunk-dsdtq087.js} +1 -1
- package/dist/cli/{chunk-d0hpes59.js → chunk-e58kj4gm.js} +1 -1
- package/dist/cli/chunk-ee314raa.js +2 -0
- package/dist/cli/{chunk-f76gzvq3.js → chunk-fgtc1xq9.js} +1 -1
- package/dist/cli/{chunk-h7vwn62z.js → chunk-fq2pefc5.js} +1 -1
- package/dist/cli/{chunk-4117kgmx.js → chunk-g4p5znv1.js} +1 -1
- package/dist/cli/{chunk-1ca2j9w6.js → chunk-g9xknhjq.js} +1 -1
- package/dist/cli/chunk-gbd5j3dv.js +2 -0
- package/dist/cli/{chunk-4xvmrg86.js → chunk-ggjmcqpt.js} +1 -1
- package/dist/cli/chunk-hv79ys32.js +1 -1
- package/dist/cli/chunk-hz1x3hak.js +3 -0
- package/dist/cli/{chunk-h64rsbrx.js → chunk-hzz020xx.js} +1 -1
- package/dist/cli/{chunk-ma21srvx.js → chunk-j1qy79fd.js} +1 -1
- package/dist/cli/chunk-j9tsdn1d.js +2 -0
- package/dist/cli/chunk-jejmt4k0.js +140 -0
- package/dist/cli/chunk-jj5f4n51.js +1 -1
- package/dist/cli/{chunk-x3n4y5sp.js → chunk-k225696b.js} +1 -1
- package/dist/cli/{chunk-d7xbm9jp.js → chunk-kag7cfbr.js} +1 -1
- package/dist/cli/{chunk-283vd9h7.js → chunk-kdnk8m5j.js} +1 -1
- package/dist/cli/{chunk-xd93fm1k.js → chunk-kxp04jnp.js} +1 -1
- package/dist/cli/chunk-m4z6kepz.js +3 -0
- package/dist/cli/{chunk-372g44dg.js → chunk-mjyhy152.js} +1 -1
- package/dist/cli/chunk-mqvkd2g1.js +2 -0
- package/dist/cli/{chunk-t4qr9sv4.js → chunk-mvtqstte.js} +1 -1
- package/dist/cli/{chunk-m2a19yt4.js → chunk-mxx0y791.js} +1 -1
- package/dist/cli/{chunk-d9643dwf.js → chunk-n2zgej1a.js} +1 -1
- package/dist/cli/{chunk-xn2f56tg.js → chunk-n94vzkbe.js} +1 -1
- package/dist/cli/{chunk-9gh8kgjs.js → chunk-njncqdwf.js} +1 -1
- package/dist/cli/{chunk-t52gzaeb.js → chunk-nw092jak.js} +1 -1
- package/dist/cli/{chunk-gafkp74h.js → chunk-pfagrkt6.js} +1 -1
- package/dist/cli/{chunk-511103e8.js → chunk-pnrftncm.js} +1 -1
- package/dist/cli/{chunk-9c3brkb3.js → chunk-pqr6td4z.js} +1 -1
- package/dist/cli/{chunk-tvdrvgen.js → chunk-qcjkactr.js} +1 -1
- package/dist/cli/{chunk-v1r83m3n.js → chunk-qs359899.js} +1 -1
- package/dist/cli/{chunk-n6marmjn.js → chunk-qzzmd63x.js} +2 -3
- package/dist/cli/{chunk-fn7ffyam.js → chunk-r2bb8b11.js} +1 -1
- package/dist/cli/chunk-rjy900p4.js +4 -0
- package/dist/cli/{chunk-b035pej7.js → chunk-rmsdybs3.js} +1 -1
- package/dist/cli/{chunk-6mtfafaw.js → chunk-rztqccs3.js} +1 -1
- package/dist/cli/chunk-s1308b3d.js +63 -0
- package/dist/cli/{chunk-ne6x6g2c.js → chunk-se00fc8v.js} +1 -1
- package/dist/cli/{chunk-zcrs4sm2.js → chunk-shtm72c0.js} +1 -1
- package/dist/cli/{chunk-gq57he03.js → chunk-t9vjj8sy.js} +1 -1
- package/dist/cli/{chunk-0d7517kk.js → chunk-ta1p2nwa.js} +1 -1
- package/dist/cli/chunk-teh40faw.js +1 -1
- package/dist/cli/{chunk-eyvw70yb.js → chunk-tnatqcy7.js} +1 -1
- package/dist/cli/{chunk-mq88bvj6.js → chunk-tt58819p.js} +1 -1
- package/dist/cli/{chunk-pdq3ya2e.js → chunk-tw9tfs1d.js} +2 -2
- package/dist/cli/chunk-v6dz59gy.js +2 -0
- package/dist/cli/chunk-v6v272b3.js +3 -0
- package/dist/cli/{chunk-hhcmzs67.js → chunk-vkv1v3xn.js} +6 -28
- package/dist/cli/chunk-vkz6kkmm.js +24 -0
- package/dist/cli/chunk-vp29jgbt.js +4 -0
- package/dist/cli/chunk-vpsarakj.js +2 -0
- package/dist/cli/{chunk-745pg3xm.js → chunk-vr2r2ncm.js} +1 -1
- package/dist/cli/{chunk-3y9xzbqh.js → chunk-vvqwgcge.js} +1 -1
- package/dist/cli/{chunk-m73x5nry.js → chunk-vwpf55cr.js} +2 -2
- package/dist/cli/{chunk-vz0f8e8q.js → chunk-vytrhm50.js} +1 -1
- package/dist/cli/{chunk-c63vgbkw.js → chunk-w7v71t7e.js} +1 -1
- package/dist/cli/{chunk-hcvp84b3.js → chunk-wfgrn8p2.js} +1 -1
- package/dist/cli/chunk-wzss4pzh.js +3 -0
- package/dist/cli/chunk-x1ghdmp1.js +1 -1
- package/dist/cli/chunk-x1jxxmtc.js +3 -0
- package/dist/cli/{chunk-d7f022eb.js → chunk-x83xbzjd.js} +3 -3
- package/dist/cli/{chunk-22rgewg0.js → chunk-x85rcep5.js} +1 -1
- package/dist/cli/{chunk-j9b285x6.js → chunk-x89zw7br.js} +1 -1
- package/dist/cli/{chunk-ccwe9n56.js → chunk-xg42e2ff.js} +1 -1
- package/dist/cli/{chunk-jfcn77n9.js → chunk-xte4phd1.js} +1 -1
- package/dist/cli/chunk-y8b2p44q.js +1 -1
- package/dist/cli/chunk-ys6v36ph.js +2 -0
- package/dist/cli/{chunk-n8t4y0bj.js → chunk-yzdrhytj.js} +1 -1
- package/dist/cli/{chunk-qna1s1r4.js → chunk-z3qz6enf.js} +1 -1
- package/dist/cli/chunk-z3wrbtcq.js +22 -0
- package/dist/cli/{chunk-cfzg8q3e.js → chunk-z43e08kw.js} +1 -1
- package/dist/cli/{chunk-ehm9abge.js → chunk-zpwvjt14.js} +7 -5
- package/dist/cli/{chunk-k235z3ct.js → chunk-zrf93zrx.js} +1 -1
- package/dist/cli/index.js +9 -12
- package/dist/cli/pty-host-node.mjs +1 -3
- package/dist/cli/rove-run.js +1 -1
- package/dist/completions/rove.bash +1 -1
- package/dist/completions/rove.fish +1 -1
- package/dist/completions/rove.zsh +1 -1
- package/dist/skills/rove/SKILL.md +4 -3
- package/dist/skills/rove/references/api-flags.md +12 -5
- package/dist/web/assets/index-BVQbpUea.css +1 -0
- package/dist/web/assets/index-Cn_7vMHX.js +152 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
- package/dist/web/index.html +2 -2
- package/dist/web/relay/index.html +34 -0
- package/dist/web-ui/assets/{harness-pEyVtWV4.js → harness-C7ciHx2w.js} +2 -2
- package/dist/web-ui/assets/{index-Dyg-qcgB.js → index-B1FLht5L.js} +2 -2
- package/dist/web-ui/assets/{index-Bsn8NuKa.css → index-BDIkPexK.css} +1 -1
- package/dist/web-ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/cli/chunk-1eef3z5v.js +0 -3
- package/dist/cli/chunk-1n03wzx4.js +0 -3
- package/dist/cli/chunk-387w0db1.js +0 -2
- package/dist/cli/chunk-4a05fgdh.js +0 -2
- package/dist/cli/chunk-59p3h5e9.js +0 -2
- package/dist/cli/chunk-6xga1a2d.js +0 -3
- package/dist/cli/chunk-84pvn784.js +0 -2
- package/dist/cli/chunk-883fyezq.js +0 -3
- package/dist/cli/chunk-a4r45qdn.js +0 -3
- package/dist/cli/chunk-brgfsm5v.js +0 -2
- package/dist/cli/chunk-bw98jn7g.js +0 -18
- package/dist/cli/chunk-cbrt4s7m.js +0 -3
- package/dist/cli/chunk-d9e4ssy7.js +0 -12
- package/dist/cli/chunk-dgx02565.js +0 -3
- package/dist/cli/chunk-g4dtqbrj.js +0 -2
- package/dist/cli/chunk-gctqthxq.js +0 -97
- package/dist/cli/chunk-hmjg1qh3.js +0 -2
- package/dist/cli/chunk-jn10xeft.js +0 -2
- package/dist/cli/chunk-nbpq1tkx.js +0 -98
- package/dist/cli/chunk-ns35rdyk.js +0 -3
- package/dist/cli/chunk-pap7k3b4.js +0 -3
- package/dist/cli/chunk-r7kmn4js.js +0 -2
- package/dist/cli/chunk-tz373e5w.js +0 -3
- package/dist/cli/chunk-vp171as9.js +0 -65
- package/dist/cli/chunk-vtcr71fs.js +0 -8
- package/dist/cli/chunk-w7v4jzwy.js +0 -3
- package/dist/cli/chunk-xv3217yv.js +0 -2
- package/dist/cli/chunk-yhytkwpv.js +0 -2
- package/dist/web/assets/index-C5t3gibI.css +0 -1
- package/dist/web/assets/index-Djp7M6r8.js +0 -122
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{MachineFeed}from"./chunk-9zswhjkv.js";import{COLORBLIND_KEY,DEFAULT_BASE_REF,DEFAULT_COLORBLIND,DEFAULT_GLYPHS,DEFAULT_GLYPH_SET,DEFAULT_PREFIX_TAP_PRESENTATION,DEFAULT_RUNNING_TITLE,DEFAULT_SCROLLBACK_ROWS,DEFAULT_TASK_COLORS,DEFAULT_WORKING_BORDER,GLYPH_SETS,GLYPH_SET_KEY,PREFIX_TAP_PRESENTATIONS,PREFIX_TAP_PRESENTATION_KEY,RUNNING_TITLE_KEY,SCROLLBACK_ROWS_KEY,SCROLLBACK_ROWS_MAX,SCROLLBACK_ROWS_MIN,SPLIT_STYLE_KEY,TAB_STRIP_HIDE_SINGLE_KEY,TAB_STRIP_MODES,TAB_STRIP_MODE_KEY,TASK_COLORS_KEY,WORKING_BORDER_KEY,appearanceChoices,buildPRPrompt,cloneRepo,conversationTitleOf,createKeybindingsFile,deriveFolderName,detailFragment,detectWorktreeOpener,diffCommentsKey,displayEndpoint,engineIntegrations,findAvailableFolderName,formatDiffComments,getCurrentBranch,issueChatTaskTitle,issueProjectPrompt,issueWorktreePrompt,listLocalBranches,normalizeColorblind,normalizeGlyphSet,normalizePrefixTapPresentation,normalizeRunningTitle,normalizeScrollbackRows,normalizeSplitStyle,normalizeTaskColors,normalizeWorkingBorder,openExternally,openWorktree,prChip,readPluginRows,resolveCloneTarget,resolveTabStripMode,setPluginEnabled,spawnDetached,taskColor,unsentComments,validateCloneTarget,validateGitUrl,validateRepoPath,worktreeRowLabel}from"./chunk-vkv1v3xn.js";import"./chunk-c25vea1q.js";import"./chunk-x89zw7br.js";import"./chunk-qs359899.js";import{buildCIPromptForWorktree}from"./chunk-vkz6kkmm.js";import{checkFrame,ghText,isLoopbackHost,parseFailedRunLog,runIdFromDetailsUrl}from"./chunk-1g6vhgfe.js";import{tabStatus,taskStatus}from"./chunk-6hcyvg0m.js";import{installSkillErrors,missingSkillNames}from"./chunk-5vrg6h71.js";import{invokeVerb}from"./chunk-671ak750.js";import{argvToParams,executeVerb}from"./chunk-fgtc1xq9.js";import{DEFAULT_SOUND_VOLUME,FINISHED_ALERTS_KEY,RAIL_FOLD_STYLE_KEY,RoveKeymap,SOUND_VOLUME_KEY,SOUND_VOLUME_STEPS,ZEN_ACTIVE_KEY,ZEN_DEFAULT,attentionEdges,attentionKindFor,normalizeSoundVolume,prefixFirstStrokes,readKeybindingsFile,resetKeybindingsFileCache,resolveUserKeybindings}from"./chunk-x85rcep5.js";import{RemoteOrchestrator,liveRowTokens}from"./chunk-v6v272b3.js";import{startFileWatchTrigger}from"./chunk-r3s2t9af.js";import"./chunk-6xr1prk6.js";import{HOSTED_THEMES,ThemeInstallError,hostedThemeUrl,parseTheme,writeUserTheme}from"./chunk-wfgrn8p2.js";import{buildContext,checkRef,worktreeDiff,worktreeRefs}from"./chunk-z3wrbtcq.js";import{AUTO_ROUTING_TIERS,autoRoutingKey,describeTierBlock,listFiles,liveSourceProtocol,loadPreviewData,planWorktreeHandoff,readAutoRoutingTable,readClassifierConfig,resolveBase,runWorktreeGit,secretStatus,tierBlock}from"./chunk-zpwvjt14.js";import{ensurePluginEnginesLoaded}from"./chunk-kdnk8m5j.js";import"./chunk-qcjkactr.js";import{listPaneLaunches}from"./chunk-rjfdzjhj.js";import{detectEngineStatus,detectEngineStatuses,probeableEngineIds,summarizeEngines}from"./chunk-r2bb8b11.js";import{DEFAULT_EDITOR_KIND,EDITOR_CUSTOM_KEY,EDITOR_KINDS,EDITOR_KIND_KEY,normalizeEditorKind}from"./chunk-agvtzads.js";import"./chunk-pm91e38v.js";import{deriveTitleFromPrompt}from"./chunk-2xsxcdp6.js";import"./chunk-17e2tgyg.js";import{PROJECT_SIBLING_BASE,WORKTREE_BASE_CUSTOM_KEY,worktreeBaseKindOf}from"./chunk-1vxtd8rh.js";import"./chunk-q8ffd1pc.js";import"./chunk-4hestsjp.js";import"./chunk-qgrg9a5n.js";import"./chunk-2bgmby1d.js";import{loadPluginRegistry}from"./chunk-n0rdn2fn.js";import"./chunk-5mhvjytn.js";import"./chunk-atn06ncf.js";import"./chunk-tt58819p.js";import{forwardVerb,invokeRouted,machineDaemonSocket,requireMachine}from"./chunk-ta1p2nwa.js";import{localPtySocketPath}from"./chunk-vytrhm50.js";import{repoIdOf}from"./chunk-vp29jgbt.js";import{WORKTREE_BASE_KEY}from"./chunk-gq36nzg8.js";import"./chunk-sq6zj3fb.js";import"./chunk-qkehg2c1.js";import"./chunk-shtm72c0.js";import{resolveThemeSlotHex}from"./chunk-j1qy79fd.js";import{ensurePtyHostReachable}from"./chunk-z43e08kw.js";import"./chunk-w77s2df8.js";import"./chunk-mqfvzdpr.js";import{DEFAULT_THEME,DEFAULT_THEME_MODE,FOCUS_ACCENT_SLOTS,THEME_MODE_PREFERENCES,defaultTransparentBackground,diffInks,resolveTheme}from"./chunk-cf3vpsm5.js";import"./chunk-rxqdw6s5.js";import{BUNDLED_THEME_JSONS,loadUserThemes,userThemesDir}from"./chunk-kxp04jnp.js";import{conversationTitle,expandHome,filterSubdirs,initialTabs,listSubdirs,moveTab,splitPathForDirSuggest,stripTrailingSlash,tabPtyKeyFor}from"./chunk-tw9tfs1d.js";import{AUTO_STATUS_KEY,DISPATCHER_KEY}from"./chunk-61cn88e7.js";import{defaultEngineCommand,engineDisplayName,engineRenamedFrom,protocolEntry,roveApiInvocation}from"./chunk-atg13xab.js";import{ApiError,REMOTE_CALLER}from"./chunk-r5j8fycq.js";import{BUILTIN_MODS,installedBuiltinMods,isBuiltinModId,setBuiltinModInstalled}from"./chunk-xfgqae9t.js";import"./chunk-j1z9kgyd.js";import"./chunk-g9ba25q5.js";import"./chunk-ggnq2vvk.js";import"./chunk-hpay0ax8.js";import{isNonEmptyRepoFile}from"./chunk-catrr0z3.js";import{terminalTabsKey}from"./chunk-mdc7b01e.js";import"./chunk-tn9gtf18.js";import"./chunk-895yqjjz.js";import"./chunk-sak1tphp.js";import"./chunk-y1xnjx8x.js";import{ensureDaemonReachable}from"./chunk-e58kj4gm.js";import"./chunk-0a65j1zj.js";import"./chunk-bcyex84w.js";import"./chunk-vvqwgcge.js";import{engineEntry,vendorsWithTurnReader}from"./chunk-x83xbzjd.js";import"./chunk-2tfnscxk.js";import{installedEngineIds}from"./chunk-hzz020xx.js";import"./chunk-w7zv47mp.js";import"./chunk-etfh8f4z.js";import{resolvePreferredVendor,setRepoLastActiveVendor}from"./chunk-9rj2fx6v.js";import{getDisabledEngineIds,getSavedRepos,resolveMainRepoRoot}from"./chunk-rfp0ayra.js";import"./chunk-0r1jf8m9.js";import"./chunk-t7j8g9f6.js";import"./chunk-dmzg9zdc.js";import"./chunk-40dgv828.js";import"./chunk-kg9kmwt9.js";import"./chunk-6xppe9bj.js";import{CURRENT_VERSION,checkLatestVersion,fetchReleaseNotes,fetchReleaseNotesRange}from"./chunk-9ckvdy0j.js";import"./chunk-zsfyj36g.js";import"./chunk-2r1fgng9.js";import"./chunk-2brn8e1y.js";import"./chunk-7mwvvj46.js";import{loadStateFile,replaceStateFile,updateStateFile}from"./chunk-32j6y5d1.js";import"./chunk-mjyhy152.js";import{DEFAULT_LOCALE,LOCALES,isLocaleId}from"./chunk-wzss4pzh.js";import"./chunk-cmp64q4j.js";import{RoveDaemonClient,isDaemonVersionStale}from"./chunk-qzzmd63x.js";import{MOD_INPUT_MAX,MOD_KEY_PATTERN,MOD_PANE_ID_PATTERN,ROVE_API_VERSION}from"./chunk-96kvwzj8.js";import"./chunk-7bakcyes.js";import{roveCliInvocation}from"./chunk-8qkyd589.js";import"./chunk-7kysxgbq.js";import"./chunk-hz1x3hak.js";import{MULTI_REPO_CAP,rollupPr}from"./chunk-x1jxxmtc.js";import{DEFAULT_TASK_VENDOR}from"./chunk-xy8be792.js";import{ALL_VENDORS,isBuiltinVendor}from"./chunk-h2m7em1e.js";import"./chunk-7ghwdjdc.js";import"./chunk-8t7qbr1e.js";import"./chunk-bejhca6z.js";import"./chunk-br8r72bf.js";import"./chunk-yt15y085.js";import{defaultUiPrefsStatePath,promptAttachmentsDir,resolveProductHomeDir,roveStateDir}from"./chunk-z1nj22va.js";import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import"./chunk-0g4wc808.js";import{samePath}from"./chunk-pn5ygqn6.js";import{defaultDaemonSocketPath,defaultPtyHostSocketPath}from"./chunk-eqdbfgq5.js";import"./chunk-f8gg8cqh.js";import"./chunk-nc46gddg.js";import"./chunk-pxk2c2vj.js";import{__commonJS,__require,__toESM}from"./chunk-91sykzqq.js";var require_ms=__commonJS((exports,module)=>{var s=1000,m=s*60,h=m*60,d=h*24,w=d*7,y=d*365.25;module.exports=function(val,options){options=options||{};var type=typeof val;if(type==="string"&&val.length>0)return parse(val);else if(type==="number"&&isFinite(val))return options.long?fmtLong(val):fmtShort(val);throw Error("val is not a non-empty string or a valid number. val="+JSON.stringify(val))};function parse(str){if(str=String(str),str.length>100)return;var match=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(str);if(!match)return;var n=parseFloat(match[1]),type=(match[2]||"ms").toLowerCase();switch(type){case"years":case"year":case"yrs":case"yr":case"y":return n*y;case"weeks":case"week":case"w":return n*w;case"days":case"day":case"d":return n*d;case"hours":case"hour":case"hrs":case"hr":case"h":return n*h;case"minutes":case"minute":case"mins":case"min":case"m":return n*m;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}function fmtShort(ms){var msAbs=Math.abs(ms);if(msAbs>=d)return Math.round(ms/d)+"d";if(msAbs>=h)return Math.round(ms/h)+"h";if(msAbs>=m)return Math.round(ms/m)+"m";if(msAbs>=s)return Math.round(ms/s)+"s";return ms+"ms"}function fmtLong(ms){var msAbs=Math.abs(ms);if(msAbs>=d)return plural(ms,msAbs,d,"day");if(msAbs>=h)return plural(ms,msAbs,h,"hour");if(msAbs>=m)return plural(ms,msAbs,m,"minute");if(msAbs>=s)return plural(ms,msAbs,s,"second");return ms+" ms"}function plural(ms,msAbs,n,name){var isPlural=msAbs>=n*1.5;return Math.round(ms/n)+" "+name+(isPlural?"s":"")}});var require_common=__commonJS((exports,module)=>{function setup(env){createDebug.debug=createDebug,createDebug.default=createDebug,createDebug.coerce=coerce,createDebug.disable=disable,createDebug.enable=enable,createDebug.enabled=enabled,createDebug.humanize=require_ms(),createDebug.destroy=destroy,Object.keys(env).forEach((key)=>{createDebug[key]=env[key]}),createDebug.names=[],createDebug.skips=[],createDebug.formatters={};function selectColor(namespace){let hash=0;for(let i=0;i<namespace.length;i++)hash=(hash<<5)-hash+namespace.charCodeAt(i),hash|=0;return createDebug.colors[Math.abs(hash)%createDebug.colors.length]}createDebug.selectColor=selectColor;function createDebug(namespace){let prevTime,enableOverride=null,namespacesCache,enabledCache;function debug(...args){if(!debug.enabled)return;let self=debug,curr=Number(new Date),ms=curr-(prevTime||curr);if(self.diff=ms,self.prev=prevTime,self.curr=curr,prevTime=curr,args[0]=createDebug.coerce(args[0]),typeof args[0]!=="string")args.unshift("%O");let index=0;args[0]=args[0].replace(/%([a-zA-Z%])/g,(match,format)=>{if(match==="%%")return"%";index++;let formatter=createDebug.formatters[format];if(typeof formatter==="function"){let val=args[index];match=formatter.call(self,val),args.splice(index,1),index--}return match}),createDebug.formatArgs.call(self,args),(self.log||createDebug.log).apply(self,args)}if(debug.namespace=namespace,debug.useColors=createDebug.useColors(),debug.color=createDebug.selectColor(namespace),debug.extend=extend,debug.destroy=createDebug.destroy,Object.defineProperty(debug,"enabled",{enumerable:!0,configurable:!1,get:()=>{if(enableOverride!==null)return enableOverride;if(namespacesCache!==createDebug.namespaces)namespacesCache=createDebug.namespaces,enabledCache=createDebug.enabled(namespace);return enabledCache},set:(v)=>{enableOverride=v}}),typeof createDebug.init==="function")createDebug.init(debug);return debug}function extend(namespace,delimiter){let newDebug=createDebug(this.namespace+(typeof delimiter>"u"?":":delimiter)+namespace);return newDebug.log=this.log,newDebug}function enable(namespaces){createDebug.save(namespaces),createDebug.namespaces=namespaces,createDebug.names=[],createDebug.skips=[];let split=(typeof namespaces==="string"?namespaces:"").trim().replace(/\s+/g,",").split(",").filter(Boolean);for(let ns of split)if(ns[0]==="-")createDebug.skips.push(ns.slice(1));else createDebug.names.push(ns)}function matchesTemplate(search,template){let searchIndex=0,templateIndex=0,starIndex=-1,matchIndex=0;while(searchIndex<search.length)if(templateIndex<template.length&&(template[templateIndex]===search[searchIndex]||template[templateIndex]==="*"))if(template[templateIndex]==="*")starIndex=templateIndex,matchIndex=searchIndex,templateIndex++;else searchIndex++,templateIndex++;else if(starIndex!==-1)templateIndex=starIndex+1,matchIndex++,searchIndex=matchIndex;else return!1;while(templateIndex<template.length&&template[templateIndex]==="*")templateIndex++;return templateIndex===template.length}function disable(){let namespaces=[...createDebug.names,...createDebug.skips.map((namespace)=>"-"+namespace)].join(",");return createDebug.enable(""),namespaces}function enabled(name){for(let skip of createDebug.skips)if(matchesTemplate(name,skip))return!1;for(let ns of createDebug.names)if(matchesTemplate(name,ns))return!0;return!1}function coerce(val){if(val instanceof Error)return val.stack||val.message;return val}function destroy(){console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")}return createDebug.enable(createDebug.load()),createDebug}module.exports=setup});var require_browser=__commonJS((exports,module)=>{exports.formatArgs=formatArgs;exports.save=save;exports.load=load;exports.useColors=useColors;exports.storage=localstorage();exports.destroy=(()=>{let warned=!1;return()=>{if(!warned)warned=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.")}})();exports.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"];function useColors(){if(typeof window<"u"&&window.process&&(window.process.type==="renderer"||window.process.__nwjs))return!0;if(typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let m;return typeof document<"u"&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<"u"&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<"u"&&navigator.userAgent&&(m=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(m[1],10)>=31||typeof navigator<"u"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}function formatArgs(args){if(args[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+args[0]+(this.useColors?"%c ":" ")+"+"+module.exports.humanize(this.diff),!this.useColors)return;let c="color: "+this.color;args.splice(1,0,c,"color: inherit");let index=0,lastC=0;args[0].replace(/%[a-zA-Z%]/g,(match)=>{if(match==="%%")return;if(index++,match==="%c")lastC=index}),args.splice(lastC,0,c)}exports.log=console.debug||console.log||(()=>{});function save(namespaces){try{if(namespaces)exports.storage.setItem("debug",namespaces);else exports.storage.removeItem("debug")}catch(error){}}function load(){let r;try{r=exports.storage.getItem("debug")||exports.storage.getItem("DEBUG")}catch(error){}if(!r&&typeof process<"u"&&"env"in process)r=process.env.DEBUG;return r}function localstorage(){try{return localStorage}catch(error){}}module.exports=require_common()(exports);var{formatters}=module.exports;formatters.j=function(v){try{return JSON.stringify(v)}catch(error){return"[UnexpectedJSONParseError]: "+error.message}}});var require_has_flag=__commonJS((exports,module)=>{module.exports=(flag,argv=process.argv)=>{let prefix=flag.startsWith("-")?"":flag.length===1?"-":"--",position=argv.indexOf(prefix+flag),terminatorPosition=argv.indexOf("--");return position!==-1&&(terminatorPosition===-1||position<terminatorPosition)}});var require_supports_color=__commonJS((exports,module)=>{var os=__require("os"),tty=__require("tty"),hasFlag=require_has_flag(),{env}=process,forceColor;if(hasFlag("no-color")||hasFlag("no-colors")||hasFlag("color=false")||hasFlag("color=never"))forceColor=0;else if(hasFlag("color")||hasFlag("colors")||hasFlag("color=true")||hasFlag("color=always"))forceColor=1;if("FORCE_COLOR"in env)if(env.FORCE_COLOR==="true")forceColor=1;else if(env.FORCE_COLOR==="false")forceColor=0;else forceColor=env.FORCE_COLOR.length===0?1:Math.min(parseInt(env.FORCE_COLOR,10),3);function translateLevel(level){if(level===0)return!1;return{level,hasBasic:!0,has256:level>=2,has16m:level>=3}}function supportsColor(haveStream,streamIsTTY){if(forceColor===0)return 0;if(hasFlag("color=16m")||hasFlag("color=full")||hasFlag("color=truecolor"))return 3;if(hasFlag("color=256"))return 2;if(haveStream&&!streamIsTTY&&forceColor===void 0)return 0;let min=forceColor||0;if(env.TERM==="dumb")return min;if(process.platform==="win32"){let osRelease=os.release().split(".");if(Number(osRelease[0])>=10&&Number(osRelease[2])>=10586)return Number(osRelease[2])>=14931?3:2;return 1}if("CI"in env){if(["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI","GITHUB_ACTIONS","BUILDKITE"].some((sign)=>(sign in env))||env.CI_NAME==="codeship")return 1;return min}if("TEAMCITY_VERSION"in env)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION)?1:0;if(env.COLORTERM==="truecolor")return 3;if("TERM_PROGRAM"in env){let version=parseInt((env.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(env.TERM_PROGRAM){case"iTerm.app":return version>=3?3:2;case"Apple_Terminal":return 2}}if(/-256(color)?$/i.test(env.TERM))return 2;if(/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM))return 1;if("COLORTERM"in env)return 1;return min}function getSupportLevel(stream){let level=supportsColor(stream,stream&&stream.isTTY);return translateLevel(level)}module.exports={supportsColor:getSupportLevel,stdout:translateLevel(supportsColor(!0,tty.isatty(1))),stderr:translateLevel(supportsColor(!0,tty.isatty(2)))}});var require_node=__commonJS((exports,module)=>{var tty=__require("tty"),util=__require("util");exports.init=init;exports.log=log;exports.formatArgs=formatArgs;exports.save=save;exports.load=load;exports.useColors=useColors;exports.destroy=util.deprecate(()=>{},"Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");exports.colors=[6,2,3,4,5,1];try{let supportsColor=require_supports_color();if(supportsColor&&(supportsColor.stderr||supportsColor).level>=2)exports.colors=[20,21,26,27,32,33,38,39,40,41,42,43,44,45,56,57,62,63,68,69,74,75,76,77,78,79,80,81,92,93,98,99,112,113,128,129,134,135,148,149,160,161,162,163,164,165,166,167,168,169,170,171,172,173,178,179,184,185,196,197,198,199,200,201,202,203,204,205,206,207,208,209,214,215,220,221]}catch(error){}exports.inspectOpts=Object.keys(process.env).filter((key)=>{return/^debug_/i.test(key)}).reduce((obj,key)=>{let prop=key.substring(6).toLowerCase().replace(/_([a-z])/g,(_,k)=>{return k.toUpperCase()}),val=process.env[key];if(/^(yes|on|true|enabled)$/i.test(val))val=!0;else if(/^(no|off|false|disabled)$/i.test(val))val=!1;else if(val==="null")val=null;else val=Number(val);return obj[prop]=val,obj},{});function useColors(){return"colors"in exports.inspectOpts?Boolean(exports.inspectOpts.colors):tty.isatty(process.stderr.fd)}function formatArgs(args){let{namespace:name,useColors:useColors2}=this;if(useColors2){let c=this.color,colorCode="\x1B[3"+(c<8?c:"8;5;"+c),prefix=` ${colorCode};1m${name} \x1B[0m`;args[0]=prefix+args[0].split(`
|
|
3
|
+
`).join(`
|
|
4
|
+
`+prefix),args.push(colorCode+"m+"+module.exports.humanize(this.diff)+"\x1B[0m")}else args[0]=getDate()+name+" "+args[0]}function getDate(){if(exports.inspectOpts.hideDate)return"";return new Date().toISOString()+" "}function log(...args){return process.stderr.write(util.formatWithOptions(exports.inspectOpts,...args)+`
|
|
5
|
+
`)}function save(namespaces){if(namespaces)process.env.DEBUG=namespaces;else delete process.env.DEBUG}function load(){return process.env.DEBUG}function init(debug){debug.inspectOpts={};let keys=Object.keys(exports.inspectOpts);for(let i=0;i<keys.length;i++)debug.inspectOpts[keys[i]]=exports.inspectOpts[keys[i]]}module.exports=require_common()(exports);var{formatters}=module.exports;formatters.o=function(v){return this.inspectOpts.colors=this.useColors,util.inspect(v,this.inspectOpts).split(`
|
|
6
|
+
`).map((str)=>str.trim()).join(" ")};formatters.O=function(v){return this.inspectOpts.colors=this.useColors,util.inspect(v,this.inspectOpts)}});var require_src=__commonJS((exports,module)=>{if(typeof process>"u"||process.type==="renderer"||!1||process.__nwjs)module.exports=require_browser();else module.exports=require_node()});import{spawnSync}from"child_process";import{copyFileSync,existsSync as existsSync5,mkdirSync as mkdirSync4,readFileSync as readFileSync4}from"fs";import{hostname as osHostname2}from"os";import{join as join6}from"path";import WebSocket2 from"ws";var import_debug2=__toESM(require_src(),1);import*as net2 from"net";import*as tls from"tls";import assert from"assert";import*as net from"net";import*as http from"http";import{Agent as HttpsAgent}from"https";var INTERNAL=Symbol("AgentBaseInternalState");class Agent2 extends http.Agent{constructor(opts){super(opts);this[INTERNAL]={}}isSecureEndpoint(options){if(options){if(typeof options.secureEndpoint==="boolean")return options.secureEndpoint;if(typeof options.protocol==="string")return options.protocol==="https:"}let{stack}=Error();if(typeof stack!=="string")return!1;return stack.split(`
|
|
7
|
+
`).some((l)=>l.indexOf("(https.js:")!==-1||l.indexOf("node:https:")!==-1)}incrementSockets(name){if(this.maxSockets===1/0&&this.maxTotalSockets===1/0)return null;if(!this.sockets[name])this.sockets[name]=[];let fakeSocket=new net.Socket({writable:!1});return this.sockets[name].push(fakeSocket),this.totalSocketCount++,fakeSocket}decrementSockets(name,socket){if(!this.sockets[name]||socket===null)return;let sockets=this.sockets[name],index=sockets.indexOf(socket);if(index!==-1){if(sockets.splice(index,1),this.totalSocketCount--,sockets.length===0)delete this.sockets[name]}}getName(options){if(this.isSecureEndpoint(options))return HttpsAgent.prototype.getName.call(this,options);return super.getName(options)}createSocket(req,options,cb){let connectOpts={...options,secureEndpoint:this.isSecureEndpoint(options)},name=this.getName(connectOpts),fakeSocket=this.incrementSockets(name);Promise.resolve().then(()=>this.connect(req,connectOpts)).then((socket)=>{if(this.decrementSockets(name,fakeSocket),typeof socket.addRequest==="function")try{return socket.addRequest(req,connectOpts)}catch(err){return cb(err)}this[INTERNAL].currentSocket=socket,super.createSocket(req,options,cb)},(err)=>{this.decrementSockets(name,fakeSocket),cb(err)})}createConnection(){let socket=this[INTERNAL].currentSocket;if(this[INTERNAL].currentSocket=void 0,!socket)throw Error("No socket was returned in the `connect()` function");return socket}get defaultPort(){return this[INTERNAL].defaultPort??(this.protocol==="https:"?443:80)}set defaultPort(v){if(this[INTERNAL])this[INTERNAL].defaultPort=v}get protocol(){return this[INTERNAL].protocol??(this.isSecureEndpoint()?"https:":"http:")}set protocol(v){if(this[INTERNAL])this[INTERNAL].protocol=v}}import{URL as URL2}from"url";var import_debug=__toESM(require_src(),1),debug=import_debug.default("https-proxy-agent:parse-proxy-response");function parseProxyResponse(socket){return new Promise((resolve,reject)=>{let buffersLength=0,buffers=[];function read(){let b=socket.read();if(b)ondata(b);else socket.once("readable",read)}function cleanup(){socket.removeListener("end",onend),socket.removeListener("error",onerror),socket.removeListener("readable",read)}function onend(){cleanup(),debug("onend"),reject(Error("Proxy connection ended before receiving CONNECT response"))}function onerror(err){cleanup(),debug("onerror %o",err),reject(err)}function ondata(b){buffers.push(b),buffersLength+=b.length;let buffered=Buffer.concat(buffers,buffersLength),endOfHeaders=buffered.indexOf(`\r
|
|
8
|
+
\r
|
|
9
|
+
`);if(endOfHeaders===-1){debug("have not received end of HTTP headers yet..."),read();return}let headerParts=buffered.slice(0,endOfHeaders).toString("ascii").split(`\r
|
|
10
|
+
`),firstLine=headerParts.shift();if(!firstLine)return socket.destroy(),reject(Error("No header received from proxy CONNECT response"));let firstLineParts=firstLine.split(" "),statusCode=+firstLineParts[1],statusText=firstLineParts.slice(2).join(" "),headers={};for(let header of headerParts){if(!header)continue;let firstColon=header.indexOf(":");if(firstColon===-1)return socket.destroy(),reject(Error(`Invalid header from proxy CONNECT response: "${header}"`));let key=header.slice(0,firstColon).toLowerCase(),value=header.slice(firstColon+1).trimStart(),current=headers[key];if(typeof current==="string")headers[key]=[current,value];else if(Array.isArray(current))current.push(value);else headers[key]=value}debug("got proxy server response: %o %o",firstLine,headers),cleanup(),resolve({connect:{statusCode,statusText,headers},buffered})}socket.on("error",onerror),socket.on("end",onend),read()})}function createNegotiateAuth(){return async({response,scheme})=>{if(scheme.toLowerCase()!=="negotiate")throw Error(`Expected Negotiate scheme but got "${scheme}"`);let kerberos;try{kerberos=await import("kerberos")}catch{throw Error('The "kerberos" package is required for Negotiate proxy authentication. Install it with: npm install kerberos')}let proxyAuthenticate=response.headers["proxy-authenticate"]||"",challengeHeader=Array.isArray(proxyAuthenticate)?proxyAuthenticate[0]:proxyAuthenticate,serverToken=typeof challengeHeader==="string"&&challengeHeader.includes(" ")?challengeHeader.split(" ").slice(1).join(" "):void 0,token=await(await kerberos.initializeClient("HTTP@proxy",{mechOID:kerberos.GSS_MECH_OID_SPNEGO})).step(serverToken||"");if(!token)throw Error("Kerberos client.step() returned no token");return{headers:{"Proxy-Authorization":`Negotiate ${token}`}}}}var debug2=import_debug2.default("https-proxy-agent"),setServernameFromNonIpHost=(options)=>{if(options.servername===void 0&&options.host&&!net2.isIP(options.host))return{...options,servername:options.host};return options};class HttpsProxyAgent extends Agent2{constructor(proxy,opts){super(opts);if(this.options={path:void 0},this.proxy=typeof proxy==="string"?new URL2(proxy):proxy,this.proxyHeaders=opts?.headers??{},debug2("Creating new HttpsProxyAgent instance: %o",this.proxy.href),opts?.negotiate)this.onProxyAuth=createNegotiateAuth();else if(opts?.onProxyAuth)this.onProxyAuth=opts.onProxyAuth;let host=(this.proxy.hostname||this.proxy.host).replace(/^\[|\]$/g,""),port=this.proxy.port?parseInt(this.proxy.port,10):this.proxy.protocol==="https:"?443:80;this.connectOpts={ALPNProtocols:["http/1.1"],...opts?omit(opts,"headers","onProxyAuth","negotiate"):null,host,port}}async connect(req,opts){let{proxy}=this;if(!opts.host)throw TypeError('No "host" provided');let socket;if(proxy.protocol==="https:")debug2("Creating `tls.Socket`: %o",this.connectOpts),socket=tls.connect(setServernameFromNonIpHost(this.connectOpts));else debug2("Creating `net.Socket`: %o",this.connectOpts),socket=net2.connect(this.connectOpts);let headers=typeof this.proxyHeaders==="function"?this.proxyHeaders():{...this.proxyHeaders},host=net2.isIPv6(opts.host)?`[${opts.host}]`:opts.host,payload=`CONNECT ${host}:${opts.port} HTTP/1.1\r
|
|
11
|
+
`;if(proxy.username||proxy.password){let auth=`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;headers["Proxy-Authorization"]=`Basic ${Buffer.from(auth).toString("base64")}`}if(headers.Host=`${host}:${opts.port}`,!headers["Proxy-Connection"])headers["Proxy-Connection"]=this.keepAlive?"Keep-Alive":"close";for(let name of Object.keys(headers))payload+=`${name}: ${headers[name]}\r
|
|
12
|
+
`;let proxyResponsePromise=parseProxyResponse(socket);socket.write(`${payload}\r
|
|
13
|
+
`);let{connect:connect3,buffered}=await proxyResponsePromise;if(req.emit("proxyConnect",connect3),this.emit("proxyConnect",connect3,req),req.emit("proxy",{proxy:this.proxy.href,socket}),connect3.statusCode===200){if(req.once("socket",resume),opts.secureEndpoint)return debug2("Upgrading socket connection to TLS"),tls.connect({...omit(setServernameFromNonIpHost(opts),"host","path","port"),socket});return socket}if(connect3.statusCode===407&&this.onProxyAuth){debug2("Got 407 response, invoking onProxyAuth callback"),socket.destroy();let proxyAuthenticate=connect3.headers["proxy-authenticate"]||"",scheme=Array.isArray(proxyAuthenticate)?proxyAuthenticate[0].split(/\s/)[0]:proxyAuthenticate.split(/\s/)[0],authResponse=await this.onProxyAuth({response:connect3,scheme});return this._connectWithAuth(req,opts,authResponse.headers)}socket.destroy();let fakeSocket=new net2.Socket({writable:!1});return fakeSocket.readable=!0,req.once("socket",(s)=>{debug2("Replaying proxy buffer for failed request"),assert(s.listenerCount("data")>0),s.push(buffered),s.push(null)}),fakeSocket}async _connectWithAuth(req,opts,authHeaders){let{proxy}=this,socket;if(proxy.protocol==="https:")socket=tls.connect(setServernameFromNonIpHost(this.connectOpts));else socket=net2.connect(this.connectOpts);let headers=typeof this.proxyHeaders==="function"?this.proxyHeaders():{...this.proxyHeaders},host=net2.isIPv6(opts.host)?`[${opts.host}]`:opts.host,payload=`CONNECT ${host}:${opts.port} HTTP/1.1\r
|
|
14
|
+
`;if(proxy.username||proxy.password){let auth=`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;headers["Proxy-Authorization"]=`Basic ${Buffer.from(auth).toString("base64")}`}if(Object.assign(headers,authHeaders),headers.Host=`${host}:${opts.port}`,!headers["Proxy-Connection"])headers["Proxy-Connection"]=this.keepAlive?"Keep-Alive":"close";for(let name of Object.keys(headers))payload+=`${name}: ${headers[name]}\r
|
|
15
|
+
`;let proxyResponsePromise=parseProxyResponse(socket);socket.write(`${payload}\r
|
|
16
|
+
`);let{connect:connect3}=await proxyResponsePromise;if(req.emit("proxyConnect",connect3),this.emit("proxyConnect",connect3,req),connect3.statusCode===200){if(req.once("socket",resume),opts.secureEndpoint)return debug2("Upgrading socket connection to TLS"),tls.connect({...omit(setServernameFromNonIpHost(opts),"host","path","port"),socket});return socket}throw socket.destroy(),Error(`Proxy authentication failed with status ${connect3.statusCode} after retry`)}}HttpsProxyAgent.protocols=["http","https"];function resume(socket){setImmediate(()=>{socket.resume()})}function omit(obj,...keys){let ret={},key;for(key in obj)if(!keys.includes(key))ret[key]=obj[key];return ret}var subtle=(globalThis.crypto??globalThis.crypto).subtle,te=new TextEncoder;function b64uEncode(bytes){let s="";for(let i=0;i<bytes.length;i+=32768)s+=String.fromCharCode(...bytes.subarray(i,i+32768));return btoa(s).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function b64uDecode(s){let t=s.replace(/-/g,"+").replace(/_/g,"/"),raw=atob(t+"=".repeat((4-t.length%4)%4)),out=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)out[i]=raw.charCodeAt(i);return out}function concatBytes(...parts){let total=parts.reduce((n,p)=>n+p.length,0),out=new Uint8Array(total),o=0;for(let p of parts)out.set(p,o),o+=p.length;return out}function timingSafeEqual(a,b){if(a.length!==b.length)return!1;let diff=0;for(let i=0;i<a.length;i++)diff|=a[i]^b[i];return diff===0}function ab(view){return view.buffer.slice(view.byteOffset,view.byteOffset+view.byteLength)}async function sha256(input){return new Uint8Array(await subtle.digest("SHA-256",ab(input)))}async function sha256Hex(s){let d=await sha256(te.encode(s));return Array.from(d,(b)=>b.toString(16).padStart(2,"0")).join("")}async function deriveRoutingToken(code){let key=await subtle.importKey("raw",ab(te.encode(code)),"HKDF",!1,["deriveBits"]),bits=await subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:te.encode("relay-e2e/v1/routing"),info:new Uint8Array(0)},key,128);return b64uEncode(new Uint8Array(bits))}async function generateEphemeral(){let pair=await subtle.generateKey({name:"ECDH",namedCurve:"P-256"},!1,["deriveBits"]);return{privateKey:pair.privateKey,publicKeyRaw:new Uint8Array(await subtle.exportKey("raw",pair.publicKey))}}async function hkdfWithLabel(shared,salt,label,transcript){let key=await subtle.importKey("raw",ab(shared),"HKDF",!1,["deriveBits"]);return new Uint8Array(await subtle.deriveBits({name:"HKDF",hash:"SHA-256",salt:ab(salt),info:ab(concatBytes(te.encode(label),transcript))},key,256))}async function deriveSessionKeys(p){let peer=await subtle.importKey("raw",ab(p.peerPublicKeyRaw),{name:"ECDH",namedCurve:"P-256"},!1,[]),shared=new Uint8Array(await subtle.deriveBits({name:"ECDH",public:peer},p.privateKey,256)),transcript=await sha256(concatBytes(te.encode("relay-e2e/v1"),p.initiatorPublicKeyRaw,p.responderPublicKeyRaw,p.initiatorNonce,p.responderNonce)),salt=te.encode(p.code),[c2s,s2c,confirmR,confirmI]=await Promise.all([hkdfWithLabel(shared,salt,"c2s|",transcript),hkdfWithLabel(shared,salt,"s2c|",transcript),hkdfWithLabel(shared,salt,"confirm-r|",transcript),hkdfWithLabel(shared,salt,"confirm-i|",transcript)]);return{c2s:await subtle.importKey("raw",ab(c2s),"AES-GCM",!1,["encrypt","decrypt"]),s2c:await subtle.importKey("raw",ab(s2c),"AES-GCM",!1,["encrypt","decrypt"]),confirmResponder:confirmR,confirmInitiator:confirmI}}class GcmStream{#counter=0n;#lastReceived=null;#key;constructor(key){this.#key=key}async seal(plaintext){let iv=new Uint8Array(12);new DataView(iv.buffer).setBigUint64(4,this.#counter++);let ct=new Uint8Array(await subtle.encrypt({name:"AES-GCM",iv:ab(iv)},this.#key,ab(plaintext)));return concatBytes(iv,ct)}async open(frame){if(frame.length<12)throw Error("E2E frame too short");let iv=frame.subarray(0,12),ct=frame.subarray(12),counter=new DataView(iv.buffer,iv.byteOffset+4,8).getBigUint64(0);if(this.#lastReceived!==null&&counter<=this.#lastReceived)throw Error(`E2E replay detected (counter ${counter} <= ${this.#lastReceived})`);return this.#lastReceived=counter,new Uint8Array(await subtle.decrypt({name:"AES-GCM",iv:ab(iv)},this.#key,ab(ct)))}}var E2E_PATH="/__alma_e2e__";class E2ESession{#role;#code;#send;#onReady;#onFrame;#onError;#eph=null;#myNonce=crypto.getRandomValues(new Uint8Array(16));#keys=null;#sendStream=null;#recvStream=null;#state="init";#outbox=[];#outgoing=Promise.resolve();#incoming=Promise.resolve();#enc=new TextEncoder;#dec=new TextDecoder;constructor(opts){this.#role=opts.role,this.#code=opts.code,this.#send=opts.send,this.#onReady=opts.onReady,this.#onFrame=opts.onFrame,this.#onError=opts.onError}async start(){if(this.#role!=="initiator")return;this.#eph=await generateEphemeral(),this.#state="await-hi",this.#send(JSON.stringify({t:"hi",epk:b64uEncode(this.#eph.publicKeyRaw),n:b64uEncode(this.#myNonce)}))}isReady(){return this.#state==="ready"}sendFrame(frame){if(this.#state==="failed")return Promise.resolve();if(this.#state!=="ready"||!this.#sendStream)return this.#outbox.push(frame),Promise.resolve();let p=this.#outgoing.then(async()=>{if(this.#state==="failed"||!this.#sendStream)throw Error("E2E session failed");let wire=await this.#sendStream.seal(this.#enc.encode(JSON.stringify(frame)));this.#send(wire)});return this.#outgoing=p.catch((e)=>this.#fail(e instanceof Error?e.message:String(e))),p}onMessage(data){return this.#incoming=this.#incoming.then(()=>this.#consume(data)).catch(()=>{}),this.#incoming}async#consume(data){if(this.#state==="failed")return;try{if(typeof data==="string")await this.#onHandshake(JSON.parse(data));else{if(!this.#recvStream)return;let plain=await this.#recvStream.open(data);this.#onFrame(JSON.parse(this.#dec.decode(plain)))}}catch(e){this.#fail(e instanceof Error?e.message:String(e))}}async#onHandshake(msg){if(this.#role==="responder"&&msg.t==="hi"&&this.#state==="init"){let peerPub=b64uDecode(String(msg.epk)),peerNonce=b64uDecode(String(msg.n));this.#eph=await generateEphemeral(),this.#keys=await deriveSessionKeys({privateKey:this.#eph.privateKey,peerPublicKeyRaw:peerPub,code:this.#code,initiatorPublicKeyRaw:peerPub,responderPublicKeyRaw:this.#eph.publicKeyRaw,initiatorNonce:peerNonce,responderNonce:this.#myNonce}),this.#state="await-ok",this.#send(JSON.stringify({t:"hi",epk:b64uEncode(this.#eph.publicKeyRaw),n:b64uEncode(this.#myNonce),cr:b64uEncode(this.#keys.confirmResponder)}));return}if(this.#role==="initiator"&&msg.t==="hi"&&this.#state==="await-hi"){let peerPub=b64uDecode(String(msg.epk)),peerNonce=b64uDecode(String(msg.n));if(this.#keys=await deriveSessionKeys({privateKey:this.#eph.privateKey,peerPublicKeyRaw:peerPub,code:this.#code,initiatorPublicKeyRaw:this.#eph.publicKeyRaw,responderPublicKeyRaw:peerPub,initiatorNonce:this.#myNonce,responderNonce:peerNonce}),!timingSafeEqual(b64uDecode(String(msg.cr)),this.#keys.confirmResponder)){this.#send(JSON.stringify({t:"err",message:"auth-failed"})),this.#fail("Handshake authentication failed (wrong code or man-in-the-middle)");return}this.#send(JSON.stringify({t:"ok",ci:b64uEncode(this.#keys.confirmInitiator)})),this.#sendStream=new GcmStream(this.#keys.c2s),this.#recvStream=new GcmStream(this.#keys.s2c),await this.#markReady();return}if(this.#role==="responder"&&msg.t==="ok"&&this.#state==="await-ok"){if(!timingSafeEqual(b64uDecode(String(msg.ci)),this.#keys.confirmInitiator)){this.#fail("Handshake authentication failed (wrong code or man-in-the-middle)");return}this.#sendStream=new GcmStream(this.#keys.s2c),this.#recvStream=new GcmStream(this.#keys.c2s),await this.#markReady();return}if(msg.t==="err")this.#fail(`Peer rejected handshake: ${String(msg.message||"unknown")}`)}async#markReady(){this.#state="ready",this.#onReady();let pending=this.#outbox;this.#outbox=[];for(let f of pending)await this.sendFrame(f)}#fail(message){if(this.#state!=="failed")this.#state="failed",this.#onError(message)}}import WebSocket from"ws";var CHUNK_SIZE=32768;class E2EResponder{#session;#localPort;#httpAborts=new Map;#localSockets=new Map;#destroyed=!1;#sendRaw;constructor(opts){this.#localPort=opts.localPort,this.#sendRaw=opts.send,this.#session=new E2ESession({role:"responder",code:opts.code,send:(data)=>{if(!this.#destroyed)this.#sendRaw(data)},onReady:()=>{},onFrame:(frame)=>this.#onInnerFrame(frame),onError:(message)=>{opts.onError?.(message),this.destroy()}})}onMessage(data){return this.#session.onMessage(data)}destroy(){if(this.#destroyed)return;this.#destroyed=!0;for(let a of this.#httpAborts.values())a.abort();this.#httpAborts.clear();for(let s of this.#localSockets.values())try{s.close()}catch{}this.#localSockets.clear()}#send(frame){if(!this.#destroyed)this.#session.sendFrame(frame)}#onInnerFrame(frame){switch(frame.t){case"hreq":this.#proxyHttp(frame);break;case"hcancel":{let a=this.#httpAborts.get(String(frame.id));if(a)this.#httpAborts.delete(String(frame.id)),a.abort();break}case"wopen":this.#proxyWsOpen(String(frame.id),String(frame.path||"/"));break;case"wmsg":{let id=String(frame.id),ws=this.#localSockets.get(id);if(!ws||ws.readyState!==WebSocket.OPEN)return;if(typeof frame.s==="string")ws.send(frame.s);else if(typeof frame.b==="string")ws.send(Buffer.from(frame.b,"base64"));break}case"wclose":{let id=String(frame.id),ws=this.#localSockets.get(id);if(!ws)return;this.#localSockets.delete(id);try{ws.close(frame.code??1000,frame.reason)}catch{}break}}}async#proxyHttp(frame){let{id}=frame,abort=new AbortController;this.#httpAborts.set(String(id),abort);try{let headers={};for(let[k,v]of Object.entries(frame.headers||{})){let lower=k.toLowerCase();if(lower!=="host"&&lower!=="connection"&&lower!=="content-length"&&lower!=="accept-encoding"&&typeof v==="string")headers[lower]=v}headers["accept-encoding"]="identity";let method=(frame.method||"GET").toUpperCase(),body=frame.b&&method!=="GET"&&method!=="HEAD"?Buffer.from(frame.b,"base64"):void 0,res=await fetch(`http://127.0.0.1:${this.#localPort}${frame.path}`,{method,headers,body,signal:abort.signal,redirect:"manual"}),resHeaders={};if(res.headers.forEach((v,k)=>{let lower=k.toLowerCase();if(lower!=="connection"&&lower!=="keep-alive"&&lower!=="transfer-encoding")resHeaders[lower]=v}),this.#send({t:"hres",id,status:res.status,headers:resHeaders}),res.body){let reader=res.body.getReader();for(;;){let{done,value}=await reader.read();if(done)break;if(!this.#httpAborts.has(String(id)))return;for(let o=0;o<value.length;o+=CHUNK_SIZE)this.#send({t:"hchunk",id,b:Buffer.from(value.subarray(o,o+CHUNK_SIZE)).toString("base64")})}}this.#send({t:"hend",id})}catch(e){this.#send({t:"herr",id,message:abort.signal.aborted?"cancelled":e instanceof Error?e.message:String(e)})}finally{this.#httpAborts.delete(String(id))}}#proxyWsOpen(id,path){let ws;try{ws=new WebSocket(`ws://127.0.0.1:${this.#localPort}${path}`)}catch(e){this.#send({t:"werr",id,message:e instanceof Error?e.message:String(e)});return}this.#localSockets.set(id,ws),ws.on("open",()=>this.#send({t:"wopened",id})),ws.on("message",(data,isBinary)=>{if(isBinary){let buf=Buffer.isBuffer(data)?data:Buffer.from(data);this.#send({t:"wmsg",id,b:buf.toString("base64")})}else this.#send({t:"wmsg",id,s:data.toString()})}),ws.on("close",(code,reason)=>{if(this.#localSockets.has(id))this.#localSockets.delete(id),this.#send({t:"wclose",id,code,reason:reason?.toString()})}),ws.on("error",(e)=>{if(this.#localSockets.has(id)&&ws.readyState===WebSocket.CONNECTING)this.#localSockets.delete(id),this.#send({t:"werr",id,message:e.message})})}}function classifyDisconnect(code,errCode,error){if(errCode==="ECONNREFUSED")return"connection refused \u2014 wrong server URL/port, or relay down";if(errCode==="ENOTFOUND"||errCode==="EAI_AGAIN")return"DNS lookup failed \u2014 check the server hostname";if(errCode==="ETIMEDOUT")return"timed out reaching the relay \u2014 blocked or slow network path";if(errCode==="ECONNRESET"||errCode==="EPIPE")return"connection reset mid-path \u2014 proxy/firewall cut it; a retry usually recovers";if(errCode&&/CERT|TLS|EPROTO/.test(errCode))return"TLS failed \u2014 certificate problem or a TLS-intercepting middlebox";switch(code){case 1000:return"closed normally";case 1001:return"relay went away \u2014 server shutdown or redeploy";case 1006:return"socket died without a close frame \u2014 network switch, NAT idle timeout, or blocked path (a proxy may fix it)";case 1015:return"TLS handshake failed \u2014 TLS-intercepting middlebox or wrong scheme";case 4000:return"another agent took this pairing \u2014 reconnect() to reclaim it";case 1011:return"relay internal error"}if(code!==null&&code>=4000&&code<5000)return"relay policy rejection \u2014 see PROTOCOL.md \xA79";if(error)return"transport error \u2014 see the message above";return"unexpected close \u2014 see PROTOCOL.md \xA79"}var BASE_RECONNECT_MS=1000,MAX_RECONNECT_MS=30000,HEARTBEAT_MS=15000,BACKPRESSURE_BYTES=16777216,CHUNK_SIZE2=32768;function fmtDuration(ms){if(ms<1000)return`${ms} ms`;if(ms<60000)return`${(ms/1000).toFixed(1)} s`;return`${(ms/60000).toFixed(1)} min`}function hostOf(url){try{return new URL(url).host}catch{return url}}class RelayAgent{#config;#ws=null;#state="stopped";#lastError=null;#connectedAt=null;#stopped=!0;#reconnectTimer=null;#reconnectDelay=BASE_RECONNECT_MS;#heartbeatTimer=null;#httpAborts=new Map;#localSockets=new Map;#responders=new Map;#attempt=0;#connectStartedAt=null;#lastWsError=null;#lastErrCode=null;constructor(config){this.#config=config}getStatus(){return{state:this.#state,lastError:this.#lastError,connectedAt:this.#connectedAt,activeTunnels:this.#responders.size+this.#localSockets.size}}#setState(state){if(this.#state!==state){this.#state=state;try{this.#config.onStateChange?.(this.getStatus())}catch(e){console.warn("[RelayAgent] onStateChange threw:",e)}}}#diag(e){if(!this.#config.quiet)if(e.type==="connect")console.log(`[RelayAgent] connect attempt ${e.attempt} \u2192 ${hostOf(e.server)} (proxy: ${e.proxy??"none"})`);else if(e.type==="connected")console.log(`[RelayAgent] connected to relay server (attempt ${e.attempt}, ${e.ms} ms)`);else if(e.type==="disconnect"){let cause=e.error??`close ${e.code}`,retry=e.nextRetryMs!=null?` \u2014 retrying in ${e.nextRetryMs} ms`:"",lasted=e.lastedMs!=null?` after ${fmtDuration(e.lastedMs)}`:"";console.warn(`[RelayAgent] disconnected${lasted} (attempt ${e.attempt}): ${cause} \u2014 ${e.hint}${retry}`)}else if(e.type==="replaced"){let lasted=e.lastedMs!=null?` after ${fmtDuration(e.lastedMs)}`:"";console.warn(`[RelayAgent] disconnected${lasted}: ${e.hint}`)}else{let lasted=e.lastedMs!=null?` after ${fmtDuration(e.lastedMs)}`:"";console.warn(`[RelayAgent] relay stopped answering pings${lasted} \u2014 reconnecting`)}try{this.#config.onDiag?.(e)}catch(e2){console.warn("[RelayAgent] onDiag threw:",e2)}}#resolveProxy(){let p=this.#config.proxy??process.env.HTTPS_PROXY??process.env.https_proxy??process.env.HTTP_PROXY??process.env.http_proxy??null;return p&&p.trim()?p:null}start(){this.#stopped=!1,this.#connect()}reconnect(){if(this.#reconnectTimer)clearTimeout(this.#reconnectTimer),this.#reconnectTimer=null;if(this.#reconnectDelay=BASE_RECONNECT_MS,this.#attempt=0,this.#stopped=!1,this.#stopHeartbeat(),this.#ws){let old=this.#ws;this.#ws=null;try{old.close(1000,"Reconnecting")}catch{}}this.#teardownTunnels(),this.#connectedAt=null,this.#connect()}stop(){if(this.#stopped=!0,this.#stopHeartbeat(),this.#reconnectTimer)clearTimeout(this.#reconnectTimer),this.#reconnectTimer=null;if(this.#teardownTunnels(),this.#ws){try{this.#ws.close(1000,"Relay disabled")}catch{}this.#ws=null}this.#setState("stopped"),this.#connectedAt=null}async#connect(){if(this.#stopped)return;this.#setState("connecting"),this.#attempt+=1,this.#connectStartedAt=Date.now(),this.#diag({type:"connect",server:this.#config.serverUrl,proxy:this.#resolveProxy(),attempt:this.#attempt});try{let routingToken=await deriveRoutingToken(this.#config.code),tokenHash=await sha256Hex(routingToken);this.#openSocket(tokenHash)}catch(e){this.#onDisconnect(e instanceof Error?e.message:String(e))}}#openSocket(tokenHash){if(this.#stopped)return;this.#lastWsError=null,this.#lastErrCode=null;let base=this.#config.serverUrl.replace(/\/+$/,"").replace(/^http/,"ws"),proxy=this.#resolveProxy(),agent=proxy?new HttpsProxyAgent(proxy):void 0,ws=new WebSocket2(`${base}/agent?th=${tokenHash}`,{maxPayload:268435456,...agent?{agent}:{}});this.#ws=ws,ws.on("open",()=>{this.#startHeartbeat(ws),this.#send({t:"register",tokenHash,name:this.#config.deviceName||"relay-agent"})}),ws.on("message",(data)=>{let frame;try{frame=JSON.parse(data.toString())}catch{return}this.#handleFrame(frame)}),ws.on("close",(code,reason)=>{if(this.#ws!==ws)return;if(this.#ws=null,this.#stopHeartbeat(),code===4000){this.#onReplaced();return}let error=code===1000?null:this.#lastWsError??`Relay connection closed (${code}${reason?.length?`: ${reason}`:""})`;this.#onDisconnect(error,{code,reason:reason?.length?reason.toString():null,errCode:this.#lastErrCode})}),ws.on("error",(e)=>{this.#lastError=e.message,this.#lastWsError=e.message,this.#lastErrCode=e.code??null})}#startHeartbeat(ws){this.#stopHeartbeat();let missed=!1;ws.on("pong",()=>{missed=!1}),this.#heartbeatTimer=setInterval(()=>{if(this.#ws===ws&&ws.readyState===WebSocket2.OPEN){if(missed){this.#diag({type:"heartbeat-missed",lastedMs:this.#connectedAt?Date.now()-this.#connectedAt:null}),ws.terminate();return}missed=!0;try{ws.ping()}catch{}}},HEARTBEAT_MS),this.#heartbeatTimer.unref?.()}#stopHeartbeat(){if(this.#heartbeatTimer)clearInterval(this.#heartbeatTimer),this.#heartbeatTimer=null}#onReplaced(){this.#teardownTunnels();let lastedMs=this.#connectedAt?Date.now()-this.#connectedAt:null;if(this.#connectedAt=null,this.#diag({type:"replaced",hint:"another agent took over this pairing \u2014 call reconnect() to reclaim it",lastedMs}),this.#stopped)this.#setState("stopped");else this.#lastError="Another agent took over this pairing",this.#setState("replaced")}#onDisconnect(error,close=null){this.#teardownTunnels();let lastedMs=this.#connectedAt?Date.now()-this.#connectedAt:null;if(this.#connectedAt=null,error)this.#lastError=error,this.#setState("error");if(this.#diag({type:"disconnect",code:close?.code??null,reason:close?.reason??null,error,hint:classifyDisconnect(close?.code??null,close?.errCode??null,error),lastedMs,nextRetryMs:this.#stopped?null:this.#reconnectDelay,attempt:this.#attempt}),this.#stopped){this.#setState("stopped");return}if(this.#state!=="error")this.#setState("connecting");this.#reconnectTimer=setTimeout(()=>{this.#reconnectTimer=null,this.#connect()},this.#reconnectDelay),this.#reconnectDelay=Math.min(2*this.#reconnectDelay,MAX_RECONNECT_MS)}#teardownTunnels(){for(let a of this.#httpAborts.values())a.abort();this.#httpAborts.clear();for(let s of this.#localSockets.values())try{s.close()}catch{}this.#localSockets.clear();for(let r of this.#responders.values())r.destroy();this.#responders.clear()}#send(frame){if(this.#ws&&this.#ws.readyState===WebSocket2.OPEN)this.#ws.send(JSON.stringify(frame))}async#waitForDrain(){while(this.#ws&&this.#ws.readyState===WebSocket2.OPEN&&this.#ws.bufferedAmount>BACKPRESSURE_BYTES)await new Promise((r)=>setTimeout(r,50))}#handleFrame(frame){switch(frame.t){case"registered":this.#lastError=null,this.#connectedAt=Date.now(),this.#setState("connected"),this.#reconnectDelay=BASE_RECONNECT_MS,this.#diag({type:"connected",attempt:this.#attempt,ms:this.#connectStartedAt?Date.now()-this.#connectStartedAt:0}),this.#attempt=0;break;case"error":if(this.#lastError=String(frame.message||"Relay error"),this.#setState("error"),!this.#config.quiet)console.error("[RelayAgent] Relay rejected connection:",this.#lastError);break;case"hreq":this.#proxyHttp(frame);break;case"hcancel":{let a=this.#httpAborts.get(String(frame.id));if(a)this.#httpAborts.delete(String(frame.id)),a.abort();break}case"wopen":{let id=String(frame.id),path=String(frame.path||"/");if(path===E2E_PATH||path.startsWith(`${E2E_PATH}?`))this.#openE2EResponder(id);else this.#openLocalSocket(id,path);break}case"wmsg":{let id=String(frame.id),responder=this.#responders.get(id);if(responder){if(typeof frame.s==="string")responder.onMessage(frame.s);else if(typeof frame.b==="string")responder.onMessage(new Uint8Array(Buffer.from(frame.b,"base64")));return}let local=this.#localSockets.get(id);if(!local||local.readyState!==WebSocket2.OPEN)return;if(typeof frame.s==="string")local.send(frame.s);else if(typeof frame.b==="string")local.send(Buffer.from(frame.b,"base64"));break}case"wclose":{let id=String(frame.id),responder=this.#responders.get(id);if(responder){this.#responders.delete(id),responder.destroy();return}let local=this.#localSockets.get(id);if(!local)return;this.#localSockets.delete(id);try{local.close()}catch{}break}}}#openE2EResponder(id){let responder=new E2EResponder({code:this.#config.code,localPort:this.#config.localPort,send:(data)=>{if(typeof data==="string")this.#send({t:"wmsg",id,s:data});else this.#send({t:"wmsg",id,b:Buffer.from(data).toString("base64")})},onError:(e)=>console.error("[RelayAgent] E2E tunnel error:",e)});this.#responders.set(id,responder),this.#send({t:"wopened",id})}async#proxyHttp(frame){let{id}=frame,abort=new AbortController;this.#httpAborts.set(String(id),abort);try{let headers={};for(let[k,v]of Object.entries(frame.headers||{})){let lower=k.toLowerCase();if(lower!=="host"&&lower!=="connection"&&lower!=="content-length"&&lower!=="accept-encoding"&&typeof v==="string")headers[lower]=v}headers["accept-encoding"]="identity";let method=(frame.method||"GET").toUpperCase(),body=frame.b&&method!=="GET"&&method!=="HEAD"?Buffer.from(frame.b,"base64"):void 0,res=await fetch(`http://127.0.0.1:${this.#config.localPort}${frame.path}`,{method,headers,body,signal:abort.signal,redirect:"manual"}),resHeaders={};if(res.headers.forEach((v,k)=>{let lower=k.toLowerCase();if(lower!=="connection"&&lower!=="keep-alive"&&lower!=="transfer-encoding")resHeaders[lower]=v}),this.#send({t:"hres",id,status:res.status,headers:resHeaders}),res.body){let reader=res.body.getReader();for(;;){let{done,value}=await reader.read();if(done)break;if(!this.#httpAborts.has(String(id)))return;for(let o=0;o<value.length;o+=CHUNK_SIZE2)await this.#waitForDrain(),this.#send({t:"hchunk",id,b:Buffer.from(value.subarray(o,o+CHUNK_SIZE2)).toString("base64")})}}this.#send({t:"hend",id})}catch(e){this.#send({t:"herr",id,message:abort.signal.aborted?"cancelled":e instanceof Error?e.message:String(e)})}finally{this.#httpAborts.delete(String(id))}}#openLocalSocket(id,path){let ws;try{ws=new WebSocket2(`ws://127.0.0.1:${this.#config.localPort}${path}`)}catch(e){this.#send({t:"werr",id,message:e instanceof Error?e.message:String(e)});return}this.#localSockets.set(id,ws),ws.on("open",()=>this.#send({t:"wopened",id})),ws.on("message",(data,isBinary)=>{if(isBinary){let buf=Buffer.isBuffer(data)?data:Buffer.from(data);this.#send({t:"wmsg",id,b:buf.toString("base64")})}else this.#send({t:"wmsg",id,s:data.toString()})}),ws.on("close",(code,reason)=>{if(this.#localSockets.has(id))this.#localSockets.delete(id),this.#send({t:"wclose",id,code,reason:reason?.toString()})}),ws.on("error",(e)=>{if(this.#localSockets.has(id)&&ws.readyState===WebSocket2.CONNECTING)this.#localSockets.delete(id),this.#send({t:"werr",id,message:e.message})})}}var QrCodeDataType=((QrCodeDataType2)=>{return QrCodeDataType2[QrCodeDataType2.Border=-1]="Border",QrCodeDataType2[QrCodeDataType2.Data=0]="Data",QrCodeDataType2[QrCodeDataType2.Function=1]="Function",QrCodeDataType2[QrCodeDataType2.Position=2]="Position",QrCodeDataType2[QrCodeDataType2.Timing=3]="Timing",QrCodeDataType2[QrCodeDataType2.Alignment=4]="Alignment",QrCodeDataType2})(QrCodeDataType||{}),LOW=[0,1],MEDIUM=[1,0],QUARTILE=[2,3],HIGH=[3,2],EccMap={L:LOW,M:MEDIUM,Q:QUARTILE,H:HIGH},NUMERIC_REGEX=/^\d*$/,ALPHANUMERIC_REGEX=/^[A-Z0-9 $%*+./:-]*$/,ALPHANUMERIC_CHARSET="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:",MIN_VERSION=1,MAX_VERSION=40,PENALTY_N1=3,PENALTY_N2=3,PENALTY_N3=40,PENALTY_N4=10,ECC_CODEWORDS_PER_BLOCK=[[-1,7,10,15,20,26,18,20,24,30,18,20,24,26,30,22,24,28,30,28,28,28,28,30,30,26,28,30,30,30,30,30,30,30,30,30,30,30,30,30,30],[-1,10,16,26,18,24,16,18,22,22,26,30,22,22,24,24,28,28,26,26,26,26,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28],[-1,13,22,18,26,18,24,18,22,20,24,28,26,24,20,30,24,28,28,26,30,28,30,30,30,30,28,30,30,30,30,30,30,30,30,30,30,30,30,30,30],[-1,17,28,22,16,22,28,26,26,24,28,24,28,22,24,24,30,28,28,26,28,30,24,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30]],NUM_ERROR_CORRECTION_BLOCKS=[[-1,1,1,1,1,1,2,2,2,2,4,4,4,4,4,6,6,6,6,7,8,8,9,9,10,12,12,12,13,14,15,16,17,18,19,19,20,21,22,24,25],[-1,1,1,1,2,2,4,4,4,5,5,5,8,9,9,10,10,11,13,14,16,17,17,18,20,21,23,25,26,28,29,31,33,35,37,38,40,43,45,47,49],[-1,1,1,2,2,4,4,6,6,8,8,8,10,12,16,12,17,16,18,21,20,23,23,25,27,29,34,34,35,38,40,43,45,48,51,53,56,59,62,65,68],[-1,1,1,2,4,4,4,5,6,8,8,11,11,16,16,18,16,19,21,25,25,25,34,30,32,35,37,40,42,45,48,51,54,57,60,63,66,70,74,77,81]];class QrCode{constructor(version,ecc,dataCodewords,msk){if(this.version=version,this.ecc=ecc,version<MIN_VERSION||version>MAX_VERSION)throw RangeError("Version value out of range");if(msk<-1||msk>7)throw RangeError("Mask value out of range");this.size=version*4+17;let row=Array.from({length:this.size}).fill(!1);for(let i=0;i<this.size;i++)this.modules.push(row.slice()),this.types.push(row.map(()=>0));this.drawFunctionPatterns();let allCodewords=this.addEccAndInterleave(dataCodewords);if(this.drawCodewords(allCodewords),msk===-1){let minPenalty=1e9;for(let i=0;i<8;i++){this.applyMask(i),this.drawFormatBits(i);let penalty=this.getPenaltyScore();if(penalty<minPenalty)msk=i,minPenalty=penalty;this.applyMask(i)}}this.mask=msk,this.applyMask(msk),this.drawFormatBits(msk)}size;mask;modules=[];types=[];getModule(x,y){return x>=0&&x<this.size&&y>=0&&y<this.size&&this.modules[y][x]}drawFunctionPatterns(){for(let i=0;i<this.size;i++)this.setFunctionModule(6,i,i%2===0,QrCodeDataType.Timing),this.setFunctionModule(i,6,i%2===0,QrCodeDataType.Timing);this.drawFinderPattern(3,3),this.drawFinderPattern(this.size-4,3),this.drawFinderPattern(3,this.size-4);let alignPatPos=this.getAlignmentPatternPositions(),numAlign=alignPatPos.length;for(let i=0;i<numAlign;i++)for(let j=0;j<numAlign;j++)if(!(i===0&&j===0||i===0&&j===numAlign-1||i===numAlign-1&&j===0))this.drawAlignmentPattern(alignPatPos[i],alignPatPos[j]);this.drawFormatBits(0),this.drawVersion()}drawFormatBits(mask){let data=this.ecc[1]<<3|mask,rem=data;for(let i=0;i<10;i++)rem=rem<<1^(rem>>>9)*1335;let bits=(data<<10|rem)^21522;for(let i=0;i<=5;i++)this.setFunctionModule(8,i,getBit(bits,i));this.setFunctionModule(8,7,getBit(bits,6)),this.setFunctionModule(8,8,getBit(bits,7)),this.setFunctionModule(7,8,getBit(bits,8));for(let i=9;i<15;i++)this.setFunctionModule(14-i,8,getBit(bits,i));for(let i=0;i<8;i++)this.setFunctionModule(this.size-1-i,8,getBit(bits,i));for(let i=8;i<15;i++)this.setFunctionModule(8,this.size-15+i,getBit(bits,i));this.setFunctionModule(8,this.size-8,!0)}drawVersion(){if(this.version<7)return;let rem=this.version;for(let i=0;i<12;i++)rem=rem<<1^(rem>>>11)*7973;let bits=this.version<<12|rem;for(let i=0;i<18;i++){let color=getBit(bits,i),a=this.size-11+i%3,b=Math.floor(i/3);this.setFunctionModule(a,b,color),this.setFunctionModule(b,a,color)}}drawFinderPattern(x,y){for(let dy=-4;dy<=4;dy++)for(let dx=-4;dx<=4;dx++){let dist=Math.max(Math.abs(dx),Math.abs(dy)),xx=x+dx,yy=y+dy;if(xx>=0&&xx<this.size&&yy>=0&&yy<this.size)this.setFunctionModule(xx,yy,dist!==2&&dist!==4,QrCodeDataType.Position)}}drawAlignmentPattern(x,y){for(let dy=-2;dy<=2;dy++)for(let dx=-2;dx<=2;dx++)this.setFunctionModule(x+dx,y+dy,Math.max(Math.abs(dx),Math.abs(dy))!==1,QrCodeDataType.Alignment)}setFunctionModule(x,y,isDark,type=QrCodeDataType.Function){this.modules[y][x]=isDark,this.types[y][x]=type}addEccAndInterleave(data){let ver=this.version,ecl=this.ecc;if(data.length!==getNumDataCodewords(ver,ecl))throw RangeError("Invalid argument");let numBlocks=NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver],blockEccLen=ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver],rawCodewords=Math.floor(getNumRawDataModules(ver)/8),numShortBlocks=numBlocks-rawCodewords%numBlocks,shortBlockLen=Math.floor(rawCodewords/numBlocks),blocks=[],rsDiv=reedSolomonComputeDivisor(blockEccLen);for(let i=0,k=0;i<numBlocks;i++){let dat=data.slice(k,k+shortBlockLen-blockEccLen+(i<numShortBlocks?0:1));k+=dat.length;let ecc=reedSolomonComputeRemainder(dat,rsDiv);if(i<numShortBlocks)dat.push(0);blocks.push(dat.concat(ecc))}let result=[];for(let i=0;i<blocks[0].length;i++)blocks.forEach((block,j)=>{if(i!==shortBlockLen-blockEccLen||j>=numShortBlocks)result.push(block[i])});return result}drawCodewords(data){if(data.length!==Math.floor(getNumRawDataModules(this.version)/8))throw RangeError("Invalid argument");let i=0;for(let right=this.size-1;right>=1;right-=2){if(right===6)right=5;for(let vert=0;vert<this.size;vert++)for(let j=0;j<2;j++){let x=right-j,y=(right+1&2)===0?this.size-1-vert:vert;if(!this.types[y][x]&&i<data.length*8)this.modules[y][x]=getBit(data[i>>>3],7-(i&7)),i++}}}applyMask(mask){if(mask<0||mask>7)throw RangeError("Mask value out of range");for(let y=0;y<this.size;y++)for(let x=0;x<this.size;x++){let invert;switch(mask){case 0:invert=(x+y)%2===0;break;case 1:invert=y%2===0;break;case 2:invert=x%3===0;break;case 3:invert=(x+y)%3===0;break;case 4:invert=(Math.floor(x/3)+Math.floor(y/2))%2===0;break;case 5:invert=x*y%2+x*y%3===0;break;case 6:invert=(x*y%2+x*y%3)%2===0;break;case 7:invert=((x+y)%2+x*y%3)%2===0;break;default:throw Error("Unreachable")}if(!this.types[y][x]&&invert)this.modules[y][x]=!this.modules[y][x]}}getPenaltyScore(){let result=0;for(let y=0;y<this.size;y++){let runColor=!1,runX=0,runHistory=[0,0,0,0,0,0,0];for(let x=0;x<this.size;x++)if(this.modules[y][x]===runColor){if(runX++,runX===5)result+=PENALTY_N1;else if(runX>5)result++}else{if(this.finderPenaltyAddHistory(runX,runHistory),!runColor)result+=this.finderPenaltyCountPatterns(runHistory)*PENALTY_N3;runColor=this.modules[y][x],runX=1}result+=this.finderPenaltyTerminateAndCount(runColor,runX,runHistory)*PENALTY_N3}for(let x=0;x<this.size;x++){let runColor=!1,runY=0,runHistory=[0,0,0,0,0,0,0];for(let y=0;y<this.size;y++)if(this.modules[y][x]===runColor){if(runY++,runY===5)result+=PENALTY_N1;else if(runY>5)result++}else{if(this.finderPenaltyAddHistory(runY,runHistory),!runColor)result+=this.finderPenaltyCountPatterns(runHistory)*PENALTY_N3;runColor=this.modules[y][x],runY=1}result+=this.finderPenaltyTerminateAndCount(runColor,runY,runHistory)*PENALTY_N3}for(let y=0;y<this.size-1;y++)for(let x=0;x<this.size-1;x++){let color=this.modules[y][x];if(color===this.modules[y][x+1]&&color===this.modules[y+1][x]&&color===this.modules[y+1][x+1])result+=PENALTY_N2}let dark=0;for(let row of this.modules)dark=row.reduce((sum,color)=>sum+(color?1:0),dark);let total=this.size*this.size,k=Math.ceil(Math.abs(dark*20-total*10)/total)-1;return result+=k*PENALTY_N4,result}getAlignmentPatternPositions(){if(this.version===1)return[];else{let numAlign=Math.floor(this.version/7)+2,step=this.version===32?26:Math.ceil((this.version*4+4)/(numAlign*2-2))*2,result=[6];for(let pos=this.size-7;result.length<numAlign;pos-=step)result.splice(1,0,pos);return result}}finderPenaltyCountPatterns(runHistory){let n=runHistory[1],core=n>0&&runHistory[2]===n&&runHistory[3]===n*3&&runHistory[4]===n&&runHistory[5]===n;return(core&&runHistory[0]>=n*4&&runHistory[6]>=n?1:0)+(core&&runHistory[6]>=n*4&&runHistory[0]>=n?1:0)}finderPenaltyTerminateAndCount(currentRunColor,currentRunLength,runHistory){if(currentRunColor)this.finderPenaltyAddHistory(currentRunLength,runHistory),currentRunLength=0;return currentRunLength+=this.size,this.finderPenaltyAddHistory(currentRunLength,runHistory),this.finderPenaltyCountPatterns(runHistory)}finderPenaltyAddHistory(currentRunLength,runHistory){if(runHistory[0]===0)currentRunLength+=this.size;runHistory.pop(),runHistory.unshift(currentRunLength)}}function appendBits(val,len,bb){if(len<0||len>31||val>>>len!==0)throw RangeError("Value out of range");for(let i=len-1;i>=0;i--)bb.push(val>>>i&1)}function getBit(x,i){return(x>>>i&1)!==0}class QrSegment{constructor(mode,numChars,bitData){if(this.mode=mode,this.numChars=numChars,this.bitData=bitData,numChars<0)throw RangeError("Invalid argument");this.bitData=bitData.slice()}getData(){return this.bitData.slice()}}var MODE_NUMERIC=[1,10,12,14],MODE_ALPHANUMERIC=[2,9,11,13],MODE_BYTE=[4,8,16,16];function numCharCountBits(mode,ver){return mode[Math.floor((ver+7)/17)+1]}function makeBytes(data){let bb=[];for(let b of data)appendBits(b,8,bb);return new QrSegment(MODE_BYTE,data.length,bb)}function makeNumeric(digits){if(!isNumeric(digits))throw RangeError("String contains non-numeric characters");let bb=[];for(let i=0;i<digits.length;){let n=Math.min(digits.length-i,3);appendBits(Number.parseInt(digits.substring(i,i+n),10),n*3+1,bb),i+=n}return new QrSegment(MODE_NUMERIC,digits.length,bb)}function makeAlphanumeric(text){if(!isAlphanumeric(text))throw RangeError("String contains unencodable characters in alphanumeric mode");let bb=[],i;for(i=0;i+2<=text.length;i+=2){let temp=ALPHANUMERIC_CHARSET.indexOf(text.charAt(i))*45;temp+=ALPHANUMERIC_CHARSET.indexOf(text.charAt(i+1)),appendBits(temp,11,bb)}if(i<text.length)appendBits(ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)),6,bb);return new QrSegment(MODE_ALPHANUMERIC,text.length,bb)}function makeSegments(text){if(text==="")return[];else if(isNumeric(text))return[makeNumeric(text)];else if(isAlphanumeric(text))return[makeAlphanumeric(text)];else return[makeBytes(toUtf8ByteArray(text))]}function isNumeric(text){return NUMERIC_REGEX.test(text)}function isAlphanumeric(text){return ALPHANUMERIC_REGEX.test(text)}function getTotalBits(segs,version){let result=0;for(let seg of segs){let ccbits=numCharCountBits(seg.mode,version);if(seg.numChars>=1<<ccbits)return Number.POSITIVE_INFINITY;result+=4+ccbits+seg.bitData.length}return result}function toUtf8ByteArray(str){str=encodeURI(str);let result=[];for(let i=0;i<str.length;i++)if(str.charAt(i)!=="%")result.push(str.charCodeAt(i));else result.push(Number.parseInt(str.substring(i+1,i+3),16)),i+=2;return result}function getNumRawDataModules(ver){if(ver<MIN_VERSION||ver>MAX_VERSION)throw RangeError("Version number out of range");let result=(16*ver+128)*ver+64;if(ver>=2){let numAlign=Math.floor(ver/7)+2;if(result-=(25*numAlign-10)*numAlign-55,ver>=7)result-=36}return result}function getNumDataCodewords(ver,ecl){return Math.floor(getNumRawDataModules(ver)/8)-ECC_CODEWORDS_PER_BLOCK[ecl[0]][ver]*NUM_ERROR_CORRECTION_BLOCKS[ecl[0]][ver]}function reedSolomonComputeDivisor(degree){if(degree<1||degree>255)throw RangeError("Degree out of range");let result=[];for(let i=0;i<degree-1;i++)result.push(0);result.push(1);let root=1;for(let i=0;i<degree;i++){for(let j=0;j<result.length;j++)if(result[j]=reedSolomonMultiply(result[j],root),j+1<result.length)result[j]^=result[j+1];root=reedSolomonMultiply(root,2)}return result}function reedSolomonComputeRemainder(data,divisor){let result=divisor.map((_)=>0);for(let b of data){let factor=b^result.shift();result.push(0),divisor.forEach((coef,i)=>result[i]^=reedSolomonMultiply(coef,factor))}return result}function reedSolomonMultiply(x,y){if(x>>>8!==0||y>>>8!==0)throw RangeError("Byte out of range");let z=0;for(let i=7;i>=0;i--)z=z<<1^(z>>>7)*285,z^=(y>>>i&1)*x;return z}function encodeSegments(segs,ecl,minVersion=1,maxVersion=40,mask=-1,boostEcl=!0){if(!(MIN_VERSION<=minVersion&&minVersion<=maxVersion&&maxVersion<=MAX_VERSION)||mask<-1||mask>7)throw RangeError("Invalid value");let version,dataUsedBits;for(version=minVersion;;version++){let dataCapacityBits2=getNumDataCodewords(version,ecl)*8,usedBits=getTotalBits(segs,version);if(usedBits<=dataCapacityBits2){dataUsedBits=usedBits;break}if(version>=maxVersion)throw RangeError("Data too long")}for(let newEcl of[MEDIUM,QUARTILE,HIGH])if(boostEcl&&dataUsedBits<=getNumDataCodewords(version,newEcl)*8)ecl=newEcl;let bb=[];for(let seg of segs){appendBits(seg.mode[0],4,bb),appendBits(seg.numChars,numCharCountBits(seg.mode,version),bb);for(let b of seg.getData())bb.push(b)}let dataCapacityBits=getNumDataCodewords(version,ecl)*8;appendBits(0,Math.min(4,dataCapacityBits-bb.length),bb),appendBits(0,(8-bb.length%8)%8,bb);for(let padByte=236;bb.length<dataCapacityBits;padByte^=253)appendBits(padByte,8,bb);let dataCodewords=Array.from({length:Math.ceil(bb.length/8)},()=>0);return bb.forEach((b,i)=>dataCodewords[i>>>3]|=b<<7-(i&7)),new QrCode(version,ecl,dataCodewords,mask)}function encode(data,options){let{ecc="L",boostEcc=!1,minVersion=1,maxVersion=40,maskPattern=-1,border=1}=options||{},segment=typeof data==="string"?makeSegments(data):Array.isArray(data)?[makeBytes(data)]:void 0;if(!segment)throw Error(`uqr only supports encoding string and binary data, but got: ${typeof data}`);let qr=encodeSegments(segment,EccMap[ecc],minVersion,maxVersion,maskPattern,boostEcc),result=addBorder({version:qr.version,maskPattern:qr.mask,size:qr.size,data:qr.modules,types:qr.types},border);if(options?.invert)result.data=result.data.map((row)=>row.map((mod)=>!mod));return options?.onEncoded?.(result),result}function addBorder(input,border=1){if(!border)return input;let{size}=input,newSize=size+border*2;input.size=newSize,input.data.forEach((row)=>{for(let i=0;i<border;i++)row.unshift(!1),row.push(!1)});for(let i=0;i<border;i++)input.data.unshift(Array.from({length:newSize},(_)=>!1)),input.data.push(Array.from({length:newSize},(_)=>!1));let b=QrCodeDataType.Border;input.types.forEach((row)=>{for(let i=0;i<border;i++)row.unshift(b),row.push(b)});for(let i=0;i<border;i++)input.types.unshift(Array.from({length:newSize},(_)=>b)),input.types.push(Array.from({length:newSize},(_)=>b));return input}function getDataAt(data,x,y,defaults=!1){if(x<0||y<0||x>=data.length||y>=data.length)return defaults;return data[y][x]}function renderUnicodeCompact(data,options={}){let palette={WHITE_ALL:"\u2588",WHITE_BLACK:"\u2580",BLACK_WHITE:"\u2584",BLACK_ALL:" "},result=encode(data,options),WHITE=!1,BLACK=!0,at=(x,y)=>getDataAt(result.data,x,y,!0),lines=[],line="";for(let row=0;row<result.size;row+=2){for(let col=0;col<result.size;col++)if(at(col,row)===!1&&at(col,row+1)===!1)line+=palette.WHITE_ALL;else if(at(col,row)===!1&&at(col,row+1)===!0)line+=palette.WHITE_BLACK;else if(at(col,row)===!0&&at(col,row+1)===!1)line+=palette.BLACK_WHITE;else line+=palette.BLACK_ALL;lines.push(line),line=""}return lines.join(`
|
|
17
|
+
`)}import{createHash,randomBytes,timingSafeEqual as timingSafeEqual2}from"crypto";import{chmodSync,existsSync,mkdirSync,readFileSync,writeFileSync}from"fs";import{dirname}from"path";function loadOrCreateToken(path,rotate=!1){if(!rotate&&existsSync(path)){let saved=readFileSync(path,"utf8").trim();if(/^[A-Za-z0-9_-]{43}$/.test(saved))return saved}let token=randomBytes(32).toString("base64url");return mkdirSync(dirname(path),{recursive:!0,mode:448}),writeFileSync(path,`${token}
|
|
18
|
+
`,{mode:384}),chmodSync(path,384),token}function tokenMatches(expected,presented){if(!presented)return!1;let a=Buffer.from(expected),b=Buffer.from(presented);if(a.length!==b.length)return!1;return timingSafeEqual2(a,b)}var WEB_SUBPROTOCOL="rove.v1",TOKEN_SUBPROTOCOL_PREFIX="rove.bearer.";function presentedToken(req,allowQuery=!1){let header=req.headers.get("authorization");if(header?.startsWith("Bearer "))return header.slice(7).trim();for(let offered of(req.headers.get("sec-websocket-protocol")??"").split(",")){let name=offered.trim();if(name.startsWith(TOKEN_SUBPROTOCOL_PREFIX))return name.slice(TOKEN_SUBPROTOCOL_PREFIX.length)}return allowQuery?new URL(req.url).searchParams.get("token"):null}function browserOriginAllowed(origin,host,publicHost){try{let url=new URL(origin);if(host!==null&&url.host===host.toLowerCase())return!0;return publicHost!==null&&url.protocol==="https:"&&url.host===publicHost.toLowerCase()}catch{return!1}}function offersWebSubprotocol(req){return(req.headers.get("sec-websocket-protocol")??"").split(",").some((p)=>p.trim()===WEB_SUBPROTOCOL)}function webUrl(opts){let shown=opts.host.includes(":")?`[${opts.host}]`:opts.host;return`${opts.tls?`https://${shown}`:`http://${shown}:${opts.port}`}/#token=${opts.token}`}function relayLink(opts){return`${opts.server}/#code=${opts.code}&token=${opts.token}`}function pairingUrl(opts){let shown=opts.host.includes(":")?`[${opts.host}]`:opts.host,origin=opts.tls?`wss://${shown}`:`ws://${shown}:${opts.port}`,preset=opts.preset==="none"?"":`&preset=${opts.preset}`;return`${origin}/?token=${opts.token}${preset}`}var hashToken=(token)=>createHash("sha256").update(token).digest("hex");function listDevices(path){if(!existsSync(path))return[];try{let raw=JSON.parse(readFileSync(path,"utf8"));return Array.isArray(raw)?raw.filter((d)=>!!d&&typeof d==="object"&&typeof d.name==="string"&&/^[0-9a-f]{64}$/.test(d.sha256)):[]}catch{return[]}}function writeDevices(path,devices){mkdirSync(dirname(path),{recursive:!0,mode:448}),writeFileSync(path,`${JSON.stringify(devices,null,2)}
|
|
19
|
+
`,{mode:384}),chmodSync(path,384)}function addDevice(path,name,now=new Date){if(!/^[\w.-]{1,40}$/.test(name))throw Error(`device name must be 1-40 letters, digits, . _ or -, got ${name}`);let token=randomBytes(32).toString("base64url"),rest=listDevices(path).filter((d)=>d.name!==name);return writeDevices(path,[...rest,{name,sha256:hashToken(token),createdAt:now.toISOString()}]),token}function revokeDevice(path,name){let devices=listDevices(path),rest=devices.filter((d)=>d.name!==name);if(rest.length===devices.length)return!1;return writeDevices(path,rest),!0}function revokeAllDevices(path){let count=listDevices(path).length;if(count>0)writeDevices(path,[]);return count}function startupCredentials(opts){let sharedToken=loadOrCreateToken(opts.tokenPath,opts.rotate),revoked=opts.rotate?revokeAllDevices(opts.devicesPath):0,token=opts.device?addDevice(opts.devicesPath,opts.device):sharedToken;return{sharedToken,token,revoked}}function deviceOf(path,presented){if(!presented)return null;let hash=Buffer.from(hashToken(presented));for(let device of listDevices(path)){let stored=Buffer.from(device.sha256);if(stored.length===hash.length&&timingSafeEqual2(stored,hash))return device.name}return null}import{createPublicKey,verify}from"crypto";class AccessDenied extends Error{constructor(reason){super(reason);this.name="AccessDenied"}}var fetchJwksOverHttps=async(url)=>{let res=await fetch(url,{signal:AbortSignal.timeout(1e4)});if(!res.ok)throw Error(`GET ${url} \u2192 ${res.status}`);let body=await res.json();if(!body||typeof body!=="object"||!("keys"in body)||!Array.isArray(body.keys))throw Error(`${url} did not return a JWKS`);return{keys:body.keys}},REFETCH_MIN_MS=60000,LEEWAY_S=30;function decodeSegment(segment,what){try{let value=JSON.parse(Buffer.from(segment,"base64url").toString("utf8"));if(value&&typeof value==="object"&&!Array.isArray(value))return value}catch{}throw new AccessDenied(`malformed JWT ${what}`)}function createAccessVerifier(access,deps={}){let fetchJwks=deps.fetchJwks??fetchJwksOverHttps,now=deps.now??Date.now,certsUrl=`${access.teamDomain}/cdn-cgi/access/certs`,keys=new Map,fetchedAt=Number.NEGATIVE_INFINITY,refresh=async()=>{fetchedAt=now();let jwks=await fetchJwks(certsUrl),next=new Map;for(let jwk of jwks.keys){if(jwk.kty!=="RSA"||!jwk.kid)continue;next.set(jwk.kid,createPublicKey({key:jwk,format:"jwk"}))}keys=next},keyFor=async(kid)=>{let key=keys.get(kid);if(!key&&now()-fetchedAt>=REFETCH_MIN_MS)await refresh(),key=keys.get(kid);if(!key)throw new AccessDenied(`no signing key ${kid} at ${certsUrl}`);return key};return{async verify(assertion){if(!assertion)throw new AccessDenied("missing Cf-Access-Jwt-Assertion header");let parts=assertion.split(".");if(parts.length!==3)throw new AccessDenied("malformed JWT");let[h,p,s]=parts,header=decodeSegment(h,"header");if(header.alg!=="RS256")throw new AccessDenied(`unexpected JWT alg ${String(header.alg)}`);if(typeof header.kid!=="string")throw new AccessDenied("JWT has no kid");let key=await keyFor(header.kid);if(!verify("RSA-SHA256",Buffer.from(`${h}.${p}`),key,Buffer.from(s,"base64url")))throw new AccessDenied("bad JWT signature");let claims=decodeSegment(p,"payload");if(claims.iss!==access.teamDomain)throw new AccessDenied(`JWT issuer ${String(claims.iss)} is not ${access.teamDomain}`);if(!(Array.isArray(claims.aud)?claims.aud:[claims.aud]).includes(access.aud))throw new AccessDenied("JWT audience does not match this Access application");let nowS=now()/1000;if(typeof claims.exp!=="number"||claims.exp+LEEWAY_S<nowS)throw new AccessDenied("JWT expired");if(typeof claims.nbf==="number"&&claims.nbf-LEEWAY_S>nowS)throw new AccessDenied("JWT not yet valid")}}}function teamDomainOf(team){let bare=team.trim().replace(/^https?:\/\//,"").replace(/\/.*$/,"");if(!/^[A-Za-z0-9-]+(\.cloudflareaccess\.com)?$/.test(bare))throw Error(`--cf-team must be a team name or <team>.cloudflareaccess.com, got ${team}`);return`https://${bare.endsWith(".cloudflareaccess.com")?bare:`${bare}.cloudflareaccess.com`}`}function relayOriginOf(raw){let url;try{url=new URL(raw.trim())}catch{throw Error(`--relay-server must be a URL like https://relay.example.com, got ${raw}`)}let loopback=url.hostname==="127.0.0.1"||url.hostname==="localhost";if(url.protocol!=="https:"&&!(url.protocol==="http:"&&loopback))throw Error(`--relay-server must be https:// (http:// only for 127.0.0.1 or localhost), got ${raw}`);return url.origin}function parseBridgeArgs(argv,deps={}){let config=deps.config??{},hosts=[],preset="none",port=7878,publicHost=null,cfTeam=config.cfTeam,cfAud=config.cfAud,relayServer=config.relayServer,rotateToken=!1,rotateRelay=!1,relayShellDir=null,qr=!0,device=null,action="serve";for(let i=0;i<argv.length;i++){let arg=argv[i],value=()=>{let v=argv[++i];if(v===void 0||v.startsWith("--"))throw Error(`${arg} needs a value
|
|
20
|
+
usage: rove-bridge [--preset tailscale|cf|relay] [--host <addr>]... [--port <n>]
|
|
21
|
+
[--public-host <name>] [--cf-team <team>] [--cf-aud <aud>]
|
|
22
|
+
[--relay-server <url>] [--rotate-relay] [--relay-shell <dir>]
|
|
23
|
+
[--rotate-token] [--no-qr] [--device <name>]
|
|
24
|
+
[--list-devices] [--revoke-device <name>] [--setup]
|
|
25
|
+
[--set-password] [--clear-password] [--unlock]
|
|
26
|
+
|
|
27
|
+
(no preset) listen on 127.0.0.1 only (or each --host you name). With
|
|
28
|
+
--public-host <name> (a tunnel to 127.0.0.1 serving https://<name>)
|
|
29
|
+
the links name it and a page from that origin is accepted.
|
|
30
|
+
--preset tailscale listen on this Mac's Tailscale IPv4 only; fails if Tailscale
|
|
31
|
+
is not up. With --public-host <name> (a \`tailscale serve\`
|
|
32
|
+
HTTPS name) the phone dials wss://<name>.
|
|
33
|
+
--preset cf listen on 127.0.0.1 for cloudflared; every connection must
|
|
34
|
+
carry a valid Cloudflare Access JWT. Needs --public-host
|
|
35
|
+
(the tunnel hostname), --cf-team and --cf-aud.
|
|
36
|
+
--preset relay \`rove --web\` only: listen on 127.0.0.1 and dial out to a relay-kit
|
|
37
|
+
Worker (--relay-server <url>); the browser reaches this machine
|
|
38
|
+
through it, end-to-end encrypted. The bearer token still applies.
|
|
39
|
+
--rotate-relay mint a new relay pairing code, unpairing every relay browser
|
|
40
|
+
--relay-shell <dir> write the page the relay Worker serves (index.html), then exit
|
|
41
|
+
--port <n> TCP port (default 7878)
|
|
42
|
+
--rotate-token mint a new pairing token, revoking every paired phone
|
|
43
|
+
--no-qr print the pairing URL without a QR code
|
|
44
|
+
--device <name> give this phone/browser its own token and print its link;
|
|
45
|
+
revoke it alone later with --revoke-device <name>
|
|
46
|
+
--list-devices show the devices that have their own token, then exit
|
|
47
|
+
--revoke-device <n> revoke one device's token (others keep working), then exit
|
|
48
|
+
--setup check Tailscale / cloudflared here and print the exact
|
|
49
|
+
commands to reach this bridge from a phone; changes nothing
|
|
50
|
+
--set-password prompt (twice, no echo) for a browser login password; the page
|
|
51
|
+
then offers a password field that mints its own device token.
|
|
52
|
+
Also lifts a lockout. Never taken from argv or the environment.
|
|
53
|
+
--clear-password remove the browser login password, then exit
|
|
54
|
+
--unlock lift a password-login lockout, then exit
|
|
55
|
+
|
|
56
|
+
--cf-team, --cf-aud, --public-host and --relay-server may also live in
|
|
57
|
+
<ROVE_HOME>/.rove/bridge/config.json as cfTeam, cfAud, publicHost, relayServer.`);return v};if(arg==="--host")hosts.push(value());else if(arg==="--preset"){let p=value();if(p==="tailscale")preset="tailscale";else if(p==="cf"||p==="cloudflare")preset="cf";else if(p==="relay")preset="relay";else throw Error(`--preset must be tailscale, cf or relay, got ${p}`)}else if(arg==="--port"){let raw=value();if(port=Number(raw),!Number.isInteger(port)||port<1||port>65535)throw Error(`--port must be 1..65535, got ${raw}`)}else if(arg==="--public-host")publicHost=value();else if(arg==="--cf-team")cfTeam=value();else if(arg==="--cf-aud")cfAud=value();else if(arg==="--relay-server")relayServer=value();else if(arg==="--rotate-relay")rotateRelay=!0;else if(arg==="--relay-shell")relayShellDir=value(),action="relay-shell";else if(arg==="--rotate-token")rotateToken=!0;else if(arg==="--no-qr")qr=!1;else if(arg==="--device")device=value();else if(arg==="--list-devices")action="list-devices";else if(arg==="--revoke-device")device=value(),action="revoke-device";else if(arg==="--setup")action="setup";else if(arg==="--set-password"){if(argv[i+1]!==void 0&&!argv[i+1]?.startsWith("--"))throw Error("--set-password takes no value; it prompts on the terminal");action="set-password"}else if(arg==="--clear-password")action="clear-password";else if(arg==="--unlock")action="unlock";else throw Error(`unknown argument ${arg}
|
|
58
|
+
usage: rove-bridge [--preset tailscale|cf|relay] [--host <addr>]... [--port <n>]
|
|
59
|
+
[--public-host <name>] [--cf-team <team>] [--cf-aud <aud>]
|
|
60
|
+
[--relay-server <url>] [--rotate-relay] [--relay-shell <dir>]
|
|
61
|
+
[--rotate-token] [--no-qr] [--device <name>]
|
|
62
|
+
[--list-devices] [--revoke-device <name>] [--setup]
|
|
63
|
+
[--set-password] [--clear-password] [--unlock]
|
|
64
|
+
|
|
65
|
+
(no preset) listen on 127.0.0.1 only (or each --host you name). With
|
|
66
|
+
--public-host <name> (a tunnel to 127.0.0.1 serving https://<name>)
|
|
67
|
+
the links name it and a page from that origin is accepted.
|
|
68
|
+
--preset tailscale listen on this Mac's Tailscale IPv4 only; fails if Tailscale
|
|
69
|
+
is not up. With --public-host <name> (a \`tailscale serve\`
|
|
70
|
+
HTTPS name) the phone dials wss://<name>.
|
|
71
|
+
--preset cf listen on 127.0.0.1 for cloudflared; every connection must
|
|
72
|
+
carry a valid Cloudflare Access JWT. Needs --public-host
|
|
73
|
+
(the tunnel hostname), --cf-team and --cf-aud.
|
|
74
|
+
--preset relay \`rove --web\` only: listen on 127.0.0.1 and dial out to a relay-kit
|
|
75
|
+
Worker (--relay-server <url>); the browser reaches this machine
|
|
76
|
+
through it, end-to-end encrypted. The bearer token still applies.
|
|
77
|
+
--rotate-relay mint a new relay pairing code, unpairing every relay browser
|
|
78
|
+
--relay-shell <dir> write the page the relay Worker serves (index.html), then exit
|
|
79
|
+
--port <n> TCP port (default 7878)
|
|
80
|
+
--rotate-token mint a new pairing token, revoking every paired phone
|
|
81
|
+
--no-qr print the pairing URL without a QR code
|
|
82
|
+
--device <name> give this phone/browser its own token and print its link;
|
|
83
|
+
revoke it alone later with --revoke-device <name>
|
|
84
|
+
--list-devices show the devices that have their own token, then exit
|
|
85
|
+
--revoke-device <n> revoke one device's token (others keep working), then exit
|
|
86
|
+
--setup check Tailscale / cloudflared here and print the exact
|
|
87
|
+
commands to reach this bridge from a phone; changes nothing
|
|
88
|
+
--set-password prompt (twice, no echo) for a browser login password; the page
|
|
89
|
+
then offers a password field that mints its own device token.
|
|
90
|
+
Also lifts a lockout. Never taken from argv or the environment.
|
|
91
|
+
--clear-password remove the browser login password, then exit
|
|
92
|
+
--unlock lift a password-login lockout, then exit
|
|
93
|
+
|
|
94
|
+
--cf-team, --cf-aud, --public-host and --relay-server may also live in
|
|
95
|
+
<ROVE_HOME>/.rove/bridge/config.json as cfTeam, cfAud, publicHost, relayServer.`)}if(preset==="cf")publicHost??=config.publicHost??null;if(publicHost!==null&&!/^[A-Za-z0-9.-]+$/.test(publicHost))throw Error(`--public-host must be a bare hostname (no scheme, port or path), got ${publicHost}`);if(preset!=="none"&&hosts.length>0)throw Error("--host cannot be combined with --preset; the preset picks the address");let base={port,rotateToken,rotateRelay,relayShellDir,qr,device,action,relayServer:null};if(preset==="tailscale"){let ts=deps.tailscale?.()??{ipv4:null,dnsName:null};if(!ts.ipv4)throw Error("--preset tailscale: `tailscale ip -4` returned no address \u2014 is Tailscale installed and up?");return{...base,preset,hosts:[ts.ipv4],publicHost:publicHost??ts.dnsName,tls:publicHost!==null,cloudflare:null}}if(preset==="cf"){if(!cfTeam||!cfAud)throw Error("--preset cf needs --cf-team and --cf-aud (or cfTeam/cfAud in bridge/config.json)");if(!publicHost)throw Error("--preset cf needs --public-host: the tunnel hostname the phone dials");return{...base,preset,hosts:["127.0.0.1"],publicHost,tls:!0,cloudflare:{teamDomain:teamDomainOf(cfTeam),aud:cfAud.trim()}}}if(preset==="relay"){if(!relayServer)throw Error("--preset relay needs --relay-server <url> (or relayServer in bridge/config.json)");if(publicHost!==null)throw Error("--public-host cannot be combined with --preset relay");return{...base,preset,hosts:["127.0.0.1"],publicHost:null,tls:!1,cloudflare:null,relayServer:relayOriginOf(relayServer)}}return{...base,preset,hosts:hosts.length>0?[...new Set(hosts)]:["127.0.0.1"],publicHost,tls:publicHost!==null,cloudflare:null}}function isSnapshot(value){return!!value&&typeof value==="object"&&"rows"in value&&Array.isArray(value.rows)&&"rev"in value&&typeof value.rev==="number"}class DispatchFeed{client;snapshot=null;listeners=new Set;constructor(client){this.client=client;client.on("dispatch.snapshot",(frame)=>this.accept(frame.payload,!1))}current(){return this.snapshot}async resync(){this.accept(await this.client.request("dispatch.list").catch(()=>null),!0)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}accept(value,whole){if(!isSnapshot(value))return;if(!whole&&this.snapshot&&value.rev<=this.snapshot.rev)return;this.snapshot=value;for(let listener of this.listeners)listener(value)}}class TaskFeed{read;now;latest=null;latestKey="";listeners=new Set;timer=null;inflight=null;constructor(read,now=Date.now){this.read=read;this.now=now}get active(){return this.listeners.size>0}poke(){if(this.listeners.size===0||this.timer)return;this.timer=setTimeout(()=>{this.timer=null,this.refresh().catch(()=>{})},250)}refresh(){if(this.inflight)return this.inflight;let p=this.read().then((payload)=>{this.latest={payload,readAt:this.now()};let key=JSON.stringify(payload,(k,v)=>k==="forMs"?void 0:v);if(key!==this.latestKey){this.latestKey=key;for(let listener of this.listeners)listener(payload)}return payload});this.inflight=p;let clear=()=>{if(this.inflight===p)this.inflight=null};return p.then(clear,clear),p}current(){if(!this.latest)return this.refresh();let elapsed=Math.max(0,this.now()-this.latest.readAt),{payload}=this.latest;return Promise.resolve({...payload,tasks:payload.tasks.map((t)=>t.activity?{...t,activity:{...t.activity,forMs:t.activity.forMs+elapsed}}:t)})}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}var STALE_NOTICE_MS=1e4;class NoticeFeed{now;listeners=new Set;lastAt=0;constructor(now=Date.now){this.now=now}push(notice){if(!notice||typeof notice.title!=="string"||typeof notice.at!=="number")return;if(notice.at<=this.lastAt)return;if(this.lastAt=notice.at,this.now()-notice.at>1e4)return;let{title,body,kind,taskId,source,at}=notice,clean={title,kind:typeof kind==="string"?kind:"done",at,...body?{body}:{},...taskId?{taskId}:{},...source?{source}:{}};for(let listener of this.listeners)listener(clean)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}var BRIDGE_PROTOCOL_VERSION=1,OPS=["hello","api.invoke","tasks.subscribe","tasks.list","engines.list","repos.list","task.create","task.delete","task.land","task.tabs","tab.new","tab.close","term.attach","term.input","term.resize","term.detach","term.ack","term.revive","diff.files","diff.file","diff.refs","attention.dismiss","ui.viewing","push.key","push.status","push.subscribe","push.unsubscribe"];function isCoreOp(op){return OPS.includes(op)}class BridgeError extends Error{code;detail;constructor(code,message,detail){super(message);this.code=code;this.detail=detail;this.name="BridgeError"}}function parseRequest(raw,known=isCoreOp){let frame;try{frame=JSON.parse(raw)}catch{throw new BridgeError("BAD_FRAME","frame is not JSON")}if(!frame||typeof frame!=="object"||Array.isArray(frame))throw new BridgeError("BAD_FRAME","frame is not an object");let{id,op,args}=frame;if(typeof id!=="number"||!Number.isSafeInteger(id))throw new BridgeError("BAD_FRAME","frame needs an integer id");if(typeof op!=="string"||!known(op))throw Object.assign(new BridgeError("UNKNOWN_OP",`unknown op: ${String(op)}`),{requestId:id});if(args!==void 0&&(args===null||typeof args!=="object"||Array.isArray(args)))throw Object.assign(new BridgeError("BAD_ARGS","args must be an object"),{requestId:id});return{id,op,args:args??{}}}function requestIdOf(err){let id=err&&typeof err==="object"&&"requestId"in err?err.requestId:null;return typeof id==="number"?id:null}function str(args,name){let v=args[name];if(typeof v!=="string"||v.length===0)throw new BridgeError("BAD_ARGS",`${name} must be a non-empty string`);return v}function optStr(args,name){let v=args[name];if(v===void 0||v===null||v==="")return;if(typeof v!=="string")throw new BridgeError("BAD_ARGS",`${name} must be a string`);return v}function optInt(args,name,min,max){let v=args[name];if(v===void 0||v===null)return;if(typeof v!=="number"||!Number.isInteger(v)||v<min||v>max)throw new BridgeError("BAD_ARGS",`${name} must be an integer in ${min}..${max}`);return v}function optBool(args,name){let v=args[name];if(v===void 0||v===null)return!1;if(typeof v!=="boolean")throw new BridgeError("BAD_ARGS",`${name} must be a boolean`);return v}function responseOk(id,result){return JSON.stringify({id,ok:!0,result:result??{}})}function responseError(id,code,message,detail){return JSON.stringify({id,ok:!1,error:{...detail,code,message}})}function pushEvent(event,data){return JSON.stringify({event,data})}function asBridgeError(err,fallback){let code=err&&typeof err==="object"&&"code"in err?err.code:void 0,data=err instanceof ApiError?err.data:void 0;return new BridgeError(typeof code==="string"?code:fallback,err instanceof Error?err.message:String(err),data)}function taskIdOf(argv){for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--task-id")return argv[i+1];if(arg?.startsWith("--task-id="))return arg.slice(10)}return}function createBridgeApi(client,remote){return{machineOf:(taskId)=>remote?.machineOf(taskId),async verb(name,argv,machine){if(machine!==void 0&&machine!=="local"&&!remote)throw new BridgeError("UNKNOWN_MACHINE",`this bridge serves no machine named "${machine}"`);let taskId=taskIdOf(argv),alias=machine!==void 0&&machine!=="local"?machine:taskId===void 0?void 0:remote?.machineOf(taskId);if(alias!==void 0&&remote){let envelope;try{envelope=await remote.forward(alias,name,argvToParams(name,argv))}catch(err){throw asBridgeError(err,"RPC_ERROR")}if(envelope.ok)return envelope.result;let{code,message,...data}=envelope.error;throw new BridgeError(code,message,data)}try{return await invokeVerb(name,argv,{client})}catch(err){throw asBridgeError(err,"RPC_ERROR")}},async rpc(name,payload){try{return await client.request(name,payload)}catch(err){throw asBridgeError(err,"RPC_ERROR")}}}}var REASON_BY_CODE={MACHINE_UNREACHABLE:"offline",MACHINE_VERSION_SKEW:"outdated",UNKNOWN_VERB:"outdated",INBOUND_DENIED:"denied"};async function attempt(call){try{return{ok:!0,result:await call}}catch(err){if(!(err instanceof BridgeError))throw err;return{ok:!1,reason:Object.hasOwn(REASON_BY_CODE,err.code)?REASON_BY_CODE[err.code]:"failed",message:err.message}}}function machineOf(args){let machine=optStr(args,"machine");if(machine!==void 0&&machine.length>64)throw new BridgeError("BAD_ARGS","machine is too long");return machine==="local"?void 0:machine}function repoRows(raw){let rows=raw?.repos;if(!Array.isArray(rows))return[];return rows.flatMap((row)=>typeof row?.id==="string"&&Array.isArray(row.checkouts)?[{id:row.id,primary:typeof row.primary==="string"?row.primary:null,checkouts:row.checkouts.filter((c)=>typeof c==="string")}]:[])}class MachineStore{rows=[];set(statuses){this.rows=statuses.filter((s)=>!s.duplicateOf).map((s)=>({alias:s.alias,hostLabel:s.hostLabel,state:s.state,version:s.version??null,reason:s.state==="online"?null:s.reason??null}))}get(){return this.rows}}var machineStore=new MachineStore;function createMachineOps(store=machineStore){return{"machines.list":{kind:"read",destructive:!1,wraps:"MachineFeed.machinesSignal (the daemon's machines channel, as the TUI sidebar shows it)",async run(){return{machines:store.get()}}},"machines.repos":{kind:"read",destructive:!1,wraps:"repo-list (on `machine`): that machine's repos by id, with their checkouts",async run(args,ctx){let machine=machineOf(args);if(!machine)throw new BridgeError("BAD_ARGS","machine must name a registered machine");let state=store.get().find((row)=>row.alias===machine)?.state;if(state!==void 0&&state!=="online")return{repos:[],reason:state==="mismatch"||state==="unsupported"?"outdated":"offline",message:`machine "${machine}" is ${state}`};let out=await attempt(ctx.api.verb("repo-list",[],machine));return out.ok?{repos:repoRows(out.result)}:{repos:[],reason:out.reason,message:out.message}}}}}var machineOps=createMachineOps();var KEY_HINTS_ENABLED_KEY="hints.keyboard.enabled",PANE_HINT_USED_KEYS=["hints.sidebar.used","hints.files.used"],RAIL_FOLD_STYLES=appearanceChoices("railFold",[]).map((c)=>c.value),bool=(key,fallback,extra)=>({kind:"bool",read:(s)=>typeof s[key]==="boolean"?s[key]:fallback,write:(v)=>({[key]:v,...extra?.(v)})}),choice=(key,choices,read,stored=(v)=>v)=>({kind:"enum",choices:()=>choices,read:(s)=>read(s[key]),write:(v)=>({[key]:stored(v)})}),appearance=(setting)=>appearanceChoices(setting,[]).map((c)=>c.value),text=(raw)=>typeof raw==="string"?raw.trim():"";function worktreeKind(state){return worktreeBaseKindOf(text(state[WORKTREE_BASE_KEY]))}function worktreeCustomPath(state){return text(state[WORKTREE_BASE_CUSTOM_KEY])||(worktreeKind(state)==="custom"?text(state[WORKTREE_BASE_KEY]):"")}var WORKTREE_KINDS=["default","nextToProject","custom"],PREF_DEFS={locale:choice("locale",LOCALES.map((l)=>l.id),(raw)=>isLocaleId(raw)?raw:DEFAULT_LOCALE),theme:{kind:"enum",choices:(themes)=>themes,read:(s)=>typeof s.activeTheme==="string"&&s.activeTheme?s.activeTheme:DEFAULT_THEME,write:(v)=>({activeTheme:v})},themeMode:choice("themeMode",THEME_MODE_PREFERENCES,(raw)=>THEME_MODE_PREFERENCES.includes(raw)?raw:DEFAULT_THEME_MODE),transparent:bool("transparentBackground",defaultTransparentBackground()),focusAccent:choice("focusAccent",FOCUS_ACCENT_SLOTS,(raw)=>FOCUS_ACCENT_SLOTS.includes(raw)?raw:"primary"),splitStyle:choice(SPLIT_STYLE_KEY,appearance("splitStyle"),normalizeSplitStyle),railFold:choice(RAIL_FOLD_STYLE_KEY,RAIL_FOLD_STYLES,(raw)=>RAIL_FOLD_STYLES.includes(raw)?raw:"glyphs"),workingBorder:choice(WORKING_BORDER_KEY,appearance("workingBorder"),(raw)=>normalizeWorkingBorder(raw??DEFAULT_WORKING_BORDER)),taskColors:choice(TASK_COLORS_KEY,appearance("taskColors"),(raw)=>normalizeTaskColors(raw??DEFAULT_TASK_COLORS)),glyphSet:choice(GLYPH_SET_KEY,appearance("glyphSet"),(raw)=>normalizeGlyphSet(raw??DEFAULT_GLYPH_SET)),colorblind:choice(COLORBLIND_KEY,appearance("colorblind"),(raw)=>normalizeColorblind(raw??DEFAULT_COLORBLIND)),runningTitle:choice(RUNNING_TITLE_KEY,appearance("runningTitle"),(raw)=>normalizeRunningTitle(raw??DEFAULT_RUNNING_TITLE)),toast:bool("notifications.toast.enabled",!0),sound:bool("notifications.sound.enabled",!0),soundVolume:choice(SOUND_VOLUME_KEY,SOUND_VOLUME_STEPS,(raw)=>normalizeSoundVolume(raw??DEFAULT_SOUND_VOLUME)),crossTask:bool("notifications.crossTask.enabled",!0),finishedAlerts:bool(FINISHED_ALERTS_KEY,!0),keyHints:bool(KEY_HINTS_ENABLED_KEY,!0,(on)=>on?Object.fromEntries(PANE_HINT_USED_KEYS.map((k)=>[k,!1])):{}),zen:bool(ZEN_ACTIVE_KEY,ZEN_DEFAULT),editorKind:choice(EDITOR_KIND_KEY,EDITOR_KINDS,(raw)=>normalizeEditorKind(raw??DEFAULT_EDITOR_KIND)),worktreeBase:{kind:"enum",choices:()=>WORKTREE_KINDS,read:(s)=>worktreeKind(s),write:(kind,s)=>{let current=text(s[WORKTREE_BASE_KEY]),remember=worktreeKind(s)==="custom"&&kind!=="custom"?{[WORKTREE_BASE_CUSTOM_KEY]:current}:{};if(kind==="default")return{...remember,[WORKTREE_BASE_KEY]:""};if(kind==="nextToProject")return{...remember,[WORKTREE_BASE_KEY]:PROJECT_SIBLING_BASE};let custom=worktreeCustomPath(s);if(!custom)throw new BridgeError("BAD_ARGS","no custom worktree base is remembered; set one in the TUI");return{[WORKTREE_BASE_KEY]:custom}}},scrollbackRows:{kind:"int",min:SCROLLBACK_ROWS_MIN,max:SCROLLBACK_ROWS_MAX,read:(s)=>normalizeScrollbackRows(s[SCROLLBACK_ROWS_KEY]??DEFAULT_SCROLLBACK_ROWS),write:(v)=>({[SCROLLBACK_ROWS_KEY]:normalizeScrollbackRows(v)})},tabStrip:{kind:"enum",choices:()=>TAB_STRIP_MODES,read:(s)=>resolveTabStripMode(s[TAB_STRIP_MODE_KEY],s[TAB_STRIP_HIDE_SINGLE_KEY]),write:(v)=>({[TAB_STRIP_MODE_KEY]:v})},prefixTap:choice(PREFIX_TAP_PRESENTATION_KEY,PREFIX_TAP_PRESENTATIONS,(raw)=>normalizePrefixTapPresentation(raw??DEFAULT_PREFIX_TAP_PRESENTATION)),remoteProjects:bool("experimental.remoteProjects",!1),autoStatus:bool(AUTO_STATUS_KEY,!1),dispatcher:bool(DISPATCHER_KEY,!1)};function prefInfo(state){return{editorCustomSet:text(state[EDITOR_CUSTOM_KEY]).length>0,worktreeCustomPath:worktreeCustomPath(state)}}function checkPrefValue(name,def,value,themes){let bad=(why)=>{throw new BridgeError("BAD_ARGS",`${name} ${why}`)};if(def.kind==="bool")return typeof value==="boolean"?value:bad("must be a boolean");if(def.kind==="int"){if(typeof value!=="number"||!Number.isInteger(value))return bad("must be an integer");return value<def.min||value>def.max?bad(`must be ${def.min}\u2013${def.max}`):value}let choices=def.choices(themes);if((typeof value==="string"||typeof value==="number")&&choices.includes(value))return value;return bad(`must be one of ${choices.join("|")}`)}var VIEWS=["browser","files","diff"];class PanelFeed{now;listeners=new Set;lastKey="";lastAt=0;constructor(now=Date.now){this.now=now}push(panel){if(!panel||typeof panel.taskId!=="string"||typeof panel.at!=="number"||!VIEWS.includes(panel.view))return;let{taskId,view,url,path,at}=panel,key=JSON.stringify([at,taskId,view,url,path]);if(at<this.lastAt||key===this.lastKey)return;if(this.lastAt=at,this.lastKey=key,this.now()-at>STALE_NOTICE_MS)return;let clean={taskId,view,at,...typeof url==="string"?{url}:{},...typeof path==="string"?{path}:{}};for(let listener of this.listeners)listener(clean)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}import{randomBytes as randomBytes2,scrypt,timingSafeEqual as timingSafeEqual3}from"crypto";import{chmodSync as chmodSync2,existsSync as existsSync2,mkdirSync as mkdirSync2,readFileSync as readFileSync2,renameSync,rmSync,writeFileSync as writeFileSync2}from"fs";import{dirname as dirname2,join}from"path";var COST={N:32768,r:8,p:1},KEY_LENGTH=32,MIN_PASSWORD_LENGTH=8,MAX_PASSWORD_LENGTH=1024;function webPasswordPaths(bridgeDir){return{password:join(bridgeDir,"web-password.json"),lockout:join(bridgeDir,"web-login-lock.json")}}function derive(password,salt,cost){return new Promise((resolve,reject)=>{scrypt(password,salt,KEY_LENGTH,{...cost,maxmem:256*cost.N*cost.r},(err,key)=>err?reject(err):resolve(key))})}async function hashPassword(password){let salt=randomBytes2(16),key=await derive(password,salt,COST);return{algo:"scrypt",...COST,salt:salt.toString("base64"),hash:key.toString("base64")}}async function verifyPassword(record,password){let expected=Buffer.from(record.hash,"base64"),key=await derive(password,Buffer.from(record.salt,"base64"),record);return key.length===expected.length&&timingSafeEqual3(key,expected)}function writeOwnerOnly(path,body){mkdirSync2(dirname2(path),{recursive:!0,mode:448}),chmodSync2(dirname2(path),448);let tmp=`${path}.${process.pid}.tmp`;writeFileSync2(tmp,body,{mode:384}),chmodSync2(tmp,384),renameSync(tmp,path)}function readPassword(path){if(!existsSync2(path))return null;try{let raw=JSON.parse(readFileSync2(path,"utf8")),pow2=(n)=>typeof n==="number"&&Number.isInteger(Math.log2(n));if(raw.algo!=="scrypt"||!pow2(raw.N)||raw.N<COST.N||raw.N>1048576)return null;if(typeof raw.r!=="number"||typeof raw.p!=="number")return null;if(typeof raw.salt!=="string"||typeof raw.hash!=="string")return null;return raw}catch{return null}}function writePassword(path,record){writeOwnerOnly(path,`${JSON.stringify(record,null,2)}
|
|
96
|
+
`)}function clearPassword(path){if(!existsSync2(path))return!1;return rmSync(path,{force:!0}),!0}var IP_FAILURE_LIMIT=5,IP_LOCK_MS=900000,GLOBAL_FAILURE_LIMIT=20,GLOBAL_WINDOW_MS=86400000,EMPTY_LOCKOUT={ips:{},failures:[],globalLocked:!1};function lockOf(state,ip,now){if(state.globalLocked)return{kind:"global"};let until=state.ips[ip]?.lockedUntil;return until&&until>now?{kind:"ip",retryAfterMs:until-now}:null}function prune(state,now){let ips={};for(let[ip,rec]of Object.entries(state.ips))if(rec.lockedUntil!==null&&rec.lockedUntil>now||now-rec.last<GLOBAL_WINDOW_MS)ips[ip]=rec;return{...state,ips,failures:state.failures.filter((t)=>now-t<GLOBAL_WINDOW_MS)}}function recordFailure(prev,ip,now){let state=prune(prev,now),old=state.ips[ip],failures=(old&&old.lockedUntil!==null&&old.lockedUntil<=now?0:old?.failures??0)+1,lockedUntil=failures>=IP_FAILURE_LIMIT?now+IP_LOCK_MS:null,all=[...state.failures,now],next={ips:{...state.ips,[ip]:{failures,lockedUntil,last:now}},failures:all,globalLocked:state.globalLocked||all.length>=GLOBAL_FAILURE_LIMIT};return{state:next,lock:lockOf(next,ip,now)}}function recordSuccess(state,ip){let{[ip]:_,...rest}=state.ips;return{...state,ips:rest}}function readLockout(path){if(!existsSync2(path))return EMPTY_LOCKOUT;try{let raw=JSON.parse(readFileSync2(path,"utf8")),ips={};for(let[ip,rec]of Object.entries(raw.ips??{}))if(rec&&typeof rec.failures==="number"&&typeof rec.last==="number")ips[ip]={failures:rec.failures,last:rec.last,lockedUntil:rec.lockedUntil??null};let failures=Array.isArray(raw.failures)?raw.failures.filter((t)=>typeof t==="number"):[];return{ips,failures,globalLocked:raw.globalLocked===!0}}catch{return{...EMPTY_LOCKOUT,globalLocked:!0}}}function writeLockout(path,state){writeOwnerOnly(path,`${JSON.stringify(state)}
|
|
97
|
+
`)}function promptHidden(question){let stdin=process.stdin;if(!stdin.isTTY)return Promise.reject(Error("--set-password needs an interactive terminal"));return process.stdout.write(question),stdin.setRawMode(!0),stdin.setEncoding("utf8"),stdin.resume(),new Promise((resolve,reject)=>{let value="",finish=()=>{stdin.off("data",onData),stdin.setRawMode(!1),stdin.pause(),process.stdout.write(`
|
|
98
|
+
`)},onData=(chunk)=>{for(let ch of chunk){if(ch==="\r"||ch===`
|
|
99
|
+
`){finish(),resolve(value);return}if(ch==="\x03"||ch==="\x04"){finish(),reject(Error("cancelled"));return}if(ch==="\x7F"||ch==="\b")value=[...value].slice(0,-1).join("");else if(ch>=" ")value+=ch}};stdin.on("data",onData)})}async function runPasswordAction(action,bridgeDir,prompt=promptHidden){let paths=webPasswordPaths(bridgeDir);if(action==="unlock")return writeLockout(paths.lockout,EMPTY_LOCKOUT),console.log("password login unlocked"),0;if(action==="clear-password")return console.log(clearPassword(paths.password)?"browser password removed":"no browser password was set"),writeLockout(paths.lockout,EMPTY_LOCKOUT),0;let first=await prompt("New browser password: ");if(first.length<MIN_PASSWORD_LENGTH||first.length>MAX_PASSWORD_LENGTH)return console.error(`the password must be ${MIN_PASSWORD_LENGTH}-${MAX_PASSWORD_LENGTH} characters; nothing changed`),1;if(await prompt("Again: ")!==first)return console.error("the two entries differ; nothing changed"),1;return writePassword(paths.password,await hashPassword(first)),writeLockout(paths.lockout,EMPTY_LOCKOUT),console.log(`browser password set (${paths.password}). A page without a token now shows a password field;
|
|
100
|
+
each login gets its own device token (see --list-devices).`),0}class PromptFeed{now;listeners=new Set;seen=new Set;constructor(now=Date.now){this.now=now}push(prompt){if(!prompt||typeof prompt.promptId!=="string"||typeof prompt.title!=="string")return;if(typeof prompt.at!=="number"||this.now()-prompt.at>600000)return;if(this.seen.has(prompt.promptId))return;this.seen.add(prompt.promptId);let{promptId,title,placeholder,initial,at}=prompt,clean={promptId,title,at,...typeof placeholder==="string"?{placeholder}:{},...typeof initial==="string"?{initial}:{}};for(let listener of this.listeners)listener(clean)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}}import{createHash as createHash2}from"crypto";import{existsSync as existsSync3,readFileSync as readFileSync3}from"fs";import{createCipheriv,createECDH,createPrivateKey,hkdfSync,randomBytes as randomBytes3,sign}from"crypto";var RECORD_SIZE=4096,VAPID_SUBJECT="https://rove.run",JWT_LIFETIME_S=43200,b64url=(bytes)=>Buffer.from(bytes).toString("base64url"),fromB64url=(text2)=>Buffer.from(text2,"base64url");function generateVapidKeys(){let ecdh=createECDH("prime256v1");return ecdh.generateKeys(),{publicKey:b64url(ecdh.getPublicKey()),privateKey:b64url(ecdh.getPrivateKey())}}var hkdf=(ikm,salt,info,length)=>Buffer.from(hkdfSync("sha256",ikm,salt,info,length));function encryptPayload(plaintext,keys,fixed={}){let uaPublic=fromB64url(keys.p256dh),authSecret=fromB64url(keys.auth),server=createECDH("prime256v1");if(fixed.serverPrivateKey)server.setPrivateKey(Buffer.from(fixed.serverPrivateKey));else server.generateKeys();let asPublic=server.getPublicKey(),ecdhSecret=server.computeSecret(uaPublic),keyInfo=Buffer.concat([Buffer.from("WebPush: info\x00"),uaPublic,asPublic]),ikm=hkdf(ecdhSecret,authSecret,keyInfo,32),salt=Buffer.from(fixed.salt??randomBytes3(16)),cek=hkdf(ikm,salt,Buffer.from("Content-Encoding: aes128gcm\x00"),16),nonce=hkdf(ikm,salt,Buffer.from("Content-Encoding: nonce\x00"),12),cipher=createCipheriv("aes-128-gcm",cek,nonce),sealed=Buffer.concat([cipher.update(Buffer.concat([plaintext,Buffer.from([2])])),cipher.final()]),header=Buffer.alloc(21);return salt.copy(header,0),header.writeUInt32BE(RECORD_SIZE,16),header.writeUInt8(asPublic.length,20),Buffer.concat([header,asPublic,sealed,cipher.getAuthTag()])}function vapidAuthorization(endpoint,vapid,nowMs=Date.now()){let pub=fromB64url(vapid.publicKey),key=createPrivateKey({format:"jwk",key:{kty:"EC",crv:"P-256",d:vapid.privateKey,x:b64url(pub.subarray(1,33)),y:b64url(pub.subarray(33,65))}}),part=(value)=>b64url(Buffer.from(JSON.stringify(value))),claims={aud:new URL(endpoint).origin,exp:Math.floor(nowMs/1000)+JWT_LIFETIME_S,sub:VAPID_SUBJECT},unsigned=`${part({typ:"JWT",alg:"ES256"})}.${part(claims)}`,signature=sign("sha256",Buffer.from(unsigned),{key,dsaEncoding:"ieee-p1363"});return`vapid t=${unsigned}.${b64url(signature)}, k=${vapid.publicKey}`}var PUSH_HOSTS=[/^fcm\.googleapis\.com$/,/(^|\.)push\.apple\.com$/,/^updates\.push\.services\.mozilla\.com$/,/(^|\.)notify\.windows\.com$/];function isPushEndpoint(endpoint){let url;try{url=new URL(endpoint)}catch{return!1}return url.protocol==="https:"&&url.port===""&&!url.username&&PUSH_HOSTS.some((host)=>host.test(url.hostname))}async function sendPush(target,payload,vapid,fetchImpl=fetch){let body=encryptPayload(Buffer.from(JSON.stringify(payload)),target);return(await fetchImpl(target.endpoint,{method:"POST",headers:{Authorization:vapidAuthorization(target.endpoint,vapid),"Content-Encoding":"aes128gcm","Content-Type":"application/octet-stream",TTL:String(86400),Urgency:"high"},body,signal:AbortSignal.timeout(1e4)})).status}var MAX_SUBSCRIPTIONS=32,sharedCredential=(token)=>`token:${createHash2("sha256").update(token).digest("hex").slice(0,32)}`;class PushHub{deps;file;visible=new Set;previous=null;unsubscribeFeed=null;constructor(deps){this.deps=deps;this.file=this.load(),this.follow()}publicKey(){return this.file.vapid.publicKey}subscribe(raw,credential){let target=parseSubscription(raw),now=(this.deps.now??(()=>new Date))().toISOString(),others=this.file.subscriptions.filter((s)=>s.endpoint!==target.endpoint);if(others.length>=MAX_SUBSCRIPTIONS)throw new BridgeError("TOO_MANY",`at most ${MAX_SUBSCRIPTIONS} browsers can be subscribed`);this.save([...others,{...target,credential,createdAt:now}])}unsubscribe(endpoint,credential){let before=this.file.subscriptions,after=before.filter((s)=>!(s.endpoint===endpoint&&s.credential===credential));if(after.length===before.length)return!1;return this.save(after),!0}subscribed(endpoint,credential){return this.file.subscriptions.some((s)=>s.endpoint===endpoint&&s.credential===credential)}setVisible(viewer,visible){if(visible)this.visible.add(viewer);else this.visible.delete(viewer)}follow(){let want=this.file.subscriptions.length>0;if(want&&!this.unsubscribeFeed)this.previous=null,this.unsubscribeFeed=this.deps.feed.subscribe((payload)=>void this.onTasks(payload));else if(!want&&this.unsubscribeFeed)this.unsubscribeFeed(),this.unsubscribeFeed=null}async onTasks(payload){let next=new Map;for(let task of payload.tasks)if(task.activity)next.set(task.id,task.activity.state);let edges=attentionEdges(this.previous,next,null,attentionKindFor);if(this.previous=next,edges.length===0||this.visible.size>0||!this.deps.crossTask())return;let titles=new Map(payload.tasks.map((task)=>[task.id,task.title])),live=this.file.subscriptions.filter((s)=>this.deps.credentialValid(s.credential)),gone=new Set(this.file.subscriptions.filter((s)=>!live.includes(s)).map((s)=>s.endpoint)),send=this.deps.send??((target,message,vapid)=>sendPush(target,message,vapid));for(let{key,kind}of edges){let message={taskId:key,kind,title:titles.get(key)??key};await Promise.all(live.map(async(s)=>{try{let status=await send(s,message,this.file.vapid);if(status===404||status===410)gone.add(s.endpoint);else if(status>=400)this.deps.log?.(`[rove-bridge] push ${new URL(s.endpoint).host}: HTTP ${status}`)}catch(err){this.deps.log?.(`[rove-bridge] push ${new URL(s.endpoint).host}: ${String(err)}`)}}))}if(gone.size>0)this.save(this.file.subscriptions.filter((s)=>!gone.has(s.endpoint)))}load(){try{if(existsSync3(this.deps.path)){let raw=JSON.parse(readFileSync3(this.deps.path,"utf8"));if(raw?.vapid?.publicKey&&raw.vapid.privateKey&&Array.isArray(raw.subscriptions))return raw}}catch{}let fresh={vapid:generateVapidKeys(),subscriptions:[]};return writeOwnerOnly(this.deps.path,`${JSON.stringify(fresh,null,2)}
|
|
101
|
+
`),fresh}save(subscriptions){this.file={...this.file,subscriptions},writeOwnerOnly(this.deps.path,`${JSON.stringify(this.file,null,2)}
|
|
102
|
+
`),this.follow()}}function parseSubscription(raw){let sub=raw,endpoint=sub?.endpoint;if(typeof endpoint!=="string"||!isPushEndpoint(endpoint))throw new BridgeError("BAD_ARGS","endpoint must be a browser push service URL");let{p256dh,auth}=sub?.keys??{};if(typeof p256dh!=="string"||fromB64url(p256dh).length!==65||fromB64url(p256dh)[0]!==4)throw new BridgeError("BAD_ARGS","keys.p256dh must be an uncompressed P-256 key");if(typeof auth!=="string"||fromB64url(auth).length!==16)throw new BridgeError("BAD_ARGS","keys.auth must be 16 bytes");return{endpoint,p256dh,auth}}import{isAbsolute,normalize}from"path";function gateRows(gates,tasks){let titles=new Map(tasks.map((t)=>[t.id,t.title]));return gates.filter((g)=>g.status==="open"||g.status==="answered"&&g.handoff==="undelivered").map((g)=>({id:g.id,taskId:g.taskId,tabId:g.tabId,question:g.question,options:g.options.map((o)=>({key:o.key,label:o.label,...o.detail?{detail:o.detail}:{}})),...g.recommended?{recommended:g.recommended}:{},...g.context?{context:g.context}:{},status:g.status,...g.status==="answered"&&g.handoff?{handoff:g.handoff}:{},createdAt:g.createdAt,label:titles.get(g.taskId)??g.taskId})).sort((a,b)=>a.createdAt.localeCompare(b.createdAt)||a.id.localeCompare(b.id,void 0,{numeric:!0}))}function dismissHitsOnlyGates(attention,taskId,tabId){let here=attention.filter((r)=>r.taskId===taskId&&r.tabId===(tabId??null));return here.length>0&&here.every((r)=>r.state==="decision_needed")}function flag(name,value){return`--${name}=${value}`}function oneOf(args,name,values){let v=args[name];if(typeof v!=="string"||!values.includes(v))throw new BridgeError("BAD_ARGS",`${name} must be one of ${values.join("|")}`);return v}function optOneOf(args,name,values){let v=args[name];if(v===void 0||v===null||v==="")return;return oneOf(args,name,values)}function bool2(args,name){let v=args[name];if(typeof v!=="boolean")throw new BridgeError("BAD_ARGS",`${name} must be a boolean`);return v}function text2(args,name,max){let v=args[name];if(typeof v!=="string"||v.trim().length===0)throw new BridgeError("BAD_ARGS",`${name} must be non-empty text`);if(v.length>max)throw new BridgeError("BAD_ARGS",`${name} is longer than ${max} characters`);return v}function optText(args,name,max){let v=args[name];if(v===void 0||v===null||v==="")return;return text2(args,name,max)}function sendReason(reason){if(reason===void 0||reason===null||reason==="")return;return typeof reason==="string"?reason:JSON.stringify(reason)}function taskId(args,name="taskId"){let v=args[name];if(typeof v!=="string"||!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(v))throw new BridgeError("BAD_ARGS",`${name} is not a task id`);return v}function tabId(args,name="tabId"){let v=args[name];if(typeof v!=="string"||!/^tab-\d{1,6}$/.test(v))throw new BridgeError("BAD_ARGS",`${name} is not a tab id`);return v}function absPath(args,name){let v=args[name];if(typeof v!=="string"||!v.startsWith("/")||v.includes("\x00"))throw new BridgeError("BAD_ARGS",`${name} must be an absolute path`);return v}import{execFile}from"child_process";function projectKey(path){try{return resolveMainRepoRoot(path)}catch{return path}}var taskOpDeps={savedRepos:()=>getSavedRepos(),rememberEngine:(repo,engine)=>{setRepoLastActiveVendor(projectKey(repo),engine)},preferredEngine:(repo)=>resolvePreferredVendor(repo===void 0?void 0:projectKey(repo)),listLocalBranches,getCurrentBranch,validateRepoPath,cloneRepo,validateGitUrl,validateCloneTarget,resolveCloneTarget,findAvailableFolderName,deriveFolderName,gh:runGh,repoId:(path)=>repoIdOf(path),fetchPullHead},GIT_REF=/^[A-Za-z0-9][A-Za-z0-9._/-]{0,199}$/,MODEL=/^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,99}$/,EFFORT_LEVEL=/^[a-z0-9][a-z0-9-]{0,19}$/,ENGINE_ID=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/,FOLDER=/^[A-Za-z0-9_][A-Za-z0-9._-]{0,99}$/;function gitRef(args,name){let v=args[name];if(typeof v!=="string"||!GIT_REF.test(v)||v.includes("..")||v.includes("//")||v.includes("/.")||v.endsWith("/")||v.endsWith(".")||v.endsWith(".lock"))throw new BridgeError("BAD_ARGS",`${name} is not a valid git ref name`);return v}function optGitRef(args,name){let v=args[name];return v===void 0||v===null||v===""?void 0:gitRef(args,name)}function model(args,name="model"){let v=args[name];if(typeof v!=="string"||!MODEL.test(v))throw new BridgeError("BAD_ARGS",`${name} is not a model id`);return v}function optModel(args,name="model"){let v=args[name];return v===void 0||v===null||v===""?void 0:model(args,name)}function effortLevel(args,name="level"){let v=args[name];if(typeof v!=="string"||!EFFORT_LEVEL.test(v))throw new BridgeError("BAD_ARGS",`${name} is not an effort level`);return v}async function knownEngineIds(ctx){let res=await ctx.api.verb("engine-list",[]);return new Set((res.engines??[]).map((e)=>e.id))}function checkEngineShape(id,name){if(typeof id!=="string"||!ENGINE_ID.test(id))throw new BridgeError("BAD_ARGS",`${name} is not an engine id`);return id}async function engineId(args,name,ctx){let id=checkEngineShape(args[name],name);if(!(await knownEngineIds(ctx)).has(id))throw new BridgeError("UNKNOWN_ENGINE",`${name} ${id} is not a known engine`);return id}var FANOUT_CAP=10;function parseAgents(args,name="agents"){let raw=args[name];if(typeof raw!=="string"||raw.length===0||raw.length>300)throw new BridgeError("BAD_ARGS",`${name} must be engine:count pairs like claude:2,codex:1`);let plan=[],total=0;for(let part of raw.split(",")){let m=/^([^:]+):(\d{1,2})$/.exec(part);if(!m)throw new BridgeError("BAD_ARGS",`${name} entry "${part}" must be engine:count`);let id=checkEngineShape(m[1],name),n=Number.parseInt(m[2],10);if(n<1)throw new BridgeError("BAD_ARGS",`${name} count for ${id} must be at least 1`);if(plan.some((p)=>p.id===id))throw new BridgeError("BAD_ARGS",`${name} names ${id} twice`);if(total+=n,total>FANOUT_CAP)throw new BridgeError("BAD_ARGS",`${name} asks for more than ${FANOUT_CAP} agents`);plan.push({id,n})}return plan}async function checkAgentEngines(plan,ctx){let known=await knownEngineIds(ctx);for(let{id}of plan)if(!known.has(id))throw new BridgeError("UNKNOWN_ENGINE",`agents engine ${id} is not a known engine`)}async function knownRepo(args,ctx,name="repo"){let repo=absPath(args,name);if(taskOpDeps.savedRepos().includes(repo))return repo;let{tasks}=await ctx.api.rpc("task.list");if(!tasks.some((t)=>t.repo===repo))throw new BridgeError("UNKNOWN_REPO",`${repo} is not a known project`);return repo}function runGh(args,timeoutMs){let{promise,resolve}=Promise.withResolvers();return execFile("gh",[...args],{timeout:timeoutMs,maxBuffer:8000000,env:{...process.env,GH_PROMPT_DISABLED:"1",NO_COLOR:"1"}},(err,stdout,stderr)=>{if(!err)return resolve({ok:!0,stdout});if(err.code==="ENOENT")return resolve({ok:!1,reason:"missing"});let signedOut=/gh auth login|not logged in|authentication/i.test(stderr);resolve({ok:!1,reason:signedOut?"unauthenticated":"failed"})}),promise}function fetchPullHead(repo,number){let{promise,resolve}=Promise.withResolvers(),ref=`refs/rove/pr/${number}`;return execFile("git",["-C",repo,"fetch","--no-tags","--quiet","origin",`+refs/pull/${number}/head:${ref}`],{timeout:60000,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}},(err,_stdout,stderr)=>resolve(err?{ok:!1,error:stderr.trim()||String(err.message)}:{ok:!0,ref})),promise}var SHORTHAND=/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;async function githubCloneUrl(shorthand){let name=shorthand.replace(/\.git$/i,""),protocol=await taskOpDeps.gh(["config","get","git_protocol"],3000);return protocol.ok&&protocol.stdout.trim()==="ssh"?`git@github.com:${name}.git`:`https://github.com/${name}.git`}async function cloneSource(args){let raw=typeof args.url==="string"?args.url.trim():"";if(SHORTHAND.test(raw)&&!/\/\.+(git)?$/i.test(raw))return cloneUrl({url:await githubCloneUrl(raw)});return cloneUrl(args)}var REPO_ID=/^(?:[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z0-9.-]+(?:\/[A-Za-z0-9._-]+){2,}|[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100})$/;function remoteRepoRef(args,name="repo"){let v=args[name];if(typeof v==="string"&&v.startsWith("/"))return absPath(args,name);if(typeof v==="string"&&REPO_ID.test(v))return v;try{return cloneUrl(args,name)}catch{throw new BridgeError("BAD_ARGS",`${name} must be an absolute path or a repo id (owner/name, host/owner/name, or a clone URL)`)}}var URL_SCHEME=/^(https?|ssh|git):\/\/[A-Za-z0-9[]\S*$/i,URL_SCP=/^[A-Za-z0-9._-]+@[A-Za-z0-9][A-Za-z0-9.-]*:\S+$/;function cloneUrl(args,name="url"){let v=args[name];if(typeof v!=="string")throw new BridgeError("BAD_ARGS",`${name} must be a git URL`);let url=v.trim(),lower=url.toLowerCase();if(url.length===0||url.length>2000||url.startsWith("-")||lower.startsWith("ext::")||lower.startsWith("fd::")||lower.startsWith("file:")||url.includes("@-")||/\p{Cc}/u.test(url)||!(URL_SCHEME.test(url)||URL_SCP.test(url)))throw new BridgeError("BAD_ARGS",`${name} must be an https, http, ssh or git URL (or user@host:path)`);return url}function cloneFolder(folder){if(!FOLDER.test(folder)||folder==="."||folder==="..")throw new BridgeError("BAD_ARGS","folder must be one plain directory name");return folder}function rowLabels(tasks,sources){let tabsByTask=new Map;for(let task of tasks){let snapshot=sources?.tabs(task.id);if(!snapshot)continue;let vendor=task.vendor??DEFAULT_TASK_VENDOR;tabsByTask.set(task.id,snapshot.tabs.map((tab)=>{let title=conversationTitle(tab,vendor,sources?.sessions.get(tabPtyKeyFor(task.id,tab)));return{id:tab.id,...title?{title}:{},engine:tab.kind==="engine"}}))}return new Map(tasks.map((task)=>{let liveTitle=conversationTitleOf(task,tabsByTask);return[task.id,worktreeRowLabel(task,liveTitle?{liveTitle}:{})]}))}function engineFor(task,engines){let command=task.command?.trim(),preset=(command?engines.find((e)=>e.id===command||e.command===command):void 0)??engines.find((e)=>e.builtin&&e.protocol===task.vendor);if(preset)return{id:preset.id,name:preset.name};return task.vendor?{id:null,name:task.vendor}:null}function rowChanges(map,worktreePath){if(!map||!map.has(worktreePath))return;let c=map.get(worktreePath);if(!c)return{unreadable:!0};return{added:c.added,deleted:c.deleted,...c.ahead!==void 0?{ahead:c.ahead}:{},...c.behind!==void 0?{behind:c.behind}:{}}}function rowTokenChips(map,taskId2,now){return liveRowTokens(map,taskId2,now).map((t)=>({text:t.text,...t.tone?{tone:t.tone}:{},source:t.source,expiresAt:t.expiresAt}))}var PR_CHIP_BY_GLYPH={[DEFAULT_GLYPHS.conflict]:"conflict",[DEFAULT_GLYPHS.checksFailing]:"failing",[DEFAULT_GLYPHS.checksPassing]:"passing"};function prChipKind(task){let chip=prChip(task);return chip?PR_CHIP_BY_GLYPH[chip.glyph]??null:null}function rowPr(pr){return{...pr.number!==void 0?{number:pr.number}:{},...pr.url?{url:pr.url}:{},...pr.title?{title:pr.title}:{},...pr.additions!==void 0?{additions:pr.additions}:{},...pr.deletions!==void 0?{deletions:pr.deletions}:{},lifecycle:pr.lifecycle,checkState:pr.checkState,...pr.reviewDecision?{reviewDecision:pr.reviewDecision}:{},...pr.mergeable?{mergeable:pr.mergeable}:{}}}function sessionPrRows(task){return(task.sessionPrs??[]).map((entry)=>{if(entry.status)return rowPr({...entry.status,url:entry.status.url??entry.url});let number=Number(entry.url.match(/\/pull\/(\d+)$/)?.[1]);return{...Number.isInteger(number)?{number}:{},url:entry.url,lifecycle:"unknown",checkState:"unknown"}})}function attentionRows(items,tasks){let byId=new Map(tasks.map((t)=>[t.id,t]));return items.map((i)=>{let resumeAt=i.state==="rate_limited"&&i.taskId?byId.get(i.taskId)?.quotaResume?.resumeAt:void 0,label=i.detail?.routine?.name,deliveryId=i.state==="report_missing"?i.detail?.delivery?.id:void 0,gateId=i.state==="decision_needed"?i.detail?.gate?.id:void 0,automationId=i.detail?.routine?.automationId;return{taskId:i.taskId,tabId:i.tabId,state:i.state,unread:i.unread,at:i.at,...resumeAt?{resumeAt}:{},...label?{label}:{},...deliveryId?{deliveryId}:{},...gateId?{gateId}:{},...automationId?{automationId}:{}}})}function machineOf2(task){let origin=task.origin;if(!origin||origin.machineId==="local")return{};return{machine:{id:origin.machineId,label:origin.hostLabel,...origin.stale?{stale:!0}:{}}}}function remoteRowStatus(activity){let tasks={};for(let[taskId2,entry]of activity){let tabless=tabStatus({activity:{state:entry.state,at:entry.at},viewed:!1});tasks[taskId2]={task:taskStatus({},{tabless}),tabs:{}}}return{tasks,liveness:!1}}function groupingInputs(status){if(!status)return{activity:null,liveTaskIds:null};let activity={},live=new Set;for(let[taskId2,{task,tabs}]of Object.entries(status.tasks)){if(task.state)activity[taskId2]={state:task.state,at:task.since};if(Object.values(tabs).some((tab)=>tab.alive))live.add(taskId2)}return{activity,liveTaskIds:status.liveness?live:null}}function taskRows(tasks,status,engines,now,extras){let byId=new Map(tasks.map((t)=>[t.id,t])),orderOf=new Map(tasks.map((t,i)=>[t.id,i]));return buildContext({repo:"",tasks,...groupingInputs(status),attention:[],gates:[],notes:[],now,limit:Number.MAX_SAFE_INTEGER}).tasks.flatMap((row)=>{let task=byId.get(row.taskId);if(!task)return[];let pr=rollupPr(task),changes=rowChanges(extras?.changes??null,task.worktreePath),sessionPrs=sessionPrRows(task),tokens=rowTokenChips(extras?.tokens,task.id,now),chip=prChipKind(task),daemon=status?.tasks[task.id];return[{id:task.id,title:task.title,label:extras?.labels?.get(task.id)??task.title,branch:task.branch,repo:task.repo,kind:task.kind??"task",status:task.status,group:row.group,rank:row.rank,activity:row.activity?{...row.activity,since:now-row.activity.forMs}:null,...daemon?{rowStatus:daemon.task,tabStatus:daemon.tabs}:{},engine:engineFor(task,engines),pr:pr?rowPr(pr):null,...sessionPrs.length>0?{sessionPrs}:{},report:task.report?{summary:task.report.summary??"",at:task.report.at}:null,deleting:task.deletion?.phase==="queued"||task.deletion?.phase==="running",pinned:task.pinned===!0,...task.groupId?{groupId:task.groupId}:{},...task.kept?{kept:!0}:{},...task.autoFixCi?{autoFixCi:{attempts:task.autoFixCi.attempts,...task.autoFixCi.stopped?{stopped:!0}:{}}}:{},...task.notes?{notes:task.notes}:{},...task.todos?{todos:task.todos}:{},...task.archived?{archived:!0}:{},...task.routine?{routine:!0}:{},...task.scratch?{scratch:!0}:{},...task.noProject?{noProject:!0}:{},order:orderOf.get(task.id)??0,createdAt:task.createdAt,updatedAt:task.updatedAt,...machineOf2(task),...task.repoId?{repoId:task.repoId}:{},...task.extraRepos?.length?{extraRepoCount:task.extraRepos.length}:{},...task.parentTaskId?{parentTaskId:task.parentTaskId}:{},...task.baseRef?{baseRef:task.baseRef}:{},...task.sharesWorkspaceOf?{sharesWorkspaceOf:task.sharesWorkspaceOf}:{},...changes?{changes}:{},...tokens.length>0?{rowTokens:tokens}:{},prChip:chip,...task.prompt!==void 0?{hasPrompt:!0}:{},...task.scheduledStart?{startsAt:task.scheduledStart.at}:{},prChipStale:chip!==null&&pr?.lastError!==void 0,...task.worktreePath?{}:{noWorktree:!0}}]})}function newTabInvocation(taskId2,tab){let command=tab.engine?[flag("command",tab.engine)]:[];if(tab.prompt===void 0&&!tab.engine&&tab.latest)command.push("--latest");if(tab.replaceTab!==void 0)return{verb:"send",argv:[flag("task-id",taskId2),"--tab=new","--plain",flag("handoff-from",tab.replaceTab),"--replace",...tab.prompt?[flag("prompt",tab.prompt)]:[],...command]};if(tab.prompt!==void 0)return{verb:"send",argv:[flag("task-id",taskId2),"--tab=new","--plain",flag("prompt",tab.prompt),...command]};return{verb:"tab-new",argv:[flag("task-id",taskId2),flag("kind",tab.kind),...command]}}function tabRows(rows,statuses){let nameOf=(vendor)=>vendor?engineDisplayName(vendor):null;return rows.map((t)=>({id:t.id,kind:t.kind,title:t.title??t.lastTitle??t.autoTitle,engineName:t.kind==="engine"?nameOf(t.liveVendor??t.vendor):null,conversation:t.kind==="engine"?conversationTitle({...t,kind:"engine",vendor:t.vendor??void 0,liveVendor:t.liveVendor??void 0},t.liveVendor??t.vendor??DEFAULT_TASK_VENDOR,t.liveTitle):null,alive:t.alive,engineAlive:t.engineAlive,...t.exit?{exit:{code:t.exit.code,signal:t.exit.signal}}:{},...t.split?{split:t.split}:{},...t.viewOf?{viewOf:t.viewOf}:{},...statuses?.[t.id]?{status:statuses[t.id]}:{}}))}async function tryRead(read){try{return await read()}catch{return null}}var ENGINE_CACHE_MS=60000;function createRoveOps(client,signals,api=createBridgeApi(client)){let engineCache=null,engineLoad=null,engines=async()=>{let rows;if(engineCache&&Date.now()-engineCache.at<ENGINE_CACHE_MS)rows=engineCache.engines;else engineLoad??=loadEngines().finally(()=>{engineLoad=null}),rows=await engineLoad;let off=getDisabledEngineIds();return rows.map((row)=>({...row,enabled:!off.includes(row.id)}))},loadEngines=async()=>{let res=await api.verb("engine-list",[]),rows=await Promise.all(res.engines.map(async({id,name,command,protocol,builtin,models})=>{let status=await tryRead(()=>detectEngineStatus(id));return{id,name,command,protocol,builtin,models:models??null,effortLevels:[...engineEntry(protocol).effortLevels??[]],acceptsModel:engineEntry(protocol).modelArgv!==void 0,...status?{ready:status.binary.found&&status.account?.kind!=="none"}:{}}}));return engineCache={at:Date.now(),engines:rows},rows},getTask=async(taskId2)=>{return(await api.verb("get-task",[flag("task-id",taskId2)])).task};return{async tasks(){let{tasks}=await client.request("task.list"),[inbox,engineRows,titleSources]=await Promise.all([tryRead(()=>client.request("attention.list")),tryRead(engines),tryRead(async()=>await signals?.titleSources?.()??null)]),now=Date.now(),statuses=signals?.rowStatus?.()??null,localStatus=statuses?{tasks:statuses,liveness:!0}:null,remoteTasks=signals?.remote?.()??[],labels=rowLabels([...tasks,...remoteTasks],titleSources),localExtras=signals?{changes:signals.changes(),tokens:signals.tokens(),labels}:void 0,pushed=signals?.remoteLive?.(),remoteStatus=pushed?remoteRowStatus(pushed.activity):null,remoteExtras=pushed?{changes:pushed.changes,tokens:pushed.tokens,labels}:void 0,rowsOf=(archived2)=>{let pick=(list)=>list.filter((t)=>t.archived===!0===archived2),local=taskRows(pick(tasks),localStatus,engineRows??[],now,localExtras),remote=taskRows(pick(remoteTasks),remoteStatus,engineRows??[],now,remoteExtras).map((row)=>({...row,order:(row.order??0)+tasks.length}));return[...local,...remote]},archived=rowsOf(!0);return{tasks:rowsOf(!1),attention:attentionRows(inbox?.items??[],tasks),gates:gateRows(signals?.gates()??[],tasks),...archived.length>0?{archived}:{}}},engines,async repos(){let{tasks}=await client.request("task.list");return[...new Set([...getSavedRepos(),...tasks.map((t)=>t.repo)])].sort()},async createTask({repo,engine,prompt,title}){let argv=[flag("repo",repo)];if(engine)argv.push(flag("command",engine));if(title)argv.push(flag("title",title));if(prompt)argv.push(flag("prompt",prompt));return{taskId:(await api.verb("add",argv)).taskId}},async deleteTask(taskId2,force,cascade){let argv=[flag("task-id",taskId2),"--wait"];if(force)argv.push("--force");if(cascade)argv.push("--cascade");let res=await api.verb("delete",argv);return{status:res.status??(res.queued?"queued":"removed")}},async landTask(taskId2,strategy){let res=await api.verb("land",[flag("task-id",taskId2),flag("strategy",strategy)]);if(res.landedOn!==void 0)return{landedOn:res.landedOn,commit:res.commit??""};return{landedOn:res.pr?.url??`${res.branch??"branch"} (pushed)`,commit:"",published:{branch:res.branch??"",prUrl:res.pr?.url??null,prError:res.prError??null}}},async tabs(taskId2){let res=await api.verb("get-task",[flag("task-id",taskId2)]);return tabRows(res.tabs??[],signals?.rowStatus?.()?.[taskId2]?.tabs)},async newTab(taskId2,tab){let call=newTabInvocation(taskId2,tab),res=await api.verb(call.verb,call.argv),tabId2=res.tabId??res.session?.split("::")[1];if(!tabId2)throw new BridgeError("SESSION_FAILED","the new tab did not report its session");if(tab.engine&&api.machineOf?.(taskId2)===void 0){let task=await tryRead(()=>getTask(taskId2));if(task)taskOpDeps.rememberEngine(task.repo,tab.engine)}return{tabId:tabId2,...res.childTaskId?{taskId:res.childTaskId}:{}}},async closeTab(taskId2,tabId2){await api.verb("tab-close",[flag("task-id",taskId2),flag("tab",tabId2)])},async reviveTab(taskId2,tabId2,restartExited){let res=await api.verb("tab-revive",[flag("task-id",taskId2),flag("tab",tabId2),...restartExited?["--restart-exited"]:[]]);return{revived:res.revived,resumed:res.resumed}},worktreeOf:async(taskId2)=>{let task=await getTask(taskId2);if(!task.worktreePath)throw new BridgeError("NO_WORKTREE",`task ${taskId2} has no worktree yet`);return task.worktreePath},viewedSession(taskId2,tabId2){let tab=loadStateFile()[terminalTabsKey(taskId2)]?.tabs?.find((t)=>t.id===tabId2);return tab?.kind==="engine"&&tab.ptyTask?{taskId:tab.ptyTask.id,tabId:tab.ptyTask.tabId??"tab-1",cwd:tab.ptyTask.worktree}:null},machineOf:(taskId2)=>api.machineOf?.(taskId2),async diffFiles(taskId2){if(api.machineOf?.(taskId2))return api.verb("worktree-diff",[flag("task-id",taskId2)]).catch((err)=>{if(err instanceof BridgeError&&err.code==="NO_WORKTREE")return{base:null,files:[]};throw err});let task=await getTask(taskId2);if(!task.worktreePath)return{base:null,files:[]};return worktreeDiff(task.worktreePath,task.prStatus?.baseRef)},async diffRefs(taskId2){if(api.machineOf?.(taskId2))return api.verb("worktree-refs",[flag("task-id",taskId2)]);let task=await getTask(taskId2);if(!task.worktreePath)throw new BridgeError("NO_WORKTREE",`task ${taskId2} has no worktree yet`);return worktreeRefs(task.worktreePath)},async diffFile(taskId2,path,scope,ignoreWhitespace,ref){if(isAbsolute(path)||normalize(path).split(/[\\/]/).includes(".."))throw new BridgeError("BAD_ARGS","path must be relative to the worktree");if(ref!==void 0&&scope!=="task")throw new BridgeError("BAD_ARGS","base only applies to the task scope");if(api.machineOf?.(taskId2))return api.verb("worktree-diff-file",[flag("task-id",taskId2),flag("path",path),flag("scope",scope),...ref!==void 0?[flag("base",ref)]:[],...ignoreWhitespace?["--ignore-whitespace"]:[]]);let task=await getTask(taskId2);if(!task.worktreePath)throw new BridgeError("NO_WORKTREE",`task ${taskId2} has no worktree yet`);let opts={ignoreWhitespace:ignoreWhitespace===!0};if(scope==="working")return loadPreviewData(task.worktreePath,path,void 0,opts);let base;if(ref!==void 0){if(base=await checkRef(task.worktreePath,ref),!base)throw new BridgeError("BAD_REF",`${ref} is not a commit in this repository`)}else base=await resolveBase(task.worktreePath,task.prStatus?.baseRef);if(!base)throw new BridgeError("NO_BASE","could not resolve a base branch for this task");return loadPreviewData(task.worktreePath,path,scope==="task"?{base,working:!0}:{base},opts)},async dismissAttention(taskId2,tabId2,deliveryId){await client.request("attention.dismiss",{taskId:taskId2,...tabId2?{tabId:tabId2}:{},...deliveryId?{deliveryId}:{}})}}}function isPayload(value){return!!value&&typeof value==="object"&&"tasks"in value&&!!value.tasks&&typeof value.tasks==="object"&&"rev"in value&&typeof value.rev==="number"}class RowStatusFeed{client;statuses=null;rev=-1;viewing=new Map;listeners=new Set;constructor(client){this.client=client;client.on("row.status",(frame)=>this.accept(frame.payload,!1))}current(){return this.statuses}async resync(){this.accept(await this.client.request("status.list"),!0);for(let[viewer,view]of this.viewing)this.sendViewing(viewer,view.taskId,view.tabId,view.fields)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}setViewing(viewer,taskId2,tabId2,fields={}){let known=this.viewing.get(viewer);if(taskId2===null){if(!this.viewing.delete(viewer))return}else{if(known?.taskId===taskId2&&known.tabId===tabId2&&JSON.stringify(known.fields)===JSON.stringify(fields))return;this.viewing.set(viewer,{taskId:taskId2,tabId:tabId2,fields})}this.sendViewing(viewer,taskId2,tabId2,fields)}noteInput(stream,data){if(!(data==="\r"||data.endsWith("\r")))return;let[taskId2,tabId2]=stream.split("::");if(!taskId2||!tabId2)return;if(this.statuses?.[taskId2]?.tabs[tabId2]?.kind!=="needs-input")return;this.client.request("activity.answered",{taskId:taskId2,tabId:tabId2}).catch(()=>{})}sendViewing(viewer,taskId2,tabId2,fields){let extra=taskId2!==null?{...tabId2?{tabId:tabId2}:{},...fields}:{};this.client.request("ui.viewing",{viewer,taskId:taskId2,...extra}).catch(()=>{})}accept(payload,reset){if(!isPayload(payload)||!reset&&payload.rev<=this.rev)return;this.rev=payload.rev,this.statuses=payload.tasks;for(let listener of this.listeners)listener()}}import{hostname as osHostname}from"os";var DESTRUCTIVE_CORE={"task.delete":!0,"task.land":!0,"tab.close":!0},DESTRUCTIVE_VERBS={delete:!0,land:!0,"remove-worktree":!0,"tab-close":!0,"issue-delete":!0,"routine-delete":!0,"note-delete":!0};function logDestructiveVerb(sink,verb,params){let ids=["task-id","tab","id"].filter((key)=>typeof params[key]==="string"||typeof params[key]==="number").map((key)=>`${key}=${String(params[key])}`);audit(sink,`api.invoke ${verb}`,ids)}function logDestructive(sink,req){let ids=["taskId","tabId","path"].filter((key)=>typeof req.args[key]==="string").map((key)=>`${key}=${String(req.args[key])}`);audit(sink,req.op,ids)}function audit(sink,what,ids){let line=`[rove-bridge] ${new Date().toISOString()} ${what} ${ids.join(" ")}`.trimEnd();(sink.log??((l)=>console.error(l)))(`${line} ${sink.who}`)}import{unlinkSync}from"fs";import{join as join2}from"path";var realDeps={installHooks:async()=>{let{ensureGlobalRoveHooks}=await import("./chunk-6q4br0pt.js");await ensureGlobalRoveHooks({quiet:!0})},uninstallHooks:async()=>{let{activityHookAdapters}=await import("./chunk-2kt0mfvf.js");await Promise.all(activityHookAdapters().map(async(adapter)=>{try{await adapter.removeActivityHooks(adapter.globalSettingsPath())}catch{}}))},resetState:()=>{replaceStateFile({});try{unlinkSync(join2(roveStateDir(),"tasks.json"))}catch(err){if(err.code!=="ENOENT")throw new BridgeError("RESET_FAILED",`could not remove tasks.json: ${err.message}`)}}};function createDevOps(deps){return{"engines.hooks.install":{kind:"write",destructive:!0,wraps:"Settings \u2192 Engines install row: ensureGlobalRoveHooks into every engine's own config",async run(){return await deps.installHooks(),{}}},"engines.hooks.uninstall":{kind:"write",destructive:!0,wraps:"Settings \u2192 Engines uninstall row: removeActivityHooks for every hook adapter",async run(){return await deps.uninstallHooks(),{}}},"dev.reset":{kind:"write",destructive:!0,wraps:"Settings \u2192 Dev reset: replaceStateFile({}), remove tasks.json, then daemon.stop {reason: restart} so its in-memory tasks go too (worktrees and engine history stay)",async run(_args,{api}){return deps.resetState(),await api.rpc("daemon.stop",{reason:"restart"}).catch(()=>{return}),{}}},"daemon.restart":{kind:"write",destructive:!0,wraps:"Settings \u2192 Dev restart: daemon RPC daemon.stop {reason: restart} (PTY host and engine sessions keep running)",async run(_args,{api}){return await api.rpc("daemon.stop",{reason:"restart"}).catch(()=>{return}),{}}}}}var devOps=createDevOps(realDeps);var MAX_FILES=50000,MAX_NOTES=200,NOTE_MAX=4000,PATH_MAX=1024,MAX_LINE=1e7,CODE_MAX=8000,stateNoteStore={read(id){let notes=[];return updateStateFile((state)=>{let v=state[diffCommentsKey(id)];return notes=Array.isArray(v)?v:[],!1}),notes},update(id,mutate){updateStateFile((state)=>{let v=state[diffCommentsKey(id)];state[diffCommentsKey(id)]=mutate(Array.isArray(v)?v:[]);return})}},defaultDeps={listFiles:(worktree)=>listFiles(worktree),notes:stateNoteStore,now:Date.now,newId:()=>crypto.randomUUID()};async function worktreeOf(api,id){let path=(await api.verb("get-task",[flag("task-id",id)])).task.worktreePath;if(!path)throw new BridgeError("NO_WORKTREE",`task ${id} has no worktree yet`);return path}function relPath(args,name){let v=text2(args,name,PATH_MAX);if(v.startsWith("/")||v.includes("\x00")||v.split(/[\\/]/).includes(".."))throw new BridgeError("BAD_ARGS",`${name} must be relative to the worktree`);return v}function lineNo(args,name){let v=optInt(args,name,1,MAX_LINE);if(v===void 0)throw new BridgeError("BAD_ARGS",`${name} must be an integer in 1..${MAX_LINE}`);return v}var samePath2=(a,b)=>a.replace(/^\/private\//,"/")===b.replace(/^\/private\//,"/");function createFilesOps(deps=defaultDeps){let spec=(s)=>s,sending=new Set;return{"files.list":spec({kind:"read",destructive:!1,wraps:"listFiles(worktree) \u2014 tracked + untracked-not-ignored files",async run(args,{api}){let id=taskId(args),all=api.machineOf?.(id)?(await api.verb("worktree-files",[flag("task-id",id)])).files:await deps.listFiles(await worktreeOf(api,id)),truncated=all.length>MAX_FILES;return{files:truncated?all.slice(0,MAX_FILES):all,truncated}}}),"file.read":spec({kind:"read",destructive:!1,wraps:"rove api worktree-file \u2014 one markdown/HTML/SVG/image/PDF file of the task's worktree (real path confined to it, 5 MB cap), base64",async run(args,{api}){return api.verb("worktree-file",[flag("task-id",taskId(args)),flag("path",relPath(args,"path"))])}}),"review.list":spec({kind:"read",destructive:!1,wraps:"state.json diffComments.<taskId>",async run(args){let notes=deps.notes.read(taskId(args));return{notes,unsent:unsentComments(notes).length}}}),"review.add":spec({kind:"write",destructive:!1,wraps:"state.json diffComments.<taskId> (append)",async run(args){let id=taskId(args),line=lineNo(args,"line"),startLine=optInt(args,"startLine",1,MAX_LINE);if(startLine!==void 0&&startLine>line)throw new BridgeError("BAD_ARGS","startLine is after line");let code=optText(args,"code",CODE_MAX),note={id:deps.newId(),filePath:relPath(args,"filePath"),...startLine!==void 0&&startLine!==line?{startLine}:{},line,body:text2(args,"body",NOTE_MAX),...code?{code}:{},createdAt:deps.now()};return deps.notes.update(id,(cur)=>{if(cur.length>=MAX_NOTES)throw new BridgeError("TOO_MANY_NOTES",`a task keeps at most ${MAX_NOTES} notes`);return[...cur,note]}),{note}}}),"review.edit":spec({kind:"write",destructive:!1,wraps:"state.json diffComments.<taskId> (rewrite one note's text in place)",async run(args){let id=taskId(args),noteId=text2(args,"id",128),body=text2(args,"body",NOTE_MAX),note;if(deps.notes.update(id,(cur)=>cur.map((c)=>{if(c.id!==noteId)return c;return note={...c,body},note})),!note)throw new BridgeError("NOT_FOUND","no such note");return{note}}}),"review.remove":spec({kind:"write",destructive:!0,wraps:"state.json diffComments.<taskId> (drop one note)",async run(args){let id=taskId(args),noteId=text2(args,"id",128),removed=!1;return deps.notes.update(id,(cur)=>{let next=cur.filter((c)=>c.id!==noteId);return removed=next.length!==cur.length,next}),{removed}}}),"review.send":spec({kind:"write",destructive:!1,wraps:"rove api send --plain (all unsent notes as one prompt; sentAt only on confirmed delivery)",async run(args,{api}){let id=taskId(args),tab=args.tabId===void 0||args.tabId===null||args.tabId===""?void 0:tabId(args);if(sending.has(id))throw new BridgeError("SEND_IN_FLIGHT","this task's notes are already being sent");let unsent=unsentComments(deps.notes.read(id));if(unsent.length===0)return{sent:0,delivered:!0};sending.add(id);try{let worktree=await worktreeOf(api,id).catch(()=>{return}),prompt=formatDiffComments(unsent,worktree),res=await api.verb("send",[flag("task-id",id),"--plain",flag("prompt",prompt),...tab?[flag("tab",tab)]:[]]);if(res.delivered!==!0||res.targetState)return{sent:0,delivered:!1,reason:sendReason(res.targetDetail??res.reason)??(res.targetState?`engine is ${res.targetState}`:"not confirmed")};let batch=new Set(unsent.map((c)=>c.id)),sentAt=deps.now();return deps.notes.update(id,(cur)=>cur.map((c)=>batch.has(c.id)&&c.sentAt===void 0?{...c,sentAt}:c)),{sent:unsent.length,delivered:!0}}finally{sending.delete(id)}}}),"worktrees.list":spec({kind:"read",destructive:!1,wraps:"daemon RPC worktree.list {network} + task.list (taskId/taskKind joined by path)",async run(args,{api}){let network=args.network===void 0?!0:args.network===!0;if(args.network!==void 0&&typeof args.network!=="boolean")throw new BridgeError("BAD_ARGS","network must be a boolean");let[list,tasks]=await Promise.all([api.rpc("worktree.list",{network}),api.rpc("task.list")]);return{projects:list.projects.map((p)=>({repo:p.repo,worktrees:p.worktrees.map((w)=>{let t=tasks.tasks.find((x)=>x.worktreePath&&samePath2(x.worktreePath,w.path));return t?{...w,taskId:t.id,taskKind:t.kind??"task"}:w})}))}}}),"worktrees.remove":spec({kind:"write",destructive:!0,wraps:"daemon RPC worktree.remove {path, force}",async run(args,{api}){let path=absPath(args,"path"),force=bool2(args,"force");if(!(await api.rpc("worktree.list",{network:!1})).projects.some((p)=>p.worktrees.some((w)=>w.path===path)))throw new BridgeError("UNKNOWN_WORKTREE","that path is not a listed worktree");try{let res=await api.rpc("worktree.remove",{path,force});return{removed:res.removed,...res.residue?{residue:res.residue}:{}}}catch(err){if(err instanceof BridgeError&&err.message.startsWith("DIRTY_WORKTREE: "))throw new BridgeError("DIRTY_WORKTREE",err.message.slice(16));throw err}}})}}var filesOps=createFilesOps();var GATE_ID=/^g-\d{1,9}$/,OPTION_KEY=/^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/,TEXT_MAX=4000,DAEMON_CODE=/^(GATE_NOT_FOUND|GATE_NOT_OPEN|BAD_OPTION): /;function gateId(args){let v=args.gateId;if(typeof v!=="string"||!GATE_ID.test(v))throw new BridgeError("BAD_ARGS","gateId is not a gate id");return v}async function answer(args,ctx){let id=gateId(args),option=args.option;if(option!==void 0&&option!==null&&(typeof option!=="string"||!OPTION_KEY.test(option)))throw new BridgeError("BAD_ARGS","option is not an option key");let text3=optText(args,"text",TEXT_MAX);if(!option&&!text3)throw new BridgeError("BAD_ARGS","answer needs an option or text");try{return{delivered:(await ctx.api.rpc("gate.answer",{gateId:id,...option?{option}:{},...text3?{text:text3}:{},by:"bridge"})).delivered===!0}}catch(err){let message=err instanceof Error?err.message:String(err),code=DAEMON_CODE.exec(message)?.[1];if(code)throw new BridgeError(code,message.replace(DAEMON_CODE,""));throw err}}var gateAnswer={kind:"write",destructive:!1,wraps:"rpc gate.answer (by: bridge)",run:answer},GATE_OPS={"gate.answer":gateAnswer};var spec=(kind,wraps,run,destructive=!1)=>({kind,destructive,wraps,run}),githubAuthOps={"github.status":spec("read","github-status (on `machine`, else this machine)",async(args,ctx)=>{let out=await attempt(ctx.api.verb("github-status",[],machineOf(args)));return out.ok?{status:out.result}:{status:null,reason:out.reason,message:out.message}}),"github.login":spec("write","github-login [--cancel] (on `machine`, else this machine)",(args,ctx)=>attempt(ctx.api.verb("github-login",optBool(args,"cancel")?[flag("cancel","true")]:[],machineOf(args)))),"github.logout":spec("write","github-logout (this machine's gh, signed out)",(_args,ctx)=>attempt(ctx.api.verb("github-logout",[])),!0),"github.sync":spec("write","github-sync --to `machine` (this machine's account signed in there)",async(args,ctx)=>{let machine=machineOf(args);if(!machine)throw new BridgeError("BAD_ARGS","machine must name a registered machine");return attempt(ctx.api.verb("github-sync",[flag("to",machine)]))})};var LIST_TIMEOUT_MS=15000;function parseRepos(stdout){let rows;try{rows=JSON.parse(stdout)}catch{return null}if(!Array.isArray(rows))return null;return rows.flatMap((row)=>typeof row?.nameWithOwner==="string"?[{nameWithOwner:row.nameWithOwner,description:typeof row.description==="string"?row.description:"",isPrivate:row.isPrivate===!0}]:[])}var githubOps={"github.repos":{kind:"read",destructive:!1,wraps:"gh repo list --limit 100 --json nameWithOwner,description,isPrivate (the machine's own gh login)",async run(){let res=await taskOpDeps.gh(["repo","list","--limit","100","--json","nameWithOwner,description,isPrivate"],LIST_TIMEOUT_MS);if(!res.ok)return{repos:[],reason:res.reason};let repos=parseRepos(res.stdout);return repos?{repos}:{repos:[],reason:"failed"}}}};var CURSOR=/^[A-Za-z0-9._~=-]{1,1024}$/;function given(args,name){let v=args[name];return v!==void 0&&v!==null&&v!==""}function optCursor(args){let raw=optText(args,"cursor",1024);if(raw===void 0)return;if(!CURSOR.test(raw))throw new BridgeError("BAD_ARGS","cursor is not a read-output cursor");return raw}var insightOps={"output.read":{kind:"read",destructive:!1,wraps:"rove api read-output (structured engine history, else a labeled terminal tail)",run(args,{api}){let argv=[flag("task-id",taskId(args))];if(given(args,"tab"))argv.push(flag("tab",tabId(args,"tab")));let source=optOneOf(args,"source",["auto","history","terminal"]);if(source)argv.push(flag("source",source));let cursor=optCursor(args);if(cursor)argv.push(flag("cursor",cursor));let limit=optInt(args,"limit",1,50);if(limit!==void 0)argv.push(flag("limit",limit));return api.verb("read-output",argv)}},"repo.digest":{kind:"read",destructive:!1,wraps:"rove api digest",run(args,{api}){let argv=[flag("repo",absPath(args,"repo"))],days=optInt(args,"sinceDays",1,365);if(days!==void 0)argv.push(flag("since-days",days));return api.verb("digest",argv)}},"turns.list":{kind:"read",destructive:!1,wraps:"rove api agent-turns",async run(args,{api}){let argv=[];if(given(args,"taskId"))argv.push(flag("task-id",taskId(args)));if(given(args,"repo"))argv.push(flag("repo",absPath(args,"repo")));let days=optInt(args,"sinceDays",1,365);if(days!==void 0)argv.push(flag("since-days",days));let limit=optInt(args,"limit",1,500);if(limit!==void 0)argv.push(flag("limit",limit));return{...await api.verb("agent-turns",argv),readers:vendorsWithTurnReader()}}},"sessions.search":{kind:"read",destructive:!1,wraps:"rove api search (user and assistant prose in every task's engine history)",run(args,{api}){let query=optText(args,"query",200)?.trim();if(!query)throw new BridgeError("BAD_ARGS","query is required");let argv=[flag("query",query)];if(given(args,"repo"))argv.push(flag("repo",absPath(args,"repo")));let limit=optInt(args,"limit",1,100);if(limit!==void 0)argv.push(flag("limit",limit));return api.verb("search",argv)}}};var CHORD_MAX=64,realDeps2={read(){return resetKeybindingsFileCache(),readKeybindingsFile()},create:createKeybindingsFile,fireAction(target){let[cmd,...rest]=[...roveCliInvocation(),"plugin","action","invoke",target];return spawnDetached(cmd,rest,{onError:(err)=>console.warn(`[rove/plugins] ${target}: ${String(err)}`)})},platform:process.platform},same=(a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null);function copyKeymap(){return RoveKeymap.map((row)=>({id:row.id,scope:row.scope,keys:[...row.keys],...row.prefixKeys?{prefixKeys:[...row.prefixKeys]}:{},...row.hint?{hint:{...row.hint}}:{}}))}function createKeybindingsOps(deps=realDeps2){return{"keybindings.get":{kind:"read",destructive:!1,wraps:"helper resolveUserKeybindings (the TUI's keybindings.yaml parser) on a copy of the default keymap",async run(){let file=deps.read(),rows=copyKeymap(),{report,prefix}=resolveUserKeybindings(file,rows,deps.platform),changed=[];for(let row of rows){let base=RoveKeymap.find((candidate)=>candidate.id===row.id);if(base&&same(base.keys,row.keys)&&same(base.prefixKeys,row.prefixKeys)&&same(base.hint,row.hint))continue;changed.push({id:row.id,keys:row.keys,...row.prefixKeys?{prefixKeys:row.prefixKeys}:{},...row.hint?{hint:row.hint}:{}})}return{path:report.path,exists:report.exists,rows:changed,prefix:{key:prefix.key,firstStrokes:prefixFirstStrokes(prefix),timeoutMs:prefix.timeoutMs},plugins:report.plugins,applied:report.applied,warnings:report.warnings}}},"keybindings.createFile":{kind:"write",destructive:!1,wraps:"helper createKeybindingsFile (the fixed, fully commented starter; never overwrites)",async run(){let file=deps.read();if(file.exists)return{path:file.path,created:!1};return{path:file.path,created:deps.create(file.path)}}},"plugin.chordAction":{kind:"write",destructive:!1,wraps:"rove plugin action invoke <target> (detached), only for an `action:` bound to this chord in keybindings.yaml",async run(a){let chord=str(a,"chord");if(chord.length>CHORD_MAX)throw new BridgeError("BAD_ARGS","chord is too long");let file=deps.read(),{report}=resolveUserKeybindings(file,copyKeymap(),deps.platform),bound=report.plugins.find((p)=>p.chord===chord&&p.kind==="action");if(!bound)throw new BridgeError("NOT_BOUND",`${chord} is not bound to a plugin action`);if(!deps.fireAction(bound.target))throw new BridgeError("SESSION_FAILED",`could not start plugin action ${bound.target}`);return{target:bound.target}}}}}var keybindingsOps=createKeybindingsOps();var PATH_MAX2=1000,PANE_ID=/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/,launchDeps={panes(){return listPaneLaunches({socketPath:defaultDaemonSocketPath(),binPath:ROVE_PRODUCT_NAME}).map((p)=>({id:`${p.pluginId}.${p.paneId}`,title:p.title,placement:p.placement}))}};async function tabNew(api,id,kind,extra){let res=await api.verb("tab-new",[flag("task-id",id),flag("kind",kind),...extra]);if(!res.tabId)throw new BridgeError("SESSION_FAILED","the new tab did not report its id");return{tabId:res.tabId,...res.leaf?{leaf:res.leaf}:{}}}var launchOps={"file.openInEditor":{kind:"write",destructive:!1,wraps:"rove api tab-new --kind editor (the Mac's configured editor on a file inside the task's worktree)",async run(a,{api}){let rel=optStr(a,"path")??"";if(!rel||rel.length>PATH_MAX2||rel.startsWith("/")||rel.includes("\x00")||rel.split(/[\\/]/).includes(".."))throw new BridgeError("BAD_ARGS","path must be relative to the worktree");let{tabId:opened}=await tabNew(api,taskId(a),"editor",[flag("file",rel)]);return{tabId:opened}}},"plugin.panes":{kind:"read",destructive:!1,wraps:"helper listPaneLaunches (panes of enabled plugins)",async run(){return{panes:launchDeps.panes()}}},"plugin.paneOpen":{kind:"write",destructive:!1,wraps:"rove api tab-new --kind pane (a registered plugin pane; its argv comes from the manifest)",async run(a,{api}){let pane=str(a,"pane");if(!PANE_ID.test(pane))throw new BridgeError("BAD_ARGS","pane is not a plugin pane id");let extra=[flag("pane",pane)];if(a.tabId!==void 0&&a.tabId!==null)extra.push(flag("tab",tabId(a)));return tabNew(api,taskId(a),"pane",extra)}},"tab.openViewport":{kind:"write",destructive:!1,wraps:"rove api tab-new --kind viewport (a tab showing another task's first engine session)",async run(a,{api}){let{tabId:opened}=await tabNew(api,taskId(a),"viewport",[flag("view-task",taskId(a,"viewTaskId"))]);return{tabId:opened}}}};var PLUGIN_ID=/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;function control(a){let plugin=text2(a,"plugin",128),pane=text2(a,"pane",64),key=text2(a,"key",64);if(!PLUGIN_ID.test(plugin)||!MOD_PANE_ID_PATTERN.test(pane)||!MOD_KEY_PATTERN.test(key))throw new BridgeError("BAD_ARGS","plugin, pane or key is not a mod id");return{plugin,pane,key}}function delivered(res){if(!res.ok)throw new BridgeError("MOD_UNAVAILABLE",`the mod did not get it (${res.reason??"unknown"})`);return{}}var modsOps={"mod.press":{kind:"write",destructive:!1,wraps:"daemon RPC mod.press (forwards a button key to the mod that drew it)",async run(a,{api}){return delivered(await api.rpc("mod.press",control(a)))}},"mod.submit":{kind:"write",destructive:!1,wraps:"daemon RPC mod.submit (forwards an input field's text to the mod that drew it)",async run(a,{api}){let value=a.value;if(typeof value!=="string")throw new BridgeError("BAD_ARGS","value must be text");if(value.length>MOD_INPUT_MAX)throw new BridgeError("BAD_ARGS",`value is longer than ${MOD_INPUT_MAX} characters`);return delivered(await api.rpc("mod.submit",{...control(a),value}))}}};var ISSUE_STATUSES=["open","doing","hold","done"],TITLE_MAX=300,BODY_MAX=20000,NAME_MAX=120,PROMPT_MAX=20000,EVENT_LIMIT=12;function issueId(args){let id=optInt(args,"id",1,1e9);if(id===void 0)throw new BridgeError("BAD_ARGS","id must be an issue number");return id}function cron(args,name){let v=text2(args,name,120).trim();if(!/^[0-9A-Za-z*/,\-?#LW]+(?: [0-9A-Za-z*/,\-?#LW]+){4}$/.test(v))throw new BridgeError("BAD_ARGS",`${name} must be five space-separated cron fields`);return v}function setIfPresent(argv,name,value){if(value!==void 0)argv.push(flag(name,value))}var pagesOps={"issue.repos":{kind:"read",destructive:!1,wraps:"daemon RPC issue.repos",run:(_args,{api})=>api.rpc("issue.repos",{})},"issue.list":{kind:"read",destructive:!1,wraps:"rove api issue-list",run:(args,{api})=>api.verb("issue-list",[flag("repo",absPath(args,"repo"))])},"issue.create":{kind:"write",destructive:!1,wraps:"rove api issue-create",run:(args,{api})=>{let argv=[flag("repo",absPath(args,"repo")),flag("title",text2(args,"title",TITLE_MAX).trim())];return setIfPresent(argv,"body",optText(args,"body",BODY_MAX)),api.verb("issue-create",argv)}},"issue.update":{kind:"write",destructive:!1,wraps:"rove api issue-update (title/body and/or task link; task=none unlinks; clearBody stores one space, the verb cannot send an empty body)",run:(args,{api})=>{let title=optText(args,"title",TITLE_MAX),typed=optText(args,"body",BODY_MAX);if(optBool(args,"clearBody")&&typed!==void 0)throw new BridgeError("BAD_ARGS","body and clearBody are exclusive");let body=optBool(args,"clearBody")?" ":typed,task=optStr(args,"task")===void 0?void 0:taskId(args,"task");if(title===void 0&&body===void 0&&task===void 0)throw new BridgeError("BAD_ARGS","issue.update needs title, body and/or task");let argv=[flag("repo",absPath(args,"repo")),flag("id",issueId(args))];return setIfPresent(argv,"title",title?.trim()),setIfPresent(argv,"body",body),setIfPresent(argv,"task",task),api.verb("issue-update",argv)}},"issue.setStatus":{kind:"write",destructive:!1,wraps:"rove api issue-update --status",run:(args,{api})=>api.verb("issue-update",[flag("repo",absPath(args,"repo")),flag("id",issueId(args)),flag("status",oneOf(args,"status",ISSUE_STATUSES))])},"issue.delete":{kind:"write",destructive:!0,wraps:"rove api issue-delete (the tracker record only; a linked task and worktree stay)",run:(args,{api})=>api.verb("issue-delete",[flag("repo",absPath(args,"repo")),flag("id",issueId(args))])},"issue.prompt":{kind:"read",destructive:!1,wraps:"rove api issue-list + the kanban drawer's issueWorktreePrompt/issueProjectPrompt helpers",async run(args,{api}){let where=oneOf(args,"where",["worktree","project"]),id=issueId(args),issue=(await api.verb("issue-list",[flag("repo",absPath(args,"repo"))])).issues.find((i)=>i.id===id);if(!issue)throw new BridgeError("ISSUE_NOT_FOUND",`no issue #${id}`);let cli=roveApiInvocation();return{title:issueChatTaskTitle(issue),prompt:where==="worktree"?issueWorktreePrompt(issue,cli):issueProjectPrompt(issue,cli)}}},"task.events":{kind:"read",destructive:!1,wraps:"daemon RPC task.recentEvents, newest first, as the drawer's EVENTS rows",async run(args,{api}){let id=taskId(args),limit=optInt(args,"limit",1,100)??EVENT_LIMIT;try{return{events:(await api.rpc("task.recentEvents",{taskId:id})).events.slice(-limit).reverse().map((event)=>({kind:event.kind,at:event.at,tail:[detailFragment(event),event.vendor??""].filter((part)=>part.length>0).join(" \xB7 ")}))}}catch(err){if(err instanceof Error&&/task not found/i.test(err.message))return{events:[]};throw err}}},"routine.list":{kind:"read",destructive:!1,wraps:"rove api routine-list",run:(_args,{api})=>api.verb("routine-list",[])},"routine.create":{kind:"write",destructive:!1,wraps:"rove api routine-create (name, optional repo \u2014 absent = no project, prompt, schedule, optional existing conversation targetTask+targetTab; never a precheck)",run:(args,{api})=>{let argv=[optStr(args,"repo")===void 0?"--no-repo":flag("repo",absPath(args,"repo")),flag("name",text2(args,"name",NAME_MAX).trim()),flag("prompt",text2(args,"prompt",PROMPT_MAX)),flag("schedule",cron(args,"schedule"))],hasTask=optStr(args,"targetTask")!==void 0,hasTab=optStr(args,"targetTab")!==void 0;if(hasTask!==hasTab)throw new BridgeError("BAD_ARGS","targetTask and targetTab go together");if(hasTask)argv.push(flag("target-task",taskId(args,"targetTask")),flag("target-tab",tabId(args,"targetTab")));return api.verb("routine-create",argv)}},"routine.update":{kind:"write",destructive:!1,wraps:"rove api routine-update (name, prompt, schedule; never a precheck)",run:(args,{api})=>{let name=optText(args,"name",NAME_MAX),prompt=optText(args,"prompt",PROMPT_MAX),schedule=optStr(args,"schedule")===void 0?void 0:cron(args,"schedule");if(name===void 0&&prompt===void 0&&schedule===void 0)throw new BridgeError("BAD_ARGS","routine.update needs name, prompt and/or schedule");let argv=[flag("id",taskId(args,"id"))];return setIfPresent(argv,"name",name?.trim()),setIfPresent(argv,"prompt",prompt),setIfPresent(argv,"schedule",schedule),api.verb("routine-update",argv)}},"routine.setEnabled":{kind:"write",destructive:!1,wraps:"rove api routine-update --enabled",run:(args,{api})=>api.verb("routine-update",[flag("id",taskId(args,"id")),flag("enabled",String(bool2(args,"enabled")))])},"routine.runNow":{kind:"write",destructive:!1,wraps:"rove api routine-run-now (skips the precheck; the schedule does not shift)",run:(args,{api})=>api.verb("routine-run-now",[flag("id",taskId(args,"id"))])},"routine.runs":{kind:"read",destructive:!1,wraps:"rove api routine-runs",run:(args,{api})=>api.verb("routine-runs",[flag("id",taskId(args,"id"))])},"routine.delete":{kind:"write",destructive:!0,wraps:"rove api routine-delete (the routine and its history; tasks it created stay)",run:(args,{api})=>api.verb("routine-delete",[flag("id",taskId(args,"id"))])},"attention.dismissRoutine":{kind:"write",destructive:!1,wraps:"daemon RPC attention.dismissRoutine (a routine episode is keyed on its schedule, not a task)",run:(args,{api})=>api.rpc("attention.dismissRoutine",{automationId:taskId(args,"automationId")})},"workitem.list":{kind:"read",destructive:!1,wraps:"daemon RPC workitem.list (same call as rove api workitem-list, plus refresh)",run:(args,{api})=>{let state=optOneOf(args,"state",["open","closed","all"]),assignee=optOneOf(args,"assignee",["@me"]),search=optText(args,"search",200),limit=optInt(args,"limit",1,50);return api.rpc("workitem.list",{repo:absPath(args,"repo"),...state?{state}:{},...assignee?{assignee}:{},...search?{search}:{},...limit!==void 0?{limit}:{},...optBool(args,"refresh")?{refresh:!0}:{}})}},"workitem.links":{kind:"read",destructive:!1,wraps:"daemon RPC task.list, folded to the tasks already started from a GitHub issue of this repo",async run(args,{api}){let repo=absPath(args,"repo"),{tasks}=await api.rpc("task.list",{});return{links:tasks.flatMap((task)=>task.linkedWorkItem?.number!==void 0&&samePath(task.repo,repo)?[{number:task.linkedWorkItem.number,taskId:task.id}]:[])}}},"workitem.start":{kind:"write",destructive:!1,wraps:"rove api workitem-start",run(args,{api}){let number=optInt(args,"number",1,1e9);if(number===void 0)throw new BridgeError("BAD_ARGS","number must be an issue number");let argv=[flag("repo",absPath(args,"repo")),flag("number",number)];return setIfPresent(argv,"vendor",optOneOf(args,"engine",ALL_VENDORS)),api.verb("workitem-start",argv)}}};var URL_MAX=2048,panelOps={"browser.probe":{kind:"read",destructive:!1,wraps:"frame probe (HTTPS HEAD to a public address; reads X-Frame-Options and CSP frame-ancestors)",async run(a){let{url,origin}=a;if(typeof url!=="string"||url.length===0||url.length>URL_MAX)throw new BridgeError("BAD_ARGS","url must be a URL string");if(origin!==void 0&&(typeof origin!=="string"||origin.length>URL_MAX))throw new BridgeError("BAD_ARGS","origin must be the page's origin string");return checkFrame(url,origin===void 0?{}:{origin})}}};var VIEW_TIMEOUT_MS=1e4,LOG_TIMEOUT_MS=30000,MAX_CHECKS=60,CHECKS_TTL_MS=30000,LOG_TTL_MS=60000,MAX_CACHED=24,LOG_LINES=40,LOG_CHARS=4000,RUN_ID=/^\d{1,15}$/,RANK={fail:0,pending:1,pass:2,skipped:3},realDeps3={gh:ghText,now:Date.now};function field(entry,key){let v=typeof entry==="object"&&entry!==null?entry[key]:void 0;return typeof v==="string"?v:""}function checkState(entry){let status=field(entry,"status").toUpperCase(),conclusion=field(entry,"conclusion").toUpperCase(),context=field(entry,"state").toUpperCase();if(status&&status!=="COMPLETED")return"pending";if(conclusion)return conclusion==="SUCCESS"?"pass":conclusion==="NEUTRAL"||conclusion==="SKIPPED"?"skipped":"fail";if(context==="SUCCESS")return"pass";if(context==="FAILURE"||context==="ERROR")return"fail";return"pending"}function durationOf(entry){let start=Date.parse(field(entry,"startedAt")),end=Date.parse(field(entry,"completedAt"));return Number.isFinite(start)&&Number.isFinite(end)&&end>=start?end-start:null}function checkRows(rollup){return rollup.map((entry,i)=>{let url=field(entry,"detailsUrl")||field(entry,"targetUrl");return{i,name:field(entry,"name")||field(entry,"context")||"check",state:checkState(entry),durationMs:durationOf(entry),url,runId:runIdFromDetailsUrl(url)}}).sort((a,b)=>RANK[a.state]-RANK[b.state]||a.i-b.i).map(({i:_,...row})=>row)}function cache(ttl){let map=new Map;return{get(key,now){let hit=map.get(key);return hit&&now-hit.at<ttl?hit.value:void 0},set(key,value,now){map.delete(key),map.set(key,{at:now,value});for(let old of map.keys()){if(map.size<=MAX_CACHED)break;map.delete(old)}}}}async function worktreeOf2(ctx,id){let res=await ctx.api.verb("get-task",[flag("task-id",id)]),{worktreePath,prStatus}=res.task;if(!worktreePath)throw new BridgeError("NO_WORKTREE",`task ${id} has no worktree yet`);return{worktree:worktreePath,pr:prStatus?.number}}function prChecksOps(deps=realDeps3){let checks=cache(CHECKS_TTL_MS),logs=cache(LOG_TTL_MS);return{"pr.checks":{kind:"read",destructive:!1,wraps:"gh pr view <n> --json statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,isDraft (cached 30s)",async run(a,ctx){let id=taskId(a),{worktree,pr}=await worktreeOf2(ctx,id);if(pr===void 0)return{outcome:"none"};let key=`${id}:${pr}`,cached=a.fresh===!0?void 0:checks.get(key,deps.now());if(cached)return cached;let read=await deps.gh(["pr","view",String(pr),"--json","statusCheckRollup,mergeable,mergeStateStatus,reviewDecision,isDraft"],worktree,VIEW_TIMEOUT_MS);if(!read.ok)return{outcome:"unavailable",detail:read.detail};let view;try{view=JSON.parse(read.stdout)}catch{return{outcome:"unavailable",detail:"gh returned unreadable JSON"}}let rows=checkRows(Array.isArray(view.statusCheckRollup)?view.statusCheckRollup:[]),result={outcome:"ok",total:rows.length,checks:rows.slice(0,MAX_CHECKS),mergeable:typeof view.mergeable==="string"?view.mergeable:"",mergeState:typeof view.mergeStateStatus==="string"?view.mergeStateStatus:"",review:typeof view.reviewDecision==="string"?view.reviewDecision:"",draft:view.isDraft===!0};return checks.set(key,result,deps.now()),result}},"pr.checkLog":{kind:"read",destructive:!1,wraps:"gh run view <runId> --log-failed (last 40 lines of one job; cached 60s)",async run(a,ctx){let id=taskId(a),run=optText(a,"runId",20)??"";if(!RUN_ID.test(run))throw new BridgeError("BAD_ARGS","runId must be a run number");let job=optText(a,"job",200)??"";if(!job)throw new BridgeError("BAD_ARGS","job is required");let{worktree}=await worktreeOf2(ctx,id),byJob=logs.get(`${worktree}:${run}`,deps.now());if(!byJob){let read=await deps.gh(["run","view",run,"--log-failed"],worktree,LOG_TIMEOUT_MS);if(!read.ok)return{outcome:"unavailable",detail:read.detail};byJob=parseFailedRunLog(read.stdout,LOG_LINES),logs.set(`${worktree}:${run}`,byJob,deps.now())}let tail=(byJob.get(job)??[]).join(`
|
|
103
|
+
`);return tail?{outcome:"ok",tail:tail.slice(-LOG_CHARS)}:{outcome:"none"}}}}}var PR_CHECKS_OPS=prChecksOps();var SLOTS={text:null,background:null,textMuted:"text",backgroundPanel:"background",backgroundElement:"background",backgroundMenu:"backgroundElement",border:"text",borderActive:"border",borderSubtle:"border",primary:"text",secondary:"text",accent:"primary",error:"text",warning:"text",success:"text",info:"text",diffAdded:"success",diffRemoved:"error",diffLineNumber:"textMuted",syntaxType:"info",diffAddedBg:"background",diffRemovedBg:"background"},hex2=(n)=>n.toString(16).padStart(2,"0"),rgbHex=(c)=>`#${c.toInts().slice(0,3).map(hex2).join("")}`;function palette(theme,mode){let out={},slot=(name)=>{let known=out[name];if(known)return known;let fallback=SLOTS[name],value=resolveThemeSlotHex(theme,name,mode)??(fallback?slot(fallback):name==="background"?"#000000":"#ffffff");return out[name]=value,value};for(let name of Object.keys(SLOTS))slot(name);let cb=diffInks(resolveTheme(theme,mode),!0);return out.diffAddedColorblind=rgbHex(cb.added),out.diffAddedBgColorblind=rgbHex(cb.addedBg),out}function taskColors(theme,mode,ids){let resolved=resolveTheme(theme,mode);return Object.fromEntries(ids.map((id)=>[id,rgbHex(taskColor(id,resolved))]))}var realDeps4={load:loadStateFile,update:(compute)=>{updateStateFile((state)=>{for(let[key,value]of Object.entries(compute(state)))if(value===void 0)delete state[key];else state[key]=value;return})},themes:()=>{let user=loadUserThemes(),names=new Set(user.map((u)=>u.name));return[...Object.entries(BUNDLED_THEME_JSONS).filter(([name])=>!names.has(name)).map(([name,theme])=>({name,bundled:!0,theme})),...user.map((u)=>({name:u.name,bundled:!1,theme:u.theme}))]}},MAX_TASK_IDS=500,MODES=["dark","light"];function readAll(deps,state){let themes=deps.themes(),names=themes.map((t)=>t.name),prefs={},options={};for(let[name,def]of Object.entries(PREF_DEFS))if(prefs[name]=def.read(state),def.kind==="enum")options[name]=def.choices(names);else if(def.kind==="int")options[name]={min:def.min,max:def.max};return{prefs,options,info:prefInfo(state),themes}}function createPrefsOps(deps){return{"prefs.get":{kind:"read",destructive:!1,wraps:"state.json Settings keys read through the TUI normalizers, plus resolved theme palettes and glyph sets",async run(){let{prefs,options,info,themes}=readAll(deps,deps.load());return{prefs,options,info,glyphSets:GLYPH_SETS,themes:themes.map((t)=>({name:t.name,bundled:t.bundled,dark:palette(t.theme,"dark"),light:palette(t.theme,"light")}))}}},"prefs.set":{kind:"write",destructive:!1,wraps:"state.json: one allowlisted Settings key per call, value checked against the TUI's own choices",async run(args){let name=args.key;if(typeof name!=="string"||!Object.hasOwn(PREF_DEFS,name))throw new BridgeError("BAD_ARGS",`key must be one of ${Object.keys(PREF_DEFS).join("|")}`);let def=PREF_DEFS[name],names=deps.themes().map((t)=>t.name),value=checkPrefValue(name,def,args.value,names);deps.update((fresh)=>def.write(value,fresh));let{prefs}=readAll(deps,deps.load());return{key:name,value:prefs[name],prefs}}},"theme.taskColors":{kind:"read",destructive:!1,wraps:"the TUI's taskColor(taskId, theme) for each id under one theme and mode",async run(args){let{themes}=readAll(deps,deps.load()),theme=themes.find((t)=>t.name===args.theme);if(!theme)throw new BridgeError("BAD_ARGS","theme is not an installed theme");let mode=MODES.find((m)=>m===args.mode);if(!mode)throw new BridgeError("BAD_ARGS","mode must be dark|light");let ids=args.taskIds;if(!Array.isArray(ids)||ids.length>MAX_TASK_IDS||!ids.every((id)=>typeof id==="string"&&id.length<=64))throw new BridgeError("BAD_ARGS",`taskIds must be at most ${MAX_TASK_IDS} task ids`);return{colors:taskColors(theme.theme,mode,ids)}}}}}var prefsOps=createPrefsOps(realDeps4);var PROMPT_ID=/^[A-Za-z0-9-]{1,64}$/,ANSWER_MAX=4000,promptOps={"ui.promptReply":{kind:"write",destructive:!1,wraps:"daemon RPC ui.promptReply (answers or cancels a plugin's input dialog; first answer wins)",async run(a,{api}){let promptId=a.promptId;if(typeof promptId!=="string"||!PROMPT_ID.test(promptId))throw new BridgeError("BAD_ARGS","promptId is not a prompt id");let value=a.value;if(value!==void 0&&(typeof value!=="string"||value.length>ANSWER_MAX))throw new BridgeError("BAD_ARGS",`value must be text of at most ${ANSWER_MAX} characters`);if(!(await api.rpc("ui.promptReply",{promptId,...value!==void 0?{value}:{}})).ok)throw new BridgeError("PROMPT_GONE","this question was already answered or timed out");return{}}}};var GH_TIMEOUT_MS=20000,TTL_MS=60000,LIMIT=50,SEARCH="is:pr is:open review-requested:@me archived:false sort:updated-desc",QUERY=`query($q: String!, $n: Int!) {
|
|
104
|
+
search(query: $q, type: ISSUE, first: $n) {
|
|
105
|
+
nodes {
|
|
106
|
+
... on PullRequest {
|
|
107
|
+
number title url isDraft createdAt updatedAt additions deletions
|
|
108
|
+
headRefName baseRefName isCrossRepository
|
|
109
|
+
author { login }
|
|
110
|
+
repository { nameWithOwner }
|
|
111
|
+
commits(last: 1) { nodes { commit { statusCheckRollup { state } } } }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}`,str2=(v)=>typeof v==="string"?v:"",num=(v)=>typeof v==="number"&&Number.isFinite(v)?v:0;function checksOf(raw){let state=raw.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state;if(state==="SUCCESS")return"passing";if(state==="FAILURE"||state==="ERROR")return"failing";if(state==="PENDING"||state==="EXPECTED")return"pending";return}function parseQueue(stdout){let parsed;try{parsed=JSON.parse(stdout)}catch{return null}let nodes=parsed?.data?.search?.nodes;return Array.isArray(nodes)?nodes:null}async function knownProjects(ctx){let paths=new Set(taskOpDeps.savedRepos()),{tasks}=await ctx.api.rpc("task.list");for(let task of tasks)paths.add(task.repo);let byId=new Map;for(let path of paths){let id=taskOpDeps.repoId(path);if(id&&!byId.has(id))byId.set(id,path)}return byId}function createReviewOps(now=Date.now){let cached=null;return{"review.queue":{kind:"read",destructive:!1,wraps:"gh api graphql (one fixed read-only search query: open PRs awaiting your review), the machine's own gh login",async run(args,ctx){if(!optBool(args,"refresh")&&cached&&now()-cached.at<=TTL_MS)return shape(cached.raw,await knownProjects(ctx));let res=await taskOpDeps.gh(["api","graphql","-F",`n=${LIMIT}`,"-f",`q=${SEARCH}`,"-f",`query=${QUERY}`],GH_TIMEOUT_MS);if(!res.ok)return{prs:[],other:0,reason:res.reason};let raw=parseQueue(res.stdout);if(!raw)return{prs:[],other:0,reason:"failed"};return cached={at:now(),raw},shape(raw,await knownProjects(ctx))}},"review.prepare":{kind:"write",destructive:!1,wraps:"git fetch origin +refs/pull/<number>/head:refs/rove/pr/<number> (local refs only; nothing is written to GitHub)",async run(args,ctx){let repo=await knownRepo(args,ctx),number=optInt(args,"number",1,1e9);if(number===void 0)throw new BridgeError("BAD_ARGS","number must be a pull request number");let res=await taskOpDeps.fetchPullHead(repo,number);if(!res.ok)throw new BridgeError("FETCH_FAILED",res.error);return{ref:res.ref}}}}}function shape(raw,projects){let prs=[],other=0;for(let pr of raw){let nameWithOwner=str2(pr.repository?.nameWithOwner);if(typeof pr.number!=="number"||typeof pr.title!=="string"||!nameWithOwner)continue;let repo=projects.get(`github.com/${nameWithOwner}`.toLowerCase());if(!repo){other++;continue}let checks=checksOf(pr);prs.push({repo,nameWithOwner,number:pr.number,title:pr.title,url:str2(pr.url),...str2(pr.author?.login)?{author:str2(pr.author?.login)}:{},isDraft:pr.isDraft===!0,createdAt:str2(pr.createdAt),updatedAt:str2(pr.updatedAt),additions:num(pr.additions),deletions:num(pr.deletions),headRef:str2(pr.headRefName),baseRef:str2(pr.baseRefName),fork:pr.isCrossRepository===!0,...checks?{checks}:{}})}return{prs,other}}var reviewOps=createReviewOps();var NAME_MAX2=60,realDeps5={load:loadStateFile,update:(compute)=>{updateStateFile((state)=>{for(let[key,value]of Object.entries(compute(state)))if(value===void 0)delete state[key];else state[key]=value;return})},installed:installedEngineIds,statuses:detectEngineStatuses,integrations:engineIntegrations,engineName:engineDisplayName,defaultCommand:defaultEngineCommand,installSkill:()=>installSkillErrors(),skillMissing:async()=>missingSkillNames(await installedEngineIds())};function strings(state,key){let raw=state[key];return Array.isArray(raw)?raw.filter((s)=>typeof s==="string"&&s.trim().length>0):[]}function trimmed(state,key){let raw=state[key];return typeof raw==="string"?raw.trim():""}async function engineIds(deps,state){return[...new Set([...ALL_VENDORS,...strings(state,"customEngineIds"),...await deps.installed()])]}function canBeDefault(state,id){return isBuiltinVendor(id)||strings(state,"customEngineIds").includes(id)}function effectiveDefault(state,ids){let off=strings(state,"disabledEngineIds"),stored=trimmed(state,"defaultVendor");if(stored&&canBeDefault(state,stored)&&!off.includes(stored))return stored;return ids.find((id)=>canBeDefault(state,id)&&!off.includes(id))??null}function listedId(args,ids){let id=text2(args,"id",64);if(!ids.includes(id))throw new BridgeError("BAD_ARGS",`${id} is not a listed engine`);return id}function createEngineOps(deps){let without=(list,id)=>list.filter((x)=>x!==id);function afterLeaving(state,ids,id,off){let remaining=ids.filter((x)=>x!==id&&!off.includes(x));if(remaining.length===0)throw new BridgeError("LAST_ENGINE","this is the last enabled engine; it stays on");if(effectiveDefault(state,ids)!==id)return{};let next=remaining.find((x)=>canBeDefault(state,x));if(!next)throw new BridgeError("LAST_ENGINE","no other enabled engine can take over as the default");return{defaultVendor:next}}return{"engines.settings":{kind:"read",destructive:!1,wraps:"settings Engines section: registry + detectEngineStatuses + engineIntegrations + state.json switches",async run(){let state=deps.load(),ids=await engineIds(deps,state),[statuses,integrations]=await Promise.all([deps.statuses(ids).catch(()=>[]),Promise.resolve(deps.integrations(ids)).catch(()=>[])]),off=strings(state,"disabledEngineIds"),custom=strings(state,"customEngineIds"),current=effectiveDefault(state,ids);return{defaultId:current,engines:ids.map((id)=>{let status=statuses.find((s)=>s.vendor===id),integration=integrations.find((i)=>i.vendor===id),override=trimmed(state,`engineCommand.${id}`),command=override?override.split(/\s+/):deps.defaultCommand(id),account=status?.account;return{id,name:deps.engineName(id),builtin:isBuiltinVendor(id),custom:custom.includes(id),enabled:!off.includes(id),isDefault:id===current,canBeDefault:canBeDefault(state,id),binary:command[0]??null,customized:override.length>0||trimmed(state,`engineName.${id}`).length>0,renamedFrom:engineRenamedFrom(id,trimmed(state,`engineName.${id}`))??null,protocol:trimmed(state,`engineProtocol.${id}`)||null,binaryFound:status?status.binary.found:null,binaryPath:status?.binary.found?status.binary.path??null:null,login:!status||account===null||account===void 0||status.accountError?"unknown":account.kind==="none"?"no":"yes",hooks:integration?.hooksSupported?integration.hookState:"unsupported",markers:integration?.markers??!1,screen:integration?.screen??!1,...integration?.configIssue?{configIssue:integration.configIssue}:{},skill:integration?.skillDir?integration.skillState:"unsupported"}})}}},"skill.install":{kind:"write",destructive:!0,wraps:"installSkillErrors (lib/skill-install.ts), as Settings \u2192 Engines Rove agent skill",async run(){return{error:deps.installSkill()}}},"skill.missing":{kind:"read",destructive:!1,wraps:"missingSkillNames (lib/skill-install.ts): runnable engines with no skill copy, for the persistent banner",async run(){return{engines:await deps.skillMissing()}}},"engine.setEnabled":{kind:"write",destructive:!0,wraps:"state.json disabledEngineIds (+ defaultVendor handoff), as Settings \u2192 Engines `space`",async run(args){let state=deps.load(),ids=await engineIds(deps,state),id=listedId(args,ids),enabled=bool2(args,"enabled");return deps.update((fresh)=>{let off=strings(fresh,"disabledEngineIds");if(enabled)return{disabledEngineIds:without(off,id)};if(off.includes(id))return{};return{disabledEngineIds:[...off,id],...afterLeaving(fresh,ids,id,off)}}),{id,enabled}}},"engine.setDefault":{kind:"write",destructive:!0,wraps:"state.json defaultVendor (+ re-enables it), as Settings \u2192 Engines `d`",async run(args){let state=deps.load(),ids=await engineIds(deps,state),id=listedId(args,ids);if(!canBeDefault(state,id))throw new BridgeError("BAD_ARGS",`${id} cannot be the default; only built-in and custom engines can`);return deps.update((fresh)=>{let off=strings(fresh,"disabledEngineIds");return{defaultVendor:id,...off.includes(id)?{disabledEngineIds:without(off,id)}:{}}}),{id}}},"engine.rename":{kind:"write",destructive:!0,wraps:"state.json engineName.<id>, as Settings \u2192 Engines `r` (blank clears)",async run(args){let state=deps.load(),id=listedId(args,await engineIds(deps,state)),raw=typeof args.name==="string"?args.name.trim():"";if(raw.length>NAME_MAX2||/[\u0000-\u001f\u007f]/.test(raw))throw new BridgeError("BAD_ARGS",`name must be at most ${NAME_MAX2} printable characters`);return deps.update(()=>({[`engineName.${id}`]:raw.length>0?raw:void 0})),{id,name:raw.length>0?raw:deps.engineName(id)}}},"engine.reset":{kind:"write",destructive:!0,wraps:"state.json engineCommand/engineName/engineProtocol/customEngineIds, as Settings \u2192 Engines `x`",async run(args){let state=deps.load(),ids=await engineIds(deps,state),id=listedId(args,ids),clear={[`engineCommand.${id}`]:void 0,[`engineName.${id}`]:void 0},removed=!1;return deps.update((fresh)=>{let custom=strings(fresh,"customEngineIds");if(!custom.includes(id))return clear;removed=!0;let off=strings(fresh,"disabledEngineIds"),handoff=off.includes(id)?{}:afterLeaving(fresh,ids,id,off);return{...clear,[`engineProtocol.${id}`]:void 0,customEngineIds:without(custom,id),disabledEngineIds:without(off,id),...handoff}}),{id,removed}}}}}var engineOps=createEngineOps(realDeps5);var MODE_KEY="autoRouting.classifier",ENDPOINT_KEY="autoRouting.classifierEndpoint",CLASSIFIER_MODES=["off","jev","custom"],real={...realDeps5,secret:(name)=>secretStatus(name).source},str3=(v)=>typeof v==="string"?v.trim():"";async function pickable(deps,ids){return ids.map((id)=>{let entry=protocolEntry(id);return{id,name:deps.engineName(id),effortLevels:entry.effortLevels??[],takesModel:Boolean(entry.modelArgv)}})}function classifierView(deps,state){let config=readClassifierConfig((key)=>state[key]),mode=config.mode.kind==="url"?"custom":config.mode.kind==="jev"?"jev":"off",endpoint=displayEndpoint(config.mode.kind,str3(state[MODE_KEY]),str3(state[ENDPOINT_KEY])),host="";try{host=endpoint?new URL(endpoint).host:""}catch{host=""}return{mode,endpointHost:host,canCustom:str3(state[ENDPOINT_KEY]).length>0&&readClassifierConfig((k)=>k===MODE_KEY?state[ENDPOINT_KEY]:state[k]).mode.kind==="url",threshold:config.threshold,keyConfigured:config.keyEnv!==void 0,keySource:config.keyEnv?deps.secret(config.keyEnv):"none",...config.keyEnv?{keyEnv:config.keyEnv}:{}}}function createRoutingOps(deps){return{"autoRouting.get":{kind:"read",destructive:!1,wraps:"settings Auto routing section: readAutoRoutingTable + tierBlock over the engine statuses + readClassifierConfig",async run(){let state=deps.load(),ids=await engineIds(deps,state),statuses=await deps.statuses(ids).catch(()=>[]),table=readAutoRoutingTable((key)=>state[key]),engineSet=new Set(ids);return{configured:table!==null,tiers:table?AUTO_ROUTING_TIERS.map((tier)=>{let target=table[tier],block=tierBlock(target,{engineIds:engineSet,accountKind:(engine)=>statuses.find((s)=>s.vendor===engine)?.account?.kind??null});return{tier,engine:target.engine,engineName:deps.engineName(target.engine),model:target.model??null,effort:target.effort??null,block:block?describeTierBlock(block):null}}):[],engines:await pickable(deps,ids),classifier:classifierView(deps,state)}}},"autoRouting.setTier":{kind:"write",destructive:!0,wraps:"state.json autoRouting.<tier>.engine|model|effort, as the Auto routing row's engine picker",async run(args){let tier=oneOf(args,"tier",AUTO_ROUTING_TIERS),state=deps.load(),engine=(await pickable(deps,await engineIds(deps,state))).find((e)=>e.id===args.engine);if(!engine)throw new BridgeError("BAD_ARGS","engine is not a listed engine");let model2=optModel(args,"model")??"";if(model2&&!engine.takesModel)throw new BridgeError("BAD_ARGS",`${engine.id} takes no model`);let effort=typeof args.effort==="string"?args.effort:"";if(effort&&!engine.effortLevels.includes(effort))throw new BridgeError("BAD_ARGS",`effort must be one of ${engine.effortLevels.join("|")||"(none)"}`);return deps.update(()=>({[autoRoutingKey(tier,"engine")]:engine.id,[autoRoutingKey(tier,"model")]:model2,[autoRoutingKey(tier,"effort")]:effort})),{tier,engine:engine.id,model:model2||null,effort:effort||null}}},"autoRouting.setClassifier":{kind:"write",destructive:!0,wraps:"state.json autoRouting.classifier: off | jev | the endpoint the TUI remembered (never a client-sent URL)",async run(args){let mode=oneOf(args,"mode",CLASSIFIER_MODES);return deps.update((fresh)=>{if(mode==="custom"){if(!classifierView(deps,fresh).canCustom)throw new BridgeError("BAD_ARGS","no custom endpoint is remembered; set one in the TUI");return{[MODE_KEY]:str3(fresh[ENDPOINT_KEY])}}let live=str3(fresh[MODE_KEY]);return{...classifierView(deps,fresh).mode==="custom"&&live?{[ENDPOINT_KEY]:live}:{},[MODE_KEY]:mode}}),{mode}}}}}var routingOps=createRoutingOps(real);import{createServer}from"net";import{join as join3}from"path";var RUN_TASKS_KEY="run.tasks",BASE_KEY="run.portBase",COUNT_KEY="run.portCount",DEFAULT_BASE=4100,DEFAULT_COUNT=900,SCRIPT=".rove/run.sh",TAB_TITLE="run",realDeps6={load:loadStateFile,update:(compute)=>void updateStateFile((state)=>{for(let[key,value]of Object.entries(compute(state)))state[key]=value;return}),portFree:(port)=>{let{promise,resolve}=Promise.withResolvers(),probe=createServer();return probe.once("error",()=>resolve(!1)),probe.listen(port,()=>probe.close(()=>resolve(!0))),promise},hasScript:(worktree)=>isNonEmptyRepoFile(join3(worktree,SCRIPT))};function readRecords(state){let raw=state[RUN_TASKS_KEY];if(!raw||typeof raw!=="object"||Array.isArray(raw))return{};let out={};for(let[id,value]of Object.entries(raw)){let r=value;if(!r||typeof r.port!=="number"||!Number.isInteger(r.port))continue;out[id]={port:r.port,...typeof r.session==="string"?{session:r.session}:{},...typeof r.taskId==="string"?{taskId:r.taskId}:{},...typeof r.tabId==="string"?{tabId:r.tabId}:{}}}return out}function portRange(state){let base=state[BASE_KEY],count=state[COUNT_KEY],okBase=typeof base==="number"&&Number.isInteger(base)&&base>=1024&&base<=65535,okCount=typeof count==="number"&&Number.isInteger(count)&&count>=1,start=okBase?base:DEFAULT_BASE;return{base:start,count:Math.max(1,Math.min(okCount?count:DEFAULT_COUNT,65536-start))}}async function pickPort(range,taken,own,running,portFree){if(own!==void 0&&(running||await portFree(own)))return own;for(let port=range.base;port<range.base+range.count;port++)if(!taken.has(port)&&port!==own&&await portFree(port))return port;throw new BridgeError("NO_PORT",`no free port in ${range.base}-${range.base+range.count-1}`)}async function worktreeOf3(api,id){return(await api.verb("get-task",[flag("task-id",id)])).task?.worktreePath||void 0}function runOps(deps=realDeps6){async function state(api,id){let worktree=api.machineOf?.(id)===void 0?await worktreeOf3(api,id):void 0,script=!!worktree&&deps.hasScript(worktree),record=readRecords(deps.load())[id],port=record?.port??null;if(!script)return{script,running:!1,port,listening:!1,url:null,record};let running=!1;if(record?.session){let{sessions}=await api.verb("pty-list",[]);running=!!sessions?.some((s)=>s.key===record.session&&s.alive)}let listening=!1;if(running&&record)listening=!!(await api.verb("ports",["--all"])).ports?.some((p)=>p.port===record.port);let url=listening&&record?`http://localhost:${record.port}`:null;return{script,running,port,listening,url,record}}let answer2=({script,running,port,listening,url})=>({script,running,port,listening,url});return{"run.status":{kind:"read",destructive:!1,wraps:"rove api get-task + pty-list + ports --all (is `.rove/run.sh` there, is its tab alive, does its port listen)",async run(a,{api}){return answer2(await state(api,taskId(a)))}},"run.start":{kind:"write",destructive:!1,wraps:"rove api tab-new --kind shell --type `env PORT=<port> sh .rove/run.sh` (the repo's own committed file; no client text)",async run(a,{api}){let id=taskId(a),now=await state(api,id);if(!now.script)throw new BridgeError("NO_RUN_SCRIPT",`this task's repo has no ${SCRIPT}`);if(now.running)return answer2(now);let all=readRecords(deps.load()),taken=new Set(Object.entries(all).flatMap(([other,r])=>other===id?[]:[r.port])),port=await pickPort(portRange(deps.load()),taken,now.record?.port,!1,deps.portFree),line=`env PORT=${port} ROVE_PORT=${port} sh ${SCRIPT}`,opened=await api.verb("tab-new",[flag("task-id",id),flag("kind","shell"),flag("type",line)]);if(!opened.tabId||!opened.session)throw new BridgeError("SESSION_FAILED","the run tab did not open");let owner=opened.childTaskId??id;return await api.verb("update",[flag("task-id",owner),flag("tab",opened.tabId),flag("title",`${TAB_TITLE} :${port}`)]).catch(()=>{}),deps.update((fresh)=>({[RUN_TASKS_KEY]:{...readRecords(fresh),[id]:{port,session:opened.session,taskId:owner,tabId:opened.tabId}}})),{script:!0,running:!0,port,listening:!1,url:null}}},"run.stop":{kind:"write",destructive:!1,wraps:"rove api tab-close on the run tab (its shell and the server under it end), then archive on the empty `shell` row `run.start` made; the task keeps its port",async run(a,{api}){let id=taskId(a),record=readRecords(deps.load())[id];if(record?.session&&record.taskId&&record.tabId){if(await api.verb("tab-close",[flag("task-id",record.taskId),flag("tab",record.tabId)]).catch(()=>{}),record.taskId!==id)await api.verb("archive",[flag("task-id",record.taskId)]).catch(()=>{});deps.update((fresh)=>({[RUN_TASKS_KEY]:{...readRecords(fresh),[id]:{port:record.port}}}))}return answer2(await state(api,id))}}}}var RUN_OPS=runOps();class UsageStore{rows=null;sessions=null;set(snapshot){this.rows=snapshot===null?null:[...snapshot.entries()].map(([vendor,usage])=>({vendor,capturedAt:usage.capturedAt,windows:usage.windows.map(({kind,label,percent,resetsAt})=>({kind,label,percent,resetsAt}))})).sort((a,b)=>a.vendor.localeCompare(b.vendor))}get(){return this.rows}setContext(map){this.sessions=map}context(session){return this.sessions?.get(session)??null}}var usageStore=new UsageStore;var PLUGIN_ID2=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/,CATEGORY=/^[a-z0-9][a-z0-9-]{0,39}$/,realDeps7={usage:usageStore,bridgeVersion:CURRENT_VERSION,engineName:engineDisplayName,listPlugins:()=>readPluginRows(),pluginIds:()=>loadPluginRegistry().plugins.map((p)=>p.id),setPluginEnabled:(id,enabled)=>setPluginEnabled(id,enabled),installedMods:()=>installedBuiltinMods(),setModInstalled:(id,installed)=>setBuiltinModInstalled(id,installed)};function createSettingsOps(deps){return{"usage.get":{kind:"read",destructive:!1,wraps:"the orchestrator's usageSnapshotSignal (daemon usage.snapshot) and, given taskId+tabId, its contextUsageSignal for that session; both held by the bridge",async run(args){let rows=deps.usage.get(),usage=rows===null?null:rows.map((row)=>({...row,name:deps.engineName(row.vendor)}));if(args.taskId===void 0)return{usage};return{usage,context:deps.usage.context(`${taskId(args)}::${tabId(args)}`)}}},"daemon.info":{kind:"read",destructive:!1,wraps:"daemon RPC daemon.status, compared with the bridge's own Rove build (isDaemonVersionStale)",async run(_args,{api}){let status=await api.rpc("daemon.status",{});return{daemonVersion:status.roveVersion??null,bridgeVersion:deps.bridgeVersion,stale:isDaemonVersionStale(status.roveVersion,deps.bridgeVersion),...status.uptimeMs!==void 0?{uptimeMs:status.uptimeMs}:{},...status.startedAt!==void 0?{startedAt:status.startedAt}:{},...status.taskCount!==void 0?{taskCount:status.taskCount}:{},...status.attachedClients!==void 0?{attachedClients:status.attachedClients}:{},...status.automationHold!==void 0?{automationHold:status.automationHold}:{}}}},"plugins.list":{kind:"read",destructive:!1,wraps:"settings Plugins section's readPluginRows (registry + manifest + last run)",async run(){return{plugins:deps.listPlugins().map((p)=>({id:p.id,version:p.version,enabled:p.enabled,linked:p.linked,platformOk:p.platformOk,hooksDeclared:p.hooksDeclared,updateAvailable:p.updateAvailable,declares:p.declares,lastRun:p.lastRun?{at:p.lastRun.at,label:p.lastRun.label,ok:p.lastRun.ok,running:p.lastRun.running}:null}))}}},"plugin.setEnabled":{kind:"write",destructive:!0,wraps:"settings Plugins section's setPluginEnabled (plugins.json; the daemon file-watches it)",async run(args){let id=text2(args,"id",64);if(!PLUGIN_ID2.test(id)||!deps.pluginIds().includes(id))throw new BridgeError("BAD_ARGS",`${id} is not an installed plugin`);let enabled=bool2(args,"enabled");return deps.setPluginEnabled(id,enabled),{id,enabled}}},"builtinMods.list":{kind:"read",destructive:!1,wraps:"builtin-mods.json: the first-party mods (Kanban, Issues) and which are installed",async run(){let installed=new Set(deps.installedMods());return{mods:BUILTIN_MODS.map((mod)=>({id:mod.id,name:mod.name,description:mod.description,installed:installed.has(mod.id)}))}}},"builtinMod.setInstalled":{kind:"write",destructive:!1,wraps:"builtin-mods.json (the TUI, the daemon and `rove api` read the same file)",async run(args){let id=text2(args,"id",64);if(!isBuiltinModId(id))throw new BridgeError("BAD_ARGS",`${id} is not a first-party mod`);let installed=bool2(args,"installed");return deps.setModInstalled(id,installed),{id,installed}}},"feedback.send":{kind:"write",destructive:!0,wraps:"rove api feedback (GitHub Discussion in the Rove repo's Feedback category, via gh)",run(args,{api}){let argv=[flag("title",text2(args,"title",200).trim()),flag("body",text2(args,"body",1e4))],category=optText(args,"category",40);if(category!==void 0){if(!CATEGORY.test(category))throw new BridgeError("BAD_ARGS","category is not a category slug");argv.push(flag("category",category))}return api.verb("feedback",argv)}}}}var settingsOps=createSettingsOps(realDeps7);var SNIPPETS_KEY="prompt.snippets",SNIPPET_NAME=/^[A-Za-z0-9][\w-]{0,31}$/,SNIPPET_TEXT_MAX=8000,SNIPPETS_MAX=100;function readSnippets(state){let raw=state[SNIPPETS_KEY];if(!Array.isArray(raw))return[];let out=[];for(let item of raw){if(!item||typeof item!=="object")continue;let{name,text:body}=item;if(typeof name==="string"&&SNIPPET_NAME.test(name)&&typeof body==="string"&&body.length>0)out.push({name,text:body})}return out}var same2=(a,b)=>a.toLowerCase()===b.toLowerCase(),realDeps8={load:loadStateFile,update:(compute)=>void updateStateFile((state)=>{let next=compute(state);for(let[key,value]of Object.entries(next))state[key]=value;return})};function name(a){let value=text2(a,"name",32);if(!SNIPPET_NAME.test(value))throw new BridgeError("BAD_ARGS","name must be letters, digits, - or _ (up to 32), starting with a letter or digit");return value}function createSnippetOps(deps){return{"snippets.get":{kind:"read",destructive:!1,wraps:"state.json prompt.snippets (name and text pairs)",async run(){return{snippets:readSnippets(deps.load())}}},"snippets.save":{kind:"write",destructive:!1,wraps:"state.json prompt.snippets: adds a snippet, or replaces the one with that name (case-insensitive)",async run(a){let key=name(a),body=text2(a,"text",SNIPPET_TEXT_MAX);return deps.update((fresh)=>{let all=readSnippets(fresh),index=all.findIndex((s)=>same2(s.name,key));if(index<0&&all.length>=SNIPPETS_MAX)throw new BridgeError("BAD_ARGS",`at most ${SNIPPETS_MAX} snippets`);if(index<0)all.push({name:key,text:body});else all[index]={name:key,text:body};return{[SNIPPETS_KEY]:all}}),{snippets:readSnippets(deps.load())}}},"snippets.remove":{kind:"write",destructive:!0,wraps:"state.json prompt.snippets: drops the snippet with that name (case-insensitive)",async run(a){let key=name(a);return deps.update((fresh)=>({[SNIPPETS_KEY]:readSnippets(fresh).filter((s)=>!same2(s.name,key))})),{snippets:readSnippets(deps.load())}}}}}var snippetOps=createSnippetOps(realDeps8);import{existsSync as existsSync4,realpathSync,statSync}from"fs";import{homedir}from"os";import{extname,join as join4,sep}from"path";var SUBDIR_CAP=200,PATH_MAX3=1000;async function reorderDaemonTab(taskId2,tab,delta,api){let{state}=await api.rpc("tabs.get",{taskId:taskId2});if(!state||!Array.isArray(state.tabs))return!1;let current={...state,activeId:state.activeId??""},next=moveTab(current,tab,delta);if(next===current)return!1;let ops=[{op:"order",ids:next.tabs.map((t)=>t.id)}];return await api.rpc("tabs.apply",{taskId:taskId2,ops}),!0}function canonical(path){try{return realpathSync(path)}catch{return null}}function runnable(path){let info=statSync(path);return!info.isFile()||(info.mode&73)!==0}var OPENABLE=new Set("pdf png jpg jpeg gif webp bmp tif tiff heic svg ico mp3 wav m4a aac flac ogg mp4 m4v mov webm mkv avi ttf otf".split(" ")),realDeps9={openExternal:openExternally,detectOpener:detectWorktreeOpener,openWorktree,exists:existsSync4,canonical,runnable,homeDir:homedir,buildCIPrompt:buildCIPromptForWorktree,prPrompt:buildPRPrompt,reorderTab:reorderDaemonTab};function typedPath(args,name2){let v=args[name2];if(typeof v!=="string"||v.length>PATH_MAX3||v.includes("\x00"))throw new BridgeError("BAD_ARGS",`${name2} must be a path`);let trimmed2=v.trim();if(!(trimmed2.startsWith("/")||trimmed2.startsWith("~")))throw new BridgeError("BAD_ARGS",`${name2} must be absolute or start with ~`);return trimmed2}function clearable(args,name2,check){let v=args[name2];if(v===void 0||v===null)return;return v===""?"":check(args,name2)}async function worktreeAndBranch(ctx,id){let res=await ctx.api.verb("get-task",[flag("task-id",id)]),{worktreePath,branch,prStatus}=res.task;if(!worktreePath)throw new BridgeError("NO_WORKTREE",`task ${id} has no worktree yet`);return{worktree:worktreePath,branch:branch??"",prNumber:prStatus?.number,baseRef:prStatus?.baseRef}}var PR_PROMPT_KINDS=["commit","createPR","fixCI","conflicts","comments"];function conflictsPrompt(pr,base){return`${pr} has merge conflicts with ${base}. Fetch, merge ${base} into this branch, resolve every conflict keeping the intent of both sides, run the project's checks, then commit the merge and push.`}function commentsPrompt(pr){return`${pr} has review feedback to address. Read it with \`gh pr view --comments\` and \`gh api repos/{owner}/{repo}/pulls/<number>/comments\`, make a change for each point (reply only where you disagree and say why), run the project's checks, then commit and push.`}var COMMIT_PROMPT="Commit the uncommitted changes in this worktree. Review them with `git status` and `git diff`, split them into logical commits when they are unrelated, write each message in the style of this repo's recent `git log`, and do not push.";function taskOps(deps=realDeps9){return{"task.landPreflight":{kind:"read",destructive:!1,wraps:"rpc task.landPreflight",async run(a,{api}){return(await api.rpc("task.landPreflight",{taskId:taskId(a)})).result}},"task.syncBase":{kind:"write",destructive:!1,wraps:"rpc task.syncBase (merge base into the worktree)",async run(a,{api}){let res=await api.rpc("task.syncBase",{taskId:taskId(a)}),failed=res.result.repos?.find((r)=>!r.ok);if(failed)throw Error(failed.error??"sync failed");return{baseRef:res.result.baseRef,alreadyCurrent:res.result.alreadyCurrent===!0}}},"task.fixChecks":{kind:"write",destructive:!1,wraps:"rpc pr.failingChecks + buildCIPromptForWorktree + rove api send --plain",async run(a,ctx){let id=taskId(a),{worktree,branch,prNumber}=await worktreeAndBranch(ctx,id),read=await ctx.api.rpc("pr.failingChecks",{taskId:id}),checks=read.checks??[];if(read.unavailable)return{outcome:"unavailable",detail:read.unavailable.detail};if(checks.length===0)return{outcome:"none"};let state={checks,totalFailing:read.totalFailing??checks.length},prompt=await deps.buildCIPrompt(worktree,{branch:branch||"HEAD",...prNumber===void 0?{}:{prNumber},...state}),argv=[flag("task-id",id),"--plain",flag("prompt",prompt)];if(a.tabId!==void 0&&a.tabId!==null)argv.push(flag("tab",tabId(a)));return await ctx.api.verb("send",argv),{outcome:"sent"}}},"task.prompt":{kind:"read",destructive:!1,wraps:"buildPRPrompt | rpc pr.failingChecks + buildCIPromptForWorktree | fixed text (commit, conflicts, comments); returns the prompt, sends nothing",async run(a,ctx){let id=taskId(a),kind=oneOf(a,"kind",PR_PROMPT_KINDS),{worktree,branch,prNumber,baseRef}=await worktreeAndBranch(ctx,id),pr=prNumber===void 0?"This task's PR":`PR #${prNumber}`;if(kind==="createPR")return{outcome:"ok",prompt:await deps.prPrompt(worktree)};if(kind==="commit")return{outcome:"ok",prompt:COMMIT_PROMPT};if(kind==="conflicts")return{outcome:"ok",prompt:conflictsPrompt(pr,baseRef||"its base branch")};if(kind==="comments")return{outcome:"ok",prompt:commentsPrompt(pr)};let read=await ctx.api.rpc("pr.failingChecks",{taskId:id}),checks=read.checks??[];if(read.unavailable)return{outcome:"unavailable",detail:read.unavailable.detail};if(checks.length===0)return{outcome:"none"};return{outcome:"ok",prompt:await deps.buildCIPrompt(worktree,{branch:branch||"HEAD",...prNumber===void 0?{}:{prNumber},checks,totalFailing:read.totalFailing??checks.length})}}},"task.openEditor":{kind:"write",destructive:!1,wraps:"helpers detectWorktreeOpener + openWorktree (the TUI's `o`) on the task's worktree, via ensure-worktree",async run(a,{api}){let id=taskId(a),path=(await api.verb("get-task",[flag("task-id",id)])).task.worktreePath??"";if(!path||!deps.exists(path))path=(await api.verb("ensure-worktree",[flag("task-id",id)])).worktreePath??"";if(!path||!deps.exists(path))throw new BridgeError("NO_WORKTREE","this task has no worktree on disk");let opener=deps.detectOpener();if(!opener)return{outcome:"noEditor"};if(!deps.openWorktree(path,opener))return{outcome:"failed",label:opener.label};return{outcome:"opened",label:opener.label}}},"task.openScratch":{kind:"write",destructive:!1,wraps:"rpc task.openDir {dir: $HOME, scratch: true}",async run(a,ctx){let home=deps.homeDir(),picked=optStr(a,"engine")===void 0?void 0:await engineId(a,"engine",ctx),res=await ctx.api.rpc("task.openDir",{dir:home,scratch:!0,vendor:picked??taskOpDeps.preferredEngine(home)});if(picked)taskOpDeps.rememberEngine(home,picked);return{taskId:res.taskId}}},"task.setEngine":{kind:"write",destructive:!1,wraps:"rpc task.setVendor (engine + reasoning level + model; '' clears)",async run(a,ctx){let id=taskId(a),vendor=await engineId(a,"engine",ctx),effort=clearable(a,"effort",(args,n)=>effortLevel(args,n)),mdl=clearable(a,"model",(args,n)=>model(args,n));return await ctx.api.rpc("task.setVendor",{taskId:id,vendor,...effort===void 0?{}:{effort},...mdl===void 0?{}:{model:mdl}}),{}}},"tab.move":{kind:"write",destructive:!1,wraps:"rpc tabs.apply order (the TUI's moveTab on tabs.get)",async run(a,{api}){let direction=oneOf(a,"direction",["up","down"]);return{moved:await deps.reorderTab(taskId(a),tabId(a),direction==="up"?-1:1,api)}}},"file.openExternal":{kind:"write",destructive:!1,wraps:"helper openExternally (the Mac's `open`) on a media/document file inside the task's worktree",async run(a,{api}){let rel=optStr(a,"path")??"";if(!rel||rel.length>PATH_MAX3||rel.startsWith("/")||rel.includes("\x00")||rel.split(/[\\/]/).includes(".."))throw new BridgeError("BAD_ARGS","path must be relative to the worktree");let worktree=(await api.verb("get-task",[flag("task-id",taskId(a))])).task.worktreePath;if(!worktree)throw new BridgeError("NO_WORKTREE","this task has no worktree yet");let root=deps.canonical(worktree),abs=deps.canonical(join4(worktree,rel));if(!root||!abs)throw new BridgeError("NOT_FOUND",`${rel} does not exist`);if(abs!==root&&!abs.startsWith(root.endsWith(sep)?root:root+sep))throw new BridgeError("BAD_ARGS","path is outside the worktree");if(!OPENABLE.has(extname(abs).slice(1).toLowerCase())||deps.runnable(abs))throw new BridgeError("NOT_OPENABLE","only images, audio, video, PDFs and fonts open in the system app");return deps.openExternal(abs),{}}},"fs.subdirs":{kind:"read",destructive:!1,wraps:"helpers splitPathForDirSuggest + listSubdirs + filterSubdirs",async run(a){let split=splitPathForDirSuggest(typedPath(a,"path")),names=filterSubdirs(listSubdirs(split.base),split.filter);return{base:split.base,home:deps.homeDir(),names:names.slice(0,SUBDIR_CAP)}}},"repo.probe":{kind:"read",destructive:!1,wraps:"helpers validateRepoPath + listLocalBranches + getCurrentBranch (+ rpc worktree.discoverAdoptable)",async run(a,{api}){let repo=stripTrailingSlash(expandHome(typedPath(a,"path"))),reason=taskOpDeps.validateRepoPath(repo);if(reason)return{ok:!1,repo,reason};let out={ok:!0,repo,branches:taskOpDeps.listLocalBranches(repo),current:taskOpDeps.getCurrentBranch(repo)};if(a.adoptable===!0){let found=await api.rpc("worktree.discoverAdoptable",{repo});out.worktrees=found.worktrees??[]}return out}}}}var TASKOPS_OPS=taskOps();async function commandFromTask(args,ctx,where){if(args.commandFrom===void 0||args.commandFrom===null||args.commandFrom==="")return null;let id=taskId(args,"commandFrom");if(where.machine)throw new BridgeError("BAD_ARGS","commandFrom needs a task on this machine");if(where.parallel)throw new BridgeError("BAD_ARGS","commandFrom conflicts with count and agents");if(!where.engine)throw new BridgeError("BAD_ARGS","commandFrom needs the engine the source task runs");if(ctx.api.machineOf?.(id))throw new BridgeError("BAD_ARGS","commandFrom needs a task on this machine");let{task}=await ctx.api.verb("get-task",[flag("task-id",id)]);if(task.vendor!==where.engine)throw new BridgeError("BAD_ARGS","commandFrom is only for the engine that task runs; pick that engine or drop it");return task.command??null}var STATUSES=["backlog","in_progress","in_review","done","canceled","error"],DIRECTIONS=["up","down","top"],PROMPT_MAX2=1e5;function extraRepos(args){let raw=args.extraRepos;if(raw===void 0||raw===null)return[];if(!Array.isArray(raw))throw new BridgeError("BAD_ARGS","extraRepos must be an array");if(raw.length+1>MULTI_REPO_CAP)throw new BridgeError("BAD_ARGS",`a task spans at most ${MULTI_REPO_CAP} repos`);return raw.map((entry,i)=>{let item=typeof entry==="object"&&entry!==null?entry:{},repo=absPath(item,"repo"),baseRef=optGitRef(item,"baseRef");if(!repo)throw new BridgeError("BAD_ARGS",`extraRepos[${i}].repo is required`);return{repo,...baseRef?{baseRef}:{}}})}function write(wraps,run,destructive=!1){return{kind:"write",destructive,wraps,run:async(args,ctx)=>run(args,ctx)}}async function done(call){return await call,{}}async function spawn(args,ctx){let machine=optText(args,"machine",64),repo=machine?remoteRepoRef(args,"repo"):absPath(args,"repo"),title=optText(args,"title",200),prompt=optText(args,"prompt",PROMPT_MAX2),branch=optGitRef(args,"branch"),baseBranch=optGitRef(args,"baseBranch"),mdl=optModel(args),effort=args.effort===void 0||args.effort===null||args.effort===""?void 0:effortLevel(args,"effort"),count=optInt(args,"count",1,10),agentsGiven=args.agents!==void 0&&args.agents!==null&&args.agents!=="",status=optOneOf(args,"status",STATUSES),pin=optBool(args,"pin"),engineGiven=args.engine!==void 0&&args.engine!==null&&args.engine!=="",extras=extraRepos(args),clone=optBool(args,"clone");if(clone&&!machine)throw new BridgeError("BAD_ARGS","clone needs a machine");if(clone&&repo.startsWith("/"))throw new BridgeError("BAD_ARGS","clone needs a repo id, not a path");if(extras.length>0&&machine)throw new BridgeError("BAD_ARGS","a multi-repo task must keep every repo on this machine");let parallel=count!==void 0||agentsGiven;if(count!==void 0&&agentsGiven)throw new BridgeError("BAD_ARGS","count and agents are exclusive");if(parallel&&!prompt)throw new BridgeError("BAD_ARGS","count/agents need a prompt");if(parallel&&branch)throw new BridgeError("BAD_ARGS","branch cannot be shared by parallel tasks");if(agentsGiven&&engineGiven)throw new BridgeError("BAD_ARGS","engine conflicts with agents");let plan=agentsGiven?parseAgents(args):void 0;if(plan&&!machine)await checkAgentEngines(plan,ctx);let engine=!engineGiven?void 0:machine?text2(args,"engine",64):await engineId(args,"engine",ctx),carried=await commandFromTask(args,ctx,{engine,machine,parallel}),argv=[flag("repo",repo)];if(clone)argv.push(flag("clone","true"));for(let extra of extras)argv.push(flag("repo",extra.repo));for(let extra of extras)if(extra.baseRef)argv.push(flag("base-for",`${extra.repo}=${extra.baseRef}`));if(title)argv.push(flag("title",title));if(branch)argv.push(flag("branch",branch));if(baseBranch)argv.push(flag("base-branch",baseBranch));if(engine)argv.push(flag("command",carried??engine));if(effort)argv.push(flag("effort",effort));if(mdl)argv.push(flag("model",mdl));if(count!==void 0)argv.push(flag("count",count));if(plan)argv.push(flag("agents",plan.map((p)=>`${p.id}:${p.n}`).join(",")));if(status)argv.push(flag("status",status));if(pin)argv.push(flag("pin","true"));if(prompt)argv.push(flag("prompt",prompt));let res=await ctx.api.verb("add",argv,machine);if(engine&&!machine)taskOpDeps.rememberEngine(repo,engine);if(Array.isArray(res.tasks))return{taskIds:res.tasks.flatMap((t)=>typeof t.taskId==="string"?[t.taskId]:[]),...res.groupId?{groupId:res.groupId}:{}};if(typeof res.taskId!=="string")throw new BridgeError("BAD_RESULT","add returned no task id");return{taskIds:[res.taskId]}}var PROGRESS_GAP_MS=150;async function clone(args,ctx){let url=await cloneSource(args),parentDir=absPath(args,"parentDir"),urlProblem=taskOpDeps.validateGitUrl(url);if(urlProblem)throw new BridgeError("BAD_ARGS",urlProblem);let given2=optStr(args,"folder"),folder=cloneFolder(given2??taskOpDeps.findAvailableFolderName(parentDir,taskOpDeps.deriveFolderName(url))),targetProblem=taskOpDeps.validateCloneTarget(parentDir,folder);if(targetProblem)throw new BridgeError("BAD_ARGS",targetProblem);let lastSent=0,result=await taskOpDeps.cloneRepo(url,taskOpDeps.resolveCloneTarget(parentDir,folder),(line)=>{let now=Date.now();if(now-lastSent<PROGRESS_GAP_MS)return;lastSent=now,ctx.progress?.(line)});if(!result.ok)throw new BridgeError("CLONE_FAILED",result.error);return{path:result.path}}async function adopt(args,ctx){let repo=await knownRepo(args,ctx),worktreePath=absPath(args,"worktreePath"),branch=optGitRef(args,"branch"),title=optText(args,"title",200),engine=args.engine===void 0||args.engine===null||args.engine===""?void 0:await engineId(args,"engine",ctx);if(!((await ctx.api.rpc("worktree.discoverAdoptable",{repo})).worktrees??[]).some((w)=>w.path===worktreePath))throw new BridgeError("NOT_ADOPTABLE",`${worktreePath} is not an adoptable worktree of ${repo}`);return{taskId:(await ctx.api.rpc("worktree.adopt",{repo,worktreePath,...branch?{branch}:{},...title?{title}:{},...engine?{vendor:engine}:{}})).task.id}}async function archive(args,ctx){let id=taskId(args),archived=bool2(args,"archived"),argv=[flag("task-id",id)];if(archived&&optBool(args,"force"))argv.push(flag("force","true"));return{ids:(await ctx.api.verb(archived?"archive":"unarchive",argv)).ids??[]}}var TASK_WRITE_OPS={"task.spawn":write("verb add",spawn),"task.rename":write("verb update --task-id --title",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("title",text2(a,"title",200))]))),"task.setBranch":write("verb update --task-id --branch",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("branch",gitRef(a,"branch"))]))),"task.moveToProject":write("verb update --task-id --project",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("project",absPath(a,"repo"))]))),"task.setCommand":write("verb update --task-id --command=<engine id>",async(a,c)=>{let id=taskId(a),engine=await engineId(a,"engine",c),res=await c.api.verb("update",[flag("task-id",id),flag("command",engine)]);return res.protocol?{protocol:res.protocol}:{}}),"task.setModel":write("verb update --task-id --model",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("model",model(a))]))),"task.setEffort":write("verb update --task-id --effort",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("effort",effortLevel(a))]))),"task.setStatus":write("verb update --task-id --status",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("status",oneOf(a,"status",STATUSES))]))),"task.pin":write("verb update --task-id --pinned",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("pinned",String(bool2(a,"pinned")))]))),"task.keep":write("verb update --task-id --kept",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("kept",String(bool2(a,"kept")))]))),"task.notes":write("rpc task.notes (notes replace + todo ops, applied together)",(a,c)=>{let notes=optText(a,"notes",20000),ops=a.ops;if(ops!==void 0&&(!Array.isArray(ops)||ops.length>50))throw new BridgeError("BAD_ARGS","ops must be a list of at most 50");return c.api.rpc("task.notes",{taskId:taskId(a),...notes!==void 0?{notes}:{},...ops?{ops}:{}})}),"task.scheduleStart":write("rpc task.scheduleStart",async(a,c)=>{let at=text2(a,"at",40);if(!Number.isFinite(Date.parse(at)))throw new BridgeError("BAD_ARGS","at must be an ISO-8601 time");return c.api.rpc("task.scheduleStart",{taskId:taskId(a),at,prompt:text2(a,"prompt",PROMPT_MAX2)})}),"task.cancelScheduledStart":write("rpc task.cancelScheduledStart",(a,c)=>done(c.api.rpc("task.cancelScheduledStart",{taskId:taskId(a)}))),"task.startScheduledNow":write("rpc task.startScheduledNow",(a,c)=>c.api.rpc("task.startScheduledNow",{taskId:taskId(a)})),"task.autoFixCi":write("verb update --task-id --auto-fix-ci",(a,c)=>done(c.api.verb("update",[flag("task-id",taskId(a)),flag("auto-fix-ci",bool2(a,"enabled")?"on":"off")]))),"task.archive":write("verb archive|unarchive --task-id [--force]",archive),"task.move":write("rpc task.move",(a,c)=>done(c.api.rpc("task.move",{taskId:taskId(a),direction:oneOf(a,"direction",DIRECTIONS)}))),"project.forget":write("rpc project.forget",async(a,c)=>done(c.api.rpc("project.forget",{repo:await knownRepo(a,c)})),!0),"notes.delete":write("verb note-delete --repo --id",async(a,c)=>{let repo=await knownRepo(a,c),id=optInt(a,"id",1,Number.MAX_SAFE_INTEGER);if(id===void 0)throw new BridgeError("BAD_ARGS","id must be an integer in 1..");return{deleted:(await c.api.verb("note-delete",[flag("repo",repo),flag("id",id)])).deleted===!0}},!0),"task.openMain":write("rpc task.ensureMain",async(a,c)=>{let repo=absPath(a,"repo"),problem=taskOpDeps.validateRepoPath(repo);if(problem)throw new BridgeError("BAD_ARGS",problem);return{taskId:(await c.api.rpc("task.ensureMain",{repo})).task.id}}),"task.openCheckout":write("rpc task.ensureMain + task.openDir {dir: the project root}",async(a,c)=>{let repo=absPath(a,"repo"),problem=taskOpDeps.validateRepoPath(repo);if(problem)throw new BridgeError("BAD_ARGS",problem);let title=optText(a,"title",200),engine=a.engine!==void 0&&a.engine!==null&&a.engine!==""?await engineId(a,"engine",c):void 0,main=await c.api.rpc("task.ensureMain",{repo}),res=await c.api.rpc("task.openDir",{dir:main.task.repo,vendor:engine??taskOpDeps.preferredEngine(main.task.repo),...title?{title}:{}});if(engine)taskOpDeps.rememberEngine(main.task.repo,engine);return{taskId:res.taskId}}),"worktree.adopt":write("rpc worktree.adopt",adopt),"repo.clone":write("helper cloneRepo (git clone, fixed argv)",clone),"task.ensureWorktree":write("verb ensure-worktree --task-id",async(a,c)=>{let res=await c.api.verb("ensure-worktree",[flag("task-id",taskId(a))]);if(typeof res.worktreePath!=="string")throw new BridgeError("BAD_RESULT","ensure-worktree returned no path");return{worktreePath:res.worktreePath}}),"task.removeWorktree":write("verb remove-worktree --task-id --force",async(a,c)=>{let argv=[flag("task-id",taskId(a))];if(optBool(a,"force"))argv.push(flag("force","true"));let res=await c.api.verb("remove-worktree",argv);return{removed:res.removed===!0,...res.worktreePath?{worktreePath:res.worktreePath}:{},...res.branch?{branch:res.branch}:{}}},!0)};var TAIL_MAX=2000;function read(wraps,run){return{kind:"read",destructive:!1,wraps,run}}async function taskGet(args,ctx){let t=(await ctx.api.verb("get-task",[flag("task-id",taskId(args))])).task,pr=rollupPr(t);return{task:{id:t.id,title:t.title,repo:t.repo,branch:t.branch,worktreePath:t.worktreePath,kind:t.kind??"task",status:t.status,pinned:t.pinned===!0,...t.archived?{archived:!0}:{},...t.vendor?{engine:t.vendor}:{},...t.command?{command:t.command}:{},...t.model?{model:t.model}:{},...t.modelEffort?{effort:t.modelEffort}:{},...t.prompt?{prompt:t.prompt}:{},...t.baseRef?{baseRef:t.baseRef}:{},...t.groupId?{groupId:t.groupId}:{},...t.extraRepos?.length?{extraRepos:t.extraRepos.map((r)=>({repo:r.repo,...r.baseRef?{baseRef:r.baseRef}:{}}))}:{},createdAt:t.createdAt,updatedAt:t.updatedAt,...pr?{pr:{...pr.number!==void 0?{number:pr.number}:{},...pr.url?{url:pr.url}:{},lifecycle:pr.lifecycle,checkState:pr.checkState,...pr.reviewDecision?{reviewDecision:pr.reviewDecision}:{},...pr.mergeable?{mergeable:pr.mergeable}:{},...pr.baseRef?{baseRef:pr.baseRef}:{}}}:{},...t.report?{report:{summary:t.report.summary??"",at:t.report.at}}:{}}}}async function taskInfo(args,ctx){let id=taskId(args),row=(await ctx.api.verb("collect",[flag("task-ids",id)])).tasks?.[0];if(!row)throw new BridgeError("NOT_FOUND",`no task ${id}`);return{taskId:row.taskId,running:row.running??null,activity:row.activity?{state:row.activity.state,forMs:row.activity.forMs}:null,changes:row.changes?{added:row.changes.added,deleted:row.changes.deleted}:null,base:row.base?{baseRef:row.base.baseRef,ahead:row.base.ahead,behind:row.base.behind}:null,tabs:(row.tabs??[]).map((tab)=>{let exit=tab.exit,tail=exit?.tail?.join(`
|
|
116
|
+
`).slice(-TAIL_MAX);return{id:tab.id,kind:tab.kind,alive:tab.alive??null,...exit?{exit:{code:exit.code,signal:exit.signal,...exit.layer?{cause:exit.layer}:{}}}:{},...tail?{tail}:{}}})}}async function repoBranches(args,ctx){let repo=await knownRepo(args,ctx);return{branches:taskOpDeps.listLocalBranches(repo),current:taskOpDeps.getCurrentBranch(repo)}}async function notesList(args,ctx){let repo=await knownRepo(args,ctx);return{notes:(await ctx.api.verb("note-list",[flag("repo",repo)])).notes??[]}}async function worktreeAdoptable(args,ctx){let repo=await knownRepo(args,ctx),res=await ctx.api.rpc("worktree.discoverAdoptable",{repo});return{worktrees:res.worktrees??[],unreadable:res.unreadable??[]}}var TASK_OPS={"task.get":read("verb get-task --task-id",taskGet),"task.info":read("verb collect --task-ids (first row, trimmed)",taskInfo),"repo.branches":read("helpers listLocalBranches + getCurrentBranch",repoBranches),"notes.list":read("verb note-list --repo",notesList),"worktree.adoptable":read("rpc worktree.discoverAdoptable",worktreeAdoptable),...TASK_WRITE_OPS};import{randomBytes as randomBytes4}from"crypto";import{mkdirSync as mkdirSync3,writeFileSync as writeFileSync3}from"fs";import{join as join5}from"path";var ATTACHMENT_TYPES={"image/png":{ext:"png",kind:"image",magic:[137,80,78,71,13,10,26,10]},"image/jpeg":{ext:"jpg",kind:"image",magic:[255,216,255]},"image/gif":{ext:"gif",kind:"image",magic:[71,73,70,56]},"image/webp":{ext:"webp",kind:"image",magic:[82,73,70,70]},"application/pdf":{ext:"pdf",kind:"pdf",magic:[37,80,68,70]}},MAX_ATTACHMENT_BYTES=5242880,MAX_ATTEMPTS=5,PROMPT_MAX3=1e5,realDeps10={attachmentsDir:promptAttachmentsDir,now:()=>new Date,nonce:()=>randomBytes4(4).toString("hex"),writeFile(path,bytes){mkdirSync3(join5(path,".."),{recursive:!0,mode:448}),writeFileSync3(path,bytes,{mode:384})},readTab(task,tab){return loadStateFile()[terminalTabsKey(task)]?.tabs?.find((t)=>t.id===tab)},planHandoff:planWorktreeHandoff,prPrompt:buildPRPrompt};function gitRef2(args,name2){let v=args[name2];if(!(typeof v==="string"&&v.length<=200&&/^[A-Za-z0-9][A-Za-z0-9._/@+-]*$/.test(v)&&!v.includes("..")&&!v.includes("//")&&!v.endsWith("/")&&!v.endsWith(".")&&!v.endsWith(".lock")&&!v.includes("@{")))throw new BridgeError("BAD_ARGS",`${name2} is not a branch name`);return v}function engineId2(args,name2="engine"){let v=optStr(args,name2);if(v===void 0)return;if(!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(v))throw new BridgeError("BAD_ARGS",`${name2} is not an engine id`);return v}function tabTitle(args){let title=text2(args,"title",80).trim();if(/[\u0000-\u001f\u007f]/.test(title))throw new BridgeError("BAD_ARGS","title must be one line");return title}async function worktreeOf4(api,id){let res=await api.verb("get-task",[flag("task-id",id)]);if(!res.task.worktreePath)throw new BridgeError("NO_WORKTREE",`task ${id} has no worktree yet`);return{task:res.task,worktree:res.task.worktreePath}}function matchesMagic(mime,bytes){let{magic}=ATTACHMENT_TYPES[mime];if(bytes.length<magic.length||!magic.every((b,i)=>bytes[i]===b))return!1;return mime!=="image/webp"||Buffer.from(bytes.subarray(8,12)).toString("ascii")==="WEBP"}function terminalOps(deps=realDeps10){return{"tab.states":{kind:"read",destructive:!1,wraps:"daemon RPC debug.inspect (activity.tabs of one task)",async run(a,{api}){let id=taskId(a),res=await api.rpc("debug.inspect"),tabs={};for(let[tab,entry]of Object.entries(res?.activity?.tabs?.[id]??{}))if(typeof entry?.state==="string"&&typeof entry.at==="number")tabs[tab]={state:entry.state,at:entry.at};return{tabs}}},"tab.rename":{kind:"write",destructive:!1,wraps:"rove api update --tab",async run(a,{api}){return await api.verb("update",[flag("task-id",taskId(a)),flag("tab",tabId(a)),flag("title",tabTitle(a))]),{}}},"tab.interrupt":{kind:"write",destructive:!1,wraps:"rove api interrupt",async run(a,{api}){let argv=[flag("task-id",taskId(a))];if(a.tabId!==void 0&&a.tabId!==null)argv.push(flag("tab",tabId(a)));return await api.verb("interrupt",argv),{}}},"tab.forkTask":{kind:"write",destructive:!1,wraps:"rove api add --fork (one attempt, from the task's branch), else --base-branch (--count for attempts)",async run(a,{api}){let count=optInt(a,"count",1,MAX_ATTEMPTS)??1,repo=absPath(a,"repo"),baseBranch=gitRef2(a,"baseBranch"),prompt=text2(a,"prompt",PROMPT_MAX3),engine=engineId2(a),title=optText(a,"title",200),tail=[flag("prompt",prompt),...engine?[flag("command",engine)]:[]],forkFrom=count===1&&a.forkFrom!==void 0&&a.forkFrom!==null?taskId(a,"forkFrom"):void 0,forkRefused;if(forkFrom)try{let res2=await api.verb("add",[flag("fork",forkFrom),flag("title",title??deriveTitleFromPrompt(prompt)),...tail]);if(res2.taskId){if(engine)taskOpDeps.rememberEngine(repo,engine);return{taskIds:[res2.taskId],forked:!0}}}catch(err){if(!(err instanceof BridgeError)||err.code!=="FORK_UNSUPPORTED")throw err;forkRefused=err.message.replace("FORK_UNSUPPORTED: ","")}let argv=[flag("repo",repo),flag("base-branch",baseBranch),...tail];if(title)argv.push(flag("title",title));if(count>1)argv.push(flag("count",count));let res=await api.verb("add",argv),taskIds=res.tasks?res.tasks.map((t)=>t.taskId):res.taskId?[res.taskId]:[];if(taskIds.length===0)throw new BridgeError("ADD_FAILED","no task was created");if(engine)taskOpDeps.rememberEngine(repo,engine);return{taskIds,...count>1&&res.groupId?{groupId:res.groupId}:{},...forkRefused?{forkRefused}:{}}}},"tab.handoff":{kind:"read",destructive:!1,wraps:"planWorktreeHandoff (transcript handoff prompt)",async run(a,{api}){let id=taskId(a),tab=tabId(a),{task,worktree}=await worktreeOf4(api,id),active=deps.readTab(id,tab)??initialTabs().tabs[0],tabVendor=(active.kind==="engine"?active.vendor:void 0)??task.vendor,plan=await deps.planHandoff(active,liveSourceProtocol(active,tabVendor),worktree,task.title);return plan.kind==="handoff"?{kind:"handoff",prompt:plan.prompt}:plan.kind==="no-transcript"?{kind:plan.kind,engine:plan.engine}:{kind:plan.kind}}},"tab.requestPR":{kind:"write",destructive:!1,wraps:"buildPRPrompt + rove api send --plain",async run(a,{api}){let id=taskId(a),{worktree}=await worktreeOf4(api,id),argv=[flag("task-id",id),"--plain",flag("prompt",await deps.prPrompt(worktree))];if(a.tabId!==void 0&&a.tabId!==null)argv.push(flag("tab",tabId(a)));return await api.verb("send",argv),{}}},"tab.send":{kind:"write",destructive:!1,wraps:"rove api send --plain [--queue] (a message typed in the page's composer, pasted and submitted by the engine's own composer; with `queue`, held for a busy tab until its turn ends)",async run(a,{api}){let argv=[flag("task-id",taskId(a)),"--plain",flag("prompt",text2(a,"prompt",PROMPT_MAX3))];if(a.tabId!==void 0&&a.tabId!==null)argv.push(flag("tab",tabId(a)));if(a.queue===!0)argv.push("--queue");let res=await api.verb("send",argv);if(res.queued===!0)return{delivered:!1,queued:!0,queueId:res.queueId,position:res.position};if(res.delivered===!0)return{delivered:!0};return{delivered:!1,reason:sendReason(res.targetDetail??res.reason)??"not confirmed"}}},"queue.list":{kind:"read",destructive:!1,wraps:"rove api queue-list --task-id (prompts held for a busy tab, oldest first)",async run(a,{api}){return await api.verb("queue-list",[flag("task-id",taskId(a))])}},"queue.edit":{kind:"write",destructive:!1,wraps:"rove api queue-edit --id --prompt (rewrite a held prompt in place)",async run(a,{api}){let id=str(a,"id");if(!/^q-\d+$/.test(id))throw new BridgeError("BAD_ARGS","id must look like q-N");return await api.verb("queue-edit",[flag("id",id),flag("prompt",str(a,"text"))])}},"queue.cancel":{kind:"write",destructive:!1,wraps:"rove api queue-cancel --id (take back a held prompt before it is pasted)",async run(a,{api}){let id=str(a,"id");if(!/^q-\d+$/.test(id))throw new BridgeError("BAD_ARGS","id must look like q-N");return await api.verb("queue-cancel",[flag("id",id)])}},"attachment.put":{kind:"write",destructive:!1,wraps:"writes ~/.rove/attachments/<generated name> (tui/lib/attachments.ts layout)",async run(a){let mime=str(a,"mime");if(!Object.hasOwn(ATTACHMENT_TYPES,mime))throw new BridgeError("BAD_ARGS",`mime must be one of ${Object.keys(ATTACHMENT_TYPES).join("|")}`);let type=ATTACHMENT_TYPES[mime],encoded=str(a,"data");if(encoded.length>Math.ceil(MAX_ATTACHMENT_BYTES*4/3)+4)throw new BridgeError("TOO_LARGE",`attachment is larger than ${MAX_ATTACHMENT_BYTES} bytes`);let bytes=Buffer.from(encoded,"base64");if(bytes.length===0)throw new BridgeError("BAD_ARGS","data is empty");if(bytes.length>MAX_ATTACHMENT_BYTES)throw new BridgeError("TOO_LARGE",`attachment is larger than ${MAX_ATTACHMENT_BYTES} bytes`);if(!matchesMagic(mime,bytes))throw new BridgeError("BAD_ARGS",`data is not a ${type.ext} file`);let stamp=deps.now().toISOString().slice(0,10).replaceAll("-",""),path=join5(deps.attachmentsDir(),`attach-${stamp}-${deps.nonce()}.${type.ext}`);return deps.writeFile(path,bytes),{path,kind:type.kind,bytes:bytes.length}}}}}var TERMINAL_OPS=terminalOps();var FETCH_TIMEOUT_MS=8000,realDeps11={async fetchText(url){let res=await fetch(url,{signal:AbortSignal.timeout(FETCH_TIMEOUT_MS)});if(!res.ok)throw Error(`HTTP ${res.status}`);return res.text()},installed:()=>loadUserThemes().map((t)=>t.name),write:(name2,theme)=>{writeUserTheme(name2,theme,!1)}};function createHostedThemeOps(deps){let cache2=new Map,load=async(name2)=>{let known=cache2.get(name2);if(known)return known;let theme=parseTheme(await deps.fetchText(hostedThemeUrl(name2)));return cache2.set(name2,theme),theme};return{"theme.hosted":{kind:"read",destructive:!1,wraps:"the rove.run theme catalogue, each theme resolved to #rrggbb for dark and light, whether it is installed, and the user themes directory",async run(){let installed=new Set(deps.installed()),settled=await Promise.allSettled(HOSTED_THEMES.map(load)),themes=[],unavailable=[];for(let[i,name2]of HOSTED_THEMES.entries()){let got=settled[i];if(got?.status!=="fulfilled"){unavailable.push(name2);continue}themes.push({name:name2,installed:installed.has(name2),dark:palette(got.value,"dark"),light:palette(got.value,"light")})}return{themes,unavailable,dir:userThemesDir()}}},"theme.install":{kind:"write",destructive:!1,wraps:"rove theme add https://rove.run/themes/<name>.json, for a catalogue name only; never overwrites",async run(args){let name2=HOSTED_THEMES.find((n)=>n===args.name);if(!name2)throw new BridgeError("BAD_ARGS",`name must be one of ${HOSTED_THEMES.join("|")}`);if(deps.installed().includes(name2))return{name:name2,installed:!1};let theme;try{theme=await load(name2)}catch(err){let reason=err instanceof Error?err.message:String(err);throw new BridgeError("UNAVAILABLE",`could not fetch ${hostedThemeUrl(name2)}: ${reason}`)}try{deps.write(name2,theme)}catch(err){if(err instanceof ThemeInstallError)throw new BridgeError("CONFLICT",err.message);throw err}return{name:name2,installed:!0}}}}}var hostedThemeOps=createHostedThemeOps(realDeps11);var DEFAULT_LIMIT=60,COMMIT_LIMIT=30,SEP="\x1F",realDeps12={base:(worktree,prBaseRef)=>resolveBase(worktree,prBaseRef),async commits(worktree,base){let res=await runWorktreeGit(worktree,["log",`${base}..HEAD`,`--format=%H${SEP}%ct${SEP}%s`,"-n",String(COMMIT_LIMIT)]);if(res.status!==0)return[];return(res.stdout??"").split(`
|
|
117
|
+
`).filter((line)=>line.length>0).flatMap((line)=>{let[sha,secs,...subject]=line.split(SEP),at=Number(secs)*1000;return sha&&Number.isFinite(at)?[{sha,at,subject:subject.join(SEP)}]:[]})}},NOISE={"tool-pre":!0,"tool-post":!0};function eventTail(event){let d=event.detail,fragment=d?.tool?.name||d?.compact?.trigger||d?.subagent?.type||d?.note||"";return[fragment.length>60?`${fragment.slice(0,59)}\u2026`:fragment,event.vendor??""].filter((part)=>part.length>0).join(" \xB7 ")}function mergeTimeline(events,commits,limit){return[...events.filter((event)=>!NOISE[event.kind]).map((event)=>({kind:event.kind,at:event.at,tail:eventTail(event),...event.tabId?{tabId:event.tabId}:{}})),...commits.map((commit)=>({kind:"commit",at:commit.at,tail:commit.subject,sha:commit.sha}))].sort((a,b)=>b.at-a.at).slice(0,limit)}function timelineOps(deps=realDeps12){return{"task.timeline":{kind:"read",destructive:!1,wraps:"rpc task.recentEvents (tool calls dropped) + git log <base>..HEAD, merged newest first",async run(a,{api}){let id=taskId(a),limit=optInt(a,"limit",1,200)??DEFAULT_LIMIT,events=await api.rpc("task.recentEvents",{taskId:id}).then((res)=>res.events).catch((err)=>{if(err instanceof Error&&/task not found/i.test(err.message))return[];throw err}),got=await api.verb("get-task",[flag("task-id",id)]),worktree=got.task.worktreePath,base=worktree?await deps.base(worktree,got.task.prStatus?.baseRef):null,commits=worktree&&base?await deps.commits(worktree,base):[];return{entries:mergeTimeline(events,commits,limit)}}}}}var TIMELINE_OPS=timelineOps();var VERSION=/^\d+\.\d+\.\d+$/,LOCALES2=["en","zh"],realDeps13={current:CURRENT_VERSION,check:(force)=>checkLatestVersion({force}),notesRange:(current,latest,locale)=>fetchReleaseNotesRange({current,latest,locale}),notes:fetchReleaseNotes};function createUpdateOps(deps=realDeps13){return{"update.check":{kind:"read",destructive:!1,wraps:"checkLatestVersion + fetchReleaseNotesRange (npm registry, GitHub releases)",async run(args){let info=await deps.check(optBool(args,"force")===!0),locale=optOneOf(args,"locale",LOCALES2)??"en",notes=info?.hasUpdate?await deps.notesRange(info.current,info.latest,locale):[];return{current:deps.current,info,notes}}},"release.notes":{kind:"read",destructive:!1,wraps:"fetchReleaseNotes (GitHub release notes for vX.Y.Z, in `locale` when translated)",async run(args){let version=text2(args,"version",32);if(!VERSION.test(version))throw new BridgeError("BAD_ARGS","version must be x.y.z");return{notes:await deps.notes(version,optOneOf(args,"locale",LOCALES2)??"en")}}}}}var updateOps=createUpdateOps();import{basename}from"path";var chip=(id)=>({id,name:engineDisplayName(id)}),welcomeDeps={cwd:()=>process.cwd(),gitOnPath:()=>Bun.which("git")!==null,async engines(){return probeableEngineIds().then(detectEngineStatuses).then(summarizeEngines).catch(()=>({usable:[],signedOut:[]}))},repoOf(dir){try{return validateRepoPath(dir)===null?resolveMainRepoRoot(dir):null}catch{return null}},branchOf:(dir)=>getCurrentBranch(dir),preferredEngine:(repo)=>resolvePreferredVendor(repo)},welcomeOps={"welcome.env":{kind:"read",destructive:!1,wraps:"helpers probeableEngineIds + detectEngineStatuses, firstTaskRepo",async run(){let[{usable,signedOut},git]=await Promise.all([welcomeDeps.engines(),welcomeDeps.gitOnPath()]),dir=welcomeDeps.cwd(),repo=welcomeDeps.repoOf(dir),start=null;if(repo!==null&&git&&usable.length>0){let pref=welcomeDeps.preferredEngine(repo);start={repo,label:basename(repo),baseRef:welcomeDeps.branchOf(dir)??welcomeDeps.branchOf(repo)??DEFAULT_BASE_REF,engine:usable.includes(pref)?pref:usable[0]??pref}}return{engines:usable.map(chip),signedOut:signedOut.map(chip),git,start}}}};var LEAF=/^leaf-\d{1,4}$/,TITLE_MAX2=80;function leafId(a){let v=a.leafId;if(v===void 0||v===null||v==="")return;if(typeof v!=="string"||!LEAF.test(v))throw new BridgeError("BAD_ARGS","leafId is not a pane id");return v}function paneTitle(a){let v=a.title;if(v===void 0||v===null)return"";if(typeof v!=="string"||v.length>TITLE_MAX2||/[\u0000-\u001f\u007f]/.test(v))throw new BridgeError("BAD_ARGS",`title must be one line of at most ${TITLE_MAX2} characters`);return v.trim()}var WORKSPACE_OPS={"tab.split":{kind:"write",destructive:!0,wraps:"rove api tab-split",async run(a,{api}){let action=oneOf(a,"action",["open","close","rename","reset"]),leaf=leafId(a);if(action!=="open"&&!leaf)throw new BridgeError("BAD_ARGS",`leafId is required to ${action}`);let cols=optInt(a,"cols",1,1000),rows=optInt(a,"rows",1,500);if(cols===void 0!==(rows===void 0))throw new BridgeError("BAD_ARGS","cols and rows go together");let argv=[flag("task-id",taskId(a)),flag("tab",tabId(a)),flag("action",action)];if(leaf)argv.push(flag("leaf",leaf));if(action==="open")argv.push(flag("direction",optOneOf(a,"direction",["right","down"])??"right"));if(action==="rename")argv.push(flag("title",paneTitle(a)));if(cols!==void 0&&rows!==void 0)argv.push(flag("cols",cols),flag("rows",rows));let res=await api.verb("tab-split",argv);return{leaf:res.leaf,changed:res.changed,split:res.split}}}};function mergeOpTables(...tables){let merged={};for(let table of tables)for(let[name2,spec2]of Object.entries(table)){if(name2 in merged)throw new BridgeError("DUPLICATE_OP",`op ${name2} is registered twice`);merged[name2]=spec2}return merged}var AREA_OPS=mergeOpTables(TASK_OPS,TERMINAL_OPS,filesOps,GATE_OPS,pagesOps,settingsOps,engineOps,insightOps,updateOps,machineOps,devOps,prefsOps,hostedThemeOps,routingOps,TASKOPS_OPS,WORKSPACE_OPS,welcomeOps,keybindingsOps,launchOps,modsOps,promptOps,snippetOps,panelOps,githubOps,reviewOps,githubAuthOps,TIMELINE_OPS,PR_CHECKS_OPS,RUN_OPS);function peerOf(req,address,deps){let surface=offersWebSubprotocol(req)?"web":"phone",origin=req.headers.get("origin"),viaTunnel=deps.access!==void 0||deps.tokenInQuery===!0;if(origin!==null&&deps.publicHost)try{viaTunnel||=new URL(origin).hostname===deps.publicHost.toLowerCase()}catch{viaTunnel=!0}let tunnelHeader=["cf-connecting-ip","cf-access-jwt-assertion","x-forwarded-for","forwarded"].some((name2)=>req.headers.has(name2)),loopback=address!==void 0&&isLoopbackHost(address.replace(/^::ffff:/i,""));return{surface,local:loopback&&!viaTunnel&&!tunnelHeader}}function viewingFields(args,peer){let flag2=(key)=>typeof args[key]==="boolean"?args[key]:void 0,[panel,panelOpen,narrow]=[flag2("panel"),flag2("panelOpen"),flag2("narrow")];return{surface:peer.surface,local:peer.local,...panel!==void 0&&peer.surface==="web"?{panel}:{},...panelOpen!==void 0?{panelOpen}:{},...narrow!==void 0?{narrow}:{}}}var PATH=/^\/hooks\/routines\/([A-Za-z0-9-]+)$/;var STATUS={fired:202,pong:200,not_found:404,bad_signature:401,disabled:409};async function routineHook(api,req,log){let match=PATH.exec(new URL(req.url).pathname);if(!match)return null;if(req.method!=="POST")return new Response(`POST only
|
|
118
|
+
`,{status:405,headers:{allow:"POST"}});if(Number(req.headers.get("content-length")??0)>1048576)return new Response(`too large
|
|
119
|
+
`,{status:413});let body=await req.text();if(body.length>1048576)return new Response(`too large
|
|
120
|
+
`,{status:413});let id=match[1],res;try{res=await api.rpc("automation.webhook",{id,body,event:req.headers.get("x-github-event"),signature:req.headers.get("x-hub-signature-256")})}catch(err){return log(`[rove-bridge] routine hook ${id}: ${err instanceof Error?err.message:String(err)}`),new Response(`daemon unavailable
|
|
121
|
+
`,{status:503})}let status=STATUS[res.verdict]??500;if(status>=400)log(`[rove-bridge] routine hook ${id}: ${res.verdict}`);return new Response(`${res.verdict}${res.event?` ${res.event}`:""}
|
|
122
|
+
`,{status})}class TermFlow{io;stallMs;streams=new Map;constructor(io,stallMs=3000){this.io=io;this.stallMs=stallMs}watch(stream){this.forget(stream),this.streams.set(stream,{unacked:0,state:"live",held:[],stall:null})}forget(stream){let s=this.streams.get(stream);if(s)this.disarm(s);this.streams.delete(stream)}data(stream,data,offset){let s=this.streams.get(stream);if(!s||offset===void 0){if(s)this.forget(stream);this.io.forward(stream,data);return}if(s.state==="resync")s.held.push({data,offset});if(s.state!=="live")return;this.send(stream,s,data)}ack(stream,chars){let s=this.streams.get(stream);if(!s)return;if(s.unacked=Math.max(0,s.unacked-chars),s.state!=="behind")return;if(s.unacked===0)this.resync(stream,s);else this.arm(stream,s)}send(stream,s,data){if(this.io.forward(stream,data),s.unacked+=data.length,s.unacked<=1e6)return;s.state="behind",this.arm(stream,s)}arm(stream,s){this.disarm(s),s.stall=setTimeout(()=>{if(s.stall=null,this.streams.get(stream)===s&&s.state==="behind")this.resync(stream,s)},this.stallMs)}disarm(s){if(s.stall!==null)clearTimeout(s.stall);s.stall=null}async resync(stream,s){this.disarm(s),s.state="resync",s.held=[];let ring=null;try{ring=await this.io.peek(stream)}catch{}if(this.streams.get(stream)!==s)return;let held=s.held;if(s.held=[],s.state="live",ring)this.io.reset(stream,ring.data),s.unacked=ring.data.length;for(let frame of held){if(s.state!=="live")break;if(!ring||frame.offset>ring.offset)this.send(stream,s,frame.data)}}}class TerminalForwarder{pty;send;attached=new Set;offs;flow;constructor(pty,send){this.pty=pty;this.send=send;this.flow=new TermFlow({forward:(stream,data)=>this.send(pushEvent("term.data",{stream,data})),reset:(stream,replay)=>this.send(pushEvent("term.reset",{stream,replay})),peek:(stream)=>this.pty.request("pty.peek",{key:stream,preamble:!0})}),this.offs=[pty.on("pty.data",(frame)=>{let{key,data,offset}=frame.payload;if(this.attached.has(key))this.flow.data(key,data,offset)}),pty.on("pty.exit",(frame)=>{let{key,code}=frame.payload;if(this.attached.has(key))this.send(pushEvent("term.exit",{stream:key,code:code??null}))})]}async attach(taskId2,tabId2,cwd,size,leafId2,acks=!1){let stream=leafId2?`${taskId2}::${tabId2}::${leafId2}`:`${taskId2}::${tabId2}`,{sessions}=await this.pty.request("pty.list").catch((err)=>{let code=err instanceof Error&&"code"in err?String(err.code):"";if(code==="ENOENT"||code==="ECONNREFUSED")return{sessions:[]};throw err}),session=sessions.find((s)=>s.key===stream);if(!session)throw new BridgeError("TAB_NOT_LIVE",`tab ${tabId2} has no hosted session`);if(!session.alive&&session.restored){let peek=await this.pty.request("pty.peek",{key:stream});return{stream,alive:!1,restored:!0,replay:peek.data}}if(this.attached.add(stream),acks)this.flow.watch(stream);try{let open=await this.pty.request("pty.open",{key:stream,cwd,...size?{cols:size.cols,rows:size.rows}:{}});return{stream,alive:open.alive,replay:open.replay}}catch(err){throw this.attached.delete(stream),this.flow.forget(stream),err}}async input(stream,data){this.requireAttached(stream),await this.pty.request("pty.write",{key:stream,data})}async resize(stream,cols,rows){this.requireAttached(stream),await this.pty.request("pty.resize",{key:stream,cols,rows})}ack(stream,chars){this.flow.ack(stream,chars)}async detach(stream){if(!this.attached.delete(stream))return;this.flow.forget(stream),await this.pty.request("pty.detach",{key:stream})}dispose(){for(let off of this.offs)off();for(let stream of this.attached)this.flow.forget(stream);this.attached.clear(),this.pty.close()}requireAttached(stream){if(!this.attached.has(stream))throw new BridgeError("NOT_ATTACHED",`not attached to ${stream}`)}}import{stat}from"fs/promises";import{extname as extname2,resolve,sep as sep2}from"path";import{gzipSync}from"zlib";var SECURITY_HEADERS={"Content-Security-Policy":"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-src https: http://localhost:* http://127.0.0.1:* http://[::1]:*; frame-ancestors 'none'; base-uri 'none'; form-action 'none'","X-Content-Type-Options":"nosniff","Referrer-Policy":"no-referrer"},COMPRESSIBLE=new Set([".html",".js",".mjs",".css",".json",".svg",".map",".txt"]),gzipped=new Map;async function gzipFile(path){let s=await stat(path),hit=gzipped.get(path);if(hit&&hit.mtimeMs===s.mtimeMs&&hit.size===s.size)return hit.body;let body=gzipSync(await Bun.file(path).bytes());return gzipped.set(path,{mtimeMs:s.mtimeMs,size:s.size,body}),body}async function serveWebFile(web,req){if(req.method!=="GET"&&req.method!=="HEAD")return null;let root=resolve(web.root),pathname;try{pathname=decodeURIComponent(new URL(req.url).pathname)}catch{return new Response(`bad path
|
|
123
|
+
`,{status:400})}let target=resolve(root,`.${pathname}`);if(target!==root&&!target.startsWith(root+sep2))return new Response(`not found
|
|
124
|
+
`,{status:404});let isFile=await stat(target).then((s)=>s.isFile(),()=>!1),path=target;if(!isFile){if(pathname.startsWith("/assets/"))return new Response(`not found
|
|
125
|
+
`,{status:404});if(path=resolve(root,"index.html"),!await Bun.file(path).exists())return new Response(`web client not built
|
|
126
|
+
`,{status:503})}let file=Bun.file(path),cache2=target.startsWith(resolve(root,"assets")+sep2)?"public, max-age=31536000, immutable":"no-cache",{"Content-Security-Policy":_,...withoutCsp}=SECURITY_HEADERS,headers={...isFile&&pathname.startsWith("/relay/")?withoutCsp:SECURITY_HEADERS,"Content-Type":file.type,"Cache-Control":cache2};if(!COMPRESSIBLE.has(extname2(path)))return new Response(req.method==="HEAD"?null:file,{headers});if(headers.Vary="Accept-Encoding",!acceptsGzip(req.headers.get("accept-encoding")??""))return new Response(req.method==="HEAD"?null:file,{headers});return headers["Content-Encoding"]="gzip",new Response(req.method==="HEAD"?null:await gzipFile(path),{headers})}function acceptsGzip(header){return header.split(",").some((part)=>{let[name2,...params]=part.trim().toLowerCase().split(";");return name2.trim()==="gzip"&&!params.some((p)=>/^\s*q=0(\.0*)?\s*$/.test(p))})}async function handle(deps,ws,req){let{ops}=deps,a=req.args,conn=ws.data,terminal=()=>{return conn.terminal??=new TerminalForwarder(deps.openPty(),(frame)=>ws.send(frame)),conn.terminal},terminalFor=async(taskId2)=>{let alias=ops.machineOf?.(taskId2);if(alias===void 0)return terminal();if(!deps.openMachinePty)throw new BridgeError("MACHINE_UNREACHABLE",`this bridge cannot reach machine ${alias}`);let open=deps.openMachinePty,pending=conn.machineTerminals.get(alias);if(!pending)pending=open(alias).then((pty)=>new TerminalForwarder(pty,(frame)=>ws.send(frame))),pending.catch(()=>conn.machineTerminals.delete(alias)),conn.machineTerminals.set(alias,pending);return pending},owner=(stream)=>conn.streamOwner.get(stream)??terminal();if(DESTRUCTIVE_CORE[req.op])logDestructive({log:deps.log,who:conn.who},req);switch(req.op){case"hello":return{protocol:BRIDGE_PROTOCOL_VERSION,apiVersion:ROVE_API_VERSION,roveVersion:deps.roveVersion,host:osHostname(),peer:conn.peer};case"api.invoke":{if(!deps.invoke)throw new BridgeError("UNKNOWN_OP","this bridge does not serve verbs");let verb=str(a,"verb"),params=a.params??{};if(typeof params!=="object"||params===null||Array.isArray(params))throw new BridgeError("BAD_ARGS","params must be an object keyed by flag name");if(Object.hasOwn(DESTRUCTIVE_VERBS,verb))logDestructiveVerb({log:deps.log,who:conn.who},verb,params);let envelope=await deps.invoke(verb,params,optStr(a,"machine"));if(envelope.ok)return envelope.result;throw new BridgeError(envelope.error.code,envelope.error.message,envelope.error)}case"tasks.subscribe":{let snapshot=await deps.feed.current();return conn.unsubscribeTasks??=deps.feed.subscribe((payload)=>ws.send(pushEvent("tasks",payload))),conn.unsubscribeTabs??=deps.tabs?.subscribe((taskIds)=>ws.send(pushEvent("tabs",{taskIds})))??null,snapshot}case"tasks.list":return deps.feed.refresh();case"engines.list":{let repo=a.repo===void 0?void 0:absPath(a,"repo");return{engines:await ops.engines(),preferred:taskOpDeps.preferredEngine(repo)}}case"repos.list":return{repos:await ops.repos()};case"task.create":return ops.createTask({repo:str(a,"repo"),engine:optStr(a,"engine"),prompt:optStr(a,"prompt"),title:optStr(a,"title")});case"task.delete":return ops.deleteTask(str(a,"taskId"),optBool(a,"force"),optBool(a,"cascade"));case"task.land":{let strategy=optStr(a,"strategy")??"merge";if(strategy!=="merge"&&strategy!=="squash")throw new BridgeError("BAD_ARGS","strategy is merge|squash");return ops.landTask(str(a,"taskId"),strategy)}case"task.tabs":return{tabs:await ops.tabs(str(a,"taskId"))};case"tab.new":{let prompt=optStr(a,"prompt"),kind=optStr(a,"kind")??"engine";if(kind!=="shell"&&kind!=="engine")throw new BridgeError("BAD_ARGS","kind is shell|engine");let engine=optStr(a,"engine"),latest=optBool(a,"latest"),replaceTab=optStr(a,"replaceTab");if(kind==="shell"&&(prompt!==void 0||engine!==void 0||latest||replaceTab!==void 0))throw new BridgeError("BAD_ARGS","a shell tab takes no prompt or engine");if(replaceTab!==void 0&&!/^tab-[A-Za-z0-9-]+$/.test(replaceTab))throw new BridgeError("BAD_ARGS","replaceTab is not a tab id");return ops.newTab(str(a,"taskId"),{kind,prompt,engine,...latest?{latest}:{},...replaceTab?{replaceTab}:{}})}case"tab.close":return await ops.closeTab(str(a,"taskId"),str(a,"tabId")),{};case"term.attach":{let taskId2=str(a,"taskId"),tabId2=str(a,"tabId"),leafId2=optStr(a,"leafId");if(leafId2!==void 0&&!/^leaf-\d{1,4}$/.test(leafId2))throw new BridgeError("BAD_ARGS","leafId is not a pane id");let cols=optInt(a,"cols",10,1000),rows=optInt(a,"rows",4,500);if(cols===void 0!==(rows===void 0))throw new BridgeError("BAD_ARGS","cols and rows go together");let remote=ops.machineOf?.(taskId2)!==void 0,shown=remote?null:ops.viewedSession(taskId2,tabId2),target=shown?.taskId??taskId2,forwarder=await terminalFor(target),cwd=shown?.cwd??await ops.worktreeOf(taskId2),shownTab=shown?.tabId??tabId2,size=cols!==void 0&&rows!==void 0?{cols,rows}:void 0,acks=optBool(a,"acks"),attach=()=>forwarder.attach(target,shownTab,cwd,size,leafId2,acks),autoRevive=!remote&&leafId2===void 0,attached=null,gone=null;try{attached=await attach()}catch(err){if(!autoRevive||!(err instanceof BridgeError&&err.code==="TAB_NOT_LIVE"))throw err;gone=err}if(autoRevive&&(gone!==null||attached?.alive===!1)){if(attached)await forwarder.detach(attached.stream);if(!await ops.reviveTab(target,shownTab).then(()=>!0,()=>!1)&&gone!==null)throw gone;attached=await attach()}if(!attached)throw gone;return conn.streamOwner.set(attached.stream,forwarder),attached}case"term.input":{let data=a.data;if(typeof data!=="string")throw new BridgeError("BAD_ARGS","data must be a string");return await owner(str(a,"stream")).input(str(a,"stream"),data),deps.rowStatus?.noteInput(str(a,"stream"),data),{}}case"term.resize":return await owner(str(a,"stream")).resize(str(a,"stream"),optInt(a,"cols",10,1000)??80,optInt(a,"rows",4,500)??24),{};case"term.ack":return conn.streamOwner.get(str(a,"stream"))?.ack(str(a,"stream"),optInt(a,"chars",0,1e9)??0),{};case"term.detach":{let stream=str(a,"stream"),forwarder=conn.streamOwner.get(stream)??conn.terminal;conn.streamOwner.delete(stream),await forwarder?.detach(stream)}return{};case"term.revive":{let taskId2=str(a,"taskId"),tabId2=str(a,"tabId"),shown=ops.machineOf?.(taskId2)!==void 0?null:ops.viewedSession(taskId2,tabId2);return ops.reviveTab(shown?.taskId??taskId2,shown?.tabId??tabId2,optBool(a,"restartExited"))}case"diff.files":return ops.diffFiles(str(a,"taskId"));case"diff.file":{let scope=str(a,"scope");if(scope!=="branch"&&scope!=="working"&&scope!=="task")throw new BridgeError("BAD_ARGS","scope is branch|working|task");return ops.diffFile(str(a,"taskId"),str(a,"path"),scope,optBool(a,"ignoreWhitespace"),optStr(a,"base"))}case"diff.refs":return ops.diffRefs(str(a,"taskId"));case"attention.dismiss":{let taskId2=str(a,"taskId"),tabId2=optStr(a,"tabId"),deliveryId=optStr(a,"deliveryId");if(optStr(a,"gateId")||!deliveryId&&dismissHitsOnlyGates((await deps.feed.current()).attention,taskId2,tabId2))throw new BridgeError("GATE_NOT_DISMISSABLE","a decision is answered with gate.answer, not dismissed");return await ops.dismissAttention(taskId2,tabId2,deliveryId),{}}case"push.key":return{publicKey:requirePush(deps).publicKey()};case"push.status":return{subscribed:requirePush(deps).subscribed(a.endpoint,conn.credential)};case"push.subscribe":return requirePush(deps).subscribe(a.subscription,conn.credential),{};case"push.unsubscribe":return{removed:requirePush(deps).unsubscribe(a.endpoint,conn.credential)};case"ui.viewing":{let taskId2=a.taskId===null?null:str(a,"taskId"),tabId2=taskId2===null?null:optStr(a,"tabId")??null;return deps.push?.setVisible(conn.viewer,taskId2!==null),deps.rowStatus?.setViewing(conn.viewer,taskId2,tabId2,viewingFields(a,conn.peer)),{}}default:{let spec2=(deps.areaOps??AREA_OPS)[req.op];if(!spec2)throw new BridgeError("UNKNOWN_OP",`unknown op: ${req.op}`);if(spec2.destructive)logDestructive({log:deps.log,who:conn.who},req);return spec2.run(a,{api:deps.api,progress:(line)=>ws.send(pushEvent("op.progress",{id:req.id,line}))})}}}function requirePush(deps){if(!deps.push)throw new BridgeError("UNAVAILABLE","this bridge sends no push notifications");return deps.push}async function onMessage(deps,ws,raw){let req,area=deps.areaOps??AREA_OPS;try{req=parseRequest(raw,(op)=>isCoreOp(op)||Object.hasOwn(area,op))}catch(err){let e=err instanceof BridgeError?err:new BridgeError("BAD_FRAME",String(err));ws.send(responseError(requestIdOf(err),e.code,e.message));return}try{ws.send(responseOk(req.id,await handle(deps,ws,req)))}catch(err){let code=err instanceof BridgeError?err.code:"INTERNAL",detail=err instanceof BridgeError?err.detail:void 0;ws.send(responseError(req.id,code,err instanceof Error?err.message:String(err),detail))}}var nextViewer=0;function startBridgeServer(deps,listen){return Bun.serve({hostname:listen.hostname,port:listen.port,async fetch(req,server){let log=deps.log??((line)=>console.error(line)),from=req.headers.get("cf-connecting-ip")??server.requestIP(req)?.address??"?";if(deps.access)try{await deps.access.verify(req.headers.get("cf-access-jwt-assertion"))}catch(err){return log(`[rove-bridge] 401 from ${from}: Cloudflare Access \u2014 ${err instanceof Error?err.message:String(err)}`),new Response(`unauthorized
|
|
127
|
+
`,{status:401})}let upgrade=req.headers.get("upgrade")?.toLowerCase()==="websocket";if(!upgrade){let hook=await routineHook(deps.api,req,log);if(hook)return hook}if(!upgrade&&deps.login){let answer2=await deps.login.handle(req,server.requestIP(req)?.address??"?");if(answer2)return answer2}if(!upgrade&&deps.web){let file=await serveWebFile(deps.web,req);if(file)return file}let origin=req.headers.get("origin");if(origin!==null&&!browserOriginAllowed(origin,req.headers.get("host"),deps.publicHost??null))return log(`[rove-bridge] 403 from ${from}: cross-origin upgrade from ${origin}`),new Response(`forbidden
|
|
128
|
+
`,{status:403});let presented=presentedToken(req,deps.tokenInQuery===!0),device=tokenMatches(deps.token,presented)?null:deps.deviceOf?.(presented)??void 0;if(device===void 0)return log(`[rove-bridge] 401 from ${from}: missing or wrong bearer token`),new Response(`unauthorized
|
|
129
|
+
`,{status:401});if(device)log(`[rove-bridge] ${new Date().toISOString()} device ${device} connected from ${from}`);let data={peer:peerOf(req,server.requestIP(req)?.address,deps),viewer:`bridge-${++nextViewer}`,who:`device=${device??"shared-token"} from=${from}`,credential:device??sharedCredential(deps.token),terminal:null,machineTerminals:new Map,streamOwner:new Map,unsubscribeTasks:null,unsubscribeTabs:null,unsubscribeNotices:null,unsubscribeMods:null,unsubscribePrompts:null,unsubscribeDispatches:null,unsubscribePanels:null},headers=offersWebSubprotocol(req)?{"Sec-WebSocket-Protocol":WEB_SUBPROTOCOL}:void 0;if(server.upgrade(req,{data,headers}))return;return new Response(`rove-bridge speaks WebSocket only
|
|
130
|
+
`,{status:426})},websocket:{open(ws){if(ws.data.unsubscribeNotices=deps.notices?.subscribe((n)=>ws.send(pushEvent("notice",n)))??null,ws.data.unsubscribePrompts=deps.prompts?.subscribe((p)=>ws.send(pushEvent("prompt",p)))??null,ws.data.unsubscribePanels=deps.panels?.subscribe((p)=>ws.send(pushEvent("panel",p)))??null,deps.mods)ws.send(pushEvent("mods",{panes:deps.mods.current()})),ws.data.unsubscribeMods=deps.mods.subscribe((panes)=>ws.send(pushEvent("mods",{panes})));if(deps.dispatches){let now=deps.dispatches.current();if(now)ws.send(pushEvent("dispatch",now));ws.data.unsubscribeDispatches=deps.dispatches.subscribe((snapshot)=>ws.send(pushEvent("dispatch",snapshot)))}},maxPayloadLength:8388608,message(ws,message){onMessage(deps,ws,typeof message==="string"?message:message.toString("utf8"))},close(ws){deps.rowStatus?.setViewing(ws.data.viewer,null,null),deps.push?.setVisible(ws.data.viewer,!1),ws.data.unsubscribeTasks?.(),ws.data.unsubscribeTabs?.(),ws.data.unsubscribeNotices?.(),ws.data.unsubscribeMods?.(),ws.data.unsubscribePrompts?.(),ws.data.unsubscribeDispatches?.(),ws.data.unsubscribePanels?.(),ws.data.terminal?.dispose();for(let pending of ws.data.machineTerminals.values())pending.then((forwarder)=>forwarder.dispose(),()=>{})}}})}class SessionTitles{open;now;client=null;titles=new Map;signature="";readAt=Number.NEGATIVE_INFINITY;inflight=null;timer=null;constructor(open,now=Date.now){this.open=open;this.now=now}async current(){if(this.now()-this.readAt<2000)return this.titles;return this.poll()}watch(active,onChange,intervalMs=2000){if(this.timer)return;this.timer=setInterval(()=>{if(active())this.tick().then((moved)=>moved&&onChange())},intervalMs)}async tick(){let before=this.signature;return await this.poll(),this.signature!==before}stop(){clearInterval(this.timer??void 0),this.timer=null,this.client?.close(),this.client=null}poll(){return this.inflight??=this.read().finally(()=>{this.inflight=null}),this.inflight}async read(){let titles=new Map;try{this.client??=this.open();let{sessions=[]}=await this.client.request("pty.list",{});for(let session of sessions){let title=session.title?.trim();if(session.alive!==!1&&typeof session.key==="string"&&title)titles.set(session.key,title)}}catch{this.client?.close(),this.client=null}return this.titles=titles,this.readAt=this.now(),this.signature=JSON.stringify([...titles]),titles}}class TabFeed{read;latest;listeners=new Set;constructor(read2){this.read=read2;this.latest=this.snapshots()}check(){let next=this.snapshots(),changed=new Set;for(let[id,json]of next)if(this.latest.get(id)!==json)changed.add(id);for(let id of this.latest.keys())if(!next.has(id))changed.add(id);if(this.latest=next,changed.size===0)return;let taskIds=[...changed];for(let listener of this.listeners)listener(taskIds)}subscribe(listener){return this.listeners.add(listener),()=>this.listeners.delete(listener)}snapshots(){let out=new Map;for(let[key,value]of Object.entries(this.read()))if(key.startsWith("terminalTabs."))out.set(key.slice(13),JSON.stringify(value));return out}}var MAX_BODY_BYTES=4096,LOOPBACK=/^(127\.|::1$|::ffff:127\.)/,json=(status,body,headers={})=>new Response(JSON.stringify(body),{status,headers:{"Content-Type":"application/json","Cache-Control":"no-store",...headers}});function loginDeviceName(devicesPath,now){let stamp=now.toISOString().replace(/[-:]/g,"").replace("T","-").slice(0,15),taken=new Set(listDevices(devicesPath).map((d)=>d.name)),base=`web-login-${stamp}`;if(!taken.has(base))return base;let n=2;while(taken.has(`${base}-${n}`))n++;return`${base}-${n}`}function createWebLogin(opts){let paths=webPasswordPaths(opts.bridgeDir),now=opts.now??Date.now,delay=opts.failureDelayMs??(()=>300+Math.floor(Math.random()*200)),queue=Promise.resolve(),serial=(fn)=>{let run=queue.then(fn,fn);return queue=run.catch(()=>{}),run},clientIp=(req,peer)=>{let forwarded=req.headers.get("cf-connecting-ip")?.trim();if(opts.trustForwardedIp&&forwarded&&LOOPBACK.test(peer)&&/^[0-9A-Fa-f:.]{2,45}$/.test(forwarded))return forwarded;return peer},sameOrigin=(req)=>{let origin=req.headers.get("origin");if(origin===null||!browserOriginAllowed(origin,req.headers.get("host"),opts.publicHost))return!1;let site=req.headers.get("sec-fetch-site");return site===null||site==="same-origin"},stamp=()=>new Date(now()).toISOString(),lockedResponse=(lock)=>lock.kind==="global"?json(429,{error:"locked; unlock on the host",locked:"global"}):json(429,{error:"locked",locked:"ip",retryAfterSeconds:Math.ceil(lock.retryAfterMs/1000)},{"Retry-After":String(Math.ceil(lock.retryAfterMs/1000))});async function login(req,peer){let ip=clientIp(req,peer);if(!sameOrigin(req))return opts.log(`[rove-bridge] ${stamp()} 403 from ${ip}: cross-origin /login from ${req.headers.get("origin")}`),json(403,{error:"forbidden"});if(!(req.headers.get("content-type")??"").toLowerCase().startsWith("application/json"))return json(415,{error:"expected application/json"});let body=await req.text();if(body.length>MAX_BODY_BYTES)return json(413,{error:"too large"});let password;try{password=JSON.parse(body)?.password}catch{password=void 0}if(typeof password!=="string"||password.length===0||password.length>MAX_PASSWORD_LENGTH)return json(400,{error:"expected {password}"});let entered=password,outcome=await serial(async()=>{let record=readPassword(paths.password);if(!record)return{kind:"gone"};let state=readLockout(paths.lockout),held=lockOf(state,ip,now());if(held)return{kind:"locked",lock:held};if(await verifyPassword(record,entered)){writeLockout(paths.lockout,recordSuccess(state,ip));let name2=loginDeviceName(opts.devicesPath,new Date(now()));return{kind:"ok",name:name2,token:addDevice(opts.devicesPath,name2,new Date(now()))}}let next=recordFailure(state,ip,now());return writeLockout(paths.lockout,next.state),{kind:"failed",lock:next.lock,count:next.state.failures.length}});switch(outcome.kind){case"gone":return json(404,{error:"not found"});case"locked":return opts.log(`[rove-bridge] ${stamp()} login refused from ${ip}: locked (${outcome.lock.kind})`),lockedResponse(outcome.lock);case"ok":return opts.log(`[rove-bridge] ${stamp()} login from ${ip}: minted device ${outcome.name}`),json(200,{token:outcome.token,device:outcome.name});case"failed":{if(opts.log(`[rove-bridge] ${stamp()} login failed from ${ip} (${outcome.count} in 24h)`),outcome.lock?.kind==="ip")opts.log(`[rove-bridge] ${stamp()} locked ${ip} for 15 minutes`);if(outcome.lock?.kind==="global")opts.log(`[rove-bridge] ${stamp()} password login locked for everyone; run \`rove --web --unlock\``);return await new Promise((resolve2)=>setTimeout(resolve2,delay())),outcome.lock?lockedResponse(outcome.lock):json(401,{error:"login failed"})}}}function logout(req,peer){if(!sameOrigin(req))return json(403,{error:"forbidden"});let device=deviceOf(opts.devicesPath,presentedToken(req));if(device&&revokeDevice(opts.devicesPath,device))opts.log(`[rove-bridge] ${stamp()} device ${device} signed out from ${clientIp(req,peer)}`);return new Response(null,{status:204,headers:{"Cache-Control":"no-store"}})}return{async handle(req,peer){let path=new URL(req.url).pathname;if(path==="/logout"&&req.method==="POST")return logout(req,peer);if(path!=="/login")return null;if(!readPassword(paths.password))return null;if(req.method==="GET")return json(200,{password:!0});if(req.method==="POST")return login(req,peer);return json(405,{error:"method not allowed"},{Allow:"GET, POST"})}}}var TICK_MS=15000,TABS_DEBOUNCE_MS=200,STATUS_RECONNECT_MS=1000,STATUS_RECONNECT_MAX_MS=30000,TAILSCALE_BINARIES=["tailscale","/Applications/Tailscale.app/Contents/MacOS/Tailscale"];function tailscaleInfo(){for(let bin of TAILSCALE_BINARIES){let ip=spawnSync(bin,["ip","-4"],{encoding:"utf8",timeout:5000}),ipv4=ip.status===0?ip.stdout.split(`
|
|
131
|
+
`)[0]?.trim()??"":"";if(!ipv4)continue;let status=spawnSync(bin,["status","--json"],{encoding:"utf8",timeout:5000}),dnsName=null;try{let self=JSON.parse(status.stdout).Self;if(self&&typeof self==="object"&&"DNSName"in self&&typeof self.DNSName==="string")dnsName=self.DNSName.replace(/\.$/,"")||null}catch{}return{ipv4,dnsName}}return{ipv4:null,dnsName:null}}function readConfig(path){if(!existsSync5(path))return{};let raw=JSON.parse(readFileSync4(path,"utf8"));if(!raw||typeof raw!=="object")throw Error(`${path} must hold a JSON object`);let fields=new Map(Object.entries(raw)),pick=(k)=>{let v=fields.get(k);return typeof v==="string"&&v.trim()?v.trim():void 0};return{cfTeam:pick("cfTeam"),cfAud:pick("cfAud"),publicHost:pick("publicHost"),relayServer:pick("relayServer")}}async function runBridge(argv,opts){let home=resolveProductHomeDir(),bridgeDir=join6(home,".rove","bridge"),args;try{args=parseBridgeArgs(argv,{config:readConfig(join6(bridgeDir,"config.json")),tailscale:tailscaleInfo})}catch(err){console.error(`${opts.name}: ${err instanceof Error?err.message:String(err)}`),process.exit(2)}Reflect.deleteProperty(process.env,"ROVE_TASK_ID"),Reflect.deleteProperty(process.env,"ROVE_TAB_ID"),ensurePluginEnginesLoaded();let devicesPath=join6(bridgeDir,"devices.json");if(args.action==="list-devices"){let devices=listDevices(devicesPath);console.log(devices.length?devices.map((d)=>`${d.name} since ${d.createdAt}`).join(`
|
|
132
|
+
`):"no device tokens");return}if(args.action==="revoke-device"){let name2=args.device??"";console.log(revokeDevice(devicesPath,name2)?`revoked ${name2}`:`no device named ${name2}`);return}if(args.action==="setup"){printSetup(args.port);return}if((args.action==="relay-shell"||args.preset==="relay")&&!opts.web)console.error(`${opts.name}: the relay is for the browser client; run it as \`rove --web\``),process.exit(2);if(args.action==="relay-shell"&&opts.web){let shell=join6(opts.web.root,"relay","index.html");if(!existsSync5(shell))console.error(`${opts.name}: ${shell} is missing; build the web client first`),process.exit(1);let dir=args.relayShellDir??".";mkdirSync4(dir,{recursive:!0}),copyFileSync(shell,join6(dir,"index.html")),console.log(`wrote ${join6(dir,"index.html")}`);return}if(args.action==="set-password"||args.action==="clear-password"||args.action==="unlock"){try{process.exitCode=await runPasswordAction(args.action,bridgeDir)}catch(err){console.error(`${opts.name}: ${err instanceof Error?err.message:String(err)}`),process.exitCode=1}return}let{sharedToken,token,revoked}=startupCredentials({tokenPath:join6(bridgeDir,"token"),devicesPath,rotate:args.rotateToken,device:args.device});if(revoked>0)console.log(`${opts.name}: --rotate-token signed out ${revoked} device token(s)`);let daemonSocket=await ensureDaemonReachable(void 0,"autospawn");await ensurePtyHostReachable();let orchestrator=new RemoteOrchestrator(new RoveDaemonClient(daemonSocket),{role:"gui",graphicsOut:()=>{}});await orchestrator.init();let hub=new MachineFeed(orchestrator),machines=hub.machinesSignal();machineStore.set(machines.get()),machines.subscribe(()=>machineStore.set(machines.get()));let verbClient=new RoveDaemonClient(daemonSocket),machineOf3=(taskId2)=>{let alias=hub.tasksSignal().get().find((row)=>String(row.id)===taskId2)?.origin?.machineId;return alias&&alias!=="local"?alias:void 0},statusClient=new RoveDaemonClient(daemonSocket),rowStatus=new RowStatusFeed(statusClient),dispatches=new DispatchFeed(statusClient),panels=new PanelFeed;statusClient.onChannel("panel.show",(panel)=>panels.push(panel));let statusRetryMs=STATUS_RECONNECT_MS,syncStatus=async()=>{try{await statusClient.subscribe({channels:["row.status","dispatch.snapshot","panel.show"],role:"pane"}),await rowStatus.resync(),await dispatches.resync(),statusRetryMs=STATUS_RECONNECT_MS}catch{statusClient.forceDisconnect(),setTimeout(()=>void syncStatus(),statusRetryMs),statusRetryMs=Math.min(statusRetryMs*2,STATUS_RECONNECT_MAX_MS)}};statusClient.onLifecycle("close",()=>{setTimeout(()=>void syncStatus(),STATUS_RECONNECT_MS)}),await syncStatus();let api=createBridgeApi(verbClient,{machineOf:machineOf3,forward:(alias,verb,params)=>forwardVerb(requireMachine(alias),verb,params)}),sessionTitles=new SessionTitles(()=>new RoveDaemonClient(defaultPtyHostSocketPath())),ops=createRoveOps(verbClient,{changes:()=>orchestrator.worktreeChangesSignal()(),tokens:()=>orchestrator.rowTokensSignal()(),gates:()=>orchestrator.gatesSignal()(),remote:()=>hub.tasksSignal().get().filter((task)=>task.origin&&task.origin.machineId!=="local").map((task)=>({...task,kind:task.kind??"task",pinned:task.pinned??!1})),remoteLive:()=>hub.liveSignal().get(),rowStatus:()=>rowStatus.current(),titleSources:async()=>{let sessions=await sessionTitles.current(),state=loadStateFile();return{sessions,tabs:(taskId2)=>state[terminalTabsKey(taskId2)]}}},api),feed=new TaskFeed(()=>ops.tasks());for(let signal of[orchestrator.tasksSignal(),rowStatus,orchestrator.attentionInboxSignal(),orchestrator.worktreeChangesSignal(),orchestrator.rowTokensSignal(),orchestrator.gatesSignal(),hub.tasksSignal(),hub.liveSignal()])signal.subscribe(()=>feed.poke());let usage=orchestrator.usageSnapshotSignal();usageStore.set(usage()),usage.subscribe(()=>usageStore.set(usage()));let context=orchestrator.contextUsageSignal();usageStore.setContext(context()),context.subscribe(()=>usageStore.setContext(context())),setInterval(()=>feed.poke(),TICK_MS),sessionTitles.watch(()=>feed.active,()=>feed.poke());let notices=new NoticeFeed,noticeStore=orchestrator.noticeStore();noticeStore.subscribe(()=>notices.push(noticeStore.get()));let prompts=new PromptFeed,promptStore=orchestrator.uiPromptStore();promptStore.subscribe(()=>prompts.push(promptStore.get()));let modPanes=orchestrator.modPanesSignal(),mods={current:()=>modPanes.get(),subscribe:(listener)=>modPanes.subscribe(()=>listener(modPanes.get()))},statePath=defaultUiPrefsStatePath(),readState=()=>{try{let raw=JSON.parse(readFileSync4(statePath,"utf8"));return raw&&typeof raw==="object"?raw:{}}catch{return{}}},tabs=new TabFeed(readState);startFileWatchTrigger({filePath:statePath,debounceMs:TABS_DEBOUNCE_MS,onTrigger:()=>{tabs.check(),feed.poke()},onError:()=>{}});let ptySocket=defaultPtyHostSocketPath(),push=opts.web?new PushHub({path:join6(bridgeDir,"push.json"),feed,crossTask:()=>PREF_DEFS.crossTask.read(readState())===!0,credentialValid:(credential)=>credential===sharedCredential(token)||listDevices(devicesPath).some((d)=>d.name===credential),log:(line)=>console.error(line)}):null,deps={token:sharedToken,ops,feed,notices,prompts,panels,mods,tabs,rowStatus,dispatches,deviceOf:(presented)=>deviceOf(devicesPath,presented),openPty:()=>new RoveDaemonClient(ptySocket),openMachinePty:async(alias)=>{let entry=requireMachine(alias);if(!await machineDaemonSocket(entry))throw new BridgeError("MACHINE_UNREACHABLE",`machine "${alias}" did not answer`);return new RoveDaemonClient(localPtySocketPath(alias))},roveVersion:CURRENT_VERSION,api,invoke:(verb,params,machine)=>invokeRouted({verb,params,machine,isLocalTask:(id)=>verbClient.request("task.get",{taskId:id}).then(()=>!0,()=>!1),runHere:()=>executeVerb(verb,{params},{client:verbClient,caller:REMOTE_CALLER})}),...args.cloudflare?{access:createAccessVerifier(args.cloudflare)}:{},...opts.web?{web:opts.web}:{},...opts.web?{login:createWebLogin({bridgeDir,devicesPath,publicHost:args.publicHost,trustForwardedIp:args.preset==="cf"||args.preset==="none"&&args.publicHost!==null,log:(line)=>console.error(line)})}:{},publicHost:args.publicHost,...push?{push}:{}},relay=args.relayServer?startRelay(deps,args.relayServer,bridgeDir,args.rotateRelay):null,servers=args.hosts.map((hostname)=>{try{return startBridgeServer(deps,{hostname,port:args.port})}catch(err){if(!(err&&typeof err==="object"&&("code"in err)&&err.code==="EADDRINUSE"))throw err;console.error(`${opts.name}: ${hostname}:${args.port} is already in use (another rove-bridge?). Stop it or pass --port <n>.`),process.exit(2)}});console.log(`${opts.name} ${CURRENT_VERSION} \u2014 Rove home ${home}`);for(let server of servers){let host=server.hostname??"127.0.0.1",port=server.port??args.port;if(console.log(`listening on ${host}:${port}${args.preset==="none"?"":` (preset ${args.preset})`}`),host==="0.0.0.0"||host==="::")console.log(" WARNING: all interfaces, plain ws:// \u2014 prefer --preset tailscale or --preset cf");let publicHost=args.publicHost??host;if(opts.web)console.log(`
|
|
133
|
+
open: ${webUrl({host:publicHost,port,token,tls:args.tls})}`);if(opts.web&&readPassword(webPasswordPaths(bridgeDir).password))console.log(`password login: on at ${webUrl({host:publicHost,port,token:"",tls:args.tls}).split("#")[0]}`);if(relay)continue;let url=pairingUrl({host:publicHost,port,token,tls:args.tls,preset:args.preset});if(console.log(`${opts.web?"":`
|
|
134
|
+
`}pair: ${url}`),args.qr&&!opts.web)console.log(renderUnicodeCompact(url))}if(relay&&args.relayServer){let link=relayLink({server:args.relayServer,code:relay.code,token});if(console.log(`
|
|
135
|
+
relay (end-to-end encrypted through ${args.relayServer}):
|
|
136
|
+
${link}`),args.qr)console.log(renderUnicodeCompact(link));console.log("`--rotate-relay` unpairs every browser paired through the relay.")}if(args.cloudflare)console.log(`
|
|
137
|
+
Cloudflare Access: team ${args.cloudflare.teamDomain}, every connection needs a valid JWT for this AUD.`),console.log(`Point the tunnel at http://127.0.0.1:${args.port} (e.g. ingress service for ${args.publicHost}).`);console.log(`
|
|
138
|
+
The ${opts.web?"links are passwords":"pairing URL is a password"}. \`--rotate-token\` revokes every paired phone${opts.web?" and browser":""}.`)}function startRelay(deps,server,bridgeDir,rotate){let code=loadOrCreateToken(join6(bridgeDir,"relay-code"),rotate),ingress=startBridgeServer({...deps,tokenInQuery:!0},{hostname:"127.0.0.1",port:0});return new RelayAgent({serverUrl:server,code,localPort:ingress.port??0,deviceName:osHostname2()}).start(),{code}}function printSetup(port){let has=(cmd,args)=>{let res=spawnSync(cmd,args,{encoding:"utf8"});return res.status===0?(res.stdout??"").trim():null},ts=has("tailscale",["status","--json"]),cf=has("cloudflared",["--version"]),lines=["Reaching this bridge from another device:",""];if(ts)lines.push(" Tailscale is up here. Private to your tailnet:",` rove --web --preset tailscale --port ${port}`," With HTTPS on your MagicDNS name (needed for a phone browser):",` tailscale serve --bg ${port} then rove --web --preset tailscale --public-host <this-machine>.<tailnet>.ts.net`);else lines.push(" Tailscale: not running here (https://tailscale.com/download), the simplest private path.");if(lines.push(""),cf)lines.push(` cloudflared is installed (${cf.split(`
|
|
139
|
+
`)[0]}). Behind Cloudflare Access:`,` cloudflared tunnel --url http://127.0.0.1:${port} (or a named tunnel to your hostname)`," rove --web --preset cf --public-host <hostname> --cf-team <team> --cf-aud <aud>");else lines.push(" cloudflared: not installed; only needed to publish through Cloudflare Access.");lines.push(""," Relay, end-to-end encrypted (no VPN, no Access; the relay sees only ciphertext). Deploy"," relay-kit's Worker to your own Cloudflare account once, with this machine's page on it:"," git clone https://github.com/nanami-0713/relay-kit && cd relay-kit/workers/relay"," rove --web --relay-shell public",` printf '\\n[assets]\\ndirectory = "public"\\n' >> wrangler.toml`," npx wrangler login && npx wrangler deploy"," rove --web --preset relay --relay-server https://<worker>.<account>.workers.dev"," *.workers.dev is blocked on many mainland China networks: bind a custom domain (wrangler.toml)."," Redo --relay-shell + deploy only when the page changes; the app itself loads from this machine."),lines.push(""," Give each phone its own token: rove --web --device <name>; revoke one with --revoke-device <name>."),console.log(lines.join(`
|
|
140
|
+
`))}export{runBridge};
|