thinkpool-pair 0.7.292 → 0.7.293

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
@@ -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; joining/reloading clients get it replayed, so the room
148
- never opens blank while the bridge is up. **Nothing is stored server-side** —
149
- history lives exactly as long as the bridge runs.
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/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') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.292",
3
+ "version": "0.7.293",
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
+