thinkpool-pair 0.6.12 → 0.6.14
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/bridge.mjs +6 -0
- package/claude-session.mjs +25 -1
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -217,6 +217,9 @@ const name = process.env.TP_NAME || os.userInfo().username || 'host'
|
|
|
217
217
|
// Cheap reads, no subprocess: directory name + .git/HEAD.
|
|
218
218
|
const cwd = process.cwd()
|
|
219
219
|
const repoLabel = path.basename(cwd)
|
|
220
|
+
// thinkpool-pair's own version — surfaced in the room's welcome banner.
|
|
221
|
+
let VERSION = null
|
|
222
|
+
try { VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { /* unknown — banner omits it */ }
|
|
220
223
|
let branch = null
|
|
221
224
|
try {
|
|
222
225
|
const head = fs.readFileSync(path.join(cwd, '.git', 'HEAD'), 'utf8').trim()
|
|
@@ -270,6 +273,9 @@ const bcast = (event, payload) => {
|
|
|
270
273
|
const announce = () =>
|
|
271
274
|
bcast('bridge', {
|
|
272
275
|
v: 2, name, repo: repoLabel, branch,
|
|
276
|
+
// cwd + version: the host's working dir + thinkpool-pair version, shown in
|
|
277
|
+
// the room's welcome banner. Re-sent per announce so late joiners get them.
|
|
278
|
+
cwd, version: VERSION,
|
|
273
279
|
// updir: where room file-drops land (forward-slash normalised — the web
|
|
274
280
|
// client string-joins host paths onto it; Node accepts `/` on Windows).
|
|
275
281
|
updir: UPDIR.split(path.sep).join('/'),
|
package/claude-session.mjs
CHANGED
|
@@ -39,6 +39,21 @@ export function classifyRisk(toolName, input) {
|
|
|
39
39
|
return 'medium'
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// ── safe-doc writes — auto-allow regardless of permission mode ──
|
|
43
|
+
// The repo MANDATES end-of-session writes (devlogs under .claude/SESSIONS/, and
|
|
44
|
+
// CLAUDE.md updates). They're append-only documentation with no runtime blast
|
|
45
|
+
// radius. Carding them in `default` mode dead-ended a phone-driven paired session
|
|
46
|
+
// (2026-06-15 SESSIONS-gate: every write threw a room card, deny-default + the
|
|
47
|
+
// "do not retry" deny-reason made Claude abandon the write and re-explain). These
|
|
48
|
+
// paths skip the card always; Bash/network/destructive/other writes are unchanged.
|
|
49
|
+
// Spec: docs/specs/2026-06-15-paired-permission-safe-doc-writes.md.
|
|
50
|
+
const SAFE_DOC_RE = /(^|\/)\.claude\/SESSIONS\/|(^|\/)CLAUDE\.md$/
|
|
51
|
+
export function isSafeDocWrite(toolName, input) {
|
|
52
|
+
if (!WRITE_TOOLS.has(toolName)) return false
|
|
53
|
+
const p = (input && (input.file_path || input.notebook_path)) || ''
|
|
54
|
+
return SAFE_DOC_RE.test(p)
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
// ── input stream — a generator we keep open and feed turns into ──
|
|
43
58
|
function makeInputStream() {
|
|
44
59
|
const queue = []
|
|
@@ -92,6 +107,7 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
92
107
|
// setPermissionMode) route through it once streaming.
|
|
93
108
|
let mode = 'default' // mirrors Claude Code's ⇧⇥ cycle
|
|
94
109
|
const alwaysAllow = new Set() // tool:risk signatures the user chose "don't ask again" for
|
|
110
|
+
const toolStart = new Map() // tool_use id → start time, for the duration badge
|
|
95
111
|
|
|
96
112
|
const emit = (evt) => { try { onEvent?.(evt) } catch { /* never let a consumer throw into the loop */ } }
|
|
97
113
|
|
|
@@ -145,10 +161,12 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
145
161
|
// "Don't ask again" is keyed by tool + risk tier, so allowing medium Bash
|
|
146
162
|
// never silently allows a future destructive one (high always re-asks).
|
|
147
163
|
const sig = `${toolName}:${risk}`
|
|
164
|
+
const safeDoc = isSafeDocWrite(toolName, toolInput)
|
|
148
165
|
const auto =
|
|
149
166
|
risk === 'low' ||
|
|
150
167
|
mode === 'bypassPermissions' ||
|
|
151
168
|
(mode === 'acceptEdits' && WRITE_TOOLS.has(toolName) && risk !== 'high') ||
|
|
169
|
+
safeDoc ||
|
|
152
170
|
alwaysAllow.has(sig)
|
|
153
171
|
let decision = 'allow'
|
|
154
172
|
if (!auto) {
|
|
@@ -193,13 +211,19 @@ export function startClaudeSession({ cwd, model, resume, onEvent, requestPermiss
|
|
|
193
211
|
emit({ kind: 'system', sessionId, model: m.model || model || null })
|
|
194
212
|
break
|
|
195
213
|
case 'assistant':
|
|
214
|
+
// Stamp tool-call start times so tool_result can report a duration.
|
|
215
|
+
for (const b of (m.message?.content || [])) {
|
|
216
|
+
if (b?.type === 'tool_use' && b.id) toolStart.set(b.id, Date.now())
|
|
217
|
+
}
|
|
196
218
|
emit({ kind: 'assistant', blocks: simplifyBlocks(m.message?.content) })
|
|
197
219
|
break
|
|
198
220
|
case 'user':
|
|
199
221
|
// tool_result blocks arrive on the user-role echo
|
|
200
222
|
for (const b of (m.message?.content || [])) {
|
|
201
223
|
if (b?.type === 'tool_result') {
|
|
202
|
-
|
|
224
|
+
const start = toolStart.get(b.tool_use_id)
|
|
225
|
+
if (start != null) toolStart.delete(b.tool_use_id)
|
|
226
|
+
emit({ kind: 'tool_result', toolUseId: b.tool_use_id, content: b.content, isError: !!b.is_error, durationMs: start != null ? Date.now() - start : undefined })
|
|
203
227
|
}
|
|
204
228
|
}
|
|
205
229
|
break
|