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 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('/'),
@@ -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
- emit({ kind: 'tool_result', toolUseId: b.tool_use_id, content: b.content, isError: !!b.is_error })
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {