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.
- package/LICENSE +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6266
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -60
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -92
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -79
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +13 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- 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})}}
|
package/privacy-report.mjs
CHANGED
|
@@ -1,108 +1 @@
|
|
|
1
|
-
import fs from
|
|
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}
|