@wakeshift/pi 1.4.40 → 1.4.42
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/index.js +113 -11
- package/package.json +5 -5
package/index.js
CHANGED
|
@@ -120,6 +120,83 @@ const linked = (sid) => {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// The session key of the session that installed this extension (issue
|
|
124
|
+
// #1106). Its result that offered the install went by before there was an
|
|
125
|
+
// extension, so no beacon came with it (nothing named the session yet), and
|
|
126
|
+
// it connects from the next result of the app's tools, whose beacon the core
|
|
127
|
+
// adds only for a call that names the session: by the session_key that
|
|
128
|
+
// result told the agent to pass, and the session's ID. Where the agent
|
|
129
|
+
// leaves the key out, the extension passes it, while the session has no
|
|
130
|
+
// link: taken from the session's own results of that server's tools from
|
|
131
|
+
// the hour before the extension was installed (installed_at, which no
|
|
132
|
+
// result after the install can change), and only if they hold one key (one
|
|
133
|
+
// quoted in what a tool showed, beside the session's own, passes nothing).
|
|
134
|
+
const KEY_LINE = /Pass session_key="(wsc_[A-Za-z0-9_-]{22,26})" to this app's tools/g
|
|
135
|
+
const KEY_WINDOW_MS = 3600000
|
|
136
|
+
const installedAt = () => {
|
|
137
|
+
try {
|
|
138
|
+
const t = Date.parse(JSON.parse(readFileSync(join(home(), "pi", "config.json"), "utf8")).installed_at)
|
|
139
|
+
return Number.isFinite(t) ? t : 0
|
|
140
|
+
} catch {
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// pi's name for server's MCP tool tool (its createMcpToolName, as the
|
|
145
|
+
// connector's PiToolName checks beacons): mcp__<server>__<tool> with every
|
|
146
|
+
// character but ASCII letters, digits and _ turned into _, or, when that's
|
|
147
|
+
// too long or taken, its first 55 characters, _ and 8 hex digits of
|
|
148
|
+
// SHA-256(server NUL tool).
|
|
149
|
+
const PI_MAX_TOOL = 64
|
|
150
|
+
const piToolName = (server, tool, name) => {
|
|
151
|
+
const full = (MCP_PREFIX + server + "__" + tool).replace(/[^A-Za-z0-9_]/g, "_")
|
|
152
|
+
if (name === full && full.length <= PI_MAX_TOOL) return true
|
|
153
|
+
const hash = createHash("sha256").update(server + "\0" + tool).digest("hex").slice(0, 8)
|
|
154
|
+
return name === full.slice(0, PI_MAX_TOOL - hash.length - 1) + "_" + hash
|
|
155
|
+
}
|
|
156
|
+
// The MCP server whose tool name is (pi's own record of it: the tool's
|
|
157
|
+
// namespace, whose one alias is the server's exact name), only if no other
|
|
158
|
+
// server pi has could have a tool of that name: two servers whose names
|
|
159
|
+
// pi's naming makes the same (a-b and a_b), or one whose name starts
|
|
160
|
+
// another's (foo and foo__bar), leave the name to neither.
|
|
161
|
+
const serverOfTool = (tools, name) => {
|
|
162
|
+
const serverOf = (t) => {
|
|
163
|
+
const ns = t?.namespace
|
|
164
|
+
const a = Array.isArray(ns?.aliases) && ns.aliases.length === 1 ? ns.aliases[0] : undefined
|
|
165
|
+
return typeof a === "string" && a && ns.name === MCP_PREFIX + a.replace(/-/g, "_") ? a : undefined
|
|
166
|
+
}
|
|
167
|
+
if (!Array.isArray(tools)) return undefined
|
|
168
|
+
const server = serverOf(tools.find((t) => t?.name === name))
|
|
169
|
+
if (!server) return undefined
|
|
170
|
+
const servers = new Set(tools.map(serverOf).filter(Boolean))
|
|
171
|
+
const could = (s) => {
|
|
172
|
+
const prefix = (MCP_PREFIX + s + "__").replace(/[^A-Za-z0-9_]/g, "_")
|
|
173
|
+
return name.startsWith(prefix) || name.startsWith(prefix.slice(0, PI_MAX_TOOL - 9))
|
|
174
|
+
}
|
|
175
|
+
const owners = [...servers].filter(could)
|
|
176
|
+
return owners.length === 1 && owners[0] === server ? server : undefined
|
|
177
|
+
}
|
|
178
|
+
// The key, from the session's results of the exact server the called tool
|
|
179
|
+
// is (server, from serverOfTool): each result pi says came from that
|
|
180
|
+
// server's tool (details), named as pi names it.
|
|
181
|
+
const sessionKey = (entries, server) => {
|
|
182
|
+
const at = installedAt()
|
|
183
|
+
if (!at || !server || !Array.isArray(entries)) return ""
|
|
184
|
+
const keys = new Set()
|
|
185
|
+
for (const e of entries) {
|
|
186
|
+
const m = e && e.type === "message" ? e.message : undefined
|
|
187
|
+
const d = m?.details
|
|
188
|
+
if (m?.role !== "toolResult" || typeof m.toolName !== "string" || !d || d.server !== server || typeof d.tool !== "string") continue
|
|
189
|
+
if (!piToolName(d.server, d.tool, m.toolName)) continue
|
|
190
|
+
const t = typeof m.timestamp === "number" ? m.timestamp : Date.parse(e.timestamp)
|
|
191
|
+
if (!(t < at && t >= at - KEY_WINDOW_MS)) continue
|
|
192
|
+
for (const b of Array.isArray(m.content) ? m.content : []) {
|
|
193
|
+
if (b?.type !== "text" || typeof b.text !== "string") continue
|
|
194
|
+
for (const x of b.text.matchAll(KEY_LINE)) keys.add(x[1])
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return keys.size === 1 ? [...keys][0] : ""
|
|
198
|
+
}
|
|
199
|
+
|
|
123
200
|
// The connector's errors, for the user to look at.
|
|
124
201
|
const log = () => {
|
|
125
202
|
try {
|
|
@@ -172,7 +249,12 @@ export default function wakeshift(pi) {
|
|
|
172
249
|
let ctx // the session's context, from session_start
|
|
173
250
|
let sid = ""
|
|
174
251
|
let child // the session's bridge, once it's connected
|
|
175
|
-
|
|
252
|
+
// Bridges still starting. More than one only for beacons: two results in
|
|
253
|
+
// one turn each carry one, and each bridge redeems its own (the connector
|
|
254
|
+
// takes one redeem at a time, the second handing back the first's link),
|
|
255
|
+
// so neither is stopped for the other.
|
|
256
|
+
const starting = new Set()
|
|
257
|
+
const carrying = new WeakSet() // bridges started with a beacon
|
|
176
258
|
let disposed = false
|
|
177
259
|
let retry // { timer, delay, input }
|
|
178
260
|
const stopped = new WeakSet() // bridges this extension ended on purpose
|
|
@@ -198,7 +280,7 @@ export default function wakeshift(pi) {
|
|
|
198
280
|
retry.delay = Math.min(retry.delay * 2, RETRY_MAX_MS)
|
|
199
281
|
retry.timer = setTimeout(() => {
|
|
200
282
|
retry.timer = undefined
|
|
201
|
-
if (!child && !starting && !disposed) void start(retry.input)
|
|
283
|
+
if (!child && !starting.size && !disposed) void start(retry.input)
|
|
202
284
|
}, wait)
|
|
203
285
|
retry.timer.unref?.()
|
|
204
286
|
}
|
|
@@ -241,13 +323,14 @@ export default function wakeshift(pi) {
|
|
|
241
323
|
} finally {
|
|
242
324
|
if (typeof errs === "number") { try { closeSync(errs) } catch {} }
|
|
243
325
|
}
|
|
244
|
-
starting
|
|
326
|
+
starting.add(c)
|
|
327
|
+
if (input?.hook_event_name === "PostToolUse") carrying.add(c)
|
|
245
328
|
let first = true
|
|
246
329
|
let connected = false
|
|
247
330
|
const done = (text, noteId) => {
|
|
248
331
|
if (!first) return
|
|
249
332
|
first = false
|
|
250
|
-
|
|
333
|
+
starting.delete(c)
|
|
251
334
|
clearTimeout(timer)
|
|
252
335
|
const ack = noteId ? () => { try { c.stdin.write(JSON.stringify({ ack: noteId }) + "\n") } catch {} } : undefined
|
|
253
336
|
resolve(text ? { text, ack } : undefined)
|
|
@@ -323,27 +406,43 @@ export default function wakeshift(pi) {
|
|
|
323
406
|
settle()
|
|
324
407
|
if (nudged) clearTimeout(nudged)
|
|
325
408
|
nudged = undefined
|
|
326
|
-
|
|
409
|
+
for (const c of starting) stop(c)
|
|
327
410
|
stop(child)
|
|
328
|
-
starting
|
|
411
|
+
starting.clear()
|
|
412
|
+
child = undefined
|
|
329
413
|
queue.length = 0
|
|
330
414
|
inflight = []
|
|
331
415
|
})
|
|
332
416
|
|
|
333
417
|
// The app's tools that take Wakeshift's arguments learn which program and
|
|
334
418
|
// session the call is from: harness "pi", and conversation_id this session's
|
|
335
|
-
// ID, whatever the model passed.
|
|
419
|
+
// ID, whatever the model passed. Tools whose schema declares both; and,
|
|
420
|
+
// while this session has no link, the app's tools that declare only
|
|
421
|
+
// session_key (those that subscribe to nothing, which take all three),
|
|
422
|
+
// with the session's key where the agent left it out (sessionKey), so
|
|
423
|
+
// their result can connect the session that installed the extension.
|
|
336
424
|
pi.on("tool_call", async (event, c) => {
|
|
337
425
|
if (!isActive()) return
|
|
338
426
|
try {
|
|
339
427
|
if (typeof event.toolName !== "string" || !event.toolName.startsWith(MCP_PREFIX) || !event.input || typeof event.input !== "object") return
|
|
340
428
|
const id = c?.sessionManager?.getSessionId?.() || sid
|
|
341
429
|
if (!id) return
|
|
342
|
-
const
|
|
430
|
+
const tools = pi.getAllTools()
|
|
431
|
+
const tool = tools.find((t) => t.name === event.toolName)
|
|
343
432
|
const props = tool?.parameters?.properties
|
|
344
|
-
if (!props || typeof props !== "object"
|
|
433
|
+
if (!props || typeof props !== "object") return
|
|
434
|
+
const both = "harness" in props && "conversation_id" in props
|
|
435
|
+
const unlinked = "session_key" in props && !child && !starting.size && !linked(id)
|
|
436
|
+
if (!both && !unlinked) return
|
|
345
437
|
event.input.harness = "pi"
|
|
346
438
|
event.input.conversation_id = id
|
|
439
|
+
const passed = event.input.session_key
|
|
440
|
+
if (unlinked && !(typeof passed === "string" && passed.trim())) {
|
|
441
|
+
let entries
|
|
442
|
+
try { entries = (c?.sessionManager ?? ctx?.sessionManager)?.getBranch?.() } catch {}
|
|
443
|
+
const key = sessionKey(entries, serverOfTool(tools, event.toolName))
|
|
444
|
+
if (key) event.input.session_key = key
|
|
445
|
+
}
|
|
347
446
|
} catch {}
|
|
348
447
|
})
|
|
349
448
|
|
|
@@ -372,9 +471,12 @@ export default function wakeshift(pi) {
|
|
|
372
471
|
const d = event.details
|
|
373
472
|
const mode = c?.mode ?? ctx?.mode
|
|
374
473
|
if (isActive() && id && mode !== "print" && mode !== "json" && event.toolName?.startsWith(MCP_PREFIX) && d && typeof d.server === "string" && typeof d.tool === "string" && trailer(raw ?? event.content)) {
|
|
375
|
-
// A fresh beacon replaces a pending retry, and a bridge still
|
|
474
|
+
// A fresh beacon replaces a pending retry, and a bridge still
|
|
475
|
+
// starting without one; one still redeeming another beacon (an
|
|
476
|
+
// earlier result of the same turn) carries on, so that beacon's
|
|
477
|
+
// session is merged into this one's rather than lost.
|
|
376
478
|
settle()
|
|
377
|
-
|
|
479
|
+
for (const pending of [...starting]) if (!carrying.has(pending)) stop(pending)
|
|
378
480
|
const note = await start({ hook_event_name: "PostToolUse", session_id: id, call_id: event.toolCallId, tool_name: event.toolName,
|
|
379
481
|
server: d.server, mcp_tool: d.tool, tool_response: { content: raw ?? event.content } })
|
|
380
482
|
if (note) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wakeshift/pi",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.42",
|
|
4
4
|
"description": "Wakeshift's extension for pi: messages from the apps your agent watches through Wakeshift wake your pi sessions, even while they're idle.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://wakeshift.ai/docs/agents",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"node": ">=22"
|
|
32
32
|
},
|
|
33
33
|
"optionalDependencies": {
|
|
34
|
-
"@wakeshift/connect-darwin-arm64": "1.4.
|
|
35
|
-
"@wakeshift/connect-darwin-x64": "1.4.
|
|
36
|
-
"@wakeshift/connect-linux-arm64": "1.4.
|
|
37
|
-
"@wakeshift/connect-linux-x64": "1.4.
|
|
34
|
+
"@wakeshift/connect-darwin-arm64": "1.4.42",
|
|
35
|
+
"@wakeshift/connect-darwin-x64": "1.4.42",
|
|
36
|
+
"@wakeshift/connect-linux-arm64": "1.4.42",
|
|
37
|
+
"@wakeshift/connect-linux-x64": "1.4.42"
|
|
38
38
|
},
|
|
39
39
|
"bugs": {
|
|
40
40
|
"url": "https://github.com/wakeshift/connectors/issues"
|