thinkpool-pair 0.7.40 → 0.7.41
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 +8 -1
- package/bridge.mjs +12 -3
- package/launcher.mjs +42 -18
- package/package.json +1 -1
package/account.mjs
CHANGED
|
@@ -260,13 +260,20 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
260
260
|
const tick = async () => {
|
|
261
261
|
let rooms = []
|
|
262
262
|
try {
|
|
263
|
-
|
|
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)
|
|
264
270
|
rooms = data || []
|
|
265
271
|
} catch { /* transient — keep existing children, retry next tick */ }
|
|
266
272
|
const dirs = loadDirs()
|
|
267
273
|
for (const r of rooms) {
|
|
268
274
|
const room = r.code
|
|
269
275
|
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)
|
|
270
277
|
const dir = dirs[room] || DEFAULT_DIR // auto-serve unbound sessions in the launch dir
|
|
271
278
|
if (!fs.existsSync(dir)) {
|
|
272
279
|
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
|
@@ -164,7 +164,7 @@ if (argv[0] === 'provider') { const { runProvider } = await import('./provider.m
|
|
|
164
164
|
// so launchd / systemd / CI / pipes are byte-for-byte unchanged.
|
|
165
165
|
// Spec: docs/specs/2026-06-19-bridge-unified-launcher.md (Part 1).
|
|
166
166
|
if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
167
|
-
const { runLauncher } = await import('./launcher.mjs')
|
|
167
|
+
const { runLauncher, detectState } = await import('./launcher.mjs')
|
|
168
168
|
const svc = await import('./service.mjs')
|
|
169
169
|
const BRIDGE = fileURLToPath(import.meta.url)
|
|
170
170
|
const io = {
|
|
@@ -186,9 +186,18 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
186
186
|
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []); process.exit(0) },
|
|
187
187
|
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room); process.exit(0) },
|
|
188
188
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
189
|
-
|
|
189
|
+
// Provider config is NOT terminal — it writes provider.json and returns to the
|
|
190
|
+
// menu so you can pick a model, switch back, or do something else. (It used to
|
|
191
|
+
// process.exit(0), which dumped you to the shell mid-setup.)
|
|
192
|
+
setProvider: async ({ kind, base, token, model }) => {
|
|
193
|
+
const { runProvider } = await import('./provider.mjs')
|
|
194
|
+
const args = kind === 'custom'
|
|
195
|
+
? ['custom', '--base', base, '--token', token, ...(model ? ['--model', model] : [])]
|
|
196
|
+
: ['anthropic']
|
|
197
|
+
await runProvider(args)
|
|
198
|
+
},
|
|
190
199
|
}
|
|
191
|
-
await runLauncher({ actions, io })
|
|
200
|
+
await runLauncher({ actions, io, refresh: detectState })
|
|
192
201
|
process.exit(0)
|
|
193
202
|
}
|
|
194
203
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|