@leftoff/cli 0.9.8-beta.2 → 0.9.8-beta.21

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.
@@ -111,6 +111,8 @@ Only Macs registered for that user can connect: the first with `leftoff relay re
111
111
 
112
112
  Every few seconds (`peers.pollMs`, default 4 s) each daemon asks every machine that accepts for `needs.list`, so another Mac's permission prompts and questions show on this board with a ⌂ machine chip. Answer, reply and Go on another machine's session go out as envelopes. When that isn't possible, the button is grey and says why: not allowed on that machine, unreachable, key changed, no key yet (that Mac needs 0.5.2), or no route. Answering from a phone keeps its own rules (Settings → Privacy & security), and Go stays a local-browser action.
113
113
 
114
+ Phone notifications (0.9.8) ride the same envelopes: `push.sync` carries the Web Push subscriptions a browser turned on (endpoint, keys, a device label) with the VAPID keys they were made with, plus 30-day removal tombstones; each side merges (newer word per endpoint wins) and answers with its own, so every Mac can push for its own sessions and only those. It is answered even when a Mac doesn't allow incoming commands, because it runs nothing there. Kept on each Mac in `~/.config/checkpoints/push/` (0600), never in the synced folder. Shared when a device subscribes or unsubscribes, at start, and every 20 minutes.
115
+
114
116
  ## Pairing through Syncthing (0.5.3)
115
117
 
116
118
  Before two Macs share a folder they have no channel to each other, so pairing rides on Syncthing itself. Leftoff never opens a port for it.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.9.8-beta.2",
3
+ "version": "0.9.8-beta.21",
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/core.js CHANGED
@@ -299,8 +299,28 @@ export function resumeCommand(session) {
299
299
  const id = session.id || session.sessionId
300
300
  const cwd = resumeDir(session)
301
301
  const wt = resumeWorktreeName(session)
302
- return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + (isCodex(session) ? codexInvocation(id) : claudeInvocation(session, [...(wt ? ['--worktree', wt] : []), '--resume', id]))
302
+ const args = [...(wt ? ['--worktree', wt] : []), '--resume', id]
303
+ return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + (isCodex(session) ? codexInvocation(id) : wasLiveSession(session) ? liveInvocation(args) : claudeInvocation(session, args))
304
+ }
305
+
306
+ // 0.9.8: a session started with `leftoff live` (it had Leftoff's Live channel)
307
+ // resumes as one, `leftoff live -- --resume <id>`, so it shows up Live again.
308
+ // Codex and Ollama sessions resume their own way (Live is Claude Code only).
309
+ export function wasLiveSession(session) {
310
+ if (!session || isCodex(session) || (session.backend && session.backend.kind === 'ollama')) return false
311
+ return !!(session.wasLive || (session.liveLink && session.liveLink.on))
312
+ }
313
+ // 0.9.8: the same resume, but as Live (Leftoff's channel), for a Claude Code
314
+ // session that wasn't Live: exit it, then run this. null when it already resumes
315
+ // Live, or can't be Live (Codex, Ollama).
316
+ export function liveResumeCommand(session) {
317
+ if (!session || isCodex(session) || (session.backend && session.backend.kind === 'ollama') || wasLiveSession(session)) return null
318
+ const id = session.id || session.sessionId
319
+ const cwd = resumeDir(session)
320
+ const wt = resumeWorktreeName(session)
321
+ return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + liveInvocation([...(wt ? ['--worktree', wt] : []), '--resume', id])
303
322
  }
323
+ export function liveInvocation(args = []) { return 'leftoff live --' + (args.length ? ' ' + args.map(shArg).join(' ') : '') }
304
324
 
305
325
  // 0.8.0: a Codex session (its rollout under ~/.codex/sessions) resumes with `codex resume <id>`.
306
326
  export function isCodex(session) { return !!session && session.codingAgent === 'codex' }
@@ -547,7 +567,7 @@ const SESSION_ORDER = ['id', 'title', 'title_source', 'machine', 'cwd', 'repo',
547
567
  export const SESSION_EXTRA = ['inTurn', 'needsInput', 'backgroundTasks', 'turnsSinceCheckpoint', 'dormantStretch',
548
568
  'finalCheckpointDone', 'firstPrompt', 'lastPreview', 'lastNotification', 'lastEventAt', 'statusChanges', 'folder',
549
569
  'pid', 'tty', 'pidStartedAt', 'pidSource', 'term', 'closedAt', 'answers', 'lastNotificationMessage', 'needsInputAt', 'question', 'questionAt',
550
- 'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints', 'liveLink']
570
+ 'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints', 'liveLink', 'transcriptTurns', 'wasLive']
551
571
 
552
572
  export const SESSION_FIELDS = ['id', 'title', 'titleSource', 'machine', 'cwd', 'repo', 'transcriptPath', 'model', 'surface', 'capture',
553
573
  'status', 'startedAt', 'lastActivityAt', 'lastHeartbeatAt', 'endedAt', 'endReason', 'turns', 'lastCheckpointId',
@@ -800,7 +820,7 @@ export function checkpointDue(s, now, cfg = DEFAULT_CONFIG) {
800
820
  export function isEmptySession(s, checkpointCount = 0) {
801
821
  if (!s) return true
802
822
  // 0.7.1: a prompt counts. A long first turn (a worktree bug fix) has no completed turn yet.
803
- return !(Number(s.turns) > 0) && !s.title && !s.firstPrompt && !(Number(checkpointCount) > 0) && !s.lastCheckpointId &&
823
+ return !(Number(s.turns) > 0) && !(Number(s.transcriptTurns) > 0) && !s.title && !s.firstPrompt && !(Number(checkpointCount) > 0) && !s.lastCheckpointId &&
804
824
  !(Number(s.historyTurns) > 0) && s.startSource !== 'resume'
805
825
  }
806
826
 
@@ -876,7 +896,7 @@ export function applyEvent(s, ev) {
876
896
  s.needsInput = false
877
897
  s.question = null
878
898
  s.lastNotification = null
879
- if (d.prompt && !s.firstPrompt) s.firstPrompt = clip(String(d.prompt).replace(/^\s*<channel\b[^>]*>([\s\S]*?)<\/channel>\s*$/, '$1').trim(), 300) // 0.9.8: a Live link message, without its wrapper
899
+ if (d.prompt && (!s.firstPrompt || /^\s*<channel\b/.test(s.firstPrompt))) s.firstPrompt = clip(String(d.prompt).replace(/^\s*<channel\b[^>]*>/, '').replace(/<\/channel>\s*$/, '').trim(), 300) || s.firstPrompt || null // 0.9.8: a Live link message, without its wrapper (even one the 300-character clip cut short)
880
900
  if (d.countTurn === true) s.turns = (s.turns || 0) + 1 // 0.8.1: Codex counts a turn at its prompt when no task_started came first
881
901
  if (s.capture === 'hooks') { s.inTurn = true; s.activeSince = s.activeSince || at }
882
902
  }
@@ -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.2' // keep in step with .claude-plugin/plugin.json
31
+ const MOD_VERSION = '0.9.8-beta.21' // 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.2",
3
+ "version": "0.9.8-beta.21",
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",