research-copilot 0.1.2 → 0.2.0
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/README.md +22 -0
- package/app/out/main/index.mjs +609 -64
- package/app/out/renderer/assets/{MilkdownMarkdownEditor-CCiFOpuq.js → MilkdownMarkdownEditor-Czh2N6UQ.js} +50 -50
- package/app/out/renderer/assets/{arc-BR5G9xaE.js → arc-BWoErJNa.js} +1 -1
- package/app/out/renderer/assets/{blockDiagram-c4efeb88-JmvDTsGU.js → blockDiagram-c4efeb88-Bod-vAlS.js} +8 -8
- package/app/out/renderer/assets/{c4Diagram-c83219d4-Daf_3gE1.js → c4Diagram-c83219d4-CTVUA_li.js} +3 -3
- package/app/out/renderer/assets/{channel-xtutyETs.js → channel-CxGr5Q5E.js} +1 -1
- package/app/out/renderer/assets/{classDiagram-beda092f-BFWEqrCW.js → classDiagram-beda092f-DABwUrsU.js} +6 -6
- package/app/out/renderer/assets/{classDiagram-v2-2358418a-BQw7RI0A.js → classDiagram-v2-2358418a-CFt8hqf5.js} +10 -10
- package/app/out/renderer/assets/{clone-uoV60hcB.js → clone-BL91dKYn.js} +1 -1
- package/app/out/renderer/assets/{createText-1719965b-BaRII2sm.js → createText-1719965b-DGkv4rEO.js} +2 -2
- package/app/out/renderer/assets/{edges-96097737-CL7Yc4hz.js → edges-96097737-Gf41lQOd.js} +3 -3
- package/app/out/renderer/assets/{erDiagram-0228fc6a-B9hgyxu6.js → erDiagram-0228fc6a-Dj75BiRy.js} +5 -5
- package/app/out/renderer/assets/{flowDb-c6c81e3f-b_RS-jIJ.js → flowDb-c6c81e3f-C_xVBMxS.js} +1 -1
- package/app/out/renderer/assets/{flowDiagram-50d868cf-CPB3IueC.js → flowDiagram-50d868cf-B-lLn2XC.js} +12 -12
- package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-DM8cFvdZ.js → flowDiagram-v2-4f6560a1-BFnLU3PE.js} +12 -12
- package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-BsxABHy9.js → flowchart-elk-definition-6af322e1-DmjfyXbt.js} +6 -6
- package/app/out/renderer/assets/{ganttDiagram-a2739b55-DpMib95K.js → ganttDiagram-a2739b55-BTPRekAy.js} +3 -3
- package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-C0OtwErh.js → gitGraphDiagram-82fe8481-1riYxgGS.js} +2 -2
- package/app/out/renderer/assets/{graph-CXef_RHM.js → graph-CvDtMlX-.js} +1 -1
- package/app/out/renderer/assets/{index-Bg4LHaeu.js → index-0kPJXDfu.js} +3 -3
- package/app/out/renderer/assets/{index-CTF1A-5m.js → index-3LdRym1K.js} +3 -3
- package/app/out/renderer/assets/{index-5325376f-0FtzFTBH.js → index-5325376f-BGaoNMNN.js} +6 -6
- package/app/out/renderer/assets/{index-Crf9Pipm.js → index-8tvmsRje.js} +3 -3
- package/app/out/renderer/assets/{index-CMDsy41q.js → index-B4djqBxS.js} +1 -1
- package/app/out/renderer/assets/{index-CKjCQ1EB.js → index-B9lieynj.js} +6 -6
- package/app/out/renderer/assets/{index-Cx3Vwh3q.js → index-BCOrnr8q.js} +4 -4
- package/app/out/renderer/assets/{index-DRyElXV-.js → index-BK5rYWMs.js} +5 -5
- package/app/out/renderer/assets/{index-vGIhunyU.js → index-BVNrdWzl.js} +6 -6
- package/app/out/renderer/assets/{index-BBH0Chbw.js → index-BgSz3yUy.js} +6 -6
- package/app/out/renderer/assets/{index-Dy2bySYF.js → index-Bii7x9Rr.js} +3 -3
- package/app/out/renderer/assets/{index-CFaiDIr7.js → index-BnRwUKpv.js} +3 -3
- package/app/out/renderer/assets/{index-uZnv8lTU.js → index-BxOmAXUZ.js} +3 -3
- package/app/out/renderer/assets/{index-C_cgOzmt.js → index-CUPy7R5v.js} +39 -20
- package/app/out/renderer/assets/{index-DeVfJmHc.js → index-CXN1f9OT.js} +3 -3
- package/app/out/renderer/assets/{index-BE4XBnng.js → index-CnL9yPzK.js} +3 -3
- package/app/out/renderer/assets/{index-C-_uCjZJ.css → index-Ctwkk-AW.css} +2 -10
- package/app/out/renderer/assets/{index-BHo8axTp.js → index-D2fFfHUR.js} +6 -6
- package/app/out/renderer/assets/{index-DuAPj57k.js → index-DrvR7Peq.js} +3 -3
- package/app/out/renderer/assets/{index-CabfPYgf.js → index-NHbUPOmb.js} +3 -3
- package/app/out/renderer/assets/{index-tz7ZKjP9.js → index-O3gvL3-Z.js} +3 -3
- package/app/out/renderer/assets/{index-BohTbJeP.js → index-cAZJ88Np.js} +6 -6
- package/app/out/renderer/assets/{index-OqY0JVi2.js → index-qS7qbXvX.js} +3 -3
- package/app/out/renderer/assets/{index-D9-3cc7l.js → index-y5XZ-0EB.js} +4 -4
- package/app/out/renderer/assets/{index-C1Hf3CJw.js → index-zr8uxb8p.js} +6 -6
- package/app/out/renderer/assets/{infoDiagram-8eee0895-CPFVhSvg.js → infoDiagram-8eee0895-Cq8aXV8u.js} +2 -2
- package/app/out/renderer/assets/{journeyDiagram-c64418c1-PKaxJ2mn.js → journeyDiagram-c64418c1-D4ewDrYD.js} +4 -4
- package/app/out/renderer/assets/{layout-CawlN23W.js → layout-CZmLZO9t.js} +2 -2
- package/app/out/renderer/assets/{line-C_cMMDTP.js → line-D7kWOiRx.js} +1 -1
- package/app/out/renderer/assets/{linear-CnzgpVoT.js → linear-B055Dz0c.js} +1 -1
- package/app/out/renderer/assets/{mindmap-definition-8da855dc-2dVBAm3g.js → mindmap-definition-8da855dc-D6EW4QCj.js} +3 -3
- package/app/out/renderer/assets/{pieDiagram-a8764435-p6hNN8aY.js → pieDiagram-a8764435-BX_Dz4T9.js} +3 -3
- package/app/out/renderer/assets/{quadrantDiagram-1e28029f-JJT_eOsi.js → quadrantDiagram-1e28029f-BsI6xGsm.js} +3 -3
- package/app/out/renderer/assets/{requirementDiagram-08caed73-Ck4auzva.js → requirementDiagram-08caed73-c2d8T0BS.js} +5 -5
- package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-DICtg7Jw.js → sankeyDiagram-a04cb91d-CkDhRKRC.js} +2 -2
- package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-Bv7njQoz.js → sequenceDiagram-c5b8d532-DS0RKYnD.js} +3 -3
- package/app/out/renderer/assets/{stateDiagram-1ecb1508-CmuiBQ0q.js → stateDiagram-1ecb1508-BjTK27QX.js} +6 -6
- package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-DErEMYcv.js → stateDiagram-v2-c2b004d7-D1wWbeR3.js} +10 -10
- package/app/out/renderer/assets/{styles-b4e223ce-CEjXkOYY.js → styles-b4e223ce-DXUfbXTM.js} +1 -1
- package/app/out/renderer/assets/{styles-ca3715f6-BJWKCKia.js → styles-ca3715f6-CE_JRTmB.js} +1 -1
- package/app/out/renderer/assets/{styles-d45a18b0-BrhRky7i.js → styles-d45a18b0-CdtAXXSE.js} +4 -4
- package/app/out/renderer/assets/{svgDrawCommon-b86b1483-RWkoQoOd.js → svgDrawCommon-b86b1483-dCxPWgBl.js} +1 -1
- package/app/out/renderer/assets/{timeline-definition-faaaa080-25xmyyis.js → timeline-definition-faaaa080-B7ZP3Dqw.js} +3 -3
- package/app/out/renderer/assets/{xychartDiagram-f5964ef8-DwBkod9W.js → xychartDiagram-f5964ef8-CXagmo1Q.js} +5 -5
- package/app/out/renderer/index.html +2 -2
- package/bin/cli.mjs +4 -1
- package/package.json +1 -1
package/app/out/main/index.mjs
CHANGED
|
@@ -2,10 +2,10 @@ import { app, shell, ipcMain, BrowserWindow, dialog, Menu } from "electron";
|
|
|
2
2
|
import fs, { existsSync as existsSync$1 } from "node:fs";
|
|
3
3
|
import { execFile, execSync } from "node:child_process";
|
|
4
4
|
import { resolve, join, sep, isAbsolute, extname, basename, dirname, relative } from "path";
|
|
5
|
-
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync,
|
|
5
|
+
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync } from "fs";
|
|
6
6
|
import os$1, { homedir } from "os";
|
|
7
7
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
8
|
-
import {
|
|
8
|
+
import { completeSimple, getModel } from "@mariozechner/pi-ai";
|
|
9
9
|
import { createCodingTools, createGrepTool, createFindTool, createLsTool, DEFAULT_COMPACTION_SETTINGS, estimateTokens, shouldCompact, generateSummary } from "@mariozechner/pi-coding-agent";
|
|
10
10
|
import { Type } from "@sinclair/typebox";
|
|
11
11
|
import path from "node:path";
|
|
@@ -15,6 +15,7 @@ import { promisify } from "node:util";
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { createHash as createHash$1 } from "crypto";
|
|
18
|
+
import { execFile as execFile$1 } from "child_process";
|
|
18
19
|
import __cjs_mod__ from "node:module";
|
|
19
20
|
const __filename = import.meta.filename;
|
|
20
21
|
const __dirname = import.meta.dirname;
|
|
@@ -634,6 +635,8 @@ const PATHS = {
|
|
|
634
635
|
memoryRoot: ".research-pilot/memory-v2",
|
|
635
636
|
explainDir: ".research-pilot/memory-v2/explain",
|
|
636
637
|
sessionSummaries: ".research-pilot/memory-v2/session-summaries",
|
|
638
|
+
// Structured long-term memory (auto-memory)
|
|
639
|
+
memory: ".research-pilot/memory",
|
|
637
640
|
// Skills
|
|
638
641
|
skills: ".research-pilot/skills",
|
|
639
642
|
skillsConfig: ".research-pilot/skills-config.json"
|
|
@@ -1251,6 +1254,263 @@ function createResearchMemoryTools(params) {
|
|
|
1251
1254
|
createArtifactSearchTool(params.projectPath)
|
|
1252
1255
|
];
|
|
1253
1256
|
}
|
|
1257
|
+
function slugify(text) {
|
|
1258
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
1259
|
+
}
|
|
1260
|
+
function memoryFilename(type, name) {
|
|
1261
|
+
return `${type}_${slugify(name)}.md`;
|
|
1262
|
+
}
|
|
1263
|
+
function memoryDir(projectPath) {
|
|
1264
|
+
return join(projectPath, PATHS.memory);
|
|
1265
|
+
}
|
|
1266
|
+
function ensureMemoryDir(projectPath) {
|
|
1267
|
+
const dir = memoryDir(projectPath);
|
|
1268
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1269
|
+
}
|
|
1270
|
+
function formatFrontmatter(fm) {
|
|
1271
|
+
return [
|
|
1272
|
+
"---",
|
|
1273
|
+
`name: ${fm.name}`,
|
|
1274
|
+
`description: ${fm.description}`,
|
|
1275
|
+
`type: ${fm.type}`,
|
|
1276
|
+
"---"
|
|
1277
|
+
].join("\n");
|
|
1278
|
+
}
|
|
1279
|
+
function parseFrontmatter(text) {
|
|
1280
|
+
const match = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
1281
|
+
if (!match) return null;
|
|
1282
|
+
const fm = {};
|
|
1283
|
+
for (const line of match[1].split("\n")) {
|
|
1284
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
1285
|
+
if (kv) fm[kv[1]] = kv[2].trim();
|
|
1286
|
+
}
|
|
1287
|
+
if (!fm.name || !fm.type) return null;
|
|
1288
|
+
const validTypes = ["user", "feedback", "project", "reference"];
|
|
1289
|
+
if (!validTypes.includes(fm.type)) return null;
|
|
1290
|
+
return {
|
|
1291
|
+
frontmatter: { name: fm.name, description: fm.description || "", type: fm.type },
|
|
1292
|
+
body: match[2].trim()
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function writeMemoryFile(projectPath, entry) {
|
|
1296
|
+
ensureMemoryDir(projectPath);
|
|
1297
|
+
const filePath = join(memoryDir(projectPath), entry.filename);
|
|
1298
|
+
const content = `${formatFrontmatter(entry.frontmatter)}
|
|
1299
|
+
|
|
1300
|
+
${entry.content}
|
|
1301
|
+
`;
|
|
1302
|
+
writeFileSync(filePath, content, "utf-8");
|
|
1303
|
+
return filePath;
|
|
1304
|
+
}
|
|
1305
|
+
function readMemoryFile(projectPath, filename) {
|
|
1306
|
+
const filePath = join(memoryDir(projectPath), filename);
|
|
1307
|
+
if (!existsSync(filePath)) return null;
|
|
1308
|
+
try {
|
|
1309
|
+
const text = readFileSync(filePath, "utf-8");
|
|
1310
|
+
const parsed = parseFrontmatter(text);
|
|
1311
|
+
if (!parsed) return null;
|
|
1312
|
+
return { frontmatter: parsed.frontmatter, content: parsed.body, filename };
|
|
1313
|
+
} catch {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function deleteMemoryFile(projectPath, filename) {
|
|
1318
|
+
const filePath = join(memoryDir(projectPath), filename);
|
|
1319
|
+
if (!existsSync(filePath)) return false;
|
|
1320
|
+
try {
|
|
1321
|
+
unlinkSync(filePath);
|
|
1322
|
+
return true;
|
|
1323
|
+
} catch {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
function listMemoryFiles(projectPath) {
|
|
1328
|
+
const dir = memoryDir(projectPath);
|
|
1329
|
+
if (!existsSync(dir)) return [];
|
|
1330
|
+
try {
|
|
1331
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
|
|
1332
|
+
const entries = [];
|
|
1333
|
+
for (const filename of files) {
|
|
1334
|
+
const entry = readMemoryFile(projectPath, filename);
|
|
1335
|
+
if (entry) entries.push(entry);
|
|
1336
|
+
}
|
|
1337
|
+
return entries;
|
|
1338
|
+
} catch {
|
|
1339
|
+
return [];
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function findMemoryByName(projectPath, name) {
|
|
1343
|
+
const lower = name.toLowerCase();
|
|
1344
|
+
return listMemoryFiles(projectPath).find((e) => e.frontmatter.name.toLowerCase() === lower) ?? null;
|
|
1345
|
+
}
|
|
1346
|
+
function buildMemoryIndex(entries) {
|
|
1347
|
+
if (entries.length === 0) return "";
|
|
1348
|
+
return entries.map((e) => {
|
|
1349
|
+
const desc = e.frontmatter.description.slice(0, 100).replace(/\n/g, " ");
|
|
1350
|
+
return `- [${e.frontmatter.name}](memory/${e.filename}) — ${desc}`;
|
|
1351
|
+
}).join("\n");
|
|
1352
|
+
}
|
|
1353
|
+
function updateAgentMdIndex(projectPath, entries) {
|
|
1354
|
+
const record = findArtifactById(projectPath, AGENT_MD_ID);
|
|
1355
|
+
const currentContent = record?.artifact?.type === "note" ? record.artifact.content || "" : "";
|
|
1356
|
+
const marker = "## Agent Memory";
|
|
1357
|
+
const markerIdx = currentContent.indexOf(marker);
|
|
1358
|
+
const userInstructions = markerIdx >= 0 ? currentContent.slice(0, markerIdx).trimEnd() : currentContent.trimEnd();
|
|
1359
|
+
const indexContent = buildMemoryIndex(entries);
|
|
1360
|
+
const newContent = indexContent ? `${userInstructions}
|
|
1361
|
+
|
|
1362
|
+
${marker}
|
|
1363
|
+
|
|
1364
|
+
${indexContent}
|
|
1365
|
+
` : `${userInstructions}
|
|
1366
|
+
|
|
1367
|
+
${marker}
|
|
1368
|
+
`;
|
|
1369
|
+
if (newContent.length > AGENT_MD_MAX_CHARS) {
|
|
1370
|
+
return { success: false, charCount: newContent.length };
|
|
1371
|
+
}
|
|
1372
|
+
updateArtifact(projectPath, AGENT_MD_ID, { content: newContent });
|
|
1373
|
+
return { success: true, charCount: newContent.length };
|
|
1374
|
+
}
|
|
1375
|
+
function migrateAgentMemoryToFile(projectPath) {
|
|
1376
|
+
const record = findArtifactById(projectPath, AGENT_MD_ID);
|
|
1377
|
+
if (!record) return false;
|
|
1378
|
+
const content = record.artifact?.type === "note" ? record.artifact.content || "" : "";
|
|
1379
|
+
const marker = "## Agent Memory";
|
|
1380
|
+
const markerIdx = content.indexOf(marker);
|
|
1381
|
+
if (markerIdx < 0) return false;
|
|
1382
|
+
const agentMemory = content.slice(markerIdx + marker.length).trim();
|
|
1383
|
+
if (!agentMemory || /\[.*\]\(memory\/.*\)/.test(agentMemory)) return false;
|
|
1384
|
+
ensureMemoryDir(projectPath);
|
|
1385
|
+
const entry = {
|
|
1386
|
+
frontmatter: {
|
|
1387
|
+
name: "Legacy notes",
|
|
1388
|
+
description: "Migrated from agent.md Agent Memory section",
|
|
1389
|
+
type: "project"
|
|
1390
|
+
},
|
|
1391
|
+
content: agentMemory,
|
|
1392
|
+
filename: memoryFilename("project", "legacy-notes")
|
|
1393
|
+
};
|
|
1394
|
+
writeMemoryFile(projectPath, entry);
|
|
1395
|
+
const allEntries = listMemoryFiles(projectPath);
|
|
1396
|
+
updateAgentMdIndex(projectPath, allEntries);
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
const VALID_TYPES$1 = ["user", "feedback", "project", "reference"];
|
|
1400
|
+
function createSaveMemoryTool(projectPath) {
|
|
1401
|
+
return {
|
|
1402
|
+
name: "save-memory",
|
|
1403
|
+
description: 'Save a memory to long-term storage. Each memory becomes a file in .research-pilot/memory/ and an index entry in agent.md (visible every turn). Types: "user" (preferences/background), "feedback" (corrections to behavior), "project" (decisions/deadlines/context), "reference" (external pointers/reusable info). If a memory with the same name+type exists, it is updated.',
|
|
1404
|
+
parameters: {
|
|
1405
|
+
type: "object",
|
|
1406
|
+
properties: {
|
|
1407
|
+
type: {
|
|
1408
|
+
type: "string",
|
|
1409
|
+
enum: VALID_TYPES$1,
|
|
1410
|
+
description: "Memory category"
|
|
1411
|
+
},
|
|
1412
|
+
name: {
|
|
1413
|
+
type: "string",
|
|
1414
|
+
description: "Short identifier (used as title and filename slug)"
|
|
1415
|
+
},
|
|
1416
|
+
content: {
|
|
1417
|
+
type: "string",
|
|
1418
|
+
description: "The memory content (markdown). Keep it concise and focused."
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1421
|
+
required: ["type", "name", "content"]
|
|
1422
|
+
},
|
|
1423
|
+
execute: async (input) => {
|
|
1424
|
+
const type = String(input.type || "");
|
|
1425
|
+
const name = String(input.name || "").trim();
|
|
1426
|
+
const content = String(input.content || "").trim();
|
|
1427
|
+
if (!VALID_TYPES$1.includes(type)) {
|
|
1428
|
+
return toolError("INVALID_PARAMETER", `Invalid memory type: ${type}. Must be one of: ${VALID_TYPES$1.join(", ")}`, {
|
|
1429
|
+
suggestions: ['Use "user", "feedback", "project", or "reference".']
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
|
|
1433
|
+
suggestions: ["Provide a short descriptive name for this memory."]
|
|
1434
|
+
});
|
|
1435
|
+
if (!content) return toolError("MISSING_PARAMETER", "content is required.", {
|
|
1436
|
+
suggestions: ["Provide the memory content to save."]
|
|
1437
|
+
});
|
|
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
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function createDeleteMemoryTool(projectPath) {
|
|
1471
|
+
return {
|
|
1472
|
+
name: "delete-memory",
|
|
1473
|
+
description: "Delete a memory by name. Removes the file and its index entry in agent.md.",
|
|
1474
|
+
parameters: {
|
|
1475
|
+
type: "object",
|
|
1476
|
+
properties: {
|
|
1477
|
+
name: {
|
|
1478
|
+
type: "string",
|
|
1479
|
+
description: "Name of the memory to delete (case-insensitive match)"
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
required: ["name"]
|
|
1483
|
+
},
|
|
1484
|
+
execute: async (input) => {
|
|
1485
|
+
const name = String(input.name || "").trim();
|
|
1486
|
+
if (!name) return toolError("MISSING_PARAMETER", "name is required.", {
|
|
1487
|
+
suggestions: ["Provide the name of the memory to delete."]
|
|
1488
|
+
});
|
|
1489
|
+
const existing = findMemoryByName(projectPath, name);
|
|
1490
|
+
if (!existing) {
|
|
1491
|
+
return toolError("NOT_FOUND", `Memory not found: "${name}"`, {
|
|
1492
|
+
suggestions: ["Check the memory name — it is case-insensitive. Current memories are listed in agent.md."]
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
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
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function createMemoryTools(projectPath) {
|
|
1509
|
+
return [
|
|
1510
|
+
createSaveMemoryTool(projectPath),
|
|
1511
|
+
createDeleteMemoryTool(projectPath)
|
|
1512
|
+
];
|
|
1513
|
+
}
|
|
1254
1514
|
const WEB_DEFAULTS = {
|
|
1255
1515
|
braveMinIntervalMs: 200,
|
|
1256
1516
|
arxivMinIntervalMs: 3e3,
|
|
@@ -1696,12 +1956,20 @@ Memory model:
|
|
|
1696
1956
|
- Session context is maintained automatically via periodic summaries.
|
|
1697
1957
|
- For quick-reference info, create a note via artifact-create({ type: "note", ... }).
|
|
1698
1958
|
|
|
1699
|
-
Long-term memory (
|
|
1700
|
-
- agent.md
|
|
1701
|
-
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1959
|
+
Long-term memory (auto-memory):
|
|
1960
|
+
- Your agent.md "## Agent Memory" section shows an index of saved memories. It is injected into your context every turn, so you always see what is remembered.
|
|
1961
|
+
- Use save-memory to persist information across sessions. Each memory becomes a file in .research-pilot/memory/ with one of four types:
|
|
1962
|
+
* user — who the user is: role, expertise, preferences, communication style
|
|
1963
|
+
* feedback — corrections to your behavior: "don't do X", "when I say Y I mean Z"
|
|
1964
|
+
* project — key decisions, deadlines, collaborators, research directions
|
|
1965
|
+
* reference — pointers to external resources, reusable facts, definitions
|
|
1966
|
+
- Use delete-memory to remove outdated entries by name.
|
|
1967
|
+
- WHEN to save: user explicitly states a preference, corrects your behavior, a non-obvious project decision is made, or user points to an external resource. Most turns do NOT warrant saving a memory — only save when you learn something genuinely new that a future session would need.
|
|
1968
|
+
- WHEN NOT to save: routine task results, things already in workspace files or git, ephemeral conversation details, information derivable from the codebase, anything already captured in an existing memory.
|
|
1969
|
+
- Before saving, check agent.md index — if a similar memory exists, update it instead of creating a duplicate.
|
|
1970
|
+
- Keep each memory atomic (one concept) and concise.
|
|
1971
|
+
- Note: save-memory is for cross-session meta-information (preferences, context). Use artifact-create for work products (notes, analysis results, review memos).
|
|
1972
|
+
- You can read full memory files with the read tool at .research-pilot/memory/<filename>.
|
|
1705
1973
|
|
|
1706
1974
|
Coding tasks:
|
|
1707
1975
|
- For code implementation, follow test-first workflow: write/update test → confirm it fails → implement → confirm it passes.
|
|
@@ -3678,15 +3946,139 @@ function createResearchTools(ctx) {
|
|
|
3678
3946
|
tools.push(createLiteratureSearchTool(ctx));
|
|
3679
3947
|
tools.push(createConvertDocumentTool(ctx));
|
|
3680
3948
|
tools.push(createDataAnalyzeTool(ctx));
|
|
3681
|
-
const
|
|
3949
|
+
const artifactTools = createResearchMemoryTools({
|
|
3682
3950
|
sessionId: ctx.sessionId,
|
|
3683
3951
|
projectPath: ctx.projectPath
|
|
3684
3952
|
});
|
|
3685
|
-
for (const tool of
|
|
3953
|
+
for (const tool of artifactTools) {
|
|
3954
|
+
tools.push(wrapResearchTool(tool));
|
|
3955
|
+
}
|
|
3956
|
+
const structuredMemoryTools = createMemoryTools(ctx.projectPath);
|
|
3957
|
+
for (const tool of structuredMemoryTools) {
|
|
3686
3958
|
tools.push(wrapResearchTool(tool));
|
|
3687
3959
|
}
|
|
3688
3960
|
return tools;
|
|
3689
3961
|
}
|
|
3962
|
+
const VALID_TYPES = ["user", "feedback", "project", "reference"];
|
|
3963
|
+
const EXTRACTION_PROMPT = `Analyze the recent conversation above and extract information worth remembering across sessions.
|
|
3964
|
+
|
|
3965
|
+
Rules:
|
|
3966
|
+
- Only extract DURABLE, IMPORTANT information — things a future session would need.
|
|
3967
|
+
- Types: "user" (preferences/background), "feedback" (behavior corrections), "project" (decisions/deadlines), "reference" (external pointers).
|
|
3968
|
+
- Ignore text inside "[Previous conversation summary]" or "[Session context]" markers — that is old context, not new information.
|
|
3969
|
+
- Do NOT extract: routine task results, ephemeral details, things already in workspace files.
|
|
3970
|
+
- Each memory should be atomic — one concept per entry.
|
|
3971
|
+
- If nothing is worth saving, return an empty array.
|
|
3972
|
+
|
|
3973
|
+
Return ONLY a JSON array (no markdown fences, no explanation):
|
|
3974
|
+
[{"type":"user|feedback|project|reference","name":"short-name","description":"one line","content":"full text"}]
|
|
3975
|
+
Or: []`;
|
|
3976
|
+
function simplifyMessages(messages, maxMessages) {
|
|
3977
|
+
const recent = messages.slice(-20);
|
|
3978
|
+
const result = [];
|
|
3979
|
+
for (const msg of recent) {
|
|
3980
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
3981
|
+
let content = "";
|
|
3982
|
+
if (typeof msg.content === "string") {
|
|
3983
|
+
content = msg.content;
|
|
3984
|
+
} else if (Array.isArray(msg.content)) {
|
|
3985
|
+
for (const block of msg.content) {
|
|
3986
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
3987
|
+
if (block.type === "text" && "text" in block) {
|
|
3988
|
+
const text = block.text;
|
|
3989
|
+
content += text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
|
|
3990
|
+
content += "\n";
|
|
3991
|
+
} else if (block.type === "tool_use" && "name" in block) {
|
|
3992
|
+
content += `[Called ${block.name}]
|
|
3993
|
+
`;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
content = content.trim();
|
|
3999
|
+
if (content) {
|
|
4000
|
+
result.push({
|
|
4001
|
+
role: msg.role,
|
|
4002
|
+
content: content.slice(0, 2e3),
|
|
4003
|
+
timestamp: Date.now()
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return result;
|
|
4008
|
+
}
|
|
4009
|
+
function agentCalledSaveMemoryThisTurn(messages) {
|
|
4010
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
4011
|
+
const msg = messages[i];
|
|
4012
|
+
if (msg.role === "user") break;
|
|
4013
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
4014
|
+
for (const block of msg.content) {
|
|
4015
|
+
if (block && typeof block === "object" && "type" in block && block.type === "tool_use") {
|
|
4016
|
+
if (block.name === "save-memory") return true;
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
return false;
|
|
4022
|
+
}
|
|
4023
|
+
async function maybeExtractMemories(config, messages, turnCount, extractEveryN = 3) {
|
|
4024
|
+
if (process.env.RESEARCH_COPILOT_AUTO_EXTRACT === "0") return;
|
|
4025
|
+
if (turnCount % extractEveryN !== 0) return;
|
|
4026
|
+
if (agentCalledSaveMemoryThisTurn(messages)) {
|
|
4027
|
+
if (config.debug) console.log("[Extractor] Skipped — agent called save-memory this turn");
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
try {
|
|
4031
|
+
const simplified = simplifyMessages(messages, 20);
|
|
4032
|
+
if (simplified.length < 2) return;
|
|
4033
|
+
simplified.push({
|
|
4034
|
+
role: "user",
|
|
4035
|
+
content: EXTRACTION_PROMPT,
|
|
4036
|
+
timestamp: Date.now()
|
|
4037
|
+
});
|
|
4038
|
+
const result = await completeSimple(config.model, {
|
|
4039
|
+
systemPrompt: config.systemPrompt,
|
|
4040
|
+
messages: simplified
|
|
4041
|
+
}, {
|
|
4042
|
+
maxTokens: 1024,
|
|
4043
|
+
apiKey: config.apiKey
|
|
4044
|
+
});
|
|
4045
|
+
const textContent = result.content.find((c) => c.type === "text");
|
|
4046
|
+
const text = textContent?.text?.trim() ?? "";
|
|
4047
|
+
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);
|
|
4051
|
+
if (!Array.isArray(extracted) || extracted.length === 0) return;
|
|
4052
|
+
ensureMemoryDir(config.projectPath);
|
|
4053
|
+
let written = 0;
|
|
4054
|
+
for (const mem of extracted) {
|
|
4055
|
+
if (!mem.type || !mem.name || !mem.content) continue;
|
|
4056
|
+
if (!VALID_TYPES.includes(mem.type)) continue;
|
|
4057
|
+
const entry = {
|
|
4058
|
+
frontmatter: {
|
|
4059
|
+
name: mem.name,
|
|
4060
|
+
description: (mem.description || mem.content.slice(0, 120)).replace(/\n/g, " "),
|
|
4061
|
+
type: mem.type
|
|
4062
|
+
},
|
|
4063
|
+
content: mem.content,
|
|
4064
|
+
filename: memoryFilename(mem.type, mem.name)
|
|
4065
|
+
};
|
|
4066
|
+
writeMemoryFile(config.projectPath, entry);
|
|
4067
|
+
written++;
|
|
4068
|
+
}
|
|
4069
|
+
if (written > 0) {
|
|
4070
|
+
const allEntries = listMemoryFiles(config.projectPath);
|
|
4071
|
+
updateAgentMdIndex(config.projectPath, allEntries);
|
|
4072
|
+
if (config.debug) {
|
|
4073
|
+
console.log(`[Extractor] Saved ${written} memories from conversation`);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
} catch (err) {
|
|
4077
|
+
if (config.debug) {
|
|
4078
|
+
console.warn("[Extractor] Failed:", err);
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
3690
4082
|
const SKILL_FILE_NAME = "SKILL.md";
|
|
3691
4083
|
const MAX_SCAN_DEPTH = 3;
|
|
3692
4084
|
let _builtinSkillsRoot = null;
|
|
@@ -4497,6 +4889,11 @@ ${message}`;
|
|
|
4497
4889
|
});
|
|
4498
4890
|
if (turnHistory.length > 8) turnHistory.shift();
|
|
4499
4891
|
void maybeGenerateSummary();
|
|
4892
|
+
void maybeExtractMemories(
|
|
4893
|
+
{ projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
|
|
4894
|
+
agent.state.messages,
|
|
4895
|
+
turnCount
|
|
4896
|
+
);
|
|
4500
4897
|
if (debug) {
|
|
4501
4898
|
console.log(`[Chat] Result: success=true, hasOutput=${!!responseText}, turn=${turnCount}`);
|
|
4502
4899
|
}
|
|
@@ -5054,7 +5451,7 @@ async function enrichPaperArtifacts(options) {
|
|
|
5054
5451
|
onProgress?.({ paperId: paper.id, status: "done" });
|
|
5055
5452
|
} catch (err) {
|
|
5056
5453
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
5057
|
-
{
|
|
5454
|
+
if (debug) {
|
|
5058
5455
|
console.error(`[enrich] Error enriching "${paper.title?.slice(0, 60)}":`, err);
|
|
5059
5456
|
}
|
|
5060
5457
|
failed++;
|
|
@@ -5066,7 +5463,7 @@ async function enrichPaperArtifacts(options) {
|
|
|
5066
5463
|
onProgress?.({ paperId: paper.id, status: "failed" });
|
|
5067
5464
|
}
|
|
5068
5465
|
}
|
|
5069
|
-
{
|
|
5466
|
+
if (debug) {
|
|
5070
5467
|
console.log(`[enrich] Done: ${enriched} enriched, ${skipped} skipped, ${failed} failed`);
|
|
5071
5468
|
}
|
|
5072
5469
|
return {
|
|
@@ -5156,11 +5553,11 @@ function searchEntities(projectPath, query, types) {
|
|
|
5156
5553
|
score: hit.score
|
|
5157
5554
|
}));
|
|
5158
5555
|
}
|
|
5159
|
-
const MENTION_RE = /@(note|paper|data|file|url):(?:"([^"]
|
|
5556
|
+
const MENTION_RE = /@(note|paper|data|file|url):(?:"((?:[^"\\]|\\.)*)"|(\S+))/g;
|
|
5160
5557
|
function parseMentions(message) {
|
|
5161
5558
|
const mentions = [];
|
|
5162
5559
|
const cleanMessage = message.replace(MENTION_RE, (_match, type, quoted, unquoted) => {
|
|
5163
|
-
const key = quoted || unquoted;
|
|
5560
|
+
const key = (quoted || unquoted).replace(/\\"/g, '"');
|
|
5164
5561
|
const raw = _match;
|
|
5165
5562
|
mentions.push({ type, key, raw });
|
|
5166
5563
|
return `[${type}: ${key}]`;
|
|
@@ -5185,11 +5582,11 @@ function getCachedMarkdown(filePath, projectPath) {
|
|
|
5185
5582
|
const mtime = stat.mtimeMs;
|
|
5186
5583
|
const cacheDir = join(projectPath, PATHS.documentCache);
|
|
5187
5584
|
const cacheKey2 = getCacheKey(filePath, mtime);
|
|
5188
|
-
const
|
|
5189
|
-
if (!existsSync(
|
|
5585
|
+
const cachePath2 = join(cacheDir, cacheKey2);
|
|
5586
|
+
if (!existsSync(cachePath2)) {
|
|
5190
5587
|
return null;
|
|
5191
5588
|
}
|
|
5192
|
-
const entry = JSON.parse(readFileSync(
|
|
5589
|
+
const entry = JSON.parse(readFileSync(cachePath2, "utf-8"));
|
|
5193
5590
|
if (entry.sourcePath !== filePath || entry.sourceMtime !== mtime) {
|
|
5194
5591
|
return null;
|
|
5195
5592
|
}
|
|
@@ -5204,14 +5601,14 @@ function setCachedMarkdown(filePath, markdown, projectPath) {
|
|
|
5204
5601
|
const mtime = stat.mtimeMs;
|
|
5205
5602
|
const cacheDir = ensureCacheDir(projectPath);
|
|
5206
5603
|
const cacheKey2 = getCacheKey(filePath, mtime);
|
|
5207
|
-
const
|
|
5604
|
+
const cachePath2 = join(cacheDir, cacheKey2);
|
|
5208
5605
|
const entry = {
|
|
5209
5606
|
sourcePath: filePath,
|
|
5210
5607
|
sourceMtime: mtime,
|
|
5211
5608
|
markdown,
|
|
5212
5609
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5213
5610
|
};
|
|
5214
|
-
writeFileSync(
|
|
5611
|
+
writeFileSync(cachePath2, JSON.stringify(entry, null, 2), "utf-8");
|
|
5215
5612
|
} catch (err) {
|
|
5216
5613
|
console.warn("[document-cache] Failed to cache markdown:", err);
|
|
5217
5614
|
}
|
|
@@ -5271,6 +5668,10 @@ function resolveEntity(ref, dir, entityType, projectPath) {
|
|
|
5271
5668
|
const DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".docx", ".xlsx", ".pptx", ".doc", ".xls", ".ppt", ".epub"]);
|
|
5272
5669
|
function resolveFile(ref, projectPath) {
|
|
5273
5670
|
const filePath = resolve(projectPath, ref.key);
|
|
5671
|
+
const normalizedProject = resolve(projectPath);
|
|
5672
|
+
if (!filePath.startsWith(normalizedProject + "/") && filePath !== normalizedProject) {
|
|
5673
|
+
return { ref, label: `file: ${ref.key}`, content: "", error: `Path outside workspace: ${ref.key}` };
|
|
5674
|
+
}
|
|
5274
5675
|
if (!existsSync(filePath)) {
|
|
5275
5676
|
return { ref, label: `file: ${ref.key}`, content: "", error: `File not found: ${ref.key}` };
|
|
5276
5677
|
}
|
|
@@ -5383,6 +5784,9 @@ NOTE: This is a data entity. The actual data is in the file at the path above. U
|
|
|
5383
5784
|
}
|
|
5384
5785
|
return JSON.stringify(entity, null, 2);
|
|
5385
5786
|
}
|
|
5787
|
+
const REFRESH_THROTTLE_MS = 5e3;
|
|
5788
|
+
const MAX_FILES = 5e3;
|
|
5789
|
+
const MAX_DEPTH = 8;
|
|
5386
5790
|
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5387
5791
|
"node_modules",
|
|
5388
5792
|
"__pycache__",
|
|
@@ -5395,10 +5799,58 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
5395
5799
|
".venv",
|
|
5396
5800
|
"venv"
|
|
5397
5801
|
]);
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5802
|
+
let cachedFiles = [];
|
|
5803
|
+
let cachedProjectPath = "";
|
|
5804
|
+
let lastRefreshAt = 0;
|
|
5805
|
+
let refreshPromise = null;
|
|
5806
|
+
async function getFileList(projectPath) {
|
|
5807
|
+
if (projectPath !== cachedProjectPath) {
|
|
5808
|
+
cachedFiles = [];
|
|
5809
|
+
cachedProjectPath = projectPath;
|
|
5810
|
+
lastRefreshAt = 0;
|
|
5811
|
+
}
|
|
5812
|
+
const now = Date.now();
|
|
5813
|
+
if (cachedFiles.length > 0 && now - lastRefreshAt < REFRESH_THROTTLE_MS) {
|
|
5814
|
+
return cachedFiles;
|
|
5815
|
+
}
|
|
5816
|
+
if (refreshPromise) return refreshPromise;
|
|
5817
|
+
refreshPromise = refreshFileList(projectPath).finally(() => {
|
|
5818
|
+
refreshPromise = null;
|
|
5819
|
+
});
|
|
5820
|
+
return refreshPromise;
|
|
5821
|
+
}
|
|
5822
|
+
async function refreshFileList(projectPath) {
|
|
5823
|
+
try {
|
|
5824
|
+
const files = await gitLsFiles(projectPath);
|
|
5825
|
+
cachedFiles = files.slice(0, MAX_FILES);
|
|
5826
|
+
} catch {
|
|
5827
|
+
cachedFiles = walkFilesSync(projectPath);
|
|
5828
|
+
}
|
|
5829
|
+
lastRefreshAt = Date.now();
|
|
5830
|
+
return cachedFiles;
|
|
5831
|
+
}
|
|
5832
|
+
function gitLsFiles(cwd) {
|
|
5833
|
+
return new Promise((resolve2, reject) => {
|
|
5834
|
+
execFile$1(
|
|
5835
|
+
"git",
|
|
5836
|
+
["ls-files", "-c", "-o", "--exclude-standard"],
|
|
5837
|
+
{ cwd, maxBuffer: 20 * 1024 * 1024, timeout: 5e3 },
|
|
5838
|
+
(err, stdout) => {
|
|
5839
|
+
if (err) return reject(err);
|
|
5840
|
+
const files = stdout.split("\n").filter(Boolean);
|
|
5841
|
+
if (files.length === 0) return reject(new Error("empty"));
|
|
5842
|
+
resolve2(files);
|
|
5843
|
+
}
|
|
5844
|
+
);
|
|
5845
|
+
});
|
|
5846
|
+
}
|
|
5847
|
+
function walkFilesSync(root) {
|
|
5848
|
+
const out = [];
|
|
5849
|
+
walk(root, "", 0, out);
|
|
5850
|
+
return out;
|
|
5851
|
+
}
|
|
5852
|
+
function walk(root, rel, depth, out) {
|
|
5853
|
+
if (depth > MAX_DEPTH || out.length >= MAX_FILES) return;
|
|
5402
5854
|
const dir = rel ? join(root, rel) : root;
|
|
5403
5855
|
let entries;
|
|
5404
5856
|
try {
|
|
@@ -5408,7 +5860,7 @@ function walkFiles(root, rel, depth, out) {
|
|
|
5408
5860
|
}
|
|
5409
5861
|
for (const name of entries) {
|
|
5410
5862
|
if (name.startsWith(".")) continue;
|
|
5411
|
-
if (out.length >=
|
|
5863
|
+
if (out.length >= MAX_FILES) return;
|
|
5412
5864
|
const childRel = rel ? `${rel}/${name}` : name;
|
|
5413
5865
|
const full = join(dir, name);
|
|
5414
5866
|
let stat;
|
|
@@ -5419,22 +5871,107 @@ function walkFiles(root, rel, depth, out) {
|
|
|
5419
5871
|
}
|
|
5420
5872
|
if (stat.isDirectory()) {
|
|
5421
5873
|
if (SKIP_DIRS.has(name)) continue;
|
|
5422
|
-
|
|
5874
|
+
walk(root, childRel, depth + 1, out);
|
|
5423
5875
|
} else {
|
|
5424
|
-
out.push(
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5876
|
+
out.push(childRel);
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
let cache = null;
|
|
5881
|
+
let cachePath = "";
|
|
5882
|
+
function getEntityCache(projectPath) {
|
|
5883
|
+
if (cache && cachePath === projectPath) return cache;
|
|
5884
|
+
cache = {
|
|
5885
|
+
notes: listNotes(projectPath),
|
|
5886
|
+
papers: listLiterature(projectPath),
|
|
5887
|
+
data: listData(projectPath)
|
|
5888
|
+
};
|
|
5889
|
+
cachePath = projectPath;
|
|
5890
|
+
return cache;
|
|
5891
|
+
}
|
|
5892
|
+
function invalidateEntityCache() {
|
|
5893
|
+
cache = null;
|
|
5894
|
+
}
|
|
5895
|
+
const SCORE_MATCH = 16;
|
|
5896
|
+
const BONUS_FIRST_CHAR = 8;
|
|
5897
|
+
const BONUS_BOUNDARY = 8;
|
|
5898
|
+
const BONUS_CONSECUTIVE = 4;
|
|
5899
|
+
const PENALTY_GAP_START = 3;
|
|
5900
|
+
const PENALTY_GAP_EXT = 1;
|
|
5901
|
+
function fuzzyMatch(items, needle, getText, limit = 30) {
|
|
5902
|
+
if (!needle) {
|
|
5903
|
+
return items.slice(0, limit).map((item) => ({ item, score: 0 }));
|
|
5904
|
+
}
|
|
5905
|
+
const needleLower = needle.toLowerCase();
|
|
5906
|
+
const results = [];
|
|
5907
|
+
let worstInTopK = -Infinity;
|
|
5908
|
+
for (const item of items) {
|
|
5909
|
+
const haystack = getText(item);
|
|
5910
|
+
const score = scoreMatch(needleLower, haystack.toLowerCase(), haystack);
|
|
5911
|
+
if (score <= 0) continue;
|
|
5912
|
+
if (results.length >= limit && score <= worstInTopK) continue;
|
|
5913
|
+
insertSorted(results, { item, score }, limit);
|
|
5914
|
+
if (results.length >= limit) {
|
|
5915
|
+
worstInTopK = results[results.length - 1].score;
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
return results;
|
|
5919
|
+
}
|
|
5920
|
+
function scoreMatch(needle, haystackLower, haystack) {
|
|
5921
|
+
let score = 0;
|
|
5922
|
+
let ni = 0;
|
|
5923
|
+
let consecutive = 0;
|
|
5924
|
+
let lastMatchIdx = -1;
|
|
5925
|
+
for (let hi = 0; hi < haystackLower.length && ni < needle.length; hi++) {
|
|
5926
|
+
if (haystackLower[hi] === needle[ni]) {
|
|
5927
|
+
score += SCORE_MATCH;
|
|
5928
|
+
if (hi === 0) score += BONUS_FIRST_CHAR;
|
|
5929
|
+
if (isBoundary(haystack, hi)) score += BONUS_BOUNDARY;
|
|
5930
|
+
if (lastMatchIdx === hi - 1) {
|
|
5931
|
+
consecutive++;
|
|
5932
|
+
score += BONUS_CONSECUTIVE * consecutive;
|
|
5933
|
+
} else {
|
|
5934
|
+
const gap = lastMatchIdx >= 0 ? hi - lastMatchIdx - 1 : 0;
|
|
5935
|
+
if (gap > 0) score -= PENALTY_GAP_START + PENALTY_GAP_EXT * (gap - 1);
|
|
5936
|
+
consecutive = 1;
|
|
5937
|
+
}
|
|
5938
|
+
lastMatchIdx = hi;
|
|
5939
|
+
ni++;
|
|
5430
5940
|
}
|
|
5431
5941
|
}
|
|
5942
|
+
return ni === needle.length ? score : 0;
|
|
5432
5943
|
}
|
|
5433
|
-
function
|
|
5944
|
+
function isBoundary(s, i) {
|
|
5945
|
+
if (i === 0) return true;
|
|
5946
|
+
const prev = s.charCodeAt(i - 1);
|
|
5947
|
+
const cur = s.charCodeAt(i);
|
|
5948
|
+
if (prev === 47 || prev === 92 || prev === 45 || prev === 95 || prev === 46 || prev === 32) {
|
|
5949
|
+
return true;
|
|
5950
|
+
}
|
|
5951
|
+
if (prev >= 97 && prev <= 122 && cur >= 65 && cur <= 90) return true;
|
|
5952
|
+
return false;
|
|
5953
|
+
}
|
|
5954
|
+
function insertSorted(arr, entry, limit) {
|
|
5955
|
+
let lo = 0;
|
|
5956
|
+
let hi = arr.length;
|
|
5957
|
+
while (lo < hi) {
|
|
5958
|
+
const mid = lo + hi >>> 1;
|
|
5959
|
+
if (arr[mid].score >= entry.score) {
|
|
5960
|
+
lo = mid + 1;
|
|
5961
|
+
} else {
|
|
5962
|
+
hi = mid;
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
arr.splice(lo, 0, entry);
|
|
5966
|
+
if (arr.length > limit) arr.pop();
|
|
5967
|
+
}
|
|
5968
|
+
const MAX_EMPTY_QUERY_FILES = 30;
|
|
5969
|
+
async function getCandidates(projectPath, typeFilter, query) {
|
|
5434
5970
|
const candidates = [];
|
|
5435
5971
|
const q = query?.toLowerCase() ?? "";
|
|
5972
|
+
const entities = getEntityCache(projectPath);
|
|
5436
5973
|
if (!typeFilter || typeFilter === "note") {
|
|
5437
|
-
for (const n of
|
|
5974
|
+
for (const n of entities.notes) {
|
|
5438
5975
|
candidates.push({
|
|
5439
5976
|
type: "note",
|
|
5440
5977
|
value: n.id.slice(0, 8),
|
|
@@ -5444,7 +5981,7 @@ function getCandidates(projectPath, typeFilter, query) {
|
|
|
5444
5981
|
}
|
|
5445
5982
|
}
|
|
5446
5983
|
if (!typeFilter || typeFilter === "paper") {
|
|
5447
|
-
for (const l of
|
|
5984
|
+
for (const l of entities.papers) {
|
|
5448
5985
|
candidates.push({
|
|
5449
5986
|
type: "paper",
|
|
5450
5987
|
value: l.citeKey,
|
|
@@ -5454,7 +5991,7 @@ function getCandidates(projectPath, typeFilter, query) {
|
|
|
5454
5991
|
}
|
|
5455
5992
|
}
|
|
5456
5993
|
if (!typeFilter || typeFilter === "data") {
|
|
5457
|
-
for (const d of
|
|
5994
|
+
for (const d of entities.data) {
|
|
5458
5995
|
candidates.push({
|
|
5459
5996
|
type: "data",
|
|
5460
5997
|
value: d.id.slice(0, 8),
|
|
@@ -5464,16 +6001,25 @@ function getCandidates(projectPath, typeFilter, query) {
|
|
|
5464
6001
|
}
|
|
5465
6002
|
}
|
|
5466
6003
|
if (!typeFilter || typeFilter === "file") {
|
|
5467
|
-
|
|
5468
|
-
|
|
6004
|
+
const files = await getFileList(projectPath);
|
|
6005
|
+
for (const rel of files) {
|
|
6006
|
+
let detail;
|
|
6007
|
+
try {
|
|
6008
|
+
const stat = statSync(join(projectPath, rel));
|
|
6009
|
+
detail = `${(stat.size / 1024).toFixed(1)}KB`;
|
|
6010
|
+
} catch {
|
|
6011
|
+
}
|
|
6012
|
+
candidates.push({ type: "file", value: rel, label: rel, detail });
|
|
5469
6013
|
}
|
|
5470
6014
|
}
|
|
5471
6015
|
if (q) {
|
|
5472
|
-
return
|
|
5473
|
-
|
|
5474
|
-
|
|
6016
|
+
return fuzzyMatch(
|
|
6017
|
+
candidates,
|
|
6018
|
+
q,
|
|
6019
|
+
(c) => `${c.label} ${c.value} ${c.detail ?? ""}`,
|
|
6020
|
+
50
|
|
6021
|
+
).map((r) => r.item);
|
|
5475
6022
|
}
|
|
5476
|
-
const MAX_EMPTY_QUERY_FILES = 30;
|
|
5477
6023
|
const fileCount = candidates.filter((c) => c.type === "file").length;
|
|
5478
6024
|
if (fileCount > MAX_EMPTY_QUERY_FILES) {
|
|
5479
6025
|
const nonFiles = candidates.filter((c) => c.type !== "file");
|
|
@@ -5791,7 +6337,8 @@ function initializeProject(path2) {
|
|
|
5791
6337
|
PATHS.memoryRoot,
|
|
5792
6338
|
PATHS.explainDir,
|
|
5793
6339
|
PATHS.sessionSummaries,
|
|
5794
|
-
PATHS.skills
|
|
6340
|
+
PATHS.skills,
|
|
6341
|
+
PATHS.memory
|
|
5795
6342
|
];
|
|
5796
6343
|
for (const dir of dirs) {
|
|
5797
6344
|
const fullPath = join(path2, dir);
|
|
@@ -5812,8 +6359,9 @@ function initializeProject(path2) {
|
|
|
5812
6359
|
writeFileSync(projectFile, JSON.stringify(defaultConfig, null, 2));
|
|
5813
6360
|
}
|
|
5814
6361
|
ensureAgentMd(path2);
|
|
6362
|
+
migrateAgentMemoryToFile(path2);
|
|
5815
6363
|
const migration = migrateLegacyArtifacts(path2);
|
|
5816
|
-
if (migration.updatedFiles > 0) {
|
|
6364
|
+
if (migration.updatedFiles > 0 && process.env.RESEARCH_COPILOT_DEBUG) {
|
|
5817
6365
|
console.log(`[ResearchPilot] migrated legacy artifacts: files=${migration.updatedFiles}, literature->paper=${migration.convertedLiteratureType}, data.name removed=${migration.removedDataNameField}`);
|
|
5818
6366
|
}
|
|
5819
6367
|
}
|
|
@@ -5840,7 +6388,7 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
5840
6388
|
reasoningEffort: state.currentReasoningEffort,
|
|
5841
6389
|
projectPath: state.projectPath,
|
|
5842
6390
|
sessionId: state.sessionId,
|
|
5843
|
-
debug:
|
|
6391
|
+
debug: !!process.env.RESEARCH_COPILOT_DEBUG,
|
|
5844
6392
|
onStream: (chunk) => {
|
|
5845
6393
|
state.realtimeBuffer.appendChunk(chunk);
|
|
5846
6394
|
safeSend(win, "agent:stream-chunk", chunk);
|
|
@@ -5889,17 +6437,20 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
5889
6437
|
}
|
|
5890
6438
|
}
|
|
5891
6439
|
}
|
|
5892
|
-
if (tool === "artifact-create" && result && typeof result === "object" && "success" in result) {
|
|
6440
|
+
if ((tool === "artifact-create" || tool === "artifact-update") && result && typeof result === "object" && "success" in result) {
|
|
5893
6441
|
const r2 = result;
|
|
5894
6442
|
if (r2.success) {
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
6443
|
+
invalidateEntityCache();
|
|
6444
|
+
if (tool === "artifact-create") {
|
|
6445
|
+
safeSend(win, "agent:entity-created", {
|
|
6446
|
+
type: r2.data?.type || "artifact",
|
|
6447
|
+
id: r2.data?.id,
|
|
6448
|
+
title: r2.data?.title
|
|
6449
|
+
});
|
|
6450
|
+
if (r2.data?.filePath) {
|
|
6451
|
+
const absPath = isAbsolute(r2.data.filePath) ? r2.data.filePath : resolve(runProjectPath, r2.data.filePath);
|
|
6452
|
+
safeSend(win, "agent:file-created", absPath);
|
|
6453
|
+
}
|
|
5903
6454
|
}
|
|
5904
6455
|
}
|
|
5905
6456
|
}
|
|
@@ -6098,23 +6649,17 @@ function registerIpcHandlers() {
|
|
|
6098
6649
|
sessionId: state.sessionId,
|
|
6099
6650
|
projectPath: state.projectPath,
|
|
6100
6651
|
paperIds,
|
|
6101
|
-
debug:
|
|
6652
|
+
debug: !!process.env.RESEARCH_COPILOT_DEBUG,
|
|
6102
6653
|
onProgress: (event) => {
|
|
6103
6654
|
safeSend(win, "enrich:progress", event);
|
|
6104
6655
|
}
|
|
6105
6656
|
});
|
|
6106
6657
|
});
|
|
6107
|
-
handleWindow("mention:candidates", ({ state }, query, type) => {
|
|
6108
|
-
if (!state.projectPath)
|
|
6109
|
-
console.warn("[mention:candidates] No projectPath set");
|
|
6110
|
-
return [];
|
|
6111
|
-
}
|
|
6658
|
+
handleWindow("mention:candidates", async ({ state }, query, type) => {
|
|
6659
|
+
if (!state.projectPath) return [];
|
|
6112
6660
|
try {
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
return result;
|
|
6116
|
-
} catch (err) {
|
|
6117
|
-
console.error("[mention:candidates] Error:", err);
|
|
6661
|
+
return await getCandidates(state.projectPath, type, query);
|
|
6662
|
+
} catch {
|
|
6118
6663
|
return [];
|
|
6119
6664
|
}
|
|
6120
6665
|
});
|