research-copilot 0.1.3 → 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/app/out/main/index.mjs +588 -81
- 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/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"
|
|
@@ -1244,56 +1247,268 @@ function createArtifactSearchTool(projectPath) {
|
|
|
1244
1247
|
}
|
|
1245
1248
|
};
|
|
1246
1249
|
}
|
|
1247
|
-
function
|
|
1250
|
+
function createResearchMemoryTools(params) {
|
|
1251
|
+
return [
|
|
1252
|
+
createArtifactCreateTool(params.sessionId, params.projectPath),
|
|
1253
|
+
createArtifactUpdateTool(params.projectPath),
|
|
1254
|
+
createArtifactSearchTool(params.projectPath)
|
|
1255
|
+
];
|
|
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;
|
|
1248
1290
|
return {
|
|
1249
|
-
name: "
|
|
1250
|
-
|
|
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.',
|
|
1251
1404
|
parameters: {
|
|
1252
1405
|
type: "object",
|
|
1253
1406
|
properties: {
|
|
1254
|
-
|
|
1407
|
+
type: {
|
|
1408
|
+
type: "string",
|
|
1409
|
+
enum: VALID_TYPES$1,
|
|
1410
|
+
description: "Memory category"
|
|
1411
|
+
},
|
|
1412
|
+
name: {
|
|
1255
1413
|
type: "string",
|
|
1256
|
-
description:
|
|
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."
|
|
1257
1419
|
}
|
|
1258
1420
|
},
|
|
1259
|
-
required: ["
|
|
1421
|
+
required: ["type", "name", "content"]
|
|
1260
1422
|
},
|
|
1261
1423
|
execute: async (input) => {
|
|
1262
|
-
const
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
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."]
|
|
1266
1434
|
});
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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."]
|
|
1282
1493
|
});
|
|
1283
1494
|
}
|
|
1495
|
+
deleteMemoryFile(projectPath, existing.filename);
|
|
1496
|
+
const allEntries = listMemoryFiles(projectPath);
|
|
1497
|
+
updateAgentMdIndex(projectPath, allEntries);
|
|
1284
1498
|
return {
|
|
1285
1499
|
success: true,
|
|
1286
|
-
data: {
|
|
1500
|
+
data: {
|
|
1501
|
+
message: `Memory deleted: ${name}`,
|
|
1502
|
+
totalMemories: allEntries.length
|
|
1503
|
+
}
|
|
1287
1504
|
};
|
|
1288
1505
|
}
|
|
1289
1506
|
};
|
|
1290
1507
|
}
|
|
1291
|
-
function
|
|
1508
|
+
function createMemoryTools(projectPath) {
|
|
1292
1509
|
return [
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
createArtifactSearchTool(params.projectPath),
|
|
1296
|
-
createUpdateMemoryTool(params.projectPath)
|
|
1510
|
+
createSaveMemoryTool(projectPath),
|
|
1511
|
+
createDeleteMemoryTool(projectPath)
|
|
1297
1512
|
];
|
|
1298
1513
|
}
|
|
1299
1514
|
const WEB_DEFAULTS = {
|
|
@@ -1741,11 +1956,20 @@ Memory model:
|
|
|
1741
1956
|
- Session context is maintained automatically via periodic summaries.
|
|
1742
1957
|
- For quick-reference info, create a note via artifact-create({ type: "note", ... }).
|
|
1743
1958
|
|
|
1744
|
-
Long-term memory:
|
|
1745
|
-
-
|
|
1746
|
-
-
|
|
1747
|
-
|
|
1748
|
-
|
|
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>.
|
|
1749
1973
|
|
|
1750
1974
|
Coding tasks:
|
|
1751
1975
|
- For code implementation, follow test-first workflow: write/update test → confirm it fails → implement → confirm it passes.
|
|
@@ -3722,15 +3946,139 @@ function createResearchTools(ctx) {
|
|
|
3722
3946
|
tools.push(createLiteratureSearchTool(ctx));
|
|
3723
3947
|
tools.push(createConvertDocumentTool(ctx));
|
|
3724
3948
|
tools.push(createDataAnalyzeTool(ctx));
|
|
3725
|
-
const
|
|
3949
|
+
const artifactTools = createResearchMemoryTools({
|
|
3726
3950
|
sessionId: ctx.sessionId,
|
|
3727
3951
|
projectPath: ctx.projectPath
|
|
3728
3952
|
});
|
|
3729
|
-
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) {
|
|
3730
3958
|
tools.push(wrapResearchTool(tool));
|
|
3731
3959
|
}
|
|
3732
3960
|
return tools;
|
|
3733
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
|
+
}
|
|
3734
4082
|
const SKILL_FILE_NAME = "SKILL.md";
|
|
3735
4083
|
const MAX_SCAN_DEPTH = 3;
|
|
3736
4084
|
let _builtinSkillsRoot = null;
|
|
@@ -4541,6 +4889,11 @@ ${message}`;
|
|
|
4541
4889
|
});
|
|
4542
4890
|
if (turnHistory.length > 8) turnHistory.shift();
|
|
4543
4891
|
void maybeGenerateSummary();
|
|
4892
|
+
void maybeExtractMemories(
|
|
4893
|
+
{ projectPath, model: piModel, apiKey, systemPrompt: enrichedSystem, debug },
|
|
4894
|
+
agent.state.messages,
|
|
4895
|
+
turnCount
|
|
4896
|
+
);
|
|
4544
4897
|
if (debug) {
|
|
4545
4898
|
console.log(`[Chat] Result: success=true, hasOutput=${!!responseText}, turn=${turnCount}`);
|
|
4546
4899
|
}
|
|
@@ -5200,11 +5553,11 @@ function searchEntities(projectPath, query, types) {
|
|
|
5200
5553
|
score: hit.score
|
|
5201
5554
|
}));
|
|
5202
5555
|
}
|
|
5203
|
-
const MENTION_RE = /@(note|paper|data|file|url):(?:"([^"]
|
|
5556
|
+
const MENTION_RE = /@(note|paper|data|file|url):(?:"((?:[^"\\]|\\.)*)"|(\S+))/g;
|
|
5204
5557
|
function parseMentions(message) {
|
|
5205
5558
|
const mentions = [];
|
|
5206
5559
|
const cleanMessage = message.replace(MENTION_RE, (_match, type, quoted, unquoted) => {
|
|
5207
|
-
const key = quoted || unquoted;
|
|
5560
|
+
const key = (quoted || unquoted).replace(/\\"/g, '"');
|
|
5208
5561
|
const raw = _match;
|
|
5209
5562
|
mentions.push({ type, key, raw });
|
|
5210
5563
|
return `[${type}: ${key}]`;
|
|
@@ -5229,11 +5582,11 @@ function getCachedMarkdown(filePath, projectPath) {
|
|
|
5229
5582
|
const mtime = stat.mtimeMs;
|
|
5230
5583
|
const cacheDir = join(projectPath, PATHS.documentCache);
|
|
5231
5584
|
const cacheKey2 = getCacheKey(filePath, mtime);
|
|
5232
|
-
const
|
|
5233
|
-
if (!existsSync(
|
|
5585
|
+
const cachePath2 = join(cacheDir, cacheKey2);
|
|
5586
|
+
if (!existsSync(cachePath2)) {
|
|
5234
5587
|
return null;
|
|
5235
5588
|
}
|
|
5236
|
-
const entry = JSON.parse(readFileSync(
|
|
5589
|
+
const entry = JSON.parse(readFileSync(cachePath2, "utf-8"));
|
|
5237
5590
|
if (entry.sourcePath !== filePath || entry.sourceMtime !== mtime) {
|
|
5238
5591
|
return null;
|
|
5239
5592
|
}
|
|
@@ -5248,14 +5601,14 @@ function setCachedMarkdown(filePath, markdown, projectPath) {
|
|
|
5248
5601
|
const mtime = stat.mtimeMs;
|
|
5249
5602
|
const cacheDir = ensureCacheDir(projectPath);
|
|
5250
5603
|
const cacheKey2 = getCacheKey(filePath, mtime);
|
|
5251
|
-
const
|
|
5604
|
+
const cachePath2 = join(cacheDir, cacheKey2);
|
|
5252
5605
|
const entry = {
|
|
5253
5606
|
sourcePath: filePath,
|
|
5254
5607
|
sourceMtime: mtime,
|
|
5255
5608
|
markdown,
|
|
5256
5609
|
cachedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5257
5610
|
};
|
|
5258
|
-
writeFileSync(
|
|
5611
|
+
writeFileSync(cachePath2, JSON.stringify(entry, null, 2), "utf-8");
|
|
5259
5612
|
} catch (err) {
|
|
5260
5613
|
console.warn("[document-cache] Failed to cache markdown:", err);
|
|
5261
5614
|
}
|
|
@@ -5315,6 +5668,10 @@ function resolveEntity(ref, dir, entityType, projectPath) {
|
|
|
5315
5668
|
const DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set([".pdf", ".docx", ".xlsx", ".pptx", ".doc", ".xls", ".ppt", ".epub"]);
|
|
5316
5669
|
function resolveFile(ref, projectPath) {
|
|
5317
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
|
+
}
|
|
5318
5675
|
if (!existsSync(filePath)) {
|
|
5319
5676
|
return { ref, label: `file: ${ref.key}`, content: "", error: `File not found: ${ref.key}` };
|
|
5320
5677
|
}
|
|
@@ -5427,6 +5784,9 @@ NOTE: This is a data entity. The actual data is in the file at the path above. U
|
|
|
5427
5784
|
}
|
|
5428
5785
|
return JSON.stringify(entity, null, 2);
|
|
5429
5786
|
}
|
|
5787
|
+
const REFRESH_THROTTLE_MS = 5e3;
|
|
5788
|
+
const MAX_FILES = 5e3;
|
|
5789
|
+
const MAX_DEPTH = 8;
|
|
5430
5790
|
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5431
5791
|
"node_modules",
|
|
5432
5792
|
"__pycache__",
|
|
@@ -5439,10 +5799,58 @@ const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
5439
5799
|
".venv",
|
|
5440
5800
|
"venv"
|
|
5441
5801
|
]);
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
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;
|
|
5446
5854
|
const dir = rel ? join(root, rel) : root;
|
|
5447
5855
|
let entries;
|
|
5448
5856
|
try {
|
|
@@ -5452,7 +5860,7 @@ function walkFiles(root, rel, depth, out) {
|
|
|
5452
5860
|
}
|
|
5453
5861
|
for (const name of entries) {
|
|
5454
5862
|
if (name.startsWith(".")) continue;
|
|
5455
|
-
if (out.length >=
|
|
5863
|
+
if (out.length >= MAX_FILES) return;
|
|
5456
5864
|
const childRel = rel ? `${rel}/${name}` : name;
|
|
5457
5865
|
const full = join(dir, name);
|
|
5458
5866
|
let stat;
|
|
@@ -5463,22 +5871,107 @@ function walkFiles(root, rel, depth, out) {
|
|
|
5463
5871
|
}
|
|
5464
5872
|
if (stat.isDirectory()) {
|
|
5465
5873
|
if (SKIP_DIRS.has(name)) continue;
|
|
5466
|
-
|
|
5874
|
+
walk(root, childRel, depth + 1, out);
|
|
5467
5875
|
} else {
|
|
5468
|
-
out.push(
|
|
5469
|
-
type: "file",
|
|
5470
|
-
value: childRel,
|
|
5471
|
-
label: childRel,
|
|
5472
|
-
detail: `${(stat.size / 1024).toFixed(1)}KB`
|
|
5473
|
-
});
|
|
5876
|
+
out.push(childRel);
|
|
5474
5877
|
}
|
|
5475
5878
|
}
|
|
5476
5879
|
}
|
|
5477
|
-
|
|
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++;
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
return ni === needle.length ? score : 0;
|
|
5943
|
+
}
|
|
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) {
|
|
5478
5970
|
const candidates = [];
|
|
5479
5971
|
const q = query?.toLowerCase() ?? "";
|
|
5972
|
+
const entities = getEntityCache(projectPath);
|
|
5480
5973
|
if (!typeFilter || typeFilter === "note") {
|
|
5481
|
-
for (const n of
|
|
5974
|
+
for (const n of entities.notes) {
|
|
5482
5975
|
candidates.push({
|
|
5483
5976
|
type: "note",
|
|
5484
5977
|
value: n.id.slice(0, 8),
|
|
@@ -5488,7 +5981,7 @@ function getCandidates(projectPath, typeFilter, query) {
|
|
|
5488
5981
|
}
|
|
5489
5982
|
}
|
|
5490
5983
|
if (!typeFilter || typeFilter === "paper") {
|
|
5491
|
-
for (const l of
|
|
5984
|
+
for (const l of entities.papers) {
|
|
5492
5985
|
candidates.push({
|
|
5493
5986
|
type: "paper",
|
|
5494
5987
|
value: l.citeKey,
|
|
@@ -5498,7 +5991,7 @@ function getCandidates(projectPath, typeFilter, query) {
|
|
|
5498
5991
|
}
|
|
5499
5992
|
}
|
|
5500
5993
|
if (!typeFilter || typeFilter === "data") {
|
|
5501
|
-
for (const d of
|
|
5994
|
+
for (const d of entities.data) {
|
|
5502
5995
|
candidates.push({
|
|
5503
5996
|
type: "data",
|
|
5504
5997
|
value: d.id.slice(0, 8),
|
|
@@ -5508,16 +6001,25 @@ function getCandidates(projectPath, typeFilter, query) {
|
|
|
5508
6001
|
}
|
|
5509
6002
|
}
|
|
5510
6003
|
if (!typeFilter || typeFilter === "file") {
|
|
5511
|
-
|
|
5512
|
-
|
|
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 });
|
|
5513
6013
|
}
|
|
5514
6014
|
}
|
|
5515
6015
|
if (q) {
|
|
5516
|
-
return
|
|
5517
|
-
|
|
5518
|
-
|
|
6016
|
+
return fuzzyMatch(
|
|
6017
|
+
candidates,
|
|
6018
|
+
q,
|
|
6019
|
+
(c) => `${c.label} ${c.value} ${c.detail ?? ""}`,
|
|
6020
|
+
50
|
|
6021
|
+
).map((r) => r.item);
|
|
5519
6022
|
}
|
|
5520
|
-
const MAX_EMPTY_QUERY_FILES = 30;
|
|
5521
6023
|
const fileCount = candidates.filter((c) => c.type === "file").length;
|
|
5522
6024
|
if (fileCount > MAX_EMPTY_QUERY_FILES) {
|
|
5523
6025
|
const nonFiles = candidates.filter((c) => c.type !== "file");
|
|
@@ -5835,7 +6337,8 @@ function initializeProject(path2) {
|
|
|
5835
6337
|
PATHS.memoryRoot,
|
|
5836
6338
|
PATHS.explainDir,
|
|
5837
6339
|
PATHS.sessionSummaries,
|
|
5838
|
-
PATHS.skills
|
|
6340
|
+
PATHS.skills,
|
|
6341
|
+
PATHS.memory
|
|
5839
6342
|
];
|
|
5840
6343
|
for (const dir of dirs) {
|
|
5841
6344
|
const fullPath = join(path2, dir);
|
|
@@ -5856,6 +6359,7 @@ function initializeProject(path2) {
|
|
|
5856
6359
|
writeFileSync(projectFile, JSON.stringify(defaultConfig, null, 2));
|
|
5857
6360
|
}
|
|
5858
6361
|
ensureAgentMd(path2);
|
|
6362
|
+
migrateAgentMemoryToFile(path2);
|
|
5859
6363
|
const migration = migrateLegacyArtifacts(path2);
|
|
5860
6364
|
if (migration.updatedFiles > 0 && process.env.RESEARCH_COPILOT_DEBUG) {
|
|
5861
6365
|
console.log(`[ResearchPilot] migrated legacy artifacts: files=${migration.updatedFiles}, literature->paper=${migration.convertedLiteratureType}, data.name removed=${migration.removedDataNameField}`);
|
|
@@ -5933,17 +6437,20 @@ async function ensureCoordinator(state, win, model, options) {
|
|
|
5933
6437
|
}
|
|
5934
6438
|
}
|
|
5935
6439
|
}
|
|
5936
|
-
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) {
|
|
5937
6441
|
const r2 = result;
|
|
5938
6442
|
if (r2.success) {
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
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
|
+
}
|
|
5947
6454
|
}
|
|
5948
6455
|
}
|
|
5949
6456
|
}
|
|
@@ -6148,10 +6655,10 @@ function registerIpcHandlers() {
|
|
|
6148
6655
|
}
|
|
6149
6656
|
});
|
|
6150
6657
|
});
|
|
6151
|
-
handleWindow("mention:candidates", ({ state }, query, type) => {
|
|
6658
|
+
handleWindow("mention:candidates", async ({ state }, query, type) => {
|
|
6152
6659
|
if (!state.projectPath) return [];
|
|
6153
6660
|
try {
|
|
6154
|
-
return getCandidates(state.projectPath, type, query);
|
|
6661
|
+
return await getCandidates(state.projectPath, type, query);
|
|
6155
6662
|
} catch {
|
|
6156
6663
|
return [];
|
|
6157
6664
|
}
|