thinkpool-pair 0.7.24 → 0.7.25

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 +8 -0
  2. package/package.json +2 -1
  3. package/provider.mjs +120 -0
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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.24",
3
+ "version": "0.7.25",
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,120 @@
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 custom --base <url> --token <key>
23
+ npx thinkpool-pair provider anthropic # reset to Claude
24
+ ───────────────────────────────────────────────────────────── */
25
+ import os from 'node:os'
26
+ import fs from 'node:fs'
27
+ import path from 'node:path'
28
+
29
+ const DIR = path.join(os.homedir(), '.thinkpool-pair')
30
+ const FILE = path.join(DIR, 'provider.json')
31
+
32
+ // The pre-baked GLM/Z.ai preset. --token is always required (never bake a key
33
+ // into source); --base / --model override these if the endpoint ever moves.
34
+ const GLM = { baseUrl: 'https://api.z.ai/api/anthropic', model: 'glm-5.2' }
35
+
36
+ function ensureDir() { try { fs.mkdirSync(DIR, { recursive: true }) } catch { /* noop */ } }
37
+ function opt(args, flag) { const i = args.indexOf(flag); return i >= 0 ? args[i + 1] : undefined }
38
+
39
+ export function loadProvider() {
40
+ try { return JSON.parse(fs.readFileSync(FILE, 'utf8')) } catch { return null }
41
+ }
42
+
43
+ export function saveProvider(p) {
44
+ ensureDir()
45
+ fs.writeFileSync(FILE, JSON.stringify(p, null, 2), { mode: 0o600 })
46
+ }
47
+
48
+ /**
49
+ * Merge the persisted provider choice (or TP_PROVIDER* env overrides) into
50
+ * process.env. Call once at startup, before spawning any child. Idempotent.
51
+ * A no-op for the default Anthropic provider — we only ever SET the GLM/custom
52
+ * vars, never clobber a host's real Claude login env. Env vars win over the
53
+ * file so a one-off launch can flip providers without writing anything.
54
+ * @returns {{provider:string,baseUrl?:string,model?:string,source:string}}
55
+ */
56
+ export function applyProviderEnv() {
57
+ const cfg = loadProvider() || {}
58
+ const provider = (process.env.TP_PROVIDER || cfg.provider || 'anthropic').toLowerCase()
59
+ if (provider === 'anthropic' || !provider) return { provider: 'anthropic', source: 'default' }
60
+
61
+ const baseUrl = process.env.TP_ANTHROPIC_BASE_URL || cfg.baseUrl
62
+ const authToken = process.env.TP_ANTHROPIC_AUTH_TOKEN || cfg.authToken
63
+ const model = process.env.TP_ANTHROPIC_MODEL || cfg.model
64
+ if (!baseUrl || !authToken) {
65
+ 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`)
66
+ return { provider: 'anthropic', source: 'incomplete' }
67
+ }
68
+ process.env.ANTHROPIC_BASE_URL = baseUrl
69
+ process.env.ANTHROPIC_AUTH_TOKEN = authToken
70
+ if (model) process.env.ANTHROPIC_MODEL = model
71
+ return { provider, baseUrl, model, source: cfg.provider ? 'file' : 'env' }
72
+ }
73
+
74
+ /** CLI handler for `thinkpool-pair provider [...]`. */
75
+ export async function runProvider(args) {
76
+ const sub = (args[0] || '').toLowerCase()
77
+
78
+ // ── show ──
79
+ if (!sub || sub === 'show' || sub === 'status') {
80
+ const cfg = loadProvider()
81
+ if (!cfg || cfg.provider === 'anthropic' || !cfg.provider) {
82
+ console.error('\n ◆ provider: anthropic (regular Claude login) — the default.\n')
83
+ } else {
84
+ 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`)
85
+ }
86
+ return
87
+ }
88
+
89
+ // ── reset to Anthropic ──
90
+ if (sub === 'anthropic' || sub === 'clear' || sub === 'reset') {
91
+ try { fs.unlinkSync(FILE) } catch { /* noop */ }
92
+ console.error('\n ✓ provider reset to anthropic (regular Claude login).\n')
93
+ return
94
+ }
95
+
96
+ // ── GLM (Z.ai) preset ──
97
+ if (sub === 'glm') {
98
+ const token = opt(args, '--token') || opt(args, '--key')
99
+ const baseUrl = opt(args, '--base') || opt(args, '--base-url') || GLM.baseUrl
100
+ const model = opt(args, '--model') || GLM.model
101
+ if (!token) { console.error('\n ✗ glm needs --token <zai_…>. Get one at https://z.ai/\n'); process.exit(1) }
102
+ saveProvider({ provider: 'glm', baseUrl, authToken: token, model, savedAt: Date.now() })
103
+ 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`)
104
+ return
105
+ }
106
+
107
+ // ── arbitrary Anthropic-compatible endpoint ──
108
+ if (sub === 'custom') {
109
+ const token = opt(args, '--token') || opt(args, '--key')
110
+ const baseUrl = opt(args, '--base') || opt(args, '--base-url')
111
+ const model = opt(args, '--model')
112
+ if (!token || !baseUrl) { console.error('\n ✗ custom needs --base <url> and --token <key>.\n'); process.exit(1) }
113
+ saveProvider({ provider: 'custom', baseUrl, authToken: token, model: model || undefined, savedAt: Date.now() })
114
+ console.error(`\n ✓ provider set to custom (${baseUrl}).\n Restart the bridge to apply.\n`)
115
+ return
116
+ }
117
+
118
+ 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 custom --base <url> --token <key> [--model <m>]\n npx thinkpool-pair provider anthropic # reset to Claude\n')
119
+ process.exit(1)
120
+ }