@leftoff/cli 0.9.8-beta.5 → 0.9.8-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,7 @@ What changed in Leftoff (called Checkpoints until 0.4.5), newest first. The boar
15
15
 
16
16
  - **A session from another Mac loads from that Mac.** On your leftoff.dev board, opening a session that lives on another Mac gets its Live chat, timeline, files and Find lists straight from that Mac, so nothing comes up empty. Find lists works from any Mac now, and when that Mac is offline the page says so plainly.
17
17
  - **Resume a Live session as Live.** Resume and Copy resume on a session you started with `leftoff live` now run `leftoff live -- --resume <id>`, so it reconnects to Live. Anything after `--` goes to Claude Code as is.
18
+ - **The Live badge pulses while Claude is working** on a turn, and stays still when the session is idle (no motion with Reduce Motion on).
18
19
  ### Fixed
19
20
  - Live chat: **Jump to latest ↓** now shows whenever you've scrolled up, not only when something new arrives, and the chat stays at the bottom while Claude's reply grows.
20
21
  - Your leftoff.dev board no longer sits on a blank page while it waits for your Mac. It shows "Connecting to your Mac…" right away and never waits more than about 10 seconds.
package/bin/leftoff.js CHANGED
@@ -306,7 +306,7 @@ end run`;Og=()=>process.env.CHECKPOINTS_JUMP_PLATFORM||process.platform});import
306
306
  `).trim();return t.length>e?t.slice(0,e-1)+"…":t},Ll=(n,e,t)=>Object.assign(new Error(t),{status:n,code:e});jl=class extends dL{constructor({now:e=()=>Date.now(),takeMs:t=$x,listenMs:s=zx,keep:r=500}={}){super(),this.now=e,this.takeMs=t,this.listenMs=s,this.keep=r,this.channels=new Map,this.msgs=new Map}chan(e){let t=String(e),s=this.channels.get(t);return s||(s={pid:t,pids:[t],permission:!1,version:null,since:null,lastPoll:0,waiters:new Set,queue:[],up:!1},this.channels.set(t,s)),s}hello(e,{pids:t=[],permission:s=!1,version:r=null}={}){let o=this.chan(e);return o.pids=[...new Set([String(e),...t.map(String)].filter(i=>/^\d{1,10}$/.test(i)))],o.permission=!!s,o.version=r?String(r).slice(0,20):null,o.lastPoll=this.now(),this.mark(o),o}find(e){if(e==null)return null;let t=String(e);if(this.channels.has(t))return this.channels.get(t);for(let s of this.channels.values())if(s.pids.includes(t))return s;return null}listening(e){let t=this.find(e);return!!t&&(t.waiters.size>0||this.now()-t.lastPoll<this.listenMs)}mark(e){let t=e.waiters.size>0||this.now()-e.lastPoll<this.listenMs;t&&!e.up?(e.up=!0,e.since=this.now(),this.emit("up",e)):!t&&e.up&&(e.up=!1,this.emit("down",e))}put(e,{kind:t="prompt",text:s="",meta:r={},by:o=null,sessionId:i=null,verdict:a=null}={}){let l=this.find(e),c=this.now(),u={id:Jx(t==="event"?"ev":t==="verdict"?"vd":"lv"),kind:t,text:s,meta:r,by:o,sessionId:i,verdict:a,pid:l?l.pid:String(e),state:"sent",at:c,takenAt:null,settledAt:null,error:null};if(this.msgs.set(u.id,u),this.trim(),!l||!this.listening(e))return this.fail(u,"not_live"),u;let d=l.waiters.values().next().value;return d?(u.takenAt=c,d.resolve(u)):t==="verdict"?l.queue.unshift(u):l.queue.push(u),u}take(e,t=zg,s=null){let r=this.chan(e);r.lastPoll=this.now(),this.mark(r),this.sweep();let o=r.queue.shift();if(o)return o.takenAt=this.now(),Promise.resolve(o);let i=Math.max(0,Math.min(Number(t)||0,zg));return new Promise(a=>{let l={resolve:null,timer:null};l.resolve=c=>{clearTimeout(l.timer),r.waiters.delete(l),r.lastPoll=this.now(),a(c)},l.timer=setTimeout(()=>l.resolve(null),i),l.timer.unref&&l.timer.unref(),r.waiters.add(l),s&&s.addEventListener("abort",()=>{r.waiters.has(l)&&(clearTimeout(l.timer),r.waiters.delete(l),a(null))},{once:!0})})}ack(e,t,{ok:s=!0,error:r=null}={}){let o=this.msgs.get(String(t||"")),i=this.find(e);if(!o||!i||o.pid!==i.pid)throw Ll(404,"not_found","no such message for this session");return o.state!=="sent"||(s?(o.state="received",o.settledAt=this.now(),this.emit("settled",o)):this.fail(o,String(r||"channel_error").slice(0,80))),o}fail(e,t){if(e.state!=="sent")return;e.state="failed",e.error=t,e.settledAt=this.now();let s=this.channels.get(e.pid);s&&(s.queue=s.queue.filter(r=>r!==e)),this.emit("settled",e)}get(e){return this.msgs.get(String(e||""))||null}sweep(){let e=this.now();for(let t of this.msgs.values())t.state==="sent"&&e-t.at>=this.takeMs&&this.fail(t,t.takenAt?"no_answer_from_session":"not_picked_up");for(let t of this.channels.values())this.mark(t)}trim(){if(!(this.msgs.size<=this.keep))for(let e of this.msgs.values()){if(this.msgs.size<=this.keep)break;e.state!=="sent"&&this.msgs.delete(e.id)}}publicMsg(e){return e?{id:e.id,kind:e.kind,state:e.state,error:e.error,at:new Date(e.at).toISOString(),settledAt:e.settledAt?new Date(e.settledAt).toISOString():null}:null}};Xg='If Claude Code says "server:leftoff · no MCP server configured", ignore it: that check skips --mcp-config servers; the channel still loads.'});function Of(n=0){return{phase:"sent",at:n,last:n,tool:null,waiting:null,ran:[],endedAt:null}}function lt(n,e={}){let t={...n||Of(e.at||0),ran:[...n&&n.ran||[]]},s=e.at||t.last;if(pL.has(t.phase)&&!(t.phase==="replied"&&e.type==="tool_done"))return t;switch(e.type){case"delivered":t.phase==="sent"&&(t.phase="received");break;case"failed":t.phase==="sent"&&(t.phase="failed",t.error=e.error||null,t.endedAt=s);break;case"started":case"text":t.phase!=="replied"&&(t.phase="working"),e.type==="text"&&(t.waiting=null);break;case"tool":t.phase="working",t.tool={name:Mi(e.tool,40),detail:Mi(e.detail,300)},t.waiting=null;break;case"tool_done":{let r=Mi(e.tool||t.tool&&t.tool.name,40);r&&t.ran.push({tool:r,detail:Mi(e.detail||t.tool&&t.tool.detail,300),asked:!!e.asked}),t.ran.length>20&&(t.ran=t.ran.slice(-20)),t.tool=null,t.waiting=null;break}case"ask":t.phase="working",t.waiting={tool:Mi(e.tool,40),detail:Mi(e.detail,300)};break;case"ask_done":t.waiting=null;break;case"reply":t.phase="replied",t.tool=null,t.waiting=null,t.endedAt=s;break;case"ended":t.tool=null,t.waiting=null,t.endedAt=s,t.phase=e.text?"replied":"stopped";break;case"lost":t.tool=null,t.waiting=null,t.endedAt=s,t.phase="stopped",t.lost=!0;break;default:return t}return t.last=s,t}function Zx(n){let e=new Set,t=[];for(let s of n&&n.ran||[])s.asked||e.has(s.tool)||(e.add(s.tool),t.push(s.tool));return t}var pL,Mi,Qx=G(()=>{pL=new Set(["replied","failed"]),Mi=(n,e)=>{let t=String(n??"").replace(/\s+/g," ").trim();return t.length>e?t.slice(0,e-1)+"…":t}});import ry from"node:fs";import mL from"node:os";import Ii from"node:path";import gL from"node:crypto";function e1(n,e=process.env){let t=Ii.join(e.XDG_CONFIG_HOME||Ii.join(e.HOME||mL.homedir(),".config"),"checkpoints","live"),s=gL.createHash("sha256").update(Ii.resolve(String(n||"."))).digest("hex").slice(0,12);return Ii.join(t,s)}var yL,wL,Nf,t1=G(()=>{Hn();yL=200;wL=/^[\w.-]{1,120}$/,Nf=class{constructor(e,{keep:t=yL,fsImpl:s=ry}={}){this.dir=e,this.keep=t,this.fs=s}file(e){return wL.test(String(e||""))?Ii.join(this.dir,String(e)+".json"):null}load(e){let t=this.file(e);if(!t)return[];let s;try{s=JSON.parse(this.fs.readFileSync(t,"utf8"))}catch{return[]}return(s&&Array.isArray(s.entries)?s.entries.filter(o=>o&&typeof o=="object"&&typeof o.from=="string"):[]).slice(-this.keep)}save(e,t){let s=this.file(e);if(!s)return!1;let r=(t||[]).slice(-this.keep);return r.length?(this.fs===ry?(ry.mkdirSync(this.dir,{recursive:!0,mode:448}),dn(s,JSON.stringify({v:1,sessionId:e,savedAt:new Date().toISOString(),entries:r})+`
307
307
  `)):(this.fs.mkdirSync(this.dir,{recursive:!0}),this.fs.writeFileSync(s,JSON.stringify({v:1,sessionId:e,entries:r})+`
308
308
  `)),!0):(this.remove(e),!0)}remove(e){let t=this.file(e);if(!t)return!1;try{return this.fs.unlinkSync(t),!0}catch{return!1}}ids(){try{return this.fs.readdirSync(this.dir).filter(e=>e.endsWith(".json")&&!e.startsWith(".")).map(e=>e.slice(0,-5))}catch{return[]}}prune(e){let t=0;for(let s of this.ids())!e(s)&&this.remove(s)&&t++;try{for(let s of this.fs.readdirSync(this.dir))if(s.startsWith(".")&&s.endsWith(".tmp"))try{this.fs.unlinkSync(Ii.join(this.dir,s))}catch{}}catch{}return t}}});import kL from"node:path";function bL(n={}){if(n.by&&typeof n.by=="object")return{device:n.by.device?Mn(n.by.device,60):null,transport:["local","relay","direct","tailscale","peer"].includes(n.by.transport)?n.by.transport:"peer"};let e=String(n.userAgent||""),t=/iPhone/.test(e)?"iPhone":/iPad/.test(e)?"iPad":/Android/.test(e)?"Android phone":null;return n.via==="peer"?{device:n.peer?n.peer+"’s board":"another Mac",transport:"peer"}:n.relay?{device:n.device?Mn(n.device,60):t||"a paired device",transport:n.direct?"direct":"relay"}:n.via==="remote"?{device:Mn(String(n.who||"").replace(/\s*\(through the relay\)\s*$/,""),60)||t||"a signed-in browser",transport:"tailscale"}:{device:"this Mac",transport:"local"}}function vL(n){let e=Qo(n).split(`
309
- `).map(t=>t.trim()).find(Boolean)||"";return e?Mn(e.replace(/\s+/g," "),60):null}function xL(n,e,t){let s=null;try{let i=JSON.parse(String(t||""));i&&typeof i=="object"&&!Array.isArray(i)&&(s=i)}catch{}let r=s?tg(n,s):{verb:n==="Bash"?"Run":"Use "+n,detail:Mn(String(t||"").replace(/\s+/g," "),400),note:null},o=Mn(String(e||"").replace(/\s+/g," "),200);return o&&o!=="Run shell command"&&o!==r.detail&&(r.note=o),r}function AL(n=[],e=[],t=iy){let s=[],r=new Map;for(let l of n){let c=l.from==="claude"&&l.replyTo&&r.get(l.replyTo);if(c){c.entries.push(l);continue}let u={at:l.at,entries:[l]};s.push(u),l.id&&r.set(l.id,u)}for(let l of e)s.push(l);let o=l=>{let c=Date.parse(l.at||"");return Number.isFinite(c)?c:0},i=s.map((l,c)=>[l,c]).sort((l,c)=>o(l[0])-o(c[0])||l[1]-c[1]).map(l=>l[0]),a=i.slice(-t);return{messages:a.flatMap(l=>l.entries),earlier:i.length-a.length}}function _L(n,e){try{return!!Zn(e).id}catch{return!1}}var Nn,Mn,SL,oy,lr,iy,Mf,n1=G(()=>{Ni();no();Dl();we();Qx();Tu();t1();Nn=n=>n?new Date(n).toISOString():null,Mn=(n,e)=>{let t=String(n??"").trim();return t.length>e?t.slice(0,e-1)+"…":t},SL=/^[a-km-z]{5}$/,oy=new Set(["replied","failed"]);lr=n=>/^\s*<channel\b/.test(String(n||""));iy=20;Mf=class{constructor(e,{takeMs:t,listenMs:s,sweepMs:r=5e3,tailMs:o=1500,eventKey:i=null,eventLimit:a=null,dir:l=null,store:c=null,saveMs:u=300}={}){this.e=e,this.store=c||new Nf(l||e1(e.root)),this.saveMs=u,this.saveTimers=new Map,this.hydrated=new Set,this.histories=new Map,this.tailMs=o,this.tails=new Map,this.counts=new Map,this.tailTimer=null,this.hub=new jl({now:e.now,...t?{takeMs:t}:{},...s?{listenMs:s}:{}}),this.sweepMs=r,this.logs=new Map,this.forwarded=new Map,this.seenSigs=new Map,this.eventKey=i||(()=>Oi()),this.eventLimit=sy(a||{}),this.timer=null,this.hub.on("up",d=>this.onUpDown(d,!0)),this.hub.on("down",d=>this.onUpDown(d,!1)),this.hub.on("settled",d=>this.onSettled(d))}start(){if(this.timer)return;try{let t=this.store.prune(s=>{let r=this.e.sessions.get(s);return!!r&&!r.archived});t&&this.e.log("live: removed "+t+" saved thread"+(t===1?"":"s")+" of archived sessions")}catch(t){this.e.log("live: tidying saved threads failed: "+t.message)}let e=new Set;try{e=new Set(this.store.ids())}catch{}for(let t of this.e.sessions.values())this.e.isMine(t)&&!t.wasLive&&(t.liveLink||e.has(t.id))&&(t.wasLive=!0,this.e.persist(t,!0));for(let t of this.e.sessions.values())this.e.isMine(t)&&t.liveLink&&this.mark(t,null);this.timer=setInterval(()=>{try{this.hub.sweep()}catch{}},this.sweepMs),this.timer.unref&&this.timer.unref(),this.tailTimer=setInterval(()=>{try{this.tailAll()}catch(t){this.e.log("live: transcript read failed: "+t.message)}},this.tailMs),this.tailTimer.unref&&this.tailTimer.unref()}stop(){this.timer&&clearInterval(this.timer),this.timer=null,this.tailTimer&&clearInterval(this.tailTimer),this.tailTimer=null,this.flush()}logOf(e){let t=String(e||"");if(!this.hydrated.has(t)&&(this.hydrated.add(t),!this.logs.has(t))){let s=[];try{s=this.store.load(t)}catch{}if(s.length){this.logs.set(t,s);try{this.settleOrphans(t)}catch(r){this.e.log("live: settling the saved thread failed: "+r.message)}}}return this.logs.get(t)||[]}setLog(e,t){this.logs.set(e,t.slice(-Tf)),this.dirty(e)}dirty(e){if(!e||this.saveTimers.has(e))return;let t=setTimeout(()=>{this.saveTimers.delete(e),this.save(e)},this.saveMs);t.unref&&t.unref(),this.saveTimers.set(e,t)}save(e){let t=this.e.sessions.get(e);if(!(t&&t.archived))try{this.store.save(e,this.logs.get(e)||[])}catch(s){this.e.log("live: saving the thread failed: "+s.message)}}flush(){for(let[e,t]of[...this.saveTimers])clearTimeout(t),this.saveTimers.delete(e),this.save(e)}forget(e){let t=this.saveTimers.get(e);t&&(clearTimeout(t),this.saveTimers.delete(e)),this.logs.delete(e),this.hydrated.delete(e),this.histories.delete(e);try{this.store.remove(e)}catch{}}settleOrphans(e){let t=this.logs.get(e)||[],s=this.e.sessions.get(e),r=t.filter(l=>l.msg&&!this.hub.get(l.msg)&&(l.from==="event"&&(l.state||"sent")==="sent"||l.from!=="claude"&&l.from!=="event"&&l.status&&!oy.has(l.status.phase)&&l.status.phase!=="stopped"));if(!r.length)return;let o=s&&s.transcriptPath&&s.capture!=="codex"?em(s.transcriptPath):[],i=this.e.now(),a=!!(s&&!s.endedAt&&s.status==="active");for(let l of r){let c=o.length?Zp(o,l.msg):null;if(l.from==="event"){l.state=c&&c.found?"received":"failed",l.state==="failed"&&(l.error="lost_in_restart");continue}if(c&&c.found){if(l.state="received",l.status.phase==="sent"&&(l.status=lt(l.status,{type:"delivered",at:i})),!c.ended&&a){l.status=lt(l.status,{type:"started",at:i}),s&&this.ensureTail(s);continue}let u=c.final||"";l.status=lt(l.status,u?{type:"ended",text:u,at:i}:{type:"lost",at:i}),u&&s&&this.upsertAnswer(s,l,{text:u,via:"transcript",done:!0})}else l.status=lt(l.status,{type:"lost",at:i})}this.dirty(e)}sessionForPids(e=[]){let t=new Set((e||[]).map(String)),s=null;for(let r of this.e.sessions.values())!this.e.isMine(r)||r.endedAt||r.pid==null||!t.has(String(r.pid))||(!s||String(r.lastEventAt||r.startedAt||"")>String(s.lastEventAt||s.startedAt||""))&&(s=r);return s}chanOf(e){if(!e||!this.e.isMine(e)||e.pid==null||e.endedAt)return null;let t=this.hub.find(e.pid);return t?this.newerOnPid(e)?null:t:null}newerOnPid(e){for(let t of this.e.sessions.values())if(!(t===e||t.endedAt||t.pid==null||String(t.pid)!==String(e.pid)||!this.e.isMine(t))&&String(t.lastEventAt||t.startedAt||"")>String(e.lastEventAt||e.startedAt||""))return t;return null}follow(e,t){if(!this.e.isMine(e)||e.pid==null||e.endedAt||e.nonInteractive)return;let s=t.data||{};if(!(t.type==="session.started"||t.type==="turn_started"||t.type==="turn_completed"||t.type==="activity"&&(s.kind==="prompt"||s.kind==="tool")))return;let o=this.hub.find(e.pid);if(!(!o||!this.hub.listening(o.pid))&&!this.newerOnPid(e)){for(let i of[...this.e.sessions.values()])i===e||i.endedAt||i.pid==null||String(i.pid)!==String(e.pid)||!this.e.isMine(i)||this.retire(i);(!e.liveLink||!e.liveLink.on)&&this.mark(e,{on:!0,permission:!!o.permission,since:Nn(o.since)})}}retire(e){return e.liveLink&&this.mark(e,null),!dt(e,(this.e.checkpoints.get(e.id)||[]).length)||e.transcriptTurns>0?!1:(e.endedAt=Nn(this.e.now()),e.endReason="replaced",e.inTurn=!1,this.e.persist(e,!0),this.e.log("live: closed empty session "+String(e.id).slice(0,8)+" (its process went on as another session)"),this.e.emit("session.updated",this.e.publicSession(e)),!0)}isLive(e){let t=this.chanOf(e);return!!t&&this.hub.listening(t.pid)}publicOf(e){if(this.e.isMine(e)){let s=this.chanOf(e);if(!s||!this.hub.listening(s.pid))return null;let r=[...this.logOf(e.id)].reverse().find(o=>o.from==="claude");return{on:!0,permission:!!s.permission,since:Nn(s.since),lastReplyAt:r?r.at:null}}let t=e.liveLink;return t&&t.on&&!e.endedAt?{on:!0,permission:!!t.permission,since:t.since||null}:null}onUpDown(e,t){let s=this.sessionForPids(e.pids);s&&this.mark(s,t?{on:!0,permission:!!e.permission,since:Nn(e.since)}:null)}mark(e,t){let s=JSON.stringify(e.liveLink||null);e.liveLink=t;let r=!!(t&&t.on&&!e.wasLive);r&&(e.wasLive=!0),!(JSON.stringify(t||null)===s&&!r)&&(this.e.log("live: "+(t?"linked":"unlinked")+" session "+String(e.id).slice(0,8)),this.e.persist(e,!0),this.e.emit("session.updated",this.e.publicSession(e)))}addLog(e,t){this.setLog(e,[...this.logOf(e),{at:Nn(this.e.now()),...t}]);let s=this.e.sessions.get(e);return s&&this.e.emit("session.updated",this.e.publicSession(s)),t}publicEntry(e){let t=e.msg?this.hub.get(e.msg):null,s=t?t.state:e.state||null,r=t?t.error:e.error||null;return{id:e.msg||e.id,from:e.from,text:e.text,at:e.at,where:e.where||null,by:e.by||null,source:e.source||null,replyTo:e.replyTo||null,via:e.via||null,...e.from==="claude"?{done:e.done!==!1,ranWithoutAsking:e.ran||[]}:{state:s,error:r,errorText:s==="failed"?Pl(r):null,status:e.status||null}}}onSettled(e){if(e.kind==="verdict"){let r=e.askId?this.e.asks.get(e.askId):null;r&&e.state==="received"&&(r.delivered=!0);return}if(!e.sessionId)return;let t=this.logOf(e.sessionId).find(r=>r.msg===e.id);t&&(t.state=e.state,t.error=e.error||null,this.dirty(e.sessionId)),t&&t.status&&(t.status=lt(t.status||Of(Date.parse(t.at)||this.e.now()),{type:e.state==="received"?"delivered":"failed",at:this.e.now(),error:e.error}));let s=this.e.sessions.get(e.sessionId);s&&this.e.emit("session.updated",this.e.publicSession(s))}async view(e,{turns:t=iy}={}){let s=this.e.sessions.get(e);if(!s)throw C(404,"not_found","no such session");let r=Math.max(1,Math.min(Tf+500,Math.floor(Number(t))||iy));if(!this.e.isMine(s)){let c=await this.e.viaPeer(s.machine,"session.live.get",{sessionId:e,turns:r},"Reading that session",{since:"0.9.8"});return{...c.result,peer:s.machine,peerVia:c.via}}try{this.tailOne(s)}catch{}let o=this.publicOf(s),i=this.logOf(e).map(c=>this.publicEntry(c)),a=[];try{a=this.historyOf(s)}catch(c){this.e.log("live: reading the transcript failed: "+c.message)}let l=AL(i,a,r);return{sessionId:e,machine:this.e.machine,live:o,messages:l.messages,earlier:l.earlier,turns:r}}historyOf(e){if(!e.transcriptPath||e.capture==="codex")return[];let t=this.histories.get(e.id);(!t||t.file!==e.transcriptPath)&&(t=new Au(e.transcriptPath),this.histories.set(e.id,t)),t.read();let s=new Set(this.logOf(e.id).map(r=>r.msg).filter(Boolean));return oS(t.turns,{skipIds:s})}async send(e,t,s={}){this.e.assertNotAgent(e,"sending to a session"),this.e.assertMayAnswer(s);let r=this.e.sessions.get(e);if(!r)throw C(404,"not_found","no such session");let o=Ci(t),i=bL(s);if(!this.e.isMine(r)){let c=await this.e.viaPeer(r.machine,"session.live",{sessionId:r.id,text:o,by:i},"Sending to that session",{since:"0.9.8"});return{...c.result,peer:r.machine,peerVia:c.via}}if(r.endedAt)throw C(409,"closed","That session is closed.");if(!this.isLive(r))throw C(409,"not_live","This session isn’t live-linked. Start Claude Code with `leftoff live` to send to it from here.");let a=nr(s);this.ensureTail(r);let l=this.hub.put(r.pid,{kind:"prompt",text:o,by:{where:a,via:s.via||"loopback"},sessionId:r.id});return this.nameFromPrompt(r,o),this.addLog(r.id,{msg:l.id,from:"you",text:o,where:a,by:i,state:l.state,error:l.error||null,status:lt(Of(this.e.now()),l.state==="failed"?{type:"failed",error:l.error,at:this.e.now()}:{type:"noop"})}),this.e.audit(r.id,{at:Nn(l.at),kind:"live",tool:null,what:"live link",summary:"Sent: “"+Mn(o,80)+"”",action:"live",where:a,via:s.via||"loopback",who:s.who||null}),{ok:!0,id:l.id,state:l.state,error:l.error,sessionId:r.id}}hello(e={}){let t=String(e.pid||"");if(!/^\d{1,10}$/.test(t))throw C(400,"bad_pid","pid: the Claude Code process id");let s=this.hub.hello(t,{pids:Array.isArray(e.pids)?e.pids:[],permission:e.permission===!0,version:e.version}),r=this.sessionForPids(s.pids);return r&&this.mark(r,{on:!0,permission:!!s.permission,since:Nn(s.since)}),{ok:!0,sessionId:r?r.id:null,machine:this.e.machine}}async take(e,t,s){let r=await this.hub.take(String(e),t,s);if(!r)return{msg:null};let o=r.kind==="event"?r.by&&r.by.from||"webhook":r.kind==="prompt"?"leftoff":null;return{msg:{id:r.id,kind:r.kind,text:r.text,meta:r.meta||{},from:o,verdict:r.verdict||null}}}ack(e={}){return{ok:!0,state:this.hub.ack(String(e.pid||""),e.id,{ok:e.ok!==!1,error:e.error||null}).state}}reply(e={}){let t=this.sessionForPids([e.pid,...Array.isArray(e.pids)?e.pids:[]].filter(Boolean));if(!t)throw C(404,"no_session","Leftoff doesn’t know this session yet");let s=Ci(e.text),r=e.replyTo?String(e.replyTo).slice(0,40):null,o=this.pendingFor(t.id,r);if(o){o.replyText=s;let i=this.turnOf(t,o),a=Qp({transcript:i&&i.final,reply:s});return this.upsertAnswer(t,o,{text:a.text,via:a.via,done:a.via==="reply"||!this.tails.has(t.id)||!!(i&&i.ended)}),o.status=lt(o.status,{type:this.tails.has(t.id)?"text":"reply",at:this.e.now()}),this.touch(t.id),{ok:!0,sessionId:t.id}}return this.addLog(t.id,{id:"cr_"+Date.now().toString(36),from:"claude",via:"reply",text:s,replyTo:r}),{ok:!0,sessionId:t.id}}permission(e={}){let t=String(e.requestId||"");if(!SL.test(t))throw C(400,"bad_request_id","request_id: five letters a-z (no l)");let s=this.sessionForPids([e.pid,...Array.isArray(e.pids)?e.pids:[]].filter(Boolean));if(!s)throw C(404,"no_session","Leftoff doesn’t know this session yet; the terminal decides");if(this.e.cfg.answers&&this.e.cfg.answers.enabled===!1)return{id:null,state:"gone",reason:"answering_off"};let r=Mn(e.tool,80)||"a tool",o=xL(r,e.description,e.inputPreview),i=typeof e.cwd=="string"&&kL.isAbsolute(e.cwd)?e.cwd:s.cwd||null,a=this.e.asks.openChannel({sessionId:s.id,requestId:t,pid:String(s.pid),tool:r,describe:o,preview:String(e.inputPreview||"").slice(0,2e4),projectDir:i});return{id:a.id,state:a.state}}verdict(e){if(!e.channel||e.state!=="answered"||!e.decision)return null;let t=e.decision.behavior==="allow"?"allow":"deny",s=this.hub.put(e.channel.pid,{kind:"verdict",verdict:{requestId:e.channel.requestId,behavior:t},sessionId:e.sessionId});return s.askId=e.id,s.state==="failed"&&this.e.log("live: verdict for "+e.channel.requestId+" not delivered ("+s.error+")"),s}channelAlive(e){return!!(e&&e.channel&&this.hub.listening(e.channel.pid))}onIngest(e,t){if(!this.e.isMine(e))return;if(t.type==="session.opened"&&e.pid!=null&&this.hub.listening(e.pid)){let i=this.hub.find(e.pid);this.mark(e,{on:!0,permission:!!i.permission,since:Nn(i.since)})}t.type==="session.ended"&&e.liveLink&&(this.mark(e,null),this.tails.delete(e.id),this.counts.delete(e.id));let s=t.data||{};this.follow(e,t),t.type==="activity"&&s.kind==="tool"&&s.phase==="post"&&s.tool&&this.e.asks.settleSession(e.id,"answered_in_terminal",{tool:String(s.tool),channelOnly:!0}),t.type==="activity"&&s.kind==="prompt"&&s.prompt&&(this.isLive(e)||e.liveLink||$r(s.prompt))&&this.nameFromPrompt(e,s.prompt),this.isLive(e)&&(t.type==="turn_completed"||t.type==="activity"&&s.kind==="prompt")&&this.countTurns(e);let r=this.pendingFor(e.id);if(!r)return;let o=this.e.now();t.type==="turn_started"||t.type==="activity"&&s.kind==="prompt"?(r.status=lt(r.status,{type:"started",at:o}),this.touch(e.id)):t.type==="activity"&&s.kind==="tool"&&s.tool&&!this.tails.has(e.id)?(r.status=lt(r.status,{type:s.phase==="pre"?"tool":"tool_done",tool:s.tool,at:o,asked:this.askedFor(e.id,s.tool,r)}),this.touch(e.id)):t.type==="turn_completed"&&this.finishTurn(e,r,s.lastAssistantPreview||"")}onAsk(e,t){if(!e||e.kind!=="permission")return;let s=this.e.sessions.get(e.sessionId);if(!s||!this.e.isMine(s))return;let r=this.pendingFor(s.id);if(!r)return;let o=e.describe||{};r.status=lt(r.status,t==="opened"?{type:"ask",tool:e.tool,detail:o.detail||"",at:this.e.now()}:{type:"ask_done",at:this.e.now()}),this.dirty(s.id)}pendingFor(e,t=null){let s=this.logOf(e);if(t){let r=s.find(o=>o.msg===t&&o.from!=="claude");if(r)return r}return s.find(r=>r.from!=="claude"&&r.msg&&r.status&&!oy.has(r.status.phase)&&r.status.phase!=="stopped")||null}touch(e){this.dirty(e);let t=this.e.sessions.get(e);t&&this.e.emit("session.updated",this.e.publicSession(t))}nameFromPrompt(e,t){if(e.nonInteractive)return!1;let s=Qo(t);if(!s)return!1;let r=!1;if((!e.firstPrompt||lr(e.firstPrompt))&&(e.firstPrompt=Mn(s,300),r=!0),!e.title||lr(e.title)){let o=vL(s);o&&(e.title=o,e.titleSource="prompt",e.titleChangedAt=Nn(this.e.now()),r=!0,this.e.log("live: named session "+String(e.id).slice(0,8)+" from its first prompt"))}return r&&this.e.persist(e,!0),r}countTurns(e,{lines:t=null}={}){if(!e.transcriptPath||e.capture==="codex")return!1;let s=this.counts.get(e.id);if((!s||s.file!==e.transcriptPath)&&(s={file:e.transcriptPath,seen:new Set,stats:{prompts:0,firstPrompt:null}},this.counts.set(e.id,s),t=em(e.transcriptPath)),!t||!t.length)return this.applyCount(e,s);let r=t.filter(o=>{let i=o&&(o.uuid||(o.timestamp?o.type+":"+o.timestamp:null));return i?s.seen.has(i)?!1:(s.seen.add(i),!0):!0});return s.seen.size>2e4&&(s.seen=new Set([...s.seen].slice(-1e4))),rS(r,s.stats),this.applyCount(e,s)}applyCount(e,t){let s=!1;return t.stats.prompts>(e.transcriptTurns||0)&&(e.transcriptTurns=t.stats.prompts,s=!0),t.stats.firstPrompt&&(!e.title||lr(e.title)||!e.firstPrompt||lr(e.firstPrompt))&&(s=this.nameFromPrompt(e,t.stats.firstPrompt)||s),s&&this.e.persist(e,!0),s}ensureTail(e){if(!e.transcriptPath||e.capture==="codex")return null;let t=this.tails.get(e.id);return(!t||t.file!==e.transcriptPath)&&(t=new Ka(e.transcriptPath),this.tails.set(e.id,t)),t}turnOf(e,t){let s=this.tails.get(e.id);return!s||!t||!t.msg?null:Zp(s.lines,t.msg)}askedFor(e,t,s){let r=Date.parse(s&&s.at)||0;return[...this.e.asks.items.values()].some(o=>o.sessionId===e&&o.tool===t&&o.kind==="permission"&&o.createdAt>=r-1e3)}upsertAnswer(e,t,{text:s,via:r,done:o}){if(!s)return null;let i=this.logOf(e.id),a="ans_"+t.msg,l=i.find(c=>c.id===a);return l||(l={at:Nn(this.e.now()),id:a,from:"claude",replyTo:t.msg},this.setLog(e.id,[...i,l])),this.dirty(e.id),l.text=Mn(s,6e4),l.via=r,l.done=o!==!1,l.ran=Zx(t.status),l}finishTurn(e,t,s=""){t.status.phase==="sent"&&(t.status=lt(t.status,{type:"delivered",at:this.e.now()}));let r=this.tails.get(e.id);if(r)try{r.read()}catch{}let o=this.turnOf(e,t);o&&this.applyTurn(e,t,o);let i=Qp({transcript:o&&o.final,reply:t.replyText,preview:s});t.status=lt(t.status,{type:"ended",text:i.text,at:this.e.now()}),i.text&&this.upsertAnswer(e,t,{text:i.text,via:i.via,done:!0}),this.touch(e.id)}applyTurn(e,t,s){if(!s.found)return!1;let r=this.e.now(),o=!1;t.seenTools=t.seenTools||{},(t.status.phase==="received"||t.status.phase==="sent")&&(t.status=lt(t.status,{type:"delivered",at:r}),t.status=lt(t.status,{type:"started",at:r}),o=!0);for(let l of s.blocks){if(l.type!=="tool")continue;t.seenTools[l.id]||(t.seenTools[l.id]="run",t.status=lt(t.status,{type:"tool",tool:l.name,detail:l.detail,at:r}),o=!0),l.done&&t.seenTools[l.id]!=="done"&&(t.seenTools[l.id]="done",t.status=lt(t.status,{type:"tool_done",tool:l.name,detail:l.detail,asked:this.askedFor(e.id,l.name,t),at:r}),o=!0)}let i=s.final||s.latest,a=this.logOf(e.id).find(l=>l.id==="ans_"+t.msg);return i&&(!a||a.text!==i)&&(t.status=lt(t.status,{type:"text",at:r}),this.upsertAnswer(e,t,{text:i,via:"transcript",done:!1}),o=!0),s.ended&&!oy.has(t.status.phase)?(this.finishTurn(e,t),!0):o}tailAll(){for(let e of this.e.sessions.values())this.tailOne(e)}tailOne(e){if(!this.e.isMine(e)||e.endedAt)return!1;let t=this.pendingFor(e.id);if(!t&&!this.tails.has(e.id)&&!this.isLive(e))return!1;let s=this.ensureTail(e);if(!s)return!1;let r=s.read(),o=!1,i=sS(r);return i&&i!==e.title&&!_L(this.e,i)&&(e.title=Mn(i,120),e.titleSource="custom",e.titleChangedAt=Nn(this.e.now()),this.e.persist(e,!0),this.e.log("live: session "+String(e.id).slice(0,8)+" renamed in Claude Code"),o=!0),t&&r.length&&(o=this.applyTurn(e,t,this.turnOf(e,t))||o),this.isLive(e)&&(r.length||!this.counts.has(e.id))&&(o=this.countTurns(e,{lines:r})||o),o&&this.touch(e.id),o}async event({raw:e="",authorization:t="",signature:s="",relayed:r=!1}={}){let o=this.eventKey(),i=Cf({authorization:t,signature:s,raw:e,key:o,relayed:r,now:this.e.now(),seen:this.seenSigs});if(!i.ok)throw C(i.status,i.code,i.message);if(!this.eventLimit.take("events"))throw C(429,"rate_limited","Too many events; at most 10 in a row, then one every 6 seconds.");let a;try{a=JSON.parse(e||"{}")}catch{throw C(400,"bad_json","body is not valid JSON")}let l=ty(a),c=[...this.e.sessions.values()].filter(f=>!f.endedAt&&!f.nonInteractive&&(this.e.isMine(f)?this.isLive(f):!!(f.liveLink&&f.liveLink.on))),u=ny(c.map(f=>({...this.e.publicSession(f),_s:f})),l.session);if(u.error==="ambiguous")throw C(409,"ambiguous",'"'+l.session+'" matches '+u.matches.length+" live sessions: "+u.matches.slice(0,5).map(f=>(f.title||f.id.slice(0,8))+" ("+f.id.slice(0,8)+")").join(", ")+". Use the id.");if(!u.session)throw C(404,"no_live_session",'No live-linked session matches "'+l.session+'". Start it with `leftoff live`.');let d=u.session._s;if(!this.e.isMine(d)){let f=await this.e.viaPeer(d.machine,"live.event",{sessionId:d.id,text:l.text,from:l.from,meta:l.meta},"Sending to that session",{since:"0.9.8"});return f.result&&f.result.id&&(this.forwarded.set(f.result.id,d.machine),this.forwarded.size>500&&this.forwarded.delete(this.forwarded.keys().next().value)),{...f.result,machine:d.machine}}return this.deliverEvent(d,l,{via:r?"relay":"local",how:i.how})}deliverEvent(e,t,{via:s="local"}={}){if(!this.isLive(e))throw C(409,"not_live","That session isn’t live-linked any more.");let r=this.hub.put(e.pid,{kind:"event",text:t.text,meta:t.meta,by:{from:t.from,via:s},sessionId:e.id});return this.addLog(e.id,{msg:r.id,from:"event",source:t.from,text:t.text,state:r.state,error:r.error||null}),this.e.log("live: event from "+t.from+" ("+s+") for session "+String(e.id).slice(0,8)+" → "+r.state),{ok:!0,id:r.id,state:r.state,error:r.error,sessionId:e.id,machine:this.e.machine}}async peerEvent(e={}){let t=this.e.sessions.get(String(e.sessionId||""));if(!t||!this.e.isMine(t))throw C(404,"not_found","no such session on "+this.e.machine);return this.deliverEvent(t,{text:Ci(e.text),from:String(e.from||"webhook").slice(0,60),meta:e.meta&&typeof e.meta=="object"?e.meta:{}},{via:"peer"})}async eventState(e,t={}){let s=this.eventKey(),r=Cf({...t,raw:"",key:s,now:this.e.now(),seen:new Map});if(!r.ok)throw C(r.status,r.code,r.message);let o=this.hub.get(e);if(o&&o.kind==="event")return{id:o.id,state:o.state,error:o.error,errorText:o.state==="failed"?Pl(o.error):null,machine:this.e.machine};let i=this.forwarded.get(String(e));if(i)return{...(await this.e.viaPeer(i,"live.event.get",{id:e},"Checking that event",{since:"0.9.8"})).result,machine:i};throw C(404,"not_found","no such event (they are kept in memory, since the daemon started)")}peerEventState(e){let t=this.hub.get(e);if(!t||t.kind!=="event")throw C(404,"not_found","no such event");return{id:t.id,state:t.state,error:t.error,errorText:t.state==="failed"?Pl(t.error):null}}}});import TL from"node:crypto";var EL,s1,ay,If,r1=G(()=>{no();Dl();Pt();EL=300*1e3,s1=3,ay=n=>n?new Date(n).toISOString():null,If=class{constructor(e,{ttlMs:t=EL,burst:s=3,everyMs:r=6e4}={}){this.e=e,this.ttlMs=t,this.items=new Map,this.bucket={tokens:s,last:null,burst:s,everyMs:r}}stop(){}take(){let e=this.bucket,t=this.e.now();return e.last==null&&(e.last=t),e.tokens=Math.min(e.burst,e.tokens+(t-e.last)/e.everyMs),e.last=t,e.tokens<1?!1:(e.tokens-=1,!0)}sweep(){let e=this.e.now();for(let t of this.items.values())t.state==="pending"&&e>=t.expiresAt&&(t.state="expired",t.settledAt=e,this.changed(t));if(this.items.size>50)for(let[t,s]of this.items){if(this.items.size<=50)break;s.state!=="pending"&&this.items.delete(t)}}changed(e){this.e.emit("restore.updated",{ask:e.id,state:e.state})}publicOf(e){return{id:e.id,machine:this.e.machine,state:e.state,count:e.ids.length,titles:e.titles,who:e.who,askedAt:ay(e.at),expiresAt:ay(e.expiresAt),settledAt:ay(e.settledAt),...e.result?{result:e.result}:{},...e.error?{error:e.error}:{}}}async request(e={},t={}){let s=e.machine?String(e.machine):this.e.machine;if(s!==this.e.machine){let r=await this.e.viaPeer(s,"restore.ask",{ids:e.ids,who:t.who||nr(t)},"Restore on "+s,{since:"0.9.8"});return{...r.result,machine:s,peerVia:r.via}}return this.ask(e,t)}ask(e={},t={}){if(!Array.isArray(e.ids)||!e.ids.length)throw C(400,"bad_request","ids: the session ids to restore");this.sweep();let s=this.e.restoreTargets(e.ids);if(!s.length)throw C(409,"nothing_to_restore","None of those sessions can be restored now (already open, or restored).");if([...this.items.values()].filter(l=>l.state==="pending").length>=s1)throw C(429,"too_many","There are already "+s1+" Restore requests waiting on "+this.e.machine+".");if(!this.take())throw C(429,"slow_down","too many Restore requests; wait a minute");let r=this.e.now(),o=String(e.who||t.who||(t.via==="peer"?t.peer:null)||nr(t)||"another device").slice(0,120),i={id:"ra_"+TL.randomBytes(9).toString("base64url"),ids:s.map(l=>l.id),titles:s.slice(0,5).map(l=>l.title||String(l.id).slice(0,8)),who:o,via:t.via||"remote",at:r,expiresAt:r+this.ttlMs,state:"pending",settledAt:null,result:null,error:null};this.items.set(i.id,i);let a=i.ids.length;try{this.e.notifier({title:Ge,subtitle:"Restore "+a+" session"+(a===1?"":"s")+" on this Mac?",message:"Asked from "+o+". Confirm on this Mac’s board (Leftoff → the restore card).",sessionId:"restore"})}catch(l){this.e.log("restore ask: notification: "+l.message)}return this.e.log("restore: "+a+" asked from "+o+" ("+i.id+"), waiting for a confirm here"),this.changed(i),this.publicOf(i)}get(e){this.sweep();let t=this.items.get(String(e||""));if(!t)throw C(404,"not_found","That Restore request is gone (the daemon restarted?)");return this.publicOf(t)}async status(e,t){return t&&t!==this.e.machine?{...(await this.e.viaPeer(t,"restore.ask.get",{id:e},"Checking on "+t,{since:"0.9.8"})).result,machine:t}:this.get(e)}pending(){return this.sweep(),[...this.items.values()].filter(e=>e.state==="pending").map(e=>this.publicOf(e))}async confirm(e){this.sweep();let t=this.items.get(String(e||""));if(!t)throw C(404,"not_found","That Restore request is gone");if(t.state!=="pending")throw C(409,"not_pending","That Restore request was already "+t.state+".");t.state="confirming";try{let s=await this.e.restore(t.ids);t.state="confirmed",t.result={how:s.how||null,opened:s.opened||0,failed:s.failed||0,message:s.message||null}}catch(s){t.state="failed",t.error=s.message}return t.settledAt=this.e.now(),this.changed(t),this.publicOf(t)}decline(e){this.sweep();let t=this.items.get(String(e||""));if(!t)throw C(404,"not_found","That Restore request is gone");if(t.state!=="pending")throw C(409,"not_pending","That Restore request was already "+t.state+".");return t.state="declined",t.settledAt=this.e.now(),this.e.log("restore: declined "+t.id+" (asked from "+t.who+")"),this.changed(t),this.publicOf(t)}}});var cy={};ot(cy,{BIN:()=>$e,HOOK_EVENTS:()=>o1,HOOK_OPTS:()=>i1,LEGACY_BIN:()=>NL,claudeSettingsPath:()=>Me,hasSettingsHooks:()=>Li,hookCommand:()=>l1,installSettingsHooks:()=>co,legacyServiceFile:()=>uo,legacyServiceStopCommands:()=>d1,migrateLegacyService:()=>Bl,readClaudeSettings:()=>Ul,serviceContent:()=>Wn,serviceEnableHint:()=>ly,serviceFile:()=>yt,servicePlatform:()=>zn,serviceRoot:()=>jf,serviceStartCommands:()=>In,setSettingsEnv:()=>ji,settingsHooksBlock:()=>c1,uninstallSettingsHooks:()=>Hl});import et from"node:fs";import cr from"node:os";import Ye from"node:path";import{fileURLToPath as CL}from"node:url";import{spawnSync as OL}from"node:child_process";function Me(){return process.env.CLAUDE_SETTINGS_PATH||Ye.join(process.env.CLAUDE_CONFIG_DIR||Ye.join(cr.homedir(),".claude"),"settings.json")}function Lf(n){return/^[A-Za-z0-9_./-]+$/.test(n)?n:JSON.stringify(n)}function l1(n,e=$e){return Lf(process.execPath)+" "+Lf(e)+" hook "+n+" --tag="+ML}function c1(n=$e){let e={};for(let t of o1)e[t]=[{hooks:[{type:"command",command:l1(t,n),timeout:5,...i1[t]||{}}]}];return e}function Ul(n=Me()){try{let e=et.readFileSync(n,"utf8");return e.trim()?JSON.parse(e):{}}catch{return null}}function Li(n=Me()){let e=Ul(n),t=e?JSON.stringify(e.hooks||{}):"";return a1.some(s=>t.includes(s))}function ji(n,e,t=Me()){let s=Ul(t),r=s||{},o=r.env?r.env[n]:void 0;return e==null&&o===void 0||o===e?!1:(s&&et.existsSync(t)?et.writeFileSync(t+".leftoff-backup-"+new Date().toISOString().replace(/[:.]/g,"-"),et.readFileSync(t,"utf8")):et.mkdirSync(Ye.dirname(t),{recursive:!0}),e==null?(delete r.env[n],Object.keys(r.env).length||delete r.env):r.env={...r.env||{},[n]:e},et.writeFileSync(t,JSON.stringify(r,null,2)+`
309
+ `).map(t=>t.trim()).find(Boolean)||"";return e?Mn(e.replace(/\s+/g," "),60):null}function xL(n,e,t){let s=null;try{let i=JSON.parse(String(t||""));i&&typeof i=="object"&&!Array.isArray(i)&&(s=i)}catch{}let r=s?tg(n,s):{verb:n==="Bash"?"Run":"Use "+n,detail:Mn(String(t||"").replace(/\s+/g," "),400),note:null},o=Mn(String(e||"").replace(/\s+/g," "),200);return o&&o!=="Run shell command"&&o!==r.detail&&(r.note=o),r}function AL(n=[],e=[],t=iy){let s=[],r=new Map;for(let l of n){let c=l.from==="claude"&&l.replyTo&&r.get(l.replyTo);if(c){c.entries.push(l);continue}let u={at:l.at,entries:[l]};s.push(u),l.id&&r.set(l.id,u)}for(let l of e)s.push(l);let o=l=>{let c=Date.parse(l.at||"");return Number.isFinite(c)?c:0},i=s.map((l,c)=>[l,c]).sort((l,c)=>o(l[0])-o(c[0])||l[1]-c[1]).map(l=>l[0]),a=i.slice(-t);return{messages:a.flatMap(l=>l.entries),earlier:i.length-a.length}}function _L(n,e){try{return!!Zn(e).id}catch{return!1}}var Nn,Mn,SL,oy,lr,iy,Mf,n1=G(()=>{Ni();no();Dl();we();Qx();Tu();t1();Nn=n=>n?new Date(n).toISOString():null,Mn=(n,e)=>{let t=String(n??"").trim();return t.length>e?t.slice(0,e-1)+"…":t},SL=/^[a-km-z]{5}$/,oy=new Set(["replied","failed"]);lr=n=>/^\s*<channel\b/.test(String(n||""));iy=20;Mf=class{constructor(e,{takeMs:t,listenMs:s,sweepMs:r=5e3,tailMs:o=1500,eventKey:i=null,eventLimit:a=null,dir:l=null,store:c=null,saveMs:u=300}={}){this.e=e,this.store=c||new Nf(l||e1(e.root)),this.saveMs=u,this.saveTimers=new Map,this.hydrated=new Set,this.histories=new Map,this.tailMs=o,this.tails=new Map,this.counts=new Map,this.tailTimer=null,this.hub=new jl({now:e.now,...t?{takeMs:t}:{},...s?{listenMs:s}:{}}),this.sweepMs=r,this.logs=new Map,this.forwarded=new Map,this.seenSigs=new Map,this.eventKey=i||(()=>Oi()),this.eventLimit=sy(a||{}),this.timer=null,this.hub.on("up",d=>this.onUpDown(d,!0)),this.hub.on("down",d=>this.onUpDown(d,!1)),this.hub.on("settled",d=>this.onSettled(d))}start(){if(this.timer)return;try{let t=this.store.prune(s=>{let r=this.e.sessions.get(s);return!!r&&!r.archived});t&&this.e.log("live: removed "+t+" saved thread"+(t===1?"":"s")+" of archived sessions")}catch(t){this.e.log("live: tidying saved threads failed: "+t.message)}let e=new Set;try{e=new Set(this.store.ids())}catch{}for(let t of this.e.sessions.values())this.e.isMine(t)&&!t.wasLive&&(t.liveLink||e.has(t.id))&&(t.wasLive=!0,this.e.persist(t,!0));for(let t of this.e.sessions.values())this.e.isMine(t)&&t.liveLink&&this.mark(t,null);this.timer=setInterval(()=>{try{this.hub.sweep()}catch{}},this.sweepMs),this.timer.unref&&this.timer.unref(),this.tailTimer=setInterval(()=>{try{this.tailAll()}catch(t){this.e.log("live: transcript read failed: "+t.message)}},this.tailMs),this.tailTimer.unref&&this.tailTimer.unref()}stop(){this.timer&&clearInterval(this.timer),this.timer=null,this.tailTimer&&clearInterval(this.tailTimer),this.tailTimer=null,this.flush()}logOf(e){let t=String(e||"");if(!this.hydrated.has(t)&&(this.hydrated.add(t),!this.logs.has(t))){let s=[];try{s=this.store.load(t)}catch{}if(s.length){this.logs.set(t,s);try{this.settleOrphans(t)}catch(r){this.e.log("live: settling the saved thread failed: "+r.message)}}}return this.logs.get(t)||[]}setLog(e,t){this.logs.set(e,t.slice(-Tf)),this.dirty(e)}dirty(e){if(!e||this.saveTimers.has(e))return;let t=setTimeout(()=>{this.saveTimers.delete(e),this.save(e)},this.saveMs);t.unref&&t.unref(),this.saveTimers.set(e,t)}save(e){let t=this.e.sessions.get(e);if(!(t&&t.archived))try{this.store.save(e,this.logs.get(e)||[])}catch(s){this.e.log("live: saving the thread failed: "+s.message)}}flush(){for(let[e,t]of[...this.saveTimers])clearTimeout(t),this.saveTimers.delete(e),this.save(e)}forget(e){let t=this.saveTimers.get(e);t&&(clearTimeout(t),this.saveTimers.delete(e)),this.logs.delete(e),this.hydrated.delete(e),this.histories.delete(e);try{this.store.remove(e)}catch{}}settleOrphans(e){let t=this.logs.get(e)||[],s=this.e.sessions.get(e),r=t.filter(l=>l.msg&&!this.hub.get(l.msg)&&(l.from==="event"&&(l.state||"sent")==="sent"||l.from!=="claude"&&l.from!=="event"&&l.status&&!oy.has(l.status.phase)&&l.status.phase!=="stopped"));if(!r.length)return;let o=s&&s.transcriptPath&&s.capture!=="codex"?em(s.transcriptPath):[],i=this.e.now(),a=!!(s&&!s.endedAt&&s.status==="active");for(let l of r){let c=o.length?Zp(o,l.msg):null;if(l.from==="event"){l.state=c&&c.found?"received":"failed",l.state==="failed"&&(l.error="lost_in_restart");continue}if(c&&c.found){if(l.state="received",l.status.phase==="sent"&&(l.status=lt(l.status,{type:"delivered",at:i})),!c.ended&&a){l.status=lt(l.status,{type:"started",at:i}),s&&this.ensureTail(s);continue}let u=c.final||"";l.status=lt(l.status,u?{type:"ended",text:u,at:i}:{type:"lost",at:i}),u&&s&&this.upsertAnswer(s,l,{text:u,via:"transcript",done:!0})}else l.status=lt(l.status,{type:"lost",at:i})}this.dirty(e)}sessionForPids(e=[]){let t=new Set((e||[]).map(String)),s=null;for(let r of this.e.sessions.values())!this.e.isMine(r)||r.endedAt||r.pid==null||!t.has(String(r.pid))||(!s||String(r.lastEventAt||r.startedAt||"")>String(s.lastEventAt||s.startedAt||""))&&(s=r);return s}chanOf(e){if(!e||!this.e.isMine(e)||e.pid==null||e.endedAt)return null;let t=this.hub.find(e.pid);return t?this.newerOnPid(e)?null:t:null}newerOnPid(e){for(let t of this.e.sessions.values())if(!(t===e||t.endedAt||t.pid==null||String(t.pid)!==String(e.pid)||!this.e.isMine(t))&&String(t.lastEventAt||t.startedAt||"")>String(e.lastEventAt||e.startedAt||""))return t;return null}follow(e,t){if(!this.e.isMine(e)||e.pid==null||e.endedAt||e.nonInteractive)return;let s=t.data||{};if(!(t.type==="session.started"||t.type==="turn_started"||t.type==="turn_completed"||t.type==="activity"&&(s.kind==="prompt"||s.kind==="tool")))return;let o=this.hub.find(e.pid);if(!(!o||!this.hub.listening(o.pid))&&!this.newerOnPid(e)){for(let i of[...this.e.sessions.values()])i===e||i.endedAt||i.pid==null||String(i.pid)!==String(e.pid)||!this.e.isMine(i)||this.retire(i);(!e.liveLink||!e.liveLink.on)&&this.mark(e,{on:!0,permission:!!o.permission,since:Nn(o.since)})}}retire(e){return e.liveLink&&this.mark(e,null),!dt(e,(this.e.checkpoints.get(e.id)||[]).length)||e.transcriptTurns>0?!1:(e.endedAt=Nn(this.e.now()),e.endReason="replaced",e.inTurn=!1,this.e.persist(e,!0),this.e.log("live: closed empty session "+String(e.id).slice(0,8)+" (its process went on as another session)"),this.e.emit("session.updated",this.e.publicSession(e)),!0)}isLive(e){let t=this.chanOf(e);return!!t&&this.hub.listening(t.pid)}publicOf(e){if(this.e.isMine(e)){let s=this.chanOf(e);if(!s||!this.hub.listening(s.pid))return null;let r=[...this.logOf(e.id)].reverse().find(l=>l.from==="claude"),o=this.pendingFor(e.id),i=o&&o.status,a=i?{phase:i.phase,at:i.at||null,last:i.last||null,waiting:!!i.waiting}:null;return{on:!0,permission:!!s.permission,since:Nn(s.since),lastReplyAt:r?r.at:null,...a?{turn:a}:{}}}let t=e.liveLink;return t&&t.on&&!e.endedAt?{on:!0,permission:!!t.permission,since:t.since||null}:null}onUpDown(e,t){let s=this.sessionForPids(e.pids);s&&this.mark(s,t?{on:!0,permission:!!e.permission,since:Nn(e.since)}:null)}mark(e,t){let s=JSON.stringify(e.liveLink||null);e.liveLink=t;let r=!!(t&&t.on&&!e.wasLive);r&&(e.wasLive=!0),!(JSON.stringify(t||null)===s&&!r)&&(this.e.log("live: "+(t?"linked":"unlinked")+" session "+String(e.id).slice(0,8)),this.e.persist(e,!0),this.e.emit("session.updated",this.e.publicSession(e)))}addLog(e,t){this.setLog(e,[...this.logOf(e),{at:Nn(this.e.now()),...t}]);let s=this.e.sessions.get(e);return s&&this.e.emit("session.updated",this.e.publicSession(s)),t}publicEntry(e){let t=e.msg?this.hub.get(e.msg):null,s=t?t.state:e.state||null,r=t?t.error:e.error||null;return{id:e.msg||e.id,from:e.from,text:e.text,at:e.at,where:e.where||null,by:e.by||null,source:e.source||null,replyTo:e.replyTo||null,via:e.via||null,...e.from==="claude"?{done:e.done!==!1,ranWithoutAsking:e.ran||[]}:{state:s,error:r,errorText:s==="failed"?Pl(r):null,status:e.status||null}}}onSettled(e){if(e.kind==="verdict"){let r=e.askId?this.e.asks.get(e.askId):null;r&&e.state==="received"&&(r.delivered=!0);return}if(!e.sessionId)return;let t=this.logOf(e.sessionId).find(r=>r.msg===e.id);t&&(t.state=e.state,t.error=e.error||null,this.dirty(e.sessionId)),t&&t.status&&(t.status=lt(t.status||Of(Date.parse(t.at)||this.e.now()),{type:e.state==="received"?"delivered":"failed",at:this.e.now(),error:e.error}));let s=this.e.sessions.get(e.sessionId);s&&this.e.emit("session.updated",this.e.publicSession(s))}async view(e,{turns:t=iy}={}){let s=this.e.sessions.get(e);if(!s)throw C(404,"not_found","no such session");let r=Math.max(1,Math.min(Tf+500,Math.floor(Number(t))||iy));if(!this.e.isMine(s)){let c=await this.e.viaPeer(s.machine,"session.live.get",{sessionId:e,turns:r},"Reading that session",{since:"0.9.8"});return{...c.result,peer:s.machine,peerVia:c.via}}try{this.tailOne(s)}catch{}let o=this.publicOf(s),i=this.logOf(e).map(c=>this.publicEntry(c)),a=[];try{a=this.historyOf(s)}catch(c){this.e.log("live: reading the transcript failed: "+c.message)}let l=AL(i,a,r);return{sessionId:e,machine:this.e.machine,live:o,messages:l.messages,earlier:l.earlier,turns:r}}historyOf(e){if(!e.transcriptPath||e.capture==="codex")return[];let t=this.histories.get(e.id);(!t||t.file!==e.transcriptPath)&&(t=new Au(e.transcriptPath),this.histories.set(e.id,t)),t.read();let s=new Set(this.logOf(e.id).map(r=>r.msg).filter(Boolean));return oS(t.turns,{skipIds:s})}async send(e,t,s={}){this.e.assertNotAgent(e,"sending to a session"),this.e.assertMayAnswer(s);let r=this.e.sessions.get(e);if(!r)throw C(404,"not_found","no such session");let o=Ci(t),i=bL(s);if(!this.e.isMine(r)){let c=await this.e.viaPeer(r.machine,"session.live",{sessionId:r.id,text:o,by:i},"Sending to that session",{since:"0.9.8"});return{...c.result,peer:r.machine,peerVia:c.via}}if(r.endedAt)throw C(409,"closed","That session is closed.");if(!this.isLive(r))throw C(409,"not_live","This session isn’t live-linked. Start Claude Code with `leftoff live` to send to it from here.");let a=nr(s);this.ensureTail(r);let l=this.hub.put(r.pid,{kind:"prompt",text:o,by:{where:a,via:s.via||"loopback"},sessionId:r.id});return this.nameFromPrompt(r,o),this.addLog(r.id,{msg:l.id,from:"you",text:o,where:a,by:i,state:l.state,error:l.error||null,status:lt(Of(this.e.now()),l.state==="failed"?{type:"failed",error:l.error,at:this.e.now()}:{type:"noop"})}),this.e.audit(r.id,{at:Nn(l.at),kind:"live",tool:null,what:"live link",summary:"Sent: “"+Mn(o,80)+"”",action:"live",where:a,via:s.via||"loopback",who:s.who||null}),{ok:!0,id:l.id,state:l.state,error:l.error,sessionId:r.id}}hello(e={}){let t=String(e.pid||"");if(!/^\d{1,10}$/.test(t))throw C(400,"bad_pid","pid: the Claude Code process id");let s=this.hub.hello(t,{pids:Array.isArray(e.pids)?e.pids:[],permission:e.permission===!0,version:e.version}),r=this.sessionForPids(s.pids);return r&&this.mark(r,{on:!0,permission:!!s.permission,since:Nn(s.since)}),{ok:!0,sessionId:r?r.id:null,machine:this.e.machine}}async take(e,t,s){let r=await this.hub.take(String(e),t,s);if(!r)return{msg:null};let o=r.kind==="event"?r.by&&r.by.from||"webhook":r.kind==="prompt"?"leftoff":null;return{msg:{id:r.id,kind:r.kind,text:r.text,meta:r.meta||{},from:o,verdict:r.verdict||null}}}ack(e={}){return{ok:!0,state:this.hub.ack(String(e.pid||""),e.id,{ok:e.ok!==!1,error:e.error||null}).state}}reply(e={}){let t=this.sessionForPids([e.pid,...Array.isArray(e.pids)?e.pids:[]].filter(Boolean));if(!t)throw C(404,"no_session","Leftoff doesn’t know this session yet");let s=Ci(e.text),r=e.replyTo?String(e.replyTo).slice(0,40):null,o=this.pendingFor(t.id,r);if(o){o.replyText=s;let i=this.turnOf(t,o),a=Qp({transcript:i&&i.final,reply:s});return this.upsertAnswer(t,o,{text:a.text,via:a.via,done:a.via==="reply"||!this.tails.has(t.id)||!!(i&&i.ended)}),o.status=lt(o.status,{type:this.tails.has(t.id)?"text":"reply",at:this.e.now()}),this.touch(t.id),{ok:!0,sessionId:t.id}}return this.addLog(t.id,{id:"cr_"+Date.now().toString(36),from:"claude",via:"reply",text:s,replyTo:r}),{ok:!0,sessionId:t.id}}permission(e={}){let t=String(e.requestId||"");if(!SL.test(t))throw C(400,"bad_request_id","request_id: five letters a-z (no l)");let s=this.sessionForPids([e.pid,...Array.isArray(e.pids)?e.pids:[]].filter(Boolean));if(!s)throw C(404,"no_session","Leftoff doesn’t know this session yet; the terminal decides");if(this.e.cfg.answers&&this.e.cfg.answers.enabled===!1)return{id:null,state:"gone",reason:"answering_off"};let r=Mn(e.tool,80)||"a tool",o=xL(r,e.description,e.inputPreview),i=typeof e.cwd=="string"&&kL.isAbsolute(e.cwd)?e.cwd:s.cwd||null,a=this.e.asks.openChannel({sessionId:s.id,requestId:t,pid:String(s.pid),tool:r,describe:o,preview:String(e.inputPreview||"").slice(0,2e4),projectDir:i});return{id:a.id,state:a.state}}verdict(e){if(!e.channel||e.state!=="answered"||!e.decision)return null;let t=e.decision.behavior==="allow"?"allow":"deny",s=this.hub.put(e.channel.pid,{kind:"verdict",verdict:{requestId:e.channel.requestId,behavior:t},sessionId:e.sessionId});return s.askId=e.id,s.state==="failed"&&this.e.log("live: verdict for "+e.channel.requestId+" not delivered ("+s.error+")"),s}channelAlive(e){return!!(e&&e.channel&&this.hub.listening(e.channel.pid))}onIngest(e,t){if(!this.e.isMine(e))return;if(t.type==="session.opened"&&e.pid!=null&&this.hub.listening(e.pid)){let i=this.hub.find(e.pid);this.mark(e,{on:!0,permission:!!i.permission,since:Nn(i.since)})}t.type==="session.ended"&&e.liveLink&&(this.mark(e,null),this.tails.delete(e.id),this.counts.delete(e.id));let s=t.data||{};this.follow(e,t),t.type==="activity"&&s.kind==="tool"&&s.phase==="post"&&s.tool&&this.e.asks.settleSession(e.id,"answered_in_terminal",{tool:String(s.tool),channelOnly:!0}),t.type==="activity"&&s.kind==="prompt"&&s.prompt&&(this.isLive(e)||e.liveLink||$r(s.prompt))&&this.nameFromPrompt(e,s.prompt),this.isLive(e)&&(t.type==="turn_completed"||t.type==="activity"&&s.kind==="prompt")&&this.countTurns(e);let r=this.pendingFor(e.id);if(!r)return;let o=this.e.now();t.type==="turn_started"||t.type==="activity"&&s.kind==="prompt"?(r.status=lt(r.status,{type:"started",at:o}),this.touch(e.id)):t.type==="activity"&&s.kind==="tool"&&s.tool&&!this.tails.has(e.id)?(r.status=lt(r.status,{type:s.phase==="pre"?"tool":"tool_done",tool:s.tool,at:o,asked:this.askedFor(e.id,s.tool,r)}),this.touch(e.id)):t.type==="turn_completed"&&this.finishTurn(e,r,s.lastAssistantPreview||"")}onAsk(e,t){if(!e||e.kind!=="permission")return;let s=this.e.sessions.get(e.sessionId);if(!s||!this.e.isMine(s))return;let r=this.pendingFor(s.id);if(!r)return;let o=e.describe||{};r.status=lt(r.status,t==="opened"?{type:"ask",tool:e.tool,detail:o.detail||"",at:this.e.now()}:{type:"ask_done",at:this.e.now()}),this.touch(s.id)}pendingFor(e,t=null){let s=this.logOf(e);if(t){let r=s.find(o=>o.msg===t&&o.from!=="claude");if(r)return r}return s.find(r=>r.from!=="claude"&&r.msg&&r.status&&!oy.has(r.status.phase)&&r.status.phase!=="stopped")||null}touch(e){this.dirty(e);let t=this.e.sessions.get(e);t&&this.e.emit("session.updated",this.e.publicSession(t))}nameFromPrompt(e,t){if(e.nonInteractive)return!1;let s=Qo(t);if(!s)return!1;let r=!1;if((!e.firstPrompt||lr(e.firstPrompt))&&(e.firstPrompt=Mn(s,300),r=!0),!e.title||lr(e.title)){let o=vL(s);o&&(e.title=o,e.titleSource="prompt",e.titleChangedAt=Nn(this.e.now()),r=!0,this.e.log("live: named session "+String(e.id).slice(0,8)+" from its first prompt"))}return r&&this.e.persist(e,!0),r}countTurns(e,{lines:t=null}={}){if(!e.transcriptPath||e.capture==="codex")return!1;let s=this.counts.get(e.id);if((!s||s.file!==e.transcriptPath)&&(s={file:e.transcriptPath,seen:new Set,stats:{prompts:0,firstPrompt:null}},this.counts.set(e.id,s),t=em(e.transcriptPath)),!t||!t.length)return this.applyCount(e,s);let r=t.filter(o=>{let i=o&&(o.uuid||(o.timestamp?o.type+":"+o.timestamp:null));return i?s.seen.has(i)?!1:(s.seen.add(i),!0):!0});return s.seen.size>2e4&&(s.seen=new Set([...s.seen].slice(-1e4))),rS(r,s.stats),this.applyCount(e,s)}applyCount(e,t){let s=!1;return t.stats.prompts>(e.transcriptTurns||0)&&(e.transcriptTurns=t.stats.prompts,s=!0),t.stats.firstPrompt&&(!e.title||lr(e.title)||!e.firstPrompt||lr(e.firstPrompt))&&(s=this.nameFromPrompt(e,t.stats.firstPrompt)||s),s&&this.e.persist(e,!0),s}ensureTail(e){if(!e.transcriptPath||e.capture==="codex")return null;let t=this.tails.get(e.id);return(!t||t.file!==e.transcriptPath)&&(t=new Ka(e.transcriptPath),this.tails.set(e.id,t)),t}turnOf(e,t){let s=this.tails.get(e.id);return!s||!t||!t.msg?null:Zp(s.lines,t.msg)}askedFor(e,t,s){let r=Date.parse(s&&s.at)||0;return[...this.e.asks.items.values()].some(o=>o.sessionId===e&&o.tool===t&&o.kind==="permission"&&o.createdAt>=r-1e3)}upsertAnswer(e,t,{text:s,via:r,done:o}){if(!s)return null;let i=this.logOf(e.id),a="ans_"+t.msg,l=i.find(c=>c.id===a);return l||(l={at:Nn(this.e.now()),id:a,from:"claude",replyTo:t.msg},this.setLog(e.id,[...i,l])),this.dirty(e.id),l.text=Mn(s,6e4),l.via=r,l.done=o!==!1,l.ran=Zx(t.status),l}finishTurn(e,t,s=""){t.status.phase==="sent"&&(t.status=lt(t.status,{type:"delivered",at:this.e.now()}));let r=this.tails.get(e.id);if(r)try{r.read()}catch{}let o=this.turnOf(e,t);o&&this.applyTurn(e,t,o);let i=Qp({transcript:o&&o.final,reply:t.replyText,preview:s});t.status=lt(t.status,{type:"ended",text:i.text,at:this.e.now()}),i.text&&this.upsertAnswer(e,t,{text:i.text,via:i.via,done:!0}),this.touch(e.id)}applyTurn(e,t,s){if(!s.found)return!1;let r=this.e.now(),o=!1;t.seenTools=t.seenTools||{},(t.status.phase==="received"||t.status.phase==="sent")&&(t.status=lt(t.status,{type:"delivered",at:r}),t.status=lt(t.status,{type:"started",at:r}),o=!0);for(let l of s.blocks){if(l.type!=="tool")continue;t.seenTools[l.id]||(t.seenTools[l.id]="run",t.status=lt(t.status,{type:"tool",tool:l.name,detail:l.detail,at:r}),o=!0),l.done&&t.seenTools[l.id]!=="done"&&(t.seenTools[l.id]="done",t.status=lt(t.status,{type:"tool_done",tool:l.name,detail:l.detail,asked:this.askedFor(e.id,l.name,t),at:r}),o=!0)}let i=s.final||s.latest,a=this.logOf(e.id).find(l=>l.id==="ans_"+t.msg);return i&&(!a||a.text!==i)&&(t.status=lt(t.status,{type:"text",at:r}),this.upsertAnswer(e,t,{text:i,via:"transcript",done:!1}),o=!0),s.ended&&!oy.has(t.status.phase)?(this.finishTurn(e,t),!0):o}tailAll(){for(let e of this.e.sessions.values())this.tailOne(e)}tailOne(e){if(!this.e.isMine(e)||e.endedAt)return!1;let t=this.pendingFor(e.id);if(!t&&!this.tails.has(e.id)&&!this.isLive(e))return!1;let s=this.ensureTail(e);if(!s)return!1;let r=s.read(),o=!1,i=sS(r);return i&&i!==e.title&&!_L(this.e,i)&&(e.title=Mn(i,120),e.titleSource="custom",e.titleChangedAt=Nn(this.e.now()),this.e.persist(e,!0),this.e.log("live: session "+String(e.id).slice(0,8)+" renamed in Claude Code"),o=!0),t&&r.length&&(o=this.applyTurn(e,t,this.turnOf(e,t))||o),this.isLive(e)&&(r.length||!this.counts.has(e.id))&&(o=this.countTurns(e,{lines:r})||o),o&&this.touch(e.id),o}async event({raw:e="",authorization:t="",signature:s="",relayed:r=!1}={}){let o=this.eventKey(),i=Cf({authorization:t,signature:s,raw:e,key:o,relayed:r,now:this.e.now(),seen:this.seenSigs});if(!i.ok)throw C(i.status,i.code,i.message);if(!this.eventLimit.take("events"))throw C(429,"rate_limited","Too many events; at most 10 in a row, then one every 6 seconds.");let a;try{a=JSON.parse(e||"{}")}catch{throw C(400,"bad_json","body is not valid JSON")}let l=ty(a),c=[...this.e.sessions.values()].filter(f=>!f.endedAt&&!f.nonInteractive&&(this.e.isMine(f)?this.isLive(f):!!(f.liveLink&&f.liveLink.on))),u=ny(c.map(f=>({...this.e.publicSession(f),_s:f})),l.session);if(u.error==="ambiguous")throw C(409,"ambiguous",'"'+l.session+'" matches '+u.matches.length+" live sessions: "+u.matches.slice(0,5).map(f=>(f.title||f.id.slice(0,8))+" ("+f.id.slice(0,8)+")").join(", ")+". Use the id.");if(!u.session)throw C(404,"no_live_session",'No live-linked session matches "'+l.session+'". Start it with `leftoff live`.');let d=u.session._s;if(!this.e.isMine(d)){let f=await this.e.viaPeer(d.machine,"live.event",{sessionId:d.id,text:l.text,from:l.from,meta:l.meta},"Sending to that session",{since:"0.9.8"});return f.result&&f.result.id&&(this.forwarded.set(f.result.id,d.machine),this.forwarded.size>500&&this.forwarded.delete(this.forwarded.keys().next().value)),{...f.result,machine:d.machine}}return this.deliverEvent(d,l,{via:r?"relay":"local",how:i.how})}deliverEvent(e,t,{via:s="local"}={}){if(!this.isLive(e))throw C(409,"not_live","That session isn’t live-linked any more.");let r=this.hub.put(e.pid,{kind:"event",text:t.text,meta:t.meta,by:{from:t.from,via:s},sessionId:e.id});return this.addLog(e.id,{msg:r.id,from:"event",source:t.from,text:t.text,state:r.state,error:r.error||null}),this.e.log("live: event from "+t.from+" ("+s+") for session "+String(e.id).slice(0,8)+" → "+r.state),{ok:!0,id:r.id,state:r.state,error:r.error,sessionId:e.id,machine:this.e.machine}}async peerEvent(e={}){let t=this.e.sessions.get(String(e.sessionId||""));if(!t||!this.e.isMine(t))throw C(404,"not_found","no such session on "+this.e.machine);return this.deliverEvent(t,{text:Ci(e.text),from:String(e.from||"webhook").slice(0,60),meta:e.meta&&typeof e.meta=="object"?e.meta:{}},{via:"peer"})}async eventState(e,t={}){let s=this.eventKey(),r=Cf({...t,raw:"",key:s,now:this.e.now(),seen:new Map});if(!r.ok)throw C(r.status,r.code,r.message);let o=this.hub.get(e);if(o&&o.kind==="event")return{id:o.id,state:o.state,error:o.error,errorText:o.state==="failed"?Pl(o.error):null,machine:this.e.machine};let i=this.forwarded.get(String(e));if(i)return{...(await this.e.viaPeer(i,"live.event.get",{id:e},"Checking that event",{since:"0.9.8"})).result,machine:i};throw C(404,"not_found","no such event (they are kept in memory, since the daemon started)")}peerEventState(e){let t=this.hub.get(e);if(!t||t.kind!=="event")throw C(404,"not_found","no such event");return{id:t.id,state:t.state,error:t.error,errorText:t.state==="failed"?Pl(t.error):null}}}});import TL from"node:crypto";var EL,s1,ay,If,r1=G(()=>{no();Dl();Pt();EL=300*1e3,s1=3,ay=n=>n?new Date(n).toISOString():null,If=class{constructor(e,{ttlMs:t=EL,burst:s=3,everyMs:r=6e4}={}){this.e=e,this.ttlMs=t,this.items=new Map,this.bucket={tokens:s,last:null,burst:s,everyMs:r}}stop(){}take(){let e=this.bucket,t=this.e.now();return e.last==null&&(e.last=t),e.tokens=Math.min(e.burst,e.tokens+(t-e.last)/e.everyMs),e.last=t,e.tokens<1?!1:(e.tokens-=1,!0)}sweep(){let e=this.e.now();for(let t of this.items.values())t.state==="pending"&&e>=t.expiresAt&&(t.state="expired",t.settledAt=e,this.changed(t));if(this.items.size>50)for(let[t,s]of this.items){if(this.items.size<=50)break;s.state!=="pending"&&this.items.delete(t)}}changed(e){this.e.emit("restore.updated",{ask:e.id,state:e.state})}publicOf(e){return{id:e.id,machine:this.e.machine,state:e.state,count:e.ids.length,titles:e.titles,who:e.who,askedAt:ay(e.at),expiresAt:ay(e.expiresAt),settledAt:ay(e.settledAt),...e.result?{result:e.result}:{},...e.error?{error:e.error}:{}}}async request(e={},t={}){let s=e.machine?String(e.machine):this.e.machine;if(s!==this.e.machine){let r=await this.e.viaPeer(s,"restore.ask",{ids:e.ids,who:t.who||nr(t)},"Restore on "+s,{since:"0.9.8"});return{...r.result,machine:s,peerVia:r.via}}return this.ask(e,t)}ask(e={},t={}){if(!Array.isArray(e.ids)||!e.ids.length)throw C(400,"bad_request","ids: the session ids to restore");this.sweep();let s=this.e.restoreTargets(e.ids);if(!s.length)throw C(409,"nothing_to_restore","None of those sessions can be restored now (already open, or restored).");if([...this.items.values()].filter(l=>l.state==="pending").length>=s1)throw C(429,"too_many","There are already "+s1+" Restore requests waiting on "+this.e.machine+".");if(!this.take())throw C(429,"slow_down","too many Restore requests; wait a minute");let r=this.e.now(),o=String(e.who||t.who||(t.via==="peer"?t.peer:null)||nr(t)||"another device").slice(0,120),i={id:"ra_"+TL.randomBytes(9).toString("base64url"),ids:s.map(l=>l.id),titles:s.slice(0,5).map(l=>l.title||String(l.id).slice(0,8)),who:o,via:t.via||"remote",at:r,expiresAt:r+this.ttlMs,state:"pending",settledAt:null,result:null,error:null};this.items.set(i.id,i);let a=i.ids.length;try{this.e.notifier({title:Ge,subtitle:"Restore "+a+" session"+(a===1?"":"s")+" on this Mac?",message:"Asked from "+o+". Confirm on this Mac’s board (Leftoff → the restore card).",sessionId:"restore"})}catch(l){this.e.log("restore ask: notification: "+l.message)}return this.e.log("restore: "+a+" asked from "+o+" ("+i.id+"), waiting for a confirm here"),this.changed(i),this.publicOf(i)}get(e){this.sweep();let t=this.items.get(String(e||""));if(!t)throw C(404,"not_found","That Restore request is gone (the daemon restarted?)");return this.publicOf(t)}async status(e,t){return t&&t!==this.e.machine?{...(await this.e.viaPeer(t,"restore.ask.get",{id:e},"Checking on "+t,{since:"0.9.8"})).result,machine:t}:this.get(e)}pending(){return this.sweep(),[...this.items.values()].filter(e=>e.state==="pending").map(e=>this.publicOf(e))}async confirm(e){this.sweep();let t=this.items.get(String(e||""));if(!t)throw C(404,"not_found","That Restore request is gone");if(t.state!=="pending")throw C(409,"not_pending","That Restore request was already "+t.state+".");t.state="confirming";try{let s=await this.e.restore(t.ids);t.state="confirmed",t.result={how:s.how||null,opened:s.opened||0,failed:s.failed||0,message:s.message||null}}catch(s){t.state="failed",t.error=s.message}return t.settledAt=this.e.now(),this.changed(t),this.publicOf(t)}decline(e){this.sweep();let t=this.items.get(String(e||""));if(!t)throw C(404,"not_found","That Restore request is gone");if(t.state!=="pending")throw C(409,"not_pending","That Restore request was already "+t.state+".");return t.state="declined",t.settledAt=this.e.now(),this.e.log("restore: declined "+t.id+" (asked from "+t.who+")"),this.changed(t),this.publicOf(t)}}});var cy={};ot(cy,{BIN:()=>$e,HOOK_EVENTS:()=>o1,HOOK_OPTS:()=>i1,LEGACY_BIN:()=>NL,claudeSettingsPath:()=>Me,hasSettingsHooks:()=>Li,hookCommand:()=>l1,installSettingsHooks:()=>co,legacyServiceFile:()=>uo,legacyServiceStopCommands:()=>d1,migrateLegacyService:()=>Bl,readClaudeSettings:()=>Ul,serviceContent:()=>Wn,serviceEnableHint:()=>ly,serviceFile:()=>yt,servicePlatform:()=>zn,serviceRoot:()=>jf,serviceStartCommands:()=>In,setSettingsEnv:()=>ji,settingsHooksBlock:()=>c1,uninstallSettingsHooks:()=>Hl});import et from"node:fs";import cr from"node:os";import Ye from"node:path";import{fileURLToPath as CL}from"node:url";import{spawnSync as OL}from"node:child_process";function Me(){return process.env.CLAUDE_SETTINGS_PATH||Ye.join(process.env.CLAUDE_CONFIG_DIR||Ye.join(cr.homedir(),".claude"),"settings.json")}function Lf(n){return/^[A-Za-z0-9_./-]+$/.test(n)?n:JSON.stringify(n)}function l1(n,e=$e){return Lf(process.execPath)+" "+Lf(e)+" hook "+n+" --tag="+ML}function c1(n=$e){let e={};for(let t of o1)e[t]=[{hooks:[{type:"command",command:l1(t,n),timeout:5,...i1[t]||{}}]}];return e}function Ul(n=Me()){try{let e=et.readFileSync(n,"utf8");return e.trim()?JSON.parse(e):{}}catch{return null}}function Li(n=Me()){let e=Ul(n),t=e?JSON.stringify(e.hooks||{}):"";return a1.some(s=>t.includes(s))}function ji(n,e,t=Me()){let s=Ul(t),r=s||{},o=r.env?r.env[n]:void 0;return e==null&&o===void 0||o===e?!1:(s&&et.existsSync(t)?et.writeFileSync(t+".leftoff-backup-"+new Date().toISOString().replace(/[:.]/g,"-"),et.readFileSync(t,"utf8")):et.mkdirSync(Ye.dirname(t),{recursive:!0}),e==null?(delete r.env[n],Object.keys(r.env).length||delete r.env):r.env={...r.env||{},[n]:e},et.writeFileSync(t,JSON.stringify(r,null,2)+`
310
310
  `),!0)}function co(n=Me(),{bin:e=$e}={}){let t={};if(et.existsSync(n)){let s=et.readFileSync(n,"utf8");t=s.trim()?JSON.parse(s):{};let r=n+".leftoff-backup-"+new Date().toISOString().replace(/[:.]/g,"-");et.writeFileSync(r,s)}else et.mkdirSync(Ye.dirname(n),{recursive:!0});u1(t),t.hooks=t.hooks||{};for(let[s,r]of Object.entries(c1(e)))t.hooks[s]=[...t.hooks[s]||[],...r];return et.writeFileSync(n,JSON.stringify(t,null,2)+`
311
311
  `),n}function u1(n){if(!n.hooks)return 0;let e=0;for(let t of Object.keys(n.hooks)){let s=(n.hooks[t]||[]).map(r=>{let o=(r.hooks||[]).length,i=(r.hooks||[]).filter(a=>!(a.command&&IL(a.command)));return e+=o-i.length,{...r,hooks:i}}).filter(r=>r.hooks.length);s.length?n.hooks[t]=s:delete n.hooks[t]}return Object.keys(n.hooks).length||delete n.hooks,e}function Hl(n=Me()){if(!et.existsSync(n))return 0;let e=JSON.parse(et.readFileSync(n,"utf8")||"{}"),t=u1(e);return t&&et.writeFileSync(n,JSON.stringify(e,null,2)+`
312
312
  `),t}function zn(){return process.env.CHECKPOINTS_SERVICE_PLATFORM||process.platform}function yt(n=zn()){return n==="darwin"?Ye.join(cr.homedir(),"Library","LaunchAgents",rp+".plist"):Ye.join(process.env.XDG_CONFIG_HOME||Ye.join(cr.homedir(),".config"),"systemd","user",Mr)}function uo(n=zn()){return n==="darwin"?Ye.join(cr.homedir(),"Library","LaunchAgents",k0+".plist"):Ye.join(process.env.XDG_CONFIG_HOME||Ye.join(cr.homedir(),".config"),"systemd","user",op)}function Fl(n){return String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Wn({root:n=Ye.join(cr.homedir(),"checkpoints"),bin:e=$e,platform:t=zn()}={}){let s=Ye.resolve(n)!==Ye.join(cr.homedir(),"checkpoints"),r=Ye.join(n,".daemon.log");if(t==="darwin"){let o=s?` <key>EnvironmentVariables</key><dict><key>CHECKPOINTS_DIR</key><string>${Fl(n)}</string></dict>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.9.8-beta.5",
3
+ "version": "0.9.8-beta.6",
4
4
  "description": "Leftoff (formerly Checkpoints): WTF was I working on? Tracks every Claude Code session on this machine, writes a 3-line checkpoint when one goes quiet, and adds /cp (checkpoint now), /cp-name and a /leftoff pane. Talks to the local Leftoff daemon on 127.0.0.1:7429 and falls back to writing markdown in ~/checkpoints.",
5
5
  "author": {
6
6
  "name": "Joel"
@@ -28,7 +28,7 @@ const END_TIMEOUT_MS = 1000
28
28
  const MODEL_TIMEOUT_MS = 20000
29
29
 
30
30
  // Module state. $.state is not used: the pane redraws with $.ui.invalidate.
31
- const MOD_VERSION = '0.9.8-beta.5' // keep in step with .claude-plugin/plugin.json
31
+ const MOD_VERSION = '0.9.8-beta.6' // keep in step with .claude-plugin/plugin.json
32
32
 
33
33
  const st = {
34
34
  internal: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftoff/cli",
3
- "version": "0.9.8-beta.5",
3
+ "version": "0.9.8-beta.6",
4
4
  "description": "Leftoff: WTF was I working on? Tracks every Claude Code session, writes a 3-line checkpoint when one goes quiet, and gives you a daily digest and a board, on your Mac and (optionally) your phone.",
5
5
  "keywords": [
6
6
  "claude",
package/ui/app.css CHANGED
@@ -2038,6 +2038,13 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
2038
2038
 
2039
2039
  /* 0.9.8 Live link: the Live badge, and the session page's message box with its delivery states */
2040
2040
  .pill.live-pill { color: var(--green); background: var(--tint-green); font-weight: 600; } /* not .live: that class is the open/closed dot */
2041
+ /* Its dot pulses while Claude is running a turn (a soft ring growing out of it); idle, it stays still. The text doesn't blink. */
2042
+ .pill.live-pill.working::before { animation: live-pulse 1.6s ease-in-out infinite; }
2043
+ @keyframes live-pulse {
2044
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 55%, transparent); }
2045
+ 80%, 100% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--green) 0%, transparent); }
2046
+ }
2047
+ @media (prefers-reduced-motion: reduce) { .pill.live-pill.working::before, .s-active::before { animation: none; } }
2041
2048
  .live-box { margin: 16px 0 18px; border: 1px solid color-mix(in srgb, var(--green) 35%, var(--line)); background: linear-gradient(180deg, var(--tint-green), transparent 70%), var(--card); border-radius: var(--radius); padding: 12px 14px 14px; }
2042
2049
  .live-box.off { border-color: var(--line); background: var(--card); }
2043
2050
  .live-box.off p { margin: 4px 0 0; font-size: 13px; line-height: 1.5; }
package/ui/app.js CHANGED
@@ -19,10 +19,12 @@ import { cardPrsHtml, sessionPrsSection, prsPageHtml, prsBodyHtml, prRowHtml, pr
19
19
  import { wantsSetupBanner, setupCandidate, localLeftoffAnswers, thisOneSuffix, isPhoneUa } from './viewer.js'
20
20
  import { restoreAccess, hintHtml, restoreWhen, askLine } from './restore-gate.js'
21
21
  import { defaultEmoji, PICKER_SET, isEmoji } from './machines.js'
22
- import { sessionTitle, titleIsWhere, titleHtml, cardPills, foldsAsEmpty, threadHtml, earlierHtml, createLiveKeeper, liveHref, parseLiveRoute, hasUnread, scrollDecision, atBottom, jumpView } from './live-view.js' // 0.9.8 Live link polish
22
+ import { sessionTitle, titleIsWhere, titleHtml, cardPills, foldsAsEmpty, threadHtml, earlierHtml, createLiveKeeper, liveHref, parseLiveRoute, hasUnread, scrollDecision, atBottom, jumpView, liveBadgeHtml, lockPulses } from './live-view.js' // 0.9.8 Live link polish
23
23
  import { statusView, elapsedText } from './live-status.js'
24
24
  import { todosSection, sessionTodosSection, cardHandoffChips, sessionFilesSection, docViewerBody, docDownloadHref, docMenuItems, docFileName, renderDoc } from './handoffs.js'
25
25
  const $app = document.getElementById('app')
26
+ // 0.9.8: the pulsing dots (Live while Claude works, active, a blocking ask) keep their beat through the board's redraws
27
+ if (typeof MutationObserver === 'function') new MutationObserver(() => lockPulses(document)).observe(document.body, { childList: true, subtree: true })
26
28
  const state = { sessions: [], health: null, cfg: null, route: null, machine: localStorage.getItem('cp.machine') || '', drafts: {}, pendingAsk: {}, mayAnswer: true, needOpen: {}, needTurns: {} }
27
29
  const multi = () => !!(state.health && state.health.machines && state.health.machines.length > 1)
28
30
  const agentNames = () => [...new Set(((state.health && state.health.agents) || []).map((a) => a.display || a.name))]
@@ -119,7 +121,8 @@ function copiedMark(btn) {
119
121
  }
120
122
  const pill = (status) => '<span class="pill s-' + esc(status) + '">' + esc(LABEL[status] || status) + '</span>'
121
123
  // 0.9.8 Live link: the session's channel is listening (send it prompts from here)
122
- const liveBadge = (s) => (cardPills(s).live ? ' <span class="pill live-pill" title="Live link: send this session messages from the board or your phone">Live</span>' : '')
124
+ // It pulses while Claude is running a turn (liveWorking), steady when idle; lockPulses keeps redraws from restarting it.
125
+ const liveBadge = (s) => liveBadgeHtml(s)
123
126
  // The status pill beside it: a live session that is only waiting shows just Live (never "dormant").
124
127
  const statusPill = (s) => { const st = cardPills(s).status; return st ? pill(st) : '' }
125
128
 
package/ui/live-view.js CHANGED
@@ -65,6 +65,44 @@ export function cardPills(s) {
65
65
  return { status: ['active', 'needs_input', 'background'].includes(st) ? st : null, live: true }
66
66
  }
67
67
 
68
+ // Is Claude running a turn in this live session right now? The Live pill's dot
69
+ // pulses while it is. The same signals the board already shows: the session's
70
+ // own status (a turn typed in the terminal, which the hooks see start), or the
71
+ // board message whose turn is still going, read the way its Working bubble
72
+ // reads it (spinning: sent, received or working; not waiting for your OK, not
73
+ // gone quiet). Idle, waiting on you, or another Mac's daemon that doesn't say:
74
+ // a steady dot.
75
+ export function liveWorking(s, now = Date.now()) {
76
+ if (!cardPills(s).live) return false
77
+ if (s.status === 'active') return true
78
+ const t = s.liveLink && s.liveLink.turn
79
+ if (!t || !t.phase) return false
80
+ const v = statusView(t, now)
81
+ return !!(v && v.spin)
82
+ }
83
+ const LIVE_TIP = 'Live link: send this session messages from the board or your phone'
84
+ export function liveBadgeHtml(s, now = Date.now()) {
85
+ if (!cardPills(s).live) return ''
86
+ const w = liveWorking(s, now)
87
+ return ' <span class="pill live-pill' + (w ? ' working' : '') + '" title="' + (w ? 'Claude is working on a turn · ' : '') + LIVE_TIP + '">Live</span>'
88
+ }
89
+
90
+ // The board redraws a card (innerHTML) on every update, which would restart a
91
+ // CSS pulse each time: a visible hitch every second or so while a session
92
+ // works. Pinning every pulse to the page's own clock (start time 0) makes a
93
+ // fresh element pick up exactly where the one it replaced was, and keeps the
94
+ // dots on the page beating together. Reduced motion has no animations to pin.
95
+ export const PULSE_NAMES = new Set(['live-pulse', 'pulse', 'needpulse'])
96
+ export function lockPulses(doc) {
97
+ if (!doc || typeof doc.getAnimations !== 'function') return 0
98
+ let n = 0
99
+ for (const a of doc.getAnimations()) {
100
+ if (!PULSE_NAMES.has(a.animationName) || a.startTime === 0) continue
101
+ try { a.startTime = 0; n++ } catch {}
102
+ }
103
+ return n
104
+ }
105
+
68
106
  // The "N empty sessions" fold never takes a live session.
69
107
  export function foldsAsEmpty(s) { return !!(s && s.empty && !s.pinned && !(s.needsYou && s.needsYou.blocking) && !isLiveSession(s)) }
70
108