thinkpool-pair 0.7.364 → 0.7.366

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 (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
package/presence.mjs CHANGED
@@ -1,191 +1 @@
1
- /* ─────────────────────────────────────────────────────────────
2
- presence.mjs — a throttled wrapper for Supabase Realtime presence
3
- track(), shared by the account supervisor (account.mjs) and every
4
- room bridge (bridge.mjs).
5
-
6
- The bug it fixes: supabase-js auto-rejoins a dropped channel and
7
- re-fires `SUBSCRIBED`; our subscribe handlers re-`track()` presence
8
- on each one. With no throttle a SINGLE drop can spiral —
9
- track() → trip the per-client presence rate limit
10
- → server closes the channel → auto-rejoin → SUBSCRIBED
11
- → track() → trip again → … forever
12
- which the server logs as `ClientPresenceRateLimitReached:
13
- :client_rate_limit_exceeded`. The supervisor stays alive and keeps
14
- relaying sessions, but its presence never re-sticks, so the dashboard
15
- reads "No bridge connected" while sessions plainly run (the
16
- 2026-06-17 / -22 / -24 recurrences).
17
-
18
- makeThrottledTrack coalesces bursts to at most one track() per
19
- window: the FIRST call fires immediately (presence appears fast),
20
- any further calls inside the window collapse into a single trailing
21
- fire with the latest payload. At minMs=5000 that's ≤0.2 track/s —
22
- far under any presence rate limit — so a reconnect storm can never
23
- trip it, while the steady-state callers (a 15s tick, a token refresh)
24
- are unaffected (their interval already exceeds the window).
25
- ───────────────────────────────────────────────────────────── */
26
-
27
- // channel: a supabase RealtimeChannel (stable across reconnects — supabase-js
28
- // rejoins the same object, so channel.track stays valid). Returns a function you
29
- // call with the presence payload anywhere you'd have called channel.track(payload).
30
- // onStatus (optional): called with each track()'s server status ('ok' | 'timed out' |
31
- // 'error') so a caller can retain the last one for a postmortem log line. Best-effort —
32
- // a track that throws reports 'error' and never breaks the caller.
33
- export function makeThrottledTrack(channel, { minMs = 5000, onStatus = null } = {}) {
34
- let last = 0
35
- let timer = null
36
- let pending = null
37
- const fire = async () => {
38
- timer = null
39
- last = Date.now()
40
- const payload = pending
41
- pending = null
42
- // Await the track so we can retain its status for the next postmortem. fire() is never
43
- // awaited by callers, so this stays fire-and-forget — the await only sequences the
44
- // onStatus callback; it never blocks a caller and a rejection can't escape (caught here).
45
- try { const status = await channel.track(payload); onStatus?.(status ?? 'ok') }
46
- catch { onStatus?.('error') /* presence track is best-effort */ }
47
- }
48
- return (payload) => {
49
- pending = payload
50
- const since = Date.now() - last
51
- if (since >= minMs) {
52
- if (timer) { clearTimeout(timer) }
53
- fire() // leading edge — presence appears immediately
54
- } else if (!timer) {
55
- timer = setTimeout(fire, minMs - since) // trailing edge — coalesce the burst
56
- }
57
- }
58
- }
59
-
60
- // A service process must not wedge its entire startup behind the first Realtime
61
- // SUBSCRIBED callback. Settle the startup gate on success, a terminal channel state,
62
- // or a bounded timeout while leaving the callback attached for later auto-rejoins.
63
- export function subscribeWithoutStartupDeadlock(channel, onStatus, {
64
- timeoutMs = 15_000,
65
- setTimer = setTimeout,
66
- clearTimer = clearTimeout,
67
- } = {}) {
68
- if (!channel?.subscribe || typeof onStatus !== 'function') return Promise.resolve({ connected: false, status: 'unavailable' })
69
- return new Promise((resolve) => {
70
- let settled = false
71
- const finish = (connected, status) => {
72
- if (settled) return
73
- settled = true
74
- clearTimer(timer)
75
- resolve({ connected, status })
76
- }
77
- const timer = setTimer(() => finish(false, 'STARTUP_TIMEOUT'), Math.max(1, Number(timeoutMs) || 1))
78
- channel.subscribe((status) => {
79
- onStatus(status)
80
- if (status === 'SUBSCRIBED') finish(true, status)
81
- else if (status === 'CLOSED' || status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') finish(false, status)
82
- })
83
- })
84
- }
85
-
86
- export function recoveryBackoffMs(attempt, { baseMs = 20_000, capMs = 300_000 } = {}) {
87
- const n = Math.max(0, Math.floor(Number(attempt) || 0))
88
- return Math.min(Math.max(1, Number(capMs) || 1), Math.max(1, Number(baseMs) || 1) * (2 ** Math.min(n, 10)))
89
- }
90
-
91
- // A RealtimeChannel keeps its last presenceState() locally. After CHANNEL_ERROR /
92
- // CLOSED that cache can still contain our own key even though the server (and every
93
- // dashboard subscriber) has already removed it. Treat every subscribe/error edge as
94
- // a new evidence generation: only a presence sync delivered in that generation may
95
- // certify the cached self key.
96
- export function reducePresenceChannelEvidence(state = {}, event, now = Date.now()) {
97
- const current = {
98
- subscribedAt: Number(state.subscribedAt) || 0,
99
- syncSeenAt: Number(state.syncSeenAt) || 0,
100
- }
101
- if (event === 'sync') return { ...current, syncSeenAt: now }
102
- if (event === 'SUBSCRIBED') {
103
- // A reconnect loop may report SUBSCRIBED repeatedly without ever delivering
104
- // presence sync. Preserve the first unsynced boundary so retries cannot keep
105
- // pushing the watchdog deadline out forever.
106
- if (current.subscribedAt && !current.syncSeenAt) return current
107
- return { subscribedAt: now, syncSeenAt: 0 }
108
- }
109
- if (event === 'CLOSED' || event === 'CHANNEL_ERROR' || event === 'TIMED_OUT') {
110
- // Likewise, repeated error callbacks are one failure episode until a fresh
111
- // sync proves recovery.
112
- if (current.subscribedAt && !current.syncSeenAt) return current
113
- return { subscribedAt: now, syncSeenAt: 0 }
114
- }
115
- return current
116
- }
117
-
118
- // Recycling a websocket is useful only when plain HTTP is healthy enough to prove
119
- // this is a Realtime-only wedge. During a full network/Supabase outage, respawning a
120
- // launchd service cannot help and merely creates a restart storm.
121
- export function presenceRecoveryEligible({ claimHeld, lastClaimOkAt, now, maxClaimSilenceMs = 40_000 }) {
122
- return !!claimHeld && Number.isFinite(lastClaimOkAt) && (now - lastClaimOkAt) < maxClaimSilenceMs
123
- }
124
-
125
- /* ─────────────────────────────────────────────────────────────
126
- presenceSelfEchoVerdict — the pure trigger for the account
127
- supervisor's presence self-echo watchdog (2026-07-08 recurrence).
128
-
129
- The failure it catches: the supervisor is alive, its claim is held
130
- (claim_bridge is a plain HTTP RPC, so it stays healthy), children
131
- serve rooms — yet an anonymous probe of tpacct shows ZERO presence
132
- keys. After a `realtime CLOSED` blip the channel goes zombie and
133
- channel.track() lands nowhere, forever. The claim watchdog
134
- (claimLoopWedged) can't see this — the claim rides HTTP, not the
135
- socket. The server's presence SYNC is the only proof: if OUR OWN
136
- key is absent from acct.presenceState() while we're tracking and
137
- hold the claim, the track never reached the server.
138
-
139
- Given how long our key has been missing (now - selfEchoMissingSince),
140
- escalate in two stages:
141
- missing < graceMs → 'ok' (normal; a fresh track takes a beat to echo)
142
- graceMs ≤ missing < graceMs+respawnMs → 'recycle' (force a full realtime disconnect+reconnect)
143
- missing ≥ graceMs + respawnMs → 'respawn' (recycle didn't take → exit; supervisor reruns clean)
144
-
145
- selfEchoMissingSince is 0/null whenever our key is present OR we're
146
- not tracking; the caller stamps it the first tick the key goes
147
- missing and clears it the first tick it returns. Extracted + exported
148
- so the watchdog's trigger is unit-testable without a live socket
149
- (mirrors claimLoopWedged). Deterministic in `now` — no Date.now here.
150
- ───────────────────────────────────────────────────────────── */
151
- export function presenceSelfEchoVerdict({ tracking, selfEchoMissingSince, now, graceMs = 60_000, respawnMs = 90_000 }) {
152
- if (!tracking || !selfEchoMissingSince) return 'ok'
153
- const missingMs = now - selfEchoMissingSince
154
- if (missingMs >= graceMs + respawnMs) return 'respawn'
155
- if (missingMs >= graceMs) return 'recycle'
156
- return 'ok'
157
- }
158
-
159
- // Turn the supervisor's raw account-channel observation into the state consumed by
160
- // presenceSelfEchoVerdict. A successful SUBSCRIBED callback is enough to start a
161
- // bounded startup grace period even when Supabase never sends the first presence
162
- // sync. Without that bound, `syncSeenAt === 0` disabled the watchdog forever: the
163
- // HTTP claim stayed healthy while account presence and every private pair bus were
164
- // dead, so a genuinely live partner room was reported as offline indefinitely.
165
- //
166
- // Once a sync has been seen, the original fail-safe still applies normally: a
167
- // present self key clears the episode; a missing key starts at the observation tick.
168
- export function presenceSelfEchoObservation({
169
- tracking,
170
- subscribedAt,
171
- syncSeenAt,
172
- present,
173
- missingSince,
174
- now,
175
- graceMs = 60_000,
176
- respawnMs = 90_000,
177
- }) {
178
- if (!tracking || !subscribedAt) return { missingSince: 0, verdict: 'ok' }
179
- if (syncSeenAt && present) return { missingSince: 0, verdict: 'ok' }
180
- const nextMissingSince = missingSince || (syncSeenAt ? now : subscribedAt)
181
- return {
182
- missingSince: nextMissingSince,
183
- verdict: presenceSelfEchoVerdict({
184
- tracking,
185
- selfEchoMissingSince: nextMissingSince,
186
- now,
187
- graceMs,
188
- respawnMs,
189
- }),
190
- }
191
- }
1
+ export function makeThrottledTrack(e,{minMs:n=5e3,onStatus:t=null}={}){let r=0,s=null,c=null;const i=async()=>{s=null,r=Date.now();const n=c;c=null;try{const r=await e.track(n);t?.(r??"ok")}catch{t?.("error")}};return e=>{c=e;const t=Date.now()-r;t>=n?(s&&clearTimeout(s),i()):s||(s=setTimeout(i,n-t))}}export function subscribeWithoutStartupDeadlock(e,n,{timeoutMs:t=15e3,setTimer:r=setTimeout,clearTimer:s=clearTimeout}={}){return e?.subscribe&&"function"==typeof n?new Promise(c=>{let i=!1;const o=(e,n)=>{i||(i=!0,s(a),c({connected:e,status:n}))},a=r(()=>o(!1,"STARTUP_TIMEOUT"),Math.max(1,Number(t)||1));e.subscribe(e=>{n(e),"SUBSCRIBED"===e?o(!0,e):"CLOSED"!==e&&"CHANNEL_ERROR"!==e&&"TIMED_OUT"!==e||o(!1,e)})}):Promise.resolve({connected:!1,status:"unavailable"})}export function recoveryBackoffMs(e,{baseMs:n=2e4,capMs:t=3e5}={}){const r=Math.max(0,Math.floor(Number(e)||0));return Math.min(Math.max(1,Number(t)||1),Math.max(1,Number(n)||1)*2**Math.min(r,10))}export function reducePresenceChannelEvidence(e={},n,t=Date.now()){const r={subscribedAt:Number(e.subscribedAt)||0,syncSeenAt:Number(e.syncSeenAt)||0};return"sync"===n?{...r,syncSeenAt:t}:"SUBSCRIBED"===n||"CLOSED"===n||"CHANNEL_ERROR"===n||"TIMED_OUT"===n?r.subscribedAt&&!r.syncSeenAt?r:{subscribedAt:t,syncSeenAt:0}:r}export function presenceRecoveryEligible({claimHeld:e,lastClaimOkAt:n,now:t,maxClaimSilenceMs:r=4e4}){return!!e&&Number.isFinite(n)&&t-n<r}export function presenceSelfEchoVerdict({tracking:e,selfEchoMissingSince:n,now:t,graceMs:r=6e4,respawnMs:s=9e4}){if(!e||!n)return"ok";const c=t-n;return c>=r+s?"respawn":c>=r?"recycle":"ok"}export function presenceSelfEchoObservation({tracking:e,subscribedAt:n,syncSeenAt:t,present:r,missingSince:s,now:c,graceMs:i=6e4,respawnMs:o=9e4}){if(!e||!n)return{missingSince:0,verdict:"ok"};if(t&&r)return{missingSince:0,verdict:"ok"};const a=s||(t?c:n);return{missingSince:a,verdict:presenceSelfEchoVerdict({tracking:e,selfEchoMissingSince:a,now:c,graceMs:i,respawnMs:o})}}
@@ -1,108 +1 @@
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
- ['Agent picker visibility', 'agent-visibility.json', exists(path.join(configDir, 'agent-visibility.json')), 'which installed runtimes are hidden from terminal pickers'],
46
- ['Room directories', 'dirs.json / served.json', exists(path.join(configDir, 'dirs.json')) || exists(path.join(configDir, 'served.json')), 'room-to-project directory mappings'],
47
- ['Bridge logs', 'update.log and service logs', exists(path.join(configDir, 'update.log')), 'update and background-service diagnostics'],
48
- ]
49
-
50
- return {
51
- generatedAt: new Date().toISOString(),
52
- configDir: displayPath(configDir, home),
53
- projectDirs: [...projectDirs].sort(),
54
- localRecords,
55
- outbound: [
56
- [webHost, 'Thinkpool configuration, linking, version checks, and product APIs'],
57
- [supabaseHost, 'authentication, room database, storage, and realtime transport'],
58
- ['registry.npmjs.org', 'package installation and updates initiated with npm or npx'],
59
- ...[...providerHosts].sort().map((host) => [host, 'AI provider selected on this bridge']),
60
- ].filter(([host]) => host),
61
- remote: [
62
- ['Thinkpool cloud receives', 'account/profile data; room membership and metadata; room chat; cleaned terminal output; terminal and room events; attachments; operational request metadata'],
63
- ['Thinkpool cloud does not receive as a repository', 'a crawled copy of the project directory or repository file tree'],
64
- ['Readable provider key', 'stored locally by the bridge; Thinkpool relays only a browser-sealed envelope'],
65
- ['AI prompts and code context', 'sent by the local coding agent to the selected AI provider; content depends on that runtime and the task'],
66
- ],
67
- limits: [
68
- 'Coding-agent runtimes and commands can contact domains beyond the bridge list above.',
69
- 'Thinkpool room transcripts are readable by Thinkpool infrastructure today; they are not end-to-end encrypted.',
70
- 'This report describes configured access. It is not a packet capture or a guarantee that a third-party agent obeys the same boundary.',
71
- ],
72
- }
73
- }
74
-
75
- export function formatPrivacyReport(report) {
76
- const yn = (value) => value ? 'present' : 'not present'
77
- const lines = [
78
- '',
79
- ' THINKPOOL PRIVACY REPORT',
80
- ` generated ${report.generatedAt}`,
81
- '',
82
- ' LOCAL ACCESS',
83
- ` Bridge configuration: ${report.configDir}`,
84
- ...report.projectDirs.map((dir) => ` Project directory: ${dir}`),
85
- '',
86
- ' LOCAL RECORDS',
87
- ...report.localRecords.map(([label, file, present, contains]) => ` ${label}: ${yn(present)} (${file})\n contains: ${contains}`),
88
- '',
89
- ' OUTBOUND DOMAINS',
90
- ...report.outbound.map(([host, reason]) => ` ${host}\n ${reason}`),
91
- '',
92
- ' REMOTE DATA',
93
- ...report.remote.map(([label, detail]) => ` ${label}:\n ${detail}`),
94
- '',
95
- ' IMPORTANT LIMITS',
96
- ...report.limits.map((line) => ` - ${line}`),
97
- '',
98
- ' Full data map: https://thinkpool.io/data',
99
- '',
100
- ]
101
- return lines.join('\n')
102
- }
103
-
104
- export function runPrivacyReport(options) {
105
- const report = buildPrivacyReport(options)
106
- process.stdout.write(formatPrivacyReport(report))
107
- return report
108
- }
1
+ import e from"node:fs";import o from"node:os";import t from"node:path";const r=e=>{try{return new URL(e).host}catch{return null}},n=(o,t)=>{try{return JSON.parse(e.readFileSync(o,"utf8"))}catch{return t}},i=(e,o)=>{const r=t.resolve(String(e||""));return r===o?"~":r.startsWith(`${o}${t.sep}`)?`~${r.slice(o.length)}`:r};export function buildPrivacyReport({home:a=o.homedir(),cwd:s=process.cwd(),env:d=process.env,exists:c=e.existsSync,read:p=n}={}){const l=t.join(a,".thinkpool-pair"),u=p(t.join(l,"dirs.json"),{})||{},m=p(t.join(l,"served.json"),{})||{},h=p(t.join(l,"providers.json"),[])||[],y=p(t.join(l,"provider.json"),null);let g="";try{g=e.readFileSync(t.join(l,"default-dir"),"utf8").trim()}catch{}const j=new Set([s,g,...Object.values(u),...Object.values(m)].filter(Boolean).map(e=>i(e,a))),v=new Set([...h.map(e=>r(e?.baseUrl)),r(y?.baseUrl)].filter(Boolean)),b=r(d.TP_SUPABASE_URL||"https://daytvtakmlixpfbbqzjd.supabase.co"),f=r(d.TP_WEB_BASE||"https://thinkpool.io"),k=[["Saved login","auth.json",c(t.join(l,"auth.json")),"refresh token and account identity"],["Provider registry","providers.json",c(t.join(l,"providers.json")),"provider endpoint, model, and API key"],["Bridge keypair","bridge-key.json",c(t.join(l,"bridge-key.json")),"private key used to open browser-sealed provider keys"],["Agent picker visibility","agent-visibility.json",c(t.join(l,"agent-visibility.json")),"which installed runtimes are hidden from terminal pickers"],["Room directories","dirs.json / served.json",c(t.join(l,"dirs.json"))||c(t.join(l,"served.json")),"room-to-project directory mappings"],["Bridge logs","update.log and service logs",c(t.join(l,"update.log")),"update and background-service diagnostics"]];return{generatedAt:(new Date).toISOString(),configDir:i(l,a),projectDirs:[...j].sort(),localRecords:k,outbound:[[f,"Thinkpool configuration, linking, version checks, and product APIs"],[b,"authentication, room database, storage, and realtime transport"],["registry.npmjs.org","package installation and updates initiated with npm or npx"],...[...v].sort().map(e=>[e,"AI provider selected on this bridge"])].filter(([e])=>e),remote:[["Thinkpool cloud receives","account/profile data; room membership and metadata; room chat; cleaned terminal output; terminal and room events; attachments; operational request metadata"],["Thinkpool cloud does not receive as a repository","a crawled copy of the project directory or repository file tree"],["Readable provider key","stored locally by the bridge; Thinkpool relays only a browser-sealed envelope"],["AI prompts and code context","sent by the local coding agent to the selected AI provider; content depends on that runtime and the task"]],limits:["Coding-agent runtimes and commands can contact domains beyond the bridge list above.","Thinkpool room transcripts are readable by Thinkpool infrastructure today; they are not end-to-end encrypted.","This report describes configured access. It is not a packet capture or a guarantee that a third-party agent obeys the same boundary."]}}export function formatPrivacyReport(e){return[""," THINKPOOL PRIVACY REPORT",` generated ${e.generatedAt}`,""," LOCAL ACCESS",` Bridge configuration: ${e.configDir}`,...e.projectDirs.map(e=>` Project directory: ${e}`),""," LOCAL RECORDS",...e.localRecords.map(([e,o,t,r])=>{return` ${e}: ${n=t,n?"present":"not present"} (${o})\n contains: ${r}`;var n}),""," OUTBOUND DOMAINS",...e.outbound.map(([e,o])=>` ${e}\n ${o}`),""," REMOTE DATA",...e.remote.map(([e,o])=>` ${e}:\n ${o}`),""," IMPORTANT LIMITS",...e.limits.map(e=>` - ${e}`),""," Full data map: https://thinkpool.io/data",""].join("\n")}export function runPrivacyReport(e){const o=buildPrivacyReport(e);return process.stdout.write(formatPrivacyReport(o)),o}