@leftoff/cli 0.9.8-beta.8 → 0.9.8-beta.9
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 +1 -0
- package/bin/leftoff.js +2 -2
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,7 @@ What changed in Leftoff (called Checkpoints until 0.4.5), newest first. The boar
|
|
|
19
19
|
- **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).
|
|
20
20
|
- **Notifications on your phone.** Add the board to your iPhone's Home Screen (iOS 16.4+), tap **Turn on notifications** under More, and you get a notification when Claude needs your OK or a Live reply lands. The Mac running the session sends it, encrypted end to end. Any browser with Push works the same.
|
|
21
21
|
### Fixed
|
|
22
|
+
- `leftoff upgrade` asks npm for fresh package info, so a version published minutes ago installs instead of failing with "No matching version".
|
|
22
23
|
- Live chat: a message no longer stays on **Working** after Claude has answered. It settles when the turn ends, including a second message sent while Claude was busy ("Answered with the reply above"), and stuck ones clear when you update.
|
|
23
24
|
- Live chat: a message sent while an older one was stuck now gets its own reply instead of sitting on **Starting**, and each message shows the time it was sent instead of a frozen "just now".
|
|
24
25
|
- 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.
|
package/bin/leftoff.js
CHANGED
|
@@ -218,7 +218,7 @@ Turn it off first: tailscale funnel --https=`+w+" off"),1;if((y.proxy||y.other)&
|
|
|
218
218
|
`).trim()}))}function wi(n=AM){try{return tg(vM.readFileSync(n,"utf8"))}catch{return[]}}function ki(n,e,t){return n.filter(s=>(!e||mt(s.version,e)>0)&&(!t||mt(s.version,t)<=0))}function av(n,e,t){return n.map(s=>({...s,isNew:(!t||mt(s.version,t)<=0)&&(!e||mt(s.version,e)>0)}))}function Td(n,{colors:e=null,width:t=78}={}){let s=e||{bold:i=>i,dim:i=>i,cyan:i=>i,green:i=>i},r=[],o=(i,a)=>{let l=String(i).replace(/\*\*(.+?)\*\*/g,"$1").split(/\s+/),c=[],u="";for(let d of l)(u+" "+d).trim().length>t-a.length?(c.push(u),u=d):u=(u+" "+d).trim();return u&&c.push(u),c.map((d,f)=>(f?a:"")+d)};for(let i of n){r.push(s.bold(s.cyan("v"+i.version))+(i.date?s.dim(" "+i.date):"")+(i.isNew?" "+s.green("new"):""));for(let a of i.sections){r.push(" "+s.bold(a.title));for(let l of a.items)r.push(" • "+o(l," ").join(`
|
|
219
219
|
`))}r.push("")}return r.join(`
|
|
220
220
|
`)}var AM,_d,Ed=K(()=>{Em();AM=ov.resolve(ov.dirname(xM(import.meta.url)),"..","CHANGELOG.md");_d=n=>eg(n)!==null});var wv={};at(wv,{CHECK_EVERY_MS:()=>og,DEFAULT_SOURCE:()=>ml,GITHUB_REPO:()=>ts,NPM_PACKAGE:()=>gt,PACKAGE_DIRS:()=>gl,REPO_DIR:()=>rr,STAMP_FILE:()=>rg,checkForUpdate:()=>wl,defaultCacheDir:()=>cg,fetchRemoteFiles:()=>yv,installInfo:()=>yl,isNpmBuild:()=>fv,isNpxCacheDir:()=>sg,lsRemote:()=>gv,normalizeSource:()=>bi,npmDistTags:()=>lg,npmLatest:()=>CM,npmPackageUrl:()=>Od,npmRegistry:()=>Cd,npmTarget:()=>pv,npmUpgradeCommand:()=>ag,packageManagerOf:()=>hv,readPkg:()=>dv,readStamp:()=>cv,registryPath:()=>uv,renamedSource:()=>oo,runGit:()=>Zt,sshFallback:()=>mv,writeStamp:()=>ig});import Si from"node:fs";import _M from"node:os";import ps from"node:path";import{execFile as TM}from"node:child_process";import{fileURLToPath as EM}from"node:url";function bi(n){let e=String(n||"").trim();if(!e)return ml;let t=e.match(/^(?:github:)?([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:#.*)?$/);return t&&!e.includes("://")&&!e.startsWith("/")&&!e.startsWith(".")?oo("https://github.com/"+t[1]+"/"+t[2]+".git"):oo(e)}function oo(n){let e=String(n||"");for(let t of j0){let s=t.replace(/[.]/g,"\\."),r=new RegExp("^(https://github\\.com/|git@github\\.com:|ssh://git@github\\.com/)"+s+"(?:\\.git)?/?$","i"),o=e.match(r);if(o)return o[1]+ts+".git"}return e}function Zt(n,{cwd:e,timeoutMs:t=15e3,env:s=process.env}={}){return new Promise(r=>{let o={...s,GIT_TERMINAL_PROMPT:"0",GIT_ASKPASS:s.GIT_ASKPASS||"echo",SSH_ASKPASS:"echo",GIT_SSH_COMMAND:s.GIT_SSH_COMMAND||"ssh -o BatchMode=yes"};TM(s.CHECKPOINTS_GIT||"git",n,{cwd:e,timeout:t,env:o,maxBuffer:4*1024*1024},(i,a,l)=>{r({code:i?typeof i.code=="number"?i.code:-1:0,stdout:String(a||""),stderr:String(l||(i&&!a?i.message:"")||"")})})})}function cv(n=rr){try{return JSON.parse(Si.readFileSync(ps.join(n,rg),"utf8"))}catch{return null}}function ig(n,e=rr){let t={version:n.version||Q,sha:n.sha||null,source:n.source||null,at:n.at||new Date().toISOString()};return Si.writeFileSync(ps.join(e,rg),JSON.stringify(t,null,2)+`
|
|
221
|
-
`),t}function sg(n){return/[\\/]_npx[\\/]/.test(n)||/[\\/]\.npm[\\/]/.test(n)}function uv(n=gt){return n.startsWith("@")?"@"+n.slice(1).split("/").map(encodeURIComponent).join("%2f"):encodeURIComponent(n)}function Od(n=process.env,e=gt){return Cd(n)+"/"+uv(e)}function dv(n=rr){try{return JSON.parse(Si.readFileSync(ps.join(n,"package.json"),"utf8"))}catch{return null}}function fv(n=rr){let e=dv(n);return!!(e&&e.leftoffBuild==="npm")}function hv(n,e=process.env){if(e.LEFTOFF_PM)return e.LEFTOFF_PM;let t=String(n||"");return/[\\/]\.?pnpm[\\/]/i.test(t)||/[\\/]pnpm-global[\\/]/i.test(t)?"pnpm":/[\\/]\.bun[\\/]/.test(t)?"bun":"npm"}function ag(n,e=gt+"@latest"){let t=process.platform==="win32";return n==="pnpm"?[t?"pnpm.cmd":"pnpm","add","-g",e]:n==="bun"?["bun","add","-g",e]:[t?"npm.cmd":"npm","install","-g",e,"--no-fund","--no-audit","--loglevel=error"]}function Cd(n=process.env){return String(n.LEFTOFF_NPM_REGISTRY||n.npm_config_registry||"https://registry.npmjs.org").replace(/\/+$/,"")}async function lg({env:n=process.env,fetchImpl:e=fetch,timeoutMs:t=15e3}={}){let s=Od(n);try{let r=await e(s,{headers:{accept:"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8"},signal:AbortSignal.timeout(t)});if(r.status===404)return{ok:!1,tags:{},error:gt+" isn’t on "+Cd(n)+" (404)"};if(!r.ok)return{ok:!1,tags:{},error:Cd(n)+" answered "+r.status};let o=await r.json(),i={...o&&o["dist-tags"]||{}};return!i.latest&&o&&o.version&&(i.latest=o.version),i.latest?{ok:!0,tags:i,error:null}:{ok:!1,tags:{},error:"no version in the registry answer"}}catch(r){return{ok:!1,tags:{},error:r.message}}}async function CM(n={}){let e=await lg(n);return e.ok?{ok:!0,version:e.tags.latest,error:null}:{ok:!1,version:null,error:e.error}}function pv(n,e={},t=null){let s=t||(_d(n)?"beta":"latest"),r=e.latest||null,o="latest";s==="beta"&&e.beta&&(!r||mt(e.beta,r)>0)&&(r=e.beta,o="beta");let i=!!r&&mt(r,n)>0;return{version:r,tag:o,available:i,command:o==="beta"?"leftoff upgrade --beta":"leftoff upgrade"}}async function yl({repoDir:n=rr,env:e=process.env,globalRoot:t}={}){let s={version:Q,sha:null,kind:"local",source:null,repoDir:n};if(Si.existsSync(ps.join(n,".git"))){s.kind="clone";let o=await Zt(["rev-parse","HEAD"],{cwd:n,env:e});o.code===0&&(s.sha=o.stdout.trim());let i=await Zt(["remote","get-url","origin"],{cwd:n,env:e});return s.source=i.code===0?i.stdout.trim():null,s}if(fv(n))return s.kind="npm",s.source="npm:"+gt,s.pm=hv(n,e),s.npx=sg(n),s;let r=cv(n);return r&&(s.sha=r.sha||null,s.source=r.source||null),sg(n)?s.kind="npx":(t?n.startsWith(t):/[\\/]node_modules[\\/](checkpoints|leftoff|@leftoff[\\/]cli)$/.test(n))&&(s.kind="global"),s}function mv(n){let e=String(n||"").match(/^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/);return e?"git@github.com:"+e[1]+"/"+e[2]+".git":null}async function lv(n,{env:e,timeoutMs:t}){let s=await Zt(["ls-remote",n,"HEAD"],{env:e,timeoutMs:t}),r=s.code===0?(s.stdout.match(/^([0-9a-f]{40})\s+HEAD$/m)||[])[1]:null;return r?{ok:!0,sha:r,error:null,source:n}:{ok:!1,sha:null,error:ng(s.stderr)||"git ls-remote exited "+s.code,source:n}}async function gv(n,{env:e=process.env,timeoutMs:t=15e3}={}){let s=bi(n),r=await lv(s,{env:e,timeoutMs:t});if(r.ok)return r;let o=mv(s);if(!o)return r;let i=await lv(o,{env:e,timeoutMs:t});return i.ok?i:{ok:!1,sha:null,source:s,error:r.error+" (also tried "+o+": "+i.error+")"}}async function yv(n,{cacheDir:e=cg(),env:t=process.env,timeoutMs:s=3e4}={}){try{if(Si.mkdirSync(e,{recursive:!0}),!Si.existsSync(ps.join(e,"HEAD"))){let c=await Zt(["init","--bare","-q",e],{env:t});if(c.code!==0)return{ok:!1,error:ng(c.stderr)}}let r=await Zt(["fetch","-q","--depth=1",bi(n),"HEAD"],{cwd:e,env:t,timeoutMs:s});if(r.code!==0)return{ok:!1,error:ng(r.stderr)||"git fetch exited "+r.code};let o=(await Zt(["rev-parse","FETCH_HEAD"],{cwd:e,env:t})).stdout.trim()||null,i=await Zt(["show","FETCH_HEAD:CHANGELOG.md"],{cwd:e,env:t}),a=await Zt(["show","FETCH_HEAD:package.json"],{cwd:e,env:t}),l=null;try{l=JSON.parse(a.stdout).version||null}catch{}return{ok:!0,sha:o,version:l,changelog:i.code===0?i.stdout:null,error:null}}catch(r){return{ok:!1,error:r.message}}}function cg(n=process.env){return ps.join(n.XDG_CACHE_HOME||ps.join(_M.homedir(),".cache"),"checkpoints","remote.git")}async function wl({info:n,source:e,env:t=process.env,cacheDir:s,now:r=Date.now(),fetchFiles:o=!0,channel:i=null,fetchImpl:a=null}={}){let l=n||await yl({env:t});if(l.kind==="npm"){let h={checkedAt:new Date(r).toISOString(),source:"npm:"+gt,current:{version:l.version,sha:null},remote:{sha:null,version:null},available:!1,incoming:[],error:null},p=await lg({env:t,...a?{fetchImpl:a}:{}});if(!p.ok)return h.error=p.error,h;let m=pv(l.version,p.tags,i);return h.remote.version=m.version,h.remote.tag=m.tag,h.upgradeCommand=m.command,p.tags.latest&&p.tags.latest!==m.version&&(h.remote.latest=p.tags.latest),h.available=m.available,h}let c=bi(e||l.source||ml),u={checkedAt:new Date(r).toISOString(),source:c,current:{version:l.version,sha:l.sha},remote:{sha:null,version:null},available:!1,incoming:[],error:null},d=await gv(c,{env:t});if(!d.ok)return u.error=d.error,u;if(u.source=d.source,u.remote.sha=d.sha,l.sha&&l.sha===d.sha)return u;if(o){let h=await yv(u.source,{cacheDir:s||cg(t),env:t});h.ok?(u.remote.version=h.version,h.changelog&&(u.incoming=ki(tg(h.changelog),l.version,h.version).map(p=>({version:p.version,date:p.date,sections:p.sections})))):u.fetchError=h.error}let f=u.remote.version?mt(u.remote.version,l.version):null;return u.available=f===1||f===0&&!!l.sha||f===null&&!!l.sha,f===-1&&(u.available=!1),u}var rr,ml,rg,og,ng,gt,gl,kl=K(()=>{Rt();Ae();Ed();rr=ps.resolve(ps.dirname(EM(import.meta.url)),".."),ml="https://github.com/"+ts+".git",rg=".install.json",og=24*3600*1e3;ng=n=>String(n||"").trim().split(`
|
|
221
|
+
`),t}function sg(n){return/[\\/]_npx[\\/]/.test(n)||/[\\/]\.npm[\\/]/.test(n)}function uv(n=gt){return n.startsWith("@")?"@"+n.slice(1).split("/").map(encodeURIComponent).join("%2f"):encodeURIComponent(n)}function Od(n=process.env,e=gt){return Cd(n)+"/"+uv(e)}function dv(n=rr){try{return JSON.parse(Si.readFileSync(ps.join(n,"package.json"),"utf8"))}catch{return null}}function fv(n=rr){let e=dv(n);return!!(e&&e.leftoffBuild==="npm")}function hv(n,e=process.env){if(e.LEFTOFF_PM)return e.LEFTOFF_PM;let t=String(n||"");return/[\\/]\.?pnpm[\\/]/i.test(t)||/[\\/]pnpm-global[\\/]/i.test(t)?"pnpm":/[\\/]\.bun[\\/]/.test(t)?"bun":"npm"}function ag(n,e=gt+"@latest"){let t=process.platform==="win32";return n==="pnpm"?[t?"pnpm.cmd":"pnpm","add","-g",e]:n==="bun"?["bun","add","-g",e]:[t?"npm.cmd":"npm","install","-g",e,"--prefer-online","--no-fund","--no-audit","--loglevel=error"]}function Cd(n=process.env){return String(n.LEFTOFF_NPM_REGISTRY||n.npm_config_registry||"https://registry.npmjs.org").replace(/\/+$/,"")}async function lg({env:n=process.env,fetchImpl:e=fetch,timeoutMs:t=15e3}={}){let s=Od(n);try{let r=await e(s,{headers:{accept:"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8"},signal:AbortSignal.timeout(t)});if(r.status===404)return{ok:!1,tags:{},error:gt+" isn’t on "+Cd(n)+" (404)"};if(!r.ok)return{ok:!1,tags:{},error:Cd(n)+" answered "+r.status};let o=await r.json(),i={...o&&o["dist-tags"]||{}};return!i.latest&&o&&o.version&&(i.latest=o.version),i.latest?{ok:!0,tags:i,error:null}:{ok:!1,tags:{},error:"no version in the registry answer"}}catch(r){return{ok:!1,tags:{},error:r.message}}}async function CM(n={}){let e=await lg(n);return e.ok?{ok:!0,version:e.tags.latest,error:null}:{ok:!1,version:null,error:e.error}}function pv(n,e={},t=null){let s=t||(_d(n)?"beta":"latest"),r=e.latest||null,o="latest";s==="beta"&&e.beta&&(!r||mt(e.beta,r)>0)&&(r=e.beta,o="beta");let i=!!r&&mt(r,n)>0;return{version:r,tag:o,available:i,command:o==="beta"?"leftoff upgrade --beta":"leftoff upgrade"}}async function yl({repoDir:n=rr,env:e=process.env,globalRoot:t}={}){let s={version:Q,sha:null,kind:"local",source:null,repoDir:n};if(Si.existsSync(ps.join(n,".git"))){s.kind="clone";let o=await Zt(["rev-parse","HEAD"],{cwd:n,env:e});o.code===0&&(s.sha=o.stdout.trim());let i=await Zt(["remote","get-url","origin"],{cwd:n,env:e});return s.source=i.code===0?i.stdout.trim():null,s}if(fv(n))return s.kind="npm",s.source="npm:"+gt,s.pm=hv(n,e),s.npx=sg(n),s;let r=cv(n);return r&&(s.sha=r.sha||null,s.source=r.source||null),sg(n)?s.kind="npx":(t?n.startsWith(t):/[\\/]node_modules[\\/](checkpoints|leftoff|@leftoff[\\/]cli)$/.test(n))&&(s.kind="global"),s}function mv(n){let e=String(n||"").match(/^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/);return e?"git@github.com:"+e[1]+"/"+e[2]+".git":null}async function lv(n,{env:e,timeoutMs:t}){let s=await Zt(["ls-remote",n,"HEAD"],{env:e,timeoutMs:t}),r=s.code===0?(s.stdout.match(/^([0-9a-f]{40})\s+HEAD$/m)||[])[1]:null;return r?{ok:!0,sha:r,error:null,source:n}:{ok:!1,sha:null,error:ng(s.stderr)||"git ls-remote exited "+s.code,source:n}}async function gv(n,{env:e=process.env,timeoutMs:t=15e3}={}){let s=bi(n),r=await lv(s,{env:e,timeoutMs:t});if(r.ok)return r;let o=mv(s);if(!o)return r;let i=await lv(o,{env:e,timeoutMs:t});return i.ok?i:{ok:!1,sha:null,source:s,error:r.error+" (also tried "+o+": "+i.error+")"}}async function yv(n,{cacheDir:e=cg(),env:t=process.env,timeoutMs:s=3e4}={}){try{if(Si.mkdirSync(e,{recursive:!0}),!Si.existsSync(ps.join(e,"HEAD"))){let c=await Zt(["init","--bare","-q",e],{env:t});if(c.code!==0)return{ok:!1,error:ng(c.stderr)}}let r=await Zt(["fetch","-q","--depth=1",bi(n),"HEAD"],{cwd:e,env:t,timeoutMs:s});if(r.code!==0)return{ok:!1,error:ng(r.stderr)||"git fetch exited "+r.code};let o=(await Zt(["rev-parse","FETCH_HEAD"],{cwd:e,env:t})).stdout.trim()||null,i=await Zt(["show","FETCH_HEAD:CHANGELOG.md"],{cwd:e,env:t}),a=await Zt(["show","FETCH_HEAD:package.json"],{cwd:e,env:t}),l=null;try{l=JSON.parse(a.stdout).version||null}catch{}return{ok:!0,sha:o,version:l,changelog:i.code===0?i.stdout:null,error:null}}catch(r){return{ok:!1,error:r.message}}}function cg(n=process.env){return ps.join(n.XDG_CACHE_HOME||ps.join(_M.homedir(),".cache"),"checkpoints","remote.git")}async function wl({info:n,source:e,env:t=process.env,cacheDir:s,now:r=Date.now(),fetchFiles:o=!0,channel:i=null,fetchImpl:a=null}={}){let l=n||await yl({env:t});if(l.kind==="npm"){let h={checkedAt:new Date(r).toISOString(),source:"npm:"+gt,current:{version:l.version,sha:null},remote:{sha:null,version:null},available:!1,incoming:[],error:null},p=await lg({env:t,...a?{fetchImpl:a}:{}});if(!p.ok)return h.error=p.error,h;let m=pv(l.version,p.tags,i);return h.remote.version=m.version,h.remote.tag=m.tag,h.upgradeCommand=m.command,p.tags.latest&&p.tags.latest!==m.version&&(h.remote.latest=p.tags.latest),h.available=m.available,h}let c=bi(e||l.source||ml),u={checkedAt:new Date(r).toISOString(),source:c,current:{version:l.version,sha:l.sha},remote:{sha:null,version:null},available:!1,incoming:[],error:null},d=await gv(c,{env:t});if(!d.ok)return u.error=d.error,u;if(u.source=d.source,u.remote.sha=d.sha,l.sha&&l.sha===d.sha)return u;if(o){let h=await yv(u.source,{cacheDir:s||cg(t),env:t});h.ok?(u.remote.version=h.version,h.changelog&&(u.incoming=ki(tg(h.changelog),l.version,h.version).map(p=>({version:p.version,date:p.date,sections:p.sections})))):u.fetchError=h.error}let f=u.remote.version?mt(u.remote.version,l.version):null;return u.available=f===1||f===0&&!!l.sha||f===null&&!!l.sha,f===-1&&(u.available=!1),u}var rr,ml,rg,og,ng,gt,gl,kl=K(()=>{Rt();Ae();Ed();rr=ps.resolve(ps.dirname(EM(import.meta.url)),".."),ml="https://github.com/"+ts+".git",rg=".install.json",og=24*3600*1e3;ng=n=>String(n||"").trim().split(`
|
|
222
222
|
`).filter(Boolean).pop()||"";gt="@leftoff/cli",gl=[gt,"leftoff","checkpoints"]});import hn from"node:fs";import vi from"node:path";import OM from"node:crypto";function Nd(n){return!n||typeof n.toolName!="string"||!n.toolName?null:n.ruleContent!=null&&n.ruleContent!==""?n.toolName+"("+String(n.ruleContent)+")":n.toolName}function vv(n){let e=/^([A-Za-z_][\w-]*)(?:\((.*)\))?$/s.exec(String(n||""));return e?e[2]!=null?{toolName:e[1],ruleContent:e[2]}:{toolName:e[1]}:null}function MM(n){if(typeof n!="string")return null;let e=n.trim();if(!e||e.length>NM||/[\n\r`*()]/.test(e)||e.includes("$(")||e.includes("${")||/[\u2026\u22ef\u00b7]/.test(e)||/redacted/i.test(e))return null;let t=[],s="",r=null;for(let i=0;i<e.length;i++){let a=e[i],l=e[i+1];if(r){if(a==="\\"&&r==='"'){s+=a+(l||""),i++;continue}a===r&&(r=null),s+=a;continue}if(a==="\\"){if(l==null)return null;s+=a+l,i++;continue}if(a==="'"||a==='"'){r=a,s+=a;continue}if(a==="&"&&l==="&"||a==="|"&&l==="|"){t.push(s),s="",i++;continue}if(a===";"||a==="|"){t.push(s),s="";continue}s+=a}if(r)return null;t.push(s);let o=[];for(let i of t){let a=(i.match(Sv())||[]).join("");if(/[<>&]/.test(a)&&kv().test(i)||(i=i.replace(kv()," ").trim(),!i))return null;let l=i.replace(Sv(),'""');if(/[<>&{}]/.test(l)||/^[A-Za-z_][A-Za-z0-9_]*=/.test(l))return null;o.includes(i)||o.push(i)}return o.length&&o.length<=bv?o:null}function IM(n,e){let t=String(n||"");if(t==="Bash"){let s=MM(e&&e.command);return s?s.map(r=>({toolName:"Bash",ruleContent:r})):null}if(/^mcp__[\w-]+__[\w-]+$/.test(t))return[{toolName:t}];if(t==="WebFetch"&&e&&typeof e.url=="string"){let s;try{s=new URL(e.url)}catch{return null}return!/^https?:$/.test(s.protocol)||!s.hostname||/[*()]/.test(s.hostname)?null:[{toolName:"WebFetch",ruleContent:"domain:"+s.hostname}]}return null}function LM(n){if(!Array.isArray(n))return null;let e=[];for(let t of n)if(!(!t||t.type!=="addRules"||t.behavior==="deny"||t.behavior==="ask"||!Array.isArray(t.rules)))for(let s of t.rules){let r=Nd(s);r&&!/[\n\r]/.test(r)&&!e.some(o=>Nd(o)===r)&&e.push({toolName:s.toolName,...s.ruleContent!=null&&s.ruleContent!==""?{ruleContent:String(s.ruleContent)}:{}})}return e.length?e.slice(0,bv):null}function jM(n){try{let e=JSON.parse(String(n||""));return e&&typeof e=="object"&&!Array.isArray(e)?e:null}catch{return null}}function Md(n){if(!n||n.kind!=="permission")return null;let e=LM(n.suggestions);if(e)return{rules:e.map(Nd),source:"claude"};let t=n.input&&Object.keys(n.input).length?n.input:jM(n.preview),s=IM(n.tool,t);return s?{rules:s.map(Nd),source:"derived"}:null}function ms(n,e){let t=new Error(e);return t.status=409,t.code=n,t}function xv(n,e){if(!n||typeof n!="string"||!vi.isAbsolute(n))throw ms("no_folder","Leftoff doesn’t know this session’s project folder, so it can’t save the rule. Allow once instead.");let t;try{t=hn.statSync(n)}catch{t=null}if(!t||!t.isDirectory())throw ms("no_folder","The project folder "+n+" isn’t there, so the rule can’t be saved. Allow once instead.");let s=(Array.isArray(e)?e:[]).map(String).filter(Boolean);if(!s.length)throw ms("no_rule","No rule to save for this request.");let r=vi.join(n,".claude"),o=vi.join(r,"settings.local.json");try{hn.lstatSync(o).isSymbolicLink()&&(o=hn.realpathSync(o))}catch{}let i=null,a=420;try{i=hn.readFileSync(o,"utf8"),a=hn.statSync(o).mode&511}catch(p){if(p.code!=="ENOENT")throw ms("settings_unreadable","Couldn’t read "+gs+" ("+p.code+"). Nothing was changed.")}let l={};if(i!=null&&i.trim()){try{l=JSON.parse(i)}catch(p){throw ms("settings_malformed",gs+" in "+n+" isn’t valid JSON, so Leftoff left it alone. Fix it or Allow once. ("+p.message+")")}if(!l||typeof l!="object"||Array.isArray(l))throw ms("settings_malformed",gs+" in "+n+" isn’t a JSON object, so Leftoff left it alone. Allow once instead.")}let c=l.permissions==null?{}:l.permissions;if(!c||typeof c!="object"||Array.isArray(c))throw ms("settings_malformed",'"permissions" in '+gs+" isn’t an object, so Leftoff left it alone. Allow once instead.");let u=c.allow==null?[]:c.allow;if(!Array.isArray(u))throw ms("settings_malformed",'"permissions.allow" in '+gs+" isn’t a list, so Leftoff left it alone. Allow once instead.");let d=s.filter((p,m)=>!u.includes(p)&&s.indexOf(p)===m);if(!d.length)return{file:o,added:[],already:s,changed:!1};let f={...l,permissions:{...c,allow:[...u,...d]}};hn.mkdirSync(vi.dirname(o),{recursive:!0});let h=vi.join(vi.dirname(o),".settings.local.json."+process.pid+"."+OM.randomBytes(4).toString("hex")+".tmp");try{let p=hn.openSync(h,"w",a);try{hn.writeSync(p,JSON.stringify(f,null,2)+`
|
|
223
223
|
`),hn.fsyncSync(p)}finally{hn.closeSync(p)}hn.renameSync(h,o)}catch(p){try{hn.unlinkSync(h)}catch{}throw ms("settings_unwritable","Couldn’t write "+gs+" ("+(p.code||p.message)+"). Nothing was changed.")}return{file:o,added:d,already:s.filter(p=>!d.includes(p)),changed:!0}}var gs,NM,bv,kv,Sv,ug=K(()=>{gs=".claude/settings.local.json",NM=600,bv=6;kv=()=>/(^|\s)(?:[12]?>&[12]|&>\s*\/dev\/null|[12]?>\s*\/dev\/null)(?=\s|$)/g,Sv=()=>/'[^']*'|"(?:\\.|[^"\\])*"/g});import Tv from"node:crypto";import PM from"node:path";function dg(){return"rq_"+Tv.randomBytes(9).toString("base64url")}function Sl(n,e){let t=s=>Array.isArray(s)?"["+s.map(t).join(",")+"]":s&&typeof s=="object"?"{"+Object.keys(s).filter(r=>r!=="answers").sort().map(r=>JSON.stringify(r)+":"+t(s[r])).join(",")+"}":JSON.stringify(s===void 0?null:s);return Tv.createHash("sha256").update(String(n||"")+`
|
|
224
224
|
`+t(e||{})).digest("hex").slice(0,16)}function fg(n,e={}){let t=e&&typeof e=="object"?e:{};switch(n){case"Bash":return{verb:"Run",detail:yt(t.command,400),note:t.description?yt(t.description,160):null};case"Edit":case"MultiEdit":return{verb:"Edit",detail:yt(t.file_path,300),note:null};case"Write":return{verb:"Write",detail:yt(t.file_path,300),note:t.content?yt(String(t.content).split(`
|
|
@@ -512,7 +512,7 @@ leftoff live key [--new] the key for outside events (CI, webhooks), with a
|
|
|
512
512
|
# leftoff upgrade, `+new Date().toISOString()+(o?"":" (--verbose)")),n.finish)return LP({flags:n,out:e,env:t,c:r,say:a,ok:l,warn:c,fail:u,P:i});let d=s||await yl({env:t,globalRoot:t.CHECKPOINTS_NPM_GLOBAL_ROOT||void 0}),f=bi(n.source||t.CHECKPOINTS_UPDATE_SOURCE||(ee().updates||{}).source||d.source||ml);d.kind==="npm"&&(f="npm:"+gt),a(r.bold(Ge+" v"+d.version)+r.dim((d.kind==="npm"?" · installed from npm with "+d.pm+" at ":" ("+Tr(d.sha)+") · "+{clone:"a git clone at ",global:"installed globally at ",npx:"an npx copy at ",local:"installed at "}[d.kind])+d.repoDir));let h=n.beta&&OP(d);if(h)return i.warn(h),2;i.start("Checking for updates…");let p=d.kind==="npm"?n.beta?"beta":"latest":null,m=await wl({info:d,source:f,env:t,channel:p});if(m.error&&d.kind==="npm")return u("Couldn't reach the npm registry ("+Od(t)+"): "+m.error),1;if(m.error)return a(r.dim(" (git uses your own credentials; for a private repo, check `git ls-remote "+f+'` works, or set "updates": { "source": "git@github.com:'+ts+'.git" } in config.json)')),u("Couldn't reach "+f+": "+m.error),1;if(m.source&&m.source!==f&&(a(r.dim("· reached GitHub over SSH ("+m.source+")")),f=m.source),!(m.available||!d.sha&&m.remote.version&&mt(m.remote.version,d.version)>0)&&!n.force)return d.kind==="npm"&&_d(d.version)&&!n.beta?(i.done("Up to date: "+Ge+" "+d.version+", a beta"+(m.remote.version?" (latest is "+m.remote.version+")":"")),a(r.dim("Next beta: leftoff upgrade --beta")),0):(o?i.done("Up to date: "+Ge+" "+d.version):l("Up to date"+r.dim(" (remote "+Tr(m.remote.sha)+(m.remote.version?", v"+m.remote.version:"")+")")),0);let S=m.remote.version||Tr(m.remote.sha);if(i.done("Found "+S+" (you have "+d.version+")"),a(r.bold(r.cyan("Update available: "))+(m.remote.version?"v"+d.version+" → v"+m.remote.version:"")+(d.kind==="npm"?"":r.dim(" "+Tr(d.sha)+" → "+Tr(m.remote.sha)))),m.incoming.length?a(`
|
|
513
513
|
`+Td(m.incoming,{colors:r})):m.fetchError&&a(r.dim(" (couldn't read the new changelog: "+m.fetchError+")")),n.check)return a(r.dim("Run `"+(m.upgradeCommand||"leftoff upgrade")+"` to install it.")),0;i.start("Downloading "+S+"…");let y=d.repoDir;if(d.kind==="npm"){let b=ag(d.pm,gt+"@"+(m.remote.version||m.remote.tag||"latest"));a(r.dim("$ "+b.join(" ")));let x=await Lw(b[0],b.slice(1),{env:t,log:i.log});if(x.status!==0)return o||a((x.stderr||"").trim()),u(b.slice(0,3).join(" ")+" failed: "+((x.stderr||"").trim().split(`
|
|
514
514
|
`).pop()||"exit "+x.status)),1;let E=t.CHECKPOINTS_NPM_GLOBAL_ROOT||$_(t,d.pm);y=E&&Pt.existsSync(zt.join(E,gt,"package.json"))?zt.join(E,gt):d.repoDir;let O=null;try{O=JSON.parse(Pt.readFileSync(zt.join(y,"package.json"),"utf8")).version}catch{}if(m.remote.version&&O&&O!==m.remote.version)return u("npm installed "+O+", not "+m.remote.version+". npm may still be catching up; try again in a minute."),1;l("Installed "+gt+"@"+(O||m.remote.version||"latest")+r.dim(" → "+y))}else if(d.kind==="clone"){d.source&&oo(d.source)!==d.source&&(await Zt(["remote","set-url","origin",oo(d.source)],{cwd:d.repoDir,env:t})).code===0&&a(r.dim("· the repo is now "+ts+"; pointed origin at "+oo(d.source))),a(r.dim("$ git -C "+d.repoDir+" pull --ff-only"));let b=await Zt(["pull","--ff-only","-q"],{cwd:d.repoDir,env:t,timeoutMs:12e4});if(b.code!==0)return a(b.stderr.trim()),a(r.dim(" Local changes? Commit or stash them and run `leftoff upgrade` again.")),u("git pull failed: "+(b.stderr.trim().split(`
|
|
515
|
-
`).pop()||"exit "+b.code)),1;l("Pulled "+Tr(m.remote.sha))}else{let b=f.match(/^(?:https:\/\/github\.com\/|git@github\.com:)([\w.-]+\/[\w.-]+?)(?:\.git)?$/),x=b?"github:"+b[1]:f;a(r.dim("$ npm install -g "+x));let E=await Lw(z_(),["install","-g",x,"--no-fund","--no-audit","--loglevel=error"],{env:t,log:i.log});if(E.status!==0)return o||a((E.stderr||"").trim()),u("npm install -g failed: "+((E.stderr||"").trim().split(`
|
|
515
|
+
`).pop()||"exit "+b.code)),1;l("Pulled "+Tr(m.remote.sha))}else{let b=f.match(/^(?:https:\/\/github\.com\/|git@github\.com:)([\w.-]+\/[\w.-]+?)(?:\.git)?$/),x=b?"github:"+b[1]:f;a(r.dim("$ npm install -g "+x));let E=await Lw(z_(),["install","-g",x,"--prefer-online","--no-fund","--no-audit","--loglevel=error"],{env:t,log:i.log});if(E.status!==0)return o||a((E.stderr||"").trim()),u("npm install -g failed: "+((E.stderr||"").trim().split(`
|
|
516
516
|
`).pop()||"exit "+E.status)),1;let O=t.CHECKPOINTS_NPM_GLOBAL_ROOT||$_(t);y=O?TP(O):d.repoDir,l("Installed "+x+r.dim(" → "+y));try{Pt.existsSync(zt.join(y,".install.json"))||ig({sha:m.remote.sha,source:f,version:m.remote.version||void 0},y)}catch{}}i.done("Downloaded "+S),i.start("Updating the Claude Code plugin…");let k=null,v=Vi(t);if(d.kind==="npm")try{zy({from:y,env:t})}catch(b){c("Couldn't refresh the mod's files: "+b.message)}if(!v)c("`claude` isn't on PATH; update the mod later with: claude plugin marketplace update "+Vn+" && claude plugin update "+We);else{let b=oh(v,t);if(!b.installed&&b.legacyInstalled){a(r.dim("$ claude plugin marketplace update "+Vn));let x=As(v,["plugin","marketplace","update",Vn],{env:t});x.code!==0&&c("`claude plugin marketplace update "+Vn+"` failed: "+((x.stderr||x.stdout).trim().split(`
|
|
517
517
|
`).pop()||"exit "+x.code))}else if(!b.installed)a(r.dim("· The mod isn't installed through `claude plugin`; nothing to update there.")),k="skip";else{let x=!1;for(let E of[["plugin","marketplace","update",Vn],["plugin","update",We]]){a(r.dim("$ claude "+E.join(" ")));let O=await Lw(v,E,{env:{...t,CHECKPOINTS_INTERNAL:"1",LEFTOFF_INTERNAL:"1"},log:i.log,timeoutMs:12e4});if(O.status!==0){x=!0,c("`claude "+E.join(" ")+"` failed: "+((O.stderr||O.stdout).trim().split(`
|
|
518
518
|
`).pop()||"exit "+O.status));break}}l("Mod updated"+r.dim(" · restart open Claude Code sessions to load it")),x||(k="ok")}}o&&(k==="ok"?i.done("Updated the Claude Code plugin"):k==="skip"?i.done("Claude Code plugin: not installed with `claude plugin`, nothing to update"):i.stop());let T=[zt.join(y,"bin","checkpoints.js"),"upgrade","--finish","--from="+d.version,"--from-sha="+(d.sha||""),"--repo="+y];if(n["no-restart"]&&T.push("--no-restart"),!o){let b=Lh(process.execPath,T,{env:t,stdio:"inherit"});return b.status==null?1:b.status}T.push("--quiet","--log="+i.logFile);let g=Lh(process.execPath,T,{env:{...t,LEFTOFF_PROGRESS_TTY:e.isTTY&&!t.CI?"1":"0"},stdio:["ignore","inherit","pipe"],encoding:"utf8"});return g.stderr&&i.log(g.stderr.trimEnd()),g.status!==0?(i.fail("Finishing the upgrade failed (exit "+g.status+")"),g.status==null?1:g.status):0}var NP=n=>String(n).replace(/\x1b\[2m.*?\x1b\[22m/g,"").trim();function MP(n,e=3){let t=n.map(r=>{let o=r.sections.flatMap(a=>a.items),i=o.filter(a=>/^\*\*/.test(a));return i.length?i:o}),s=[];for(let r=0;s.length<e&&t.some(o=>o.length>r);r++)for(let o of t){if(s.length>=e||!o[r])continue;let i=o[r].match(/^\*\*(.+?)\*\*/),a=(i?i[1]:o[r]).replace(/[*`]/g,"").replace(/\s+/g," ").trim().replace(/[.:]$/,"");a.length>72&&(a=a.slice(0,71).replace(/\s+\S*$/,"")+"…"),s.push(a)}return s}function IP(n,e,t=Q){let s=[];for(let[r,o]of Object.entries(uo(n)))r!==e&&o.valid&&o.card.version&&mt(o.card.version,t)<0&&s.push(r+" ("+o.card.version+")");return s.sort()}async function LP({flags:n,out:e,env:t,c:s,say:r,ok:o,warn:i,fail:a,P:l}){let c=l.quiet,u=String(n.from||""),d=String(n.repo||rr),f=await yl({repoDir:d,env:t}),h=Z(),p=ht(h);if(p){let{cfg:y,applied:k}=_P(p,u,Q);if(k.length){Pt.writeFileSync(ve(h).config,JSON.stringify(y,null,2)+`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.8-beta.
|
|
3
|
+
"version": "0.9.8-beta.9",
|
|
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"
|
package/mod/hooks/register.js
CHANGED
|
@@ -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.
|
|
31
|
+
const MOD_VERSION = '0.9.8-beta.9' // 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.
|
|
3
|
+
"version": "0.9.8-beta.9",
|
|
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
|
@@ -2357,7 +2357,12 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
|
|
|
2357
2357
|
.tab-board.on, .tab-cell.on { box-shadow: inset 0 -2px 0 var(--accent); }
|
|
2358
2358
|
.tab-x { flex: none; width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; border: 0; background: none; color: var(--muted); cursor: pointer; padding: 0; border-radius: 8px; }
|
|
2359
2359
|
.tab-x .ic { width: 14px; height: 14px; }
|
|
2360
|
-
.tab-x
|
|
2360
|
+
.tab-x { position: relative; }
|
|
2361
|
+
.tab-x::before { content: ''; position: absolute; left: 50%; top: 50%; width: 24px; height: 24px; margin: -12px 0 0 -12px; border-radius: 50%; background: transparent; transition: background .12s; }
|
|
2362
|
+
.tab-x .ic { position: relative; }
|
|
2363
|
+
.tab-x:hover { color: var(--text); background: none; }
|
|
2364
|
+
.tab-x:hover::before { background: color-mix(in srgb, var(--text) 9%, transparent); }
|
|
2365
|
+
.tab-keep .ic { width: 12px; height: 12px; }
|
|
2361
2366
|
.tab-keep { opacity: 0; }
|
|
2362
2367
|
.tab-cell:hover .tab-keep, .tab-keep:focus-visible, .tab-cell.on .tab-keep { opacity: 1; }
|
|
2363
2368
|
.tab-cell.dragging { opacity: .5; }
|