thinkpool-pair 0.7.78 → 0.7.80
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 +19 -3
- package/bridge.mjs +16 -6
- package/claude-session.mjs +3 -0
- package/launcher.mjs +7 -1
- package/package.json +1 -1
- package/service.mjs +30 -7
package/README.md
CHANGED
|
@@ -7,6 +7,11 @@ no copy/paste.
|
|
|
7
7
|
> A web page can't read your terminal directly (browser security). This tiny
|
|
8
8
|
> helper is the bridge: it runs CLIs in PTYs and streams them to the room.
|
|
9
9
|
|
|
10
|
+
> **Don't want it tied to your laptop?** Run this same bridge on an always-on box
|
|
11
|
+
> you control (a cheap VM, a home server, a Raspberry Pi) so the agent stays live
|
|
12
|
+
> with your laptop closed. It's outbound-only — no ports, no public IP. See
|
|
13
|
+
> [**Run it in the cloud**](#run-it-in-the-cloud-remote-host--vm--container) below.
|
|
14
|
+
|
|
10
15
|
## Run it (one command)
|
|
11
16
|
|
|
12
17
|
```bash
|
|
@@ -63,9 +68,20 @@ cd /path/to/your/repo
|
|
|
63
68
|
npx thinkpool-pair@latest <ROOM> -- claude # structured Claude, no TTY needed
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
- **Auth
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
- **Auth** — two ways to sign in on a box with no browser. Both are your own
|
|
72
|
+
account on your own hardware, so both are fine:
|
|
73
|
+
- **API key** — `export ANTHROPIC_API_KEY=sk-ant-...` from your Anthropic
|
|
74
|
+
Console. Simplest, pay-per-token, cleanest cost tracking.
|
|
75
|
+
- **Your Claude subscription** — a server has no browser, so mint a long-lived
|
|
76
|
+
token *on a machine that does*: run `claude setup-token` there (prints a
|
|
77
|
+
~1-year `CLAUDE_CODE_OAUTH_TOKEN`), then `export CLAUDE_CODE_OAUTH_TOKEN=...`
|
|
78
|
+
on the box. Uses your existing Pro/Max plan — no per-token bill.
|
|
79
|
+
|
|
80
|
+
The Agent SDK reads either one automatically and runs its bundled `claude`, so
|
|
81
|
+
you don't need Claude Code installed separately. (Note: this is *you* running
|
|
82
|
+
*your* login on *your* machine. Handing a subscription token to a third-party
|
|
83
|
+
host to run for you is what Anthropic's terms forbid — that's why a future
|
|
84
|
+
"we host it for you" option would require an API key, not your subscription.)
|
|
69
85
|
- **Use `@latest`**: a bare `npx thinkpool-pair` reuses npx's local cache and can
|
|
70
86
|
run a stale version across restarts. `@latest` (and the service below) always
|
|
71
87
|
re-resolve the newest publish.
|
package/bridge.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import os from 'node:os'
|
|
|
32
32
|
import fs from 'node:fs'
|
|
33
33
|
import path from 'node:path'
|
|
34
34
|
import readline from 'node:readline'
|
|
35
|
-
import { spawn } from 'node:child_process'
|
|
35
|
+
import { spawn, execSync } from 'node:child_process'
|
|
36
36
|
import { fileURLToPath } from 'node:url'
|
|
37
37
|
import { randomUUID } from 'node:crypto'
|
|
38
38
|
import { createClient } from '@supabase/supabase-js'
|
|
@@ -213,12 +213,22 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
|
|
|
213
213
|
installService: ({ room = null, agentCmd } = {}) => { svc.installService(room, agentCmd ? [agentCmd] : []) },
|
|
214
214
|
uninstallService: ({ room = null } = {}) => { svc.uninstallService(room) },
|
|
215
215
|
restartService: ({ room = null } = {}) => { svc.restartService(room) },
|
|
216
|
-
// "Restart & update the bridge" —
|
|
217
|
-
//
|
|
218
|
-
//
|
|
216
|
+
// "Restart & update the bridge" — resolve the NEWEST PUBLISHED version from npm and
|
|
217
|
+
// re-pin the OS service to it (stale-proof: the rewritten launch wipes the npx cache
|
|
218
|
+
// each start so it can't re-serve an old build). We must NOT trust PKG_VERSION here:
|
|
219
|
+
// npx can run a STALE cached launcher, so this very menu may be an old version — that's
|
|
220
|
+
// the bug that left the bridge stuck on 0.7.75. `npm view` hits the registry live, so it
|
|
221
|
+
// updates correctly even from a stale launcher. Pinned to the resolved number — no auto-roll.
|
|
219
222
|
restartUpdateService: ({ room = null } = {}) => {
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
let target = null
|
|
224
|
+
try { target = execSync('npm view thinkpool-pair version', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() } catch { /* offline / no npm */ }
|
|
225
|
+
if (!target) {
|
|
226
|
+
target = PKG_VERSION
|
|
227
|
+
process.stderr.write(`\n ⚠ couldn't reach npm to resolve the newest version — re-pinning this launcher's v${PKG_VERSION} (stale-proof) and restarting…\n`)
|
|
228
|
+
} else {
|
|
229
|
+
process.stderr.write(`\n ◆ updating the background service to v${target} (newest published) and restarting…\n`)
|
|
230
|
+
}
|
|
231
|
+
svc.installService(room, [], { version: target, staleProof: true }) // rewrites plist/unit at resolved version + reloads = update + restart
|
|
222
232
|
},
|
|
223
233
|
login: async () => { const { runLogin } = await import('./account.mjs'); await runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) },
|
|
224
234
|
// Provider config is NOT terminal — it writes provider.json and returns to the
|
package/claude-session.mjs
CHANGED
|
@@ -361,6 +361,9 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
361
361
|
'LINKS & ARTIFACTS (hard rule for ThinkPool Code rooms): the room is driven from a phone or browser, and the bridge runs on the host machine — so a local filesystem path, a file:// URL, or a localhost address only opens on the host and is useless to the room (a partner literally cannot open it).',
|
|
362
362
|
'Whenever you produce an HTML artifact (a demo, mockup, preview, report, or page) or surface ANY link meant to be opened or shared, NEVER hand the room a local path, a file:// URL, or a localhost/127.0.0.1 address. Publish it to a GitHub-shareable URL that renders in a browser — push the HTML to a GitHub repo and give its GitHub Pages URL (or an equivalent raw-HTML URL that actually renders, not raw.githubusercontent.com which serves HTML as plain text) — so anyone in the room can open it. The same applies to any other link you surface: it must be one the room can reach, not a host-only path.',
|
|
363
363
|
'If you cannot publish a shareable link, say so and ask how to proceed — do not fall back to handing over a local path.',
|
|
364
|
+
'SHOW YOUR WORK, do not just describe it: the room is watched live from a phone, where a wall of text is painful to read. Whenever you build, change, or fix anything visual — a UI, a page, an HTML artifact, a chart, a diagram, a rendered result — capture a screenshot and surface the PNG (for example, read the image file into your turn) so it appears inline in the room. The room auto-uploads any image you surface. Default to showing a picture of the result over narrating it; err toward more screenshots, not fewer.',
|
|
365
|
+
'HTML PREVIEWS: for any HTML you produce, give the room something it can actually open, by whichever path is available. (a) IN-ROOM PREVIEW: if a mockup render helper is present — one that writes a manifest into the directory named by $TP_MOCKUP_OUTBOX, such as the mockup-iterate render.sh script when the bridge was launched from the thinkpool repo — use it; it surfaces an inline card with desktop and mobile views that the room can expand. (b) SHAREABLE LINK: additionally or otherwise, publish the HTML to a GitHub Pages URL per the LINKS & ARTIFACTS rule above and post the link. Never leave an HTML artifact viewable only as a local path.',
|
|
366
|
+
'RUN IT, DO NOT ASK: verify your own work before calling it done. Actually run or serve what you changed, observe that it behaves correctly, and show the evidence in the room — a screenshot of the running result, the passing test output, or the real response — rather than telling the user "this should work, go test it." If you could not verify something, say exactly what is unverified. This room follows verify-before-claiming: runtime evidence you produced, not assertion.',
|
|
364
367
|
].join(' '),
|
|
365
368
|
// Needed for live thinking-token progress (SDKThinkingTokensMessage) to
|
|
366
369
|
// flow during a turn. We ignore the fine-grained stream_event partials in
|
package/launcher.mjs
CHANGED
|
@@ -18,6 +18,11 @@ import { execSync } from 'node:child_process'
|
|
|
18
18
|
const HOME = os.homedir()
|
|
19
19
|
const CFG_DIR = path.join(HOME, '.thinkpool-pair')
|
|
20
20
|
|
|
21
|
+
// Version of THIS running launcher (read from the package it shipped with). Shown in the
|
|
22
|
+
// header so the running build is visible — and so a stale npx cache is obvious at a glance
|
|
23
|
+
// (header says 0.7.75 while npm @latest is 0.7.78 → time to "Restart & update the bridge").
|
|
24
|
+
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return null } })()
|
|
25
|
+
|
|
21
26
|
export const KNOWN_AGENTS = [
|
|
22
27
|
{ label: 'Claude Code', cmd: 'claude', resume: ['--continue'] },
|
|
23
28
|
{ label: 'Codex CLI', cmd: 'codex' },
|
|
@@ -75,6 +80,7 @@ export function detectState() {
|
|
|
75
80
|
agents: KNOWN_AGENTS.filter(a => onPath(a.cmd)),
|
|
76
81
|
accountSvc: serviceLoaded(null),
|
|
77
82
|
cwd: process.cwd(),
|
|
83
|
+
version: VERSION,
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
@@ -103,7 +109,7 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
|
|
|
103
109
|
|
|
104
110
|
const header = () => {
|
|
105
111
|
io.print('')
|
|
106
|
-
io.print(' ' + C.dim('┌ ') + C.bold('thinkpool-pair') + C.dim(' ' + '─'.repeat(40)))
|
|
112
|
+
io.print(' ' + C.dim('┌ ') + C.bold('thinkpool-pair') + (state.version ? C.dim(` v${state.version}`) : '') + C.dim(' ' + '─'.repeat(state.version ? 40 - (` v${state.version}`).length : 40)))
|
|
107
113
|
io.print(` ${C.dim('│ account ')} ${state.loggedIn ? C.green((state.email || 'linked') + ' ✓') : C.yellow('not linked')}`)
|
|
108
114
|
io.print(` ${C.dim('│ provider ')} ${state.provider}`)
|
|
109
115
|
io.print(` ${C.dim('│ agents ')} ${state.agents.length ? state.agents.map(a => a.label).join(', ') : C.yellow('none on PATH')}`)
|
package/package.json
CHANGED
package/service.mjs
CHANGED
|
@@ -27,6 +27,14 @@ import { execSync } from 'node:child_process'
|
|
|
27
27
|
const label = (room) => room ? `io.thinkpool.pair.${room.toLowerCase()}` : 'io.thinkpool.pair.account'
|
|
28
28
|
const slug = (room) => room || 'account'
|
|
29
29
|
const xml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
30
|
+
const shq = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`
|
|
31
|
+
// Stale-proof launch wrapper: wipe any cached thinkpool-pair in the npx store, then exec
|
|
32
|
+
// the pinned package FRESH. Folds update-local-service.sh into the builder — without this a
|
|
33
|
+
// deliberate "Restart & update" (or any KeepAlive restart) can re-serve a stale npx cache,
|
|
34
|
+
// which is what left Max's account bridge stuck on 0.7.75 while @latest was 0.7.78.
|
|
35
|
+
// `home` is the platform-correct, runtime-expanded home token ("$HOME" on darwin under
|
|
36
|
+
// /bin/bash; "$$HOME" inside a systemd unit so systemd emits a literal $HOME to bash).
|
|
37
|
+
const cacheWipe = (home) => `rm -rf ${home}/.npm/_npx/*/node_modules/thinkpool-pair 2>/dev/null`
|
|
30
38
|
|
|
31
39
|
// The version installing the service. By DEFAULT the service is pinned to this exact
|
|
32
40
|
// version (not @latest) so a future bad npm publish can't auto-roll to every machine's
|
|
@@ -46,7 +54,7 @@ function npxPath(platform) {
|
|
|
46
54
|
|
|
47
55
|
// Pure artifact builder — returned shape is testable without side effects.
|
|
48
56
|
// room falsy → ACCOUNT service (bare `thinkpool-pair`, auto-serves all sessions).
|
|
49
|
-
export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate = false } = {}) {
|
|
57
|
+
export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate = false, version = VERSION, staleProof = false } = {}) {
|
|
50
58
|
const npx = npxPath(platform)
|
|
51
59
|
const cwd = process.cwd()
|
|
52
60
|
const id = slug(room)
|
|
@@ -58,14 +66,18 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
58
66
|
// PINNED by default to `@${VERSION}` so a KeepAlive restart re-runs the SAME known-good
|
|
59
67
|
// version — a bad publish can't auto-roll here. `autoUpdate` opts into `@latest` tracking
|
|
60
68
|
// (and the in-process self-updater via THINKPOOL_PAIR_AUTOUPDATE below).
|
|
61
|
-
const verSpec = autoUpdate ? 'thinkpool-pair@latest' : `thinkpool-pair@${
|
|
69
|
+
const verSpec = autoUpdate ? 'thinkpool-pair@latest' : `thinkpool-pair@${version}`
|
|
62
70
|
const pkgArgs = room ? ['-y', verSpec, room] : ['-y', verSpec]
|
|
63
71
|
// In-process self-update (account.mjs / bridge.mjs) is gated on this env — only on when
|
|
64
72
|
// the user explicitly opted into auto-update.
|
|
65
73
|
const autoUpdateEnv = autoUpdate ? { THINKPOOL_PAIR_AUTOUPDATE: '1' } : {}
|
|
66
74
|
|
|
67
75
|
if (platform === 'darwin') {
|
|
68
|
-
|
|
76
|
+
// launchd KeepAlive supervises. staleProof → wrap in a cache-wiping /bin/bash launch so
|
|
77
|
+
// every (re)start re-fetches the pinned version fresh (deliberate-update path).
|
|
78
|
+
const args = staleProof
|
|
79
|
+
? ['/bin/bash', '-lc', `${cacheWipe('"$HOME"')}; exec ${[npx, ...pkgArgs, ...tail].map(shq).join(' ')}`]
|
|
80
|
+
: [npx, ...pkgArgs, ...tail]
|
|
69
81
|
const file = path.join(os.homedir(), 'Library', 'LaunchAgents', `${label(room)}.plist`)
|
|
70
82
|
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
71
83
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -102,12 +114,17 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
102
114
|
if (platform === 'linux') {
|
|
103
115
|
const args = [npx, ...pkgArgs, ...tail] // systemd Restart=always supervises
|
|
104
116
|
const file = path.join(os.homedir(), '.config', 'systemd', 'user', `${label(room)}.service`)
|
|
117
|
+
// staleProof → cache-wiping /bin/bash launch (same intent as darwin). `$$HOME` so systemd
|
|
118
|
+
// emits a literal $HOME to bash rather than expanding it itself (empty + warning).
|
|
119
|
+
const execStart = staleProof
|
|
120
|
+
? `/bin/bash -lc "${cacheWipe('$$HOME')}; exec ${[npx, ...pkgArgs, ...tail].join(' ')}"`
|
|
121
|
+
: args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(' ')
|
|
105
122
|
const content = `[Unit]
|
|
106
123
|
Description=ThinkPool Code ${desc}
|
|
107
124
|
After=network-online.target
|
|
108
125
|
|
|
109
126
|
[Service]
|
|
110
|
-
ExecStart=${
|
|
127
|
+
ExecStart=${execStart}
|
|
111
128
|
Restart=always
|
|
112
129
|
RestartSec=2
|
|
113
130
|
WorkingDirectory=${cwd}
|
|
@@ -138,8 +155,14 @@ WantedBy=default.target
|
|
|
138
155
|
throw new Error(`unsupported platform: ${platform}`)
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
// staleProof defaults ON for every real install: even a FIRST install can be launched from
|
|
159
|
+
// a stale npx cache (the launcher itself is npx-resolved), so without the cache-wiping
|
|
160
|
+
// wrapper a fresh `install-service` could pin a stale build just like the update path did.
|
|
161
|
+
// Pinning still holds (we pin a fixed version number) — staleProof only guarantees that
|
|
162
|
+
// pinned version is fetched fresh on every (re)start. buildArtifact keeps staleProof=false
|
|
163
|
+
// so its unit tests assert the pure pinned shape.
|
|
164
|
+
export function installService(room, cmdArgs = [], { autoUpdate = false, version = VERSION, staleProof = true } = {}) {
|
|
165
|
+
const a = buildArtifact(process.platform, { room, cmdArgs, autoUpdate, version, staleProof })
|
|
143
166
|
fs.mkdirSync(a.logDir, { recursive: true })
|
|
144
167
|
fs.mkdirSync(path.dirname(a.file), { recursive: true })
|
|
145
168
|
fs.writeFileSync(a.file, a.content)
|
|
@@ -155,7 +178,7 @@ export function installService(room, cmdArgs = [], { autoUpdate = false } = {})
|
|
|
155
178
|
? (process.platform === 'win32'
|
|
156
179
|
? 'auto-update ON: a new thinkpool-pair@latest applies on the next login/restart (Windows has no in-process self-update).'
|
|
157
180
|
: `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).`)
|
|
158
|
-
: `PINNED to ${
|
|
181
|
+
: `PINNED to ${version}${staleProof ? ' (stale-proof: every restart re-fetches it fresh, never a stale npx cache)' : ''} (safe default — a bad publish can't auto-roll to this always-on bridge). To update: use "Restart & update the bridge" (or re-run install-service from a newer thinkpool-pair). Pass --auto-update to track @latest instead.`
|
|
159
182
|
const removeArg = room ? ` ${room}` : ''
|
|
160
183
|
const what = room ? `room ${room}` : 'your account (auto-serves every session)'
|
|
161
184
|
process.stderr.write(` ◆ ${what}\n ◆ ${a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: thinkpool-pair uninstall-service${removeArg}\n\n`)
|