thinkpool-pair 0.7.318 → 0.7.320

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 CHANGED
@@ -31,14 +31,14 @@ The bridge self-heals at three levels — all cross-platform (macOS / Linux / Wi
31
31
  backoff). Zero-dependency. Combined with session restore (below), a crash is
32
32
  invisible — the room reconnects and the Claude session resumes where it left off.
33
33
  ```bash
34
- npx thinkpool-pair <ROOM> --supervise -- claude
34
+ npx thinkpool-pair@latest <ROOM> --supervise -- claude
35
35
  ```
36
36
  - **Boot-persistent service** — survive reboot/logout too. Installs the right
37
37
  native service for your OS (launchd on macOS, systemd `--user` on Linux, a
38
38
  Startup-folder script on Windows):
39
39
  ```bash
40
- npx thinkpool-pair install-service <ROOM> -- claude # set and forget
41
- npx thinkpool-pair uninstall-service <ROOM> # remove it
40
+ npx thinkpool-pair@latest install-service <ROOM> -- claude # set and forget
41
+ npx thinkpool-pair@latest uninstall-service <ROOM> # remove it
42
42
  ```
43
43
  The service runs `npx thinkpool-pair@latest`, so it **auto-updates** — new
44
44
  versions apply on the next restart, no re-install. (Linux: run
@@ -186,7 +186,7 @@ that format:
186
186
  > not OpenRouter's Anthropic endpoint.
187
187
 
188
188
  ```bash
189
- npx thinkpool-pair provider custom --base <url> --token <key> [--model <name>]
189
+ npx thinkpool-pair@latest provider custom --base <url> --token <key> [--model <name>]
190
190
  ```
191
191
 
192
192
  Common base urls (the agent appends `/v1/messages`):
@@ -203,11 +203,11 @@ Common base urls (the agent appends `/v1/messages`):
203
203
  > gateway is for. Avoid LiteLLM 1.82.7 / 1.82.8 (compromised releases); use a
204
204
  > current version.
205
205
 
206
- Show the current provider (`npx thinkpool-pair provider`), or reset back to your
206
+ Show the current provider (`npx thinkpool-pair@latest provider`), or reset back to your
207
207
  regular Claude login:
208
208
 
209
209
  ```bash
210
- npx thinkpool-pair provider anthropic
210
+ npx thinkpool-pair@latest provider anthropic
211
211
  ```
212
212
 
213
213
  Provider config is stored in `~/.thinkpool-pair/provider.json` (mode 0600) and
package/account.mjs CHANGED
@@ -15,7 +15,7 @@ import { createClient } from '@supabase/supabase-js'
15
15
  import os from 'node:os'
16
16
  import { saveAuth, loadAuth, markAuthReconnectRequired, loadDirs, bindDir, loadServed, rememberServed, loadDefaultDir, saveDefaultDir, claudeRecentProjectDir } from './auth-store.mjs'
17
17
  import { isSafeToRestart } from './update-gate.mjs'
18
- import { makeThrottledTrack, presenceSelfEchoObservation } from './presence.mjs'
18
+ import { makeThrottledTrack, presenceRecoveryEligible, presenceSelfEchoObservation, reducePresenceChannelEvidence } from './presence.mjs'
19
19
  import { publicKeyB64, announceProviders, listProviders, addProvider, addProviderModel, removeProvider, unseal } from './providers.mjs'
20
20
  import { supervisorServes, supervisorRoomsToStop } from './serve-consent.mjs'
21
21
  import { resolveServeDir } from './serve-dir.mjs'
@@ -24,6 +24,7 @@ import { installedAgentCommands } from './agent-detect.mjs'
24
24
  import { hostMemoryAdmission } from './host-memory.mjs'
25
25
  import { createPairBusBroker, mergePairRoomRoster, pairBusStatusFromRealtime, PAIR_BUS_STATUS } from './pair-bus.mjs'
26
26
  import { clearSupervisorReady, supervisorPresenceEchoed, writeSupervisorReady } from './supervisor-ready.mjs'
27
+ import { PAIR_CLI, pairCli } from './command-guidance.mjs'
27
28
 
28
29
  const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return null } })()
29
30
 
@@ -121,7 +122,7 @@ export async function runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) {
121
122
  process.stderr.write(`\n ◆ Link this device to your ThinkPool account.\n Open (signed in) and approve:\n\n ${url}\n\n code: ${user_code}\n\n Waiting…\n`)
122
123
  const started = Date.now()
123
124
  const poll = async () => {
124
- if (Date.now() - started > 5 * 60 * 1000) { process.stderr.write('\n ◇ link timed out — run `npx thinkpool-pair login` again.\n'); process.exit(1) }
125
+ if (Date.now() - started > 5 * 60 * 1000) { process.stderr.write(`\n ◇ link timed out — run \`${pairCli('login')}\` again.\n`); process.exit(1) }
125
126
  try {
126
127
  // A discarded `error` costs one tick — the loop retries in a second, and a
127
128
  // permanent failure still lands on the "link timed out" branch above rather than
@@ -130,10 +131,10 @@ export async function runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) {
130
131
  const { data: r } = await sb.rpc('claim_device_code', { p_device_code: device_code })
131
132
  if (r?.status === 'approved' && r.session?.refresh_token) {
132
133
  saveAuth(r.session)
133
- process.stderr.write(`\n ◆ linked as ${r.session.email || 'your account'}. You can close the browser tab.\n Now run: npx thinkpool-pair\n`)
134
+ process.stderr.write(`\n ◆ linked as ${r.session.email || 'your account'}. You can close the browser tab.\n Now run: ${PAIR_CLI}\n`)
134
135
  process.exit(0)
135
136
  }
136
- if (r?.status === 'expired' || r?.status === 'not_found') { process.stderr.write('\n ◇ link expired — run `npx thinkpool-pair login` again.\n'); process.exit(1) }
137
+ if (r?.status === 'expired' || r?.status === 'not_found') { process.stderr.write(`\n ◇ link expired — run \`${pairCli('login')}\` again.\n`); process.exit(1) }
137
138
  } catch { /* transient — keep polling */ }
138
139
  setTimeout(poll, 3000)
139
140
  }
@@ -397,18 +398,18 @@ export function scheduleTokenRefresh({ sb, session, onRefreshed, onReconnectRequ
397
398
 
398
399
  export function runBind(room, dir) {
399
400
  const r = (room || '').toUpperCase().trim()
400
- if (!r) { console.error('usage: npx thinkpool-pair bind <ROOM> <directory>'); process.exit(1) }
401
+ if (!r) { console.error(`usage: ${pairCli('bind', '<ROOM>', '<directory>')}`); process.exit(1) }
401
402
  const abs = path.resolve(dir || '.')
402
403
  if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) { console.error(`✗ not a directory: ${abs}`); process.exit(1) }
403
404
  bindDir(r, abs)
404
- console.error(`✓ bound ${r} → ${abs}\n Run \`npx thinkpool-pair\` (or restart it) to serve this session.`)
405
+ console.error(`✓ bound ${r} → ${abs}\n Run \`${PAIR_CLI}\` (or restart it) to serve this session.`)
405
406
  process.exit(0)
406
407
  }
407
408
 
408
409
  // ── account supervisor: a headless child bridge per bound room, in its dir ──
409
410
  export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
410
411
  if (!acquireSingleton()) {
411
- console.error('\n ◇ An account bridge is already running on this machine\n (~/.thinkpool-pair/account.lock). Not starting a second — two would race\n your saved login. Stop the other one first, or share a single room with\n `npx thinkpool-pair <ROOM>`.\n')
412
+ console.error(`\n ◇ An account bridge is already running on this machine\n (~/.thinkpool-pair/account.lock). Not starting a second — two would race\n your saved login. Stop the other one first, or share a single room with\n \`${pairCli('<ROOM>')}\`.\n`)
412
413
  process.exit(0)
413
414
  }
414
415
  // We own account.lock now, so any previous readiness receipt is stale. A fresh
@@ -430,7 +431,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
430
431
  // Reached ONLY when there is no saved login at all (not linked). Stand down CLEANLY
431
432
  // with exit 0: launchd KeepAlive has SuccessfulExit=false, so exit 0 is NOT respawned
432
433
  // — no crash storm for the "never logged in" case. The user links with `login`.
433
- console.error('\n ◇ Not linked to a ThinkPool account on this machine.\n Link it: npx thinkpool-pair login\n')
434
+ console.error(`\n ◇ Not linked to a ThinkPool account on this machine.\n Link it: ${pairCli('login')}\n`)
434
435
  process.exit(0)
435
436
  }
436
437
  if (auth.reconnectRequired) {
@@ -470,9 +471,9 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
470
471
  ? ' (auto-updating background service — survives reboot, tracks @latest)\n'
471
472
  : isManagedService
472
473
  ? ' (background service — survives reboot, restarts on crash, pinned to this version)\n'
473
- : ' (run hands-off + auto-updating: npx thinkpool-pair install-service)\n'
474
+ : ` (run hands-off + auto-updating: ${pairCli('install-service')})\n`
474
475
  const _effDefault = loadDefaultDir() || claudeRecentProjectDir() || DEFAULT_DIR
475
- process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${email}\n New sessions auto-serve from ${_effDefault}${loadDefaultDir() ? ' (your default)' : (claudeRecentProjectDir() ? ' (your most-recent Claude Code project)' : '')}\n (change it: npx thinkpool-pair set-default-dir <dir> · one room: npx thinkpool-pair bind <code> <dir>)\n${svcHint}`)
476
+ process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${email}\n New sessions auto-serve from ${_effDefault}${loadDefaultDir() ? ' (your default)' : (claudeRecentProjectDir() ? ' (your most-recent Claude Code project)' : '')}\n (change it: ${pairCli('set-default-dir', '<dir>')} · one room: ${pairCli('bind', '<code>', '<dir>')})\n${svcHint}`)
476
477
 
477
478
  const children = new Map() // room -> child process
478
479
  const childIdle = new Map() // room -> bool (last idle report: in the quiet window)
@@ -679,8 +680,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
679
680
  // and stamps presenceSyncSeenAt. acctSubscribedAt bounds the startup case where
680
681
  // SUBSCRIBED fires but the first sync never arrives: that is a broken Realtime
681
682
  // channel, not an unknowable state, once the listener was attached before join.
682
- let presenceSyncSeenAt = 0
683
- let acctSubscribedAt = 0
683
+ let presenceEvidence = reducePresenceChannelEvidence()
684
684
  let initialTickDone = false
685
685
  let readinessWritten = false
686
686
  const publishReadiness = () => {
@@ -691,14 +691,24 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
691
691
  readinessWritten = writeSupervisorReady({ version: VERSION, bridgeId: BRIDGE_ID })
692
692
  if (!readinessWritten) process.stderr.write('\n ◇ Could not record local supervisor readiness; the bridge remains running but installers will not claim it was confirmed.\n')
693
693
  }
694
- acct.on('presence', { event: 'sync' }, () => { presenceSyncSeenAt = Date.now(); publishReadiness() })
694
+ acct.on('presence', { event: 'sync' }, () => {
695
+ presenceEvidence = reducePresenceChannelEvidence(presenceEvidence, 'sync')
696
+ publishReadiness()
697
+ })
695
698
 
696
699
  // Re-track on EVERY (re)subscribe, not just the first: a realtime reconnect
697
700
  // (network blip, or a token swap mid-flight) rejoins the channel and must
698
701
  // re-announce, or the dashboard would read "no bridge" until the next restart.
699
702
  await new Promise((res) => acct.subscribe((st) => {
700
- if (st === 'SUBSCRIBED') { if (!acctSubscribedAt) acctSubscribedAt = Date.now(); pushPresence(); res() }
703
+ if (st === 'SUBSCRIBED') {
704
+ presenceEvidence = reducePresenceChannelEvidence(presenceEvidence, st)
705
+ pushPresence(); res()
706
+ }
701
707
  else if (st === 'CLOSED' || st === 'CHANNEL_ERROR' || st === 'TIMED_OUT') {
708
+ // Invalidate the old generation before inspecting presenceState again. The
709
+ // channel retains a local self key across failures; accepting that cache is
710
+ // what left the managed service zombified while a fresh foreground run worked.
711
+ presenceEvidence = reducePresenceChannelEvidence(presenceEvidence, st)
702
712
  // Realtime dropped (the 2026-07-08 `realtime CLOSED` blip). The socket normally
703
713
  // auto-reconnects, but nudge it so presence — and the claim heartbeat that shares
704
714
  // this client — recover instead of the loop standing by against its own stale row.
@@ -946,7 +956,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
946
956
  const why = choice.reason === 'home'
947
957
  ? `no project directory set — refusing to serve it from your home folder (${choice.dir}); the agent would lose its session context and the room would show "${path.basename(choice.dir)}" as the repo.`
948
958
  : 'no directory to serve it from.'
949
- process.stderr.write(`\n ◇ ${room}: ${why}\n Bind it to its repo: npx thinkpool-pair bind ${room} <repo-dir>\n`)
959
+ process.stderr.write(`\n ◇ ${room}: ${why}\n Bind it to its repo: ${pairCli('bind', room, '<repo-dir>')}\n`)
950
960
  }
951
961
  continue
952
962
  }
@@ -1177,12 +1187,16 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
1177
1187
  const presenceWatch = setInterval(() => {
1178
1188
  if (stopping) return
1179
1189
  const now = Date.now()
1190
+ const recoveryEligible = presenceRecoveryEligible({ claimHeld, lastClaimOkAt, now })
1180
1191
  let present = false
1181
1192
  try { present = !!acct.presenceState?.()[machine] } catch { present = false }
1182
1193
  const observation = presenceSelfEchoObservation({
1183
- tracking: claimHeld,
1184
- subscribedAt: acctSubscribedAt,
1185
- syncSeenAt: presenceSyncSeenAt,
1194
+ // A fresh HTTP claim distinguishes a Realtime-only zombie from a full
1195
+ // connectivity outage. In the latter case, wait in-process; a respawn cannot
1196
+ // restore the network and launchd would otherwise loop indefinitely.
1197
+ tracking: recoveryEligible,
1198
+ subscribedAt: Math.max(presenceEvidence.subscribedAt, lastClaimOkAt),
1199
+ syncSeenAt: presenceEvidence.syncSeenAt,
1186
1200
  present,
1187
1201
  missingSince: selfEchoMissingSince,
1188
1202
  now,
package/bridge.mjs CHANGED
@@ -68,6 +68,7 @@ import { requiresSdkAdmission, sdkSmokePassed } from './sdk-admission.mjs'
68
68
  import { hermesUserInputResponse } from './question-response.mjs'
69
69
  import { hostMemoryAdmission } from './host-memory.mjs'
70
70
  import { queueAbortBarrier, waitForAbortBarrier } from './abort-turn-barrier.mjs'
71
+ import { PAIR_CLI, pairCli } from './command-guidance.mjs'
71
72
 
72
73
  const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
73
74
  import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
@@ -292,7 +293,7 @@ if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); r
292
293
  if (argv[0] === 'set-default-dir') {
293
294
  const { saveDefaultDir } = await import('./auth-store.mjs')
294
295
  let d = (argv[1] || '').trim(); if (d.startsWith('~')) d = d.replace(/^~/, os.homedir())
295
- if (!d) { console.error(' usage: npx thinkpool-pair set-default-dir <dir>'); process.exit(1) }
296
+ if (!d) { console.error(` usage: ${pairCli('set-default-dir', '<dir>')}`); process.exit(1) }
296
297
  saveDefaultDir(d)
297
298
  console.log(` ◆ default project dir set → ${d}\n New/unbound sessions auto-serve from here — no per-session bind. Takes effect next serve tick.`)
298
299
  process.exit(0)
@@ -437,7 +438,7 @@ function checkSdkCompat() {
437
438
  if (!argv[0] || argv[0].startsWith('-')) { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) }
438
439
 
439
440
  const room = (argv[0] || '').toUpperCase().trim()
440
- if (!room) { console.error('usage: npx thinkpool-pair <ROOM> [--headless] [--continue|--fresh] [-- <command…>] | npx thinkpool-pair (account mode)'); process.exit(1) }
441
+ if (!room) { console.error(`usage: ${pairCli('<ROOM>', '[--headless]', '[--continue|--fresh]', '[-- <command…>]')} | ${PAIR_CLI} (account mode)`); process.exit(1) }
441
442
  // Security (audit 2026-07-02): `room` becomes a filesystem path segment under
442
443
  // ~/.thinkpool-pair (session-store) AND is interpolated into launchctl/systemd
443
444
  // service labels. sessions.id is unconstrained TEXT, so refuse path-unsafe / shell-
@@ -549,7 +550,7 @@ if (dashIdx >= 0) {
549
550
  }
550
551
  } else if (!headless) {
551
552
  if (installedAgents.length === 0) {
552
- console.error(`\n No known coding-agent CLI found on your PATH.\n Install one (claude / codex / gemini / aider / cursor-agent / opencode …)\n or share a specific command: npx thinkpool-pair ${room} -- <your-command>\n`)
553
+ console.error(`\n No known coding-agent CLI found on your PATH.\n Install one (claude / codex / gemini / aider / cursor-agent / opencode …)\n or share a specific command: ${pairCli(room, '--', '<your-command>')}\n`)
553
554
  process.exit(1)
554
555
  }
555
556
  attachedCmd = await pickAgent(installedAgents)
@@ -831,7 +832,7 @@ let myServeUid = null // this bridge's authed uid (null = anon)
831
832
  // 401 must NEVER serve, or the gate is bypassed (DEFECT 1). gateFailAction classifies.
832
833
  if (gateFailAction(e) === 'refuse') {
833
834
  process.stderr.write(refusalMessage({ room, name: null, reason: 'no-identity' }))
834
- process.stderr.write(`\n ◇ (serve-consent: auth rejected at the gate — ${e.message}; the stored login is expired or revoked. Re-run: npx thinkpool-pair login)\n`)
835
+ process.stderr.write(`\n ◇ (serve-consent: auth rejected at the gate — ${e.message}; the stored login is expired or revoked. Re-run: ${pairCli('login')})\n`)
835
836
  process.exit(0)
836
837
  }
837
838
  // Availability failure → fail OPEN (spec: never strand a user on a flaky fetch).
@@ -1402,7 +1403,7 @@ let standalonePairChannel = null // standalone owner bridge's direct paired-sess
1402
1403
  // standalone bridge (no supervisor) or a disabled/timed-out request returns { error }.
1403
1404
  function pairRequest(type, payload = {}, timeoutMs = 4000) {
1404
1405
  if (process.env.TP_CROSSROOM_OFF === '1') return Promise.resolve({ error: 'Cross-session reach is turned off in this room.' })
1405
- if (!IS_ACCOUNT_CHILD) return Promise.resolve({ error: 'Cross-session reach needs the ThinkPool account bridge running your sessions (npx thinkpool-pair). This room is running standalone, so it can only see its own terminals — use read_terminal.' })
1406
+ if (!IS_ACCOUNT_CHILD) return Promise.resolve({ error: `Cross-session reach needs the ThinkPool account bridge running your sessions (${PAIR_CLI}). This room is running standalone, so it can only see its own terminals — use read_terminal.` })
1406
1407
  const reqId = randomUUID()
1407
1408
  return new Promise((resolve) => {
1408
1409
  const timer = setTimeout(() => { pairWaiters.delete(reqId); resolve({ error: 'The other session did not respond in time.' }) }, timeoutMs)
@@ -0,0 +1,8 @@
1
+ // Canonical, cache-busting invocation for every command printed to people.
2
+ export const PAIR_CLI = 'npx thinkpool-pair@latest'
3
+
4
+ export function pairCli(...args) {
5
+ return [PAIR_CLI, ...args.filter((arg) => arg !== undefined && arg !== null && String(arg).length > 0)]
6
+ .map(String)
7
+ .join(' ')
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.318",
3
+ "version": "0.7.320",
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": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bridge.mjs",
11
+ "command-guidance.mjs",
11
12
  "abort-turn-barrier.mjs",
12
13
  "host-memory.mjs",
13
14
  "sdk-smoke.mjs",
package/presence.mjs CHANGED
@@ -57,6 +57,40 @@ export function makeThrottledTrack(channel, { minMs = 5000, onStatus = null } =
57
57
  }
58
58
  }
59
59
 
60
+ // A RealtimeChannel keeps its last presenceState() locally. After CHANNEL_ERROR /
61
+ // CLOSED that cache can still contain our own key even though the server (and every
62
+ // dashboard subscriber) has already removed it. Treat every subscribe/error edge as
63
+ // a new evidence generation: only a presence sync delivered in that generation may
64
+ // certify the cached self key.
65
+ export function reducePresenceChannelEvidence(state = {}, event, now = Date.now()) {
66
+ const current = {
67
+ subscribedAt: Number(state.subscribedAt) || 0,
68
+ syncSeenAt: Number(state.syncSeenAt) || 0,
69
+ }
70
+ if (event === 'sync') return { ...current, syncSeenAt: now }
71
+ if (event === 'SUBSCRIBED') {
72
+ // A reconnect loop may report SUBSCRIBED repeatedly without ever delivering
73
+ // presence sync. Preserve the first unsynced boundary so retries cannot keep
74
+ // pushing the watchdog deadline out forever.
75
+ if (current.subscribedAt && !current.syncSeenAt) return current
76
+ return { subscribedAt: now, syncSeenAt: 0 }
77
+ }
78
+ if (event === 'CLOSED' || event === 'CHANNEL_ERROR' || event === 'TIMED_OUT') {
79
+ // Likewise, repeated error callbacks are one failure episode until a fresh
80
+ // sync proves recovery.
81
+ if (current.subscribedAt && !current.syncSeenAt) return current
82
+ return { subscribedAt: now, syncSeenAt: 0 }
83
+ }
84
+ return current
85
+ }
86
+
87
+ // Recycling a websocket is useful only when plain HTTP is healthy enough to prove
88
+ // this is a Realtime-only wedge. During a full network/Supabase outage, respawning a
89
+ // launchd service cannot help and merely creates a restart storm.
90
+ export function presenceRecoveryEligible({ claimHeld, lastClaimOkAt, now, maxClaimSilenceMs = 40_000 }) {
91
+ return !!claimHeld && Number.isFinite(lastClaimOkAt) && (now - lastClaimOkAt) < maxClaimSilenceMs
92
+ }
93
+
60
94
  /* ─────────────────────────────────────────────────────────────
61
95
  presenceSelfEchoVerdict — the pure trigger for the account
62
96
  supervisor's presence self-echo watchdog (2026-07-08 recurrence).
package/provider.mjs CHANGED
@@ -31,6 +31,7 @@
31
31
  import os from 'node:os'
32
32
  import fs from 'node:fs'
33
33
  import path from 'node:path'
34
+ import { pairCli } from './command-guidance.mjs'
34
35
 
35
36
  const DIR = path.join(os.homedir(), '.thinkpool-pair')
36
37
  const FILE = path.join(DIR, 'provider.json')
@@ -68,7 +69,7 @@ export function applyProviderEnv() {
68
69
  const authToken = process.env.TP_ANTHROPIC_AUTH_TOKEN || cfg.authToken
69
70
  const model = process.env.TP_ANTHROPIC_MODEL || cfg.model
70
71
  if (!baseUrl || !authToken) {
71
- process.stderr.write(`\n ◇ provider "${provider}" is set but is missing baseUrl/authToken.\n Finish setup: npx thinkpool-pair provider custom --base <url> --token <key>\n Falling back to regular Claude for now.\n`)
72
+ process.stderr.write(`\n ◇ provider "${provider}" is set but is missing baseUrl/authToken.\n Finish setup: ${pairCli('provider', 'custom', '--base', '<url>', '--token', '<key>')}\n Falling back to regular Claude for now.\n`)
72
73
  return { provider: 'anthropic', source: 'incomplete' }
73
74
  }
74
75
  process.env.ANTHROPIC_BASE_URL = baseUrl
@@ -121,9 +122,9 @@ export async function runProvider(args) {
121
122
 
122
123
  console.error(
123
124
  '\n usage:\n' +
124
- ' npx thinkpool-pair provider # show current\n' +
125
- ' npx thinkpool-pair provider custom --base <url> --token <key> [--model <m>] # any Anthropic-compatible endpoint\n' +
126
- ' npx thinkpool-pair provider anthropic # reset to Claude\n\n' +
125
+ ` ${pairCli('provider')} # show current\n` +
126
+ ` ${pairCli('provider', 'custom', '--base', '<url>', '--token', '<key>', '[--model <m>]')} # any Anthropic-compatible endpoint\n` +
127
+ ` ${pairCli('provider', 'anthropic')} # reset to Claude\n\n` +
127
128
  ' common base urls (SDK appends /v1/messages):\n' +
128
129
  ' Z.ai GLM → https://api.z.ai/api/anthropic\n' +
129
130
  ' OpenRouter → https://openrouter.ai/api\n'
package/serve-consent.mjs CHANGED
@@ -18,6 +18,8 @@
18
18
  // so even an old bridge can no longer READ a grant (the column is gone) and will refuse
19
19
  // as a non-owner. That closes the bypass.
20
20
 
21
+ import { pairCli } from './command-guidance.mjs'
22
+
21
23
  // serveDecision — may THIS bridge serve THIS room?
22
24
  // ownerId : code_sessions.owner_id (uuid | null)
23
25
  // myUid : this bridge's authed user id (uuid | null) — null when the bridge is anon
@@ -109,7 +111,7 @@ export function refusalMessage ({ room, name, reason }) {
109
111
  const label = name ? `"${name}"` : '(unnamed)'
110
112
  if (reason === 'no-identity') {
111
113
  return `\n ◇ Refusing to serve room ${room} — this bridge is not signed in.\n` +
112
- ` A room is served only by its owner's bridge. Sign in first: npx thinkpool-pair login\n`
114
+ ` A room is served only by its owner's bridge. Sign in first: ${pairCli('login')}\n`
113
115
  }
114
116
  return `\n ◇ Refusing to serve room ${room} ${label} — it is owned by another person; only their bridge serves it.\n` +
115
117
  ` Pairing gives you a read-only view across the pair, not hosting rights. There is no override.\n`
package/service.mjs CHANGED
@@ -24,6 +24,7 @@ import path from 'node:path'
24
24
  import { execSync } from 'node:child_process'
25
25
  import { hostMemoryAdmission } from './host-memory.mjs'
26
26
  import { readSupervisorReady, supervisorReadyMatches } from './supervisor-ready.mjs'
27
+ import { pairCli } from './command-guidance.mjs'
27
28
 
28
29
  // Service identity. Account mode has no room → a single stable id so there's
29
30
  // exactly one account service per machine (a second install replaces it).
@@ -499,10 +500,10 @@ export function installService(room, cmdArgs = [], { autoUpdate = false, version
499
500
  ? (process.platform === 'win32'
500
501
  ? 'auto-update ON: a new thinkpool-pair@latest applies on the next login/restart (Windows has no in-process self-update).'
501
502
  : `auto-update ON: checks npm ~every 30 min and restarts to apply a new @latest once sessions are idle (≥${idleSecs}s quiet; they resume in place).`)
502
- : `PINNED to ${version} (stable local runtime; restarts do not depend on npx or the network). To update: use "Restart & update the bridge" (or re-run install-service from a newer thinkpool-pair). Pass --auto-update to track @latest instead.`
503
+ : `PINNED to ${version} (stable local runtime; restarts do not depend on npx or the network). To update: use "Restart & update the bridge" (or run ${pairCli('install-service', room || undefined)}). Pass --auto-update to track @latest instead.`
503
504
  const removeArg = room ? ` ${room}` : ''
504
505
  const what = room ? `room ${room}` : 'your account (auto-serves every session)'
505
- process.stderr.write(` ◆ ${what}\n ◆ ${process.platform === 'darwin' ? 'Launchd is completing and verifying the reload independently.' : a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: thinkpool-pair uninstall-service${removeArg}\n\n`)
506
+ process.stderr.write(` ◆ ${what}\n ◆ ${process.platform === 'darwin' ? 'Launchd is completing and verifying the reload independently.' : a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: ${pairCli('uninstall-service')}${removeArg}\n\n`)
506
507
  return true
507
508
  }
508
509