@leftoff/cli 0.9.18-beta.7 → 0.9.18-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 +5 -0
- package/bin/leftoff-statusline.js +9 -0
- package/bin/leftoff.js +511 -511
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +3 -3
- package/package.json +1 -1
- package/ui/app.css +1 -0
- package/ui/app.js +70 -7
- package/ui/live-status.js +27 -2
- package/ui/viewport-fit.js +22 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ What changed in Leftoff (called Checkpoints until 0.4.5), newest first. The boar
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
- **leftoff ↗ shows in Claude Code's status line again.** The npm package was missing the small script the status line runs, so nothing showed at the bottom of Claude Code, and `leftoff doctor` still said it was fine. The script ships now, and doctor says when the status line points at a file or a Node that's gone. Run `leftoff upgrade --beta`; your status line setting stays as it is.
|
|
8
|
+
- **Clearer reasons when Claude Code can't answer.** If Claude Code's config file (~/.claude.json) isn't valid JSON, Claude Code 2.1.318 refuses to start instead of resetting it. Ask, summaries and walkthroughs now say so, name the file and how to fix it, and let Codex answer meanwhile. `leftoff doctor` points it out too. Claude Code's newer "signed out" wordings are recognised everywhere, and its sign-in-ends warning in the last 10 days no longer hides the real error. Needs `leftoff restart`.
|
|
9
|
+
- **The login warning reads the date from Claude Code.** With Claude Code 2.1.318 or later, Leftoff asks Claude Code itself when its claude.ai sign-in ends (and every board turn, Ask and walkthrough refreshes it), so the day-ahead warning works on a Mac without turning on the Keychain check, and an old leftover login file can no longer raise a false "isn't signed in". Older Claude Code works as before. Needs `leftoff restart`.
|
|
10
|
+
- **Switching keyboards while typing keeps the reply box in view.** On iPhone, switching from Apple's keyboard to another one (Raycast, Gboard, SwiftKey) mid-message could leave the reply box hidden behind the keyboard, and switching back didn't bring it back until you closed the keyboard. A swap changes the keyboard's height, iOS can report a passing "no keyboard" on the way, and the new height can arrive with no report of its own, so full screen stayed sized for no keyboard. While you type, full screen now follows every report from that moment's numbers, looks again a few beats after each one, and moves the reply box back into view if it still isn't. A board reload is enough.
|
|
11
|
+
- **Photos picked on a phone attach again.** On iPhone, choosing a photo from the paperclip's picker closed the picker and nothing showed: coming back from the picker counts as a wake-up, which redraws the page, and iOS hands over the photo only after that, to the old picker the redraw had thrown away. The picker now lives outside the page, so a redraw can't lose what you picked, and a pick that still can't attach says why. Full screen also measures again as a phone's keyboard comes up, for keyboards (Raycast, Gboard, SwiftKey) that report late. A board reload is enough.
|
|
7
12
|
- **Walkthroughs say whether a step follows or departs from a decision.** A decision chip on a step now reads ≠ Departs when the step goes against it, and the step always gets a Look closely note naming the decision; from there the decision viewer offers to link one of the session's own decisions as its replacement. A session's own decisions show in its walkthrough right away, marked ◌ Proposed, with Accept and Reject in the viewer. Needs `leftoff restart` and `leftoff upgrade --beta`.
|
|
8
13
|
- **Weak tests caught without the model.** Walk me through it flags a test change that removes assertions and adds none, adds a skip or .only, asserts a value against itself, or mocks the module the test is for, and gives it a Look closely note even if the model put it under Safe to skip. Needs `leftoff restart`.
|
|
9
14
|
- **Test plans from merged PRs.** A project's page, its decision log and the PRs page (pick merged PRs, then Test plan for these) make a short manual script for what merged lately: up to about 12 checks that fit 15, 30 or 60 minutes, each with where to go, the steps, what you should see and why it's on the list, ordered by risk. Risky callouts, weak tests and changed decisions always become checks or are listed under Not covered with the reason. Tick checks off, note what failed and propose a decision from it, then Copy as Markdown or Download .html. `leftoff testplan --project NAME` prints it. Needs `leftoff restart`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import i from"node:fs";import c from"node:os";import l from"node:path";import{spawnSync as U}from"node:child_process";function O(e,t=40){return String(e||"").toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g,"").replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,t).replace(/-+$/g,"")||"untitled"}function g(e){return O(String(e||"").replace(/\.local$/i,""),40)||"unknown"}function T({env:e,hostname:t,cfg:n={}}={}){if(e)return g(e);let r=n.machineNames||{};return t&&r[t]?g(r[t]):n.machine?g(n.machine):g(t||"unknown")}var _="[A-Za-z0-9_.-]*(?:key|token|secret|passw(?:or)?d|pwd|auth(?!or)|credential|cookie|signature)[A-Za-z0-9_.-]*",ee=[[/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+\/=-]{6,}/gi,"$1 …"],[/\b(authorization|cookie|x-api-key)(\s*[:=]\s*)("[^"]*"|'[^']*'|\S+(?:\s+[A-Za-z0-9._~+\/=-]{12,})?)/gi,"$1$2…"],[/(:\/\/)[^\s\/@:]+:[^\s\/@]+@/g,"$1…@"],[new RegExp("(--?"+_+`)(=|\\s+)("[^"]*"|'[^']*'|\\S+)`,"gi"),"$1$2…"],[new RegExp("\\b("+_+`)(\\s*[=:]\\s*)("[^"]*"|'[^']*'|[^\\s&;,]+)`,"gi"),"$1$2…"],[/\b([A-Z][A-Z0-9_]{1,})=("[^"]*"|'[^']*'|[^\s;&|]+)/g,"$1=…"],[/\b(?:sk|pk|rk|ghp|gho|ghs|ghu|ghr|github_pat|glpat|xox[abprs]|AKIA|ASIA|npm|hf|ya29)[-_][A-Za-z0-9_-]{8,}/g,"…"],[/\bAKIA[A-Z0-9]{12,}\b/g,"…"],[/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]+)?/g,"…"],[/(?<![\/\w.-])(?=[A-Za-z0-9_+=-]*\d)(?=[A-Za-z0-9_+=-]*[A-Za-z])[A-Za-z0-9_+=-]{32,}(?![\/\w.-])/g,"…"]];var te=10*6e4;var v={short:{label:"Short",maxTokens:300,approx:"~300 tokens",blurb:"Doing / Stopped at / Next. Three lines, nothing else."},informative:{label:"Informative",maxTokens:700,approx:"~700 tokens",blurb:"The three lines, plus key decisions, open questions, files touched and why, and what didn't work."},long:{label:"Long",maxTokens:1500,approx:"~1500 tokens",blurb:"Everything in Informative, plus a short narrative recap and the next 3 steps."}},ne=Object.keys(v);var x=["You write checkpoints for a developer juggling many coding-agent sessions, read when they come back after half an hour, a day or a weekend. Given the recent transcript of ONE session, start with exactly these five lines:","Goal: <the outcome the developer is after, from what they asked for, in a few plain words; not the latest step. Keep the previous goal unless they changed course or it is done and they moved on>","Progress: <one of Just started, Underway, Mostly done, Done, Stuck> - <what is left between here and the goal, a few words>","Doing: <what the session was working on, one sentence, concrete nouns: files, features, errors>","Stopped at: <the exact state when it went quiet: what passed, what failed, what is half-done; say what was actually run or checked, and what is only assumed>","Next: <the single next action a human or the agent should take>",'If the session is blocked on the human (a question, a permission, a decision), start Next with "YOU:".',"Describe only what the transcript shows; the lines above it (repo, branch, title, what was asked, previous checkpoint) are background and may be out of date."],y=['Only if the transcript shows a decision (a choice between real alternatives that the developer made or approved, a plan they approved, a correction they gave such as "no, do X instead", or a trade-off the agent chose and said so), add after those lines a "Decided:" heading and one "- " line per decision, at most 4:',"- <what was decided, in the words the transcript uses> · why: <the reason given, one line> · over: <alternative>; <alternative> · from: <asked | plan | correction | agent>","A task done, a file edited or a routine step is not a decision. Never invent a choice or a reason the transcript does not show. When unsure, or when there is none, leave the Decided heading out entirely. Skip decisions listed as already recorded."],A=['Then these sections, each a heading line followed by "- " bullets (write "- none" when there is nothing; never invent):',"Approach notes:","- <how the work is being done: the approach taken, a pattern or constraint it follows; one line each, at most 4. Not a choice between alternatives: those go only under Decided>","Open questions:","- <an unresolved question or uncertainty, at most 3>","Files:","- <path> — <why it was touched, a few words; at most 6 files>","Didn't work:","- <an approach that was tried and abandoned or failed, and why; at most 3>"],R=["Then:","Recap: <a narrative paragraph of 4-7 sentences: the goal, how the session got here, the current state>","Next steps:","1. <the next step>","2. <the step after>","3. <the step after that>"],M={short:[...x,...y,"Write only those five lines (and Decided, when there is one).",'Optionally a 6th line "Title: <max 6 words>" if asked for a title.',"No preamble. No markdown. Under 90 words total."].join(`
|
|
3
|
+
`),informative:[...x,...y,...A,'Optionally a final line "Title: <max 6 words>" if asked for a title.',"No preamble. No other markdown. Under 280 words total."].join(`
|
|
4
|
+
`),long:[...x,...y,...A,...R,'Optionally a final line "Title: <max 6 words>" if asked for a title.',"No preamble. No other markdown. Under 630 words total."].join(`
|
|
5
|
+
`)},re=M.short;var D="Write it in the same language as the user's own messages (English if unsure), in one language only: never mix in words or characters from another language.",oe="Write a title of at most 6 words for a coding session, based on its first prompt. "+D+" Reply with the title only, no quotes, no punctuation at the end.";var ie=[["latin",new RegExp("\\p{Script=Latin}","u")],["han",/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Bopomofo}]/u],["hangul",new RegExp("\\p{Script=Hangul}","u")],["cyrillic",new RegExp("\\p{Script=Cyrillic}","u")],["greek",new RegExp("\\p{Script=Greek}","u")],["arabic",new RegExp("\\p{Script=Arabic}","u")],["hebrew",new RegExp("\\p{Script=Hebrew}","u")],["thai",new RegExp("\\p{Script=Thai}","u")],["devanagari",new RegExp("\\p{Script=Devanagari}","u")]];var $=new Set("the a an and or but if then so of to in on at by for from with without into onto about over under after before is are was were be been being am do does did done has have had having can could will would should shall may might must i me my we us our you your he him his she her it its they them their this that these those there here what which who whom whose when where why how not no yes all any some each every both more most less many much few other another such only own same than too very just also again still now today up down out off via per as because while until since though although whether let lets please hey hi thanks ok okay one two three first last next new old way something anything nothing everything".split(" ")),P=new Set([...$,..."add added adding adds allow api app apps ask asks asking answer answers auth auto backend background bad base basic batch better big board bot box branch break broken bug bugs build builds button cache call calls card cards case change changes chat check checks clean cleanup clear cli click client close code column columns command commit config connect copy core count create cron css current dark data date db debug default delete deploy design detail details dev diff doc docs driver drop edit editor element empty end error errors event events explain export extra fail fails failing fast feature field fields file files filter find fix fixes flag flaky flow folder font form format full get git give go good graph group grouping guard guide handle handler header help hide history hook hooks host icon idea image import improve info input install issue issues item items job jobs join json key keys kind label layout lazy line lines link links lint list lists live load local lock log logic login logs look loop main make map mark menu merge message messages method migrate mobile mode model module move multi name names nav note notes number open option options order output overview page pages panel parse parser part pass patch path phone pick plan play polish port post prompt prompts proof pull push query question questions queue quick read readme real record refactor release remote remove rename render repo report request reset response rest result results retry review route router rule run runs runner save scan schema screen script scroll search session sessions set setup ship show sidebar sign size slow small sort source spec split start state status step stop store style sync table tables tag task tasks test tests text theme time timeout tip title titles today token tool tools top track tree trim try tune type typo typos ui undo unit up update upgrade url use user users value values view wait warn watch web wire word words work worker write wrong yaml focus bonus plus bus virus menu cursor support transcript transcripts tidy bump speed toggle suite crash startup prototype investigate audit align avoid batch bind boot cancel cap cart chart cleanup compare compose convert cover cut define detect disable enable draft drag embed ensure expose extract fetch finish fit fold forms gate handoff hover inline keep limit lint map match measure memo mock mute note pin poll port preview profile prune rate reach rebase redo reduce refresh reload repair replace reply resize resolve restore reuse revert rework rewrite scope seed send serve share shrink skip slot snap spike stub swap tidy trace tweak unify unlock untangle verify wrap zoom basics summary context notes intro".split(" ")]),se=new Set("de del la las el los un una unos unas y con para por que como sobre su sus le les des du une et pour avec sur dans au aux ce cette ces est der das und mit für fur von zu auf ein eine einen dem ist nicht über uber gli della delle dei di per con nel uma não nao cum est sunt atque quae qui quod".split(" ").filter(e=>!P.has(e)));var F="leftoff ↗",j=" · ",S=7429,k="\x1B",E=k+"\\";function W(e,t){return k+"]8;;"+e+E+t+k+"]8;;"+E}function z(e){return"/#/s/"+encodeURIComponent(String(e))}function q(e,t){return"http://127.0.0.1:"+(Number(e)||S)+z(t)}function I(e=process.env,t=c.homedir()){if(e.LEFTOFF_DIR||e.CHECKPOINTS_DIR)return e.LEFTOFF_DIR||e.CHECKPOINTS_DIR;try{let n=l.join(e.XDG_CONFIG_HOME||l.join(t,".config"),"checkpoints","folder"),r=i.readFileSync(n,"utf8").trim();if(r)return r}catch{}return l.join(t,"checkpoints")}function H(e=process.env,t=c.homedir()){let n=e.LEFTOFF_PORT||e.CHECKPOINTS_PORT;if(n&&Number(n))return Number(n);try{let r=JSON.parse(i.readFileSync(l.join(I(e,t),"config.json"),"utf8"));if(Number(r.port))return Number(r.port)}catch{}return S}function B(e,{port:t=S}={}){let n=e&&typeof e.session_id=="string"?e.session_id.trim():"";return n?W(q(t,n),F):""}function K(e){let t=String(e||"");if(!t.startsWith("b64:"))return null;try{return Buffer.from(t.slice(4),"base64url").toString("utf8")}catch{return null}}function Z(e,t){let n=String(e||"").replace(/\r?\n+$/,"");if(!t)return n;if(!n.trim())return t;let r=n.split(`
|
|
6
|
+
`),s=/\x1b\[[0-9;]*m/.test(r[0])&&!/\x1b\[0?m(?!.*\x1b\[)/.test(r[0]);return r[0]=r[0]+(s?k+"[0m":"")+j+t,r.join(`
|
|
7
|
+
`)}function G(e){for(let t of[e.LEFTOFF_STATUS_SHELL,"/bin/bash","/bin/sh"])if(t&&i.existsSync(t))return t;return!0}function Y(e,t,{env:n=process.env,cwd:r=process.cwd(),timeoutMs:s=1e4,run:d=U,home:a=c.homedir()}={}){try{let o=d(e,{shell:G(n),input:t,encoding:"utf8",env:n,cwd:X(r,a),timeout:s,maxBuffer:1048576});return{out:String(o&&o.stdout||""),code:o&&typeof o.status=="number"?o.status:1}}catch{return{out:"",code:1}}}function N(e){try{return!!e&&i.statSync(e).isDirectory()}catch{return!1}}function X(e,t=c.homedir()){if(N(e))return e;let n="";try{n=process.cwd()}catch{}for(let r of[n,t])if(N(r))return r;return"/"}function C(e,t,n={}){let{env:r=process.env,home:s=c.homedir(),cwd:d=process.cwd(),run:a}=n,o=null;try{o=JSON.parse(t||"{}")}catch{}let p=(e||[]).find(w=>String(w).startsWith("--wrap=")),f=p?K(p.slice(7)):null,u=S;try{u=H(r,s)}catch{}let h=B(o,{port:u});try{n.noName||Q(o,{env:r,home:s})}catch{}if(!f)return h;let m=Y(f,t,{env:r,cwd:o&&o.cwd||d,run:a,home:s});return Z(m.out,h)}function Q(e,{env:t=process.env,home:n=c.homedir(),tmp:r=c.tmpdir(),now:s=Date.now(),machine:d=null}={}){let a=e&&typeof e.session_id=="string"?e.session_id.trim():"",o=e&&typeof e.session_name=="string"?e.session_name.trim().slice(0,200):"";if(!a||!o||/[\/]/.test(a)||a.length>100)return!1;let p=l.join(r,"leftoff-names"),f=l.join(p,a.replace(/[^A-Za-z0-9_-]/g,"_"));try{if(i.readFileSync(f,"utf8")===o)return!1}catch{}let u=I(t,n);if(!i.existsSync(l.join(u,"config.json")))return!1;let h=d||V(t,u),m=l.join(u,".spool",h,a.replace(/[^A-Za-z0-9_-]/g,"_"));i.mkdirSync(m,{recursive:!0});let w={v:1,sessionId:a,at:new Date(s).toISOString(),type:"activity",data:{kind:"name",generatedTitle:o}};return i.writeFileSync(l.join(m,s+"-"+Math.random().toString(16).slice(2,10)+".json"),JSON.stringify(w)+`
|
|
8
|
+
`,{flag:"wx"}),i.mkdirSync(p,{recursive:!0}),i.writeFileSync(f,o),!0}function V(e,t){let n={};try{n=JSON.parse(i.readFileSync(l.join(t,"config.json"),"utf8"))}catch{}return J({env:e.CHECKPOINTS_MACHINE,hostname:c.hostname(),cfg:n})}function J(e){return T(e)}function L(e=0){try{return i.readFileSync(e,"utf8")}catch{return""}}var b="";try{b=C(process.argv.slice(2),L())}catch{}b&&process.stdout.write(b+`
|
|
9
|
+
`);process.exitCode=0;
|