thinkpool-pair 0.7.176 → 0.7.177
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/agent-notify.mjs +131 -0
- package/bridge.mjs +92 -2
- package/claude-session.mjs +7 -1
- package/package.json +2 -1
package/agent-notify.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
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 (!isUserFacingLane(entry)) return false
|
|
62
|
+
if (subtype === 'aborted') return false
|
|
63
|
+
if (!startedAt) return false
|
|
64
|
+
if (!Number.isFinite(now)) return false
|
|
65
|
+
return (now - startedAt) >= minTurnMs
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* The one-line "what is it asking me" for a permission card.
|
|
69
|
+
AskUserQuestion cards carry `questions`; ordinary tool cards degrade to
|
|
70
|
+
`<ToolName> <first meaningful arg>`, matching the local stderr line. */
|
|
71
|
+
export function permissionSummary(payload) {
|
|
72
|
+
const p = payload || {}
|
|
73
|
+
const qs = Array.isArray(p.questions) ? p.questions.map(q => q?.question).filter(Boolean) : []
|
|
74
|
+
if (qs.length) return clipSummary(qs.join(' / '))
|
|
75
|
+
if (p.risk === 'plan') return clipSummary(p.plan ? `Plan ready: ${p.plan}` : 'A plan is ready for review')
|
|
76
|
+
const i = p.input
|
|
77
|
+
const arg = i && (i.command || i.file_path || i.path || i.pattern || i.url || i.query || i.description)
|
|
78
|
+
const name = p.toolName || 'A tool'
|
|
79
|
+
return clipSummary(arg ? `${name} ${arg}` : name)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Per-session permission-card notifier.
|
|
83
|
+
|
|
84
|
+
arm(id, payload) — a card was raised. Start the grace timer.
|
|
85
|
+
resolve(id) — the card was answered (human click, mode-flip
|
|
86
|
+
auto-approval, or a drain-to-deny). If its timer had
|
|
87
|
+
already FIRED we pushed a banner, so retract it.
|
|
88
|
+
clearAll() — turn ended / session died. Every outstanding card is
|
|
89
|
+
dead; retract anything we announced.
|
|
90
|
+
|
|
91
|
+
Timers are injected so the unit test drives them synchronously. onFire /
|
|
92
|
+
onRetract are fire-and-forget (they end up as REST inserts); a throw in
|
|
93
|
+
either must never take the bridge down. */
|
|
94
|
+
export function createPermNotifier({
|
|
95
|
+
graceMs = DEFAULT_PERM_GRACE_MS,
|
|
96
|
+
onFire = () => {},
|
|
97
|
+
onRetract = () => {},
|
|
98
|
+
setTimer = setTimeout,
|
|
99
|
+
clearTimer = clearTimeout,
|
|
100
|
+
} = {}) {
|
|
101
|
+
const cards = new Map() // id → { timer, fired, payload }
|
|
102
|
+
const safe = (fn, id, payload) => { try { fn(id, payload) } catch { /* never break the turn */ } }
|
|
103
|
+
|
|
104
|
+
const settle = (id) => {
|
|
105
|
+
const rec = cards.get(id)
|
|
106
|
+
if (!rec) return
|
|
107
|
+
if (rec.timer) { try { clearTimer(rec.timer) } catch { /* noop */ } }
|
|
108
|
+
cards.delete(id)
|
|
109
|
+
if (rec.fired) safe(onRetract, id, rec.payload)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
arm(id, payload) {
|
|
114
|
+
if (!id || cards.has(id)) return
|
|
115
|
+
const rec = { timer: null, fired: false, payload }
|
|
116
|
+
rec.timer = setTimer(() => {
|
|
117
|
+
rec.timer = null
|
|
118
|
+
rec.fired = true
|
|
119
|
+
safe(onFire, id, payload)
|
|
120
|
+
}, graceMs)
|
|
121
|
+
// Never hold the process open on a permission timer.
|
|
122
|
+
try { rec.timer?.unref?.() } catch { /* not a node timer (test double) */ }
|
|
123
|
+
cards.set(id, rec)
|
|
124
|
+
},
|
|
125
|
+
resolve: settle,
|
|
126
|
+
clearAll() { for (const id of [...cards.keys()]) settle(id) },
|
|
127
|
+
// test/debug introspection only
|
|
128
|
+
size: () => cards.size,
|
|
129
|
+
fired: (id) => !!cards.get(id)?.fired,
|
|
130
|
+
}
|
|
131
|
+
}
|
package/bridge.mjs
CHANGED
|
@@ -39,6 +39,9 @@ import { randomUUID } from 'node:crypto'
|
|
|
39
39
|
import { createClient } from '@supabase/supabase-js'
|
|
40
40
|
import { serveDecision, fetchServeRow, refusalMessage, gateFailAction } from './serve-consent.mjs'
|
|
41
41
|
import { reapTerminalRow as _reapTerminalRow } from './reap-terminal.mjs'
|
|
42
|
+
// Slice 3 — the pure decision layer for agent push events (turn-done / needs-input).
|
|
43
|
+
// Unit-tested in bridge/agent-notify.test.mjs; everything with I/O stays here.
|
|
44
|
+
import { createPermNotifier, shouldNotifyTurnDone, permissionSummary, clipSummary, isUserFacingLane, DEFAULT_MIN_TURN_MS } from './agent-notify.mjs'
|
|
42
45
|
// resolveProviderEnv(id) → {ANTHROPIC_BASE_URL,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_MODEL} for a
|
|
43
46
|
// registered custom provider, or null for the built-in/unknown (leave the default env intact).
|
|
44
47
|
// Multi-provider BYOK slice 1: a lane spawned with a `provider` id runs on that endpoint.
|
|
@@ -763,6 +766,46 @@ async function budgetRpc (fn, args) {
|
|
|
763
766
|
const t = await r.text()
|
|
764
767
|
return t ? JSON.parse(t) : null
|
|
765
768
|
}
|
|
769
|
+
|
|
770
|
+
// ── Agent push events — spec docs/specs/2026-07-09-code-notifications-rework.md (Slice 3).
|
|
771
|
+
// The bridge is the only process that knows a turn ended or a permission card went
|
|
772
|
+
// unanswered, and it CANNOT call /api/notify-code (webhook-secret-gated; this is a
|
|
773
|
+
// published npm package). So it writes a compact `__agent` row into code_messages with
|
|
774
|
+
// the SAME authed REST path budgetRpc uses; the existing push_notify_on_code_message
|
|
775
|
+
// trigger fans it out and api/notify-code.js routes on payload.__agent.
|
|
776
|
+
//
|
|
777
|
+
// Durable by construction: the row is also the transport-neutral event log the iPhone
|
|
778
|
+
// sender will read, and it feeds get_code_unread_counts for free.
|
|
779
|
+
//
|
|
780
|
+
// Fire-and-forget in the strictest sense — an anon bridge (no codeAuthToken/uid), a
|
|
781
|
+
// dead network, or a 4xx must never surface in the turn. TP_AGENT_NOTIFY_OFF=1 kills
|
|
782
|
+
// the whole path. The web client filters `ae:` rows out of its seed/replay (Slice 2),
|
|
783
|
+
// so an old client paired with a new bridge renders nothing new.
|
|
784
|
+
const AGENT_NOTIFY_OFF = process.env.TP_AGENT_NOTIFY_OFF === '1'
|
|
785
|
+
const MIN_TURN_MS = Number(process.env.TP_NOTIFY_MIN_TURN_MS) > 0
|
|
786
|
+
? Number(process.env.TP_NOTIFY_MIN_TURN_MS) : DEFAULT_MIN_TURN_MS
|
|
787
|
+
function persistAgentEvent (payload) {
|
|
788
|
+
if (AGENT_NOTIFY_OFF || !codeAuthToken || !myServeUid || !room) return
|
|
789
|
+
const row = {
|
|
790
|
+
id: `ae:${randomUUID()}`,
|
|
791
|
+
session_code: room,
|
|
792
|
+
author_id: myServeUid,
|
|
793
|
+
payload: { __agent: true, ts: Date.now(), ...payload },
|
|
794
|
+
}
|
|
795
|
+
fetch(`${SUPABASE_URL}/rest/v1/code_messages`, {
|
|
796
|
+
method: 'POST',
|
|
797
|
+
headers: {
|
|
798
|
+
apikey: SUPABASE_ANON,
|
|
799
|
+
Authorization: `Bearer ${codeAuthToken}`,
|
|
800
|
+
'Content-Type': 'application/json',
|
|
801
|
+
Prefer: 'return=minimal',
|
|
802
|
+
},
|
|
803
|
+
body: JSON.stringify(row),
|
|
804
|
+
}).then((r) => {
|
|
805
|
+
if (!r.ok && process.env.TP_DEBUG) process.stderr.write(`\n ◆ agent-event insert ${r.status}\n`)
|
|
806
|
+
}).catch(() => { /* best-effort — a missed banner is not worth a stack trace */ })
|
|
807
|
+
}
|
|
808
|
+
|
|
766
809
|
// Fire-and-forget: fold a completed turn's output tokens into the room's durable monthly
|
|
767
810
|
// ledger. output_tokens = the billed reasoning+output spend the indicator already tracks.
|
|
768
811
|
function recordRoomSpend (tokens) {
|
|
@@ -1542,6 +1585,13 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
1542
1585
|
// order (not readdir/filesystem order). Legacy recs (no openedAt) derive it from the
|
|
1543
1586
|
// first transcript event ts, so even the first post-fix restart is ordered right.
|
|
1544
1587
|
openedAt: openedAt || (Array.isArray(log) ? (log.find((e) => e?.ts)?.ts || 0) : 0) || Date.now() }
|
|
1588
|
+
// Slice 3 — a permission card left unanswered past the grace window pushes
|
|
1589
|
+
// "<lane> — needs you: <what>"; answering it anywhere retracts the banner
|
|
1590
|
+
// everywhere. Worker/flow lanes are excluded at arm() time (isUserFacingLane).
|
|
1591
|
+
entry.permNotifier = createPermNotifier({
|
|
1592
|
+
onFire: (_cardId, summary) => persistAgentEvent({ kind: 'needs-input', term: id, termName: termNames[id] || null, summary }),
|
|
1593
|
+
onRetract: (_cardId) => persistAgentEvent({ kind: 'needs-resolved', term: id, termName: termNames[id] || null }),
|
|
1594
|
+
})
|
|
1545
1595
|
// C1 (RT-2): per-term contiguous seq counter. Seeded from the restored log's max
|
|
1546
1596
|
// so a bridge restart resumes ABOVE every persisted seq — fresh events never reuse
|
|
1547
1597
|
// an old seq, which is what keeps the between-turns restart dup-free (see
|
|
@@ -2135,8 +2185,11 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
2135
2185
|
// roster broadcast (delivered to every subscriber) instead of sniffing the live
|
|
2136
2186
|
// code-event stream a passive dashboard channel misses. Covers every turn path
|
|
2137
2187
|
// (normal, spawn, flow lane, cross-room) since they all flow through onEvent.
|
|
2188
|
+
// The RISING edge is also where a turn's wall clock starts — the only place the
|
|
2189
|
+
// bridge sees "this lane just began working". `result`/`error` reads it to decide
|
|
2190
|
+
// whether the turn was long enough that nobody was watching (Slice 3).
|
|
2138
2191
|
{ const busyNow = entry.session?.turnActive ?? false
|
|
2139
|
-
if (busyNow !== entry._busyAnn) { entry._busyAnn = busyNow; announce() } }
|
|
2192
|
+
if (busyNow !== entry._busyAnn) { if (busyNow) entry._turnStart = Date.now(); entry._busyAnn = busyNow; announce() } }
|
|
2140
2193
|
// Capture this session's current MODEL (resolved full id, with version — from
|
|
2141
2194
|
// system/result `model` or usage `ctx.model`) and the SDK's supported-model
|
|
2142
2195
|
// LIST (from the `models` event) so the announce carries them to EVERY device.
|
|
@@ -2247,6 +2300,30 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
2247
2300
|
bcast('code-event', { term: id, evt: { kind: 'compact', status: 'done', ts: Date.now() } })
|
|
2248
2301
|
entry.compactBy = null
|
|
2249
2302
|
}
|
|
2303
|
+
// ── Slice 3: the turn settled. Two things happen, in this order.
|
|
2304
|
+
// 1. Every outstanding permission card on this lane is now dead (the SDK
|
|
2305
|
+
// won't ask again for a turn that ended) — clearAll retracts any
|
|
2306
|
+
// "needs you" banner we already pushed. Do this even for aborted turns
|
|
2307
|
+
// and non-user-facing lanes: a stale card can never fire later.
|
|
2308
|
+
// 2. If the turn ran long enough that nobody was plausibly watching, and
|
|
2309
|
+
// the lane is one a human opened, and they didn't press Stop — write a
|
|
2310
|
+
// `turn-done` event row. An `error` turn uses the same neutral copy.
|
|
2311
|
+
// _turnStart is cleared unconditionally so a stray post-settle event can't
|
|
2312
|
+
// re-notify off a stale stamp.
|
|
2313
|
+
if (evt.kind === 'result' || evt.kind === 'error') {
|
|
2314
|
+
const startedAt = entry._turnStart
|
|
2315
|
+
entry._turnStart = null
|
|
2316
|
+
entry.permNotifier?.clearAll()
|
|
2317
|
+
if (shouldNotifyTurnDone({ entry, subtype: evt.subtype, startedAt, now: Date.now(), minTurnMs: MIN_TURN_MS })) {
|
|
2318
|
+
persistAgentEvent({
|
|
2319
|
+
kind: 'turn-done',
|
|
2320
|
+
term: id,
|
|
2321
|
+
termName: termNames[id] || null,
|
|
2322
|
+
summary: clipSummary(evt.kind === 'result' ? evt.resultText : evt.message),
|
|
2323
|
+
subtype: evt.subtype || (evt.kind === 'error' ? 'error' : null),
|
|
2324
|
+
})
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2250
2327
|
// Single tail: stamp into the log (transcript kinds only), broadcast live,
|
|
2251
2328
|
// print locally, persist. Shared so a deferred image event re-enters it once
|
|
2252
2329
|
// its base64 has been lifted to a Storage path (see deferImageEvent).
|
|
@@ -2281,6 +2358,13 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
|
|
|
2281
2358
|
const payload = { term: id, id: req.id, toolName: req.toolName, input: req.input, risk: req.risk, plan: req.plan, questions: req.questions }
|
|
2282
2359
|
entry.pending.set(req.id, { resolve, payload })
|
|
2283
2360
|
bcast('code-perm-req', payload)
|
|
2361
|
+
// Slice 3 — arm the grace timer. Fires a "needs you" push only if the card is
|
|
2362
|
+
// STILL unanswered 20s from now (you're not in the room). Every resolve path —
|
|
2363
|
+
// the code-perm broadcast, drainPending, and the bypass/acceptEdits mode-flip
|
|
2364
|
+
// auto-approvals — calls permNotifier.resolve(id), which retracts the banner
|
|
2365
|
+
// if it had fired. Worker + flow-slice lanes are skipped: their cards belong to
|
|
2366
|
+
// an agent's fan-out, and a 12-lane flow would fan 12 banners at one person.
|
|
2367
|
+
if (isUserFacingLane(entry)) entry.permNotifier?.arm(req.id, permissionSummary(payload))
|
|
2284
2368
|
process.stderr.write(req.risk === 'plan'
|
|
2285
2369
|
? `\n ${A.mag}◆ plan ready — approve in the room.${A.rst}\n`
|
|
2286
2370
|
: req.risk === 'ask'
|
|
@@ -2310,6 +2394,8 @@ function drainPending(s) {
|
|
|
2310
2394
|
if (!s?.pending) return
|
|
2311
2395
|
for (const [, p] of s.pending) { try { p.resolve('deny') } catch { /* noop */ } }
|
|
2312
2396
|
s.pending.clear()
|
|
2397
|
+
// Every card just died → retract any "needs you" banner they raised (Slice 3).
|
|
2398
|
+
s.permNotifier?.clearAll()
|
|
2313
2399
|
}
|
|
2314
2400
|
|
|
2315
2401
|
// Switching a terminal to bypassPermissions must RETROACTIVELY clear the cards the
|
|
@@ -2329,6 +2415,7 @@ function allowPending(s) {
|
|
|
2329
2415
|
if (p?.payload?.risk === 'plan' || p?.payload?.risk === 'ask') continue
|
|
2330
2416
|
try { p.resolve('allow') } catch { /* noop */ }
|
|
2331
2417
|
s.pending.delete(key)
|
|
2418
|
+
s.permNotifier?.resolve(key) // auto-approved → retract the "needs you" banner
|
|
2332
2419
|
}
|
|
2333
2420
|
}
|
|
2334
2421
|
|
|
@@ -2347,6 +2434,7 @@ function acceptEditsPending(s) {
|
|
|
2347
2434
|
if (!ACCEPT_EDIT_TOOLS.has(tn)) continue
|
|
2348
2435
|
try { p.resolve('allow') } catch { /* noop */ }
|
|
2349
2436
|
s.pending.delete(key)
|
|
2437
|
+
s.permNotifier?.resolve(key) // auto-approved → retract the "needs you" banner
|
|
2350
2438
|
}
|
|
2351
2439
|
}
|
|
2352
2440
|
|
|
@@ -2742,7 +2830,9 @@ channel
|
|
|
2742
2830
|
const p = s && payload.id && s.pending.get(payload.id)
|
|
2743
2831
|
// Pass the raw decision through — normal tools use allow/deny, plan cards
|
|
2744
2832
|
// use run/accept/keep (claude-session interprets). Default deny on missing.
|
|
2745
|
-
|
|
2833
|
+
// Answered from a device → retract the "needs you" banner on every OTHER device
|
|
2834
|
+
// (permNotifier.resolve is a no-op unless its grace timer already fired).
|
|
2835
|
+
if (p) { s.pending.delete(payload.id); s.permNotifier?.resolve(payload.id); p.resolve(payload.decision || 'deny') }
|
|
2746
2836
|
})
|
|
2747
2837
|
.on('broadcast', { event: 'code-abort' }, ({ payload }) => {
|
|
2748
2838
|
const s = payload?.term && sessions.get(payload.term)
|
package/claude-session.mjs
CHANGED
|
@@ -978,7 +978,13 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
|
|
|
978
978
|
}
|
|
979
979
|
if (best) curModel = best
|
|
980
980
|
}
|
|
981
|
-
|
|
981
|
+
// resultText: the SDK's final assistant text, clipped. The bridge folds it
|
|
982
|
+
// into the `turn-done` push body ("<lane> — done: <summary>") so a banner
|
|
983
|
+
// says what happened, not just that something did. Clipped + whitespace-
|
|
984
|
+
// collapsed here so neither the broadcast frame nor the persisted log grows
|
|
985
|
+
// by a full turn's prose. Absent on aborted/error results — nothing to say.
|
|
986
|
+
{ const rt = typeof m.result === 'string' ? m.result.replace(/\s+/g, ' ').trim().slice(0, 200) : ''
|
|
987
|
+
emit({ kind: 'result', subtype: m.subtype, sessionId, model: curModel, costUsd: m.total_cost_usd, usage: m.usage, numTurns: m.num_turns, durationMs: m.duration_ms ?? null, denials: Array.isArray(m.permission_denials) ? m.permission_denials.length : 0, resultText: rt || null }) }
|
|
982
988
|
// Arm the Haiku fallback: if Claude's own prompt_suggestion doesn't arrive within
|
|
983
989
|
// the window, generate one from the last assistant reply. (Skip aborted turns.)
|
|
984
990
|
if (sugTimer) clearTimeout(sugTimer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.177",
|
|
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": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"claude-session.mjs",
|
|
16
16
|
"turn-stall.mjs",
|
|
17
17
|
"update-gate.mjs",
|
|
18
|
+
"agent-notify.mjs",
|
|
18
19
|
"event-id.mjs",
|
|
19
20
|
"recap.mjs",
|
|
20
21
|
"transcript-sanitize.mjs",
|