@oh-my-pi/pi-coding-agent 14.5.12 → 14.5.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/CHANGELOG.md +45 -0
- package/package.json +18 -10
- package/src/cli/jupyter-cli.ts +1 -1
- package/src/commit/pipeline.ts +4 -3
- package/src/config/model-equivalence.ts +49 -16
- package/src/config/model-registry.ts +100 -25
- package/src/config/model-resolver.ts +29 -15
- package/src/config/settings-schema.ts +20 -6
- package/src/config/settings.ts +9 -8
- package/src/config.ts +18 -6
- package/src/eval/backend.ts +43 -0
- package/src/eval/eval.lark +43 -0
- package/src/eval/index.ts +5 -0
- package/src/eval/js/context-manager.ts +717 -0
- package/src/eval/js/executor.ts +131 -0
- package/src/eval/js/index.ts +46 -0
- package/src/eval/js/prelude.ts +2 -0
- package/src/eval/js/prelude.txt +84 -0
- package/src/eval/js/tool-bridge.ts +124 -0
- package/src/eval/parse.ts +337 -0
- package/src/{ipy → eval/py}/executor.ts +2 -180
- package/src/{ipy → eval/py}/gateway-coordinator.ts +2 -2
- package/src/eval/py/index.ts +58 -0
- package/src/{ipy → eval/py}/kernel.ts +9 -45
- package/src/{ipy → eval/py}/prelude.py +39 -227
- package/src/eval/types.ts +48 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +8 -10
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +9 -0
- package/src/lsp/index.ts +395 -0
- package/src/lsp/types.ts +15 -4
- package/src/main.ts +35 -14
- package/src/mcp/manager.ts +22 -0
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/{python-execution.ts → eval-execution.ts} +11 -4
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/tool-execution.ts +3 -4
- package/src/modes/controllers/command-controller.ts +28 -8
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/rpc/rpc-client.ts +9 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/types.ts +3 -3
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/tools/eval.md +92 -0
- package/src/prompts/tools/lsp.md +7 -3
- package/src/sdk.ts +64 -35
- package/src/session/agent-session.ts +152 -46
- package/src/session/messages.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/system-prompt.ts +34 -66
- package/src/task/agents.ts +4 -5
- package/src/task/executor.ts +5 -9
- package/src/tools/archive-reader.ts +9 -3
- package/src/tools/browser/launch.ts +22 -0
- package/src/tools/browser/readable.ts +11 -6
- package/src/tools/browser/registry.ts +25 -244
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/tab-protocol.ts +101 -0
- package/src/tools/browser/tab-supervisor.ts +429 -0
- package/src/tools/browser/tab-worker-entry.ts +21 -0
- package/src/tools/browser/tab-worker.ts +1006 -0
- package/src/tools/browser.ts +17 -32
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/{python.ts → eval.ts} +324 -315
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/image-gen.ts +2 -2
- package/src/tools/index.ts +62 -100
- package/src/tools/read.ts +0 -6
- package/src/tools/recipe/runners/pkg.ts +34 -32
- package/src/tools/renderers.ts +2 -2
- package/src/tools/resolve.ts +7 -2
- package/src/tools/todo-write.ts +0 -1
- package/src/tools/tool-timeouts.ts +2 -2
- package/src/tools/write.ts +8 -1
- package/src/utils/markit.ts +15 -7
- package/src/utils/tools-manager.ts +5 -5
- package/src/web/scrapers/crossref.ts +3 -3
- package/src/web/scrapers/devto.ts +1 -1
- package/src/web/scrapers/discourse.ts +5 -5
- package/src/web/scrapers/firefox-addons.ts +1 -1
- package/src/web/scrapers/flathub.ts +2 -2
- package/src/web/scrapers/gitlab.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/mastodon.ts +9 -9
- package/src/web/scrapers/mdn.ts +11 -7
- package/src/web/scrapers/pub-dev.ts +1 -1
- package/src/web/scrapers/rawg.ts +3 -3
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/spdx.ts +1 -1
- package/src/web/scrapers/stackoverflow.ts +2 -2
- package/src/web/scrapers/types.ts +53 -39
- package/src/web/scrapers/w3c.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/provider.ts +121 -39
- package/src/web/search/providers/gemini.ts +4 -4
- package/src/web/search/render.ts +2 -2
- package/src/ipy/modules.ts +0 -144
- package/src/prompts/tools/python.md +0 -57
- package/src/tools/browser/vm.ts +0 -792
- /package/src/{ipy → eval/py}/cancellation.ts +0 -0
- /package/src/{ipy → eval/py}/prelude.ts +0 -0
- /package/src/{ipy → eval/py}/runtime.ts +0 -0
package/src/lsp/client.ts
CHANGED
|
@@ -153,6 +153,15 @@ const CLIENT_CAPABILITIES = {
|
|
|
153
153
|
valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
|
|
154
154
|
},
|
|
155
155
|
},
|
|
156
|
+
fileOperations: {
|
|
157
|
+
dynamicRegistration: false,
|
|
158
|
+
willCreate: false,
|
|
159
|
+
didCreate: false,
|
|
160
|
+
willRename: true,
|
|
161
|
+
didRename: true,
|
|
162
|
+
willDelete: false,
|
|
163
|
+
didDelete: false,
|
|
164
|
+
},
|
|
156
165
|
},
|
|
157
166
|
experimental: {
|
|
158
167
|
snippetTextEdit: true,
|
package/src/lsp/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type LspServerStatus,
|
|
17
17
|
notifySaved,
|
|
18
18
|
refreshFile,
|
|
19
|
+
sendNotification,
|
|
19
20
|
sendRequest,
|
|
20
21
|
setIdleTimeout,
|
|
21
22
|
syncContent,
|
|
@@ -325,6 +326,61 @@ async function formatLocationWithContext(location: Location, cwd: string): Promi
|
|
|
325
326
|
}
|
|
326
327
|
return `${header}\n${context.map(lineText => ` ${lineText}`).join("\n")}`;
|
|
327
328
|
}
|
|
329
|
+
|
|
330
|
+
const MAX_RENAME_PAIRS = 1000;
|
|
331
|
+
|
|
332
|
+
interface FileRenamePair {
|
|
333
|
+
oldUri: string;
|
|
334
|
+
newUri: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Enumerate the {oldUri, newUri} pairs needed for an LSP willRenameFiles/didRenameFiles request.
|
|
339
|
+
* For files this is a single pair. For directories this walks every regular file underneath
|
|
340
|
+
* and produces a parallel pair anchored at the new directory root.
|
|
341
|
+
*/
|
|
342
|
+
async function enumerateRenamePairs(
|
|
343
|
+
source: string,
|
|
344
|
+
dest: string,
|
|
345
|
+
): Promise<{ pairs: FileRenamePair[]; directory: boolean; exceeded: boolean }> {
|
|
346
|
+
const stat = await fs.promises.stat(source);
|
|
347
|
+
if (!stat.isDirectory()) {
|
|
348
|
+
return {
|
|
349
|
+
pairs: [{ oldUri: fileToUri(source), newUri: fileToUri(dest) }],
|
|
350
|
+
directory: false,
|
|
351
|
+
exceeded: false,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const entries = await fs.promises.readdir(source, { recursive: true, withFileTypes: true });
|
|
355
|
+
const pairs: FileRenamePair[] = [];
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (!entry.isFile()) continue;
|
|
358
|
+
if (pairs.length >= MAX_RENAME_PAIRS) {
|
|
359
|
+
return { pairs, directory: true, exceeded: true };
|
|
360
|
+
}
|
|
361
|
+
const parent = entry.parentPath ?? source;
|
|
362
|
+
const absOld = path.join(parent, entry.name);
|
|
363
|
+
const rel = path.relative(source, absOld);
|
|
364
|
+
pairs.push({
|
|
365
|
+
oldUri: fileToUri(absOld),
|
|
366
|
+
newUri: fileToUri(path.join(dest, rel)),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return { pairs, directory: true, exceeded: false };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** True when an LSP error indicates the server doesn't implement the requested method. */
|
|
373
|
+
function isMethodNotFoundError(err: unknown): boolean {
|
|
374
|
+
if (!(err instanceof Error)) return false;
|
|
375
|
+
const msg = err.message.toLowerCase();
|
|
376
|
+
return (
|
|
377
|
+
msg.includes("method not found") ||
|
|
378
|
+
msg.includes("unhandled method") ||
|
|
379
|
+
msg.includes("not supported") ||
|
|
380
|
+
msg.includes("-32601")
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
328
384
|
async function reloadServer(client: LspClient, serverName: string, signal?: AbortSignal): Promise<string> {
|
|
329
385
|
let output = `Restarted ${serverName}`;
|
|
330
386
|
const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
|
|
@@ -1312,6 +1368,345 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1312
1368
|
};
|
|
1313
1369
|
}
|
|
1314
1370
|
|
|
1371
|
+
if (action === "rename_file") {
|
|
1372
|
+
if (!file || !new_name) {
|
|
1373
|
+
return {
|
|
1374
|
+
content: [
|
|
1375
|
+
{
|
|
1376
|
+
type: "text",
|
|
1377
|
+
text: "Error: rename_file requires both `file` (source path) and `new_name` (destination path)",
|
|
1378
|
+
},
|
|
1379
|
+
],
|
|
1380
|
+
details: { action, success: false, request: params },
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const source = resolveToCwd(file, this.session.cwd);
|
|
1385
|
+
const dest = resolveToCwd(new_name, this.session.cwd);
|
|
1386
|
+
|
|
1387
|
+
if (source === dest) {
|
|
1388
|
+
return {
|
|
1389
|
+
content: [{ type: "text", text: "Error: source and destination paths are identical" }],
|
|
1390
|
+
details: { action, success: false, request: params },
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
let sourceStat: fs.Stats;
|
|
1395
|
+
try {
|
|
1396
|
+
sourceStat = await fs.promises.stat(source);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return {
|
|
1399
|
+
content: [
|
|
1400
|
+
{
|
|
1401
|
+
type: "text",
|
|
1402
|
+
text: `Error: source path does not exist: ${formatPathRelativeToCwd(source, this.session.cwd)}`,
|
|
1403
|
+
},
|
|
1404
|
+
],
|
|
1405
|
+
details: { action, success: false, request: params },
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
let destExists = false;
|
|
1410
|
+
try {
|
|
1411
|
+
await fs.promises.stat(dest);
|
|
1412
|
+
destExists = true;
|
|
1413
|
+
} catch {
|
|
1414
|
+
// expected: destination must not exist
|
|
1415
|
+
}
|
|
1416
|
+
if (destExists) {
|
|
1417
|
+
return {
|
|
1418
|
+
content: [
|
|
1419
|
+
{
|
|
1420
|
+
type: "text",
|
|
1421
|
+
text: `Error: destination already exists: ${formatPathRelativeToCwd(dest, this.session.cwd)}`,
|
|
1422
|
+
},
|
|
1423
|
+
],
|
|
1424
|
+
details: { action, success: false, request: params },
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const enumerated = await enumerateRenamePairs(source, dest);
|
|
1429
|
+
if (enumerated.exceeded) {
|
|
1430
|
+
return {
|
|
1431
|
+
content: [
|
|
1432
|
+
{
|
|
1433
|
+
type: "text",
|
|
1434
|
+
text: `Error: directory contains more than ${MAX_RENAME_PAIRS} files; rename in smaller batches to keep LSP edits accurate`,
|
|
1435
|
+
},
|
|
1436
|
+
],
|
|
1437
|
+
details: { action, success: false, request: params },
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
const { pairs } = enumerated;
|
|
1441
|
+
if (pairs.length === 0) {
|
|
1442
|
+
return {
|
|
1443
|
+
content: [{ type: "text", text: "Error: no files to rename" }],
|
|
1444
|
+
details: { action, success: false, request: params },
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const lspParams = { files: pairs };
|
|
1449
|
+
const servers = getLspServers(config);
|
|
1450
|
+
const respondingServers = new Set<string>();
|
|
1451
|
+
const perServerEdits: Array<{ serverName: string; edit: WorkspaceEdit }> = [];
|
|
1452
|
+
const serverNotes: string[] = [];
|
|
1453
|
+
|
|
1454
|
+
for (const [serverName, serverConfig] of servers) {
|
|
1455
|
+
throwIfAborted(signal);
|
|
1456
|
+
try {
|
|
1457
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1458
|
+
if (isProjectAwareLspServer(serverConfig)) {
|
|
1459
|
+
await waitForProjectLoaded(client, signal);
|
|
1460
|
+
}
|
|
1461
|
+
const result = (await sendRequest(
|
|
1462
|
+
client,
|
|
1463
|
+
"workspace/willRenameFiles",
|
|
1464
|
+
lspParams,
|
|
1465
|
+
signal,
|
|
1466
|
+
)) as WorkspaceEdit | null;
|
|
1467
|
+
respondingServers.add(serverName);
|
|
1468
|
+
if (result && (result.changes || result.documentChanges)) {
|
|
1469
|
+
perServerEdits.push({ serverName, edit: result });
|
|
1470
|
+
}
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1473
|
+
throw err;
|
|
1474
|
+
}
|
|
1475
|
+
if (!isMethodNotFoundError(err)) {
|
|
1476
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1477
|
+
serverNotes.push(` ${serverName}: ${msg}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const sourceLabel = formatPathRelativeToCwd(source, this.session.cwd);
|
|
1483
|
+
const destLabel = formatPathRelativeToCwd(dest, this.session.cwd);
|
|
1484
|
+
const fileCountLabel = sourceStat.isDirectory()
|
|
1485
|
+
? `${pairs.length} file${pairs.length !== 1 ? "s" : ""} under ${sourceLabel}`
|
|
1486
|
+
: sourceLabel;
|
|
1487
|
+
|
|
1488
|
+
const shouldApply = apply !== false;
|
|
1489
|
+
if (!shouldApply) {
|
|
1490
|
+
const lines: string[] = [];
|
|
1491
|
+
lines.push(`Rename preview: ${fileCountLabel} → ${destLabel}`);
|
|
1492
|
+
if (perServerEdits.length === 0) {
|
|
1493
|
+
lines.push(" No LSP edits would be applied");
|
|
1494
|
+
} else {
|
|
1495
|
+
for (const { serverName, edit } of perServerEdits) {
|
|
1496
|
+
const edits = formatWorkspaceEdit(edit, this.session.cwd);
|
|
1497
|
+
if (edits.length === 0) continue;
|
|
1498
|
+
lines.push(` ${serverName}:`);
|
|
1499
|
+
for (const e of edits) {
|
|
1500
|
+
lines.push(` ${e}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (serverNotes.length > 0) {
|
|
1505
|
+
lines.push(" Server notes:");
|
|
1506
|
+
lines.push(...serverNotes);
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1510
|
+
details: {
|
|
1511
|
+
action,
|
|
1512
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
1513
|
+
success: true,
|
|
1514
|
+
request: params,
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const summary: string[] = [];
|
|
1520
|
+
for (const { serverName, edit } of perServerEdits) {
|
|
1521
|
+
const applied = await applyWorkspaceEdit(edit, this.session.cwd);
|
|
1522
|
+
if (applied.length > 0) {
|
|
1523
|
+
summary.push(` ${serverName}:`);
|
|
1524
|
+
summary.push(...applied.map(line => ` ${line}`));
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
1529
|
+
await fs.promises.rename(source, dest);
|
|
1530
|
+
summary.push(` Renamed ${sourceLabel} → ${destLabel}`);
|
|
1531
|
+
|
|
1532
|
+
for (const [serverName, serverConfig] of servers) {
|
|
1533
|
+
try {
|
|
1534
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1535
|
+
for (const { oldUri } of pairs) {
|
|
1536
|
+
if (client.openFiles.has(oldUri)) {
|
|
1537
|
+
await sendNotification(client, "textDocument/didClose", {
|
|
1538
|
+
textDocument: { uri: oldUri },
|
|
1539
|
+
});
|
|
1540
|
+
client.openFiles.delete(oldUri);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
await sendNotification(client, "workspace/didRenameFiles", lspParams);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1546
|
+
throw err;
|
|
1547
|
+
}
|
|
1548
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1549
|
+
serverNotes.push(` ${serverName}: ${msg}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (serverNotes.length > 0) {
|
|
1554
|
+
summary.push(" Server notes:");
|
|
1555
|
+
summary.push(...serverNotes);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const header = `Renamed ${fileCountLabel} → ${destLabel}`;
|
|
1559
|
+
return {
|
|
1560
|
+
content: [{ type: "text", text: `${header}\n${summary.join("\n")}` }],
|
|
1561
|
+
details: {
|
|
1562
|
+
action,
|
|
1563
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
1564
|
+
success: true,
|
|
1565
|
+
request: params,
|
|
1566
|
+
},
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (action === "capabilities") {
|
|
1571
|
+
let serverList: Array<[string, ServerConfig]>;
|
|
1572
|
+
if (file && file !== "*") {
|
|
1573
|
+
const resolved = resolveToCwd(file, this.session.cwd);
|
|
1574
|
+
serverList = getLspServersForFile(config, resolved);
|
|
1575
|
+
if (serverList.length === 0) {
|
|
1576
|
+
return {
|
|
1577
|
+
content: [{ type: "text", text: "No language server found for this file" }],
|
|
1578
|
+
details: { action, success: false, request: params },
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
} else {
|
|
1582
|
+
serverList = getLspServers(config);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (serverList.length === 0) {
|
|
1586
|
+
return {
|
|
1587
|
+
content: [{ type: "text", text: "No language servers configured" }],
|
|
1588
|
+
details: { action, success: false, request: params },
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const sections: string[] = [];
|
|
1593
|
+
const respondingServers = new Set<string>();
|
|
1594
|
+
for (const [serverName, serverConfig] of serverList) {
|
|
1595
|
+
throwIfAborted(signal);
|
|
1596
|
+
try {
|
|
1597
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1598
|
+
respondingServers.add(serverName);
|
|
1599
|
+
const caps = client.serverCapabilities ?? {};
|
|
1600
|
+
sections.push(`${serverName}:`);
|
|
1601
|
+
sections.push(` capabilities: ${JSON.stringify(caps, null, 2).split("\n").join("\n ")}`);
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1604
|
+
throw err;
|
|
1605
|
+
}
|
|
1606
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1607
|
+
sections.push(`${serverName}: failed to start (${msg})`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
return {
|
|
1612
|
+
content: [{ type: "text", text: sections.join("\n") }],
|
|
1613
|
+
details: {
|
|
1614
|
+
action,
|
|
1615
|
+
serverName: Array.from(respondingServers).join(", "),
|
|
1616
|
+
success: true,
|
|
1617
|
+
request: params,
|
|
1618
|
+
},
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
if (action === "request") {
|
|
1623
|
+
const method = query?.trim();
|
|
1624
|
+
if (!method) {
|
|
1625
|
+
return {
|
|
1626
|
+
content: [
|
|
1627
|
+
{
|
|
1628
|
+
type: "text",
|
|
1629
|
+
text: "Error: action=request requires `query` to specify the LSP method name (e.g., 'rust-analyzer/expandMacro')",
|
|
1630
|
+
},
|
|
1631
|
+
],
|
|
1632
|
+
details: { action, success: false, request: params },
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
let chosenServer: [string, ServerConfig] | null = null;
|
|
1637
|
+
let resolvedTarget: string | null = null;
|
|
1638
|
+
if (file && file !== "*") {
|
|
1639
|
+
resolvedTarget = resolveToCwd(file, this.session.cwd);
|
|
1640
|
+
chosenServer = getLspServerForFile(config, resolvedTarget);
|
|
1641
|
+
if (!chosenServer) {
|
|
1642
|
+
return {
|
|
1643
|
+
content: [{ type: "text", text: "No language server found for this file" }],
|
|
1644
|
+
details: { action, success: false, request: params },
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
} else {
|
|
1648
|
+
const all = getLspServers(config);
|
|
1649
|
+
if (all.length === 0) {
|
|
1650
|
+
return {
|
|
1651
|
+
content: [{ type: "text", text: "No language servers configured" }],
|
|
1652
|
+
details: { action, success: false, request: params },
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
chosenServer = all[0];
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const [chosenName, chosenConfig] = chosenServer;
|
|
1659
|
+
let requestParams: unknown;
|
|
1660
|
+
if (params.payload !== undefined) {
|
|
1661
|
+
try {
|
|
1662
|
+
requestParams = JSON.parse(params.payload);
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1665
|
+
return {
|
|
1666
|
+
content: [{ type: "text", text: `Error: invalid JSON in payload: ${msg}` }],
|
|
1667
|
+
details: { action, serverName: chosenName, success: false, request: params },
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
} else if (resolvedTarget) {
|
|
1671
|
+
const uri = fileToUri(resolvedTarget);
|
|
1672
|
+
if (line !== undefined) {
|
|
1673
|
+
const character = await resolveSymbolColumn(resolvedTarget, line, symbol);
|
|
1674
|
+
requestParams = { textDocument: { uri }, position: { line: line - 1, character } };
|
|
1675
|
+
} else {
|
|
1676
|
+
requestParams = { textDocument: { uri } };
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
requestParams = {};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
try {
|
|
1683
|
+
const client = await getOrCreateClient(chosenConfig, this.session.cwd);
|
|
1684
|
+
if (resolvedTarget) {
|
|
1685
|
+
await ensureFileOpen(client, resolvedTarget, signal);
|
|
1686
|
+
}
|
|
1687
|
+
const result = await sendRequest(client, method, requestParams, signal);
|
|
1688
|
+
const formatted =
|
|
1689
|
+
result === null || result === undefined
|
|
1690
|
+
? "null"
|
|
1691
|
+
: typeof result === "string"
|
|
1692
|
+
? result
|
|
1693
|
+
: JSON.stringify(result, null, 2);
|
|
1694
|
+
return {
|
|
1695
|
+
content: [{ type: "text", text: `${chosenName} ← ${method}:\n${formatted}` }],
|
|
1696
|
+
details: { action, serverName: chosenName, success: true, request: params },
|
|
1697
|
+
};
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
if (err instanceof ToolAbortError || signal?.aborted) {
|
|
1700
|
+
throw new ToolAbortError();
|
|
1701
|
+
}
|
|
1702
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1703
|
+
return {
|
|
1704
|
+
content: [{ type: "text", text: `LSP error from ${chosenName} on ${method}: ${msg}` }],
|
|
1705
|
+
details: { action, serverName: chosenName, success: false, request: params },
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1315
1710
|
// `*` means workspace scope for symbols/reload; other actions need a concrete file.
|
|
1316
1711
|
const isWorkspace = file === "*";
|
|
1317
1712
|
const requiresFile = !file && action !== "reload";
|
package/src/lsp/types.ts
CHANGED
|
@@ -15,21 +15,32 @@ export const lspSchema = Type.Object({
|
|
|
15
15
|
"hover",
|
|
16
16
|
"symbols",
|
|
17
17
|
"rename",
|
|
18
|
+
"rename_file",
|
|
18
19
|
"code_actions",
|
|
19
20
|
"type_definition",
|
|
20
21
|
"implementation",
|
|
21
22
|
"status",
|
|
22
23
|
"reload",
|
|
24
|
+
"capabilities",
|
|
25
|
+
"request",
|
|
23
26
|
],
|
|
24
27
|
{ description: "LSP operation" },
|
|
25
28
|
),
|
|
26
|
-
file: Type.Optional(Type.String({ description: "File path" })),
|
|
29
|
+
file: Type.Optional(Type.String({ description: "File path or source path for rename_file" })),
|
|
27
30
|
line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })),
|
|
28
31
|
symbol: Type.Optional(Type.String({ description: "Symbol/substring to locate on the line" })),
|
|
29
|
-
query: Type.Optional(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
query: Type.Optional(
|
|
33
|
+
Type.String({ description: "Search query, code-action selector, or LSP method name for action=request" }),
|
|
34
|
+
),
|
|
35
|
+
new_name: Type.Optional(Type.String({ description: "New name for rename, or destination path for rename_file" })),
|
|
36
|
+
apply: Type.Optional(Type.Boolean({ description: "Apply edits (default: true for rename/rename_file)" })),
|
|
32
37
|
timeout: Type.Optional(Type.Number({ description: "Request timeout in seconds" })),
|
|
38
|
+
payload: Type.Optional(
|
|
39
|
+
Type.String({
|
|
40
|
+
description:
|
|
41
|
+
"JSON-encoded params for action=request. When omitted, params are auto-built from file/line/symbol.",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
33
44
|
});
|
|
34
45
|
|
|
35
46
|
export type LspParams = Static<typeof lspSchema>;
|
package/src/main.ts
CHANGED
|
@@ -242,18 +242,25 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
const lastVersion = settings.get("lastChangelogVersion");
|
|
245
|
+
if (lastVersion === VERSION) {
|
|
246
|
+
// Steady state: user already saw the current version's changelog. Skip the file read + parse.
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
245
250
|
const changelogPath = getChangelogPath();
|
|
246
251
|
const entries = await parseChangelog(changelogPath);
|
|
247
252
|
|
|
248
253
|
if (!lastVersion) {
|
|
249
254
|
if (entries.length > 0) {
|
|
250
255
|
settings.set("lastChangelogVersion", VERSION);
|
|
256
|
+
await flushChangelogVersion();
|
|
251
257
|
return entries.map(e => e.content).join("\n\n");
|
|
252
258
|
}
|
|
253
259
|
} else {
|
|
254
260
|
const newEntries = getNewEntries(entries, lastVersion);
|
|
255
261
|
if (newEntries.length > 0) {
|
|
256
262
|
settings.set("lastChangelogVersion", VERSION);
|
|
263
|
+
await flushChangelogVersion();
|
|
257
264
|
return newEntries.map(e => e.content).join("\n\n");
|
|
258
265
|
}
|
|
259
266
|
}
|
|
@@ -261,6 +268,14 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
261
268
|
return undefined;
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
async function flushChangelogVersion(): Promise<void> {
|
|
272
|
+
try {
|
|
273
|
+
await settings.flush();
|
|
274
|
+
} catch (error: unknown) {
|
|
275
|
+
logger.warn("Failed to persist lastChangelogVersion", { error });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
264
279
|
async function createSessionManager(parsed: Args, cwd: string): Promise<SessionManager | undefined> {
|
|
265
280
|
if (parsed.fork) {
|
|
266
281
|
if (parsed.noSession) {
|
|
@@ -615,8 +630,19 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
615
630
|
process.exit(1);
|
|
616
631
|
}
|
|
617
632
|
|
|
633
|
+
// Kick off plugin-root preload in parallel with the remaining startup work.
|
|
634
|
+
// Awaited later (before extension/skill discovery in createAgentSession needs it).
|
|
635
|
+
const home = os.homedir();
|
|
636
|
+
const pluginPreloadPromise =
|
|
637
|
+
parsedArgs.pluginDirs && parsedArgs.pluginDirs.length > 0
|
|
638
|
+
? logger.time("injectPluginDirRoots", injectPluginDirRoots, home, parsedArgs.pluginDirs, getProjectDir())
|
|
639
|
+
: logger.time("preloadPluginRoots", preloadPluginRoots, home, getProjectDir());
|
|
640
|
+
// Mark the promise as handled so a synchronous failure does not surface as an unhandled-rejection
|
|
641
|
+
// warning before we reach the await site below.
|
|
642
|
+
pluginPreloadPromise.catch(() => {});
|
|
643
|
+
|
|
618
644
|
const cwd = getProjectDir();
|
|
619
|
-
await logger.time("settings:init", Settings.init, { cwd });
|
|
645
|
+
const settingsInstance = await logger.time("settings:init", Settings.init, { cwd });
|
|
620
646
|
if (parsedArgs.mode === "rpc") {
|
|
621
647
|
applyRpcDefaultSettingOverrides();
|
|
622
648
|
}
|
|
@@ -647,9 +673,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
647
673
|
const mode = parsedArgs.mode || "text";
|
|
648
674
|
|
|
649
675
|
// Initialize discovery system with settings for provider persistence
|
|
650
|
-
logger.time("initializeWithSettings");
|
|
651
|
-
initializeWithSettings(settings);
|
|
652
|
-
modelRegistry.refreshInBackground();
|
|
676
|
+
logger.time("initializeWithSettings", initializeWithSettings, settings);
|
|
653
677
|
|
|
654
678
|
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
655
679
|
const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
|
|
@@ -706,13 +730,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
706
730
|
sessionManager = await SessionManager.open(selectedPath);
|
|
707
731
|
}
|
|
708
732
|
|
|
709
|
-
|
|
710
|
-
const home = os.homedir();
|
|
711
|
-
if (parsedArgs.pluginDirs && parsedArgs.pluginDirs.length > 0) {
|
|
712
|
-
await logger.time("injectPluginDirRoots", injectPluginDirRoots, home, parsedArgs.pluginDirs!, getProjectDir());
|
|
713
|
-
} else {
|
|
714
|
-
await logger.time("preloadPluginRoots", preloadPluginRoots, home, getProjectDir());
|
|
715
|
-
}
|
|
733
|
+
await pluginPreloadPromise;
|
|
716
734
|
|
|
717
735
|
// Background marketplace auto-update — never blocks startup.
|
|
718
736
|
const autoUpdate = settings.get("marketplace.autoUpdate");
|
|
@@ -759,6 +777,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
759
777
|
sessionOptions.authStorage = authStorage;
|
|
760
778
|
sessionOptions.modelRegistry = modelRegistry;
|
|
761
779
|
sessionOptions.hasUI = isInteractive;
|
|
780
|
+
sessionOptions.settings = settingsInstance;
|
|
762
781
|
|
|
763
782
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
764
783
|
if (parsedArgs.apiKey) {
|
|
@@ -778,7 +797,10 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
778
797
|
createAgentSession,
|
|
779
798
|
sessionOptions,
|
|
780
799
|
);
|
|
781
|
-
|
|
800
|
+
// Kick off background model discovery only after createAgentSession finishes its parallel
|
|
801
|
+
// discovery arms; running these concurrently contends for the event loop and stretches
|
|
802
|
+
// every parallel arm by ~30ms.
|
|
803
|
+
modelRegistry.refreshInBackground();
|
|
782
804
|
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
|
|
783
805
|
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
|
784
806
|
}
|
|
@@ -856,8 +878,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
856
878
|
await runAcpMode(session, createAcpSession);
|
|
857
879
|
} else if (isInteractive) {
|
|
858
880
|
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
|
|
859
|
-
logger.time("main:getChangelogForDisplay");
|
|
860
|
-
const changelogMarkdown = await getChangelogForDisplay(parsedArgs);
|
|
881
|
+
const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
|
|
861
882
|
|
|
862
883
|
const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
|
|
863
884
|
if (scopedModelsForDisplay.length > 0) {
|
package/src/mcp/manager.ts
CHANGED
|
@@ -78,6 +78,21 @@ function delay(ms: number): Promise<void> {
|
|
|
78
78
|
return Bun.sleep(ms);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Stable, total ordering on MCP tools by name.
|
|
83
|
+
*
|
|
84
|
+
* Anthropic prompt caching keys on byte-identical tool definitions: any reorder
|
|
85
|
+
* of the tools array invalidates the tools cache breakpoint and forces a full
|
|
86
|
+
* prefix rebuild on the next request. MCP servers connect/reconnect at arbitrary
|
|
87
|
+
* times, so the natural "insertion order" of `#tools` is non-deterministic.
|
|
88
|
+
* Sorting after every mutation makes the array bytes independent of connection
|
|
89
|
+
* sequence.
|
|
90
|
+
*/
|
|
91
|
+
export function sortMCPToolsByName<T extends { name: string }>(tools: T[]): T[] {
|
|
92
|
+
tools.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
93
|
+
return tools;
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
export function resolveSubscriptionPostAction(
|
|
82
97
|
notificationsEnabled: boolean,
|
|
83
98
|
currentEpoch: number,
|
|
@@ -459,6 +474,10 @@ export class MCPManager {
|
|
|
459
474
|
}
|
|
460
475
|
}
|
|
461
476
|
|
|
477
|
+
// Stable sort by name so the order is independent of connection completion.
|
|
478
|
+
// See `sortMCPToolsByName` for the cache-stability rationale.
|
|
479
|
+
sortMCPToolsByName(allTools);
|
|
480
|
+
|
|
462
481
|
// Update cached tools
|
|
463
482
|
this.#tools = allTools;
|
|
464
483
|
allowBackgroundLogging = true;
|
|
@@ -474,6 +493,9 @@ export class MCPManager {
|
|
|
474
493
|
#replaceServerTools(name: string, tools: CustomTool<TSchema, MCPToolDetails>[]): void {
|
|
475
494
|
this.#tools = this.#tools.filter(t => !t.name.startsWith(`mcp__${name}_`));
|
|
476
495
|
this.#tools.push(...tools);
|
|
496
|
+
// Stable sort by name so reconnect order does not perturb the array.
|
|
497
|
+
// See `sortMCPToolsByName` for the cache-stability rationale.
|
|
498
|
+
sortMCPToolsByName(this.#tools);
|
|
477
499
|
}
|
|
478
500
|
|
|
479
501
|
#triggerNotificationRefresh(serverName: string, kind: "tools" | "resources" | "prompts"): void {
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* by providing authorization URL, token URL, and client credentials.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { OAuthController, OAuthCredentials } from "@oh-my-pi/pi-ai";
|
|
9
8
|
import type { OAuthCallbackFlowOptions } from "@oh-my-pi/pi-ai/utils/oauth/callback-server";
|
|
10
9
|
import { OAuthCallbackFlow } from "@oh-my-pi/pi-ai/utils/oauth/callback-server";
|
|
10
|
+
import type { OAuthController, OAuthCredentials } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_PORT = 3000;
|
|
13
13
|
const CALLBACK_PATH = "/callback";
|
package/src/memories/index.ts
CHANGED
|
@@ -544,7 +544,7 @@ function shouldPersistResponseItemForMemories(message: AgentMessage): boolean {
|
|
|
544
544
|
}
|
|
545
545
|
if (role !== "toolResult") return false;
|
|
546
546
|
const toolName = (message as { toolName?: string }).toolName;
|
|
547
|
-
if (toolName === "bash" || toolName === "
|
|
547
|
+
if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "search") {
|
|
548
548
|
const text = extractMessageText(message);
|
|
549
549
|
return text.length > 0 && text.length <= 32_000;
|
|
550
550
|
}
|