@leftoff/cli 0.9.18-beta.8 → 0.9.18
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 +29 -2
- package/bin/leftoff-statusline.js +9 -0
- package/bin/leftoff.js +792 -677
- package/docs/SECURITY.md +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +259 -17
- package/mod/hooks/register.js +20 -6
- package/package.json +1 -1
- package/ui/app.css +204 -5
- package/ui/app.js +655 -198
- package/ui/chip-fade.js +1 -1
- package/ui/cp-fold.js +42 -0
- package/ui/decisions.js +81 -21
- package/ui/digest-versions.js +57 -0
- package/ui/fs-side.js +29 -0
- package/ui/html-preview.html +12 -0
- package/ui/html-preview.js +136 -0
- package/ui/icons.js +1 -0
- package/ui/live-status.js +68 -2
- package/ui/live-view.js +36 -5
- package/ui/live-wake.js +47 -0
- package/ui/mcp-setup.js +26 -0
- package/ui/prs.js +28 -10
- package/ui/pull.js +30 -3
- package/ui/relay-client.js +2 -0
- package/ui/render-digest.js +98 -0
- package/ui/render-export.js +128 -0
- package/ui/semdiff.js +58 -27
- package/ui/session-title.js +88 -0
- package/ui/settings-nav.js +1 -1
- package/ui/sglyph.js +2 -2
- package/ui/slash-commands.js +89 -0
- package/ui/slash.js +82 -0
- package/ui/subagents-view.js +41 -0
- package/ui/summary-examples.js +1 -1
- package/ui/tabs.js +10 -3
- package/ui/viewport-fit.js +48 -3
- package/ui/walk-spots.js +35 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in Leftoff (called Checkpoints until 0.4.5), newest first. The board's **What's new** page and `leftoff changelog` both read this file.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
5
|
+
## [0.9.18] - 2026-11-07
|
|
6
|
+
|
|
7
|
+
- **HTML files open as a page.** An .html file a session wrote, from its Files or from a path in a message, now opens on Preview: the page itself, drawn in a locked-down sandbox that can't reach the board, your sign-in or Leftoff's API. Code shows the source, and the choice is remembered on that device. Fonts, scripts and images from the internet stay blocked, and a line says what was blocked and from where. Show lists every address, and Allow loads them (https only) for that view only. Files the page loads from beside it, like ./style.css, aren't in the preview; a second line names them so an unstyled page explains itself. Links in the page don't open; the board tells you where they go. Needs `leftoff restart`, then reload the board.
|
|
8
|
+
- **Walk me through it: Next spot goes from where you are, and stays on screen.** Next spot (n) now picks up from the step you're reading instead of starting over at the first spot, and it sits in the bar under the reading, so it's there however far you scroll. Every spot's chip shows at its line all the time, not only after you jump to it. Hover, focus or tap a chip to see why it's a spot: the walkthrough flagged it, the model picked it, or Leftoff's own check found removed logic, auth, config or error handling. On the last step, Mark reviewed & next goes to the first step you haven't reviewed. It reads "& finish" only once every step is reviewed, and the overview then says so. Reload the board.
|
|
9
|
+
- **The reply box sits right on the keyboard again.** Yesterday's fix kept 64px clear above the keyboard's ⌃⌄✓ bar on iOS 26, and on a real iPhone that showed as an empty band between the reply box and the bar on every keyboard. The bar sits below the area iOS reports, so nothing is kept for it now: full screen is exactly the visible area. The box getting cut off was the board measuring before iOS finished resizing, so it now looks again on every keyboard change and at 0.1, 0.3, 0.6 and 1 s after you tap in, besides every frame for the first second. With the keyboard up, the thread also stays at its newest message instead of sliding back as full screen changes height. Reload the board.
|
|
10
|
+
- **Save a digest, the decision log or a walkthrough as one HTML file.** The digest and the decision log have a quiet Save as HTML (the walkthrough's Download .html is now Save as HTML too), and `leftoff export digest <date> --html`, `leftoff export decisions --scope <repo> --html` and `leftoff export walkthrough <session> --html` write the same file from the terminal. It's one .html with its styles inside and nothing else: no scripts, fonts, images or links out, so it opens offline, in Mail or Files, follows light or dark, reads on a phone and prints. On a phone the board hands it to the share sheet (Save to Files, AirDrop, Mail); elsewhere it downloads. The command writes into the current folder or the file you name with `--out` (`--out -` prints it), never into Leftoff's folder, ~/.claude or ~/.codex, and uploads nothing. Session buttons and links into the board stay on the board. Reload the board for the buttons; the command works after `leftoff upgrade --beta`.
|
|
11
|
+
- **A busy Mac no longer names a repo by its folder.** Leftoff learns a session's repo once, when it starts, and gave git 1.5 seconds to say where the repo's remote is. On a busy Mac that could run out, and the session kept its folder's name, with no GitHub repo, for good. It now waits up to 5 seconds. Needs `leftoff restart`.
|
|
12
|
+
- **Checkpoints and the digest say what got done.** A checkpoint used to keep only the summary's last status line, so the digest and Copy for standup said things like "left off where PR 53 was ready to merge" after it had merged. Now each checkpoint keeps a Got done list, read from the commands the session ran and what they printed: commits, PRs opened and merged, branches pushed, and the last test run with its counts. The summarizer gets that list as the record and is asked for outcomes rather than statuses. If its Stopped at still calls a PR the session merged "ready to merge", that line is replaced with what the facts say, and a "Merge PR 53" next step is dropped. The digest opens with What you got done: per project, in past tense (Merged PR #53: …, Opened PR #54 (not merged yet), commits, tests, decisions, then what's still open), using GitHub's merged list where it has it. Each session's line leads with its outcome. Merged PRs drop out of Today and Start here. A model may only reword the Yesterday line; a reply naming a PR or commit the facts don't have, or calling a merged PR ready, is thrown out and the line is built in code. With no model at all, the digest reads the same. Copy for standup's Yesterday uses the same facts. Checkpoints made before this have no Got done list, so their days read as before, except that GitHub's merged list still corrects a stale "ready to merge". Needs `leftoff restart`, then reload the board. Sessions in your own terminal also need `leftoff upgrade --beta` and a restart of Claude Code.
|
|
13
|
+
- **Claude can search what Leftoff wrote.** `leftoff mcp` is a small MCP server: Claude Code (or Codex) can search your digests, decisions and checkpoints and open one in full, so "what did we decide about the relay?" or "when did the keyboard fix ship?" gets an answer with its source. Five tools: search_digests (words, a date range, a project), search_decisions (a repo or project, a status), search_checkpoints, get_digest (a day, any version) and get_checkpoint (a session's latest, or one by id). Each result carries a stable address (`leftoff://digest/2026-11-06`, `leftoff://decision/gh:owner/repo/D-7f3a`, `leftoff://checkpoint/<session>/<id>`). It only reads, on this machine: it writes nothing, needs no daemon and opens no network connection. Search is plain words: every word must appear, ranked by how often they do and how recent it is. Leftoff never edits Claude's or Codex's settings; add it yourself once with `claude mcp add --scope user leftoff -- leftoff mcp` (Settings → Digest has the line and a Copy button, plus Codex's), and `leftoff doctor` says whether it's added.
|
|
14
|
+
- **A digest keeps its earlier versions.** Making a day's digest again used to replace it with no trace. Now every version this machine makes or reads is kept in its own history (~/.config/checkpoints/history; nothing new goes in the synced folder), and the digest says so quietly under its first line: "v3 · changed since 9:00 AM" (since the version this device last opened). A tap lists the versions; an earlier one opens with a note saying it's earlier and a Back to latest link. Each decision's status changes are recorded with their time too (from the log's own times, so a change made on another Mac keeps its time), and `leftoff mcp` shows them. Needs `leftoff restart`, then reload the board.
|
|
15
|
+
- **`leftoff https on` turns on https://leftoff.localhost by itself.** On a machine that already runs Leftoff, you no longer go back through all of `leftoff setup` to add it. `leftoff https off` turns it off, and `leftoff https` says whether it's on and what's left to do. Run `leftoff upgrade --beta` to get it.
|
|
16
|
+
- **Walk me through any one PR.** When a session opened several PRs, each one now has its own range in Walk me through it. Up to two show as chips; with more, one PRs chip opens a list with each PR's state. A PR's walkthrough reads only that PR's own commits, whatever branch the folder has checked out. If its branch is checked out somewhere, uncommitted work counts too. When the branch isn't on this Mac, or the PR is merged, it reads GitHub's diff with `gh pr diff`. Each PR row in the session panel and on the PRs page has one Open button that asks where: Walk me through it in Leftoff, or GitHub. Test plans use each PR's own walkthrough as well. An opened PR's details in the session panel now use the panel's full width. Needs `leftoff restart`, then reload the board.
|
|
17
|
+
- **Slash commands from the board.** Type / in a session's message box and pick a command: /model, /effort, /compact, /clear, /context or /usage. /model and /effort then list their choices. The command runs in the session, and its bubble shows what Claude Code printed. In a session Leftoff runs, /model and /effort apply to that session only. In a session in your own terminal they're greyed out, because there Claude Code also saves them as your default for new sessions. Run them in that terminal instead. Needs `leftoff restart`, then `leftoff upgrade --beta` and a restart of Claude Code for terminal sessions.
|
|
18
|
+
- **Subagents show as working.** While a subagent works, the session's own conversation goes quiet (the subagent writes to its own file), so the board could look idle, and a turn that launched a background agent was settled as done. Now the session's status reads "2 subagents working" (with a tip naming them), Live shows a line with what the newest one is doing ("Bash: pnpm test") and how long ago, and the status stays working until the subagents finish. Read from Claude Code's own files; an agent silent for 20 minutes is taken as gone so nothing sticks on working.
|
|
19
|
+
- **The board catches up when you come back to it.** On a phone, a board left open in the background could show old sessions and an old Live thread when you returned: iOS cuts the live-update connection while the app is frozen without saying so, the board trusted that connection if its last check-in was under half a minute old, a refresh already in flight when the app froze held new ones back, and a redraw skipped while you had a draft was never made. Now coming back (or the board noticing it was frozen, even when the phone doesn't say so) opens a fresh connection, reads the session list, the open session and Live again, and shows a quiet "Updated" when something changed. A lost stretch of updates (the Mac restarted, another Mac took over, or more happened than it keeps) also reads the board again.
|
|
20
|
+
- **Pull to refresh from the top bar.** On a phone, pulling down on the top row, a session's head or the Live box's head now refreshes wherever the page or the chat is scrolled to, so you no longer scroll a long thread all the way up first. Fields, menus, sideways scrollers and the session sheet still never start a pull.
|
|
21
|
+
- **Decisions you already made don't wait for a second yes.** A question you answered in the session, or a plan you approved, now goes into the decision log as Accepted, marked "automatically", with a Reject next to it. If you answer the same question differently later in the session, the earlier answer is rejected on its own. A one-off choice about the task in front of you (who does a step, whether to push now) isn't recorded at all. The agent's own trade-offs and corrections still wait for you, and so does an answer that would change a decision you accepted before. Your own Accept or Reject is never overridden. Needs `leftoff restart` and `leftoff upgrade --beta`.
|
|
22
|
+
- **https://leftoff.localhost works next to another app's Caddy.** When port 443 already belongs to another Caddy (one serving another app's https://<name>.localhost, say), `leftoff setup` no longer stops at "Something else is using port 443". It writes Leftoff's site to a file and gives you the one `import` line to add to that Caddy's Caddyfile, with the reload command. The daemon then runs no Caddy of its own, and you don't need Caddy installed yourself. When something else holds the port, setup and `leftoff doctor` now name it ("Tailscale (pid 1689)"), and macOS no longer gets a Linux-only fix. Needs `leftoff restart`, then `leftoff setup`.
|
|
23
|
+
- **Tab titles get the room.** A tab's status shape (spinner, !, dot) now sits where its × is, and the × comes back when you point at the tab. The words "Working", "Needs you" and the rest moved to the tab's tip. With many tabs open, the one you're on keeps its full width, the others stay readable and the strip scrolls with a fade where more tabs continue, and a pinned tab shrinks to its pin and status. Reload the board.
|
|
24
|
+
- **Regenerated titles no longer show Leftoff's own message tag.** With some models (Haiku, for one), a new title for a session you talk to from the board could come out as `<channel source="leftoff" …>`. The model now sees only your words, and a reply that still carries the tag is cleaned up. Needs `leftoff restart`, which also renames a session that already got such a title.
|
|
25
|
+
- **The phone's session sheet stays out of your way.** While you type in the reply box, the sheet with checkpoints, What it did and Details steps aside and comes back a beat after the keyboard closes, as it was. Before, it only stepped aside when the board measured a keyboard's worth of lost height, which some keyboards (a third-party one coming up late or in steps) never report, so a raised sheet could sit over the reply box. The sheet also scrolls back up now: in full screen a finger moving down inside it was taken as a pull to refresh, which cancelled the scroll and reloaded the board (the jump), and its head no longer changes height as it scrolls. Drag the raised sheet down from its peek, or from its content scrolled to the top, to lower it; drag the peek up to raise it.
|
|
26
|
+
- **Decisions show where you look.** A session's panel lists every decision it made, with its status and Accept / Reject right there, even after a newer checkpoint; a repo outside a project gets its own Decisions section and a ◌ count on its chip, and an empty log says where decisions come from. The checkpoint fold now names what's inside (approach notes, files…), and another tool's 401 or 403, such as an MCP server's, no longer reads as Claude signed out. Needs `leftoff restart`.
|
|
27
|
+
- **A file named in a message opens the file the sidebar opens.** A relative path Claude mentions was looked for in one folder only (the session's worktree, else its repo root, else its folder), so a path said from a parent folder ("app/docs/specs/a.md" in a session whose folder is app/), or a file in the checkout while the session had moved into its own worktree, said “Not found” while the same file opened from Files. Now it is looked for in the session's folder, its worktree and its repo root, without a repeated folder name, then matched against the session's own Files list; every candidate still passes the same allowed-folders and no-secrets check. “Not found” now names the path and where it looked.
|
|
28
|
+
- **Session names say what the session is doing.** The name is the summarizer's title, or else the first prompt tidied deterministically: no leading “Please”/“Can you”/“Hey”, a pasted GitHub link read as “PR 408”, a capital first letter, cut at a word with “…” and never on a dangling “and”/“the”/“to”. A “folder@branch · …” stuck to the front of a name by whatever named it comes off; folder@branch is now a small muted second line under the name (full folder in its tip) in full screen's bar, on the session page and in its panel, and the digest uses the same name. A session whose only name is such a stand-in gets a generated one after its first turn, as a first-prompt name does.
|
|
29
|
+
- **Fewer false "isn't signed in" warnings.** A login date in the past from Claude Code no longer turns the banner red on its own; only a turn that actually failed does. An MCP server that couldn't sign in, or a differently worded sign-in reminder, no longer reads as Claude being signed out, and Claude Code's config folder can have a space in its path. Needs `leftoff restart`.
|
|
30
|
+
- **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.
|
|
31
|
+
- **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`.
|
|
32
|
+
- **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`.
|
|
33
|
+
- **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.
|
|
7
34
|
- **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.
|
|
8
35
|
- **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`.
|
|
9
36
|
- **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`.
|
|
@@ -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 C(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 C(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 L={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(L);var b=["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 got done or was working on, one sentence, concrete nouns: files, features, errors; past tense for what is finished, e.g. "Merged PR #53, which keeps the reply box above the keyboard">',"Stopped at: <the exact state when it went quiet, as outcomes: what passed, what failed, what is half-done, what is merged and what is not; 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:".',`Say outcomes, not statuses: "Merged PR #53" or "Opened PR #53, not merged yet", never "PR 53 was ready to merge". The "Verified from the commands' output" line is the record of commits, PRs, pushes and tests: never contradict it, and never claim a merge, commit or passing test it does not show.`,"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."],x=['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, and neither is a one-off choice about this task right now (who does a step, whether to commit, push or deploy now). 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:[...b,...x,"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:[...b,...x,...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:[...b,...x,...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 $="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. "+$+" 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 D=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([...D,..."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 j="leftoff ↗",W=" · ",w=7429,k="\x1B",E=k+"\\";function F(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)||w)+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 w}function B(e,{port:t=w}={}){let n=e&&typeof e.session_id=="string"?e.session_id.trim():"";return n?F(q(t,n),j):""}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 G(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":"")+W+t,r.join(`
|
|
7
|
+
`)}function Z(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:Z(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 v(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(S=>String(S).startsWith("--wrap=")),f=p?K(p.slice(7)):null,u=w;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 G(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 S={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(S)+`
|
|
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 O(e=0){try{return i.readFileSync(e,"utf8")}catch{return""}}var y="";try{y=v(process.argv.slice(2),O())}catch{}y&&process.stdout.write(y+`
|
|
9
|
+
`);process.exitCode=0;
|