@sma1lboy/rove 0.9.281-next.31 → 0.9.281
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-01tghgf3.js +8 -0
- package/dist/cli/{chunk-jk4f0xsh.js → chunk-03fv92ya.js} +1 -1
- package/dist/cli/{chunk-7aaw17vr.js → chunk-08mb3qss.js} +1 -1
- package/dist/cli/{chunk-y7mjhjby.js → chunk-0gbg7bg8.js} +1 -1
- package/dist/cli/{chunk-zv0rrhck.js → chunk-0nkk13z9.js} +1 -1
- package/dist/cli/{chunk-zp0q931y.js → chunk-14tevt5v.js} +1 -1
- package/dist/cli/{chunk-1g6vhgfe.js → chunk-16e6jqq6.js} +1 -1
- package/dist/cli/{chunk-3hxm56h6.js → chunk-1a57ejnh.js} +1 -1
- package/dist/cli/{chunk-4z1a65bz.js → chunk-32y7f5dr.js} +1 -1
- package/dist/cli/{chunk-j54c5a0a.js → chunk-3m0ydze9.js} +1 -1
- package/dist/cli/{chunk-12wn9jtf.js → chunk-3pgp6z3k.js} +1 -1
- package/dist/cli/{chunk-amq030gb.js → chunk-3qj9yw5c.js} +2 -2
- package/dist/cli/chunk-453qpfae.js +2 -0
- package/dist/cli/{chunk-gk8efsx0.js → chunk-46qs744w.js} +1 -1
- package/dist/cli/{chunk-am5cpb9n.js → chunk-4vkrv4eb.js} +1 -1
- package/dist/cli/{chunk-hfvzvj91.js → chunk-4yr1534j.js} +1 -1
- package/dist/cli/{chunk-byh32qnx.js → chunk-5swmvt20.js} +1 -1
- package/dist/cli/{chunk-j8d9sk5j.js → chunk-6wt08a28.js} +1 -1
- package/dist/cli/{chunk-f262ywvg.js → chunk-7911jkq6.js} +1 -1
- package/dist/cli/chunk-8ng1756g.js +3 -0
- package/dist/cli/{chunk-ebdp0z1d.js → chunk-95kk75wh.js} +1 -1
- package/dist/cli/{chunk-6mk6ceb6.js → chunk-9qpea48c.js} +2 -2
- package/dist/cli/chunk-9vwb9f7q.js +7 -0
- package/dist/cli/{chunk-nqwza8b0.js → chunk-a5xbsvx2.js} +1 -1
- package/dist/cli/{chunk-t7dyf50m.js → chunk-ap3fgbbt.js} +1 -1
- package/dist/cli/{chunk-5kgq379z.js → chunk-b5fdqynx.js} +1 -1
- package/dist/cli/{chunk-x85rcep5.js → chunk-bbq6wc47.js} +1 -1
- package/dist/cli/{chunk-np398f2a.js → chunk-bbt217yd.js} +1 -1
- package/dist/cli/{chunk-w2tcb3rn.js → chunk-bm6qhcck.js} +1 -1
- package/dist/cli/{chunk-txvjg1g9.js → chunk-btxfcc77.js} +3 -3
- package/dist/cli/chunk-c9ptxthj.js +2 -0
- package/dist/cli/{chunk-zkby63ej.js → chunk-cs410j8b.js} +1 -1
- package/dist/cli/{chunk-rvjnafyt.js → chunk-cx78tyg1.js} +1 -1
- package/dist/cli/{chunk-4drm07mp.js → chunk-cx8wnz54.js} +1 -1
- package/dist/cli/{chunk-t2p22ww7.js → chunk-ejfs22aq.js} +1 -1
- package/dist/cli/chunk-f23manyf.js +8 -0
- package/dist/cli/{chunk-7nnpp89t.js → chunk-ga82ras9.js} +1 -1
- package/dist/cli/{chunk-f7egph8g.js → chunk-gzkes5bw.js} +1 -1
- package/dist/cli/{chunk-f0sahrfw.js → chunk-hmy01yg2.js} +16 -14
- package/dist/cli/{chunk-3a7x7jg7.js → chunk-hnxs5shy.js} +16 -17
- package/dist/cli/{chunk-bw2y7670.js → chunk-j2e7jgh8.js} +1 -1
- package/dist/cli/{chunk-qc9sj5wh.js → chunk-j5y7hhc2.js} +1 -1
- package/dist/cli/{chunk-qzhfs668.js → chunk-jar50zcp.js} +1 -1
- package/dist/cli/{chunk-r98h6b42.js → chunk-k2rmj0v3.js} +2 -2
- package/dist/cli/{chunk-bv050ssb.js → chunk-k3h4cpdy.js} +1 -1
- package/dist/cli/{chunk-wz307hf6.js → chunk-k9xabf0t.js} +1 -1
- package/dist/cli/{chunk-02x0ahxk.js → chunk-kp43j420.js} +1 -1
- package/dist/cli/{chunk-6cfs0nqe.js → chunk-kt5ngemc.js} +1 -1
- package/dist/cli/chunk-nkgn0bqq.js +2 -0
- package/dist/cli/{chunk-fkgemeww.js → chunk-q2nhmaq9.js} +2 -2
- package/dist/cli/{chunk-hd1szhba.js → chunk-q6cwy6v5.js} +1 -1
- package/dist/cli/{chunk-jkgp816v.js → chunk-q72w3dsx.js} +1 -1
- package/dist/cli/{chunk-hmsvt5nj.js → chunk-rqm2kn0c.js} +1 -1
- package/dist/cli/{chunk-q6tsx6zc.js → chunk-sm96b61g.js} +1 -1
- package/dist/cli/chunk-sqdmngys.js +2 -0
- package/dist/cli/{chunk-k2j0tg1h.js → chunk-tgv90744.js} +7 -3
- package/dist/cli/{chunk-vbvbwjad.js → chunk-twy9bfps.js} +1 -1
- package/dist/cli/{chunk-5g6v6tq5.js → chunk-v3ecphrz.js} +1 -1
- package/dist/cli/{chunk-mjzjah9d.js → chunk-vtay379p.js} +1 -1
- package/dist/cli/chunk-vx44yxra.js +3 -0
- package/dist/cli/{chunk-w003rpr2.js → chunk-w3kvkax7.js} +1 -1
- package/dist/cli/{chunk-s1xn6wav.js → chunk-w6ft4djp.js} +1 -1
- package/dist/cli/{chunk-8hernv9y.js → chunk-w8dmmnys.js} +1 -1
- package/dist/cli/{chunk-9f34p80j.js → chunk-ww0zctb7.js} +1 -1
- package/dist/cli/{chunk-qwwt4ch2.js → chunk-x119xqa6.js} +1 -1
- package/dist/cli/{chunk-nrx8pf9p.js → chunk-x35dfb1x.js} +1 -1
- package/dist/cli/{chunk-ggjmcqpt.js → chunk-x94e0rw4.js} +3 -3
- package/dist/cli/chunk-xaam0d8r.js +4 -0
- package/dist/cli/{chunk-azt0c2b5.js → chunk-xekk4k54.js} +1 -1
- package/dist/cli/{chunk-mqvkd2g1.js → chunk-xqvyzv62.js} +1 -1
- package/dist/cli/chunk-xxz3np4w.js +2 -0
- package/dist/cli/{chunk-p11emdb2.js → chunk-y07cp9mb.js} +1 -1
- package/dist/cli/{chunk-5dq0n3h7.js → chunk-y2v1x23z.js} +1 -1
- package/dist/cli/chunk-yn01fet6.js +3 -0
- package/dist/cli/{chunk-cakyme4s.js → chunk-z8rvxvx5.js} +1 -1
- package/dist/cli/chunk-zr058kbe.js +3 -0
- package/dist/cli/index.js +3 -3
- package/dist/cli/rove.js +3 -3
- package/dist/skills/rove/SKILL.md +4 -2
- package/dist/skills/rove/references/api-flags.md +7 -4
- package/dist/voice/asr_server.py +318 -0
- package/dist/voice/engine.py +174 -0
- package/dist/voice/fetch_model.py +30 -0
- package/dist/voice/learn.py +184 -0
- package/dist/voice/live.py +93 -0
- package/dist/voice/phonetic.py +87 -0
- package/dist/voice/polish.py +84 -0
- package/dist/voice/polish_prompt.md +50 -0
- package/dist/voice/pyproject.toml +28 -0
- package/dist/voice/uv.lock +815 -0
- package/dist/voice/vocab.py +193 -0
- package/dist/web/assets/index-Bo92_NP5.css +1 -0
- package/dist/web/assets/index-Ct8dDnCc.js +154 -0
- package/dist/web/index.html +3 -3
- package/dist/web/relay/index.html +5 -5
- package/package.json +4 -3
- package/dist/cli/chunk-0g1h35mr.js +0 -2
- package/dist/cli/chunk-1wwqf6kx.js +0 -3
- package/dist/cli/chunk-26ydgwym.js +0 -2
- package/dist/cli/chunk-2pfh3zm0.js +0 -33
- package/dist/cli/chunk-2wrt80ft.js +0 -3
- package/dist/cli/chunk-7fya3y0n.js +0 -2
- package/dist/cli/chunk-d2wndk4q.js +0 -2
- package/dist/cli/chunk-mey3vksj.js +0 -2
- package/dist/cli/chunk-nv8eyjep.js +0 -3
- package/dist/cli/chunk-ve76p2mx.js +0 -3
- package/dist/cli/chunk-wfgrn8p2.js +0 -4
- package/dist/cli/chunk-xmjknfy2.js +0 -7
- package/dist/web/assets/index-Dc0Khk0q.js +0 -149
- package/dist/web/assets/index-T8cCSdnk.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{dedupeMachines,discoverMachine,ensureForwards,learnedFrom,listMachines,localDaemonSocketPath,localPtySocketPath,updateMachine}from"./chunk-vytrhm50.js";import{DISPATCH_PROMPT_HEAD_CHARS,normalizeRemoteUrl,resolvePtyOwner}from"./chunk-vp29jgbt.js";import{taskEngineArgv}from"./chunk-
|
|
2
|
+
import{dedupeMachines,discoverMachine,ensureForwards,learnedFrom,listMachines,localDaemonSocketPath,localPtySocketPath,updateMachine}from"./chunk-vytrhm50.js";import{DISPATCH_PROMPT_HEAD_CHARS,normalizeRemoteUrl,resolvePtyOwner}from"./chunk-vp29jgbt.js";import{taskEngineArgv}from"./chunk-1a57ejnh.js";import{roveApiInvocation}from"./chunk-rjq38twt.js";import{ApiError}from"./chunk-r5j8fycq.js";import{ptyScopeFor}from"./chunk-g9ba25q5.js";import{connectIfRunning}from"./chunk-e58kj4gm.js";import{getSavedRepos,isRemoteRepoKey}from"./chunk-rfp0ayra.js";import{errorMessage}from"./chunk-2brn8e1y.js";import{loadStateFile,updateStateFile}from"./chunk-32j6y5d1.js";import{RoveDaemonClient}from"./chunk-qzzmd63x.js";import{ROVE_API_VERSION}from"./chunk-96kvwzj8.js";import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import{__require}from"./chunk-91sykzqq.js";import{hostname}from"os";async function ptyScopeOf(daemon,task){let listed=await daemon.request("task.list",{}).catch(()=>({tasks:[]})),scope=ptyScopeFor(listed.tasks,task.id);return task.ptyAlias?{...scope,alias:task.ptyAlias}:scope}async function recordSend(daemon,sent){let self=await ownSession(daemon).catch(()=>null);await daemon.request("dispatch.record",{taskId:sent.taskId,...sent.tabId?{tabId:sent.tabId}:{},promptHead:sent.prompt.slice(0,DISPATCH_PROMPT_HEAD_CHARS),delivered:sent.delivered,...sent.deliveryId?{deliveryId:sent.deliveryId}:{},...self?{fromTaskId:self.taskId,fromTabId:self.tabId}:{}}).catch(()=>{})}var selfSessionOnce,identityWarning=null;async function realProbe(){let[{openPtyHost,listSessions},{psSnapshot}]=await Promise.all([import("./chunk-nkgn0bqq.js"),import("./chunk-sakkdjyn.js")]);return{sessions:async()=>{let host=await openPtyHost();if(!host)return[];try{return await listSessions(host.rpc)}finally{host.close()}},ps:psSnapshot,pid:process.pid}}async function verifiedSelfSession(env=process.env,probe){if(!probe&&selfSessionOnce)return selfSessionOnce;let run=resolveSelfSession(env,probe);return selfSessionOnce=run,run}async function resolveSelfSession(env,probe){let taskId=env.ROVE_TASK_ID;if(!taskId)return null;let tabId=env.ROVE_TAB_ID||"tab-1";try{let p=probe??await realProbe(),key=`${taskId}::${tabId}`,session=(await p.sessions()).find((s)=>s.key===key&&s.alive);if(session?.pid){let{hasAncestor,parsePsSnapshot}=await import("./chunk-sakkdjyn.js");if(hasAncestor(parsePsSnapshot(await p.ps([session.pid,p.pid])),p.pid,session.pid))return identityWarning=null,{taskId,tabId}}}catch{}return identityWarning=`$ROVE_TASK_ID/$ROVE_TASK_ID names task ${taskId} ${tabId}, but this process is not running inside that tab (an inherited env, not an identity) \u2014 dispatcher/peer provenance omitted`,null}function takeIdentityWarning(){let warning=identityWarning;return identityWarning=null,warning}async function ownSession(daemon,env=process.env,probe){let self=await verifiedSelfSession(env,probe);if(!self)return null;let listed=await daemon.request("task.list",{}).catch(()=>({tasks:[]}));return{...self,taskId:resolvePtyOwner(listed.tasks,self.taskId,self.tabId)}}async function dispatcherEnvPayload(daemon,env=process.env,probe){let self=await ownSession(daemon,env,probe);if(!self)return{};return{dispatcherTaskId:self.taskId,dispatcherTabId:self.tabId}}async function readOwnDispatcher(daemon){let self=await ownSession(daemon);if(!self)return null;try{return(await daemon.request("task.get",{taskId:self.taskId})).task.dispatcher??null}catch{return null}}async function resolveDispatcherTab(runtime,daemon,dispatcher){let task=await daemon.request("task.get",{taskId:dispatcher.taskId}).then((res)=>res.task).catch(()=>null),{tabs,running}=await runtime.taskTabs(dispatcher.taskId,void 0,task?await ptyScopeOf(daemon,task):void 0);if(tabs.some((t)=>t.id===dispatcher.tabId&&t.alive))return dispatcher.tabId;if(running)return;throw new ApiError(`dispatcher tab ${dispatcher.tabId} on task ${dispatcher.taskId} is dead and the task has no live engine tab \u2014 the reply has nowhere to land`,"DISPATCHER_UNREACHABLE",{dispatcher,hint:`address an alive target explicitly with --task-id/--tab (see \`${ROVE_PRODUCT_NAME} api pty-list\`), or notify the user with \`${ROVE_PRODUCT_NAME} api notify\``,nextCommandArgs:["api","pty-list"]})}async function replyTabOf(runtime,daemon,taskId){let self=await ownSession(daemon);if(self?.taskId===taskId)return self.tabId;let task=await daemon.request("task.get",{taskId}).then((res)=>res.task).catch(()=>null);if(!task)return;let{tabs}=await runtime.taskTabs(taskId,taskEngineArgv(task),await ptyScopeOf(daemon,task));return tabs.find((t)=>t.kind==="engine"&&t.alive===!0&&t.engineAlive!==!1)?.id}async function withPeerProvenance(daemon,targetTaskId,prompt){let self=await ownSession(daemon),senderId=self?.taskId;if(!self||!senderId||senderId===targetTaskId)return prompt;let label=senderId;try{let res=await daemon.request("task.get",{taskId:senderId});label=res.task.title||res.task.branch||senderId}catch{}let api=roveApiInvocation(),replyTarget=`--task-id ${senderId} --tab ${self.tabId}`;return`[ROVE PEER] from "${label}" (task ${senderId} \u2014 Rove agent skill /rove, read it once per session; reply only if it changes what I do next: \`${api} send ${replyTarget} --prompt "<text>"\`; verb reference: \`${api} schema\`)
|
|
3
3
|
|
|
4
4
|
${prompt}`}import{spawnSync}from"child_process";import{existsSync}from"fs";import{join}from"path";function git(cwd,args){let res=spawnSync("git",["-C",cwd,...args],{encoding:"utf8",timeout:5000});return res.status===0?res.stdout.trim():null}function repoIdentity(path){if(!existsSync(path)||git(path,["rev-parse","--git-dir"])===null)return null;let origin=git(path,["remote","get-url","origin"]),root=git(path,["rev-list","--max-parents=0","HEAD"])?.split(`
|
|
5
5
|
`).filter(Boolean).sort()[0];return{...origin?{origin:normalizeRemoteUrl(origin)}:{},...root?{root}:{}}}function isLocalCheckout(path){return existsSync(join(path,".git"))||existsSync(path)&&git(path,["rev-parse","--git-dir"])!==null}function unpushedCommits(path,ref){if(git(path,["rev-parse","--verify","--quiet",`${ref}^{commit}`])===null)return null;let ahead=git(path,["rev-list","--count",`${ref}@{upstream}..${ref}`]);if(ahead!==null)return Number.parseInt(ahead,10)||0;let lone=git(path,["rev-list","--count",ref,"--not","--remotes"]);return lone===null?null:Number.parseInt(lone,10)||0}import{spawnSync as spawnSync2}from"child_process";import{existsSync as existsSync2,mkdirSync}from"fs";import{homedir}from"os";import{dirname,isAbsolute,join as join2,resolve}from"path";var URL_LIKE=/^[a-z][a-z0-9+.-]*:\/\//i,SCP_LIKE=/^[^@/\s]+@[^:/\s]+:[^\s]+$/,SEGMENT=/^[A-Za-z0-9_.-]+$/;function parseRepoRef(raw,cwd=process.cwd(),exists=existsSync2){let value=raw.trim(),asPath={kind:"path",path:value};if(value===""||isRemoteRepoKey(value))return asPath;if(URL_LIKE.test(value)||SCP_LIKE.test(value))return{kind:"id",id:normalizeRemoteUrl(value),cloneUrl:value};if(isAbsolute(value)||/^[A-Za-z]:[\\/]/.test(value)||/^[~.\\]/.test(value))return asPath;if(exists(resolve(cwd,value)))return asPath;let parts=value.replace(/\.git$/i,"").split("/");if(!parts.every((p)=>SEGMENT.test(p)))return asPath;if(parts.length===2){let id=`github.com/${parts.join("/")}`.toLowerCase();return{kind:"id",id,cloneUrl:`https://${id}.git`}}if(parts.length>=3&&parts[0]?.includes(".")){let id=parts.join("/").toLowerCase();return{kind:"id",id,cloneUrl:`https://${id}.git`}}return asPath}function repoIdOf(identity){if(!identity)return null;if(identity.origin)return identity.origin;return identity.root?`root:${identity.root}`:null}function findCheckouts(want,candidates){let out=[];for(let c of candidates){let sameRoot=Boolean(want.root&&c.identity.root===want.root),sameOrigin=Boolean(want.origin&&c.identity.origin===want.origin);if(!sameRoot&&!sameOrigin)continue;let fork=sameRoot&&!sameOrigin&&Boolean(want.origin&&c.identity.origin);out.push({path:c.path,via:sameRoot?"root":"origin",fork})}return out}function pickCheckout(matches,opts){let usable=matches.filter((m)=>opts.allowFork||!m.fork);if(usable.length===0){let fork=matches.find((m)=>m.fork)?.path;return fork?{path:null,fork}:{path:null}}let primary=usable.find((m)=>m.path===opts.primary);if(primary)return{path:primary.path};if(usable.length>1&&opts.isClean){let clean=usable.find((m)=>opts.isClean?.(m.path));if(clean)return{path:clean.path}}return{path:usable[0]?.path??null}}function isCleanCheckout(path){let res=spawnSync2("git",["-C",path,"status","--porcelain","--untracked-files=no"],{encoding:"utf8",timeout:5000});return res.status===0&&res.stdout.trim()===""}var REPO_PRIMARY_KEY="repoPrimary";function primaryCheckouts(state=loadStateFile()){let raw=state[REPO_PRIMARY_KEY],out={};if(raw&&typeof raw==="object"){for(let[id,path]of Object.entries(raw))if(typeof path==="string")out[id]=path}return out}function setPrimaryCheckout(id,path){updateStateFile((state)=>{let next=primaryCheckouts(state);if(path===null)Reflect.deleteProperty(next,id);else next[id]=path;return state[REPO_PRIMARY_KEY]=next,!0})}function cloneDirFor(id,home=homedir()){return join2(home,".rove","repos",...id.split("/"))}function cloneRepo(ref,home){let dir=cloneDirFor(ref.id,home);if(repoIdentity(dir))return dir;mkdirSync(dirname(dir),{recursive:!0});let res=spawnSync2("git",["clone",ref.cloneUrl,dir],{encoding:"utf8",timeout:600000,env:{...process.env,GIT_TERMINAL_PROMPT:"0"}});if(res.status!==0)throw Error((res.stderr||res.stdout||"git clone failed").trim());return dir}var LOCAL_MACHINE="local";async function machineDaemonSocket(entry){let socketPath=localDaemonSocketPath(entry.alias);if(await answers(socketPath))return socketPath;if(!entry.sockets||!await forward(entry,entry.sockets))return null;if(await answers(socketPath))return socketPath;let found=await discoverMachine(entry.alias,entry);if(!found.ok)return null;let learned=learnedFrom(found);return updateMachine(entry.alias,learned),await forward({...entry,...learned},learned.sockets)&&await answers(socketPath)?socketPath:null}function forward(entry,sockets){return ensureForwards({alias:entry.alias,config:{...entry,sockets},remoteDaemonSocket:sockets.daemon,remotePtySocket:sockets.pty})}async function answers(socketPath){let client=new RoveDaemonClient(socketPath);try{return await client.request("hello",{}),!0}catch{return!1}finally{client.close()}}async function machineTasks(entry){let socketPath=await machineDaemonSocket(entry);if(!socketPath)return null;let client=new RoveDaemonClient(socketPath);try{return(await client.request("task.list")).tasks??[]}catch{return null}finally{client.close()}}function requireMachine(alias){let entry=listMachines().find((m)=>m.alias===alias);if(entry)return entry;throw new ApiError(`no machine named "${alias}"`,"UNKNOWN_MACHINE",{hint:"list the registered machines and retry with one of their aliases, or register it first",nextCommandArgs:["machine","list"]})}var AUTO_MACHINE="auto",busy=(tasks)=>tasks.filter((task)=>task.status==="in_progress").length;async function pickMachine(localTasks,readTasks=machineTasks,machines=dedupeMachines(listMachines()),hosts){let[local,...remote]=await Promise.all([localTasks().catch(()=>[]),...machines.map((entry)=>readTasks(entry))]),loads={[LOCAL_MACHINE]:busy(local)};machines.forEach((entry,i)=>{let tasks=remote[i];if(tasks)loads[entry.alias]=busy(tasks)});let pool=[null,...machines.filter((entry)=>loads[entry.alias]!==void 0)],skipped;if(hosts){let ok=await Promise.all(pool.map((entry)=>hosts(entry).catch(()=>!1)));if(ok.some(Boolean))skipped=pool.filter((_,i)=>!ok[i]).map((entry)=>entry?.alias??LOCAL_MACHINE),pool=pool.filter((_,i)=>ok[i])}let best=null,bestLoad=Number.POSITIVE_INFINITY;for(let entry of pool){let load=loads[entry?.alias??LOCAL_MACHINE]??0;if(load<bestLoad)best=entry,bestLoad=load}return{entry:best,loads,...skipped?{skipped}:{}}}function repoWanted(repo){if(typeof repo!=="string")return null;let ref=parseRepoRef(repo);if(ref.kind==="id")return{origin:ref.id};return isLocalCheckout(ref.path)?repoIdentity(ref.path):null}function repoHosts(params){if(flagOn(params.clone))return;let want=repoWanted(params.repo);if(!want||!want.origin&&!want.root)return;return async(entry)=>{if(entry===null){if(typeof params.repo==="string"&&parseRepoRef(params.repo).kind==="path")return!0;return getSavedRepos().some((path)=>findCheckouts(want,[{path,identity:repoIdentity(path)??{}}]).length)}let socket=await machineDaemonSocket(entry);if(!socket)return!1;let client=new RoveDaemonClient(socket);try{let res=await client.request("api.invoke",{verb:"repo-match",params:{...want.origin?{origin:want.origin}:{},...want.root?{root:want.root}:{}},forwarded:!0,from:hostname()});return res.ok&&Boolean(res.result.repo)}finally{client.close()}}}async function routeTarget(args){if(args.machine===AUTO_MACHINE){let hosts=args.params?repoHosts(args.params):void 0;return(await pickMachine(args.localTasks??(async()=>[]),machineTasks,void 0,hosts)).entry}if(args.machine!==void 0&&args.machine!==LOCAL_MACHINE)return requireMachine(args.machine);if(args.machine===LOCAL_MACHINE||!args.taskId)return null;let machines=dedupeMachines(listMachines());if(machines.length===0||await args.isLocalTask(args.taskId))return null;for(let entry of machines)if((await machineTasks(entry))?.some((task)=>task.id===args.taskId))return entry;return null}async function forwardVerb(entry,verb,params,socketPath){let target=socketPath??await machineDaemonSocket(entry);if(!target)return offline(entry.alias);let client=new RoveDaemonClient(target);try{let repo=await translateRepo(client,entry.alias,params);if(repo.failure)return repo.failure;if(verb==="land"&¶ms["dry-run"]===void 0){let published=await client.request("api.invoke",{verb:"publish",params:{"task-id":params["task-id"]},forwarded:!0,from:hostname()});if(published.ok||published.error.code!=="UNKNOWN_VERB")return published}let envelope=await client.request("api.invoke",{verb,params:repo.params,forwarded:!0,from:hostname()});return await recordHere(verb,params,envelope),envelope}catch(err){let message=errorMessage(err);if(/unknown daemon request:/i.test(message))return tooOld(entry.alias);return failure("MACHINE_UNREACHABLE",`machine "${entry.alias}": ${message}`,{hint:"check the machine is up and reachable over ssh, then retry",nextCommandArgs:["machine","list"]})}finally{client.close()}}async function recordHere(verb,params,envelope){if(verb!=="send"&&verb!=="dispatch"||!envelope.ok||typeof params.prompt!=="string")return;let sent=envelope.result,taskId=typeof sent.childTaskId==="string"?sent.childTaskId:sent.taskId;if(typeof taskId!=="string")return;let tabId=typeof sent.tabId==="string"?sent.tabId:typeof sent.session==="string"?sent.session.split("::")[1]:void 0,local=await connectIfRunning().catch(()=>null);if(!local)return;try{await recordSend(local,{taskId,...tabId?{tabId}:{},prompt:params.prompt,delivered:sent.delivered===!0})}finally{local.close()}}async function translateRepo(client,alias,params){let repo=params.repo;if(typeof repo!=="string")return{params};let ref=parseRepoRef(repo),local=ref.kind==="path"&&isLocalCheckout(ref.path);if(ref.kind==="path"&&!local)return{params};let identity=ref.kind==="id"?{origin:ref.id}:repoIdentity(repo);if(!identity?.origin&&!identity?.root)return{params};let allowFork=flagOn(params["allow-fork"]),ask=await client.request("api.invoke",{verb:"repo-match",params:{...identity.origin?{origin:identity.origin}:{},...identity.root?{root:identity.root}:{},...allowFork?{"allow-fork":!0}:{}},forwarded:!0,from:hostname()});if(!ask.ok)return ask.error.code==="UNKNOWN_VERB"?{params}:{failure:ask};let answer=ask.result,found=answer.repo;if(found){let base=params["base-branch"],unpushed=local&&typeof base==="string"?unpushedCommits(repo,base):null;if(typeof base==="string"&&unpushed)return{failure:failure("UNPUSHED_BASE",`${base} has ${unpushed} commit${unpushed===1?"":"s"} no remote has; ${alias} cannot base a task on it`,{hint:`push it first (\`git -C ${repo} push -u origin ${base}\`), then dispatch again`})};return{params:{...params,repo:found}}}if(answer.fork)return{failure:failure("FORK_MATCH",`machine "${alias}" only has ${answer.fork}, a fork of ${identity.origin??repo} (same history, other origin)`,{hint:"pass --allow-fork to use it, or --clone with the repo id to fetch the repo itself there"})};if(flagOn(params.clone)&&identity.origin)return{params:{...params,repo:ref.kind==="id"?repo:identity.origin}};return{failure:failure("NO_REMOTE_CHECKOUT",`machine "${alias}" has no checkout of ${identity.origin??repo}`,{hint:identity.origin?`pass --clone to clone it on ${alias}, or --repo with ${alias}'s own path`:`clone it on ${alias} (and open it there once so Rove knows it), or pass --repo with ${alias}'s own path`})}}var flagOn=(value)=>value===!0||value==="true";function offline(alias){return failure("MACHINE_UNREACHABLE",`machine "${alias}" did not answer`,{hint:`check \`ssh ${alias} true\` works without a prompt, then retry`,nextCommandArgs:["machine","list"]})}function tooOld(alias){return failure("MACHINE_VERSION_SKEW",`machine "${alias}" runs a Rove too old to accept forwarded verbs`,{hint:`upgrade Rove on ${alias} (\`rove update\` there), then retry`})}function failure(code,message,data){return{apiVersion:ROVE_API_VERSION,ok:!1,error:{message,code,...data}}}async function invokeRouted(args){if(args.forwarded)return args.runHere();let taskId=args.params["task-id"],target;try{target=await routeTarget({machine:args.machine,taskId:typeof taskId==="string"?taskId:void 0,isLocalTask:args.isLocalTask,...args.localTasks?{localTasks:args.localTasks}:{},params:args.params})}catch(err){if(!(err instanceof ApiError))throw err;return failure(err.code,err.message,err.data??{})}return target?forwardVerb(target,args.verb,args.params):args.runHere()}async function machinePtySocket(alias){let entry=requireMachine(alias);if(!await machineDaemonSocket(entry))throw new ApiError(`machine "${alias}" did not answer`,"MACHINE_UNREACHABLE");return localPtySocketPath(alias)}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{publishRoveTerminalTitle}from"./chunk-3aqhytyy.js";import{ensureGlobalRoveHooks}from"./chunk-dkx7s5rk.js";import{LAST_RUN_VERSION_KEY,enforceResetGate}from"./chunk-
|
|
2
|
+
import{publishRoveTerminalTitle}from"./chunk-3aqhytyy.js";import{ensureGlobalRoveHooks}from"./chunk-dkx7s5rk.js";import{LAST_RUN_VERSION_KEY,enforceResetGate}from"./chunk-5swmvt20.js";import"./chunk-17e2tgyg.js";import"./chunk-2bgmby1d.js";import"./chunk-n0rdn2fn.js";import"./chunk-j1z9kgyd.js";import"./chunk-e58kj4gm.js";import"./chunk-0a65j1zj.js";import"./chunk-bcyex84w.js";import"./chunk-4gjvmfa9.js";import"./chunk-2tfnscxk.js";import"./chunk-hzz020xx.js";import"./chunk-w7zv47mp.js";import"./chunk-rfp0ayra.js";import"./chunk-40dgv828.js";import{CURRENT_VERSION,compareSemver}from"./chunk-yn01fet6.js";import"./chunk-zsfyj36g.js";import"./chunk-2brn8e1y.js";import"./chunk-7mwvvj46.js";import{detectShell}from"./chunk-bbt217yd.js";import"./chunk-eege1x0a.js";import"./chunk-qqr4tfhk.js";import{loadStateFile,patchStateFile}from"./chunk-32j6y5d1.js";import"./chunk-a5xbsvx2.js";import"./chunk-9vwb9f7q.js";import"./chunk-cmp64q4j.js";import"./chunk-qzzmd63x.js";import"./chunk-96kvwzj8.js";import"./chunk-7bakcyes.js";import"./chunk-8qkyd589.js";import"./chunk-7kysxgbq.js";import"./chunk-hz1x3hak.js";import"./chunk-x1jxxmtc.js";import"./chunk-xy8be792.js";import"./chunk-h2m7em1e.js";import"./chunk-7ghwdjdc.js";import"./chunk-8t7qbr1e.js";import"./chunk-bejhca6z.js";import"./chunk-br8r72bf.js";import"./chunk-yt15y085.js";import"./chunk-z1nj22va.js";import"./chunk-zt7wfnva.js";import"./chunk-0g4wc808.js";import"./chunk-pn5ygqn6.js";import"./chunk-eqdbfgq5.js";import"./chunk-f8gg8cqh.js";import"./chunk-nc46gddg.js";import{__require}from"./chunk-91sykzqq.js";var WELCOMED_KEY="welcomed";function shouldWelcome(state){if(state[WELCOMED_KEY]===!0)return!1;return typeof state[LAST_RUN_VERSION_KEY]!=="string"}function takeWelcome(){if(!shouldWelcome(loadStateFile()))return null;try{patchStateFile({[WELCOMED_KEY]:!0})}catch{}return{shell:detectShell()}}var WHATS_NEW_SEEN_KEY="app.whatsNewSeenVersion";function whatsNewFromVersion(state,current=CURRENT_VERSION){let stamped=state[WHATS_NEW_SEEN_KEY]??state[LAST_RUN_VERSION_KEY];if(typeof stamped!=="string"||stamped.length===0)return null;return compareSemver(current,stamped)>0?stamped:null}function takeWhatsNew(){let from=whatsNewFromVersion(loadStateFile());try{patchStateFile({[WHATS_NEW_SEEN_KEY]:CURRENT_VERSION})}catch{}return from}async function startTui(){let whatsNewFrom=takeWhatsNew(),welcome=takeWelcome();enforceResetGate(),publishRoveTerminalTitle(),await ensureGlobalRoveHooks();let{loadPluginEngines}=await import("./chunk-6mg5zp8y.js");loadPluginEngines();let{startSkillStatus}=await import("./chunk-rqm2kn0c.js");startSkillStatus();let{startWorkspaceHost}=await import("./chunk-hnxs5shy.js");await startWorkspaceHost({whatsNewFrom,welcome})}export{startTui};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{isDev}from"./chunk-z1nj22va.js";import{fileURLToPath}from"url";var package_default={$schema:"https://json.schemastore.org/package.json",name:"@sma1lboy/rove",version:"0.9.281",description:"Rove \u2014 the agent multiplexer for your terminal. Run coding agents on parallel tasks with isolated worktrees and persistent sessions.",keywords:["terminal","tui","cli","multiplexer","ai-agents","coding-agent","ai-coding-assistant","agentic-ai","parallel-agents","git-worktree","claude-code","codex","llm","developer-tools"],type:"module",packageManager:"bun@1.3.13",bin:{rove:"dist/cli/rove.js"},files:["dist/cli","dist/completions","dist/web","dist/web-ui","dist/skills","dist/voice","dist/*.wav","README.md","LICENSE"],publishConfig:{access:"public"},repository:{type:"git",url:"git+https://github.com/Sma1lboy/rove.git"},homepage:"https://rove.run",bugs:{url:"https://github.com/Sma1lboy/rove/issues"},engines:{bun:">=1.3.11"},scripts:{dev:"ROVE_DEV=1 bun --conditions=browser ./src/cli/rove.ts","dev:mock":"ROVE_DEV=1 bun ./src/tui-react/mock/host.tsx","dev:sandbox":"bun run scripts/dev-sandbox.ts run","dev:sandbox:reset":"bun run scripts/dev-sandbox.ts reset",build:"bun run scripts/build.ts","build:rubric":"bun run scripts/build-tier-rubric.ts",compile:"bun run scripts/compile.ts",typecheck:"tsc --noEmit","check-i18n":"bun run scripts/check-i18n.ts",test:"bun run test:fast && bun run test:socket","perf:golden":"bun scripts/perf-golden.ts","pty:soak":"bun scripts/pty-soak.ts","test:fast":"vitest run --passWithNoTests --minWorkers=1 --maxWorkers=8","test:socket":"ROVE_INCLUDE_SOCKET=1 vitest run test/daemon --pool forks --minWorkers=1 --maxWorkers=4 --passWithNoTests","test:socket:coverage":"ROVE_INCLUDE_SOCKET=1 ROVE_COVERAGE_DAEMON=1 vitest run test/daemon --coverage --pool forks --minWorkers=1 --maxWorkers=4 --passWithNoTests","test:behavior":"ROVE_INCLUDE_BEHAVIOR=1 vitest run test/behavior --pool forks --minWorkers=1 --maxWorkers=1 --retry=2 --passWithNoTests","test:render":"bun scripts/render-track.mjs",coverage:"vitest run --coverage --passWithNoTests",bench:"vitest bench --run",lint:"biome check .",knip:"knip-bun",postinstall:"bun run scripts/check-preview-deps.ts || true",prepublishOnly:"bun run typecheck && bun run build","plugin-sandbox":"bun run scripts/plugin-sandbox.ts"},"//":"biome.json + bunfig.toml live at the monorepo root since they apply repo-wide; bun.lock also lives at root (workspace-shared).",dependencies:{"@ansi-tools/parser":"^1.0.15","@opentui/core":"0.4.3","@opentui/react":"0.4.3","@xterm/addon-serialize":"^0.14.0","@xterm/addon-unicode11":"^0.9.0","@xterm/headless":"^6.0.0",react:"^19.2.8","smol-toml":"^1.7.1",ws:"^8.21.3"},optionalDependencies:{"node-pty":"^1.1.0"},devDependencies:{"@biomejs/biome":"1.9.4","@sma1lboy/rove-daemon":"0.7.23","@tsconfig/bun":"1.0.11","@types/bun":"1.3.14","@types/node":"25.6.2","@types/react":"^19.2.0","@vitest/coverage-v8":"2.1.9",knip:"^6.14.2","react-devtools-core":"^7.0.1","rove-bridge":"0.0.42","rove-weixin":"0.0.42",typescript:"5.8.2",vitest:"2.1.9"},trustedDependencies:[]};var CURRENT_VERSION=package_default.version,PACKAGE_NAME=package_default.name;function repoSlug(){let url=package_default.repository?.url;if(!url)return null;let m=url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);if(!m||!m[1]||!m[2])return null;return`${m[1]}/${m[2]}`}var UPDATE_SCRIPT_URL="https://raw.githubusercontent.com/Sma1lboy/rove/main/scripts/update.sh",UPDATE_COMMAND=`curl -fsSL ${UPDATE_SCRIPT_URL} | sh`;function updateCommandFor(target){return target===void 0||target===DEFAULT_RELEASE_CHANNEL?UPDATE_COMMAND:`${UPDATE_COMMAND} -s -- ${target}`}function owningNpmPrefix(modulePath=fileURLToPath(import.meta.url)){let at=modulePath.indexOf("/lib/node_modules/");if(at<=0)return null;return modulePath.slice(0,at)}function recommendedGlobalInstallCommand(prefix=owningNpmPrefix()){let target=`${PACKAGE_NAME}@latest`;return prefix===null?`npm install -g ${target}`:`npm install -g --prefix ${prefix} ${target}`}var BREAKING_VERSIONS=[];function breakingVersionsCrossed(from,to,breaking=BREAKING_VERSIONS){let[lo,hi]=compareSemver(from,to)<=0?[from,to]:[to,from];return breaking.filter((b)=>compareSemver(b,lo)>0&&compareSemver(b,hi)<=0)}var FETCH_TIMEOUT_MS=3000,PACKUMENT_TIMEOUT_MS=15000,RELEASE_CHANNELS=["latest","next","nightly"],DEFAULT_RELEASE_CHANNEL="latest";function channelOf(version=CURRENT_VERSION){let tag=prereleaseOf(version)?.split(".")[0];return tag==="nightly"||tag==="next"?tag:DEFAULT_RELEASE_CHANNEL}async function fetchLatestFromRegistry(packageName,channel){let ctrl=new AbortController,timer=setTimeout(()=>ctrl.abort(),FETCH_TIMEOUT_MS);try{let encoded=packageName.replace("/","%2F"),res=await fetch(`https://registry.npmjs.org/${encoded}/${channel}`,{signal:ctrl.signal,headers:{accept:"application/json"}});if(!res.ok)return null;let body=await res.json();if(typeof body.version!=="string")return null;return body.version}catch{return null}finally{clearTimeout(timer)}}async function fetchPublishedAt(packageName,version){let ctrl=new AbortController,timer=setTimeout(()=>ctrl.abort(),PACKUMENT_TIMEOUT_MS);try{let encoded=packageName.replace("/","%2F"),res=await fetch(`https://registry.npmjs.org/${encoded}`,{signal:ctrl.signal,headers:{accept:"application/json"}});if(!res.ok)return null;let stamp=(await res.json()).time?.[version],ms=typeof stamp==="string"?Date.parse(stamp):Number.NaN;return Number.isFinite(ms)?ms:null}catch{return null}finally{clearTimeout(timer)}}function isNewerSemver(latest,current){let core=compareSemver(latest,current);if(core!==0)return core>0;return comparePrerelease(prereleaseOf(latest),prereleaseOf(current))>0}function prereleaseOf(version){let dash=version.indexOf("-");return dash===-1?void 0:version.slice(dash+1)||void 0}function comparePrerelease(a,b){if(a===b)return 0;if(a===void 0)return 1;if(b===void 0)return-1;let aParts=a.split("."),bParts=b.split(".");for(let i=0;i<Math.max(aParts.length,bParts.length);i++){let av=aParts[i],bv=bParts[i];if(av===void 0)return-1;if(bv===void 0)return 1;if(av===bv)continue;let an=/^\d+$/.test(av)?Number.parseInt(av,10):null,bn=/^\d+$/.test(bv)?Number.parseInt(bv,10):null;if(an!==null&&bn!==null)return an>bn?1:-1;if(an!==null)return-1;if(bn!==null)return 1;return av>bv?1:-1}return 0}function compareSemver(aVersion,bVersion){let norm=(v)=>v.split("-")[0]??v,a=norm(aVersion).split(".").map((s)=>Number.parseInt(s,10)),b=norm(bVersion).split(".").map((s)=>Number.parseInt(s,10));for(let i=0;i<3;i++){let av=a[i]??0,bv=b[i]??0;if(Number.isNaN(av)||Number.isNaN(bv))return 0;if(av>bv)return 1;if(av<bv)return-1}return 0}async function checkLatestVersion(opts={}){let channel=opts.channel??channelOf(),fake=process.env.ROVE_FAKE_UPDATE;if(fake)return{current:CURRENT_VERSION,latest:fake,hasUpdate:isNewerSemver(fake,CURRENT_VERSION),channel};if(isDev()&&!opts.force)return null;let latest=await fetchLatestFromRegistry(PACKAGE_NAME,channel);if(!latest)return null;let hasUpdate=isNewerSemver(latest,CURRENT_VERSION),minAgeMs=(opts.minAgeHours??0)*3600000;if(hasUpdate&&minAgeMs>0){let publishedAt=await fetchPublishedAt(PACKAGE_NAME,latest),now=(opts.now??Date.now)();if(publishedAt!==null&&now-publishedAt<minAgeMs)hasUpdate=!1}return{current:CURRENT_VERSION,latest,hasUpdate,channel}}function versionFromTagName(tagName){if(typeof tagName!=="string")return null;return tagName.match(/^v(\d+\.\d+\.\d+)$/)?.[1]??null}var ZH_NOTES_ASSET="release-notes.zh.md";async function notesBody(release,locale,signal){if(locale!=="zh"||!Array.isArray(release.assets))return release.body;let asset=release.assets.find((a)=>a.name===ZH_NOTES_ASSET);if(typeof asset?.browser_download_url!=="string")return release.body;try{let res=await fetch(asset.browser_download_url,{signal}),text=res.ok?await res.text():"";return text.trim()?text:release.body}catch{return release.body}}async function fetchReleaseNotes(version,locale="en"){let slug=repoSlug();if(!slug)return null;let tag=`v${version}`,ctrl=new AbortController,timer=setTimeout(()=>ctrl.abort(),FETCH_TIMEOUT_MS);try{let res=await fetch(`https://api.github.com/repos/${slug}/releases/tags/${tag}`,{signal:ctrl.signal,headers:{accept:"application/vnd.github+json","x-github-api-version":"2022-11-28"}});if(!res.ok)return null;let body=await res.json();if(typeof body.body!=="string"||typeof body.html_url!=="string")return null;return{body:await notesBody({...body,body:body.body},locale,ctrl.signal),url:body.html_url,version}}catch{return null}finally{clearTimeout(timer)}}async function fetchReleaseNotesRange(args){let slug=repoSlug();if(!slug)return[];let limit=args.limit??100,ctrl=new AbortController,timer=setTimeout(()=>ctrl.abort(),FETCH_TIMEOUT_MS);try{let res=await fetch(`https://api.github.com/repos/${slug}/releases?per_page=${limit}`,{signal:ctrl.signal,headers:{accept:"application/vnd.github+json","x-github-api-version":"2022-11-28"}});if(!res.ok)return[];let body=await res.json();if(!Array.isArray(body))return[];let inRange=body.flatMap((release)=>{let version=versionFromTagName(release.tag_name);if(!version||typeof release.html_url!=="string"||typeof release.body!=="string")return[];if(compareSemver(version,args.current)<=0)return[];if(compareSemver(version,args.latest)>0)return[];return[{version,url:release.html_url,release:{...release,body:release.body}}]});return await Promise.all(inRange.map(async({version,url,release})=>({version,url,body:await notesBody(release,args.locale??"en",ctrl.signal)})))}catch{return[]}finally{clearTimeout(timer)}}async function fetchReleaseSummaries(limit=12){let slug=repoSlug();if(!slug)return[];let ctrl=new AbortController,timer=setTimeout(()=>ctrl.abort(),FETCH_TIMEOUT_MS);try{let res=await fetch(`https://api.github.com/repos/${slug}/releases?per_page=${limit}`,{signal:ctrl.signal,headers:{accept:"application/vnd.github+json","x-github-api-version":"2022-11-28"}});if(!res.ok)return[];let body=await res.json();if(!Array.isArray(body))return[];return body.map((release)=>{let version=versionFromTagName(release.tag_name);if(!version||typeof release.html_url!=="string")return null;return{version,url:release.html_url}}).filter((release)=>release!==null)}catch{return[]}finally{clearTimeout(timer)}}function releasePageUrl(version){let slug=repoSlug();if(!slug)return null;return`https://github.com/${slug}/releases/tag/v${version}`}
|
|
3
|
+
export{package_default,CURRENT_VERSION,PACKAGE_NAME,repoSlug,UPDATE_SCRIPT_URL,UPDATE_COMMAND,updateCommandFor,owningNpmPrefix,recommendedGlobalInstallCommand,BREAKING_VERSIONS,breakingVersionsCrossed,RELEASE_CHANNELS,DEFAULT_RELEASE_CHANNEL,channelOf,isNewerSemver,compareSemver,checkLatestVersion,ZH_NOTES_ASSET,fetchReleaseNotes,fetchReleaseNotesRange,fetchReleaseSummaries,releasePageUrl};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{Orchestrator}from"./chunk-
|
|
2
|
+
import{Orchestrator}from"./chunk-k3h4cpdy.js";import"./chunk-zr058kbe.js";import"./chunk-c1dz3qg3.js";import"./chunk-3x5g06mw.js";import"./chunk-4dt1gfb3.js";import"./chunk-xmwp6je4.js";import{PLACEHOLDER_TASK_TITLE}from"./chunk-2xsxcdp6.js";import"./chunk-1vxtd8rh.js";import"./chunk-gq36nzg8.js";import"./chunk-mqfvzdpr.js";import"./chunk-hpay0ax8.js";import"./chunk-catrr0z3.js";import"./chunk-895yqjjz.js";import"./chunk-sak1tphp.js";import"./chunk-y1xnjx8x.js";import"./chunk-2tfnscxk.js";import"./chunk-9rj2fx6v.js";import"./chunk-rfp0ayra.js";import"./chunk-t7j8g9f6.js";import"./chunk-dmzg9zdc.js";import"./chunk-40dgv828.js";import"./chunk-6xppe9bj.js";import"./chunk-2brn8e1y.js";import"./chunk-7mwvvj46.js";import"./chunk-32j6y5d1.js";import"./chunk-cmp64q4j.js";import"./chunk-7kysxgbq.js";import"./chunk-x1jxxmtc.js";import"./chunk-xy8be792.js";import"./chunk-h2m7em1e.js";import"./chunk-bejhca6z.js";import"./chunk-br8r72bf.js";import"./chunk-yt15y085.js";import"./chunk-z1nj22va.js";import"./chunk-zt7wfnva.js";import"./chunk-pn5ygqn6.js";import"./chunk-eqdbfgq5.js";import"./chunk-f8gg8cqh.js";import"./chunk-nc46gddg.js";import"./chunk-91sykzqq.js";export{PLACEHOLDER_TASK_TITLE,Orchestrator};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{canonicalize}from"./chunk-1vxtd8rh.js";import{applyJitter,exponentialBackoff}from"./chunk-2tfnscxk.js";import{getRemoteRepoConfig,isRemoteRepoKey,resolveRepoRoot}from"./chunk-rfp0ayra.js";var canonPath=canonicalize;function randomDirTaskSuffix(){return Math.random().toString(36).slice(2,6).padEnd(4,"0")}function titleFromRepo(repo){let segs=repo.split(/[/\\]/).filter(Boolean);return segs.length>0?segs[segs.length-1]??repo:repo}function normalizeMainRepo(repo){let normalized=resolveRepoRoot(repo);return{repo:normalized,key:isRemoteRepoKey(normalized)?normalized:canonPath(normalized)}}function repoWorkingDir(repo){return getRemoteRepoConfig(repo)?.basePath??repo}var GH_PR_VIEW_FIELDS="number,url,title,state,baseRefName,headRefOid,reviewDecision,mergeable,statusCheckRollup,additions,deletions";function lifecycleFromState(state,reviewDecision){switch((state??"").toUpperCase()){case"MERGED":return"merged";case"CLOSED":return"closed";case"OPEN":return(reviewDecision??"").toUpperCase()==="APPROVED"?"ready_to_merge":"open";default:return"unknown"}}function entryCheckState(entry){let status=(entry.status??"").toUpperCase(),conclusion=(entry.conclusion??"").toUpperCase(),contextState=(entry.state??"").toUpperCase();if(status&&status!=="COMPLETED")return"pending";if(conclusion){if(conclusion==="SUCCESS"||conclusion==="NEUTRAL"||conclusion==="SKIPPED")return"passing";if(conclusion==="CANCELLED")return"cancelled";return"failing"}if(contextState==="SUCCESS")return"passing";if(contextState==="PENDING"||contextState==="EXPECTED")return"pending";if(contextState==="FAILURE"||contextState==="ERROR")return"failing";return"unknown"}function checkStateFromRollup(rollup){if(!rollup||rollup.length===0)return"none";let sawPending=!1,sawUnknown=!1,sawVerdict=!1;for(let entry of rollup){let s=entryCheckState(entry);if(s==="failing")return"failing";if(s==="cancelled")continue;if(sawVerdict=!0,s==="pending")sawPending=!0;else if(s==="unknown")sawUnknown=!0}if(sawPending)return"pending";if(sawUnknown)return"unknown";return sawVerdict?"passing":"none"}function mapGhPrView(view,at){if(!view||typeof view.number!=="number")return null;return{provider:"github",lifecycle:lifecycleFromState(view.state,view.reviewDecision),checkState:checkStateFromRollup(view.statusCheckRollup),number:view.number,url:view.url,title:view.title,...typeof view.additions==="number"?{additions:view.additions}:{},...typeof view.deletions==="number"?{deletions:view.deletions}:{},baseRef:view.baseRefName,reviewDecision:view.reviewDecision||void 0,mergeable:view.mergeable||void 0,...view.headRefOid?{headSha:view.headRefOid}:{},lastCheckedAt:at}}function samePrStatus(a,b){if(a===b)return!0;if(!a||!b)return!1;return a.provider===b.provider&&a.lifecycle===b.lifecycle&&a.checkState===b.checkState&&a.number===b.number&&a.url===b.url&&a.title===b.title&&a.additions===b.additions&&a.deletions===b.deletions&&a.baseRef===b.baseRef&&a.reviewDecision===b.reviewDecision&&a.mergeable===b.mergeable&&a.headSha===b.headSha}var NO_REMOTE_PATTERNS=["none of the git remotes","no git remote","to use github cli in a github repository","not a github repository"],AUTH_PATTERNS=["gh auth login","authentication required","not logged in","http 401","bad credentials","requires authentication"],NETWORK_PATTERNS=["could not resolve host","no such host","dial tcp","connection refused","connection reset","network is unreachable","i/o timeout","timeout","tls handshake","http 5","rate limit","try again"];function matchesAny(haystack,needles){return needles.some((n)=>haystack.includes(n))}function classifyGhFailure(s){if(s.parseError)return{kind:"error",error:"parse"};if(s.timedOut)return{kind:"error",error:"timeout"};if(s.spawnError)return{kind:"error",error:"missing-binary"};let err=(s.stderr??"").toLowerCase();if(matchesAny(err,NO_REMOTE_PATTERNS))return{kind:"error",error:"no-remote"};if(matchesAny(err,AUTH_PATTERNS))return{kind:"error",error:"auth"};if(matchesAny(err,NETWORK_PATTERNS))return{kind:"error",error:"network"};return{kind:"error",error:"unknown"}}function nextPrPoll(outcome,prevFailures,now,cfg,rand=Math.random){let at=(delay)=>now+applyJitter(delay,cfg.jitterRatio,rand);if(outcome.kind==="pr")return{nextAllowedAt:at(outcome.settled?cfg.settledMs:cfg.tickMs),failures:0};if(outcome.kind==="empty")return{nextAllowedAt:at(cfg.noPrMs),failures:0};if(outcome.error==="no-remote")return{nextAllowedAt:at(cfg.noRemoteMs),failures:0};let failures=prevFailures+1;return{nextAllowedAt:at(exponentialBackoff(cfg.failureBaseMs,failures-1,cfg.failureCapMs)),failures}}function checkResolutionNotify(prev,next){if(prev!=="pending")return null;if(next==="passing"||next==="failing")return next;return null}
|
|
3
|
+
export{GH_PR_VIEW_FIELDS,mapGhPrView,samePrStatus,classifyGhFailure,nextPrPoll,checkResolutionNotify,canonPath,randomDirTaskSuffix,titleFromRepo,normalizeMainRepo,repoWorkingDir};
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
import{isPathLikeArg}from"./chunk-
|
|
4
|
-
`)}var bridge=()=>import("./chunk-
|
|
3
|
+
import{isPathLikeArg}from"./chunk-j5y7hhc2.js";import{matchPathGlob}from"./chunk-kg11c1py.js";import{CURRENT_VERSION}from"./chunk-yn01fet6.js";import{switchFlag}from"./chunk-zsfyj36g.js";import{expandTilde}from"./chunk-2r1fgng9.js";import{errorMessage}from"./chunk-2brn8e1y.js";import{rejectionReason}from"./chunk-7mwvvj46.js";import{BUILTIN_VENDORS,coerceVendorId,isBuiltinVendor}from"./chunk-h2m7em1e.js";import"./chunk-z1nj22va.js";import{ROVE_PRODUCT_NAME}from"./chunk-zt7wfnva.js";import"./chunk-pn5ygqn6.js";import"./chunk-f8gg8cqh.js";import"./chunk-nc46gddg.js";import{__require}from"./chunk-91sykzqq.js";import{resolve}from"path";var DYNAMIC_COMMANDS=new Map([["completions",async(args)=>{let{runCompletionsSubcommand}=await import("./chunk-q2nhmaq9.js");await runCompletionsSubcommand(args)}],["export",async(args)=>{let{runExportSubcommand}=await import("./chunk-aqwckba5.js");await runExportSubcommand(args)}],["repo",async(args)=>{let{runRepoSubcommand}=await import("./chunk-tjhw09bz.js");await runRepoSubcommand(args)}],["api",async(args)=>{let{runApiSubcommand}=await import("./chunk-c9ptxthj.js");await runApiSubcommand(args)}],["update",async(args)=>{let{runUpdateSubcommand}=await import("./chunk-3qj9yw5c.js");await runUpdateSubcommand(args)}],["theme",async(args)=>{let{runThemeSubcommand}=await import("./chunk-x94e0rw4.js");await runThemeSubcommand(args)}],["feedback",async(args)=>{let{runFeedbackSubcommand}=await import("./chunk-f23manyf.js");await runFeedbackSubcommand(args)}],["daemon",async(args)=>{let{runDaemonSubcommand}=await import("./chunk-k2rmj0v3.js");await runDaemonSubcommand(args)}],["machine",async(args)=>{let{runMachineSubcommand}=await import("./chunk-k9xabf0t.js");await runMachineSubcommand(args)}],["weixin",async(args)=>{let{runChannelCli}=await import("./chunk-b5fdqynx.js");await runChannelCli("weixin",args)}],["doctor",async(args)=>{let{runDoctorSubcommand}=await import("./chunk-03fv92ya.js");await runDoctorSubcommand(args)}],["config",async(args)=>{let{runConfigSubcommand}=await import("./chunk-w7v71t7e.js");await runConfigSubcommand(args)}],["reset",async(args)=>{let{runResetSubcommand}=await import("./chunk-q72w3dsx.js");await runResetSubcommand(args)}],["pty-host",async(args)=>{let{runPtyHostSubcommand}=await import("./chunk-j2e7jgh8.js");await runPtyHostSubcommand(args)}],["skill",async(args)=>{let{runSkillSubcommand}=await import("./chunk-95kk75wh.js");await runSkillSubcommand(args)}],["plugin",async(args)=>{let{runPluginSubcommand}=await import("./chunk-9qpea48c.js");await runPluginSubcommand(args)}],["hook",async(args)=>{let{runHookSubcommand}=await import("./chunk-j19av093.js");await runHookSubcommand(args)}]]);function topLevelUsage(cliName=ROVE_PRODUCT_NAME){return[`${cliName} ${CURRENT_VERSION}`,"",`Usage: ${cliName} [command] [options]`,"","Run with no command to launch PureTUI.",`Run \`${cliName} .\` (or \`${cliName} <path>\`) to open a directory as a standalone task.`,"","Commands:"," completions <shell> Generate shell completion script (bash/zsh/fish)"," add [path] Save a repo path for the new-task picker"," remove [path] Forget a saved project (inverse of add; non-destructive)"," adopt [glob] Import existing git worktrees as tasks"," export [--csv|--json] Print the task list (json/csv/table; daemon-free)"," repo <verb> Per-repo init script + first prompt (show|set|unset)",` api <verb> Scriptable RPC surface for agents (see \`${cliName} api --help\`)`," daemon <verb> Manage the daemon (start|stop|status|restart)"," machine <verb> Other computers running Rove (add|doctor|remove|list)"," weixin <verb> Talk to Rove from WeChat (login|logout|status|allow|deny|send|agent)"," doctor [--report|--fix] Diagnose daemon/PTY/engines/git; --fix walks the remedies",` config [--path] Open ${cliName}'s config file (state.json) in your editor`," reset [--hard] Stop runtimes; optionally wipe task/UI state"," theme <verb> Manage user themes (list|add|remove)",` skill <verb> Install the ${cliName} agent skill (install|status|command|print)`," plugin <verb> Install and run plugins (install|link|list|action|\u2026)"," feedback File a GitHub issue on the Rove repo",` update [version|channel|list] Self-update ${cliName}, switch channel, or list versions`,"","Options:"," -v, --version Print version"," -h, --help Print this help"," --skill Print the agent skill file and exit"," --web [bridge flags] Serve the browser client on 127.0.0.1 and print its link"].join(`
|
|
4
|
+
`)}var bridge=()=>import("./chunk-xqvyzv62.js"),CLI_NAME=ROVE_PRODUCT_NAME,ADD_USAGE=[`Usage: ${CLI_NAME} add [path]`,` ${CLI_NAME} add --remote --host <host> --user <user> --path <basePath> [--port N] [--key [path] | --password]`,"","Save a repo for the new-task picker. With --remote, register an SSH-backed","project whose worktrees live on <host>; engine launch over SSH is pending.",""].join(`
|
|
5
5
|
`);async function runAddSubcommand(rest){let arg=rest[0];if(arg==="--help"||arg==="-h"||arg==="help"){process.stdout.write(ADD_USAGE);return}if(arg==="--remote"){let{runAddRemote}=await import("./chunk-dsdtq087.js");await runAddRemote(rest.slice(1));return}if(arg?.startsWith("-"))process.stderr.write(`${CLI_NAME} add: unknown flag "${arg}"
|
|
6
6
|
|
|
7
7
|
${ADD_USAGE}`),process.exit(2);let target=resolve(process.cwd(),expandTilde(arg&&arg.length>0?arg:".")),{addSavedRepo}=await import("./chunk-hv79ys32.js"),result=addSavedRepo(target);if(result.rejected)process.stderr.write(`${CLI_NAME} add: "${arg&&arg.length>0?arg:"."}" cannot be a project \u2014 ${rejectionReason(result.rejected)} (resolved to ${result.path}).
|
|
@@ -23,4 +23,4 @@ ${ADOPT_USAGE}
|
|
|
23
23
|
pass a path glob to adopt, e.g. ${CLI_NAME} adopt '${repo}/*' --yes`);return}let matched=worktrees.filter(isMatch);if(matched.length===0){console.log(`
|
|
24
24
|
no worktrees match glob ${JSON.stringify(glob)}`);return}if(!yes){console.log(`
|
|
25
25
|
${matched.length} worktree(s) match \u2014 re-run with --yes to adopt them`);return}await adoptWorktreesInto(orch,repo,matched,vendor),console.log(`done \u2014 adopted ${matched.length} worktree(s)`)}function printTopLevelUsage(out){out.write(`${topLevelUsage()}
|
|
26
|
-
`)}var COMMANDS=new Map([["add",runAddSubcommand],["remove",runRemoveSubcommand],["adopt",runAdoptSubcommand],...DYNAMIC_COMMANDS]);async function main(){let[,,...rawArgs]=process.argv,[subcommand,...rest]=rawArgs;if(subcommand==="--version"||subcommand==="-v"||subcommand==="version"){let{CURRENT_VERSION:CURRENT_VERSION2}=await import("./chunk-
|
|
26
|
+
`)}var COMMANDS=new Map([["add",runAddSubcommand],["remove",runRemoveSubcommand],["adopt",runAdoptSubcommand],...DYNAMIC_COMMANDS]);async function main(){let[,,...rawArgs]=process.argv,[subcommand,...rest]=rawArgs;if(subcommand==="--version"||subcommand==="-v"||subcommand==="version"){let{CURRENT_VERSION:CURRENT_VERSION2}=await import("./chunk-7911jkq6.js");console.log(`${CLI_NAME} ${CURRENT_VERSION2}`);return}if(subcommand==="--help"||subcommand==="-h"||subcommand==="help"){printTopLevelUsage(process.stdout);return}if(subcommand==="--skill"){let{runSkillSubcommand}=await import("./chunk-95kk75wh.js");await runSkillSubcommand(["print"]);return}if(subcommand==="--web"){let{runWebSubcommand}=await import("./chunk-3pgp6z3k.js");await runWebSubcommand(rest);return}let handler=subcommand!==void 0?COMMANDS.get(subcommand):void 0;if(handler){await handler(rest);return}if(subcommand!==void 0&&isPathLikeArg(subcommand)){let{runOpenDirectory}=await import("./chunk-kp43j420.js");await runOpenDirectory(subcommand);return}if(subcommand!==void 0)console.error(`${CLI_NAME}: unknown command '${subcommand}'`),printTopLevelUsage(process.stderr),process.exit(2);let{publishRoveTerminalTitle}=await import("./chunk-g8wrdp7n.js");publishRoveTerminalTitle();let{launchTui}=await import("./chunk-xekk4k54.js");await launchTui()}var cliDone=main().catch(async(err)=>{let{formatCliFailure}=await import("./chunk-x119xqa6.js");console.error(formatCliFailure(err,{cliName:CLI_NAME,argv:process.argv,cwd:process.cwd(),env:process.env})),process.exit(1)});export{cliDone};
|
package/dist/cli/rove.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{join as l}from"node:path";import{createInterface as a}from"node:readline/promises";import{pathToFileURL as ss}from"node:url";import{spawnSync as Q}from"node:child_process";import{constants as
|
|
3
|
-
`)[0]?.trim()??""};function J(s){return s.trim().match(/^v?(\d+\.\d+\.\d+)/)?.[1]??null}function X(s,t=$){let c=J(s),W=J(t);if(!c||!W)return!0;let b=c.split(".").map(Number),g=W.split(".").map(Number);for(let
|
|
2
|
+
import{join as l}from"node:path";import{createInterface as a}from"node:readline/promises";import{pathToFileURL as ss}from"node:url";import{spawnSync as Q}from"node:child_process";import{constants as P,accessSync as V}from"node:fs";import{homedir as o}from"node:os";import{basename as R,delimiter as I,dirname as U,join as y}from"node:path";import{fileURLToPath as T}from"node:url";var H={$schema:"https://json.schemastore.org/package.json",name:"@sma1lboy/rove",version:"0.9.281",description:"Rove — the agent multiplexer for your terminal. Run coding agents on parallel tasks with isolated worktrees and persistent sessions.",keywords:["terminal","tui","cli","multiplexer","ai-agents","coding-agent","ai-coding-assistant","agentic-ai","parallel-agents","git-worktree","claude-code","codex","llm","developer-tools"],type:"module",packageManager:"bun@1.3.13",bin:{rove:"dist/cli/rove.js"},files:["dist/cli","dist/completions","dist/web","dist/web-ui","dist/skills","dist/voice","dist/*.wav","README.md","LICENSE"],publishConfig:{access:"public"},repository:{type:"git",url:"git+https://github.com/Sma1lboy/rove.git"},homepage:"https://rove.run",bugs:{url:"https://github.com/Sma1lboy/rove/issues"},engines:{bun:">=1.3.11"},scripts:{dev:"ROVE_DEV=1 bun --conditions=browser ./src/cli/rove.ts","dev:mock":"ROVE_DEV=1 bun ./src/tui-react/mock/host.tsx","dev:sandbox":"bun run scripts/dev-sandbox.ts run","dev:sandbox:reset":"bun run scripts/dev-sandbox.ts reset",build:"bun run scripts/build.ts","build:rubric":"bun run scripts/build-tier-rubric.ts",compile:"bun run scripts/compile.ts",typecheck:"tsc --noEmit","check-i18n":"bun run scripts/check-i18n.ts",test:"bun run test:fast && bun run test:socket","perf:golden":"bun scripts/perf-golden.ts","pty:soak":"bun scripts/pty-soak.ts","test:fast":"vitest run --passWithNoTests --minWorkers=1 --maxWorkers=8","test:socket":"ROVE_INCLUDE_SOCKET=1 vitest run test/daemon --pool forks --minWorkers=1 --maxWorkers=4 --passWithNoTests","test:socket:coverage":"ROVE_INCLUDE_SOCKET=1 ROVE_COVERAGE_DAEMON=1 vitest run test/daemon --coverage --pool forks --minWorkers=1 --maxWorkers=4 --passWithNoTests","test:behavior":"ROVE_INCLUDE_BEHAVIOR=1 vitest run test/behavior --pool forks --minWorkers=1 --maxWorkers=1 --retry=2 --passWithNoTests","test:render":"bun scripts/render-track.mjs",coverage:"vitest run --coverage --passWithNoTests",bench:"vitest bench --run",lint:"biome check .",knip:"knip-bun",postinstall:"bun run scripts/check-preview-deps.ts || true",prepublishOnly:"bun run typecheck && bun run build","plugin-sandbox":"bun run scripts/plugin-sandbox.ts"},"//":"biome.json + bunfig.toml live at the monorepo root since they apply repo-wide; bun.lock also lives at root (workspace-shared).",dependencies:{"@ansi-tools/parser":"^1.0.15","@opentui/core":"0.4.3","@opentui/react":"0.4.3","@xterm/addon-serialize":"^0.14.0","@xterm/addon-unicode11":"^0.9.0","@xterm/headless":"^6.0.0",react:"^19.2.8","smol-toml":"^1.7.1",ws:"^8.21.3"},optionalDependencies:{"node-pty":"^1.1.0"},devDependencies:{"@biomejs/biome":"1.9.4","@sma1lboy/rove-daemon":"0.7.23","@tsconfig/bun":"1.0.11","@types/bun":"1.3.14","@types/node":"25.6.2","@types/react":"^19.2.0","@vitest/coverage-v8":"2.1.9",knip:"^6.14.2","react-devtools-core":"^7.0.1","rove-bridge":"0.0.42","rove-weixin":"0.0.42",typescript:"5.8.2",vitest:"2.1.9"},trustedDependencies:[]};var q="rove";var k=q;var z="ROVE_BUN",_="ROVE_NO_BUN_BOOTSTRAP",F="ROVE_SKIP_BUN_CHECK",$=H.engines.bun.match(/\d+\.\d+\.\d+/)?.[0]??"0.0.0",L=(s)=>{try{return V(s,P.X_OK),!0}catch{return!1}},B=(s)=>s==="win32"?"bun.exe":"bun",f=(s)=>{let t=Q(s,["--version"],{encoding:"utf8",timeout:5000});if(t.error||t.status!==0)return null;return t.stdout?.split(`
|
|
3
|
+
`)[0]?.trim()??""};function J(s){return s.trim().match(/^v?(\d+\.\d+\.\d+)/)?.[1]??null}function X(s,t=$){let c=J(s),W=J(t);if(!c||!W)return!0;let b=c.split(".").map(Number),g=W.split(".").map(Number);for(let i=0;i<3;i++){let r=b[i]??0,x=g[i]??0;if(r!==x)return r>x}return!0}function Y(s=process.env){return s[F]==="1"}function A(s={}){let t=s.env??process.env,c=s.platform??process.platform,W=s.home??t.HOME??t.USERPROFILE??o(),b=B(c),g=[],i=t[z]?.trim();if(i)g.push(i);for(let G of(t.PATH??t.Path??"").split(I))if(G)g.push(y(G,b));let r=t.BUN_INSTALL?.trim();if(r)g.push(y(r,"bin",b));if(W)g.push(y(W,".bun","bin",b));let x=s.launcherDir;if(x)g.push(y(x,"..","..","node_modules","bun","bin",b)),g.push(y(x,"..","..","..","bun","bin",b));return g}function N(s={}){let t=s.isExecutable??L;return A(s).find((c)=>t(c))??null}function Z(s={}){let t=s.isExecutable??L,c=s.bunVersionOf??f;if(Y(s.env??process.env))return{bun:N(s),stale:null,unusable:null};let W=null,b=null;for(let g of A(s)){if(!t(g))continue;let i=c(g);if(i===null){b??=g;continue}if(X(i))return{bun:g,stale:W,unusable:b};W??={path:g,version:J(i)??i}}return{bun:null,stale:W,unusable:b}}function h(s){return U(T(s))}function C(s){return R(T(s)).replace(/\.[cm]?[jt]s$/,"")}function n(s=process.platform){return s==="win32"?["powershell","-NoProfile","-Command","irm bun.sh/install.ps1 | iex"]:["bash","-c","curl -fsSL https://bun.sh/install | bash"]}function D(s=k,t=process.platform){let c=t==="win32"?'powershell -c "irm bun.sh/install.ps1 | iex"':"curl -fsSL https://bun.sh/install | bash";return[`${s}: Rove runs on the Bun runtime, and no Bun was found on this machine.`,"","Install Bun, then run this command again:",` ${c}`," npm install -g bun # any platform, if you already have npm","","Or install Bun and Rove together in one step:"," curl -fsSL https://rove.run/install.sh | sh","",`Already have Bun somewhere unusual? Point Rove at it: ${z}=/path/to/bun`,""].join(`
|
|
4
4
|
`)}function j(s,t,c=k){return[`${c}: this machine's Bun is too old for Rove — found ${t} at ${s}, need ${$} or newer.`,"","Rove's terminals are built on Bun's PTY API. An older Bun ignores it silently,","so every terminal and engine tab would open empty and stay empty.","","Upgrade Bun, then run this command again:"," bun upgrade # Bun installed itself (~/.bun)"," brew upgrade bun # Homebrew"," npm install -g bun@latest # npm-managed Bun","",`Have a newer Bun elsewhere? Point Rove at it: ${z}=/path/to/bun`,`Run on this one anyway (unsupported, terminals will be blank): ${F}=1`,""].join(`
|
|
5
5
|
`)}function v(s,t=k){return[`${t}: the Bun at ${s} could not be run — \`${s} --version\` failed or never returned.`,"","Rove skipped it rather than starting through it, which would hang or fail","with a message from that binary instead of this one.","","Reinstall Bun, then run this command again:"," curl -fsSL https://bun.sh/install | bash","",`Have a working Bun elsewhere? Point Rove at it: ${z}=/path/to/bun`,""].join(`
|
|
6
6
|
`)}function w(s=process.env,t=process.stdin,c=process.stdout){if(s[_]==="1")return!1;if(s.CI==="true"||s.CI==="1")return!1;return Boolean(t.isTTY&&c.isTTY)}function M(s={},t=Q){let c=s.platform??process.platform,[W,...b]=n(c);if(!W)return null;let g=t(W,b,{stdio:"inherit"});if(g.error||g.status!==0)return null;return Z(s).bun}function u(s){if(typeof s.status==="number")return s.status;if(s.signal)return 128+(Object.hasOwn(K,s.signal)?K[s.signal]:0);return 1}var K={SIGINT:2,SIGQUIT:3,SIGKILL:9,SIGTERM:15};function S(s,t,c,W=Q){let b=W(s,[t,...c],{stdio:"inherit"});if(b.error)return process.stderr.write(`rove: could not start Bun at ${s}: ${b.error.message}. Check that this path is executable, or reinstall Bun from https://bun.sh.
|
|
7
|
-
`),1;return u(b)}var
|
|
7
|
+
`),1;return u(b)}var O=h(import.meta.url),p=C(import.meta.url),d=l(O,`${p}-run.js`);async function ts(s){let t=a({input:process.stdin,output:process.stdout});try{let c=(await t.question(s)).trim().toLowerCase();return c===""||c==="y"||c==="yes"}finally{t.close()}}async function cs(){let s={launcherDir:O},t=Z(s);if(t.bun||!w())return t;let c=t.stale?`${p}: Bun ${t.stale.version} is too old for Rove (needs ${$}+). Install the latest Bun now? [Y/n] `:`${p}: Rove runs on the Bun runtime, and none is installed. Install Bun now? [Y/n] `;if(!await ts(c))return t;return{bun:M(s),stale:t.stale,unusable:t.unusable}}var m=globalThis.Bun;if(m){let s=m.version;if(s&&!Y()&&!X(s))process.stderr.write(j(process.execPath,s,p)),process.exit(1);await import(ss(d).href)}else{let{bun:s,stale:t,unusable:c}=await cs();if(!s){let W=t?j(t.path,t.version,p):c?v(c,p):D(p);process.stderr.write(W),process.exit(1)}process.exit(S(s,d,process.argv.slice(2)))}
|
|
@@ -3,7 +3,7 @@ name: rove
|
|
|
3
3
|
description: Use when controlling Rove tasks, parallel coding attempts, hosted agent sessions, task lifecycle, or the daemon-owned issue tracker from a shell. Also the ONLY channel for messaging another agent session on this machine — `rove api send`, never a peer/MCP side channel.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- rove-skill-version:
|
|
6
|
+
<!-- rove-skill-version: 109 — bump in lockstep with ROVE_SKILL_VERSION (src/lib/skill-install.ts). -->
|
|
7
7
|
|
|
8
8
|
# Rove shell control
|
|
9
9
|
|
|
@@ -309,9 +309,11 @@ Find your section by what you were ASKED, not by Rove's noun for it:
|
|
|
309
309
|
| a prompt ends `[ROVE] … report with: rove api report`; tick a task unattended and want proof it reported, or skip a tick the task can't use | drive (`report`, `send --expect-report`, `send --if-runnable`) / read (`deliveries`, `should-run`) |
|
|
310
310
|
| make / rename / retarget / land / close a task | create / edit / lifecycle |
|
|
311
311
|
| materialize or adopt a checkout | worktree |
|
|
312
|
-
| which engines exist,
|
|
312
|
+
| which engines exist, report a Rove problem or idea | discover / feedback |
|
|
313
313
|
| an error code came back, `nextCommandArgs` | Error codes |
|
|
314
314
|
|
|
315
|
+
When the user says anything in Rove itself is awkward, broken or missing, offer to file it right away: draft a short title and body, show it, and on their OK run `rove feedback --kind bug|idea|question --title … --body-file -` (body on stdin), then share the URL. If it prints a prefilled link instead, hand that over and say to run `gh auth login`. Only for Rove; another project's bug is a `send` to its main task.
|
|
316
|
+
|
|
315
317
|
Reach for `schema` when the binary and that file disagree.
|
|
316
318
|
|
|
317
319
|
```bash
|
|
@@ -433,7 +433,7 @@ schema --verb --group --all
|
|
|
433
433
|
engine-list (none)
|
|
434
434
|
repo-match --origin --root --allow-fork
|
|
435
435
|
repo-list (none)
|
|
436
|
-
feedback --title(REQ) --body(REQ) --
|
|
436
|
+
feedback --title(REQ) --body(REQ) --kind(bug)
|
|
437
437
|
```
|
|
438
438
|
<!-- generated:end -->
|
|
439
439
|
|
|
@@ -452,9 +452,12 @@ resolves to (`{ repo, fork?, checkouts, searched }`); `repo-list` lists a
|
|
|
452
452
|
machine's repos by id (`{ repos: [{ id, primary, checkouts }] }`) — pass
|
|
453
453
|
`--machine <alias>` to ask another machine.
|
|
454
454
|
|
|
455
|
-
`feedback`
|
|
456
|
-
|
|
457
|
-
|
|
455
|
+
`feedback` files a GitHub issue on the Rove repo through `gh` (`--kind` picks the
|
|
456
|
+
label: bug, idea → enhancement, question) and appends an environment block.
|
|
457
|
+
It returns `{ filed: true, url, number }`; with no usable `gh` it returns
|
|
458
|
+
`{ filed: false, url, hint }` where `url` is a prefilled new-issue link — hand
|
|
459
|
+
it to the user. Rove's own product feedback channel, not a way to file work
|
|
460
|
+
for another project (that is a `send` to their main task; see SKILL.md).
|
|
458
461
|
|
|
459
462
|
## Error codes
|
|
460
463
|
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Dictation endpoint for rove's web client.
|
|
2
|
+
|
|
3
|
+
The phone records 16 kHz mono PCM, rove-bridge POSTs the WAV here with the text of the
|
|
4
|
+
conversation being dictated into, and this process returns the transcript. The recognizer is
|
|
5
|
+
local Whisper unless `~/.rove/voice/config.json` names a cloud endpoint (`engine.py`); the
|
|
6
|
+
vocabulary is `vocab.py`, sound-alike fixes `phonetic.py`. Everything this process keeps lives
|
|
7
|
+
under `~/.rove/voice`.
|
|
8
|
+
|
|
9
|
+
Run: `uv run --project scripts/voice --extra local scripts/voice/asr_server.py` (or Settings → Voice).
|
|
10
|
+
Binds 127.0.0.1 only; the bridge is the one public surface.
|
|
11
|
+
|
|
12
|
+
POST /transcribe {wav: hex, conversation?, context?, language?} -> {text, raw, language, ...}
|
|
13
|
+
POST /stream {session, seq, pcm: hex int16, final?, conversation?, context?, language?}
|
|
14
|
+
-> {partial} per chunk, the /transcribe reply at final (live.py)
|
|
15
|
+
GET /health
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import io
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import secrets
|
|
24
|
+
import sys
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
import wave
|
|
28
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
import engine
|
|
35
|
+
import learn
|
|
36
|
+
import live
|
|
37
|
+
import phonetic
|
|
38
|
+
import polish
|
|
39
|
+
import vocab
|
|
40
|
+
|
|
41
|
+
PORT = int(os.environ.get("ROVE_VOICE_PORT", "8791"))
|
|
42
|
+
MIN_AUDIO_SEC = 0.25
|
|
43
|
+
SILENCE_RMS = 0.004 # 16-bit speech sits far above this; fan noise and dead mics do not.
|
|
44
|
+
MAX_AUDIO_SEC = 240
|
|
45
|
+
|
|
46
|
+
_ready = threading.Event()
|
|
47
|
+
_warm_ms: float | None = None
|
|
48
|
+
_error: str | None = None
|
|
49
|
+
_backend: engine.Backend | None = None
|
|
50
|
+
_config: dict[str, Any] = {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_token() -> str:
|
|
54
|
+
"""One shared secret so a stray local process cannot drive the microphone path."""
|
|
55
|
+
path = engine.HOME / "token"
|
|
56
|
+
engine.HOME.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
if not path.exists():
|
|
58
|
+
path.write_text(secrets.token_hex(24))
|
|
59
|
+
path.chmod(0o600)
|
|
60
|
+
return path.read_text().strip()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def decode_wav(data: bytes) -> np.ndarray:
|
|
64
|
+
with wave.open(io.BytesIO(data)) as wav:
|
|
65
|
+
if wav.getnchannels() != 1:
|
|
66
|
+
raise ValueError("expected mono audio")
|
|
67
|
+
width = wav.getsampwidth()
|
|
68
|
+
rate = wav.getframerate()
|
|
69
|
+
frames = wav.readframes(wav.getnframes())
|
|
70
|
+
if width != 2:
|
|
71
|
+
raise ValueError(f"expected 16-bit samples, got {width * 8}-bit")
|
|
72
|
+
if rate != engine.SAMPLE_RATE:
|
|
73
|
+
raise ValueError(f"expected {engine.SAMPLE_RATE} Hz, got {rate}")
|
|
74
|
+
return np.frombuffer(frames, dtype="<i2").astype(np.float32) / 32768.0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def silent(pcm: np.ndarray) -> bool:
|
|
78
|
+
return not len(pcm) or float(np.sqrt(np.mean(np.square(pcm)))) < SILENCE_RMS
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def dictate(
|
|
82
|
+
backend: engine.Backend,
|
|
83
|
+
pcm: np.ndarray,
|
|
84
|
+
*,
|
|
85
|
+
conversation: str | None,
|
|
86
|
+
draft: str | None,
|
|
87
|
+
language: str | None,
|
|
88
|
+
config: dict[str, Any],
|
|
89
|
+
final: bool = True,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
"""One dictation: rank the vocabulary, recognize, normalize the script, then fix the English:
|
|
92
|
+
stored mishearings first, then whatever lands close to a term the recognizer was given. A final
|
|
93
|
+
take is then cleaned up by a chat model when config.json asks for it (polish.py); a live take's
|
|
94
|
+
partials never are."""
|
|
95
|
+
stored = vocab.load()["terms"]
|
|
96
|
+
terms = vocab.rank(conversation, draft, stored, backend.max_terms)
|
|
97
|
+
# Measured on large-v3: terms in the prompt cost a term when they were not said (68 -> 67 with the
|
|
98
|
+
# conversation's, 68 -> 64 with 15 learned ones) and gain less (70) than using them only to correct
|
|
99
|
+
# (72, and 68 when not said). Cloud recognizers take the prompt without that cost, so they get it by default.
|
|
100
|
+
in_prompt = config.get("promptContext", not isinstance(backend, engine.LocalWhisper))
|
|
101
|
+
hinted = terms if in_prompt else vocab.rank(None, draft, stored, backend.max_terms, rest=False)
|
|
102
|
+
raw, detected = backend.transcribe(pcm, hinted, language)
|
|
103
|
+
text = engine.simplified(raw) if config.get("simplified", True) else raw
|
|
104
|
+
text = vocab.correct(vocab.apply_spoken(text, stored), terms, fuzzy=config.get("fuzzy", True))
|
|
105
|
+
# Bench: +1 to +3 terms when the conversation holds them, no change otherwise (phonetic.py).
|
|
106
|
+
if config.get("phonetic", True):
|
|
107
|
+
text = phonetic.correct(text, terms)
|
|
108
|
+
out = {"text": text, "raw": raw, "language": detected, "terms": len(terms)}
|
|
109
|
+
if final and polish.enabled(config):
|
|
110
|
+
started = time.monotonic()
|
|
111
|
+
polished, error = polish.polish(text, terms, config)
|
|
112
|
+
out |= {"text": polished, "unpolished": text, "polishMs": round((time.monotonic() - started) * 1000)}
|
|
113
|
+
if error:
|
|
114
|
+
out["polishError"] = error
|
|
115
|
+
sys.stderr.write(f"voice: polish failed, kept the recognizer's text: {error}\n")
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def warm() -> None:
|
|
120
|
+
"""A cold graph costs seconds; pay it at boot, not on the first press."""
|
|
121
|
+
global _warm_ms, _error, _backend, _config
|
|
122
|
+
started = time.monotonic()
|
|
123
|
+
try:
|
|
124
|
+
_config = engine.load_config()
|
|
125
|
+
_backend = engine.backend(_config)
|
|
126
|
+
if isinstance(_backend, engine.LocalWhisper):
|
|
127
|
+
_backend.transcribe(np.zeros(engine.SAMPLE_RATE, dtype=np.float32), [], "en")
|
|
128
|
+
engine.simplified("說")
|
|
129
|
+
except Exception as err: # noqa: BLE001 - surfaced on /health, the server stays up
|
|
130
|
+
_error = str(err)
|
|
131
|
+
sys.stderr.write(f"voice: backend failed: {err}\n")
|
|
132
|
+
return
|
|
133
|
+
_warm_ms = round((time.monotonic() - started) * 1000)
|
|
134
|
+
_ready.set()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Handler(BaseHTTPRequestHandler):
|
|
138
|
+
token = ""
|
|
139
|
+
|
|
140
|
+
def log_message(self, fmt: str, *args: Any) -> None: # noqa: A003 - stdlib signature
|
|
141
|
+
sys.stderr.write("voice %s\n" % (fmt % args))
|
|
142
|
+
|
|
143
|
+
def reply(self, code: int, body: dict[str, Any]) -> None:
|
|
144
|
+
data = json.dumps(body, ensure_ascii=False).encode()
|
|
145
|
+
self.send_response(code)
|
|
146
|
+
self.send_header("content-type", "application/json")
|
|
147
|
+
self.send_header("content-length", str(len(data)))
|
|
148
|
+
self.end_headers()
|
|
149
|
+
self.wfile.write(data)
|
|
150
|
+
|
|
151
|
+
def do_GET(self) -> None: # noqa: N802 - stdlib signature
|
|
152
|
+
# /health stays open: the bridge probes it before the user says anything.
|
|
153
|
+
if self.path != "/health":
|
|
154
|
+
self.reply(404, {"error": "not_found"})
|
|
155
|
+
return
|
|
156
|
+
name = _backend.name if _backend else None
|
|
157
|
+
body = {"ready": _ready.is_set(), "model": name, "warm_ms": _warm_ms, "error": _error}
|
|
158
|
+
# Which copy answers: two bridges (release and a checkout) may each have started one.
|
|
159
|
+
self.reply(200, {**body, "pid": os.getpid(), "code": str(Path(__file__).resolve().parent)})
|
|
160
|
+
|
|
161
|
+
def body(self) -> dict[str, Any] | None:
|
|
162
|
+
if self.headers.get("x-voice-token", "") != self.token:
|
|
163
|
+
self.reply(401, {"error": "bad_token"})
|
|
164
|
+
return None
|
|
165
|
+
length = int(self.headers.get("content-length") or 0)
|
|
166
|
+
if length <= 0 or length > 32 * 1024 * 1024:
|
|
167
|
+
self.reply(400, {"error": "bad_size"})
|
|
168
|
+
return None
|
|
169
|
+
try:
|
|
170
|
+
return json.loads(self.rfile.read(length))
|
|
171
|
+
except ValueError:
|
|
172
|
+
self.reply(400, {"error": "bad_json"})
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def do_POST(self) -> None: # noqa: N802 - stdlib signature
|
|
176
|
+
routes = {
|
|
177
|
+
"/transcribe": self.transcribe,
|
|
178
|
+
"/stream": self.stream,
|
|
179
|
+
"/learn": self.learn_edit,
|
|
180
|
+
"/unlearn": self.unlearn_edit,
|
|
181
|
+
}
|
|
182
|
+
route = routes.get(self.path)
|
|
183
|
+
if route is None:
|
|
184
|
+
self.reply(404, {"error": "not_found"})
|
|
185
|
+
return
|
|
186
|
+
request = self.body()
|
|
187
|
+
if request is not None:
|
|
188
|
+
route(request)
|
|
189
|
+
|
|
190
|
+
def transcribe(self, request: dict[str, Any]) -> None:
|
|
191
|
+
try:
|
|
192
|
+
pcm = decode_wav(bytes.fromhex(request["wav"]))
|
|
193
|
+
except Exception as err: # noqa: BLE001 - every failure is the caller's audio
|
|
194
|
+
self.reply(400, {"error": f"bad_audio: {err}"})
|
|
195
|
+
return
|
|
196
|
+
self.finish_take(pcm, request.get("conversation"), request.get("context"), request.get("language"))
|
|
197
|
+
|
|
198
|
+
def finish_take(self, pcm: np.ndarray, conversation: str | None, draft: str | None, language: str | None) -> None:
|
|
199
|
+
seconds = round(len(pcm) / engine.SAMPLE_RATE, 2)
|
|
200
|
+
if seconds < MIN_AUDIO_SEC:
|
|
201
|
+
self.reply(200, {"text": "", "reason": "too_short", "seconds": seconds})
|
|
202
|
+
return
|
|
203
|
+
if seconds > MAX_AUDIO_SEC:
|
|
204
|
+
self.reply(200, {"text": "", "reason": "too_long", "seconds": seconds})
|
|
205
|
+
return
|
|
206
|
+
if silent(pcm):
|
|
207
|
+
self.reply(200, {"text": "", "reason": "silent", "seconds": seconds})
|
|
208
|
+
return
|
|
209
|
+
if _backend is None or not _ready.is_set():
|
|
210
|
+
self.reply(503, {"error": _error or "warming up"})
|
|
211
|
+
return
|
|
212
|
+
started = time.monotonic()
|
|
213
|
+
try:
|
|
214
|
+
out = dictate(_backend, pcm, conversation=conversation, draft=draft, language=language, config=_config)
|
|
215
|
+
except Exception as err: # noqa: BLE001 - report, never take the server down
|
|
216
|
+
self.reply(500, {"error": f"transcribe: {err}"})
|
|
217
|
+
return
|
|
218
|
+
ms = round((time.monotonic() - started) * 1000)
|
|
219
|
+
# Counts only, never the words: enough to see the conversation arrived and what a take cost.
|
|
220
|
+
heard = len(conversation or "")
|
|
221
|
+
terms = out["terms"]
|
|
222
|
+
sys.stderr.write(f"voice: {seconds}s audio, {heard} chars of conversation, {terms} terms, {ms} ms\n")
|
|
223
|
+
self.reply(200, {**out, "seconds": seconds, "ms": ms})
|
|
224
|
+
|
|
225
|
+
def stream(self, request: dict[str, Any]) -> None:
|
|
226
|
+
"""One chunk of a live take (live.py): the text so far while it grows, the transcript at `final`."""
|
|
227
|
+
session, seq = request.get("session"), request.get("seq")
|
|
228
|
+
if not isinstance(session, str) or not isinstance(seq, int):
|
|
229
|
+
self.reply(400, {"error": "bad_args"})
|
|
230
|
+
return
|
|
231
|
+
try:
|
|
232
|
+
raw = bytes.fromhex(request.get("pcm") or "")
|
|
233
|
+
pcm = np.frombuffer(raw[: len(raw) // 2 * 2], dtype="<i2").astype(np.float32) / 32768.0
|
|
234
|
+
take = live.append(session, seq, pcm, request)
|
|
235
|
+
except live.Gap as err:
|
|
236
|
+
self.reply(409, {"error": f"gap: {err}"})
|
|
237
|
+
return
|
|
238
|
+
except ValueError as err:
|
|
239
|
+
self.reply(400, {"error": f"bad_audio: {err}"})
|
|
240
|
+
return
|
|
241
|
+
if request.get("final"):
|
|
242
|
+
live.finish(session)
|
|
243
|
+
self.finish_take(take.pcm(), take.conversation, take.draft, take.language)
|
|
244
|
+
return
|
|
245
|
+
backend = _backend
|
|
246
|
+
if backend is None or not _ready.is_set() or not live.due(take, backend):
|
|
247
|
+
self.reply(200, {"partial": None})
|
|
248
|
+
return
|
|
249
|
+
# Never queue behind another decode: the next chunk asks again.
|
|
250
|
+
if not engine.GPU.acquire(blocking=False):
|
|
251
|
+
self.reply(200, {"partial": None})
|
|
252
|
+
return
|
|
253
|
+
try:
|
|
254
|
+
take.decoded = take.samples
|
|
255
|
+
pcm = take.pcm()
|
|
256
|
+
text = (
|
|
257
|
+
""
|
|
258
|
+
if silent(pcm)
|
|
259
|
+
else dictate(
|
|
260
|
+
backend,
|
|
261
|
+
pcm,
|
|
262
|
+
conversation=take.conversation,
|
|
263
|
+
draft=take.draft,
|
|
264
|
+
language=take.language,
|
|
265
|
+
config=_config,
|
|
266
|
+
final=False,
|
|
267
|
+
)["text"]
|
|
268
|
+
)
|
|
269
|
+
except Exception as err: # noqa: BLE001 - a failed partial only means no partial
|
|
270
|
+
sys.stderr.write(f"voice: partial failed: {err}\n")
|
|
271
|
+
text = None
|
|
272
|
+
finally:
|
|
273
|
+
engine.GPU.release()
|
|
274
|
+
self.reply(200, {"partial": text})
|
|
275
|
+
|
|
276
|
+
def learn_edit(self, request: dict[str, Any]) -> None:
|
|
277
|
+
"""A sent message against the draft its takes left: the fixes inside them join the vocabulary."""
|
|
278
|
+
base, sent, spans = request.get("base"), request.get("sent"), request.get("spans")
|
|
279
|
+
if not isinstance(base, str) or not isinstance(sent, str) or not isinstance(spans, list):
|
|
280
|
+
self.reply(400, {"error": "bad_args"})
|
|
281
|
+
return
|
|
282
|
+
if not _config.get("learn", True):
|
|
283
|
+
self.reply(200, {"learned": []})
|
|
284
|
+
return
|
|
285
|
+
pairs = [(int(s[0]), int(s[1])) for s in spans if isinstance(s, list) and len(s) == 2]
|
|
286
|
+
added = learn.remember(learn.fixes(base, pairs, sent))
|
|
287
|
+
sys.stderr.write(f"voice: learned {len(added)} from an edit\n")
|
|
288
|
+
self.reply(200, {"learned": added})
|
|
289
|
+
|
|
290
|
+
def unlearn_edit(self, request: dict[str, Any]) -> None:
|
|
291
|
+
learned = request.get("learned")
|
|
292
|
+
if not isinstance(learned, list):
|
|
293
|
+
self.reply(400, {"error": "bad_args"})
|
|
294
|
+
return
|
|
295
|
+
self.reply(200, {"undone": learn.forget([x for x in learned if isinstance(x, dict)])})
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def main() -> None:
|
|
299
|
+
Handler.token = read_token()
|
|
300
|
+
try:
|
|
301
|
+
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
|
|
302
|
+
except OSError as err:
|
|
303
|
+
# Another sidecar already answers here; it serves every bridge on this machine.
|
|
304
|
+
sys.stderr.write(f"voice: port {PORT} is taken ({err}); leaving it to that process\n")
|
|
305
|
+
raise SystemExit(0) from err
|
|
306
|
+
pidfile = engine.HOME / "sidecar.pid"
|
|
307
|
+
pidfile.write_text(str(os.getpid()))
|
|
308
|
+
threading.Thread(target=warm, daemon=True).start()
|
|
309
|
+
sys.stderr.write(f"voice listening on http://127.0.0.1:{PORT}\n")
|
|
310
|
+
try:
|
|
311
|
+
server.serve_forever()
|
|
312
|
+
finally:
|
|
313
|
+
if pidfile.exists() and pidfile.read_text().strip() == str(os.getpid()):
|
|
314
|
+
pidfile.unlink()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
main()
|