thinkpool-pair 0.7.199 → 0.7.200
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/account.mjs +2 -1
- package/agent-detect.mjs +28 -0
- package/bridge.mjs +2 -13
- package/codex-event-mapper.mjs +43 -7
- package/package.json +2 -1
package/account.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { publicKeyB64, announceProviders, listProviders, addProvider, addProvide
|
|
|
20
20
|
import { supervisorServes } from './serve-consent.mjs'
|
|
21
21
|
import { resolveServeDir } from './serve-dir.mjs'
|
|
22
22
|
import { pairKeyFor, pairTopic, CROSSROOM_BUS } from './cross-terminal.mjs'
|
|
23
|
+
import { installedAgentCommands } from './agent-detect.mjs'
|
|
23
24
|
|
|
24
25
|
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return null } })()
|
|
25
26
|
|
|
@@ -557,7 +558,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
557
558
|
// to spawn against — re-read each announce so an add/remove reflects immediately.
|
|
558
559
|
// Both are additive; older clients ignore them (feature: multi-provider BYOK, slice 1).
|
|
559
560
|
const PROVIDER_PUBKEY = publicKeyB64()
|
|
560
|
-
const pushPresence = () => trackPresence({ name: machine, version: VERSION, rooms: [...children.keys()], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), ts: Date.now() })
|
|
561
|
+
const pushPresence = () => trackPresence({ name: machine, version: VERSION, rooms: [...children.keys()], refused: [...refused].map(([code, reason]) => ({ code, reason })), service: runsAsService(), providerPubKey: PROVIDER_PUBKEY, providers: announceProviders(), agents: installedAgentCommands().map((cmd) => ({ cmd })), ts: Date.now() })
|
|
561
562
|
|
|
562
563
|
// ── Provider registry — the multi-BYOK wire contract (slice 1). ─────────────
|
|
563
564
|
// AUTH: the account channel `tpacct:<uid>` is created WITHOUT config.private:true,
|
package/agent-detect.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// One installed-agent detector shared by the room bridge and account supervisor.
|
|
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', '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
|
+
}
|
package/bridge.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import { z } from 'zod'
|
|
|
52
52
|
import { startClaudeSession } from './claude-session.mjs'
|
|
53
53
|
import { startCodexSession, readCodexDefaultModel, readCodexModels, codexConfigForMode } from './codex-session.mjs'
|
|
54
54
|
import { createManagedLaneWorktree, removeManagedLaneWorktree } from './lane-worktree.mjs'
|
|
55
|
+
import { commandOnPath } from './agent-detect.mjs'
|
|
55
56
|
|
|
56
57
|
const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
|
|
57
58
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
@@ -178,19 +179,7 @@ function claudeHasSession() {
|
|
|
178
179
|
} catch { return false }
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
const onPath = (c) => {
|
|
183
|
-
const exts = process.platform === 'win32'
|
|
184
|
-
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
185
|
-
: ['']
|
|
186
|
-
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
187
|
-
if (!dir) continue
|
|
188
|
-
for (const ext of exts) {
|
|
189
|
-
try { if (fs.existsSync(path.join(dir, c + ext))) return true } catch { /* noop */ }
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return false
|
|
193
|
-
}
|
|
182
|
+
const onPath = (command) => commandOnPath(command)
|
|
194
183
|
|
|
195
184
|
// Pick which installed agent to share → resolves to a command string. A single
|
|
196
185
|
// match (or non-TTY stdin, e.g. piped) auto-selects with no prompt.
|
package/codex-event-mapper.mjs
CHANGED
|
@@ -28,16 +28,27 @@
|
|
|
28
28
|
// the existing risk classifier (claude-session.classifyRisk) and room rendering
|
|
29
29
|
// apply unchanged. Codex v1 is observe-only (no per-tool card) — classification
|
|
30
30
|
// is for display accent + future gating, not blocking (spec §tradeoff).
|
|
31
|
+
import fs from 'node:fs'
|
|
32
|
+
|
|
33
|
+
const safeMcpPart = (value) => String(value || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
34
|
+
const mcpToolName = (item) => `mcp__${safeMcpPart(item?.server)}__${safeMcpPart(item?.tool)}`
|
|
35
|
+
const resultText = (value) => {
|
|
36
|
+
if (typeof value === 'string') return value
|
|
37
|
+
if (value == null) return ''
|
|
38
|
+
try { return JSON.stringify(value) } catch { return String(value) }
|
|
39
|
+
}
|
|
31
40
|
|
|
32
41
|
export class CodexEventMapper {
|
|
33
42
|
/**
|
|
34
43
|
* @param {{onEvent:(evt)=>void, model?:string|null, sessionId?:string|null}} opts
|
|
35
44
|
*/
|
|
36
|
-
constructor({ onEvent, model = null, sessionId = null }) {
|
|
45
|
+
constructor({ onEvent, model = null, sessionId = null, fsImpl = fs }) {
|
|
37
46
|
this._onEvent = onEvent
|
|
38
47
|
this._model = model || null
|
|
39
48
|
this._sessionId = sessionId || null
|
|
40
49
|
this._starts = new Map() // item.id → startMs (for durationMs)
|
|
50
|
+
this._fileBefore = new Map() // item.id → [{ path, kind, content }]
|
|
51
|
+
this._fs = fsImpl
|
|
41
52
|
this._systemSent = false
|
|
42
53
|
}
|
|
43
54
|
|
|
@@ -68,10 +79,18 @@ export class CodexEventMapper {
|
|
|
68
79
|
if (it.type === 'command_execution') {
|
|
69
80
|
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: 'Bash', input: { command: it.command || '' } }], parentToolUseId: null })
|
|
70
81
|
} else if (it.type === 'file_change') {
|
|
71
|
-
const
|
|
72
|
-
|
|
82
|
+
const before = (it.changes || []).map((c) => {
|
|
83
|
+
let content = ''
|
|
84
|
+
if (c.kind !== 'add' && c.path) {
|
|
85
|
+
try { content = this._fs.readFileSync(c.path, 'utf8') } catch { /* missing/unreadable → empty */ }
|
|
86
|
+
}
|
|
87
|
+
return { path: c.path || '', kind: c.kind || 'update', content }
|
|
88
|
+
})
|
|
89
|
+
this._fileBefore.set(it.id, before)
|
|
73
90
|
} else if (it.type === 'web_search') {
|
|
74
91
|
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: 'WebSearch', input: { query: it.query || '' } }], parentToolUseId: null })
|
|
92
|
+
} else if (it.type === 'mcp_tool_call') {
|
|
93
|
+
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name: mcpToolName(it), input: it.arguments || {} }], parentToolUseId: null })
|
|
75
94
|
}
|
|
76
95
|
// agent_message / reasoning have no item.started (burst on completion).
|
|
77
96
|
return
|
|
@@ -92,13 +111,30 @@ export class CodexEventMapper {
|
|
|
92
111
|
const bad = it.exit_code != null ? it.exit_code !== 0 : false
|
|
93
112
|
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.aggregated_output || '' }], isError: bad, durationMs, parentToolUseId: null })
|
|
94
113
|
} else if (it.type === 'file_change') {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
const before = this._fileBefore.get(it.id) || (it.changes || []).map((c) => ({ path: c.path || '', kind: c.kind || 'update', content: '' }))
|
|
115
|
+
this._fileBefore.delete(it.id)
|
|
116
|
+
const edits = before.map((c) => {
|
|
117
|
+
let next = ''
|
|
118
|
+
if (c.kind !== 'delete' && c.path) {
|
|
119
|
+
try { next = this._fs.readFileSync(c.path, 'utf8') } catch { /* deleted/unreadable → empty */ }
|
|
120
|
+
}
|
|
121
|
+
return { file_path: c.path, path: c.path, old_string: c.content, new_string: next, kind: c.kind }
|
|
122
|
+
})
|
|
123
|
+
const first = edits[0] || { file_path: '', path: '', old_string: '', new_string: '', kind: 'update' }
|
|
124
|
+
const name = edits.length > 1 ? 'MultiEdit' : first.kind === 'add' ? 'Write' : 'Edit'
|
|
125
|
+
const input = name === 'MultiEdit'
|
|
126
|
+
? { edits }
|
|
127
|
+
: name === 'Write'
|
|
128
|
+
? { file_path: first.file_path, path: first.path, content: first.new_string }
|
|
129
|
+
: first
|
|
130
|
+
this._emit({ kind: 'assistant', blocks: [{ type: 'tool_use', id: it.id, name, input }], parentToolUseId: null })
|
|
131
|
+
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: `${name === 'Write' ? 'wrote' : 'edited'} ${first.path}` }], isError: false, durationMs, parentToolUseId: null })
|
|
98
132
|
} else if (it.type === 'web_search') {
|
|
99
133
|
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.query || 'search completed' }], isError: false, durationMs, parentToolUseId: null })
|
|
134
|
+
} else if (it.type === 'mcp_tool_call') {
|
|
135
|
+
this._emit({ kind: 'tool_result', toolUseId: it.id, content: [{ type: 'text', text: it.error?.message || resultText(it.result) }], isError: it.status === 'failed' || !!it.error, durationMs, parentToolUseId: null })
|
|
100
136
|
}
|
|
101
|
-
//
|
|
137
|
+
// plan_update remains an additive future event shape. Unknown
|
|
102
138
|
// items are ignored rather than risking a malformed transcript row.
|
|
103
139
|
return
|
|
104
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.200",
|
|
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": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"turn-stall.mjs",
|
|
21
21
|
"update-gate.mjs",
|
|
22
22
|
"agent-notify.mjs",
|
|
23
|
+
"agent-detect.mjs",
|
|
23
24
|
"event-id.mjs",
|
|
24
25
|
"plan-meters.mjs",
|
|
25
26
|
"recap.mjs",
|