thinkpool-pair 0.7.49 → 0.7.50
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/bridge.mjs +2 -16
- package/claude-session.mjs +6 -14
- package/launcher.mjs +23 -37
- package/package.json +2 -2
- package/service.mjs +1 -29
package/bridge.mjs
CHANGED
|
@@ -142,16 +142,6 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
142
142
|
process.exit(0)
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Restart an installed boot-persistent service (re-fetches @latest, sessions resume):
|
|
146
|
-
// `thinkpool-pair restart-service [<ROOM>]` — no room → the account service.
|
|
147
|
-
if (argv[0] === 'restart-service') {
|
|
148
|
-
const arg1 = argv[1] || ''
|
|
149
|
-
const svcRoom = (!arg1 || arg1.startsWith('-')) ? null : arg1.toUpperCase().trim()
|
|
150
|
-
const svc = await import('./service.mjs')
|
|
151
|
-
svc.restartService(svcRoom)
|
|
152
|
-
process.exit(0)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
145
|
// Account mode (Phase 1, docs/specs/2026-06-16-account-bridge.md): one bridge per
|
|
156
146
|
// ACCOUNT, not per room. `login` links this device; `bind <ROOM> <dir>` maps a
|
|
157
147
|
// session to a working dir on this machine; a bare invocation discovers + serves
|
|
@@ -195,12 +185,8 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
195
185
|
child.on('exit', (code) => process.exit(code == null ? 0 : code))
|
|
196
186
|
}),
|
|
197
187
|
serveAccountForeground: async () => { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) },
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// (the service runs in its own launchd/systemd process; this launcher isn't a bridge).
|
|
201
|
-
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []) },
|
|
202
|
-
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room) },
|
|
203
|
-
restartService: ({ room = null } = {}) => { svc.restartService(room) },
|
|
188
|
+
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []); process.exit(0) },
|
|
189
|
+
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room); process.exit(0) },
|
|
204
190
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
205
191
|
// Provider config is NOT terminal — it writes provider.json and returns to the
|
|
206
192
|
// menu so you can pick a model, switch back, or do something else. (It used to
|
package/claude-session.mjs
CHANGED
|
@@ -361,8 +361,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
361
361
|
// alone defers to the server-side `tengu_prompt_suggestion` feature gate, which
|
|
362
362
|
// Anthropic flips on/off during rollout — so the ghost-text chip silently died
|
|
363
363
|
// mid-day with NO change on our side. CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION is the
|
|
364
|
-
// documented override
|
|
365
|
-
// on past the gate). opts.env REPLACES the child env, so spread the inherited one.
|
|
364
|
+
// documented override. opts.env REPLACES the child env, so spread the inherited one.
|
|
366
365
|
opts.env = { ...(env || process.env), CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION: '1' }
|
|
367
366
|
|
|
368
367
|
// Before the SDK replays this transcript, heal any cross-provider tool blocks
|
|
@@ -473,15 +472,9 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
473
472
|
// Claude Code's own predicted next prompt (promptSuggestions opt-in).
|
|
474
473
|
// Arrives once per turn AFTER `result`; surfaced verbatim as ephemeral
|
|
475
474
|
// chrome (a ghost-text chip in the room composer), never persisted.
|
|
476
|
-
process.stderr.write(`\n ◆ [suggestion] received: ${JSON.stringify(m.suggestion ?? null)}\n`)
|
|
477
475
|
if (m.suggestion) emit({ kind: 'suggestion', text: m.suggestion })
|
|
478
476
|
break
|
|
479
477
|
default:
|
|
480
|
-
// Debug: log any unhandled message type so we can see if prompt_suggestion
|
|
481
|
-
// is arriving under a different name. Remove once confirmed working.
|
|
482
|
-
if (m.type && !['stream_event', 'user', 'agent_metadata'].includes(m.type)) {
|
|
483
|
-
process.stderr.write(` [debug] unhandled sdk msg type=${m.type}${m.suggestion ? ` (HAS .suggestion=${JSON.stringify(m.suggestion)})` : ''}\n`)
|
|
484
|
-
}
|
|
485
478
|
break
|
|
486
479
|
}
|
|
487
480
|
}
|
|
@@ -541,12 +534,11 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
541
534
|
async abort() {
|
|
542
535
|
try { await q?.interrupt?.() } catch { /* noop */ }
|
|
543
536
|
// interrupt() stops the turn but emits NO terminal message. Without one the
|
|
544
|
-
// struct
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
// surfaced a result for the interrupt. (2026-06-22)
|
|
537
|
+
// struct stream ends on a non-terminal event, so on resume the SDK treats the
|
|
538
|
+
// turn as INCOMPLETE and auto-continues it — the 2026-06-22 "zombie turn that
|
|
539
|
+
// resumes itself" storm. Emit a terminal result so the turn is marked DONE +
|
|
540
|
+
// persists across refresh. Guarded on turnActive to avoid a double-emit if the
|
|
541
|
+
// SDK already surfaced one for the interrupt. (restored from 0.7.49)
|
|
550
542
|
if (turnActive) { turnActive = false; emit({ kind: 'result', subtype: 'aborted', sessionId }) }
|
|
551
543
|
},
|
|
552
544
|
end() { closed = true; clearInterval(stallTimer); input.end(); try { ac.abort() } catch { /* noop */ } },
|
package/launcher.mjs
CHANGED
|
@@ -125,10 +125,8 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
125
125
|
const how = await askChoice('run it', [
|
|
126
126
|
{ label: 'As an always-on background service', hint: 'recommended — survives reboot, auto-updates' },
|
|
127
127
|
{ label: 'Here in this terminal (foreground)', hint: 'stops when you close it' },
|
|
128
|
-
{ label: '← Back to main menu', hint: '' },
|
|
129
128
|
])
|
|
130
|
-
if (how ===
|
|
131
|
-
if (how === 0) { await actions.installService({ room: null }); resync() }
|
|
129
|
+
if (how === 0) await actions.installService({ room: null })
|
|
132
130
|
else await actions.serveAccountForeground()
|
|
133
131
|
}
|
|
134
132
|
|
|
@@ -136,17 +134,15 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
136
134
|
const what = await askChoice('install for', [
|
|
137
135
|
{ label: 'All my sessions (account)', hint: state.loggedIn ? '' : 'needs login first' },
|
|
138
136
|
{ label: 'One specific room', hint: 'no login needed' },
|
|
139
|
-
{ label: '← Back to main menu', hint: '' },
|
|
140
137
|
])
|
|
141
|
-
if (what === 2) return
|
|
142
138
|
if (what === 0) {
|
|
143
139
|
if (!state.loggedIn) { io.print('\n ' + C.yellow('Account service needs a login first (pick “Serve all my sessions”).')); return }
|
|
144
|
-
await actions.installService({ room: null });
|
|
140
|
+
await actions.installService({ room: null }); return
|
|
145
141
|
}
|
|
146
|
-
const room = (await io.ask(`\n Room code ${C.
|
|
142
|
+
const room = (await io.ask(`\n Room code ${C.cyan('▸')} `)).toUpperCase().trim()
|
|
147
143
|
if (!room) { io.print(' ' + C.yellow('no room — back to menu')); return }
|
|
148
144
|
const agent = state.agents[0] || { cmd: 'claude' }
|
|
149
|
-
await actions.installService({ room, agentCmd: agent.cmd })
|
|
145
|
+
await actions.installService({ room, agentCmd: agent.cmd })
|
|
150
146
|
}
|
|
151
147
|
|
|
152
148
|
const providerMenu = async () => {
|
|
@@ -174,49 +170,39 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
174
170
|
const pick = await askChoice('\n change', [
|
|
175
171
|
{ label: 'LLM provider', hint: `current: ${state.provider}` },
|
|
176
172
|
{ label: 'Account', hint: state.loggedIn ? `linked: ${state.email || 'yes'}` : 'not linked' },
|
|
177
|
-
{ label: 'Restart the background service', hint: state.accountSvc ? 'apply the latest update now' : 'none installed' },
|
|
178
173
|
{ label: 'Remove the background service', hint: state.accountSvc ? '' : 'none installed' },
|
|
179
174
|
{ label: '← Back to main menu', hint: '' },
|
|
180
|
-
],
|
|
181
|
-
if (pick ===
|
|
175
|
+
], 4) // Enter = Back
|
|
176
|
+
if (pick === 3) return
|
|
182
177
|
if (pick === 0) await providerMenu()
|
|
183
178
|
else if (pick === 1) { await actions.login(); resync(); return }
|
|
184
|
-
else if (pick === 2) {
|
|
185
|
-
if (state.accountSvc) { await actions.restartService({ room: null }); resync() }
|
|
186
|
-
else io.print(' ' + C.yellow('no background service installed'))
|
|
187
|
-
}
|
|
188
179
|
else {
|
|
189
|
-
if (state.accountSvc)
|
|
180
|
+
if (state.accountSvc) await actions.uninstallService({ room: null })
|
|
190
181
|
else io.print(' ' + C.yellow('no background service installed'))
|
|
191
182
|
}
|
|
192
183
|
}
|
|
193
184
|
}
|
|
194
185
|
|
|
195
186
|
// ── menu loop ──
|
|
196
|
-
// pairRoom re-execs + waits
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// The trailing "press Enter" lands on every returning path.
|
|
187
|
+
// Real actions either replace the process (pairRoom re-execs + waits; serve/install
|
|
188
|
+
// call process.exit) or run forever (account foreground), so a real "do something"
|
|
189
|
+
// choice never returns here. Dry-run/mock actions just print + return → the loop
|
|
190
|
+
// re-renders. The "press Enter" only lands on a no-op / back-out path.
|
|
201
191
|
for (;;) {
|
|
202
192
|
header()
|
|
203
193
|
io.print(' ' + C.bold('What do you want to do?') + '\n')
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
{
|
|
207
|
-
{
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
if (
|
|
216
|
-
else if (pick.key === 'serve') await serveAccount()
|
|
217
|
-
else if (pick.key === 'service') await serviceMenu()
|
|
218
|
-
else if (pick.key === 'restart') { await actions.restartService({ room: null }); resync() }
|
|
219
|
-
else if (pick.key === 'settings') await settingsMenu()
|
|
194
|
+
const choice = await askChoice('choose', [
|
|
195
|
+
{ label: 'Pair one room now', hint: 'run here, Ctrl-C stops it' },
|
|
196
|
+
{ label: 'Serve all my sessions', hint: 'account mode — needs login' },
|
|
197
|
+
{ label: 'Always-on background service', hint: 'survives reboot, auto-updates' },
|
|
198
|
+
{ label: 'Settings', hint: 'provider · account · remove service' },
|
|
199
|
+
{ label: 'Quit', hint: '' },
|
|
200
|
+
])
|
|
201
|
+
if (choice === 4) return // Quit
|
|
202
|
+
if (choice === 0) await pairRoom()
|
|
203
|
+
else if (choice === 1) await serveAccount()
|
|
204
|
+
else if (choice === 2) await serviceMenu()
|
|
205
|
+
else if (choice === 3) await settingsMenu()
|
|
220
206
|
await io.ask('\n ' + C.dim('press Enter to return to the menu…'))
|
|
221
207
|
}
|
|
222
208
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.50",
|
|
4
4
|
"description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node": ">=18"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
30
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.185",
|
|
31
31
|
"@supabase/supabase-js": "^2.45.0"
|
|
32
32
|
},
|
|
33
33
|
"optionalDependencies": {
|
package/service.mjs
CHANGED
|
@@ -126,40 +126,12 @@ export function installService(room, cmdArgs = []) {
|
|
|
126
126
|
const idleSecs = Math.max(10, parseInt(process.env.THINKPOOL_PAIR_UPDATE_IDLE, 10) || 90)
|
|
127
127
|
const autoUpdate = process.platform === 'win32'
|
|
128
128
|
? 'auto-update: a new thinkpool-pair@latest applies on the next login/restart (Windows has no in-process self-update).'
|
|
129
|
-
: `auto-update: checks npm ~every 30 min and restarts to apply a new version once sessions are idle (≥${idleSecs}s quiet; they resume in place). Not instant — to update now, restart it (
|
|
129
|
+
: `auto-update: checks npm ~every 30 min and restarts to apply a new version once sessions are idle (≥${idleSecs}s quiet; they resume in place). Not instant — to update now, restart it (re-run install-service, or 'launchctl kickstart -k gui/$(id -u)/io.thinkpool.pair.${slug(room)}' on macOS).`
|
|
130
130
|
const removeArg = room ? ` ${room}` : ''
|
|
131
131
|
const what = room ? `room ${room}` : 'your account (auto-serves every session)'
|
|
132
132
|
process.stderr.write(` ◆ ${what}\n ◆ ${a.note}\n ◆ ${autoUpdate}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: thinkpool-pair uninstall-service${removeArg}\n\n`)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
// Restart an already-installed boot-persistent service so it re-resolves
|
|
136
|
-
// thinkpool-pair@latest right now — the supported "apply the update now" path.
|
|
137
|
-
// launchd/systemd relaunch `npx … @latest`, so a just-published version is picked
|
|
138
|
-
// up; any active session resumes in place (the bridge restores from disk).
|
|
139
|
-
// room falsy → the ACCOUNT service. Returns true on success.
|
|
140
|
-
export function restartService(room) {
|
|
141
|
-
const plat = process.platform
|
|
142
|
-
if (!isServiceInstalled(room)) {
|
|
143
|
-
process.stderr.write(' ⚠ no background service installed to restart — install one first.\n')
|
|
144
|
-
return false
|
|
145
|
-
}
|
|
146
|
-
const id = label(room)
|
|
147
|
-
try {
|
|
148
|
-
if (plat === 'darwin') execSync(`launchctl kickstart -k gui/$(id -u)/${id}`, { stdio: 'inherit' })
|
|
149
|
-
else if (plat === 'linux') execSync(`systemctl --user restart ${id}.service`, { stdio: 'inherit' })
|
|
150
|
-
else {
|
|
151
|
-
// Windows Startup .cmd has no managed daemon to kick — guide the manual path.
|
|
152
|
-
process.stderr.write(' ⚠ Windows has no managed daemon — close the running thinkpool-pair window and re-launch the Startup .cmd (or reboot) to restart.\n')
|
|
153
|
-
return false
|
|
154
|
-
}
|
|
155
|
-
process.stderr.write(` ◆ restarted ${id} — it re-fetches thinkpool-pair@latest; any active session resumes in place.\n`)
|
|
156
|
-
return true
|
|
157
|
-
} catch (e) {
|
|
158
|
-
process.stderr.write(` ⚠ restart failed: ${e.message}\n`)
|
|
159
|
-
return false
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
135
|
// Is a boot-persistent service already installed? (artifact file present.)
|
|
164
136
|
// room falsy → checks the ACCOUNT service. Lets the main run skip the offer +
|
|
165
137
|
// avoid starting a second bridge that would race the service.
|