research-copilot 0.2.0 → 0.2.1

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.
Files changed (67) hide show
  1. package/app/out/main/index.mjs +751 -259
  2. package/app/out/preload/index.js +5 -0
  3. package/app/out/renderer/assets/{MilkdownMarkdownEditor-Czh2N6UQ.js → MilkdownMarkdownEditor-D7GYpVZn.js} +50 -50
  4. package/app/out/renderer/assets/{arc-BWoErJNa.js → arc-Kp4J_Jd7.js} +1 -1
  5. package/app/out/renderer/assets/{blockDiagram-c4efeb88-Bod-vAlS.js → blockDiagram-c4efeb88-DkMSdn8j.js} +8 -8
  6. package/app/out/renderer/assets/{c4Diagram-c83219d4-CTVUA_li.js → c4Diagram-c83219d4-DqAGxrYw.js} +3 -3
  7. package/app/out/renderer/assets/{channel-CxGr5Q5E.js → channel-S4GQrISQ.js} +1 -1
  8. package/app/out/renderer/assets/{classDiagram-beda092f-DABwUrsU.js → classDiagram-beda092f-B7AsTCEg.js} +6 -6
  9. package/app/out/renderer/assets/{classDiagram-v2-2358418a-CFt8hqf5.js → classDiagram-v2-2358418a-B4oFy-In.js} +10 -10
  10. package/app/out/renderer/assets/{clone-BL91dKYn.js → clone-Dv1e6zYr.js} +1 -1
  11. package/app/out/renderer/assets/{createText-1719965b-DGkv4rEO.js → createText-1719965b-HBXHvWlI.js} +2 -2
  12. package/app/out/renderer/assets/{edges-96097737-Gf41lQOd.js → edges-96097737-B6X5lcC0.js} +3 -3
  13. package/app/out/renderer/assets/{erDiagram-0228fc6a-Dj75BiRy.js → erDiagram-0228fc6a-BmBmTBlH.js} +5 -5
  14. package/app/out/renderer/assets/{flowDb-c6c81e3f-C_xVBMxS.js → flowDb-c6c81e3f-CObz36ob.js} +1 -1
  15. package/app/out/renderer/assets/{flowDiagram-50d868cf-B-lLn2XC.js → flowDiagram-50d868cf-C2hFHxwF.js} +12 -12
  16. package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-BFnLU3PE.js → flowDiagram-v2-4f6560a1-DEe8EygW.js} +12 -12
  17. package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-DmjfyXbt.js → flowchart-elk-definition-6af322e1-CgTtfYKk.js} +6 -6
  18. package/app/out/renderer/assets/{ganttDiagram-a2739b55-BTPRekAy.js → ganttDiagram-a2739b55-C5Pq4zEy.js} +3 -3
  19. package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-1riYxgGS.js → gitGraphDiagram-82fe8481-oLp0f8Ll.js} +2 -2
  20. package/app/out/renderer/assets/{graph-CvDtMlX-.js → graph-51iZ6wgR.js} +1 -1
  21. package/app/out/renderer/assets/{index-0kPJXDfu.js → index-32eUzqVW.js} +3 -3
  22. package/app/out/renderer/assets/{index-5325376f-BGaoNMNN.js → index-5325376f-yLvOW-Os.js} +6 -6
  23. package/app/out/renderer/assets/{index-O3gvL3-Z.js → index-AuZa-hTj.js} +3 -3
  24. package/app/out/renderer/assets/{index-BxOmAXUZ.js → index-B9a4DKM-.js} +3 -3
  25. package/app/out/renderer/assets/{index-NHbUPOmb.js → index-BMsuFGn6.js} +3 -3
  26. package/app/out/renderer/assets/{index-BnRwUKpv.js → index-BQA_Kvr6.js} +3 -3
  27. package/app/out/renderer/assets/{index-y5XZ-0EB.js → index-BSd80-j9.js} +4 -4
  28. package/app/out/renderer/assets/{index-zr8uxb8p.js → index-BfWWn8B_.js} +6 -6
  29. package/app/out/renderer/assets/{index-3LdRym1K.js → index-Bscx_5dF.js} +3 -3
  30. package/app/out/renderer/assets/{index-BgSz3yUy.js → index-CAOQIqEc.js} +6 -6
  31. package/app/out/renderer/assets/{index-BCOrnr8q.js → index-CTmGCKqa.js} +4 -4
  32. package/app/out/renderer/assets/{index-BK5rYWMs.js → index-CmpSV9Ld.js} +5 -5
  33. package/app/out/renderer/assets/{index-cAZJ88Np.js → index-Cn2e13ja.js} +6 -6
  34. package/app/out/renderer/assets/{index-CnL9yPzK.js → index-D_Y7v6pE.js} +3 -3
  35. package/app/out/renderer/assets/{index-BVNrdWzl.js → index-DjqJjt6u.js} +6 -6
  36. package/app/out/renderer/assets/{index-8tvmsRje.js → index-DppxBL77.js} +3 -3
  37. package/app/out/renderer/assets/{index-CUPy7R5v.js → index-Du-Z3sl4.js} +915 -72
  38. package/app/out/renderer/assets/{index-B4djqBxS.js → index-FGsCVYSr.js} +1 -1
  39. package/app/out/renderer/assets/{index-Ctwkk-AW.css → index-L4DJn7cw.css} +12 -8
  40. package/app/out/renderer/assets/{index-DrvR7Peq.js → index-UajPJYNV.js} +3 -3
  41. package/app/out/renderer/assets/{index-CXN1f9OT.js → index-_Z53hJps.js} +3 -3
  42. package/app/out/renderer/assets/{index-D2fFfHUR.js → index-_iFRQTkA.js} +6 -6
  43. package/app/out/renderer/assets/{index-B9lieynj.js → index-ohN9yRWw.js} +6 -6
  44. package/app/out/renderer/assets/{index-Bii7x9Rr.js → index-shoMWskw.js} +3 -3
  45. package/app/out/renderer/assets/{index-qS7qbXvX.js → index-y1Od1ed6.js} +3 -3
  46. package/app/out/renderer/assets/{infoDiagram-8eee0895-Cq8aXV8u.js → infoDiagram-8eee0895-Cm0Hm5ZX.js} +2 -2
  47. package/app/out/renderer/assets/{journeyDiagram-c64418c1-D4ewDrYD.js → journeyDiagram-c64418c1-A2Gw9bVu.js} +4 -4
  48. package/app/out/renderer/assets/{layout-CZmLZO9t.js → layout-C5N2nTfF.js} +2 -2
  49. package/app/out/renderer/assets/{line-D7kWOiRx.js → line-Dn6BEQAK.js} +1 -1
  50. package/app/out/renderer/assets/{linear-B055Dz0c.js → linear-8wk0rPUX.js} +1 -1
  51. package/app/out/renderer/assets/{mindmap-definition-8da855dc-D6EW4QCj.js → mindmap-definition-8da855dc-BVy6ISnb.js} +3 -3
  52. package/app/out/renderer/assets/{pieDiagram-a8764435-BX_Dz4T9.js → pieDiagram-a8764435-B9_axIHE.js} +3 -3
  53. package/app/out/renderer/assets/{quadrantDiagram-1e28029f-BsI6xGsm.js → quadrantDiagram-1e28029f-B1kmkDFg.js} +3 -3
  54. package/app/out/renderer/assets/{requirementDiagram-08caed73-c2d8T0BS.js → requirementDiagram-08caed73-C_bNWUtT.js} +5 -5
  55. package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-CkDhRKRC.js → sankeyDiagram-a04cb91d-CD2h1LiI.js} +2 -2
  56. package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-DS0RKYnD.js → sequenceDiagram-c5b8d532-B6d6cuqi.js} +3 -3
  57. package/app/out/renderer/assets/{stateDiagram-1ecb1508-BjTK27QX.js → stateDiagram-1ecb1508-CkuNj_3H.js} +6 -6
  58. package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-D1wWbeR3.js → stateDiagram-v2-c2b004d7-CevZ3tno.js} +10 -10
  59. package/app/out/renderer/assets/{styles-b4e223ce-DXUfbXTM.js → styles-b4e223ce-DAe5WQrg.js} +1 -1
  60. package/app/out/renderer/assets/{styles-ca3715f6-CE_JRTmB.js → styles-ca3715f6-BDSX88bY.js} +1 -1
  61. package/app/out/renderer/assets/{styles-d45a18b0-CdtAXXSE.js → styles-d45a18b0-SE9h7les.js} +4 -4
  62. package/app/out/renderer/assets/{svgDrawCommon-b86b1483-dCxPWgBl.js → svgDrawCommon-b86b1483-D1mpNbDQ.js} +1 -1
  63. package/app/out/renderer/assets/{timeline-definition-faaaa080-B7ZP3Dqw.js → timeline-definition-faaaa080-7Ha-nm4M.js} +3 -3
  64. package/app/out/renderer/assets/{xychartDiagram-f5964ef8-CXagmo1Q.js → xychartDiagram-f5964ef8-DLy7iyZW.js} +5 -5
  65. package/app/out/renderer/index.html +2 -2
  66. package/app/package.json +1 -1
  67. package/package.json +1 -1
@@ -1,20 +1,22 @@
1
1
  import { app, shell, ipcMain, BrowserWindow, dialog, Menu } from "electron";
2
+ import { setMaxListeners } from "node:events";
2
3
  import fs, { existsSync as existsSync$1 } from "node:fs";
3
4
  import { execFile, execSync } from "node:child_process";
4
- import { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
5
+ import path, { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
5
6
  import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync } from "fs";
6
7
  import os$1, { homedir } from "os";
8
+ import { createHash, randomUUID } from "crypto";
7
9
  import { Agent } from "@mariozechner/pi-agent-core";
8
10
  import { completeSimple, getModel } from "@mariozechner/pi-ai";
9
11
  import { createCodingTools, createGrepTool, createFindTool, createLsTool, DEFAULT_COMPACTION_SETTINGS, estimateTokens, shouldCompact, generateSummary } from "@mariozechner/pi-coding-agent";
10
12
  import { Type } from "@sinclair/typebox";
11
- import path from "node:path";
13
+ import { mkdir, writeFile } from "fs/promises";
14
+ import path$1 from "node:path";
12
15
  import fsp from "node:fs/promises";
13
- import { createHash } from "node:crypto";
16
+ import { createHash as createHash$1 } from "node:crypto";
14
17
  import { promisify } from "node:util";
15
18
  import os from "node:os";
16
19
  import { fileURLToPath } from "node:url";
17
- import { createHash as createHash$1 } from "crypto";
18
20
  import { execFile as execFile$1 } from "child_process";
19
21
  import __cjs_mod__ from "node:module";
20
22
  const __filename = import.meta.filename;
@@ -233,10 +235,6 @@ function registerConfigHandlers(handleRaw) {
233
235
  return { success: true };
234
236
  });
235
237
  }
236
- function getFileName(path2) {
237
- if (!path2) return "";
238
- return path2.split("/").pop() || path2;
239
- }
240
238
  function inferMimeType(path2) {
241
239
  const ext = extname(path2).toLowerCase();
242
240
  if (ext === ".md" || ext === ".txt") return "text/plain";
@@ -576,13 +574,39 @@ function truncateHeadTail(text, maxChars, headRatio = 0.7) {
576
574
  ...[truncated ${truncatedChars} chars]
577
575
  ${text.slice(-tailChars)}`;
578
576
  }
577
+ function truncateStructuredData(data, maxChars) {
578
+ const json = JSON.stringify(data, null, 2);
579
+ if (json.length <= maxChars) return data;
580
+ const obj = { ...data };
581
+ let largestKey = "";
582
+ let largestSize = 0;
583
+ for (const [k, v] of Object.entries(obj)) {
584
+ if (typeof v === "string" && v.length > largestSize) {
585
+ largestKey = k;
586
+ largestSize = v.length;
587
+ }
588
+ }
589
+ if (largestKey) {
590
+ const overhead = json.length - largestSize;
591
+ const fieldBudget = Math.max(1e3, maxChars - overhead);
592
+ obj[largestKey] = truncateHeadTail(obj[largestKey], fieldBudget);
593
+ }
594
+ return obj;
595
+ }
579
596
  function toAgentResult(toolName, result) {
580
597
  let text;
598
+ const MAX_RESULT_CHARS = 1e5;
581
599
  if (result.success) {
582
600
  if (result.data === void 0 || result.data === null) {
583
601
  text = `[${toolName}] OK`;
584
602
  } else if (typeof result.data === "string") {
585
- text = result.data;
603
+ text = truncateHeadTail(result.data, MAX_RESULT_CHARS);
604
+ } else if (typeof result.data === "object" && result.data !== null && !Array.isArray(result.data)) {
605
+ const bounded = truncateStructuredData(
606
+ result.data,
607
+ MAX_RESULT_CHARS
608
+ );
609
+ text = JSON.stringify(bounded, null, 2);
586
610
  } else {
587
611
  text = JSON.stringify(result.data, null, 2);
588
612
  }
@@ -607,10 +631,11 @@ ${result.suggestions.map((s) => `- ${s}`).join("\n")}`);
607
631
  }
608
632
  text = parts.join("\n");
609
633
  }
610
- const MAX_RESULT_CHARS = 1e5;
611
- const bounded = truncateHeadTail(text, MAX_RESULT_CHARS);
634
+ if (text.length > MAX_RESULT_CHARS) {
635
+ text = truncateHeadTail(text, MAX_RESULT_CHARS);
636
+ }
612
637
  return {
613
- content: [{ type: "text", text: bounded }],
638
+ content: [{ type: "text", text }],
614
639
  details: { success: result.success, tool_name: toolName }
615
640
  };
616
641
  }
@@ -1255,7 +1280,12 @@ function createResearchMemoryTools(params) {
1255
1280
  ];
1256
1281
  }
1257
1282
  function slugify(text) {
1258
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1283
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1284
+ if (slug.length < 3) {
1285
+ const hash = createHash("sha256").update(text.toLowerCase().trim()).digest("hex").slice(0, 12);
1286
+ return slug ? `${slug}-${hash}` : hash;
1287
+ }
1288
+ return slug;
1259
1289
  }
1260
1290
  function memoryFilename(type, name) {
1261
1291
  return `${type}_${slugify(name)}.md`;
@@ -1267,22 +1297,40 @@ function ensureMemoryDir(projectPath) {
1267
1297
  const dir = memoryDir(projectPath);
1268
1298
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1269
1299
  }
1300
+ function yamlSafe(value) {
1301
+ if (!value) return '""';
1302
+ if (/[:\#{}\[\]"'`\n\r|>]/.test(value) || value !== value.trim()) {
1303
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1304
+ return `"${escaped}"`;
1305
+ }
1306
+ return value;
1307
+ }
1270
1308
  function formatFrontmatter(fm) {
1271
1309
  return [
1272
1310
  "---",
1273
- `name: ${fm.name}`,
1274
- `description: ${fm.description}`,
1311
+ `name: ${yamlSafe(fm.name)}`,
1312
+ `description: ${yamlSafe(fm.description)}`,
1275
1313
  `type: ${fm.type}`,
1276
1314
  "---"
1277
1315
  ].join("\n");
1278
1316
  }
1317
+ function yamlUnescape(raw) {
1318
+ const trimmed = raw.trim();
1319
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
1320
+ return trimmed.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1321
+ }
1322
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
1323
+ return trimmed.slice(1, -1).replace(/''/g, "'");
1324
+ }
1325
+ return trimmed;
1326
+ }
1279
1327
  function parseFrontmatter(text) {
1280
1328
  const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1281
1329
  if (!match) return null;
1282
1330
  const fm = {};
1283
1331
  for (const line of match[1].split("\n")) {
1284
1332
  const kv = line.match(/^(\w+):\s*(.+)$/);
1285
- if (kv) fm[kv[1]] = kv[2].trim();
1333
+ if (kv) fm[kv[1]] = yamlUnescape(kv[2]);
1286
1334
  }
1287
1335
  if (!fm.name || !fm.type) return null;
1288
1336
  const validTypes = ["user", "feedback", "project", "reference"];
@@ -1339,9 +1387,24 @@ function listMemoryFiles(projectPath) {
1339
1387
  return [];
1340
1388
  }
1341
1389
  }
1342
- function findMemoryByName(projectPath, name) {
1390
+ function findMemoryByName(projectPath, name, type) {
1343
1391
  const lower = name.toLowerCase();
1344
- return listMemoryFiles(projectPath).find((e) => e.frontmatter.name.toLowerCase() === lower) ?? null;
1392
+ const entries = listMemoryFiles(projectPath);
1393
+ return entries.find(
1394
+ (e) => e.frontmatter.name.toLowerCase() === lower && (!type || e.frontmatter.type === type)
1395
+ ) ?? null;
1396
+ }
1397
+ function findAllMemoriesByName(projectPath, name) {
1398
+ const lower = name.toLowerCase();
1399
+ return listMemoryFiles(projectPath).filter((e) => e.frontmatter.name.toLowerCase() === lower);
1400
+ }
1401
+ let _indexWriteLock = Promise.resolve();
1402
+ function withIndexLock(fn) {
1403
+ const next = _indexWriteLock.then(fn, fn);
1404
+ _indexWriteLock = next.then(() => {
1405
+ }, () => {
1406
+ });
1407
+ return next;
1345
1408
  }
1346
1409
  function buildMemoryIndex(entries) {
1347
1410
  if (entries.length === 0) return "";
@@ -1381,6 +1444,9 @@ function migrateAgentMemoryToFile(projectPath) {
1381
1444
  if (markerIdx < 0) return false;
1382
1445
  const agentMemory = content.slice(markerIdx + marker.length).trim();
1383
1446
  if (!agentMemory || /\[.*\]\(memory\/.*\)/.test(agentMemory)) return false;
1447
+ const legacyFilename = memoryFilename("project", "legacy-notes");
1448
+ const legacyPath = join(memoryDir(projectPath), legacyFilename);
1449
+ if (existsSync(legacyPath)) return false;
1384
1450
  ensureMemoryDir(projectPath);
1385
1451
  const entry = {
1386
1452
  frontmatter: {
@@ -1435,73 +1501,98 @@ function createSaveMemoryTool(projectPath) {
1435
1501
  if (!content) return toolError("MISSING_PARAMETER", "content is required.", {
1436
1502
  suggestions: ["Provide the memory content to save."]
1437
1503
  });
1438
- ensureMemoryDir(projectPath);
1439
- const filename = memoryFilename(type, name);
1440
- const description = content.split("\n")[0].replace(/^#+\s*/, "").slice(0, 120);
1441
- const entry = {
1442
- frontmatter: { name, description, type },
1443
- content,
1444
- filename
1445
- };
1446
- writeMemoryFile(projectPath, entry);
1447
- const allEntries = listMemoryFiles(projectPath);
1448
- const indexResult = updateAgentMdIndex(projectPath, allEntries);
1449
- if (!indexResult.success) {
1450
- return toolError(
1451
- "OUTPUT_TOO_LARGE",
1452
- "agent.md index exceeded size limit. Remove some memories first.",
1453
- {
1454
- suggestions: ["Use delete-memory to remove outdated entries before saving new ones."]
1455
- }
1456
- );
1457
- }
1458
- return {
1459
- success: true,
1460
- data: {
1461
- message: `Memory saved: ${name} (${type})`,
1462
- filename,
1463
- totalMemories: allEntries.length,
1464
- agentMdChars: indexResult.charCount
1504
+ const lines = content.split("\n");
1505
+ const firstNonEmpty = lines.find((l) => l.trim().length > 0) || "";
1506
+ const description = firstNonEmpty.replace(/^#+\s*/, "").trim().slice(0, 120) || name;
1507
+ return withIndexLock(() => {
1508
+ ensureMemoryDir(projectPath);
1509
+ const filename = memoryFilename(type, name);
1510
+ const entry = {
1511
+ frontmatter: { name, description, type },
1512
+ content,
1513
+ filename
1514
+ };
1515
+ writeMemoryFile(projectPath, entry);
1516
+ const allEntries = listMemoryFiles(projectPath);
1517
+ const indexResult = updateAgentMdIndex(projectPath, allEntries);
1518
+ if (!indexResult.success) {
1519
+ deleteMemoryFile(projectPath, filename);
1520
+ return toolError(
1521
+ "OUTPUT_TOO_LARGE",
1522
+ "agent.md index exceeded size limit. Remove some memories first.",
1523
+ {
1524
+ suggestions: ["Use delete-memory to remove outdated entries before saving new ones."]
1525
+ }
1526
+ );
1465
1527
  }
1466
- };
1528
+ return {
1529
+ success: true,
1530
+ data: {
1531
+ message: `Memory saved: ${name} (${type})`,
1532
+ filename,
1533
+ totalMemories: allEntries.length,
1534
+ agentMdChars: indexResult.charCount
1535
+ }
1536
+ };
1537
+ });
1467
1538
  }
1468
1539
  };
1469
1540
  }
1470
1541
  function createDeleteMemoryTool(projectPath) {
1471
1542
  return {
1472
1543
  name: "delete-memory",
1473
- description: "Delete a memory by name. Removes the file and its index entry in agent.md.",
1544
+ description: "Delete a memory by name. Removes the file and its index entry in agent.md. If multiple memories share the same name (different types), specify type to disambiguate.",
1474
1545
  parameters: {
1475
1546
  type: "object",
1476
1547
  properties: {
1477
1548
  name: {
1478
1549
  type: "string",
1479
1550
  description: "Name of the memory to delete (case-insensitive match)"
1551
+ },
1552
+ type: {
1553
+ type: "string",
1554
+ enum: VALID_TYPES$1,
1555
+ description: "Optional: memory type to disambiguate when multiple memories share the same name"
1480
1556
  }
1481
1557
  },
1482
1558
  required: ["name"]
1483
1559
  },
1484
1560
  execute: async (input) => {
1485
1561
  const name = String(input.name || "").trim();
1562
+ const type = input.type ? String(input.type) : void 0;
1486
1563
  if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
1487
1564
  suggestions: ["Provide the name of the memory to delete."]
1488
1565
  });
1489
- const existing = findMemoryByName(projectPath, name);
1490
- if (!existing) {
1566
+ const allMatches = findAllMemoriesByName(projectPath, name);
1567
+ if (allMatches.length === 0) {
1491
1568
  return toolError("NOT_FOUND", `Memory not found: "${name}"`, {
1492
1569
  suggestions: ["Check the memory name — it is case-insensitive. Current memories are listed in agent.md."]
1493
1570
  });
1494
1571
  }
1495
- deleteMemoryFile(projectPath, existing.filename);
1496
- const allEntries = listMemoryFiles(projectPath);
1497
- updateAgentMdIndex(projectPath, allEntries);
1498
- return {
1499
- success: true,
1500
- data: {
1501
- message: `Memory deleted: ${name}`,
1502
- totalMemories: allEntries.length
1503
- }
1504
- };
1572
+ if (allMatches.length > 1 && !type) {
1573
+ const types = allMatches.map((m) => m.frontmatter.type).join(", ");
1574
+ return toolError("AMBIGUOUS", `Multiple memories named "${name}" (types: ${types}). Specify type to disambiguate.`, {
1575
+ suggestions: [`Add type parameter: one of ${types}`]
1576
+ });
1577
+ }
1578
+ const existing = type ? findMemoryByName(projectPath, name, type) : allMatches[0];
1579
+ if (!existing) {
1580
+ return toolError("NOT_FOUND", `Memory not found: "${name}" with type "${type}"`, {
1581
+ suggestions: ["Check the memory name and type."]
1582
+ });
1583
+ }
1584
+ return withIndexLock(() => {
1585
+ deleteMemoryFile(projectPath, existing.filename);
1586
+ const allEntries = listMemoryFiles(projectPath);
1587
+ updateAgentMdIndex(projectPath, allEntries);
1588
+ return {
1589
+ success: true,
1590
+ data: {
1591
+ message: `Memory deleted: ${name}`,
1592
+ totalMemories: allEntries.length
1593
+ }
1594
+ };
1595
+ });
1505
1596
  }
1506
1597
  };
1507
1598
  }
@@ -1526,8 +1617,11 @@ const WEB_DEFAULTS = {
1526
1617
  maxFetchMaxChars: 2e5,
1527
1618
  defaultFetchTimeoutMs: 3e4,
1528
1619
  maxArxivCacheEntries: 100,
1529
- arxivSearchCacheTtlMs: 10 * 60 * 1e3
1620
+ arxivSearchCacheTtlMs: 10 * 60 * 1e3,
1530
1621
  // 10 min
1622
+ /** Content above this size is saved to disk; agent gets preview + file path */
1623
+ fetchPersistThresholdChars: 3e4,
1624
+ fetchPreviewChars: 2e3
1531
1625
  };
1532
1626
  class ProviderRateGate {
1533
1627
  constructor(minIntervalMs) {
@@ -1774,7 +1868,6 @@ function createWebSearchTool(ctx) {
1774
1868
  const providerRequested = normalizeSearchProvider(params.provider);
1775
1869
  const braveApiKey = process.env.BRAVE_API_KEY?.trim();
1776
1870
  let effectiveProvider = providerRequested === "auto" ? braveApiKey ? "brave" : "arxiv" : providerRequested;
1777
- ctx.onToolCall?.("web_search", { query, count, provider: effectiveProvider });
1778
1871
  let results = [];
1779
1872
  try {
1780
1873
  if (effectiveProvider === "brave") {
@@ -1818,7 +1911,6 @@ function createWebSearchTool(ctx) {
1818
1911
  count: results.length,
1819
1912
  results
1820
1913
  };
1821
- ctx.onToolResult?.("web_search", payload);
1822
1914
  return toAgentResult("web_search", {
1823
1915
  success: true,
1824
1916
  data: payload
@@ -1830,7 +1922,7 @@ function createWebFetchTool(ctx) {
1830
1922
  return {
1831
1923
  name: "web_fetch",
1832
1924
  label: "Web Fetch",
1833
- description: "Fetch a URL and extract readable text or markdown for downstream analysis.",
1925
+ description: "Fetch a URL and extract readable text or markdown. Content over 30K chars is saved to disk — use the read tool on the returned content_path to access full content.",
1834
1926
  parameters: WebFetchSchema,
1835
1927
  execute: async (_toolCallId, rawParams) => {
1836
1928
  const params = rawParams;
@@ -1859,7 +1951,6 @@ function createWebFetchTool(ctx) {
1859
1951
  const maxChars = typeof maxCharsRaw === "number" ? Math.max(100, Math.min(WEB_DEFAULTS.maxFetchMaxChars, Math.floor(maxCharsRaw))) : WEB_DEFAULTS.defaultFetchMaxChars;
1860
1952
  const timeoutSecRaw = (typeof params.timeout_sec === "number" && Number.isFinite(params.timeout_sec) ? params.timeout_sec : void 0) ?? (typeof params.timeoutSec === "number" && Number.isFinite(params.timeoutSec) ? params.timeoutSec : void 0);
1861
1953
  const timeoutMs = typeof timeoutSecRaw === "number" ? Math.max(1e3, Math.floor(timeoutSecRaw * 1e3)) : WEB_DEFAULTS.defaultFetchTimeoutMs;
1862
- ctx.onToolCall?.("web_fetch", { url: url.toString(), extractMode, maxChars });
1863
1954
  let response;
1864
1955
  const controller = new AbortController();
1865
1956
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -1897,16 +1988,37 @@ Source: ${url.toString()}
1897
1988
  ---
1898
1989
 
1899
1990
  ${sliced}` : sliced;
1900
- const payload = {
1901
- url: url.toString(),
1902
- status_code: response.status,
1903
- content_type: contentType,
1904
- extract_mode: extractMode,
1905
- chars: normalized.length,
1906
- truncated,
1907
- content: output || "(empty response)"
1908
- };
1909
- ctx.onToolResult?.("web_fetch", payload);
1991
+ let payload;
1992
+ if (output.length > WEB_DEFAULTS.fetchPersistThresholdChars) {
1993
+ const hash = createHash("md5").update(url.toString() + Date.now()).digest("hex").slice(0, 12);
1994
+ const ext = extractMode === "markdown" ? "md" : "txt";
1995
+ const contentDir = path.join(ctx.projectPath, "web-content");
1996
+ await mkdir(contentDir, { recursive: true });
1997
+ const filePath = path.join(contentDir, `${hash}.${ext}`);
1998
+ await writeFile(filePath, output, "utf-8");
1999
+ const previewRaw = output.slice(0, WEB_DEFAULTS.fetchPreviewChars);
2000
+ const lastNl = previewRaw.lastIndexOf("\n");
2001
+ const preview = (lastNl > WEB_DEFAULTS.fetchPreviewChars * 0.5 ? previewRaw.slice(0, lastNl) : previewRaw) + "\n...";
2002
+ payload = {
2003
+ url: url.toString(),
2004
+ status_code: response.status,
2005
+ content_type: contentType,
2006
+ extract_mode: extractMode,
2007
+ chars: normalized.length,
2008
+ content_path: path.relative(ctx.workspacePath, filePath),
2009
+ preview
2010
+ };
2011
+ } else {
2012
+ payload = {
2013
+ url: url.toString(),
2014
+ status_code: response.status,
2015
+ content_type: contentType,
2016
+ extract_mode: extractMode,
2017
+ chars: normalized.length,
2018
+ truncated,
2019
+ content: output || "(empty response)"
2020
+ };
2021
+ }
1910
2022
  return toAgentResult("web_fetch", {
1911
2023
  success: response.ok,
1912
2024
  data: payload,
@@ -2774,7 +2886,6 @@ function createLiteratureSearchTool(ctx) {
2774
2886
  suggestions: ["Ensure the agent runtime has an LLM provider configured (callLlm in ResearchToolContext)."]
2775
2887
  }));
2776
2888
  }
2777
- ctx.onToolCall?.("literature-search", { query, context: extraContext });
2778
2889
  const planUserPrompt = extraContext ? `Research request: ${query}
2779
2890
 
2780
2891
  Additional context: ${extraContext}` : `Research request: ${query}`;
@@ -2951,9 +3062,9 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2951
3062
  }
2952
3063
  }
2953
3064
  }
2954
- const reviewDir = path.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
3065
+ const reviewDir = path$1.join(ctx.projectPath, ".research-pilot", "literature-runs", runId);
2955
3066
  fs.mkdirSync(reviewDir, { recursive: true });
2956
- const fullReviewPath = path.join(reviewDir, "review.json");
3067
+ const fullReviewPath = path$1.join(reviewDir, "review.json");
2957
3068
  fs.writeFileSync(fullReviewPath, JSON.stringify({
2958
3069
  plan,
2959
3070
  allPapersCount: deduplicated.length,
@@ -2962,7 +3073,7 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2962
3073
  queriesUsed,
2963
3074
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2964
3075
  }, null, 2), "utf-8");
2965
- const relReviewPath = path.relative(ctx.projectPath, fullReviewPath);
3076
+ const relReviewPath = path$1.relative(ctx.projectPath, fullReviewPath);
2966
3077
  const payload = {
2967
3078
  totalFound: deduplicated.length,
2968
3079
  reviewedCount: review.relevantPapers.length,
@@ -2985,7 +3096,6 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
2985
3096
  runId,
2986
3097
  queriesUsed: queriesUsed.slice(0, 10)
2987
3098
  };
2988
- ctx.onToolResult?.("literature-search", payload);
2989
3099
  return toAgentResult("literature-search", toolSuccess(payload, pipelineWarnings.length > 0 ? pipelineWarnings : void 0));
2990
3100
  }
2991
3101
  };
@@ -3028,16 +3138,16 @@ const FORMAT_EXTENSIONS = {
3028
3138
  zip: "zip"
3029
3139
  };
3030
3140
  function resolveWithinProject(projectPath, targetPath) {
3031
- const root = path.resolve(projectPath);
3032
- const resolved = targetPath.startsWith("/") ? path.resolve(targetPath) : path.resolve(root, targetPath);
3033
- const rel = path.relative(root, resolved);
3034
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
3141
+ const root = path$1.resolve(projectPath);
3142
+ const resolved = targetPath.startsWith("/") ? path$1.resolve(targetPath) : path$1.resolve(root, targetPath);
3143
+ const rel = path$1.relative(root, resolved);
3144
+ if (rel.startsWith("..") || path$1.isAbsolute(rel)) {
3035
3145
  throw new Error(`Path escapes project directory: ${targetPath}`);
3036
3146
  }
3037
3147
  return resolved;
3038
3148
  }
3039
3149
  function toProjectRelative(projectPath, absolutePath) {
3040
- return path.relative(path.resolve(projectPath), absolutePath);
3150
+ return path$1.relative(path$1.resolve(projectPath), absolutePath);
3041
3151
  }
3042
3152
  function isHttpUrl(value) {
3043
3153
  try {
@@ -3057,7 +3167,7 @@ function sanitizeBaseName(value) {
3057
3167
  function inferUrlBaseName(sourceUrl) {
3058
3168
  try {
3059
3169
  const parsed = new URL(sourceUrl);
3060
- const candidate = path.parse(parsed.pathname).name || parsed.hostname;
3170
+ const candidate = path$1.parse(parsed.pathname).name || parsed.hostname;
3061
3171
  return sanitizeBaseName(candidate);
3062
3172
  } catch {
3063
3173
  return sanitizeBaseName(sourceUrl);
@@ -3067,7 +3177,7 @@ function outputExtensionForMode(mode) {
3067
3177
  return mode === "text" ? ".txt" : ".md";
3068
3178
  }
3069
3179
  function extensionFromPath(filePath) {
3070
- const ext = path.extname(filePath).replace(".", "").toLowerCase();
3180
+ const ext = path$1.extname(filePath).replace(".", "").toLowerCase();
3071
3181
  return ext || void 0;
3072
3182
  }
3073
3183
  function normalizeMode(value) {
@@ -3301,15 +3411,15 @@ async function downloadToProject(projectPath, sourceUrl) {
3301
3411
  if (bytes.length > DEFAULT_MAX_DOWNLOAD_BYTES) {
3302
3412
  return { ok: false, error: `Downloaded file too large (${bytes.length} bytes > ${DEFAULT_MAX_DOWNLOAD_BYTES} bytes)` };
3303
3413
  }
3304
- const downloadDir = path.join(path.resolve(projectPath), ".research-pilot", "cache", "downloads");
3414
+ const downloadDir = path$1.join(path$1.resolve(projectPath), ".research-pilot", "cache", "downloads");
3305
3415
  await fsp.mkdir(downloadDir, { recursive: true });
3306
3416
  const urlObj = new URL(sourceUrl);
3307
3417
  const fromExt = extensionFromPath(urlObj.pathname);
3308
3418
  const fromContentType = detectFormatFromContentType(response.headers.get("content-type"));
3309
3419
  const chosenExt = FORMAT_EXTENSIONS[fromContentType || ""] || FORMAT_EXTENSIONS[fromExt || ""] || "bin";
3310
- const hash = createHash("sha256").update(sourceUrl).digest("hex").slice(0, 12);
3420
+ const hash = createHash$1("sha256").update(sourceUrl).digest("hex").slice(0, 12);
3311
3421
  const fileName = `${isoStamp()}-${hash}.${chosenExt}`;
3312
- const inputPath = path.join(downloadDir, fileName);
3422
+ const inputPath = path$1.join(downloadDir, fileName);
3313
3423
  await fsp.writeFile(inputPath, bytes);
3314
3424
  return {
3315
3425
  ok: true,
@@ -3389,9 +3499,9 @@ ${sections}
3389
3499
  `;
3390
3500
  }
3391
3501
  function buildPerRangeOutputPath(baseAbsolutePath, range, mode) {
3392
- const parsed = path.parse(baseAbsolutePath);
3502
+ const parsed = path$1.parse(baseAbsolutePath);
3393
3503
  const ext = parsed.ext || outputExtensionForMode(mode);
3394
- return path.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
3504
+ return path$1.join(parsed.dir, `${parsed.name}-${rangeFileToken(range)}${ext}`);
3395
3505
  }
3396
3506
  function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
3397
3507
  const content = text.slice(0, maxPreviewChars);
@@ -3403,14 +3513,14 @@ function buildPreview(text, maxPreviewChars = DEFAULT_PREVIEW_CHARS) {
3403
3513
  };
3404
3514
  }
3405
3515
  function defaultOutputPath(args) {
3406
- const outputDir = path.join(
3407
- path.resolve(args.projectPath),
3516
+ const outputDir = path$1.join(
3517
+ path$1.resolve(args.projectPath),
3408
3518
  ".research-pilot",
3409
3519
  "cache",
3410
3520
  "converted"
3411
3521
  );
3412
- const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path.parse(args.inputPath || "document").name || "document");
3413
- return path.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
3522
+ const baseName = args.isUrl ? inferUrlBaseName(args.sourceRaw) : sanitizeBaseName(path$1.parse(args.inputPath || "document").name || "document");
3523
+ return path$1.join(outputDir, `${baseName}-${isoStamp()}${outputExtensionForMode(args.mode)}`);
3414
3524
  }
3415
3525
  function failure(payload) {
3416
3526
  return toAgentResult("convert_document", { success: false, error: payload.error, data: payload });
@@ -3593,7 +3703,7 @@ function createConvertDocumentTool(ctx) {
3593
3703
  mode
3594
3704
  });
3595
3705
  }
3596
- await fsp.mkdir(path.dirname(outputAbsolutePath), { recursive: true });
3706
+ await fsp.mkdir(path$1.dirname(outputAbsolutePath), { recursive: true });
3597
3707
  let converter;
3598
3708
  let producedText = "";
3599
3709
  let pageCount;
@@ -3626,7 +3736,7 @@ function createConvertDocumentTool(ctx) {
3626
3736
  anyTruncated = true;
3627
3737
  }
3628
3738
  const segmentAbsPath = buildPerRangeOutputPath(outputAbsolutePath, range, mode);
3629
- await fsp.mkdir(path.dirname(segmentAbsPath), { recursive: true });
3739
+ await fsp.mkdir(path$1.dirname(segmentAbsPath), { recursive: true });
3630
3740
  await fsp.writeFile(segmentAbsPath, segmentText, "utf8");
3631
3741
  const segmentRelPath = toProjectRelative(projectPath, segmentAbsPath);
3632
3742
  const segmentPreview = buildPreview(segmentText);
@@ -3765,7 +3875,7 @@ function createDataAnalyzeTool(ctx) {
3765
3875
  suggestions: ["Valid task types: analyze, visualize, transform, model."]
3766
3876
  }));
3767
3877
  }
3768
- const absDataFile = path.resolve(ctx.workspacePath, filePath);
3878
+ const absDataFile = path$1.resolve(ctx.workspacePath, filePath);
3769
3879
  if (!fs.existsSync(absDataFile)) {
3770
3880
  return toAgentResult("data_analyze", toolError("FILE_NOT_FOUND", `File not found: ${filePath}`, {
3771
3881
  suggestions: [
@@ -3775,18 +3885,17 @@ function createDataAnalyzeTool(ctx) {
3775
3885
  context: { workspacePath: ctx.workspacePath, resolvedPath: absDataFile }
3776
3886
  }));
3777
3887
  }
3778
- ctx.onToolCall?.("data_analyze", { file_path: filePath, instructions, task_type: taskType });
3779
3888
  const runId = Date.now().toString(36);
3780
- const outputBase = path.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
3781
- const figuresDir = path.join(outputBase, "figures");
3782
- const tablesDir = path.join(outputBase, "tables");
3783
- const dataDir = path.join(outputBase, "data");
3889
+ const outputBase = path$1.join(ctx.workspacePath, ".research-pilot", "data-runs", runId);
3890
+ const figuresDir = path$1.join(outputBase, "figures");
3891
+ const tablesDir = path$1.join(outputBase, "tables");
3892
+ const dataDir = path$1.join(outputBase, "data");
3784
3893
  fs.mkdirSync(figuresDir, { recursive: true });
3785
3894
  fs.mkdirSync(tablesDir, { recursive: true });
3786
3895
  fs.mkdirSync(dataDir, { recursive: true });
3787
- const resultsFile = path.join(outputBase, "results.json");
3896
+ const resultsFile = path$1.join(outputBase, "results.json");
3788
3897
  const rawPreview = fs.readFileSync(absDataFile, "utf-8").slice(0, 2e3);
3789
- const ext = path.extname(absDataFile).toLowerCase();
3898
+ const ext = path$1.extname(absDataFile).toLowerCase();
3790
3899
  const formatHint = ext === ".csv" ? "CSV" : ext === ".tsv" ? "TSV" : ext === ".json" ? "JSON" : ext === ".xlsx" ? "XLSX" : "unknown";
3791
3900
  if (!ctx.callLlm) {
3792
3901
  return toAgentResult("data_analyze", toolError("LLM_UNAVAILABLE", "LLM not available for code generation.", {
@@ -3834,7 +3943,7 @@ function createDataAnalyzeTool(ctx) {
3834
3943
  ""
3835
3944
  ].join("\n");
3836
3945
  const fullScript = DATA_CODE_TEMPLATE + pathDefinitions + "\n" + generatedCode;
3837
- const scriptPath = path.join(outputBase, "script.py");
3946
+ const scriptPath = path$1.join(outputBase, "script.py");
3838
3947
  fs.writeFileSync(scriptPath, fullScript, "utf-8");
3839
3948
  try {
3840
3949
  const { stdout, stderr } = await execFileAsync("python3", [scriptPath], {
@@ -3861,7 +3970,7 @@ function createDataAnalyzeTool(ctx) {
3861
3970
  outputs.push({
3862
3971
  name: f,
3863
3972
  type,
3864
- path: path.relative(ctx.workspacePath, path.join(dir, f))
3973
+ path: path$1.relative(ctx.workspacePath, path$1.join(dir, f))
3865
3974
  });
3866
3975
  }
3867
3976
  }
@@ -3874,26 +3983,25 @@ function createDataAnalyzeTool(ctx) {
3874
3983
  summary: manifest.summary,
3875
3984
  warnings: manifest.warnings
3876
3985
  } : void 0,
3877
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
3986
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3878
3987
  runId
3879
3988
  };
3880
- ctx.onToolResult?.("data_analyze", payload);
3881
3989
  return toAgentResult("data_analyze", { success: true, data: payload });
3882
3990
  } catch (err) {
3883
3991
  const errorDetail = err.stderr?.slice(0, 2e3) || err.message;
3884
3992
  return toAgentResult("data_analyze", toolError("EXECUTION_FAILED", `Python execution failed: ${errorDetail}`, {
3885
3993
  retryable: true,
3886
3994
  suggestions: [
3887
- `Review the generated script at ${path.relative(ctx.workspacePath, scriptPath)} for errors.`,
3995
+ `Review the generated script at ${path$1.relative(ctx.workspacePath, scriptPath)} for errors.`,
3888
3996
  "Check that required Python packages (pandas, matplotlib, seaborn, etc.) are installed.",
3889
3997
  "Try simplifying the analysis instructions."
3890
3998
  ],
3891
3999
  context: {
3892
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
4000
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3893
4001
  runId
3894
4002
  },
3895
4003
  data: {
3896
- scriptPath: path.relative(ctx.workspacePath, scriptPath),
4004
+ scriptPath: path$1.relative(ctx.workspacePath, scriptPath),
3897
4005
  runId
3898
4006
  }
3899
4007
  }));
@@ -3941,7 +4049,7 @@ function wrapResearchTool(tool) {
3941
4049
  }
3942
4050
  function createResearchTools(ctx) {
3943
4051
  const tools = [];
3944
- tools.push(createWebSearchTool(ctx));
4052
+ tools.push(createWebSearchTool());
3945
4053
  tools.push(createWebFetchTool(ctx));
3946
4054
  tools.push(createLiteratureSearchTool(ctx));
3947
4055
  tools.push(createConvertDocumentTool(ctx));
@@ -4021,7 +4129,7 @@ function agentCalledSaveMemoryThisTurn(messages) {
4021
4129
  return false;
4022
4130
  }
4023
4131
  async function maybeExtractMemories(config, messages, turnCount, extractEveryN = 3) {
4024
- if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT === "0") return;
4132
+ if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT !== "1") return;
4025
4133
  if (turnCount % extractEveryN !== 0) return;
4026
4134
  if (agentCalledSaveMemoryThisTurn(messages)) {
4027
4135
  if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
@@ -4045,33 +4153,57 @@ async function maybeExtractMemories(config, messages, turnCount, extractEveryN =
4045
4153
  const textContent = result.content.find((c) => c.type === "text");
4046
4154
  const text = textContent?.text?.trim() ?? "";
4047
4155
  if (!text || text === "[]") return;
4048
- const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/) ?? text.match(/(\[[\s\S]*?\])/);
4049
- const jsonStr = jsonMatch?.[1]?.trim() ?? text;
4050
- const extracted = JSON.parse(jsonStr);
4156
+ let jsonStr;
4157
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
4158
+ if (fenceMatch) {
4159
+ jsonStr = fenceMatch[1].trim();
4160
+ } else {
4161
+ const firstBracket = text.indexOf("[");
4162
+ const lastBracket = text.lastIndexOf("]");
4163
+ if (firstBracket !== -1 && lastBracket > firstBracket) {
4164
+ jsonStr = text.slice(firstBracket, lastBracket + 1);
4165
+ } else {
4166
+ jsonStr = text;
4167
+ }
4168
+ }
4169
+ let extracted;
4170
+ try {
4171
+ extracted = JSON.parse(jsonStr);
4172
+ } catch (parseErr) {
4173
+ if (config.debug) {
4174
+ console.warn("[Extractor] JSON parse failed:", parseErr, "Raw text:", text.slice(0, 200));
4175
+ }
4176
+ return;
4177
+ }
4051
4178
  if (!Array.isArray(extracted) || extracted.length === 0) return;
4052
4179
  ensureMemoryDir(config.projectPath);
4053
- let written = 0;
4180
+ const validEntries = [];
4054
4181
  for (const mem of extracted) {
4055
4182
  if (!mem.type || !mem.name || !mem.content) continue;
4056
4183
  if (!VALID_TYPES.includes(mem.type)) continue;
4057
- const entry = {
4184
+ const desc = (mem.description || "").trim();
4185
+ const contentFirstLine = mem.content.split("\n").find((l) => l.trim().length > 0) || "";
4186
+ const description = (desc || contentFirstLine.replace(/^#+\s*/, "").trim().slice(0, 120) || mem.name).replace(/\n/g, " ");
4187
+ validEntries.push({
4058
4188
  frontmatter: {
4059
4189
  name: mem.name,
4060
- description: (mem.description || mem.content.slice(0, 120)).replace(/\n/g, " "),
4190
+ description,
4061
4191
  type: mem.type
4062
4192
  },
4063
4193
  content: mem.content,
4064
4194
  filename: memoryFilename(mem.type, mem.name)
4065
- };
4066
- writeMemoryFile(config.projectPath, entry);
4067
- written++;
4195
+ });
4068
4196
  }
4069
- if (written > 0) {
4197
+ if (validEntries.length === 0) return;
4198
+ await withIndexLock(() => {
4199
+ for (const entry of validEntries) {
4200
+ writeMemoryFile(config.projectPath, entry);
4201
+ }
4070
4202
  const allEntries = listMemoryFiles(config.projectPath);
4071
4203
  updateAgentMdIndex(config.projectPath, allEntries);
4072
- if (config.debug) {
4073
- console.log(`[Extractor] Saved ${written} memories from conversation`);
4074
- }
4204
+ });
4205
+ if (config.debug) {
4206
+ console.log(`[Extractor] Saved ${validEntries.length} memories from conversation`);
4075
4207
  }
4076
4208
  } catch (err) {
4077
4209
  if (config.debug) {
@@ -4123,7 +4255,7 @@ function discoverSkillFiles(rootDir) {
4123
4255
  continue;
4124
4256
  }
4125
4257
  for (const entry of entries) {
4126
- const abs = path.join(current.dir, entry.name);
4258
+ const abs = path$1.join(current.dir, entry.name);
4127
4259
  if (entry.isFile() && entry.name === SKILL_FILE_NAME) {
4128
4260
  files.push(abs);
4129
4261
  continue;
@@ -4154,7 +4286,7 @@ function parseSkillFile(skillFile, displayPath, source) {
4154
4286
  tags,
4155
4287
  triggers,
4156
4288
  path: displayPath,
4157
- dir: path.dirname(skillFile),
4289
+ dir: path$1.dirname(skillFile),
4158
4290
  source,
4159
4291
  content
4160
4292
  };
@@ -4170,34 +4302,34 @@ function inferCategory(name, description) {
4170
4302
  return "General";
4171
4303
  }
4172
4304
  function loadBuiltinSkills() {
4173
- const skillsRoot = _builtinSkillsRoot ?? path.dirname(fileURLToPath(import.meta.url));
4305
+ const skillsRoot = _builtinSkillsRoot ?? path$1.dirname(fileURLToPath(import.meta.url));
4174
4306
  if (!fs.existsSync(skillsRoot) || !fs.statSync(skillsRoot).isDirectory()) {
4175
4307
  return [];
4176
4308
  }
4177
4309
  const files = discoverSkillFiles(skillsRoot);
4178
4310
  const byName = /* @__PURE__ */ new Map();
4179
4311
  for (const file of files) {
4180
- const entry = parseSkillFile(file, `[builtin] ${path.relative(skillsRoot, file)}`, "builtin");
4312
+ const entry = parseSkillFile(file, `[builtin] ${path$1.relative(skillsRoot, file)}`, "builtin");
4181
4313
  if (entry) byName.set(entry.name, entry);
4182
4314
  }
4183
4315
  return Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
4184
4316
  }
4185
4317
  function loadWorkspaceSkills(workspacePath) {
4186
- const skillRoot = path.resolve(workspacePath, ".research-pilot", "skills");
4318
+ const skillRoot = path$1.resolve(workspacePath, ".research-pilot", "skills");
4187
4319
  if (!fs.existsSync(skillRoot) || !fs.statSync(skillRoot).isDirectory()) {
4188
4320
  return [];
4189
4321
  }
4190
4322
  const files = discoverSkillFiles(skillRoot);
4191
- const entries = files.map((file) => parseSkillFile(file, path.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
4323
+ const entries = files.map((file) => parseSkillFile(file, path$1.relative(workspacePath, file), "workspace")).filter((entry) => entry !== null);
4192
4324
  return entries.sort((a, b) => a.name.localeCompare(b.name));
4193
4325
  }
4194
4326
  function loadUserSkills() {
4195
- const userRoot = path.resolve(os.homedir(), ".research-pilot", "skills");
4327
+ const userRoot = path$1.resolve(os.homedir(), ".research-pilot", "skills");
4196
4328
  if (!fs.existsSync(userRoot) || !fs.statSync(userRoot).isDirectory()) {
4197
4329
  return [];
4198
4330
  }
4199
4331
  const files = discoverSkillFiles(userRoot);
4200
- const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
4332
+ const entries = files.map((file) => parseSkillFile(file, `[user] ~/.research-pilot/skills/${path$1.relative(userRoot, file)}`, "user")).filter((entry) => entry !== null);
4201
4333
  return entries.sort((a, b) => a.name.localeCompare(b.name));
4202
4334
  }
4203
4335
  function loadAllSkills(workspacePath) {
@@ -4242,7 +4374,7 @@ function resolveSkillDependencies(allSkills, directSelection) {
4242
4374
  return result;
4243
4375
  }
4244
4376
  function readEnabledSkills(workspacePath) {
4245
- const configPath = path.resolve(workspacePath, ".research-pilot", "skills-config.json");
4377
+ const configPath = path$1.resolve(workspacePath, ".research-pilot", "skills-config.json");
4246
4378
  try {
4247
4379
  const raw = fs.readFileSync(configPath, "utf8");
4248
4380
  const config = JSON.parse(raw);
@@ -4252,9 +4384,9 @@ function readEnabledSkills(workspacePath) {
4252
4384
  return null;
4253
4385
  }
4254
4386
  function writeEnabledSkills(workspacePath, enabledSkills) {
4255
- const configDir = path.resolve(workspacePath, ".research-pilot");
4387
+ const configDir = path$1.resolve(workspacePath, ".research-pilot");
4256
4388
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
4257
- const configPath = path.join(configDir, "skills-config.json");
4389
+ const configPath = path$1.join(configDir, "skills-config.json");
4258
4390
  fs.writeFileSync(configPath, JSON.stringify({ enabledSkills }, null, 2), "utf8");
4259
4391
  }
4260
4392
  function buildSkillManifests(workspacePath) {
@@ -4278,15 +4410,15 @@ function buildSkillManifests(workspacePath) {
4278
4410
  });
4279
4411
  }
4280
4412
  function installSkillToWorkspace(workspacePath, skillName, skillDir) {
4281
- const destDir = path.resolve(workspacePath, ".research-pilot", "skills", skillName);
4413
+ const destDir = path$1.resolve(workspacePath, ".research-pilot", "skills", skillName);
4282
4414
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
4283
4415
  copyDirSync(skillDir, destDir);
4284
4416
  }
4285
4417
  function copyDirSync(src, dest) {
4286
4418
  const entries = fs.readdirSync(src, { withFileTypes: true });
4287
4419
  for (const entry of entries) {
4288
- const srcPath = path.join(src, entry.name);
4289
- const destPath = path.join(dest, entry.name);
4420
+ const srcPath = path$1.join(src, entry.name);
4421
+ const destPath = path$1.join(dest, entry.name);
4290
4422
  if (entry.isDirectory()) {
4291
4423
  if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
4292
4424
  copyDirSync(srcPath, destPath);
@@ -4487,6 +4619,7 @@ async function createCoordinator(config) {
4487
4619
  onStream,
4488
4620
  onToolCall,
4489
4621
  onToolResult,
4622
+ onToolProgress,
4490
4623
  onUsage,
4491
4624
  onSkillLoaded
4492
4625
  } = config;
@@ -4558,11 +4691,11 @@ async function createCoordinator(config) {
4558
4691
  }
4559
4692
  }
4560
4693
  }
4561
- const wrappedOnToolResult = (tool, result, args) => {
4694
+ const wrappedOnToolResult = (tool, result, args, toolCallId) => {
4562
4695
  if (activeTurnToolCallCount !== null) {
4563
4696
  activeTurnToolCallCount++;
4564
4697
  }
4565
- onToolResult?.(tool, result, args);
4698
+ onToolResult?.(tool, result, args, toolCallId);
4566
4699
  };
4567
4700
  const toolCtx = {
4568
4701
  workspacePath: projectPath,
@@ -4685,14 +4818,14 @@ The conversation continues below.`,
4685
4818
  }
4686
4819
  },
4687
4820
  beforeToolCall: async (ctx) => {
4688
- onToolCall?.(ctx.toolCall.name, ctx.args);
4821
+ onToolCall?.(ctx.toolCall.name, ctx.args, ctx.toolCall.id);
4689
4822
  if (debug) {
4690
4823
  console.log(` [Tool] ${ctx.toolCall.name}(${JSON.stringify(ctx.args).slice(0, 120)}...)`);
4691
4824
  }
4692
4825
  return void 0;
4693
4826
  },
4694
4827
  afterToolCall: async (ctx) => {
4695
- wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args);
4828
+ wrappedOnToolResult(ctx.toolCall.name, ctx.result, ctx.args, ctx.toolCall.id);
4696
4829
  if (ctx.toolCall.name === "load_skill" && onSkillLoaded) {
4697
4830
  const args = ctx.args;
4698
4831
  const result = ctx.result;
@@ -4703,7 +4836,7 @@ The conversation continues below.`,
4703
4836
  return void 0;
4704
4837
  }
4705
4838
  });
4706
- if (onStream || onUsage) {
4839
+ if (onStream || onUsage || onToolProgress) {
4707
4840
  agent.subscribe((event) => {
4708
4841
  if (event.type === "message_update" && onStream) {
4709
4842
  if (event.assistantMessageEvent.type === "text_delta") {
@@ -4717,6 +4850,15 @@ The conversation continues below.`,
4717
4850
  onUsage(usage, usage.cost);
4718
4851
  }
4719
4852
  }
4853
+ if (onToolProgress) {
4854
+ if (event.type === "tool_execution_start") {
4855
+ onToolProgress(event.toolName, event.toolCallId, "start", { args: event.args });
4856
+ } else if (event.type === "tool_execution_update") {
4857
+ onToolProgress(event.toolName, event.toolCallId, "update", { partialResult: event.partialResult });
4858
+ } else if (event.type === "tool_execution_end") {
4859
+ onToolProgress(event.toolName, event.toolCallId, "end", { result: event.result, isError: event.isError });
4860
+ }
4861
+ }
4720
4862
  });
4721
4863
  }
4722
4864
  async function clearSessionMemory() {
@@ -4826,24 +4968,17 @@ ${historyText}`,
4826
4968
  ## User Instructions (agent.md)
4827
4969
 
4828
4970
  ${agentMdContent}`;
4829
- }
4830
- if (skillSummariesPrompt) {
4831
- enrichedSystem = `${enrichedSystem}
4832
-
4833
- ${skillSummariesPrompt}`;
4834
4971
  }
4835
4972
  agent.setSystemPrompt(enrichedSystem);
4836
- let userMessage = message;
4837
- if (mentionContext || summaryContext) {
4838
- const contextParts = [];
4839
- if (summaryContext) contextParts.push(summaryContext);
4840
- if (mentionContext) contextParts.push(mentionContext);
4841
- userMessage = `${contextParts.join("\n\n")}
4973
+ const contextParts = [];
4974
+ if (summaryContext) contextParts.push(summaryContext);
4975
+ if (skillSummariesPrompt) contextParts.push(skillSummariesPrompt);
4976
+ if (mentionContext) contextParts.push(mentionContext);
4977
+ let userMessage = contextParts.length > 0 ? `${contextParts.join("\n\n")}
4842
4978
 
4843
4979
  ---
4844
4980
 
4845
- ${message}`;
4846
- }
4981
+ ${message}` : message;
4847
4982
  let perTurnToolCallCount = 0;
4848
4983
  activeTurnToolCallCount = 0;
4849
4984
  try {
@@ -4949,10 +5084,10 @@ function sessionSummaryGet(projectPath, sessionId) {
4949
5084
  }
4950
5085
  }
4951
5086
  class RateLimiter {
4952
- constructor(configs) {
5087
+ constructor(configs2) {
4953
5088
  this.timestamps = /* @__PURE__ */ new Map();
4954
5089
  this.activeCounts = /* @__PURE__ */ new Map();
4955
- this.configs = configs;
5090
+ this.configs = configs2;
4956
5091
  }
4957
5092
  /**
4958
5093
  * Wait until a request slot is available for the given source.
@@ -5565,7 +5700,7 @@ function parseMentions(message) {
5565
5700
  return { cleanMessage, mentions };
5566
5701
  }
5567
5702
  function getCacheKey(filePath, mtime) {
5568
- const hash = createHash$1("sha256").update(`${filePath}:${mtime}`).digest("hex").slice(0, 16);
5703
+ const hash = createHash("sha256").update(`${filePath}:${mtime}`).digest("hex").slice(0, 16);
5569
5704
  const name = basename(filePath).replace(/[^a-zA-Z0-9.-]/g, "_");
5570
5705
  return `${name}-${hash}.json`;
5571
5706
  }
@@ -6037,6 +6172,10 @@ class RealtimeBuffer {
6037
6172
  isStreaming = false;
6038
6173
  progressItems = [];
6039
6174
  activityEvents = [];
6175
+ /** Tool events for chat-inline rendering (mirrors tool-events-store) */
6176
+ toolEvents = [];
6177
+ /** Track tool-call start times keyed by toolCallId for duration computation */
6178
+ toolCallStartTimes = /* @__PURE__ */ new Map();
6040
6179
  /** Append a streaming text chunk (called from onStream callback) */
6041
6180
  appendChunk(chunk) {
6042
6181
  this.streamingText += chunk;
@@ -6051,18 +6190,45 @@ class RealtimeBuffer {
6051
6190
  this.progressItems.push(item);
6052
6191
  }
6053
6192
  }
6054
- /** Record an activity event */
6193
+ /** Record an activity event and track tool-call start times */
6055
6194
  pushActivity(event) {
6195
+ if (event.type === "tool-call" && event.toolCallId) {
6196
+ this.toolCallStartTimes.set(event.toolCallId, Date.now());
6197
+ }
6056
6198
  this.activityEvents.push(event);
6057
6199
  }
6200
+ /** Record a tool event for chat-inline rendering */
6201
+ pushToolEvent(event) {
6202
+ this.toolEvents.push(event);
6203
+ }
6204
+ /** Update a tool event by toolCallId (for tool-result merge) */
6205
+ updateToolEvent(toolCallId, patch) {
6206
+ const idx = this.toolEvents.findLastIndex((e) => e.toolCallId === toolCallId);
6207
+ if (idx !== -1) {
6208
+ this.toolEvents[idx] = { ...this.toolEvents[idx], ...patch };
6209
+ }
6210
+ }
6211
+ /** Clear tool events (on new run or finalize) */
6212
+ clearToolEvents() {
6213
+ this.toolEvents = [];
6214
+ }
6215
+ /** Pop and return the start time for a tool-call, or undefined if not found */
6216
+ popToolCallStartTime(toolCallId) {
6217
+ const t = this.toolCallStartTimes.get(toolCallId);
6218
+ if (t !== void 0) this.toolCallStartTimes.delete(toolCallId);
6219
+ return t;
6220
+ }
6058
6221
  /** Clear progress and activity (called on project close or explicit reset) */
6059
6222
  clearRun() {
6060
6223
  this.progressItems = [];
6061
6224
  this.activityEvents = [];
6225
+ this.toolEvents = [];
6062
6226
  }
6063
6227
  /** Clear only activity events (called on new agent run) */
6064
6228
  clearActivity() {
6065
6229
  this.activityEvents = [];
6230
+ this.toolEvents = [];
6231
+ this.toolCallStartTimes.clear();
6066
6232
  }
6067
6233
  /** Mark streaming finished (called on agent:done) */
6068
6234
  finishStreaming() {
@@ -6075,6 +6241,8 @@ class RealtimeBuffer {
6075
6241
  this.isStreaming = false;
6076
6242
  this.progressItems = [];
6077
6243
  this.activityEvents = [];
6244
+ this.toolEvents = [];
6245
+ this.toolCallStartTimes.clear();
6078
6246
  }
6079
6247
  /** Return a snapshot the renderer can use to hydrate stores */
6080
6248
  getSnapshot() {
@@ -6082,17 +6250,366 @@ class RealtimeBuffer {
6082
6250
  streamingText: this.streamingText,
6083
6251
  isStreaming: this.isStreaming,
6084
6252
  progressItems: [...this.progressItems],
6085
- activityEvents: [...this.activityEvents]
6253
+ activityEvents: [...this.activityEvents],
6254
+ toolEvents: [...this.toolEvents]
6086
6255
  };
6087
6256
  }
6088
6257
  }
6089
6258
  function createRealtimeBuffer() {
6090
6259
  return new RealtimeBuffer();
6091
6260
  }
6261
+ function getFileName(path2) {
6262
+ if (!path2) return "";
6263
+ const parts = path2.replace(/\\/g, "/").split("/");
6264
+ return parts[parts.length - 1] || path2;
6265
+ }
6266
+ function truncStr(s, max) {
6267
+ if (!s) return "";
6268
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
6269
+ }
6270
+ function safeRecord(obj) {
6271
+ return obj && typeof obj === "object" ? obj : {};
6272
+ }
6273
+ function extractResultText(result) {
6274
+ const r = safeRecord(result);
6275
+ const content = r.content;
6276
+ return content?.[0]?.text || "";
6277
+ }
6278
+ function lastNLines(text, n) {
6279
+ const lines = text.split("\n").filter(Boolean);
6280
+ return lines.slice(-n).join("\n");
6281
+ }
6282
+ const configs = [
6283
+ // ── File tools ────────────────────────
6284
+ {
6285
+ name: "read",
6286
+ displayName: "Read",
6287
+ icon: "FileText",
6288
+ category: "file",
6289
+ formatCallSummary: (a) => {
6290
+ const path2 = a.path || "";
6291
+ const offset = a.offset;
6292
+ const limit = a.limit;
6293
+ const suffix = offset ? ` · lines ${offset}-${offset + (limit || 2e3)}` : "";
6294
+ return `${getFileName(path2)}${suffix}`;
6295
+ },
6296
+ formatCallDetail: (a) => ({ path: a.path, offset: a.offset, limit: a.limit }),
6297
+ formatResultSummary: (result) => {
6298
+ const text = extractResultText(result);
6299
+ const lineCount = text ? text.split("\n").length : 0;
6300
+ return lineCount ? `${lineCount} lines` : "Read completed";
6301
+ },
6302
+ formatResultDetail: (result) => {
6303
+ const text = extractResultText(result);
6304
+ return { lineCount: text ? text.split("\n").length : 0 };
6305
+ }
6306
+ },
6307
+ {
6308
+ name: "write",
6309
+ displayName: "Write",
6310
+ icon: "FileText",
6311
+ category: "file",
6312
+ formatCallSummary: (a) => getFileName(a.path || ""),
6313
+ formatCallDetail: (a) => ({ path: a.path }),
6314
+ formatResultSummary: (_, a) => `Written: ${getFileName(a?.path || "")}`,
6315
+ formatResultDetail: (_, a) => ({ path: a?.path })
6316
+ },
6317
+ {
6318
+ name: "edit",
6319
+ displayName: "Edit",
6320
+ icon: "FileText",
6321
+ category: "file",
6322
+ formatCallSummary: (a) => getFileName(a.path || ""),
6323
+ formatCallDetail: (a) => ({ path: a.path }),
6324
+ formatResultSummary: (_, a) => `Edited: ${getFileName(a?.path || "")}`,
6325
+ formatResultDetail: (_, a) => ({ path: a?.path })
6326
+ },
6327
+ // ── Code tools ────────────────────────
6328
+ {
6329
+ name: "bash",
6330
+ displayName: "Bash",
6331
+ icon: "Terminal",
6332
+ category: "code",
6333
+ formatCallSummary: (a) => {
6334
+ const cmd = a.command || "";
6335
+ return cmd.length > 60 ? `$ ${cmd.slice(0, 57)}...` : `$ ${cmd}`;
6336
+ },
6337
+ formatCallDetail: (a) => ({ command: truncStr(a.command, 200) }),
6338
+ formatResultSummary: () => "Command completed",
6339
+ formatResultDetail: (result) => {
6340
+ const text = extractResultText(result);
6341
+ const lines = text.split("\n").filter(Boolean);
6342
+ return { outputLines: lines.length, outputPreview: truncStr(lastNLines(text, 3), 200) };
6343
+ },
6344
+ formatProgress: (partial) => {
6345
+ const text = partial?.content?.[0]?.text;
6346
+ if (typeof text === "string" && text.length > 0) {
6347
+ return lastNLines(text, 5);
6348
+ }
6349
+ return void 0;
6350
+ }
6351
+ },
6352
+ // ── Search tools ────────────────────────
6353
+ {
6354
+ name: "grep",
6355
+ displayName: "Search",
6356
+ icon: "Search",
6357
+ category: "search",
6358
+ formatCallSummary: (a) => `"${truncStr(a.pattern, 30)}"${a.path ? ` in ${a.path}` : ""}`,
6359
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path, glob: a.glob }),
6360
+ formatResultSummary: (result) => {
6361
+ const text = extractResultText(result);
6362
+ const count = text.split("\n").filter(Boolean).length;
6363
+ return `${count} results`;
6364
+ },
6365
+ formatResultDetail: (result) => {
6366
+ const text = extractResultText(result);
6367
+ return { matchCount: text.split("\n").filter(Boolean).length };
6368
+ }
6369
+ },
6370
+ {
6371
+ name: "glob",
6372
+ displayName: "Find Files",
6373
+ icon: "Search",
6374
+ category: "search",
6375
+ formatCallSummary: (a) => a.pattern || "",
6376
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
6377
+ formatResultSummary: (result) => {
6378
+ const text = extractResultText(result);
6379
+ const count = text.split("\n").filter(Boolean).length;
6380
+ return `${count} files`;
6381
+ },
6382
+ formatResultDetail: (result) => {
6383
+ const text = extractResultText(result);
6384
+ return { fileCount: text.split("\n").filter(Boolean).length };
6385
+ }
6386
+ },
6387
+ {
6388
+ name: "find",
6389
+ displayName: "Find",
6390
+ icon: "Search",
6391
+ category: "search",
6392
+ formatCallSummary: (a) => truncStr(a.pattern || a.path, 40),
6393
+ formatCallDetail: (a) => ({ pattern: a.pattern, path: a.path }),
6394
+ formatResultSummary: () => "Find completed",
6395
+ formatResultDetail: () => ({})
6396
+ },
6397
+ {
6398
+ name: "ls",
6399
+ displayName: "List",
6400
+ icon: "FileText",
6401
+ category: "file",
6402
+ formatCallSummary: (a) => a.path || ".",
6403
+ formatCallDetail: (a) => ({ path: a.path }),
6404
+ formatResultSummary: () => "Listed",
6405
+ formatResultDetail: () => ({})
6406
+ },
6407
+ // ── Web tools ────────────────────────
6408
+ {
6409
+ name: "fetch",
6410
+ displayName: "Fetch",
6411
+ icon: "Globe",
6412
+ category: "web",
6413
+ formatCallSummary: (a) => truncStr(a.url, 50),
6414
+ formatCallDetail: (a) => ({ url: a.url }),
6415
+ formatResultSummary: (result) => {
6416
+ const text = extractResultText(result);
6417
+ const kb = (text.length / 1024).toFixed(1);
6418
+ return `${kb}KB received`;
6419
+ },
6420
+ formatResultDetail: (result) => {
6421
+ const text = extractResultText(result);
6422
+ return { sizeKB: parseFloat((text.length / 1024).toFixed(1)) };
6423
+ }
6424
+ },
6425
+ {
6426
+ name: "web_fetch",
6427
+ displayName: "Web Fetch",
6428
+ icon: "Globe",
6429
+ category: "web",
6430
+ formatCallSummary: (a) => truncStr(a.url, 50),
6431
+ formatCallDetail: (a) => ({ url: a.url }),
6432
+ formatResultSummary: (result) => {
6433
+ const r = safeRecord(result);
6434
+ const data = safeRecord(r.data);
6435
+ const charCount = data.charCount;
6436
+ if (charCount) return `${(charCount / 1024).toFixed(1)}KB received`;
6437
+ return "Fetch completed";
6438
+ },
6439
+ formatResultDetail: (result) => {
6440
+ const r = safeRecord(result);
6441
+ const data = safeRecord(r.data);
6442
+ return { charCount: data.charCount, url: data.url };
6443
+ }
6444
+ },
6445
+ {
6446
+ name: "web_search",
6447
+ displayName: "Web Search",
6448
+ icon: "Globe",
6449
+ category: "web",
6450
+ formatCallSummary: (a) => truncStr(a.query, 50),
6451
+ formatCallDetail: (a) => ({ query: a.query }),
6452
+ formatResultSummary: () => "Search completed",
6453
+ formatResultDetail: () => ({})
6454
+ },
6455
+ // ── Research tools ────────────────────────
6456
+ {
6457
+ name: "literature-search",
6458
+ displayName: "Literature Search",
6459
+ icon: "BookOpen",
6460
+ category: "research",
6461
+ formatCallSummary: (a) => truncStr(a.query, 40),
6462
+ formatCallDetail: (a) => ({ query: a.query, maxResults: a.max_results }),
6463
+ formatResultSummary: (result) => {
6464
+ const r = safeRecord(result);
6465
+ const data = safeRecord(r.data);
6466
+ const totalFound = data.totalPapersFound ?? 0;
6467
+ const saved = data.papersAutoSaved ?? 0;
6468
+ const coverage = data.coverage;
6469
+ if (totalFound > 0) {
6470
+ let s = `Found ${totalFound} papers`;
6471
+ if (coverage?.score != null) s += ` (${Math.round(coverage.score * 100)}%)`;
6472
+ if (saved > 0) s += `, saved ${saved}`;
6473
+ return s;
6474
+ }
6475
+ const local = data.localPapersUsed ?? 0;
6476
+ const external = data.externalPapersUsed ?? 0;
6477
+ return `Found ${local + external} papers`;
6478
+ },
6479
+ formatResultDetail: (result) => {
6480
+ const r = safeRecord(result);
6481
+ const data = safeRecord(r.data);
6482
+ return {
6483
+ papersFound: data.totalPapersFound ?? 0,
6484
+ papersSaved: data.papersAutoSaved ?? 0,
6485
+ coverage: data.coverage?.score
6486
+ };
6487
+ }
6488
+ },
6489
+ {
6490
+ name: "lit-subtopic",
6491
+ displayName: "Sub-topic Search",
6492
+ icon: "BookOpen",
6493
+ category: "research",
6494
+ formatCallSummary: (a) => a._summary || "Searching sub-topic",
6495
+ formatCallDetail: (a) => ({ summary: a._summary }),
6496
+ formatResultSummary: (result) => safeRecord(result).data || "Search completed",
6497
+ formatResultDetail: () => ({})
6498
+ },
6499
+ {
6500
+ name: "lit-enrich",
6501
+ displayName: "Enrich Papers",
6502
+ icon: "BookOpen",
6503
+ category: "research",
6504
+ formatCallSummary: (a) => a._summary || "Enriching paper metadata",
6505
+ formatCallDetail: (a) => ({ summary: a._summary }),
6506
+ formatResultSummary: (result) => safeRecord(result).data || "Enriched metadata",
6507
+ formatResultDetail: () => ({})
6508
+ },
6509
+ {
6510
+ name: "lit-autosave",
6511
+ displayName: "Save Papers",
6512
+ icon: "BookOpen",
6513
+ category: "research",
6514
+ formatCallSummary: (a) => a._summary || "Saving papers",
6515
+ formatCallDetail: (a) => ({ summary: a._summary }),
6516
+ formatResultSummary: (result) => safeRecord(result).data || "Saved papers",
6517
+ formatResultDetail: () => ({})
6518
+ },
6519
+ {
6520
+ name: "data_analyze",
6521
+ displayName: "Data Analysis",
6522
+ icon: "Database",
6523
+ category: "research",
6524
+ formatCallSummary: (a) => getFileName(a.file_path || "") || "data",
6525
+ formatCallDetail: (a) => ({ file_path: a.file_path }),
6526
+ formatResultSummary: () => "Analysis completed",
6527
+ formatResultDetail: () => ({})
6528
+ },
6529
+ // ── Artifact tools ────────────────────────
6530
+ {
6531
+ name: "artifact-create",
6532
+ displayName: "Create Artifact",
6533
+ icon: "Sparkles",
6534
+ category: "memory",
6535
+ formatCallSummary: (a) => {
6536
+ const type = (a.type || "artifact").toLowerCase();
6537
+ const title = truncStr(a.title, 35);
6538
+ return `${type}: ${title}`;
6539
+ },
6540
+ formatCallDetail: (a) => ({ type: a.type, title: a.title }),
6541
+ formatResultSummary: (result) => {
6542
+ const data = safeRecord(safeRecord(result).data);
6543
+ const type = data.type || "artifact";
6544
+ const title = truncStr(data.title, 30);
6545
+ return title ? `Created ${type}: ${title}` : `Created ${type}`;
6546
+ },
6547
+ formatResultDetail: (result) => {
6548
+ const data = safeRecord(safeRecord(result).data);
6549
+ return { type: data.type, title: data.title };
6550
+ }
6551
+ },
6552
+ {
6553
+ name: "artifact-update",
6554
+ displayName: "Update Artifact",
6555
+ icon: "Sparkles",
6556
+ category: "memory",
6557
+ formatCallSummary: (a) => truncStr(a.id, 30),
6558
+ formatCallDetail: (a) => ({ id: a.id }),
6559
+ formatResultSummary: () => "Updated",
6560
+ formatResultDetail: () => ({})
6561
+ },
6562
+ {
6563
+ name: "artifact-search",
6564
+ displayName: "Search Artifacts",
6565
+ icon: "Search",
6566
+ category: "memory",
6567
+ formatCallSummary: (a) => truncStr(a.query, 40),
6568
+ formatCallDetail: (a) => ({ query: a.query, types: a.types }),
6569
+ formatResultSummary: () => "Search completed",
6570
+ formatResultDetail: () => ({})
6571
+ },
6572
+ // ── System tools ────────────────────────
6573
+ {
6574
+ name: "convert_document",
6575
+ displayName: "Convert Document",
6576
+ icon: "FileText",
6577
+ category: "system",
6578
+ formatCallSummary: (a) => getFileName(a.source || ""),
6579
+ formatCallDetail: (a) => ({ source: a.source }),
6580
+ formatResultSummary: (result, a) => {
6581
+ const data = safeRecord(safeRecord(result).data);
6582
+ const skill = data.converterSkill;
6583
+ const sourceName = getFileName(a?.source || "");
6584
+ return skill ? `Converted ${sourceName} via ${skill}` : `Converted ${sourceName}`;
6585
+ },
6586
+ formatResultDetail: (result) => {
6587
+ const data = safeRecord(safeRecord(result).data);
6588
+ return { converterSkill: data.converterSkill, outputFile: data.outputFile };
6589
+ }
6590
+ },
6591
+ {
6592
+ name: "load_skill",
6593
+ displayName: "Load Skill",
6594
+ icon: "Sparkles",
6595
+ category: "system",
6596
+ formatCallSummary: (a) => a.name || "skill",
6597
+ formatCallDetail: (a) => ({ name: a.name }),
6598
+ formatResultSummary: (_, a) => `Loaded: ${a?.name || "skill"}`,
6599
+ formatResultDetail: () => ({})
6600
+ }
6601
+ ];
6602
+ const registry = /* @__PURE__ */ new Map();
6603
+ for (const config of configs) {
6604
+ registry.set(config.name, config);
6605
+ }
6606
+ function getToolRenderConfig(toolName) {
6607
+ return registry.get(toolName);
6608
+ }
6092
6609
  const EMPTY = {
6093
6610
  version: 1,
6094
6611
  updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
6095
- totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
6612
+ totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
6096
6613
  };
6097
6614
  function usagePath(baseDir) {
6098
6615
  return join(baseDir, "usage.json");
@@ -6102,6 +6619,9 @@ function loadUsageTotals(baseDir) {
6102
6619
  const raw = readFileSync(usagePath(baseDir), "utf-8");
6103
6620
  const parsed = JSON.parse(raw);
6104
6621
  if (!parsed?.totals) return { ...EMPTY };
6622
+ const t = parsed.totals;
6623
+ t.completionTokens ??= 0;
6624
+ t.cacheWriteTokens ??= 0;
6105
6625
  return parsed;
6106
6626
  } catch {
6107
6627
  return { ...EMPTY };
@@ -6130,15 +6650,17 @@ function writeAtomically(filePath, data) {
6130
6650
  }
6131
6651
  }
6132
6652
  }
6133
- function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cost) {
6653
+ function accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, cost) {
6134
6654
  const existing = loadUsageTotals(baseDir);
6135
6655
  const next = {
6136
6656
  version: 1,
6137
6657
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6138
6658
  totals: {
6139
- tokens: existing.totals.tokens + promptTokens + completionTokens,
6659
+ tokens: existing.totals.tokens + promptTokens + completionTokens + cachedTokens,
6140
6660
  promptTokens: existing.totals.promptTokens + promptTokens,
6661
+ completionTokens: existing.totals.completionTokens + completionTokens,
6141
6662
  cachedTokens: existing.totals.cachedTokens + cachedTokens,
6663
+ cacheWriteTokens: existing.totals.cacheWriteTokens + cacheWriteTokens,
6142
6664
  cost: existing.totals.cost + cost,
6143
6665
  calls: existing.totals.calls + 1
6144
6666
  }
@@ -6150,98 +6672,45 @@ function resetUsageTotals(baseDir) {
6150
6672
  const cleared = {
6151
6673
  version: 1,
6152
6674
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6153
- totals: { tokens: 0, promptTokens: 0, cachedTokens: 0, cost: 0, calls: 0 }
6675
+ totals: { tokens: 0, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cost: 0, calls: 0 }
6154
6676
  };
6155
6677
  writeAtomically(usagePath(baseDir), JSON.stringify(cleared, null, 2));
6156
6678
  return cleared;
6157
6679
  }
6158
6680
  function formatToolCall(tool, args) {
6159
6681
  const a = args && typeof args === "object" ? args : {};
6160
- switch (tool) {
6161
- case "literature-search":
6162
- return { label: `Search: ${(a.query || "").slice(0, 40)}${(a.query || "").length > 40 ? "..." : ""}`, icon: "search" };
6163
- case "lit-subtopic":
6164
- return { label: a._summary || "Searching sub-topic", icon: "search" };
6165
- case "lit-enrich":
6166
- return { label: a._summary || "Enriching paper metadata", icon: "search" };
6167
- case "lit-autosave":
6168
- return { label: a._summary || "Saving papers", icon: "file" };
6169
- case "data-analyze":
6170
- return { label: `Analyze: ${getFileName(a.filePath || "") || "data"}`, icon: "file" };
6171
- case "convert_to_markdown": {
6172
- const sourcePath = a.path || a.uri || "";
6173
- return { label: `Convert: ${getFileName(sourcePath)}`, icon: "file" };
6174
- }
6175
- case "artifact-create": {
6176
- const type = (a.type || "artifact").toLowerCase();
6177
- const title = (a.title || type).slice(0, 35);
6178
- return { label: `Create ${type}: ${title}`, icon: "file" };
6179
- }
6180
- case "read":
6181
- return { label: `Read: ${getFileName(a.path || "")}`, icon: "file" };
6182
- case "write":
6183
- return { label: `Write: ${getFileName(a.path || "")}`, icon: "file" };
6184
- case "edit":
6185
- return { label: `Edit: ${getFileName(a.path || "")}`, icon: "file" };
6186
- case "bash":
6187
- return { label: `Run command`, icon: "terminal" };
6188
- case "glob":
6189
- return { label: `Search files: ${a.pattern || ""}`, icon: "search" };
6190
- case "grep":
6191
- return { label: `Search content: ${(a.pattern || "").slice(0, 30)}`, icon: "search" };
6192
- case "fetch":
6193
- return { label: `Fetch: ${(a.url || "").slice(0, 40)}`, icon: "network" };
6194
- default:
6195
- return { label: `${tool}`, icon: "tool" };
6682
+ const config = getToolRenderConfig(tool);
6683
+ if (config) {
6684
+ return {
6685
+ label: `${config.displayName}: ${config.formatCallSummary(a)}`,
6686
+ icon: config.icon,
6687
+ detail: config.formatCallDetail(a)
6688
+ };
6196
6689
  }
6690
+ return { label: `${tool}`, icon: "tool" };
6197
6691
  }
6198
6692
  function formatToolResult(tool, result, args) {
6199
- const r = result && typeof result === "object" ? result : {};
6200
6693
  const a = args && typeof args === "object" ? args : {};
6201
- const data = r.data && typeof r.data === "object" ? r.data : {};
6202
- switch (tool) {
6203
- case "literature-search": {
6204
- const totalFound = data.totalPapersFound ?? 0;
6205
- const saved = data.papersAutoSaved ?? 0;
6206
- const coverage = data.coverage;
6207
- if (totalFound > 0) {
6208
- let summary2 = `Found ${totalFound} papers`;
6209
- if (coverage?.score != null) summary2 += ` (coverage: ${Math.round(coverage.score * 100)}%)`;
6210
- if (saved > 0) summary2 += `, saved ${saved}`;
6211
- return { label: summary2, icon: "search" };
6212
- }
6213
- const local = data.localPapersUsed ?? 0;
6214
- const external = data.externalPapersUsed ?? 0;
6215
- const savedV1 = data.savedPapers ?? 0;
6216
- let summary = `Found ${local + external} papers`;
6217
- if (local > 0) summary += ` (${local} local)`;
6218
- if (savedV1 > 0) summary += `, saved ${savedV1}`;
6219
- return { label: summary, icon: "search" };
6220
- }
6221
- case "lit-subtopic":
6222
- return { label: r.data || "Search completed", icon: "search" };
6223
- case "lit-enrich":
6224
- return { label: r.data || "Enriched metadata", icon: "search" };
6225
- case "lit-autosave":
6226
- return { label: r.data || "Saved papers", icon: "file" };
6227
- case "convert_to_markdown": {
6228
- const sourcePath = a.path || a.uri || "";
6229
- const skill = typeof data.converterSkill === "string" ? data.converterSkill : "";
6230
- const script = typeof data.converterScript === "string" ? data.converterScript : "";
6231
- if (skill && script) return { label: `Converted ${getFileName(sourcePath)} via ${skill}/${script}`, icon: "file" };
6232
- if (skill) return { label: `Converted ${getFileName(sourcePath)} via ${skill}`, icon: "file" };
6233
- return { label: `Converted ${getFileName(sourcePath)}`, icon: "file" };
6234
- }
6235
- case "artifact-create": {
6236
- const type = data.type || "artifact";
6237
- const title = data.title || "";
6238
- return { label: title ? `Created ${type}: ${title.slice(0, 30)}` : `Created ${type}`, icon: "file" };
6239
- }
6240
- default: {
6241
- const success2 = r.success !== false;
6242
- return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool" };
6243
- }
6694
+ const r = result && typeof result === "object" ? result : {};
6695
+ const success2 = r.success !== false;
6696
+ const config = getToolRenderConfig(tool);
6697
+ if (config && success2) {
6698
+ return {
6699
+ label: config.formatResultSummary(result, a),
6700
+ icon: config.icon,
6701
+ detail: config.formatResultDetail(result, a)
6702
+ };
6244
6703
  }
6704
+ if (config && !success2) {
6705
+ const errorMsg = r.error || "";
6706
+ const brief = errorMsg.length > 60 ? errorMsg.slice(0, 57) + "..." : errorMsg;
6707
+ return {
6708
+ label: brief ? `${config.displayName} failed: ${brief}` : `${config.displayName} failed`,
6709
+ icon: config.icon,
6710
+ detail: config.formatResultDetail(result, a)
6711
+ };
6712
+ }
6713
+ return { label: success2 ? `${tool} completed` : `${tool} failed`, icon: "tool", detail: { success: success2 } };
6245
6714
  }
6246
6715
  const windowStates = /* @__PURE__ */ new Map();
6247
6716
  let ipcHandlersRegistered = false;
@@ -6393,13 +6862,15 @@ async function ensureCoordinator(state, win, model, options) {
6393
6862
  state.realtimeBuffer.appendChunk(chunk);
6394
6863
  safeSend(win, "agent:stream-chunk", chunk);
6395
6864
  },
6396
- onToolCall: (tool, args) => {
6397
- const summary = formatToolCall(tool, args).label;
6398
- const event = { type: "tool-call", tool, summary };
6865
+ onToolCall: (tool, args, toolCallId) => {
6866
+ const id = toolCallId || randomUUID();
6867
+ const { label, detail } = formatToolCall(tool, args);
6868
+ const event = { type: "tool-call", tool, toolCallId: id, summary: label, detail };
6399
6869
  state.realtimeBuffer.pushActivity(event);
6870
+ state.realtimeBuffer.pushToolEvent({ type: "tool-call", tool, toolCallId: id, summary: label, detail });
6400
6871
  safeSend(win, "agent:activity", event);
6401
6872
  },
6402
- onToolResult: (tool, result, args) => {
6873
+ onToolResult: (tool, result, args, toolCallId) => {
6403
6874
  if (tool.startsWith("todo-") && result && typeof result === "object" && "success" in result) {
6404
6875
  const r2 = result;
6405
6876
  if (r2.success && r2.item) {
@@ -6414,16 +6885,16 @@ async function ensureCoordinator(state, win, model, options) {
6414
6885
  safeSend(win, "agent:file-created", absPath);
6415
6886
  }
6416
6887
  }
6417
- if (tool === "convert_to_markdown" && result && typeof result === "object" && "success" in result) {
6888
+ if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
6418
6889
  const r2 = result;
6419
6890
  if (r2.success && r2.data?.outputFile) {
6420
6891
  safeSend(win, "agent:file-created", r2.data.outputFile);
6421
6892
  }
6422
6893
  }
6423
- if (tool === "convert_to_markdown" && result && typeof result === "object" && "success" in result) {
6894
+ if (tool === "convert_document" && result && typeof result === "object" && "success" in result) {
6424
6895
  const r2 = result;
6425
- if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "path" in args) {
6426
- const sourcePath = args.path;
6896
+ if (r2.success && r2.data?.outputFile && args && typeof args === "object" && "source" in args) {
6897
+ const sourcePath = args.source;
6427
6898
  const absSourcePath = isAbsolute(sourcePath) ? sourcePath : resolve(runProjectPath, sourcePath);
6428
6899
  const absOutputPath = resolve(runProjectPath, r2.data.outputFile);
6429
6900
  if (existsSync(absOutputPath)) {
@@ -6457,11 +6928,26 @@ async function ensureCoordinator(state, win, model, options) {
6457
6928
  const r = result;
6458
6929
  const success2 = r?.success !== false;
6459
6930
  const error = !success2 ? r?.error || "Unknown error" : void 0;
6460
- const summary = formatToolResult(tool, result, args).label;
6461
- const actEvent = { type: "tool-result", tool, summary, success: success2, error };
6931
+ const { label: resultLabel, detail: resultDetail } = formatToolResult(tool, result, args);
6932
+ const startTime = toolCallId ? state.realtimeBuffer.popToolCallStartTime(toolCallId) : void 0;
6933
+ const durationMs = startTime ? Date.now() - startTime : void 0;
6934
+ const actEvent = { type: "tool-result", tool, toolCallId, summary: resultLabel, success: success2, error, resultDetail, durationMs };
6462
6935
  state.realtimeBuffer.pushActivity(actEvent);
6936
+ if (toolCallId) {
6937
+ state.realtimeBuffer.updateToolEvent(toolCallId, {
6938
+ type: "tool-result",
6939
+ summary: resultLabel,
6940
+ success: success2,
6941
+ resultDetail,
6942
+ durationMs
6943
+ });
6944
+ }
6463
6945
  safeSend(win, "agent:activity", actEvent);
6464
6946
  },
6947
+ // Tool execution progress (real-time updates during tool execution)
6948
+ onToolProgress: (tool, toolCallId, phase, data) => {
6949
+ safeSend(win, "agent:tool-progress", { tool, toolCallId, phase, data, timestamp: Date.now() });
6950
+ },
6465
6951
  // Skill activation tracking
6466
6952
  onSkillLoaded: (skillName) => {
6467
6953
  safeSend(win, "agent:skill-loaded", skillName);
@@ -6473,12 +6959,14 @@ async function ensureCoordinator(state, win, model, options) {
6473
6959
  const promptTokens = usage.input ?? 0;
6474
6960
  const completionTokens = usage.output ?? 0;
6475
6961
  const cachedTokens = usage.cacheRead ?? 0;
6962
+ const cacheWriteTokens = usage.cacheWrite ?? 0;
6476
6963
  const baseDir = join(runProjectPath, PATHS.root);
6477
- accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, rawCost);
6964
+ accumulateUsage(baseDir, promptTokens, completionTokens, cachedTokens, cacheWriteTokens, rawCost);
6478
6965
  const usageEvent = {
6479
6966
  promptTokens,
6480
6967
  completionTokens,
6481
6968
  cachedTokens,
6969
+ cacheWriteTokens,
6482
6970
  cost: rawCost,
6483
6971
  rawCost,
6484
6972
  billableCost: rawCost,
@@ -7104,7 +7592,11 @@ function destroyAllTerminals() {
7104
7592
  terminals.delete(id);
7105
7593
  }
7106
7594
  }
7595
+ setMaxListeners(20);
7107
7596
  loadApiKeysFromConfig();
7597
+ if (!process.env.PI_CACHE_RETENTION) {
7598
+ process.env.PI_CACHE_RETENTION = "long";
7599
+ }
7108
7600
  if (process.platform === "darwin" && !is.dev) {
7109
7601
  try {
7110
7602
  const shellPath = process.env.SHELL || "/bin/zsh";