thinkpool-pair 0.7.57 → 0.7.59
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 +45 -3
- package/claude-session.mjs +3 -0
- package/cmux.mjs +14 -5
- package/launcher.mjs +4 -4
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -222,6 +222,29 @@ if (argv[0] === 'serve') {
|
|
|
222
222
|
process.exit(0)
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// `npx thinkpool-pair cmux-doctor` — explain why the cmux service tier did/didn't engage.
|
|
226
|
+
// Detection is env-based (CMUX_WORKSPACE_ID/SURFACE_ID); the socket is only needed for the
|
|
227
|
+
// actual surface spawn. This prints both so a "it didn't detect cmux" report is one command.
|
|
228
|
+
if (argv[0] === 'cmux-doctor') {
|
|
229
|
+
const cmux = await import('./cmux.mjs')
|
|
230
|
+
const env = process.env
|
|
231
|
+
const w = (s) => process.stderr.write(s + '\n')
|
|
232
|
+
const reachable = cmux.cmuxReachable()
|
|
233
|
+
w('\n thinkpool-pair · cmux doctor')
|
|
234
|
+
w(` CMUX_WORKSPACE_ID : ${env.CMUX_WORKSPACE_ID || '(unset)'}`)
|
|
235
|
+
w(` CMUX_SURFACE_ID : ${env.CMUX_SURFACE_ID || '(unset)'}`)
|
|
236
|
+
w(` CMUX_SOCKET_PATH : ${env.CMUX_SOCKET_PATH || '(unset)'}`)
|
|
237
|
+
w(` cmux on PATH : ${onPath('cmux')}`)
|
|
238
|
+
w(` inCmux() : ${cmux.inCmux()}`)
|
|
239
|
+
w(` cmux ping : ${reachable ? 'ok' : 'FAILED (socket unreachable)'}`)
|
|
240
|
+
w(` detectCmux() : ${cmux.detectCmux()} ${cmux.detectCmux() ? '→ cmux-surface offered' : '→ falls back to OS service'}`)
|
|
241
|
+
w(` cmux service set : ${cmux.isCmuxServiceInstalled()}`)
|
|
242
|
+
if (!cmux.inCmux()) w('\n ⚠ not launched from cmux (no CMUX_WORKSPACE_ID/SURFACE_ID). Run this INSIDE a cmux\n terminal surface — a plain Terminal/iTerm window will not be detected.')
|
|
243
|
+
else if (!reachable) w('\n ⚠ in cmux, but the automation socket is unreachable. detectCmux() is env-based so\n the surface is still offered, but the actual spawn needs the socket — if install\n falls back to the OS service, enable cmux automation/socket access in cmux settings.')
|
|
244
|
+
else w('\n ✓ cmux detected and drivable — the cmux-surface service tier is available.')
|
|
245
|
+
process.exit(0)
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
// One-command launcher: a bare invocation on a TTY (or the `setup` alias) opens the
|
|
226
249
|
// interactive menu instead of going straight to account mode. The menu only ASSEMBLES
|
|
227
250
|
// existing commands — it re-execs `<ROOM>`, or calls installService / runLogin /
|
|
@@ -262,12 +285,31 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
262
285
|
if (room === null && cmux.isCmuxServiceInstalled()) { cmux.restartCmuxService(); process.stderr.write('\n ◆ restarted the cmux-hosted account service — sessions resume in place.\n') }
|
|
263
286
|
else svc.restartService(room)
|
|
264
287
|
},
|
|
288
|
+
// "Restart & update the bridge" — re-pin the service to THIS launcher's version
|
|
289
|
+
// (you launched via @latest, so it's the newest publish) and restart. The cmux
|
|
290
|
+
// surface re-pins + respawns; the OS service is reinstalled (rewrites the plist/unit
|
|
291
|
+
// to the new version and reloads). Pinned throughout — no auto-roll.
|
|
292
|
+
restartUpdateService: ({ room = null } = {}) => {
|
|
293
|
+
if (room === null && cmux.isCmuxServiceInstalled()) {
|
|
294
|
+
cmux.restartCmuxService({ version: PKG_VERSION })
|
|
295
|
+
process.stderr.write(`\n ◆ updated the cmux account service to v${PKG_VERSION} and restarted it — sessions resume in place.\n`)
|
|
296
|
+
} else {
|
|
297
|
+
process.stderr.write(`\n ◆ updating the background service to v${PKG_VERSION} and restarting…\n`)
|
|
298
|
+
svc.installService(room, []) // rewrites the plist/unit at this version + reloads = update + restart
|
|
299
|
+
}
|
|
300
|
+
},
|
|
265
301
|
// Account service hosted as a cmux surface — offered only inside cmux, with its
|
|
266
|
-
// weaker-persistence caveat shown first (launcher.mjs serveAccount).
|
|
302
|
+
// weaker-persistence caveat shown first (launcher.mjs serveAccount). Falls back to
|
|
303
|
+
// the OS service if the cmux socket can't actually be driven (detection is env-based).
|
|
267
304
|
detectCmux: () => cmux.detectCmux(),
|
|
268
305
|
installAccountCmux: () => {
|
|
269
|
-
|
|
270
|
-
|
|
306
|
+
try {
|
|
307
|
+
const rec = cmux.installCmuxService({ version: PKG_VERSION })
|
|
308
|
+
process.stderr.write(`\n ◆ account service now runs as a cmux surface ("${cmux.WS_NAME}") — visible and killable.\n ◆ it restarts itself on crash; stop it with: npx thinkpool-pair uninstall-service\n ◆ pinned to v${rec.version}.\n\n`)
|
|
309
|
+
} catch (e) {
|
|
310
|
+
process.stderr.write(`\n ⚠ couldn't spawn a cmux surface (${e.message || e}). The cmux automation socket may be\n disabled — run 'npx thinkpool-pair cmux-doctor' to check. Falling back to the system service.\n`)
|
|
311
|
+
svc.installService(null, [])
|
|
312
|
+
}
|
|
271
313
|
},
|
|
272
314
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
273
315
|
// Provider config is NOT terminal — it writes provider.json and returns to the
|
package/claude-session.mjs
CHANGED
|
@@ -343,6 +343,9 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
343
343
|
'Bias strongly toward DOING the work, not planning it. For a normal "add / fix / change / build X" request, just make the change directly.',
|
|
344
344
|
'Do NOT enter plan mode, do NOT call ExitPlanMode, and do NOT auto-invoke a brainstorming/planning skill UNLESS the user has switched the room into Plan mode or explicitly asks you to plan, design, or brainstorm first.',
|
|
345
345
|
'Any host-global instruction that says you must always brainstorm or plan before creative work does NOT apply here — this room is the exception.',
|
|
346
|
+
'LINKS & ARTIFACTS (hard rule for ThinkPool Code rooms): the room is driven from a phone or browser, and the bridge runs on the host machine — so a local filesystem path, a file:// URL, or a localhost address only opens on the host and is useless to the room (a partner literally cannot open it).',
|
|
347
|
+
'Whenever you produce an HTML artifact (a demo, mockup, preview, report, or page) or surface ANY link meant to be opened or shared, NEVER hand the room a local path, a file:// URL, or a localhost/127.0.0.1 address. Publish it to a GitHub-shareable URL that renders in a browser — push the HTML to a GitHub repo and give its GitHub Pages URL (or an equivalent raw-HTML URL that actually renders, not raw.githubusercontent.com which serves HTML as plain text) — so anyone in the room can open it. The same applies to any other link you surface: it must be one the room can reach, not a host-only path.',
|
|
348
|
+
'If you cannot publish a shareable link, say so and ask how to proceed — do not fall back to handing over a local path.',
|
|
346
349
|
].join(' '),
|
|
347
350
|
// Needed for live thinking-token progress (SDKThinkingTokensMessage) to
|
|
348
351
|
// flow during a turn. We ignore the fine-grained stream_event partials in
|
package/cmux.mjs
CHANGED
|
@@ -55,9 +55,14 @@ export function cmuxReachable(run = cmux) {
|
|
|
55
55
|
try { run(['ping'], { timeout: 4000 }); return true } catch { return false }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
// Detection keys ONLY on the env cmux injects into every surface it spawns — NOT on
|
|
59
|
+
// whether the socket answers. The socket ping proved flaky (it needs CMUX_SOCKET_PASSWORD
|
|
60
|
+
// and can broken-pipe even from a real surface), and gating detection on it silently
|
|
61
|
+
// killed the cmux offer in 0.7.57. If the env says cmux we offer the cmux tier; if the
|
|
62
|
+
// actual new-workspace spawn then can't reach the socket, installCmuxService throws and
|
|
63
|
+
// the caller falls back to the OS service. (cmuxReachable stays exported for cmux-doctor.)
|
|
64
|
+
export function detectCmux(env = process.env) {
|
|
65
|
+
return inCmux(env)
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export function readRecord(recordFile = RECORD) {
|
|
@@ -119,11 +124,15 @@ export function installCmuxService({ version, cwd = HOME, run = cmux, recordFile
|
|
|
119
124
|
|
|
120
125
|
// Restart = close the surface + spawn a fresh one. The account supervisor restores
|
|
121
126
|
// every active session from disk (session-store crash-resume), so sessions resume
|
|
122
|
-
// in place — same contract as an OS-service restart.
|
|
123
|
-
|
|
127
|
+
// in place — same contract as an OS-service restart. Passing `version` re-pins the
|
|
128
|
+
// surface command to that version first (the "Restart & update the bridge" path):
|
|
129
|
+
// the command stays PINNED (never @latest — no auto-roll), just bumped to the version
|
|
130
|
+
// the user explicitly launched.
|
|
131
|
+
export function restartCmuxService({ run = cmux, recordFile = RECORD, version } = {}) {
|
|
124
132
|
const rec = readRecord(recordFile)
|
|
125
133
|
if (!rec) return false
|
|
126
134
|
closeSurface(run, rec)
|
|
135
|
+
if (version) { rec.version = version; rec.command = serviceCommand(version) }
|
|
127
136
|
rec.workspace = spawnSurface(run, rec.command)
|
|
128
137
|
rec.installedAt = new Date().toISOString()
|
|
129
138
|
fs.writeFileSync(recordFile, JSON.stringify(rec, null, 2))
|
package/launcher.mjs
CHANGED
|
@@ -192,7 +192,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
192
192
|
const pick = await askChoice('\n change', [
|
|
193
193
|
{ label: 'LLM provider', hint: `current: ${state.provider}` },
|
|
194
194
|
{ label: 'Account', hint: state.loggedIn ? `linked: ${state.email || 'yes'}` : 'not linked' },
|
|
195
|
-
{ label: 'Restart the
|
|
195
|
+
{ label: 'Restart & update the bridge', hint: state.accountSvc ? 'pull the newest published version, then restart — sessions resume' : 'none installed' },
|
|
196
196
|
{ label: 'Remove the background service', hint: state.accountSvc ? '' : 'none installed' },
|
|
197
197
|
{ label: '← Back to main menu', hint: '' },
|
|
198
198
|
], 5) // Enter = Back
|
|
@@ -200,7 +200,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
200
200
|
if (pick === 0) await providerMenu()
|
|
201
201
|
else if (pick === 1) { await actions.login(); resync(); return }
|
|
202
202
|
else if (pick === 2) {
|
|
203
|
-
if (state.accountSvc) { await actions.
|
|
203
|
+
if (state.accountSvc) { await actions.restartUpdateService({ room: null }); resync() }
|
|
204
204
|
else io.print(' ' + C.yellow('no background service installed'))
|
|
205
205
|
}
|
|
206
206
|
else {
|
|
@@ -225,7 +225,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
225
225
|
{ key: 'serve', label: 'Serve all my sessions', hint: 'account mode — needs login' },
|
|
226
226
|
{ key: 'service', label: 'Always-on background service', hint: 'survives reboot, restarts on crash' },
|
|
227
227
|
]
|
|
228
|
-
if (state.accountSvc) items.push({ key: 'restart', label: 'Restart the
|
|
228
|
+
if (state.accountSvc) items.push({ key: 'restart', label: 'Restart & update the bridge', hint: 'pull the newest version + restart — sessions resume' })
|
|
229
229
|
items.push({ key: 'settings', label: 'Settings', hint: 'provider · account · restart · remove service' })
|
|
230
230
|
items.push({ key: 'quit', label: 'Quit', hint: '' })
|
|
231
231
|
const pick = items[await askChoice('choose', items)]
|
|
@@ -233,7 +233,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
233
233
|
if (pick.key === 'pair') await pairRoom()
|
|
234
234
|
else if (pick.key === 'serve') await serveAccount()
|
|
235
235
|
else if (pick.key === 'service') await serviceMenu()
|
|
236
|
-
else if (pick.key === 'restart') { await actions.
|
|
236
|
+
else if (pick.key === 'restart') { await actions.restartUpdateService({ room: null }); resync() }
|
|
237
237
|
else if (pick.key === 'settings') await settingsMenu()
|
|
238
238
|
await io.ask('\n ' + C.dim('press Enter to return to the menu…'))
|
|
239
239
|
}
|