thinkpool-pair 0.7.292 → 0.7.294
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 +16 -3
- package/account.mjs +10 -3
- package/bridge.mjs +15 -2
- package/package.json +2 -1
- package/privacy-report.mjs +108 -0
package/README.md
CHANGED
|
@@ -144,12 +144,25 @@ npx thinkpool-pair@latest <ROOM> -- claude # structured Claude, no TTY
|
|
|
144
144
|
- `pty-in` — keystrokes/prompts from the web → written to that terminal's PTY.
|
|
145
145
|
- `term-open` / `term-close` / `term-exit` — web-driven terminal lifecycle.
|
|
146
146
|
- `replay-request` / `pty-replay` — each terminal keeps a rolling ~120 KB
|
|
147
|
-
scrollback buffer
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
scrollback buffer for live recovery. Thinkpool also stores the cleaned reader
|
|
148
|
+
transcript and room events so members can reopen the room. It does not store
|
|
149
|
+
a copy of the repository or the raw PTY byte stream.
|
|
150
150
|
- `resize` — web viewport size → headless PTYs only (the attached terminal
|
|
151
151
|
follows your own TTY).
|
|
152
152
|
|
|
153
|
+
## Inspect the privacy boundary
|
|
154
|
+
|
|
155
|
+
Run a local report before pairing a repository:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx thinkpool-pair@latest privacy-report
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
It lists the project directories configured for this bridge, the local records
|
|
162
|
+
present under `~/.thinkpool-pair`, known outbound service domains, what
|
|
163
|
+
Thinkpool stores remotely, and the limits of the report. It never prints a
|
|
164
|
+
provider key, refresh token, or bridge private key.
|
|
165
|
+
|
|
153
166
|
## Run any model
|
|
154
167
|
|
|
155
168
|
ThinkPool Code runs **any model you choose** — not just Anthropic. The agent
|
package/account.mjs
CHANGED
|
@@ -416,6 +416,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
416
416
|
const childIdle = new Map() // room -> bool (last idle report: in the quiet window)
|
|
417
417
|
const childBetween = new Map() // room -> bool (between turns now — safe to restart per Contract #1)
|
|
418
418
|
const childPeer = new Map() // room -> bool (a web client is watching this room)
|
|
419
|
+
const readyRooms = new Set() // child subscribed + restored + announced usable room state
|
|
419
420
|
// Thinkpool Ensemble cross-ROOM router (Tier 1): the supervisor is the only process
|
|
420
421
|
// that knows every room on this machine, so it routes a child's read_session into the
|
|
421
422
|
// target room's child and relays the reply back. roomNames feeds list_sessions; pairPending
|
|
@@ -562,7 +563,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
562
563
|
// to spawn against — re-read each announce so an add/remove reflects immediately.
|
|
563
564
|
// Both are additive; older clients ignore them (feature: multi-provider BYOK, slice 1).
|
|
564
565
|
const PROVIDER_PUBKEY = publicKeyB64()
|
|
565
|
-
const pushPresence = () => trackPresence({ name: machine, bridgeId: BRIDGE_ID, version: VERSION, rooms: [...children.keys()], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), agents: installedAgentCommands().map((cmd) => ({ cmd })), ts: Date.now() })
|
|
566
|
+
const pushPresence = () => trackPresence({ name: machine, bridgeId: BRIDGE_ID, version: VERSION, rooms: [...children.keys()], readyRooms: [...readyRooms], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), agents: installedAgentCommands().map((cmd) => ({ cmd })), ts: Date.now() })
|
|
566
567
|
|
|
567
568
|
// ── Provider registry — the multi-BYOK wire contract (slice 1). ─────────────
|
|
568
569
|
// AUTH: the account channel `tpacct:<uid>` is created WITHOUT config.private:true,
|
|
@@ -895,12 +896,18 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
895
896
|
console.log(`child_spawn sup=${SUP_ID} room=${room} pid=${child.pid} dir=${dir} had_prior=${alreadyHad}`)
|
|
896
897
|
|
|
897
898
|
children.set(room, child)
|
|
899
|
+
readyRooms.delete(room)
|
|
898
900
|
childIdle.set(room, false) // unknown until it reports → never restart a fresh child
|
|
899
901
|
childBetween.set(room, false)
|
|
900
902
|
childPeer.set(room, false)
|
|
901
903
|
child.on('message', (m) => {
|
|
902
904
|
if (!m) return
|
|
903
|
-
if (m.t === '
|
|
905
|
+
if (m.t === 'room-ready') {
|
|
906
|
+
readyRooms.add(room)
|
|
907
|
+
console.log(`child_ready sup=${SUP_ID} room=${room} pid=${child.pid}`)
|
|
908
|
+
pushPresence()
|
|
909
|
+
}
|
|
910
|
+
else if (m.t === 'idle') { childIdle.set(room, !!m.idle); childBetween.set(room, m.between != null ? !!m.between : !!m.idle); childPeer.set(room, !!m.webPeer) }
|
|
904
911
|
else if (m.t === 'apply-update') { applyRequested = true; applyIfIdle() } // user clicked the chip
|
|
905
912
|
// ── Thinkpool Ensemble cross-ROOM routing ──────────────────────────────
|
|
906
913
|
// `room` here is the room that sent the message (this closure is per-child).
|
|
@@ -993,7 +1000,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
993
1000
|
// ── supervisor child_exit logging (cascade brg-instrument) ─────────────────
|
|
994
1001
|
console.log(`child_exit sup=${SUP_ID} room=${room} pid=${child?.pid || '?'} code=${code || '?'}`)
|
|
995
1002
|
|
|
996
|
-
children.delete(room); childIdle.delete(room); childBetween.delete(room); childPeer.delete(room); roomNames.delete(room); roomPartner.delete(room) // re-served on the next tick
|
|
1003
|
+
children.delete(room); readyRooms.delete(room); childIdle.delete(room); childBetween.delete(room); childPeer.delete(room); roomNames.delete(room); roomPartner.delete(room) // re-served on the next tick
|
|
997
1004
|
// Fail any in-flight cross-room peeks/posts this dead room had ASKED (bus-origin
|
|
998
1005
|
// entries have askerRoom null and just time out on their own).
|
|
999
1006
|
for (const [reqId, w] of pairPending) { if (w?.askerRoom === room) pairPending.delete(reqId) }
|
package/bridge.mjs
CHANGED
|
@@ -226,6 +226,12 @@ const pickAgent = (installed) => new Promise((resolve) => {
|
|
|
226
226
|
|
|
227
227
|
const argv = process.argv.slice(2)
|
|
228
228
|
|
|
229
|
+
if (argv[0] === 'privacy-report') {
|
|
230
|
+
const { runPrivacyReport } = await import('./privacy-report.mjs')
|
|
231
|
+
runPrivacyReport()
|
|
232
|
+
process.exit(0)
|
|
233
|
+
}
|
|
234
|
+
|
|
229
235
|
// Boot-persistent service install (cross-platform: launchd / systemd / Windows
|
|
230
236
|
// Startup). Subcommand form: `thinkpool-pair install-service <ROOM> [-- <cmd>]`.
|
|
231
237
|
if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
@@ -1111,7 +1117,7 @@ const announce = () => {
|
|
|
1111
1117
|
// on a custom provider without the owner's account-channel registry.
|
|
1112
1118
|
const provNames = providerNameMap()
|
|
1113
1119
|
const rev = ++announceRev
|
|
1114
|
-
return
|
|
1120
|
+
return bcastAwait('bridge', {
|
|
1115
1121
|
v: 2, name, bridge_id: BRIDGE_ID, started_at: BRIDGE_STARTED_AT, rev, repo: repoLabel, branch: readBranch(),
|
|
1116
1122
|
// sdkWarn: the auto-pulled agent SDK failed its boot compatibility smoke test —
|
|
1117
1123
|
// surfaced so the room can show a banner (turns may misbehave; pin a good SDK).
|
|
@@ -4453,7 +4459,14 @@ channel
|
|
|
4453
4459
|
openTerm({ id: ptyId, cmd: startCmd, args: attachedArgs, attached: !autoAgent })
|
|
4454
4460
|
}
|
|
4455
4461
|
}
|
|
4456
|
-
announce()
|
|
4462
|
+
await announce()
|
|
4463
|
+
// Account presence must distinguish a spawned child from a usable room.
|
|
4464
|
+
// Signal only after realtime subscribed, durable sessions restored, and the
|
|
4465
|
+
// first authoritative roster was announced. The dashboard keeps the bridge
|
|
4466
|
+
// in "Restarting" until every pre-restart room reaches this boundary.
|
|
4467
|
+
if (process.send && process.env.THINKPOOL_PAIR_ACCOUNT_CHILD === '1') {
|
|
4468
|
+
try { process.send({ t: 'room-ready', room, bridgeId: BRIDGE_ID }) } catch { /* supervisor exited */ }
|
|
4469
|
+
}
|
|
4457
4470
|
process.stderr.write(headless
|
|
4458
4471
|
? `\n ◆ thinkpool — relaying room ${room} (headless). Open terminals from the web UI.\n\n`
|
|
4459
4472
|
: `\n ◆ thinkpool — sharing "${attachedCmd}"${continuing ? ' (continuing your latest session)' : ''} into room ${room}. Open the web UI and you're both in.\n\n`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.294",
|
|
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
|
"sdk-admission.mjs",
|
|
14
14
|
"sdk-admission.mjs",
|
|
15
15
|
"launcher.mjs",
|
|
16
|
+
"privacy-report.mjs",
|
|
16
17
|
"byok-detect.mjs",
|
|
17
18
|
"context-windows.mjs",
|
|
18
19
|
"claude-session.mjs",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const cleanHost = (value) => {
|
|
6
|
+
try { return new URL(value).host } catch { return null }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const readJson = (file, fallback) => {
|
|
10
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return fallback }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const displayPath = (value, home) => {
|
|
14
|
+
const resolved = path.resolve(String(value || ''))
|
|
15
|
+
return resolved === home ? '~' : resolved.startsWith(`${home}${path.sep}`) ? `~${resolved.slice(home.length)}` : resolved
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildPrivacyReport({
|
|
19
|
+
home = os.homedir(),
|
|
20
|
+
cwd = process.cwd(),
|
|
21
|
+
env = process.env,
|
|
22
|
+
exists = fs.existsSync,
|
|
23
|
+
read = readJson,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const configDir = path.join(home, '.thinkpool-pair')
|
|
26
|
+
const dirs = read(path.join(configDir, 'dirs.json'), {}) || {}
|
|
27
|
+
const served = read(path.join(configDir, 'served.json'), {}) || {}
|
|
28
|
+
const providers = read(path.join(configDir, 'providers.json'), []) || []
|
|
29
|
+
const legacyProvider = read(path.join(configDir, 'provider.json'), null)
|
|
30
|
+
let defaultDir = ''
|
|
31
|
+
try { defaultDir = fs.readFileSync(path.join(configDir, 'default-dir'), 'utf8').trim() } catch { /* not configured */ }
|
|
32
|
+
|
|
33
|
+
const projectDirs = new Set([cwd, defaultDir, ...Object.values(dirs), ...Object.values(served)].filter(Boolean).map((p) => displayPath(p, home)))
|
|
34
|
+
const providerHosts = new Set([
|
|
35
|
+
...providers.map((provider) => cleanHost(provider?.baseUrl)),
|
|
36
|
+
cleanHost(legacyProvider?.baseUrl),
|
|
37
|
+
].filter(Boolean))
|
|
38
|
+
const supabaseHost = cleanHost(env.TP_SUPABASE_URL || 'https://daytvtakmlixpfbbqzjd.supabase.co')
|
|
39
|
+
const webHost = cleanHost(env.TP_WEB_BASE || 'https://thinkpool.io')
|
|
40
|
+
|
|
41
|
+
const localRecords = [
|
|
42
|
+
['Saved login', 'auth.json', exists(path.join(configDir, 'auth.json')), 'refresh token and account identity'],
|
|
43
|
+
['Provider registry', 'providers.json', exists(path.join(configDir, 'providers.json')), 'provider endpoint, model, and API key'],
|
|
44
|
+
['Bridge keypair', 'bridge-key.json', exists(path.join(configDir, 'bridge-key.json')), 'private key used to open browser-sealed provider keys'],
|
|
45
|
+
['Room directories', 'dirs.json / served.json', exists(path.join(configDir, 'dirs.json')) || exists(path.join(configDir, 'served.json')), 'room-to-project directory mappings'],
|
|
46
|
+
['Bridge logs', 'update.log and service logs', exists(path.join(configDir, 'update.log')), 'update and background-service diagnostics'],
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
generatedAt: new Date().toISOString(),
|
|
51
|
+
configDir: displayPath(configDir, home),
|
|
52
|
+
projectDirs: [...projectDirs].sort(),
|
|
53
|
+
localRecords,
|
|
54
|
+
outbound: [
|
|
55
|
+
[webHost, 'Thinkpool configuration, linking, version checks, and product APIs'],
|
|
56
|
+
[supabaseHost, 'authentication, room database, storage, and realtime transport'],
|
|
57
|
+
['registry.npmjs.org', 'package installation and updates initiated with npm or npx'],
|
|
58
|
+
...[...providerHosts].sort().map((host) => [host, 'AI provider selected on this bridge']),
|
|
59
|
+
].filter(([host]) => host),
|
|
60
|
+
remote: [
|
|
61
|
+
['Thinkpool cloud receives', 'account/profile data; room membership and metadata; room chat; cleaned terminal output; terminal and room events; attachments; operational request metadata'],
|
|
62
|
+
['Thinkpool cloud does not receive as a repository', 'a crawled copy of the project directory or repository file tree'],
|
|
63
|
+
['Readable provider key', 'stored locally by the bridge; Thinkpool relays only a browser-sealed envelope'],
|
|
64
|
+
['AI prompts and code context', 'sent by the local coding agent to the selected AI provider; content depends on that runtime and the task'],
|
|
65
|
+
],
|
|
66
|
+
limits: [
|
|
67
|
+
'Coding-agent runtimes and commands can contact domains beyond the bridge list above.',
|
|
68
|
+
'Thinkpool room transcripts are readable by Thinkpool infrastructure today; they are not end-to-end encrypted.',
|
|
69
|
+
'This report describes configured access. It is not a packet capture or a guarantee that a third-party agent obeys the same boundary.',
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatPrivacyReport(report) {
|
|
75
|
+
const yn = (value) => value ? 'present' : 'not present'
|
|
76
|
+
const lines = [
|
|
77
|
+
'',
|
|
78
|
+
' THINKPOOL PRIVACY REPORT',
|
|
79
|
+
` generated ${report.generatedAt}`,
|
|
80
|
+
'',
|
|
81
|
+
' LOCAL ACCESS',
|
|
82
|
+
` Bridge configuration: ${report.configDir}`,
|
|
83
|
+
...report.projectDirs.map((dir) => ` Project directory: ${dir}`),
|
|
84
|
+
'',
|
|
85
|
+
' LOCAL RECORDS',
|
|
86
|
+
...report.localRecords.map(([label, file, present, contains]) => ` ${label}: ${yn(present)} (${file})\n contains: ${contains}`),
|
|
87
|
+
'',
|
|
88
|
+
' OUTBOUND DOMAINS',
|
|
89
|
+
...report.outbound.map(([host, reason]) => ` ${host}\n ${reason}`),
|
|
90
|
+
'',
|
|
91
|
+
' REMOTE DATA',
|
|
92
|
+
...report.remote.map(([label, detail]) => ` ${label}:\n ${detail}`),
|
|
93
|
+
'',
|
|
94
|
+
' IMPORTANT LIMITS',
|
|
95
|
+
...report.limits.map((line) => ` - ${line}`),
|
|
96
|
+
'',
|
|
97
|
+
' Full data map: https://thinkpool.io/data',
|
|
98
|
+
'',
|
|
99
|
+
]
|
|
100
|
+
return lines.join('\n')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function runPrivacyReport(options) {
|
|
104
|
+
const report = buildPrivacyReport(options)
|
|
105
|
+
process.stdout.write(formatPrivacyReport(report))
|
|
106
|
+
return report
|
|
107
|
+
}
|
|
108
|
+
|