thinkpool-pair 0.7.35 → 0.7.37

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.
Files changed (3) hide show
  1. package/bridge.mjs +23 -4
  2. package/launcher.mjs +42 -18
  3. package/package.json +1 -1
package/bridge.mjs CHANGED
@@ -151,7 +151,9 @@ 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
- applyProviderEnv()
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()
155
157
 
156
158
  if (argv[0] === 'login') { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) }
157
159
  if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); runBind(argv[1], argv[2]) }
@@ -164,7 +166,7 @@ if (argv[0] === 'provider') { const { runProvider } = await import('./provider.m
164
166
  // so launchd / systemd / CI / pipes are byte-for-byte unchanged.
165
167
  // Spec: docs/specs/2026-06-19-bridge-unified-launcher.md (Part 1).
166
168
  if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
167
- const { runLauncher } = await import('./launcher.mjs')
169
+ const { runLauncher, detectState } = await import('./launcher.mjs')
168
170
  const svc = await import('./service.mjs')
169
171
  const BRIDGE = fileURLToPath(import.meta.url)
170
172
  const io = {
@@ -186,9 +188,18 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
186
188
  installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []); process.exit(0) },
187
189
  uninstallService: ({ room = null } = {}) => { svc.uninstallService(room); process.exit(0) },
188
190
  login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
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) },
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
+ },
190
201
  }
191
- await runLauncher({ actions, io })
202
+ await runLauncher({ actions, io, refresh: detectState })
192
203
  process.exit(0)
193
204
  }
194
205
 
@@ -406,6 +417,14 @@ const announce = () =>
406
417
  // cwd + version: the host's working dir + thinkpool-pair version, shown in
407
418
  // the room's welcome banner. Re-sent per announce so late joiners get them.
408
419
  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,
409
428
  // pendingUpdate: a newer thinkpool-pair is out — drives the room's "update ready"
410
429
  // chip; re-sent per announce so reloads/late-joiners see it (Slice 3).
411
430
  pendingUpdate,
package/launcher.mjs CHANGED
@@ -51,10 +51,14 @@ 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'
54
57
  return {
55
58
  loggedIn: !!auth?.refresh_token,
56
59
  email: auth?.email || auth?.user?.email || null,
57
- provider: provider?.kind === 'custom' ? `Custom (${provider.base || 'endpoint'})` : 'Anthropic (default)',
60
+ provider: isCustom ? `Custom (${provider.baseUrl || 'endpoint'})` : 'Anthropic (default)',
61
+ providerModel: isCustom ? (provider.model || null) : null,
58
62
  agents: KNOWN_AGENTS.filter(a => onPath(a.cmd)),
59
63
  accountSvc: fs.existsSync(serviceFile(null)),
60
64
  cwd: process.cwd(),
@@ -68,7 +72,11 @@ const C = {
68
72
  }
69
73
 
70
74
  // io: { ask(prompt)->Promise<string>, print(s) }. actions: real | dry-run | mock.
71
- export async function runLauncher({ actions, io, state = detectState() }) {
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() }
72
80
  const askChoice = async (prompt, options, def = 1) => {
73
81
  options.forEach((o, i) => io.print(` ${C.cyan(String(i + 1))}) ${o.label}${o.hint ? ' ' + C.dim(o.hint) : ''}`))
74
82
  const a = await io.ask(`\n ${prompt} ${C.dim(`[${def}]`)} ${C.cyan('▸')} `)
@@ -137,25 +145,41 @@ export async function runLauncher({ actions, io, state = detectState() }) {
137
145
  await actions.installService({ room, agentCmd: agent.cmd })
138
146
  }
139
147
 
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
+
140
168
  const settingsMenu = async () => {
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' })
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 }
149
179
  else {
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 })
180
+ if (state.accountSvc) await actions.uninstallService({ room: null })
181
+ else io.print(' ' + C.yellow('no background service installed'))
153
182
  }
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'))
159
183
  }
160
184
  }
161
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.35",
3
+ "version": "0.7.37",
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": {