thinkpool-pair 0.7.24 → 0.7.26
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/README.md +32 -0
- package/bridge.mjs +28 -3
- package/package.json +2 -1
- package/provider.mjs +152 -0
package/README.md
CHANGED
|
@@ -98,5 +98,37 @@ npx thinkpool-pair@latest <ROOM> -- claude # structured Claude, no TTY
|
|
|
98
98
|
- `resize` — web viewport size → headless PTYs only (the attached terminal
|
|
99
99
|
follows your own TTY).
|
|
100
100
|
|
|
101
|
+
## Alternative LLM Providers
|
|
102
|
+
|
|
103
|
+
The bridge supports using alternative LLM providers that are compatible with the Anthropic API:
|
|
104
|
+
|
|
105
|
+
### OpenRouter
|
|
106
|
+
To use OpenRouter instead of Anthropic directly:
|
|
107
|
+
```bash
|
|
108
|
+
npx thinkpool-pair provider openrouter --token sk-or-... [--model anthropic/claude-3.5-sonnet]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This will configure the bridge to use OpenRouter's API endpoint with your provided token. You can optionally specify a model.
|
|
112
|
+
|
|
113
|
+
### GLM (Z.ai)
|
|
114
|
+
To use GLM via Z.ai's Anthropic-compatible endpoint:
|
|
115
|
+
```bash
|
|
116
|
+
npx thinkpool-pair provider glm --token zai_...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Custom Provider
|
|
120
|
+
For any other Anthropic-compatible endpoint:
|
|
121
|
+
```bash
|
|
122
|
+
npx thinkpool-pair provider custom --base https://your-endpoint.com --token your-token [--model model-name]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Reset to Default
|
|
126
|
+
To reset back to using Anthropic directly:
|
|
127
|
+
```bash
|
|
128
|
+
npx thinkpool-pair provider anthropic
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Provider configurations are stored in `~/.thinkpool-pair/provider.json` and apply to all bridges on the machine.
|
|
132
|
+
|
|
101
133
|
Public anon creds are embedded (the same ones the web app ships). Override with
|
|
102
134
|
`TP_SUPABASE_URL` / `TP_SUPABASE_ANON` if needed. Set `TP_NAME` to label yourself.
|
package/bridge.mjs
CHANGED
|
@@ -143,8 +143,16 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
143
143
|
// ACCOUNT, not per room. `login` links this device; `bind <ROOM> <dir>` maps a
|
|
144
144
|
// session to a working dir on this machine; a bare invocation discovers + serves
|
|
145
145
|
// every session you're in (a headless child bridge per room, run in its dir).
|
|
146
|
+
// Apply the per-machine LLM provider choice (GLM via Z.ai, or default Anthropic)
|
|
147
|
+
// BEFORE any child is spawned. The launchd service doesn't source ~/.zshrc, so a
|
|
148
|
+
// shell export can't reach a serviced bridge — provider.json is its own source
|
|
149
|
+
// of truth. Idempotent; a no-op for the default Anthropic provider. See provider.mjs.
|
|
150
|
+
const { applyProviderEnv } = await import('./provider.mjs')
|
|
151
|
+
applyProviderEnv()
|
|
152
|
+
|
|
146
153
|
if (argv[0] === 'login') { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) }
|
|
147
154
|
if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); runBind(argv[1], argv[2]) }
|
|
155
|
+
if (argv[0] === 'provider') { const { runProvider } = await import('./provider.mjs'); await runProvider(argv.slice(1)); process.exit(0) }
|
|
148
156
|
if (!argv[0] || argv[0].startsWith('-')) { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) }
|
|
149
157
|
|
|
150
158
|
const room = (argv[0] || '').toUpperCase().trim()
|
|
@@ -589,9 +597,16 @@ function openStructured({ id, model, resume, log, commands, mode }) {
|
|
|
589
597
|
// on the entry so the ANNOUNCE can hand it to clients that connect/reload
|
|
590
598
|
// AFTER init (the one-time code-event would miss them), then re-announce.
|
|
591
599
|
if (evt.kind === 'system' && Array.isArray(evt.commands) && evt.commands.length && !entry.commands) { entry.commands = evt.commands; announce(); persist() }
|
|
592
|
-
// Chrome events (mode / usage / clear) are transient state, not
|
|
593
|
-
// broadcast + print them
|
|
594
|
-
const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear'
|
|
600
|
+
// Chrome events (mode / usage / clear / compact) are transient state, not
|
|
601
|
+
// transcript — broadcast + print them but keep out of the persisted/replayed log.
|
|
602
|
+
const chrome = evt.kind === 'mode' || evt.kind === 'usage' || evt.kind === 'clear' || evt.kind === 'compact'
|
|
603
|
+
// compact turn finished (or errored) → clear the pulsing indicator.
|
|
604
|
+
// No "done" ctl line — the SDK's own output in the transcript is the
|
|
605
|
+
// signal (compact summary on success, AbortError text on abort/too-small).
|
|
606
|
+
if ((evt.kind === 'result' || evt.kind === 'error') && entry.compacting) {
|
|
607
|
+
entry.compacting = false
|
|
608
|
+
bcast('code-event', { term: id, evt: { kind: 'compact', status: 'done', ts: Date.now() } })
|
|
609
|
+
}
|
|
595
610
|
if (!chrome) {
|
|
596
611
|
entry.log.push(evt)
|
|
597
612
|
if (entry.log.length > STRUCTURED_LOG_MAX) entry.log.shift()
|
|
@@ -806,6 +821,16 @@ channel
|
|
|
806
821
|
ctlLine('context cleared. You can continue with these answers in mind.')
|
|
807
822
|
return
|
|
808
823
|
}
|
|
824
|
+
// /compact → announce immediately, track so onEvent can emit a done signal when
|
|
825
|
+
// the SDK turn finishes. No echoYou — the ctl line IS the announcement.
|
|
826
|
+
if (/^\/compact\b/.test(text)) {
|
|
827
|
+
ctlLine('◈ compacting context…')
|
|
828
|
+
s.compacting = true
|
|
829
|
+
s.compactBy = payload.by
|
|
830
|
+
bcast('code-event', { term: payload.term, evt: { kind: 'compact', status: 'start', ts: Date.now() } })
|
|
831
|
+
s.session.sendTurn(text)
|
|
832
|
+
return
|
|
833
|
+
}
|
|
809
834
|
s.session.sendTurn(text)
|
|
810
835
|
echoYou()
|
|
811
836
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.26",
|
|
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": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"service.mjs",
|
|
14
14
|
"account.mjs",
|
|
15
15
|
"auth-store.mjs",
|
|
16
|
+
"provider.mjs",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
package/provider.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
provider.mjs — per-machine LLM provider choice for the ThinkPool
|
|
3
|
+
bridge. Lets a Code room run on GLM (Z.ai's Anthropic-compatible
|
|
4
|
+
endpoint) instead of plain Anthropic, INDEPENDENT of the launching
|
|
5
|
+
shell. Why this exists: the launchd service does NOT source ~/.zshrc,
|
|
6
|
+
so a shell `export ANTHROPIC_*` can never reach a serviced bridge —
|
|
7
|
+
its spawned Claude children fall back to the host's claude.ai login
|
|
8
|
+
and hit the Anthropic spend wall. provider.json is the bridge's OWN
|
|
9
|
+
source of truth.
|
|
10
|
+
|
|
11
|
+
applyProviderEnv() runs once at the top of bridge.mjs (the universal
|
|
12
|
+
entry point), BEFORE any child is spawned. It sets ANTHROPIC_BASE_URL
|
|
13
|
+
/ ANTHROPIC_AUTH_TOKEN / ANTHROPIC_MODEL on process.env. Because the
|
|
14
|
+
account-supervisor spawn (account.mjs) and the Agent-SDK env
|
|
15
|
+
(claude-session.mjs) both spread `...process.env`, the choice reaches
|
|
16
|
+
every code session with no other change. Default (no file, or
|
|
17
|
+
provider:"anthropic") = the host's regular Claude login, untouched.
|
|
18
|
+
|
|
19
|
+
CLI (dispatched from bridge.mjs):
|
|
20
|
+
npx thinkpool-pair provider # show current
|
|
21
|
+
npx thinkpool-pair provider glm --token <zai_…> # use GLM (Z.ai)
|
|
22
|
+
npx thinkpool-pair provider openrouter --token <sk-or-…> [--model <model>]
|
|
23
|
+
npx thinkpool-pair provider custom --base <url> --token <key>
|
|
24
|
+
npx thinkpool-pair provider anthropic # reset to Claude
|
|
25
|
+
───────────────────────────────────────────────────────────── */
|
|
26
|
+
import os from 'node:os'
|
|
27
|
+
import fs from 'node:fs'
|
|
28
|
+
import path from 'node:path'
|
|
29
|
+
|
|
30
|
+
const DIR = path.join(os.homedir(), '.thinkpool-pair')
|
|
31
|
+
const FILE = path.join(DIR, 'provider.json')
|
|
32
|
+
|
|
33
|
+
// The pre-baked GLM/Z.ai preset. --token is always required (never bake a key
|
|
34
|
+
// into source); --base / --model override these if the endpoint ever moves.
|
|
35
|
+
const GLM = { baseUrl: 'https://api.z.ai/api/anthropic', model: 'glm-5.2' }
|
|
36
|
+
|
|
37
|
+
// The pre-baked OpenRouter preset. --token is always required.
|
|
38
|
+
const OPENROUTER = { baseUrl: 'https://openrouter.ai/api/v1', model: 'anthropic/claude-3.5-sonnet' }
|
|
39
|
+
|
|
40
|
+
function ensureDir() { try { fs.mkdirSync(DIR, { recursive: true }) } catch { /* noop */ } }
|
|
41
|
+
function opt(args, flag) { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined }
|
|
42
|
+
|
|
43
|
+
export function loadProvider() {
|
|
44
|
+
try { return JSON.parse(fs.readFileSync(FILE, 'utf8')) } catch { return null }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function saveProvider(p) {
|
|
48
|
+
ensureDir()
|
|
49
|
+
fs.writeFileSync(FILE, JSON.stringify(p, null, 2), { mode: 0o600 })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Merge the persisted provider choice (or TP_PROVIDER* env overrides) into
|
|
54
|
+
* process.env. Call once at startup, before spawning any child. Idempotent.
|
|
55
|
+
* A no-op for the default Anthropic provider — we only ever SET the GLM/custom
|
|
56
|
+
* vars, never clobber a host's real Claude login env. Env vars win over the
|
|
57
|
+
* file so a one-off launch can flip providers without writing anything.
|
|
58
|
+
* @returns {{provider:string,baseUrl?:string,model?:string,source:string}}
|
|
59
|
+
*/
|
|
60
|
+
export function applyProviderEnv() {
|
|
61
|
+
const cfg = loadProvider() || {}
|
|
62
|
+
const provider = (process.env.TP_PROVIDER || cfg.provider || 'anthropic').toLowerCase()
|
|
63
|
+
if (provider === 'anthropic' || !provider) return { provider: 'anthropic', source: 'default' }
|
|
64
|
+
|
|
65
|
+
// Handle OpenRouter specifically
|
|
66
|
+
if (provider === 'openrouter') {
|
|
67
|
+
const baseUrl = process.env.TP_ANTHROPIC_BASE_URL || cfg.baseUrl || OPENROUTER.baseUrl
|
|
68
|
+
const authToken = process.env.TP_ANTHROPIC_AUTH_TOKEN || cfg.authToken
|
|
69
|
+
const model = process.env.TP_ANTHROPIC_MODEL || cfg.model || OPENROUTER.model
|
|
70
|
+
|
|
71
|
+
if (!authToken) {
|
|
72
|
+
process.stderr.write(`\n ◇ provider "${provider}" is set but is missing authToken.\n Finish setup: npx thinkpool-pair provider ${provider} --token <key>\n Falling back to regular Claude for now.\n`)
|
|
73
|
+
return { provider: 'anthropic', source: 'incomplete' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
process.env.ANTHROPIC_BASE_URL = baseUrl
|
|
77
|
+
process.env.ANTHROPIC_AUTH_TOKEN = authToken
|
|
78
|
+
if (model) process.env.ANTHROPIC_MODEL = model
|
|
79
|
+
return { provider, baseUrl, model, source: cfg.provider ? 'file' : 'env' }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const baseUrl = process.env.TP_ANTHROPIC_BASE_URL || cfg.baseUrl
|
|
83
|
+
const authToken = process.env.TP_ANTHROPIC_AUTH_TOKEN || cfg.authToken
|
|
84
|
+
const model = process.env.TP_ANTHROPIC_MODEL || cfg.model
|
|
85
|
+
if (!baseUrl || !authToken) {
|
|
86
|
+
process.stderr.write(`\n ◇ provider "${provider}" is set but is missing baseUrl/authToken.\n Finish setup: npx thinkpool-pair provider ${provider} --token <key>\n Falling back to regular Claude for now.\n`)
|
|
87
|
+
return { provider: 'anthropic', source: 'incomplete' }
|
|
88
|
+
}
|
|
89
|
+
process.env.ANTHROPIC_BASE_URL = baseUrl
|
|
90
|
+
process.env.ANTHROPIC_AUTH_TOKEN = authToken
|
|
91
|
+
if (model) process.env.ANTHROPIC_MODEL = model
|
|
92
|
+
return { provider, baseUrl, model, source: cfg.provider ? 'file' : 'env' }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** CLI handler for `thinkpool-pair provider [...]`. */
|
|
96
|
+
export async function runProvider(args) {
|
|
97
|
+
const sub = (args[0] || '').toLowerCase()
|
|
98
|
+
|
|
99
|
+
// ── show ──
|
|
100
|
+
if (!sub || sub === 'show' || sub === 'status') {
|
|
101
|
+
const cfg = loadProvider()
|
|
102
|
+
if (!cfg || cfg.provider === 'anthropic' || !cfg.provider) {
|
|
103
|
+
console.error('\n ◆ provider: anthropic (regular Claude login) — the default.\n')
|
|
104
|
+
} else {
|
|
105
|
+
console.error(`\n ◆ provider: ${cfg.provider}\n base url: ${cfg.baseUrl || '(unset)'}\n model: ${cfg.model || '(unset)'}\n token: ${cfg.authToken ? cfg.authToken.slice(0, 8) + '…' : '(unset)'}\n`)
|
|
106
|
+
}
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── reset to Anthropic ──
|
|
111
|
+
if (sub === 'anthropic' || sub === 'clear' || sub === 'reset') {
|
|
112
|
+
try { fs.unlinkSync(FILE) } catch { /* noop */ }
|
|
113
|
+
console.error('\n ✓ provider reset to anthropic (regular Claude login).\n')
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── GLM (Z.ai) preset ──
|
|
118
|
+
if (sub === 'glm') {
|
|
119
|
+
const token = opt(args, '--token') || opt(args, '--key')
|
|
120
|
+
const baseUrl = opt(args, '--base') || opt(args, '--base-url') || GLM.baseUrl
|
|
121
|
+
const model = opt(args, '--model') || GLM.model
|
|
122
|
+
if (!token) { console.error('\n ✗ glm needs --token <zai_…>. Get one at https://z.ai/\n'); process.exit(1) }
|
|
123
|
+
saveProvider({ provider: 'glm', baseUrl, authToken: token, model, savedAt: Date.now() })
|
|
124
|
+
console.error(`\n ✓ provider set to glm (Z.ai).\n base url: ${baseUrl}\n model: ${model}\n token: ${token.slice(0, 8)}…\n Restart the bridge (or its launchd service) to apply.\n`)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── OpenRouter preset ──
|
|
129
|
+
if (sub === 'openrouter') {
|
|
130
|
+
const token = opt(args, '--token') || opt(args, '--key')
|
|
131
|
+
const baseUrl = opt(args, '--base') || opt(args, '--base-url') || OPENROUTER.baseUrl
|
|
132
|
+
const model = opt(args, '--model') || OPENROUTER.model
|
|
133
|
+
if (!token) { console.error('\n ✗ openrouter needs --token <sk-or-…>. Get one at https://openrouter.ai/\n'); process.exit(1) }
|
|
134
|
+
saveProvider({ provider: 'openrouter', baseUrl, authToken: token, model, savedAt: Date.now() })
|
|
135
|
+
console.error(`\n ✓ provider set to openrouter.\n base url: ${baseUrl}\n model: ${model}\n token: ${token.slice(0, 8)}…\n Restart the bridge (or its launchd service) to apply.\n`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── arbitrary Anthropic-compatible endpoint ──
|
|
140
|
+
if (sub === 'custom') {
|
|
141
|
+
const token = opt(args, '--token') || opt(args, '--key')
|
|
142
|
+
const baseUrl = opt(args, '--base') || opt(args, '--base-url')
|
|
143
|
+
const model = opt(args, '--model')
|
|
144
|
+
if (!token || !baseUrl) { console.error('\n ✗ custom needs --base <url> and --token <key>.\n'); process.exit(1) }
|
|
145
|
+
saveProvider({ provider: 'custom', baseUrl, authToken: token, model: model || undefined, savedAt: Date.now() })
|
|
146
|
+
console.error(`\n ✓ provider set to custom (${baseUrl}).\n Restart the bridge to apply.\n`)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.error('\n usage:\n npx thinkpool-pair provider # show current\n npx thinkpool-pair provider glm --token <zai_…> [--model glm-5.2]\n npx thinkpool-pair provider openrouter --token <sk-or-…> [--model <model>]\n npx thinkpool-pair provider custom --base <url> --token <key> [--model <m>]\n npx thinkpool-pair provider anthropic # reset to Claude\n')
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|