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 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
- // Owner-only discovery (Contract C-CODE-2): a host bridges ONLY the sessions
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
- // Captured so the room announce can report the active LLM provider (anthropic,
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, detectState } = await import('./launcher.mjs')
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
- // Provider config is NOT terminal — it writes provider.json and returns to the
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, refresh: detectState })
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,
@@ -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: isCustom ? `Custom (${provider.baseUrl || 'endpoint'})` : 'Anthropic (default)',
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
- // refresh: optional () => state — re-read disk after a mutating action so the header
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
- for (;;) {
170
- const pick = await askChoice('\n change', [
171
- { label: 'LLM provider', hint: `current: ${state.provider}` },
172
- { label: 'Account', hint: state.loggedIn ? `linked: ${state.email || 'yes'}` : 'not linked' },
173
- { label: 'Remove the background service', hint: state.accountSvc ? '' : 'none installed' },
174
- { label: '← Back to main menu', hint: '' },
175
- ], 4) // Enter = Back
176
- if (pick === 3) return
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
- if (state.accountSvc) await actions.uninstallService({ room: null })
181
- else io.print(' ' + C.yellow('no background service installed'))
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.39",
3
+ "version": "0.7.40",
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": {