thinkpool-pair 0.7.39 → 0.7.40
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/account.mjs +1 -8
- package/bridge.mjs +4 -23
- package/claude-session.mjs +0 -23
- package/launcher.mjs +18 -42
- package/package.json +1 -1
package/account.mjs
CHANGED
|
@@ -260,20 +260,13 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
260
260
|
const tick = async () => {
|
|
261
261
|
let rooms = []
|
|
262
262
|
try {
|
|
263
|
-
|
|
264
|
-
// it CREATED. RLS on code_sessions returns owned OR participant rooms, so
|
|
265
|
-
// without this owner filter the supervisor auto-serves rooms you merely JOINED
|
|
266
|
-
// as a guest — spawning your machine into another owner's session that already
|
|
267
|
-
// runs its own bridge. The web watches tpacct:<session-owner>, so a
|
|
268
|
-
// participant's bridge is duplication the UI never even looks at.
|
|
269
|
-
const { data } = await sb.from('code_sessions').select('code,name,last_active_at,owner_id').eq('owner_id', session.user.id).order('last_active_at', { ascending: false }).limit(20)
|
|
263
|
+
const { data } = await sb.from('code_sessions').select('code,name,last_active_at').order('last_active_at', { ascending: false }).limit(20)
|
|
270
264
|
rooms = data || []
|
|
271
265
|
} catch { /* transient — keep existing children, retry next tick */ }
|
|
272
266
|
const dirs = loadDirs()
|
|
273
267
|
for (const r of rooms) {
|
|
274
268
|
const room = r.code
|
|
275
269
|
if (!room || children.has(room)) continue
|
|
276
|
-
if (r.owner_id && r.owner_id !== session.user.id) continue // never serve a room we don't own (defense-in-depth behind the .eq above)
|
|
277
270
|
const dir = dirs[room] || DEFAULT_DIR // auto-serve unbound sessions in the launch dir
|
|
278
271
|
if (!fs.existsSync(dir)) {
|
|
279
272
|
if (!warned.has(room)) { warned.add(room); process.stderr.write(`\n ◇ ${room}: bound directory is gone (${dir}) — re-bind it.\n`) }
|
package/bridge.mjs
CHANGED
|
@@ -151,9 +151,7 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
151
151
|
// shell export can't reach a serviced bridge — provider.json is its own source
|
|
152
152
|
// of truth. Idempotent; a no-op for the default Anthropic provider. See provider.mjs.
|
|
153
153
|
const { applyProviderEnv } = await import('./provider.mjs')
|
|
154
|
-
|
|
155
|
-
// or a custom Anthropic-compatible endpoint) to the web welcome banner.
|
|
156
|
-
const PROVIDER_INFO = applyProviderEnv()
|
|
154
|
+
applyProviderEnv()
|
|
157
155
|
|
|
158
156
|
if (argv[0] === 'login') { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) }
|
|
159
157
|
if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); runBind(argv[1], argv[2]) }
|
|
@@ -166,7 +164,7 @@ if (argv[0] === 'provider') { const { runProvider } = await import('./provider.m
|
|
|
166
164
|
// so launchd / systemd / CI / pipes are byte-for-byte unchanged.
|
|
167
165
|
// Spec: docs/specs/2026-06-19-bridge-unified-launcher.md (Part 1).
|
|
168
166
|
if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
169
|
-
const { runLauncher
|
|
167
|
+
const { runLauncher } = await import('./launcher.mjs')
|
|
170
168
|
const svc = await import('./service.mjs')
|
|
171
169
|
const BRIDGE = fileURLToPath(import.meta.url)
|
|
172
170
|
const io = {
|
|
@@ -188,18 +186,9 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
188
186
|
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []); process.exit(0) },
|
|
189
187
|
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room); process.exit(0) },
|
|
190
188
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
191
|
-
|
|
192
|
-
// menu so you can pick a model, switch back, or do something else. (It used to
|
|
193
|
-
// process.exit(0), which dumped you to the shell mid-setup.)
|
|
194
|
-
setProvider: async ({ kind, base, token, model }) => {
|
|
195
|
-
const { runProvider } = await import('./provider.mjs')
|
|
196
|
-
const args = kind === 'custom'
|
|
197
|
-
? ['custom', '--base', base, '--token', token, ...(model ? ['--model', model] : [])]
|
|
198
|
-
: ['anthropic']
|
|
199
|
-
await runProvider(args)
|
|
200
|
-
},
|
|
189
|
+
setProvider: async ({ kind, base, token }) => { const { runProvider } = await import('./provider.mjs'); await runProvider(kind === 'custom' ? ['custom', '--base', base, '--token', token] : ['anthropic']); process.exit(0) },
|
|
201
190
|
}
|
|
202
|
-
await runLauncher({ actions, io
|
|
191
|
+
await runLauncher({ actions, io })
|
|
203
192
|
process.exit(0)
|
|
204
193
|
}
|
|
205
194
|
|
|
@@ -417,14 +406,6 @@ const announce = () =>
|
|
|
417
406
|
// cwd + version: the host's working dir + thinkpool-pair version, shown in
|
|
418
407
|
// the room's welcome banner. Re-sent per announce so late joiners get them.
|
|
419
408
|
cwd, version: VERSION,
|
|
420
|
-
// provider: the LLM endpoint this bridge drives Claude Code through —
|
|
421
|
-
// 'anthropic' (the default, the host's regular Claude login) or 'custom'
|
|
422
|
-
// (any Anthropic-compatible base url: GLM/Z.ai, OpenRouter, a proxy). Shown
|
|
423
|
-
// in the room banner so both drivers see whose model they're on.
|
|
424
|
-
// providerBaseUrl rides along only for custom so the banner can show the
|
|
425
|
-
// host (e.g. openrouter.ai). Older web clients ignore unknown fields.
|
|
426
|
-
provider: PROVIDER_INFO.provider,
|
|
427
|
-
providerBaseUrl: PROVIDER_INFO.baseUrl || undefined,
|
|
428
409
|
// pendingUpdate: a newer thinkpool-pair is out — drives the room's "update ready"
|
|
429
410
|
// chip; re-sent per announce so reloads/late-joiners see it (Slice 3).
|
|
430
411
|
pendingUpdate,
|
package/claude-session.mjs
CHANGED
|
@@ -123,14 +123,6 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
123
123
|
const input = makeInputStream()
|
|
124
124
|
let sessionId = resume || null
|
|
125
125
|
let closed = false
|
|
126
|
-
// Cold-start measurement (2026-06-21, spec 2026-06-21-code-session-deploy-stability):
|
|
127
|
-
// stamp when the open begins so we can log spawn→ready (the SDK's first `init`
|
|
128
|
-
// system message — when the terminal becomes usable). New room terminals "take a
|
|
129
|
-
// long time"; settingSources:['user','project','local'] boots every host MCP
|
|
130
|
-
// server + SessionStart hook + plugin before init. This is the A-side baseline;
|
|
131
|
-
// open a terminal with TP_MCP_STRICT=1 (below) for the B-side to isolate MCP's share.
|
|
132
|
-
const spawnT0 = Date.now()
|
|
133
|
-
let readyLogged = false
|
|
134
126
|
let q = null // the live Query — control requests (interrupt /
|
|
135
127
|
// setPermissionMode) route through it once streaming.
|
|
136
128
|
// The session is CREATED in the caller's chosen mode (not hard-coded default):
|
|
@@ -310,13 +302,6 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
310
302
|
// the loop; only the coarse thinking_tokens system message is surfaced.
|
|
311
303
|
includePartialMessages: true,
|
|
312
304
|
}
|
|
313
|
-
// Measurement lever (default OFF — no behavior change). TP_MCP_STRICT=1 makes the
|
|
314
|
-
// room session skip booting the host's filesystem-configured MCP servers
|
|
315
|
-
// (strictMcpConfig + empty mcpServers map), isolating the MCP share of cold-start
|
|
316
|
-
// in the spawn→ready log below. settingSources still loads slash commands /
|
|
317
|
-
// CLAUDE.md / agents either way — strict only governs MCP server sourcing. Flip
|
|
318
|
-
// this to the default (or curate a slim allowlist) once the numbers justify it.
|
|
319
|
-
if (process.env.TP_MCP_STRICT === '1') { opts.strictMcpConfig = true; opts.mcpServers = {} }
|
|
320
305
|
if (cwd) opts.cwd = cwd
|
|
321
306
|
if (model) opts.model = model
|
|
322
307
|
if (resume) opts.resume = resume
|
|
@@ -358,14 +343,6 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
358
343
|
break
|
|
359
344
|
}
|
|
360
345
|
if (m.session_id) sessionId = m.session_id
|
|
361
|
-
// Cold-start timing — the init system message is when the terminal is
|
|
362
|
-
// usable, so spawn→here IS the latency the user waits on. One line per
|
|
363
|
-
// open. Compare a normal open vs one with TP_MCP_STRICT=1 to read off how
|
|
364
|
-
// many ms the host MCP servers cost. (resume opens also pay sanitizeSession.)
|
|
365
|
-
if (!readyLogged) {
|
|
366
|
-
readyLogged = true
|
|
367
|
-
process.stderr.write(`\n ◆ session ready in ${Date.now() - spawnT0}ms — MCP ${process.env.TP_MCP_STRICT === '1' ? 'OFF' : 'on'}${resume ? ', resume' : ', fresh'}.\n`)
|
|
368
|
-
}
|
|
369
346
|
// m.slash_commands (init message) — the commands this session really
|
|
370
347
|
// supports: built-ins + the host's custom .claude/commands. Surfaced
|
|
371
348
|
// so the room composer's autocomplete lists what ACTUALLY exists.
|
package/launcher.mjs
CHANGED
|
@@ -51,14 +51,10 @@ function serviceFile(room) {
|
|
|
51
51
|
export function detectState() {
|
|
52
52
|
const auth = readJson(path.join(CFG_DIR, 'auth.json'))
|
|
53
53
|
const provider = readJson(path.join(CFG_DIR, 'provider.json'))
|
|
54
|
-
// provider.json is written with { provider, baseUrl, model } (see provider.mjs).
|
|
55
|
-
// Anything whose `provider` field is set and not "anthropic" is a custom endpoint.
|
|
56
|
-
const isCustom = !!provider?.provider && provider.provider !== 'anthropic'
|
|
57
54
|
return {
|
|
58
55
|
loggedIn: !!auth?.refresh_token,
|
|
59
56
|
email: auth?.email || auth?.user?.email || null,
|
|
60
|
-
provider:
|
|
61
|
-
providerModel: isCustom ? (provider.model || null) : null,
|
|
57
|
+
provider: provider?.kind === 'custom' ? `Custom (${provider.base || 'endpoint'})` : 'Anthropic (default)',
|
|
62
58
|
agents: KNOWN_AGENTS.filter(a => onPath(a.cmd)),
|
|
63
59
|
accountSvc: fs.existsSync(serviceFile(null)),
|
|
64
60
|
cwd: process.cwd(),
|
|
@@ -72,11 +68,7 @@ const C = {
|
|
|
72
68
|
}
|
|
73
69
|
|
|
74
70
|
// io: { ask(prompt)->Promise<string>, print(s) }. actions: real | dry-run | mock.
|
|
75
|
-
|
|
76
|
-
// reflects the change (real bridge passes detectState; tests omit it to keep the
|
|
77
|
-
// injected state stable).
|
|
78
|
-
export async function runLauncher({ actions, io, state = detectState(), refresh = null }) {
|
|
79
|
-
const resync = () => { if (refresh) state = refresh() }
|
|
71
|
+
export async function runLauncher({ actions, io, state = detectState() }) {
|
|
80
72
|
const askChoice = async (prompt, options, def = 1) => {
|
|
81
73
|
options.forEach((o, i) => io.print(` ${C.cyan(String(i + 1))}) ${o.label}${o.hint ? ' ' + C.dim(o.hint) : ''}`))
|
|
82
74
|
const a = await io.ask(`\n ${prompt} ${C.dim(`[${def}]`)} ${C.cyan('▸')} `)
|
|
@@ -145,41 +137,25 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
145
137
|
await actions.installService({ room, agentCmd: agent.cmd })
|
|
146
138
|
}
|
|
147
139
|
|
|
148
|
-
const providerMenu = async () => {
|
|
149
|
-
const cur = state.provider + (state.providerModel ? `, model ${state.providerModel}` : '')
|
|
150
|
-
const p = await askChoice('\n provider', [
|
|
151
|
-
{ label: 'Anthropic (default)', hint: 'regular Claude login' },
|
|
152
|
-
{ label: 'Custom endpoint', hint: 'Z.ai GLM, OpenRouter, your own proxy' },
|
|
153
|
-
{ label: '← Back', hint: `current: ${cur}` },
|
|
154
|
-
], 3) // Enter = Back
|
|
155
|
-
if (p === 2) return
|
|
156
|
-
if (p === 0) { await actions.setProvider({ kind: 'anthropic' }); resync(); io.print('\n ' + C.green('✓ back on regular Claude.')); return }
|
|
157
|
-
// custom — every step can be aborted with a blank line, which backs out cleanly.
|
|
158
|
-
const base = (await io.ask(`\n Base url ${C.dim('(blank to cancel · SDK appends /v1/messages)')} ${C.cyan('▸')} `)).trim()
|
|
159
|
-
if (!base) { io.print(' ' + C.yellow('cancelled — provider unchanged')); return }
|
|
160
|
-
const token = (await io.ask(` API token ${C.dim('(blank to cancel)')} ${C.cyan('▸')} `)).trim()
|
|
161
|
-
if (!token) { io.print(' ' + C.yellow('cancelled — provider unchanged')); return }
|
|
162
|
-
const model = (await io.ask(` Model ${C.dim('(blank = endpoint default · e.g. glm-4.6)')} ${C.cyan('▸')} `)).trim()
|
|
163
|
-
await actions.setProvider({ kind: 'custom', base, token, model: model || undefined })
|
|
164
|
-
resync()
|
|
165
|
-
io.print(`\n ${C.green('✓ provider set to custom.')}\n base url: ${state.provider}\n model: ${state.providerModel || '(endpoint default)'}`)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
140
|
const settingsMenu = async () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
if (pick === 0) await providerMenu()
|
|
178
|
-
else if (pick === 1) { await actions.login(); resync(); return }
|
|
141
|
+
const pick = await askChoice('\n change', [
|
|
142
|
+
{ label: 'LLM provider', hint: `current: ${state.provider}` },
|
|
143
|
+
{ label: 'Account', hint: state.loggedIn ? `linked: ${state.email || 'yes'}` : 'not linked' },
|
|
144
|
+
{ label: 'Remove the background service', hint: state.accountSvc ? '' : 'none installed' },
|
|
145
|
+
])
|
|
146
|
+
if (pick === 0) {
|
|
147
|
+
const p = await askChoice('\n provider', [{ label: 'Anthropic (default)' }, { label: 'Custom endpoint', hint: 'Z.ai GLM, OpenRouter, your own proxy' }])
|
|
148
|
+
if (p === 0) await actions.setProvider({ kind: 'anthropic' })
|
|
179
149
|
else {
|
|
180
|
-
|
|
181
|
-
|
|
150
|
+
const base = (await io.ask(`\n Base url ${C.dim('(SDK appends /v1/messages)')} ${C.cyan('▸')} `)).trim()
|
|
151
|
+
const token = (await io.ask(` API token ${C.cyan('▸')} `)).trim()
|
|
152
|
+
await actions.setProvider({ kind: 'custom', base, token })
|
|
182
153
|
}
|
|
154
|
+
} else if (pick === 1) {
|
|
155
|
+
await actions.login()
|
|
156
|
+
} else {
|
|
157
|
+
if (state.accountSvc) await actions.uninstallService({ room: null })
|
|
158
|
+
else io.print(' ' + C.yellow('no background service installed'))
|
|
183
159
|
}
|
|
184
160
|
}
|
|
185
161
|
|