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/acp-client.mjs
CHANGED
|
@@ -1,140 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// cancellation, typed client handlers, and schemas come from the official ACP
|
|
3
|
-
// SDK. ThinkPool owns only subprocess supervision and its stricter stdout rule.
|
|
4
|
-
import { spawn } from 'node:child_process'
|
|
5
|
-
import { Readable, Transform, Writable } from 'node:stream'
|
|
6
|
-
import * as acp from '@agentclientprotocol/sdk'
|
|
7
|
-
|
|
8
|
-
const closedError = (value, fallback = 'ACP process closed') => value instanceof Error
|
|
9
|
-
? value
|
|
10
|
-
: new Error(String(value || fallback))
|
|
11
|
-
|
|
12
|
-
function strictProtocolGate(onError) {
|
|
13
|
-
let buffered = ''
|
|
14
|
-
const validate = (line) => {
|
|
15
|
-
if (!line.trim()) return
|
|
16
|
-
let message
|
|
17
|
-
try { message = JSON.parse(line) } catch { throw new Error('ACP stdout contained non-JSON protocol data') }
|
|
18
|
-
if (!message || typeof message !== 'object' || Array.isArray(message)) {
|
|
19
|
-
throw new Error('ACP stdout contained invalid JSON-RPC protocol data')
|
|
20
|
-
}
|
|
21
|
-
const hasId = Object.prototype.hasOwnProperty.call(message, 'id')
|
|
22
|
-
const hasMethod = typeof message.method === 'string' && message.method.length > 0
|
|
23
|
-
const hasResult = Object.prototype.hasOwnProperty.call(message, 'result')
|
|
24
|
-
const hasError = Object.prototype.hasOwnProperty.call(message, 'error')
|
|
25
|
-
const isCall = hasMethod && !(hasResult || hasError)
|
|
26
|
-
const isResponse = hasId && !hasMethod && hasResult !== hasError
|
|
27
|
-
if (message.jsonrpc !== '2.0' || (!isCall && !isResponse)) {
|
|
28
|
-
throw new Error('ACP stdout contained invalid JSON-RPC protocol data')
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const gate = new Transform({
|
|
32
|
-
transform(chunk, _encoding, callback) {
|
|
33
|
-
try {
|
|
34
|
-
buffered += String(chunk)
|
|
35
|
-
const lines = buffered.split('\n')
|
|
36
|
-
buffered = lines.pop() || ''
|
|
37
|
-
for (const line of lines) validate(line)
|
|
38
|
-
callback(null, chunk)
|
|
39
|
-
} catch (error) { callback(error) }
|
|
40
|
-
},
|
|
41
|
-
flush(callback) {
|
|
42
|
-
try { validate(buffered); callback(null, buffered || undefined) }
|
|
43
|
-
catch (error) { callback(error) }
|
|
44
|
-
},
|
|
45
|
-
})
|
|
46
|
-
gate.on('error', onError)
|
|
47
|
-
return gate
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class AcpClient {
|
|
51
|
-
constructor({ command, args = ['acp'], cwd, env = process.env, spawnImpl = spawn, onNotification, onRequest, onStderr, onClose, timeoutMs = 10_000 } = {}) {
|
|
52
|
-
this.command = command
|
|
53
|
-
this.args = args
|
|
54
|
-
this.cwd = cwd
|
|
55
|
-
this.env = env
|
|
56
|
-
this.spawnImpl = spawnImpl
|
|
57
|
-
this.onNotification = onNotification
|
|
58
|
-
this.onRequest = onRequest
|
|
59
|
-
this.onStderr = onStderr
|
|
60
|
-
this.onClose = onClose
|
|
61
|
-
this.timeoutMs = timeoutMs
|
|
62
|
-
this.child = null
|
|
63
|
-
this.connection = null
|
|
64
|
-
this.closedError = null
|
|
65
|
-
this.stderrTail = ''
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
get alive() { return !!this.child && !!this.connection && !this.closedError }
|
|
69
|
-
|
|
70
|
-
start() {
|
|
71
|
-
if (this.child) return
|
|
72
|
-
const child = this.spawnImpl(this.command, this.args, { cwd: this.cwd, env: this.env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
73
|
-
this.child = child
|
|
74
|
-
child.stderr?.on?.('data', (chunk) => {
|
|
75
|
-
const text = String(chunk)
|
|
76
|
-
this.stderrTail = (this.stderrTail + text).slice(-2000)
|
|
77
|
-
try { this.onStderr?.(text) } catch { /* consumer isolation */ }
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const failProtocol = (error) => {
|
|
81
|
-
const failure = closedError(error, 'ACP protocol stream failed')
|
|
82
|
-
this.connection?.close(failure)
|
|
83
|
-
this._close(failure)
|
|
84
|
-
try { child.kill('SIGTERM') } catch { /* noop */ }
|
|
85
|
-
}
|
|
86
|
-
const protocolOutput = strictProtocolGate(failProtocol)
|
|
87
|
-
child.stdout.pipe(protocolOutput)
|
|
88
|
-
const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(protocolOutput))
|
|
89
|
-
const app = acp.client({ name: 'thinkpool-pair' })
|
|
90
|
-
.onRequest(acp.methods.client.session.requestPermission, ({ params, requestId }) => {
|
|
91
|
-
if (!this.onRequest) throw Object.assign(new Error('Unsupported ACP client method: session/request_permission'), { code: -32601 })
|
|
92
|
-
return this.onRequest(acp.methods.client.session.requestPermission, params, requestId)
|
|
93
|
-
})
|
|
94
|
-
.onNotification(acp.methods.client.session.update, ({ params }) => {
|
|
95
|
-
try { this.onNotification?.(acp.methods.client.session.update, params) } catch { /* consumer isolation */ }
|
|
96
|
-
})
|
|
97
|
-
this.connection = app.connect(stream)
|
|
98
|
-
void this.connection.closed.then(() => {
|
|
99
|
-
if (!this.closedError) this._close(closedError(this.connection?.signal?.reason))
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
child.stdin?.on?.('error', (error) => this._close(error))
|
|
103
|
-
child.on('error', (error) => this._close(error))
|
|
104
|
-
child.on('close', (code, signal) => this._close(new Error(`ACP process exited ${code ?? signal ?? 'unknown'}${this.stderrTail ? `: ${this.stderrTail.trim().slice(-300)}` : ''}`)))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
request(method, params = {}, timeoutMs = this.timeoutMs) {
|
|
108
|
-
if (!this.connection || this.closedError) return Promise.reject(this.closedError || new Error('ACP process is not running'))
|
|
109
|
-
const controller = new AbortController()
|
|
110
|
-
const request = this.connection.agent.request(method, params, { cancellationSignal: controller.signal })
|
|
111
|
-
if (!(timeoutMs > 0)) return request
|
|
112
|
-
return new Promise((resolve, reject) => {
|
|
113
|
-
const timer = setTimeout(() => {
|
|
114
|
-
controller.abort(new Error(`${method} timed out after ${timeoutMs}ms`))
|
|
115
|
-
reject(new Error(`${method} timed out after ${timeoutMs}ms`))
|
|
116
|
-
}, timeoutMs)
|
|
117
|
-
timer.unref?.()
|
|
118
|
-
request.then(resolve, reject).finally(() => clearTimeout(timer))
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
notify(method, params) {
|
|
123
|
-
if (!this.connection || this.closedError) return Promise.reject(this.closedError || new Error('ACP process is not running'))
|
|
124
|
-
return this.connection.agent.notify(method, params)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
_close(error) {
|
|
128
|
-
if (this.closedError) return
|
|
129
|
-
this.closedError = closedError(error)
|
|
130
|
-
this.child = null
|
|
131
|
-
this.connection?.close(this.closedError)
|
|
132
|
-
try { this.onClose?.(this.closedError) } catch { /* consumer isolation */ }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
end() {
|
|
136
|
-
const child = this.child
|
|
137
|
-
this._close(new Error('ACP client ended'))
|
|
138
|
-
if (child) { try { child.kill('SIGTERM') } catch { /* noop */ } }
|
|
139
|
-
}
|
|
140
|
-
}
|
|
1
|
+
import{spawn as t}from"node:child_process";import{Readable as o,Transform as e,Writable as r}from"node:stream";import*as s from"@agentclientprotocol/sdk";const n=(t,o="ACP process closed")=>t instanceof Error?t:new Error(String(t||o));export class AcpClient{constructor({command:o,args:e=["acp"],cwd:r,env:s=process.env,spawnImpl:n=t,onNotification:i,onRequest:c,onStderr:l,onClose:h,timeoutMs:a=1e4}={}){this.command=o,this.args=e,this.cwd=r,this.env=s,this.spawnImpl=n,this.onNotification=i,this.onRequest=c,this.onStderr=l,this.onClose=h,this.timeoutMs=a,this.child=null,this.connection=null,this.closedError=null,this.stderrTail=""}get alive(){return!!this.child&&!!this.connection&&!this.closedError}start(){if(this.child)return;const t=this.spawnImpl(this.command,this.args,{cwd:this.cwd,env:this.env,stdio:["pipe","pipe","pipe"]});this.child=t,t.stderr?.on?.("data",t=>{const o=String(t);this.stderrTail=(this.stderrTail+o).slice(-2e3);try{this.onStderr?.(o)}catch{}});const i=function(t){let o="";const r=t=>{if(!t.trim())return;let o;try{o=JSON.parse(t)}catch{throw new Error("ACP stdout contained non-JSON protocol data")}if(!o||"object"!=typeof o||Array.isArray(o))throw new Error("ACP stdout contained invalid JSON-RPC protocol data");const e=Object.prototype.hasOwnProperty.call(o,"id"),r="string"==typeof o.method&&o.method.length>0,s=Object.prototype.hasOwnProperty.call(o,"result"),n=Object.prototype.hasOwnProperty.call(o,"error"),i=r&&!(s||n),c=e&&!r&&s!==n;if("2.0"!==o.jsonrpc||!i&&!c)throw new Error("ACP stdout contained invalid JSON-RPC protocol data")},s=new e({transform(t,e,s){try{o+=String(t);const e=o.split("\n");o=e.pop()||"";for(const t of e)r(t);s(null,t)}catch(t){s(t)}},flush(t){try{r(o),t(null,o||void 0)}catch(o){t(o)}}});return s.on("error",t),s}(o=>{const e=n(o,"ACP protocol stream failed");this.connection?.close(e),this._close(e);try{t.kill("SIGTERM")}catch{}});t.stdout.pipe(i);const c=s.ndJsonStream(r.toWeb(t.stdin),o.toWeb(i)),l=s.client({name:"thinkpool-pair"}).onRequest(s.methods.client.session.requestPermission,({params:t,requestId:o})=>{if(!this.onRequest)throw Object.assign(new Error("Unsupported ACP client method: session/request_permission"),{code:-32601});return this.onRequest(s.methods.client.session.requestPermission,t,o)}).onNotification(s.methods.client.session.update,({params:t})=>{try{this.onNotification?.(s.methods.client.session.update,t)}catch{}});this.connection=l.connect(c),this.connection.closed.then(()=>{this.closedError||this._close(n(this.connection?.signal?.reason))}),t.stdin?.on?.("error",t=>this._close(t)),t.on("error",t=>this._close(t)),t.on("close",(t,o)=>this._close(new Error(`ACP process exited ${t??o??"unknown"}${this.stderrTail?`: ${this.stderrTail.trim().slice(-300)}`:""}`)))}request(t,o={},e=this.timeoutMs){if(!this.connection||this.closedError)return Promise.reject(this.closedError||new Error("ACP process is not running"));const r=new AbortController,s=this.connection.agent.request(t,o,{cancellationSignal:r.signal});return e>0?new Promise((o,n)=>{const i=setTimeout(()=>{r.abort(new Error(`${t} timed out after ${e}ms`)),n(new Error(`${t} timed out after ${e}ms`))},e);i.unref?.(),s.then(o,n).finally(()=>clearTimeout(i))}):s}notify(t,o){return!this.connection||this.closedError?Promise.reject(this.closedError||new Error("ACP process is not running")):this.connection.agent.notify(t,o)}_close(t){if(!this.closedError){this.closedError=n(t),this.child=null,this.connection?.close(this.closedError);try{this.onClose?.(this.closedError)}catch{}}}end(){const t=this.child;if(this._close(new Error("ACP client ended")),t)try{t.kill("SIGTERM")}catch{}}}
|
package/agent-detect.mjs
CHANGED
|
@@ -1,28 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// Both surfaces announce the same machine; they must not disagree about which
|
|
3
|
-
// coding CLIs it can open merely because they publish on different channels.
|
|
4
|
-
import fs from 'node:fs'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
|
|
7
|
-
export const KNOWN_AGENT_COMMANDS = [
|
|
8
|
-
'claude', 'codex', 'thinkpool', 'gemini', 'aider', 'cursor-agent',
|
|
9
|
-
'opencode', 'copilot', 'goose', 'crush', 'qwen',
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
export function commandOnPath(command, { env = process.env, platform = process.platform, fsImpl = fs } = {}) {
|
|
13
|
-
const exts = platform === 'win32'
|
|
14
|
-
? (env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
15
|
-
: ['']
|
|
16
|
-
const delimiter = platform === 'win32' ? ';' : path.delimiter
|
|
17
|
-
for (const dir of (env.PATH || '').split(delimiter)) {
|
|
18
|
-
if (!dir) continue
|
|
19
|
-
for (const ext of exts) {
|
|
20
|
-
try { if (fsImpl.existsSync(path.join(dir, command + ext))) return true } catch { /* unreadable path entry */ }
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function installedAgentCommands(options) {
|
|
27
|
-
return KNOWN_AGENT_COMMANDS.filter((command) => commandOnPath(command, options))
|
|
28
|
-
}
|
|
1
|
+
import o from"node:fs";import n from"node:path";export const KNOWN_AGENT_COMMANDS=["claude","codex","thinkpool","gemini","aider","cursor-agent","opencode","copilot","goose","crush","qwen"];export function commandOnPath(t,{env:e=process.env,platform:r=process.platform,fsImpl:i=o}={}){const c="win32"===r?(e.PATHEXT||".EXE;.CMD;.BAT;.COM").split(";"):[""],s="win32"===r?";":n.delimiter;for(const o of(e.PATH||"").split(s))if(o)for(const e of c)try{if(i.existsSync(n.join(o,t+e)))return!0}catch{}return!1}export function installedAgentCommands(o){return KNOWN_AGENT_COMMANDS.filter(n=>commandOnPath(n,o))}
|
package/agent-notify.mjs
CHANGED
|
@@ -1,142 +1 @@
|
|
|
1
|
-
|
|
2
|
-
bridge/agent-notify.mjs — the PURE decision layer for agent push events.
|
|
3
|
-
|
|
4
|
-
The bridge is the only process that knows a turn ended or a permission
|
|
5
|
-
card went unanswered. It can't call /api/notify-code directly (that
|
|
6
|
-
endpoint is webhook-secret-gated and this is a published npm package), so
|
|
7
|
-
it writes a compact `__agent` row into code_messages and lets the existing
|
|
8
|
-
push_notify_on_code_message trigger fan it out.
|
|
9
|
-
|
|
10
|
-
Everything that DECIDES whether to write such a row lives here, with no
|
|
11
|
-
I/O, so bridge/agent-notify.test.mjs can exercise it without a room, a
|
|
12
|
-
socket, or a wall clock.
|
|
13
|
-
|
|
14
|
-
Spec: docs/specs/2026-07-09-code-notifications-rework.md (Slice 3)
|
|
15
|
-
───────────────────────────────────────────────────────────── */
|
|
16
|
-
|
|
17
|
-
// A turn shorter than this means somebody was almost certainly watching it —
|
|
18
|
-
// pushing "done" 4 seconds after they hit enter is noise, not news.
|
|
19
|
-
export const DEFAULT_MIN_TURN_MS = 30_000
|
|
20
|
-
|
|
21
|
-
// How long a permission card may sit unanswered before we assume nobody is
|
|
22
|
-
// looking at the room and push. Short enough to be useful, long enough that
|
|
23
|
-
// the common case (you're right there, you click it) never fires.
|
|
24
|
-
export const DEFAULT_PERM_GRACE_MS = 20_000
|
|
25
|
-
|
|
26
|
-
// Push copy is a banner, not a transcript.
|
|
27
|
-
export const SUMMARY_MAX = 200
|
|
28
|
-
|
|
29
|
-
/* A lane is USER-FACING when a human opened it and is waiting on it.
|
|
30
|
-
- spawnedBy: a Dispatch worker lane an agent fanned out. Its owner is
|
|
31
|
-
another agent; the human never asked for it directly.
|
|
32
|
-
- flowTaskKey: a Flow slice lane. Same reasoning — the conductor collects
|
|
33
|
-
it, and a 12-lane flow would fan 12 banners at one person.
|
|
34
|
-
Both run in inherited/bypass modes almost always, so they also rarely
|
|
35
|
-
raise permission cards. The conductor lane itself (flowSessionId but no
|
|
36
|
-
flowTaskKey) IS user-facing — that's the one a human is watching. */
|
|
37
|
-
export function isUserFacingLane(entry) {
|
|
38
|
-
if (!entry) return false
|
|
39
|
-
return !entry.spawnedBy && !entry.flowTaskKey
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/* Collapse whitespace, clip, ellipsize. Never throws on non-strings. */
|
|
43
|
-
export function clipSummary(text, max = SUMMARY_MAX) {
|
|
44
|
-
if (text == null) return ''
|
|
45
|
-
const s = String(text).replace(/\s+/g, ' ').trim()
|
|
46
|
-
if (s.length <= max) return s
|
|
47
|
-
return s.slice(0, max).replace(/\s+\S*$/, '').trimEnd() + '…'
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* Should a settled turn produce a `turn-done` event row?
|
|
51
|
-
|
|
52
|
-
No when: the lane isn't user-facing · the user pressed Stop (subtype
|
|
53
|
-
'aborted' — they know it's over, they're the one who ended it) · the turn
|
|
54
|
-
was short enough that somebody was plainly watching · we never saw the
|
|
55
|
-
turn start (startedAt null → a stray `error` event outside any turn, e.g.
|
|
56
|
-
the SDK-regression warning emitted at init).
|
|
57
|
-
|
|
58
|
-
`error` turns DO notify — with the same neutral copy as a success. Max's
|
|
59
|
-
call: "the agent stopped" is the news; the reason is in the room. */
|
|
60
|
-
export function shouldNotifyTurnDone({ entry, subtype, startedAt, now, minTurnMs = DEFAULT_MIN_TURN_MS }) {
|
|
61
|
-
if (!shouldRecordTurnDone({ entry, subtype, startedAt, now })) return false
|
|
62
|
-
return (now - startedAt) >= minTurnMs
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Every user-facing settle is durable realtime evidence for the in-app rail.
|
|
66
|
-
// The older 30s threshold remains only a PUSH eligibility rule; using it as the
|
|
67
|
-
// write gate made short turns in another session invisible to the web app.
|
|
68
|
-
export function shouldRecordTurnDone({ entry, subtype, startedAt, now }) {
|
|
69
|
-
if (!isUserFacingLane(entry)) return false
|
|
70
|
-
if (subtype === 'aborted') return false
|
|
71
|
-
if (!startedAt) return false
|
|
72
|
-
return Number.isFinite(now)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/* The one-line "what is it asking me" for a permission card.
|
|
76
|
-
AskUserQuestion cards carry `questions`; ordinary tool cards degrade to
|
|
77
|
-
`<ToolName> <first meaningful arg>`, matching the local stderr line. */
|
|
78
|
-
export function permissionSummary(payload) {
|
|
79
|
-
const p = payload || {}
|
|
80
|
-
const qs = Array.isArray(p.questions) ? p.questions.map(q => q?.question).filter(Boolean) : []
|
|
81
|
-
if (qs.length) return clipSummary(qs.join(' / '))
|
|
82
|
-
if (p.risk === 'plan') return clipSummary(p.plan ? `Plan ready: ${p.plan}` : 'A plan is ready for review')
|
|
83
|
-
const i = p.input
|
|
84
|
-
const arg = i && (i.command || i.file_path || i.path || i.pattern || i.url || i.query || i.description)
|
|
85
|
-
const name = p.toolName || 'A tool'
|
|
86
|
-
return clipSummary(arg ? `${name} ${arg}` : name)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* Per-session permission-card notifier.
|
|
90
|
-
|
|
91
|
-
arm(id, payload) — a card was raised. Start the grace timer.
|
|
92
|
-
resolve(id) — the card was answered (human click, mode-flip
|
|
93
|
-
auto-approval, or a drain-to-deny). If its timer had
|
|
94
|
-
already FIRED we pushed a banner, so retract it.
|
|
95
|
-
clearAll() — turn ended / session died. Every outstanding card is
|
|
96
|
-
dead; retract anything we announced.
|
|
97
|
-
|
|
98
|
-
Timers are injected so the unit test drives them synchronously. onFire /
|
|
99
|
-
onRetract are fire-and-forget (they end up as REST inserts); a throw in
|
|
100
|
-
either must never take the bridge down. */
|
|
101
|
-
export function createPermNotifier({
|
|
102
|
-
graceMs = DEFAULT_PERM_GRACE_MS,
|
|
103
|
-
onArm = () => {},
|
|
104
|
-
onFire = () => {},
|
|
105
|
-
onRetract = () => {},
|
|
106
|
-
onResolve = () => {},
|
|
107
|
-
setTimer = setTimeout,
|
|
108
|
-
clearTimer = clearTimeout,
|
|
109
|
-
} = {}) {
|
|
110
|
-
const cards = new Map() // id → { timer, fired, payload }
|
|
111
|
-
const safe = (fn, id, payload) => { try { fn(id, payload) } catch { /* never break the turn */ } }
|
|
112
|
-
|
|
113
|
-
const settle = (id) => {
|
|
114
|
-
const rec = cards.get(id)
|
|
115
|
-
if (!rec) return
|
|
116
|
-
if (rec.timer) { try { clearTimer(rec.timer) } catch { /* noop */ } }
|
|
117
|
-
cards.delete(id)
|
|
118
|
-
if (rec.fired) safe(onRetract, id, rec.payload)
|
|
119
|
-
safe(onResolve, id, rec.payload)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
arm(id, payload) {
|
|
124
|
-
if (!id || cards.has(id)) return
|
|
125
|
-
const rec = { timer: null, fired: false, payload }
|
|
126
|
-
safe(onArm, id, payload)
|
|
127
|
-
rec.timer = setTimer(() => {
|
|
128
|
-
rec.timer = null
|
|
129
|
-
rec.fired = true
|
|
130
|
-
safe(onFire, id, payload)
|
|
131
|
-
}, graceMs)
|
|
132
|
-
// Never hold the process open on a permission timer.
|
|
133
|
-
try { rec.timer?.unref?.() } catch { /* not a node timer (test double) */ }
|
|
134
|
-
cards.set(id, rec)
|
|
135
|
-
},
|
|
136
|
-
resolve: settle,
|
|
137
|
-
clearAll() { for (const id of [...cards.keys()]) settle(id) },
|
|
138
|
-
// test/debug introspection only
|
|
139
|
-
size: () => cards.size,
|
|
140
|
-
fired: (id) => !!cards.get(id)?.fired,
|
|
141
|
-
}
|
|
142
|
-
}
|
|
1
|
+
export const DEFAULT_MIN_TURN_MS=3e4;export const DEFAULT_PERM_GRACE_MS=2e4;export const SUMMARY_MAX=200;export function isUserFacingLane(e){return!!e&&!e.spawnedBy&&!e.flowTaskKey}export function clipSummary(e,r=200){if(null==e)return"";const t=String(e).replace(/\s+/g," ").trim();return t.length<=r?t:t.slice(0,r).replace(/\s+\S*$/,"").trimEnd()+"…"}export function shouldNotifyTurnDone({entry:e,subtype:r,startedAt:t,now:n,minTurnMs:o=3e4}){return!!shouldRecordTurnDone({entry:e,subtype:r,startedAt:t,now:n})&&n-t>=o}export function shouldRecordTurnDone({entry:e,subtype:r,startedAt:t,now:n}){return!!isUserFacingLane(e)&&"aborted"!==r&&!!t&&Number.isFinite(n)}export function permissionSummary(e){const r=e||{},t=Array.isArray(r.questions)?r.questions.map(e=>e?.question).filter(Boolean):[];if(t.length)return clipSummary(t.join(" / "));if("plan"===r.risk)return clipSummary(r.plan?`Plan ready: ${r.plan}`:"A plan is ready for review");const n=r.input,o=n&&(n.command||n.file_path||n.path||n.pattern||n.url||n.query||n.description),i=r.toolName||"A tool";return clipSummary(o?`${i} ${o}`:i)}export function createPermNotifier({graceMs:e=2e4,onArm:r=()=>{},onFire:t=()=>{},onRetract:n=()=>{},onResolve:o=()=>{},setTimer:i=setTimeout,clearTimer:a=clearTimeout}={}){const s=new Map,u=(e,r,t)=>{try{e(r,t)}catch{}},c=e=>{const r=s.get(e);if(r){if(r.timer)try{a(r.timer)}catch{}s.delete(e),r.fired&&u(n,e,r.payload),u(o,e,r.payload)}};return{arm(n,o){if(!n||s.has(n))return;const a={timer:null,fired:!1,payload:o};u(r,n,o),a.timer=i(()=>{a.timer=null,a.fired=!0,u(t,n,o)},e);try{a.timer?.unref?.()}catch{}s.set(n,a)},resolve:c,clearAll(){for(const e of[...s.keys()])c(e)},size:()=>s.size,fired:e=>!!s.get(e)?.fired}}
|
package/agent-visibility.mjs
CHANGED
|
@@ -1,67 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Installation remains detection truth: hiding a runtime does not uninstall it
|
|
4
|
-
// or prevent an explicit bridge command from opening it. This preference only
|
|
5
|
-
// controls which installed runtimes the web offers in human-facing terminal
|
|
6
|
-
// pickers. The file is shared by the account supervisor and its room children,
|
|
7
|
-
// so one dashboard toggle applies to every room served by this bridge.
|
|
8
|
-
import fs from 'node:fs'
|
|
9
|
-
import os from 'node:os'
|
|
10
|
-
import path from 'node:path'
|
|
11
|
-
|
|
12
|
-
export const PICKER_AGENT_COMMANDS = ['claude', 'codex', 'thinkpool']
|
|
13
|
-
|
|
14
|
-
const root = () => process.env.TP_PAIR_ROOT || path.join(os.homedir(), '.thinkpool-pair')
|
|
15
|
-
const defaultFile = () => path.join(root(), 'agent-visibility.json')
|
|
16
|
-
const normalize = (command) => String(command || '').toLowerCase() === 'hermes'
|
|
17
|
-
? 'thinkpool'
|
|
18
|
-
: String(command || '').toLowerCase()
|
|
19
|
-
|
|
20
|
-
export function loadHiddenAgentCommands({ file = defaultFile(), fsImpl = fs } = {}) {
|
|
21
|
-
try {
|
|
22
|
-
const saved = JSON.parse(fsImpl.readFileSync(file, 'utf8'))
|
|
23
|
-
return new Set((Array.isArray(saved?.hidden) ? saved.hidden : [])
|
|
24
|
-
.map(normalize)
|
|
25
|
-
.filter((command) => PICKER_AGENT_COMMANDS.includes(command)))
|
|
26
|
-
} catch {
|
|
27
|
-
return new Set()
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function agentVisibilityStates(commands, { hidden = loadHiddenAgentCommands() } = {}) {
|
|
32
|
-
const found = new Set((Array.isArray(commands) ? commands : []).map(normalize))
|
|
33
|
-
const states = PICKER_AGENT_COMMANDS
|
|
34
|
-
.filter((command) => found.has(command))
|
|
35
|
-
.map((cmd) => ({ cmd, enabled: !hidden.has(cmd) }))
|
|
36
|
-
// A corrupt or hand-edited preference must not leave the "+" action with no
|
|
37
|
-
// valid runtime. The first installed product runtime is the safe recovery.
|
|
38
|
-
if (states.length > 0 && !states.some((agent) => agent.enabled)) states[0].enabled = true
|
|
39
|
-
return states
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function setAgentPickerVisibility(command, enabled, {
|
|
43
|
-
installed = [],
|
|
44
|
-
file = defaultFile(),
|
|
45
|
-
fsImpl = fs,
|
|
46
|
-
} = {}) {
|
|
47
|
-
const cmd = normalize(command)
|
|
48
|
-
if (!PICKER_AGENT_COMMANDS.includes(cmd)) return { ok: false, error: 'unknown agent runtime' }
|
|
49
|
-
const states = agentVisibilityStates(installed, { hidden: loadHiddenAgentCommands({ file, fsImpl }) })
|
|
50
|
-
if (!states.some((agent) => agent.cmd === cmd)) return { ok: false, error: 'agent runtime is not installed' }
|
|
51
|
-
if (!enabled && states.filter((agent) => agent.enabled).length <= 1) {
|
|
52
|
-
return { ok: false, error: 'keep at least one agent in the terminal picker' }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const hidden = new Set(states.filter((agent) => !agent.enabled).map((agent) => agent.cmd))
|
|
56
|
-
if (enabled) hidden.delete(cmd)
|
|
57
|
-
else hidden.add(cmd)
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
fsImpl.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
61
|
-
fsImpl.writeFileSync(file, JSON.stringify({ hidden: [...hidden] }, null, 2), { mode: 0o600 })
|
|
62
|
-
try { fsImpl.chmodSync(file, 0o600) } catch { /* best-effort hardening */ }
|
|
63
|
-
} catch {
|
|
64
|
-
return { ok: false, error: 'could not save agent visibility' }
|
|
65
|
-
}
|
|
66
|
-
return { ok: true, agents: agentVisibilityStates(installed, { hidden }) }
|
|
67
|
-
}
|
|
1
|
+
import e from"node:fs";import n from"node:os";import t from"node:path";export const PICKER_AGENT_COMMANDS=["claude","codex","thinkpool"];const i=()=>t.join(process.env.TP_PAIR_ROOT||t.join(n.homedir(),".thinkpool-pair"),"agent-visibility.json"),r=e=>"hermes"===String(e||"").toLowerCase()?"thinkpool":String(e||"").toLowerCase();export function loadHiddenAgentCommands({file:n=i(),fsImpl:t=e}={}){try{const e=JSON.parse(t.readFileSync(n,"utf8"));return new Set((Array.isArray(e?.hidden)?e.hidden:[]).map(r).filter(e=>PICKER_AGENT_COMMANDS.includes(e)))}catch{return new Set}}export function agentVisibilityStates(e,{hidden:n=loadHiddenAgentCommands()}={}){const t=new Set((Array.isArray(e)?e:[]).map(r)),i=PICKER_AGENT_COMMANDS.filter(e=>t.has(e)).map(e=>({cmd:e,enabled:!n.has(e)}));return i.length>0&&!i.some(e=>e.enabled)&&(i[0].enabled=!0),i}export function setAgentPickerVisibility(n,o,{installed:d=[],file:a=i(),fsImpl:s=e}={}){const l=r(n);if(!PICKER_AGENT_COMMANDS.includes(l))return{ok:!1,error:"unknown agent runtime"};const m=agentVisibilityStates(d,{hidden:loadHiddenAgentCommands({file:a,fsImpl:s})});if(!m.some(e=>e.cmd===l))return{ok:!1,error:"agent runtime is not installed"};if(!o&&m.filter(e=>e.enabled).length<=1)return{ok:!1,error:"keep at least one agent in the terminal picker"};const c=new Set(m.filter(e=>!e.enabled).map(e=>e.cmd));o?c.delete(l):c.add(l);try{s.mkdirSync(t.dirname(a),{recursive:!0,mode:448}),s.writeFileSync(a,JSON.stringify({hidden:[...c]},null,2),{mode:384});try{s.chmodSync(a,384)}catch{}}catch{return{ok:!1,error:"could not save agent visibility"}}return{ok:!0,agents:agentVisibilityStates(d,{hidden:c})}}
|
package/auth-store.mjs
CHANGED
|
@@ -1,120 +1 @@
|
|
|
1
|
-
|
|
2
|
-
ThinkPool bridge. Files live under ~/.thinkpool-pair/ (chmod 600 for the token).
|
|
3
|
-
- auth.json: the linked account's Supabase refresh token (rotated on each use).
|
|
4
|
-
- dirs.json: per-MACHINE map of room code → absolute working directory, so one
|
|
5
|
-
account bridge can run each session's terminals in its own repo.
|
|
6
|
-
Phase 1 of the account-bridge spec (docs/specs/2026-06-16-account-bridge.md). */
|
|
7
|
-
import os from 'node:os'
|
|
8
|
-
import fs from 'node:fs'
|
|
9
|
-
import path from 'node:path'
|
|
10
|
-
|
|
11
|
-
const DIR = path.join(os.homedir(), '.thinkpool-pair')
|
|
12
|
-
const AUTH = path.join(DIR, 'auth.json')
|
|
13
|
-
const DIRS = path.join(DIR, 'dirs.json')
|
|
14
|
-
// served.json: per-room memory of the last real (non-home) directory a room was
|
|
15
|
-
// auto-served from. Distinct from dirs.json (explicit binds): this is learned, so a
|
|
16
|
-
// supervisor relaunched from $HOME still resumes each room in the repo it last ran in
|
|
17
|
-
// instead of falling back to the home folder (FVIHV1DE, 2026-06-30).
|
|
18
|
-
const SERVED = path.join(DIR, 'served.json')
|
|
19
|
-
|
|
20
|
-
function ensureDir() { try { fs.mkdirSync(DIR, { recursive: true }) } catch { /* noop */ } }
|
|
21
|
-
|
|
22
|
-
function writeAuthPayload(payload) {
|
|
23
|
-
ensureDir()
|
|
24
|
-
// ATOMIC write (2026-07-08): a crash/kill/kickstart mid-write must never leave a
|
|
25
|
-
// truncated auth.json — an unparseable token file reads as "not linked" and bricks the
|
|
26
|
-
// saved login (three bridge outages in two days, one from a restart landing mid-rotation).
|
|
27
|
-
// Write a sibling temp file, fsync it to disk, then rename OVER auth.json — rename is
|
|
28
|
-
// atomic on one filesystem, so any concurrent reader sees the whole old file or the whole
|
|
29
|
-
// new one, never a half-written token. This does NOT undo Supabase's server-side rotation
|
|
30
|
-
// (that invalidates the old token the instant refreshSession returns) — the frequency of
|
|
31
|
-
// rotation is cut by F1/F3 in account.mjs; this guarantees the file itself is never torn.
|
|
32
|
-
const tmp = `${AUTH}.tmp.${process.pid}.${Date.now()}`
|
|
33
|
-
try {
|
|
34
|
-
const fd = fs.openSync(tmp, 'w', 0o600)
|
|
35
|
-
try { fs.writeSync(fd, JSON.stringify(payload)); fs.fsyncSync(fd) } finally { fs.closeSync(fd) }
|
|
36
|
-
fs.renameSync(tmp, AUTH)
|
|
37
|
-
} catch {
|
|
38
|
-
try { fs.rmSync(tmp, { force: true }) } catch { /* noop */ }
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function saveAuth(session) {
|
|
43
|
-
if (!session?.refresh_token) return
|
|
44
|
-
writeAuthPayload({
|
|
45
|
-
refresh_token: session.refresh_token,
|
|
46
|
-
access_token: session.access_token || null,
|
|
47
|
-
// Unix SECONDS. Lets the bridge adopt the saved access token WITHOUT a refresh while
|
|
48
|
-
// it's still comfortably valid (account.mjs F1) — every skipped rotation is one fewer
|
|
49
|
-
// race window (and one fewer chance to be killed mid-rotation and brick the login).
|
|
50
|
-
expires_at: session.expires_at || null,
|
|
51
|
-
email: session.user?.email || session.email || null,
|
|
52
|
-
savedAt: Date.now(),
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
export function loadAuth() { try { return JSON.parse(fs.readFileSync(AUTH, 'utf8')) } catch { return null } }
|
|
56
|
-
export function markAuthReconnectRequired(errorCode = null) {
|
|
57
|
-
const current = loadAuth()
|
|
58
|
-
if (!current?.refresh_token) return false
|
|
59
|
-
writeAuthPayload({
|
|
60
|
-
...current,
|
|
61
|
-
reconnect_required: true,
|
|
62
|
-
reconnect_error_code: errorCode || null,
|
|
63
|
-
reconnect_required_at: Date.now(),
|
|
64
|
-
})
|
|
65
|
-
return true
|
|
66
|
-
}
|
|
67
|
-
export function clearAuth() { try { fs.unlinkSync(AUTH) } catch { /* noop */ } }
|
|
68
|
-
|
|
69
|
-
export function loadDirs() { try { return JSON.parse(fs.readFileSync(DIRS, 'utf8')) } catch { return {} } }
|
|
70
|
-
export function bindDir(room, dir) {
|
|
71
|
-
const d = loadDirs(); d[room] = dir
|
|
72
|
-
ensureDir()
|
|
73
|
-
try { fs.writeFileSync(DIRS, JSON.stringify(d, null, 2)) } catch { /* noop */ }
|
|
74
|
-
return d
|
|
75
|
-
}
|
|
76
|
-
export function unbindDir(room) {
|
|
77
|
-
const d = loadDirs(); delete d[room]
|
|
78
|
-
try { fs.writeFileSync(DIRS, JSON.stringify(d, null, 2)) } catch { /* noop */ }
|
|
79
|
-
return d
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Learned per-room serve directory (see SERVED note above). loadServed feeds the
|
|
83
|
-
// dir-resolution precedence; rememberServed records the dir a room was just served
|
|
84
|
-
// from so a later relaunch-from-home resumes it there rather than in $HOME.
|
|
85
|
-
export function loadServed() { try { return JSON.parse(fs.readFileSync(SERVED, 'utf8')) } catch { return {} } }
|
|
86
|
-
export function rememberServed(room, dir) {
|
|
87
|
-
const d = loadServed(); if (!room || !dir || d[room] === dir) return d
|
|
88
|
-
d[room] = dir
|
|
89
|
-
ensureDir()
|
|
90
|
-
try { fs.writeFileSync(SERVED, JSON.stringify(d, null, 2)) } catch { /* noop */ }
|
|
91
|
-
return d
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Account-wide DEFAULT serve directory: new/unbound rooms auto-serve HERE instead of
|
|
95
|
-
// being refused from $HOME. Set once (CLI `set-default-dir <dir>`, or the web attach
|
|
96
|
-
// card). One dir for the common "I mostly work in one repo" case; per-room binds still
|
|
97
|
-
// override it. Stored as a plain path in ~/.thinkpool-pair/default-dir.
|
|
98
|
-
const DEFAULTDIR = path.join(DIR, 'default-dir')
|
|
99
|
-
export function loadDefaultDir() { try { const d = fs.readFileSync(DEFAULTDIR, 'utf8').trim(); return d || null } catch { return null } }
|
|
100
|
-
export function saveDefaultDir(dir) { ensureDir(); try { fs.writeFileSync(DEFAULTDIR, String(dir || '')) } catch { /* noop */ } return dir }
|
|
101
|
-
export function clearDefaultDir() { try { fs.unlinkSync(DEFAULTDIR) } catch { /* noop */ } }
|
|
102
|
-
|
|
103
|
-
// Zero-config default: Claude Code tracks every project directory you've worked in
|
|
104
|
-
// (~/.claude.json `projects`, with per-project session history under
|
|
105
|
-
// ~/.claude/projects/<path-with-slashes-as-dashes>). The MOST-RECENTLY-used real project
|
|
106
|
-
// dir is where you actually code — a great default for a new/unbound room, no setup. Skips
|
|
107
|
-
// $HOME, tmp, and worktree paths; ranks by the session-dir mtime (encode path forward only,
|
|
108
|
-
// never decode — the dash encoding is lossy for paths containing '-').
|
|
109
|
-
export function claudeRecentProjectDir() {
|
|
110
|
-
try {
|
|
111
|
-
const home = os.homedir()
|
|
112
|
-
const conf = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'))
|
|
113
|
-
const projRoot = path.join(home, '.claude', 'projects')
|
|
114
|
-
const best = Object.keys(conf.projects || {})
|
|
115
|
-
.filter((p) => p && p !== home && !p.startsWith('/private/tmp') && !p.startsWith('/tmp') && !p.includes('/.claude/') && fs.existsSync(p))
|
|
116
|
-
.map((p) => { let mt = 0; try { mt = fs.statSync(path.join(projRoot, p.replace(/\//g, '-'))).mtimeMs } catch { /* no session dir */ } return { p, mt } })
|
|
117
|
-
.sort((a, b) => b.mt - a.mt)[0]
|
|
118
|
-
return best?.p || null
|
|
119
|
-
} catch { return null }
|
|
120
|
-
}
|
|
1
|
+
import t from"node:os";import e from"node:fs";import r from"node:path";const n=r.join(t.homedir(),".thinkpool-pair"),c=r.join(n,"auth.json"),o=r.join(n,"dirs.json"),i=r.join(n,"served.json");function u(){try{e.mkdirSync(n,{recursive:!0})}catch{}}function a(t){u();const r=`${c}.tmp.${process.pid}.${Date.now()}`;try{const n=e.openSync(r,"w",384);try{e.writeSync(n,JSON.stringify(t)),e.fsyncSync(n)}finally{e.closeSync(n)}e.renameSync(r,c)}catch{try{e.rmSync(r,{force:!0})}catch{}}}export function saveAuth(t){t?.refresh_token&&a({refresh_token:t.refresh_token,access_token:t.access_token||null,expires_at:t.expires_at||null,email:t.user?.email||t.email||null,savedAt:Date.now()})}export function loadAuth(){try{return JSON.parse(e.readFileSync(c,"utf8"))}catch{return null}}export function markAuthReconnectRequired(t=null){const e=loadAuth();return!!e?.refresh_token&&(a({...e,reconnect_required:!0,reconnect_error_code:t||null,reconnect_required_at:Date.now()}),!0)}export function clearAuth(){try{e.unlinkSync(c)}catch{}}export function loadDirs(){try{return JSON.parse(e.readFileSync(o,"utf8"))}catch{return{}}}export function bindDir(t,r){const n=loadDirs();n[t]=r,u();try{e.writeFileSync(o,JSON.stringify(n,null,2))}catch{}return n}export function unbindDir(t){const r=loadDirs();delete r[t];try{e.writeFileSync(o,JSON.stringify(r,null,2))}catch{}return r}export function loadServed(){try{return JSON.parse(e.readFileSync(i,"utf8"))}catch{return{}}}export function rememberServed(t,r){const n=loadServed();if(!t||!r||n[t]===r)return n;n[t]=r,u();try{e.writeFileSync(i,JSON.stringify(n,null,2))}catch{}return n}const l=r.join(n,"default-dir");export function loadDefaultDir(){try{return e.readFileSync(l,"utf8").trim()||null}catch{return null}}export function saveDefaultDir(t){u();try{e.writeFileSync(l,String(t||""))}catch{}return t}export function clearDefaultDir(){try{e.unlinkSync(l)}catch{}}export function claudeRecentProjectDir(){try{const n=t.homedir(),c=JSON.parse(e.readFileSync(r.join(n,".claude.json"),"utf8")),o=r.join(n,".claude","projects"),i=Object.keys(c.projects||{}).filter(t=>t&&t!==n&&!t.startsWith("/private/tmp")&&!t.startsWith("/tmp")&&!t.includes("/.claude/")&&e.existsSync(t)).map(t=>{let n=0;try{n=e.statSync(r.join(o,t.replace(/\//g,"-"))).mtimeMs}catch{}return{p:t,mt:n}}).sort((t,e)=>e.mt-t.mt)[0];return i?.p||null}catch{return null}}
|