@skydiveai/pi-extensions 0.1.253-beta.6 → 0.1.253-beta.8
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/dist/index.mjs +138 -13
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1169,7 +1169,58 @@ function estimateToolTokens(tool) {
|
|
|
1169
1169
|
return Math.ceil(chars / 4);
|
|
1170
1170
|
}
|
|
1171
1171
|
//#endregion
|
|
1172
|
-
//#region src/extensions/mcp/
|
|
1172
|
+
//#region src/extensions/mcp/legacy-names.ts
|
|
1173
|
+
/**
|
|
1174
|
+
* Rewrites `mcp_<server>_<tool>` names in replayed history to the current
|
|
1175
|
+
* `mcp__<server>__<tool>` registration. Runs on pi's `context` hook, which
|
|
1176
|
+
* hands us a per-call clone that is never written back to the session file,
|
|
1177
|
+
* so the stored history keeps the names the model actually emitted while the
|
|
1178
|
+
* model only ever sees the live shape (and so does not mimic the old one).
|
|
1179
|
+
*
|
|
1180
|
+
* Returns the input array untouched when nothing matched. `renamed` lists
|
|
1181
|
+
* the legacy names that were mapped, for one-shot logging by the caller.
|
|
1182
|
+
*/
|
|
1183
|
+
function rewriteLegacyMcpToolNames({ messages, legacyNames }) {
|
|
1184
|
+
const renamed = /* @__PURE__ */ new Set();
|
|
1185
|
+
if (legacyNames.size === 0) return {
|
|
1186
|
+
messages,
|
|
1187
|
+
renamed
|
|
1188
|
+
};
|
|
1189
|
+
const out = messages.map((message) => {
|
|
1190
|
+
if (message.role === "assistant") {
|
|
1191
|
+
let changed = false;
|
|
1192
|
+
const content = message.content.map((block) => {
|
|
1193
|
+
if (block.type !== "toolCall") return block;
|
|
1194
|
+
const current = legacyNames.get(block.name);
|
|
1195
|
+
if (current === void 0) return block;
|
|
1196
|
+
renamed.add(block.name);
|
|
1197
|
+
changed = true;
|
|
1198
|
+
return {
|
|
1199
|
+
...block,
|
|
1200
|
+
name: current
|
|
1201
|
+
};
|
|
1202
|
+
});
|
|
1203
|
+
return changed ? {
|
|
1204
|
+
...message,
|
|
1205
|
+
content
|
|
1206
|
+
} : message;
|
|
1207
|
+
}
|
|
1208
|
+
if (message.role === "toolResult") {
|
|
1209
|
+
const current = legacyNames.get(message.toolName);
|
|
1210
|
+
if (current === void 0) return message;
|
|
1211
|
+
renamed.add(message.toolName);
|
|
1212
|
+
return {
|
|
1213
|
+
...message,
|
|
1214
|
+
toolName: current
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
return message;
|
|
1218
|
+
});
|
|
1219
|
+
return {
|
|
1220
|
+
messages: renamed.size > 0 ? out : messages,
|
|
1221
|
+
renamed
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1173
1224
|
const MAX_LENGTH = 128;
|
|
1174
1225
|
/**
|
|
1175
1226
|
* Deterministically coerce `raw` into the Anthropic tool-name alphabet.
|
|
@@ -1185,17 +1236,46 @@ function sanitizeToolName(raw) {
|
|
|
1185
1236
|
return stripped === "" ? "tool" : stripped;
|
|
1186
1237
|
}
|
|
1187
1238
|
/**
|
|
1188
|
-
* First free `name`, `name_2`, `name_3`, ... within
|
|
1189
|
-
*
|
|
1239
|
+
* First free `name`, `name_2`, `name_3`, ... within `maxLength`. Suffixes
|
|
1240
|
+
* come out of the kept base so the whole string stays <= `maxLength`; the
|
|
1241
|
+
* caller passes the budget its names were built to (64 for MCP names, 128
|
|
1242
|
+
* for the Anthropic-wide limit) so a collision cannot push a name past it.
|
|
1190
1243
|
*/
|
|
1191
|
-
function uniqueSanitizedToolName(base, used) {
|
|
1244
|
+
function uniqueSanitizedToolName({ base, used, maxLength }) {
|
|
1192
1245
|
if (!used.has(base)) return base;
|
|
1193
1246
|
for (let n = 2;; n++) {
|
|
1194
1247
|
const suffix = `_${n}`;
|
|
1195
|
-
const candidate = base.slice(0,
|
|
1248
|
+
const candidate = base.slice(0, maxLength - suffix.length) + suffix;
|
|
1196
1249
|
if (!used.has(candidate)) return candidate;
|
|
1197
1250
|
}
|
|
1198
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* MCP tools register under Claude Code's `mcp__<server>__<tool>` shape: the
|
|
1254
|
+
* double-underscore separators are what Anthropic's models were trained on,
|
|
1255
|
+
* and they make the name splittable again (`mcp_stripe_stripe_planner` cannot
|
|
1256
|
+
* be split; `mcp__stripe__stripe_planner` can). Cap is 64, not Anthropic's
|
|
1257
|
+
* 128: `tool-history.ts` hashes replayed names over 64 and OpenAI-compatible
|
|
1258
|
+
* providers enforce 64, so a longer name would be unusable on replay anyway.
|
|
1259
|
+
*/
|
|
1260
|
+
const MCP_TOOL_PREFIX = "mcp__";
|
|
1261
|
+
const MCP_SEPARATOR = "__";
|
|
1262
|
+
const MCP_SERVER_SEGMENT_MAX_LENGTH = 32;
|
|
1263
|
+
function sanitizeServerSegment(serverName) {
|
|
1264
|
+
return sanitizeToolName(serverName).replace(/_{2,}/g, "_").slice(0, MCP_SERVER_SEGMENT_MAX_LENGTH).replace(/_+$/, "");
|
|
1265
|
+
}
|
|
1266
|
+
function buildMcpToolName({ serverName, toolName }) {
|
|
1267
|
+
const head = `${MCP_TOOL_PREFIX}${sanitizeServerSegment(serverName)}${MCP_SEPARATOR}`;
|
|
1268
|
+
const tool = sanitizeToolName(toolName).slice(0, 64 - head.length).replace(/_+$/, "");
|
|
1269
|
+
return `${head}${tool === "" ? "tool" : tool}`;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* The pre-2026-10 registered shape (`mcp_<server>_<tool>`, sanitized, no
|
|
1273
|
+
* collision suffix). Only used to map names in replayed history onto the
|
|
1274
|
+
* current registration; delete with the compat window.
|
|
1275
|
+
*/
|
|
1276
|
+
function legacyMcpToolName({ serverName, toolName }) {
|
|
1277
|
+
return sanitizeToolName(`mcp_${serverName}_${toolName}`);
|
|
1278
|
+
}
|
|
1199
1279
|
//#endregion
|
|
1200
1280
|
//#region src/extensions/mcp/mcp-config.ts
|
|
1201
1281
|
/**
|
|
@@ -1296,16 +1376,23 @@ async function readConfigMtimeMs(configPath) {
|
|
|
1296
1376
|
}
|
|
1297
1377
|
}
|
|
1298
1378
|
/**
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1301
|
-
* servers emit names that fail it (e.g. granola's
|
|
1302
|
-
* (v2)`).
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1379
|
+
* Registered as Claude Code's `mcp__<server>__<tool>` (see tool-name.ts), in
|
|
1380
|
+
* the Anthropic tools[].name alphabet or the WHOLE Messages request hard-400s
|
|
1381
|
+
* — real servers emit names that fail it (e.g. granola's
|
|
1382
|
+
* `granola.meetings.list (v2)`). Execution still calls the server with the
|
|
1383
|
+
* ORIGINAL name, and model tool_use round-trips on the registered one, so no
|
|
1384
|
+
* reverse mapping is needed. `used` dedupes collisions (two distinct raw
|
|
1385
|
+
* names can sanitize to the same string).
|
|
1306
1386
|
*/
|
|
1307
1387
|
function makeToolName(serverId, toolName, used) {
|
|
1308
|
-
return uniqueSanitizedToolName(
|
|
1388
|
+
return uniqueSanitizedToolName({
|
|
1389
|
+
base: buildMcpToolName({
|
|
1390
|
+
serverName: serverId,
|
|
1391
|
+
toolName
|
|
1392
|
+
}),
|
|
1393
|
+
used,
|
|
1394
|
+
maxLength: 64
|
|
1395
|
+
});
|
|
1309
1396
|
}
|
|
1310
1397
|
function toMcpArguments(params) {
|
|
1311
1398
|
const out = {};
|
|
@@ -1410,6 +1497,9 @@ var McpExtension = class {
|
|
|
1410
1497
|
mcpClients = /* @__PURE__ */ new Map();
|
|
1411
1498
|
registeredMcpToolNames = /* @__PURE__ */ new Set();
|
|
1412
1499
|
usedSanitizedToolNames = /* @__PURE__ */ new Set();
|
|
1500
|
+
legacyToolNames = /* @__PURE__ */ new Map();
|
|
1501
|
+
usedLegacyToolNames = /* @__PURE__ */ new Set();
|
|
1502
|
+
loggedLegacyToolNames = /* @__PURE__ */ new Set();
|
|
1413
1503
|
lastConfigMtimeMs = 0;
|
|
1414
1504
|
pendingMcpUpdate = null;
|
|
1415
1505
|
/** Non-consuming peek used by `shouldStopAfterTurn`. */
|
|
@@ -1468,6 +1558,23 @@ var McpExtension = class {
|
|
|
1468
1558
|
}));
|
|
1469
1559
|
this.registeredMcpToolNames.add(name);
|
|
1470
1560
|
this.usedSanitizedToolNames.add(name);
|
|
1561
|
+
this.recordLegacyToolName({
|
|
1562
|
+
serverId,
|
|
1563
|
+
toolName: tool.name,
|
|
1564
|
+
name
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
recordLegacyToolName({ serverId, toolName, name }) {
|
|
1568
|
+
const legacy = uniqueSanitizedToolName({
|
|
1569
|
+
base: legacyMcpToolName({
|
|
1570
|
+
serverName: serverId,
|
|
1571
|
+
toolName
|
|
1572
|
+
}),
|
|
1573
|
+
used: this.usedLegacyToolNames,
|
|
1574
|
+
maxLength: 128
|
|
1575
|
+
});
|
|
1576
|
+
this.usedLegacyToolNames.add(legacy);
|
|
1577
|
+
this.legacyToolNames.set(legacy, name);
|
|
1471
1578
|
}
|
|
1472
1579
|
async reconcile({ pi, configPath, connectTimeoutMs, contextWindow }) {
|
|
1473
1580
|
let config;
|
|
@@ -1494,6 +1601,8 @@ var McpExtension = class {
|
|
|
1494
1601
|
const desiredIds = new Set(Object.keys(config.servers));
|
|
1495
1602
|
this.registeredMcpToolNames.clear();
|
|
1496
1603
|
this.usedSanitizedToolNames.clear();
|
|
1604
|
+
this.legacyToolNames.clear();
|
|
1605
|
+
this.usedLegacyToolNames.clear();
|
|
1497
1606
|
for (const [id, existing] of [...this.mcpClients.entries()]) if (!desiredIds.has(id)) {
|
|
1498
1607
|
await closeConnected(existing);
|
|
1499
1608
|
this.mcpClients.delete(id);
|
|
@@ -1852,6 +1961,22 @@ var McpExtension = class {
|
|
|
1852
1961
|
}, "MCP reconcile failed");
|
|
1853
1962
|
}
|
|
1854
1963
|
});
|
|
1964
|
+
pi.on("context", (event) => {
|
|
1965
|
+
const { messages, renamed } = rewriteLegacyMcpToolNames({
|
|
1966
|
+
messages: event.messages,
|
|
1967
|
+
legacyNames: this.legacyToolNames
|
|
1968
|
+
});
|
|
1969
|
+
for (const legacy of renamed) {
|
|
1970
|
+
if (this.loggedLegacyToolNames.has(legacy)) continue;
|
|
1971
|
+
this.loggedLegacyToolNames.add(legacy);
|
|
1972
|
+
log$13.info({
|
|
1973
|
+
event: "mcp_tool_legacy_name",
|
|
1974
|
+
legacy_name: legacy,
|
|
1975
|
+
tool_name: this.legacyToolNames.get(legacy)
|
|
1976
|
+
}, "rewrote legacy MCP tool name in replayed history");
|
|
1977
|
+
}
|
|
1978
|
+
return { messages };
|
|
1979
|
+
});
|
|
1855
1980
|
pi.on("tool_result", async (_event, ctx) => {
|
|
1856
1981
|
const configPath = join(ctx.cwd, MCP_CONFIG_FILENAME);
|
|
1857
1982
|
let mtime;
|