@mindstudio-ai/remy 0.1.136 → 0.1.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1278,1276 +1278,1958 @@ var init_setProjectMetadata = __esm({
1278
1278
  }
1279
1279
  });
1280
1280
 
1281
- // src/tools/code/readFile.ts
1282
- import fs6 from "fs/promises";
1283
- function isBinary(buffer) {
1284
- const sample = buffer.subarray(0, 8192);
1285
- for (let i = 0; i < sample.length; i++) {
1286
- if (sample[i] === 0) {
1287
- return true;
1281
+ // src/assets.ts
1282
+ import fs6 from "fs";
1283
+ import path3 from "path";
1284
+ function findRoot(start) {
1285
+ let dir = start;
1286
+ while (dir !== path3.dirname(dir)) {
1287
+ if (fs6.existsSync(path3.join(dir, "package.json"))) {
1288
+ return dir;
1288
1289
  }
1290
+ dir = path3.dirname(dir);
1289
1291
  }
1290
- return false;
1292
+ return start;
1291
1293
  }
1292
- var DEFAULT_MAX_LINES2, readFileTool;
1293
- var init_readFile = __esm({
1294
- "src/tools/code/readFile.ts"() {
1294
+ function assetPath(...segments) {
1295
+ return path3.join(ASSETS_BASE, ...segments);
1296
+ }
1297
+ function readAsset(...segments) {
1298
+ const full = assetPath(...segments);
1299
+ try {
1300
+ return fs6.readFileSync(full, "utf-8").trim();
1301
+ } catch {
1302
+ throw new Error(`Required asset missing: ${full}`);
1303
+ }
1304
+ }
1305
+ function readJsonAsset(fallback, ...segments) {
1306
+ const full = assetPath(...segments);
1307
+ try {
1308
+ return JSON.parse(fs6.readFileSync(full, "utf-8"));
1309
+ } catch {
1310
+ return fallback;
1311
+ }
1312
+ }
1313
+ var ROOT, ASSETS_BASE;
1314
+ var init_assets = __esm({
1315
+ "src/assets.ts"() {
1295
1316
  "use strict";
1296
- DEFAULT_MAX_LINES2 = 500;
1297
- readFileTool = {
1298
- clearable: true,
1299
- definition: {
1300
- name: "readFile",
1301
- description: "Read a file's contents with line numbers. Always read a file before editing it \u2014 never guess at contents. For large files, consider using symbols first to identify the relevant section, then use offset and maxLines to read just that section. Line numbers in the output correspond to what editFile expects. Defaults to first 500 lines. Use a negative offset to read from the end of the file (e.g., offset: -50 reads the last 50 lines).",
1302
- inputSchema: {
1303
- type: "object",
1304
- properties: {
1305
- path: {
1306
- type: "string",
1307
- description: "The file path to read, relative to the project root."
1308
- },
1309
- offset: {
1310
- type: "number",
1311
- description: "Line number to start reading from (1-indexed). Use a negative number to read from the end (e.g., -50 reads the last 50 lines). Defaults to 1."
1312
- },
1313
- maxLines: {
1314
- type: "number",
1315
- description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
1316
- }
1317
- },
1318
- required: ["path"]
1319
- }
1320
- },
1321
- async execute(input) {
1322
- try {
1323
- const buffer = await fs6.readFile(input.path);
1324
- if (isBinary(buffer)) {
1325
- const size = buffer.length;
1326
- const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
1327
- return `Error: ${input.path} appears to be a binary file (${unit}). Use bash to inspect it if needed.`;
1328
- }
1329
- const content = buffer.toString("utf-8");
1330
- const allLines = content.split("\n");
1331
- const totalLines = allLines.length;
1332
- const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
1333
- let startIdx;
1334
- if (input.offset && input.offset < 0) {
1335
- startIdx = Math.max(0, totalLines + input.offset);
1336
- } else {
1337
- startIdx = Math.max(0, (input.offset || 1) - 1);
1338
- }
1339
- const sliced = allLines.slice(startIdx, startIdx + maxLines);
1340
- const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
1341
- let result = numbered;
1342
- const endLine = startIdx + sliced.length;
1343
- const displayStart = startIdx + 1;
1344
- if (endLine < totalLines) {
1345
- result += `
1346
-
1347
- (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
1348
- }
1349
- return result;
1350
- } catch (err) {
1351
- return `Error reading file: ${err.message}`;
1352
- }
1353
- }
1354
- };
1317
+ ROOT = findRoot(
1318
+ import.meta.dirname ?? path3.dirname(new URL(import.meta.url).pathname)
1319
+ );
1320
+ ASSETS_BASE = fs6.existsSync(path3.join(ROOT, "dist", "prompt")) ? path3.join(ROOT, "dist") : path3.join(ROOT, "src");
1355
1321
  }
1356
1322
  });
1357
1323
 
1358
- // src/tools/code/writeFile.ts
1359
- import fs7 from "fs/promises";
1360
- import path3 from "path";
1361
- var writeFileTool;
1362
- var init_writeFile = __esm({
1363
- "src/tools/code/writeFile.ts"() {
1364
- "use strict";
1365
- init_diff();
1366
- init_fileLock();
1367
- writeFileTool = {
1368
- clearable: true,
1369
- definition: {
1370
- name: "writeFile",
1371
- description: "Create a new file or completely overwrite an existing one. Parent directories are created automatically. Use this for new files or full rewrites. For targeted changes to existing files, use editFile instead \u2014 it preserves the parts you don't want to change and avoids errors from forgetting to include unchanged code.",
1372
- inputSchema: {
1373
- type: "object",
1374
- properties: {
1375
- path: {
1376
- type: "string",
1377
- description: "The file path to write, relative to the project root."
1378
- },
1379
- content: {
1380
- type: "string",
1381
- description: "The full content to write to the file."
1382
- }
1383
- },
1384
- required: ["path", "content"]
1385
- }
1386
- },
1387
- streaming: /* @__PURE__ */ (() => {
1388
- let lastNewlineCount = 0;
1389
- let lastPath = "";
1390
- return {
1391
- transform: async (partial) => {
1392
- const newlineCount = partial.content.split("\n").length - 1;
1393
- if (partial.path !== lastPath || newlineCount < lastNewlineCount) {
1394
- lastNewlineCount = 0;
1395
- lastPath = partial.path;
1396
- }
1397
- if (newlineCount <= lastNewlineCount) {
1398
- return null;
1399
- }
1400
- lastNewlineCount = newlineCount;
1401
- const lastNewline = partial.content.lastIndexOf("\n");
1402
- const completeContent = partial.content.substring(0, lastNewline + 1);
1403
- const oldContent = await fs7.readFile(partial.path, "utf-8").catch(() => "");
1404
- return `Writing ${partial.path} (${newlineCount} lines)
1405
- ${unifiedDiff(partial.path, oldContent, completeContent)}`;
1406
- }
1407
- };
1408
- })(),
1409
- async execute(input) {
1410
- const release = await acquireFileLock(input.path);
1411
- try {
1412
- await fs7.mkdir(path3.dirname(input.path), { recursive: true });
1413
- let oldContent = null;
1414
- try {
1415
- oldContent = await fs7.readFile(input.path, "utf-8");
1416
- } catch {
1417
- }
1418
- await fs7.writeFile(input.path, input.content, "utf-8");
1419
- const lineCount = input.content.split("\n").length;
1420
- const label = oldContent !== null ? "Wrote" : "Created";
1421
- return `${label} ${input.path} (${lineCount} lines)
1422
- ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
1423
- } catch (err) {
1424
- return `Error writing file: ${err.message}`;
1425
- } finally {
1426
- release();
1324
+ // src/compaction/index.ts
1325
+ async function compactConversation(state, apiConfig, system, tools2) {
1326
+ const insertionIndex = findSafeInsertionPoint(state.messages);
1327
+ const summaries = [];
1328
+ const tasks = [];
1329
+ const conversationMessages = getConversationMessagesForSummary(
1330
+ state.messages,
1331
+ insertionIndex
1332
+ );
1333
+ if (conversationMessages.length > 0) {
1334
+ tasks.push(
1335
+ generateSummary(
1336
+ apiConfig,
1337
+ "conversation",
1338
+ CONVERSATION_SUMMARY_PROMPT,
1339
+ conversationMessages,
1340
+ system,
1341
+ tools2
1342
+ ).then((text) => {
1343
+ if (text) {
1344
+ summaries.push({ name: "conversation", text });
1427
1345
  }
1428
- }
1429
- };
1346
+ })
1347
+ );
1430
1348
  }
1431
- });
1432
-
1433
- // src/tools/code/editFile/_helpers.ts
1434
- function buildLineOffsets(content) {
1435
- const offsets = [0];
1436
- for (let i = 0; i < content.length; i++) {
1437
- if (content[i] === "\n") {
1438
- offsets.push(i + 1);
1349
+ for (const name of SUMMARIZABLE_SUBAGENTS) {
1350
+ const subagentMessages = getSubAgentMessagesForSummary(
1351
+ state.messages,
1352
+ name,
1353
+ insertionIndex
1354
+ );
1355
+ if (subagentMessages.length > 0) {
1356
+ tasks.push(
1357
+ generateSummary(
1358
+ apiConfig,
1359
+ name,
1360
+ SUBAGENT_SUMMARY_PROMPT,
1361
+ subagentMessages,
1362
+ system,
1363
+ tools2
1364
+ ).then((text) => {
1365
+ if (text) {
1366
+ summaries.push({ name, text });
1367
+ }
1368
+ })
1369
+ );
1439
1370
  }
1440
1371
  }
1441
- return offsets;
1372
+ await Promise.all(tasks);
1373
+ const checkpointMessages = summaries.map((s) => ({
1374
+ role: "user",
1375
+ hidden: true,
1376
+ content: [
1377
+ {
1378
+ type: "summary",
1379
+ name: s.name,
1380
+ text: s.text,
1381
+ startedAt: Date.now()
1382
+ }
1383
+ ]
1384
+ }));
1385
+ if (checkpointMessages.length > 0) {
1386
+ state.messages.splice(insertionIndex, 0, ...checkpointMessages);
1387
+ }
1388
+ log2.info("Compaction complete", {
1389
+ summaries: summaries.length,
1390
+ insertionIndex,
1391
+ messagesAfter: state.messages.length - insertionIndex - checkpointMessages.length
1392
+ });
1442
1393
  }
1443
- function lineAtOffset(offsets, charIndex) {
1444
- let lo = 0;
1445
- let hi = offsets.length - 1;
1446
- while (lo < hi) {
1447
- const mid = lo + hi + 1 >> 1;
1448
- if (offsets[mid] <= charIndex) {
1449
- lo = mid;
1394
+ function findSafeInsertionPoint(messages) {
1395
+ let idx = messages.length;
1396
+ while (idx > 0) {
1397
+ const msg = messages[idx - 1];
1398
+ if (msg.role === "user" && msg.toolCallId) {
1399
+ idx--;
1450
1400
  } else {
1451
- hi = mid - 1;
1401
+ break;
1452
1402
  }
1453
1403
  }
1454
- return lo + 1;
1455
- }
1456
- function findOccurrences(content, searchString) {
1457
- if (!searchString) {
1458
- return [];
1459
- }
1460
- const offsets = buildLineOffsets(content);
1461
- const results = [];
1462
- let pos = 0;
1463
- while (pos <= content.length - searchString.length) {
1464
- const idx = content.indexOf(searchString, pos);
1465
- if (idx === -1) {
1466
- break;
1404
+ if (idx < messages.length && idx > 0) {
1405
+ const msg = messages[idx - 1];
1406
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
1407
+ const hasToolUse = msg.content.some(
1408
+ (b) => b.type === "tool"
1409
+ );
1410
+ if (hasToolUse) {
1411
+ idx--;
1412
+ }
1467
1413
  }
1468
- results.push({ index: idx, line: lineAtOffset(offsets, idx) });
1469
- pos = idx + 1;
1470
1414
  }
1471
- return results;
1415
+ return idx;
1472
1416
  }
1473
- function flexibleMatch(content, searchString) {
1474
- const contentLines = content.split("\n");
1475
- const searchLines = searchString.split("\n").map((l) => l.trimStart());
1476
- if (searchLines.length === 0) {
1477
- return null;
1417
+ function getConversationMessagesForSummary(messages, endIndex) {
1418
+ let startIdx = 0;
1419
+ for (let i = endIndex - 1; i >= 0; i--) {
1420
+ const msg = messages[i];
1421
+ if (!Array.isArray(msg.content)) {
1422
+ continue;
1423
+ }
1424
+ for (const block of msg.content) {
1425
+ if (block.type === "summary" && block.name === "conversation") {
1426
+ startIdx = i + 1;
1427
+ break;
1428
+ }
1429
+ }
1430
+ if (startIdx > 0) {
1431
+ break;
1432
+ }
1478
1433
  }
1479
- const matches = [];
1480
- for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
1481
- let allMatch = true;
1482
- for (let j = 0; j < searchLines.length; j++) {
1483
- if (contentLines[i + j].trimStart() !== searchLines[j]) {
1484
- allMatch = false;
1434
+ return messages.slice(startIdx, endIndex);
1435
+ }
1436
+ function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
1437
+ let checkpointIdx = -1;
1438
+ for (let i = endIndex - 1; i >= 0; i--) {
1439
+ const msg = messages[i];
1440
+ if (!Array.isArray(msg.content)) {
1441
+ continue;
1442
+ }
1443
+ for (const block of msg.content) {
1444
+ if (block.type === "summary" && block.name === subAgentName) {
1445
+ checkpointIdx = i;
1485
1446
  break;
1486
1447
  }
1487
1448
  }
1488
- if (allMatch) {
1489
- matches.push(i);
1449
+ if (checkpointIdx !== -1) {
1450
+ break;
1490
1451
  }
1491
1452
  }
1492
- if (matches.length !== 1) {
1453
+ const startIdx = checkpointIdx !== -1 ? checkpointIdx + 1 : 0;
1454
+ const collected = [];
1455
+ for (let i = startIdx; i < endIndex; i++) {
1456
+ const msg = messages[i];
1457
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
1458
+ continue;
1459
+ }
1460
+ for (const block of msg.content) {
1461
+ if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
1462
+ collected.push(...block.subAgentMessages);
1463
+ }
1464
+ }
1465
+ }
1466
+ return collected;
1467
+ }
1468
+ function serializeForSummary(messages) {
1469
+ return messages.map((msg) => {
1470
+ if (typeof msg.content === "string") {
1471
+ return `[${msg.role}]: ${msg.content}`;
1472
+ }
1473
+ if (!Array.isArray(msg.content)) {
1474
+ return `[${msg.role}]: (empty)`;
1475
+ }
1476
+ const blocks = msg.content;
1477
+ const parts = [];
1478
+ for (const block of blocks) {
1479
+ if (block.type === "text") {
1480
+ parts.push(block.text);
1481
+ } else if (block.type === "tool") {
1482
+ parts.push(
1483
+ `[tool: ${block.name}(${JSON.stringify(block.input).slice(0, 200)})] \u2192 ${(block.result ?? "").slice(0, 500)}`
1484
+ );
1485
+ }
1486
+ }
1487
+ return `[${msg.role}]: ${parts.join("\n")}`;
1488
+ }).join("\n\n");
1489
+ }
1490
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools) {
1491
+ const serialized = serializeForSummary(messagesToSummarize);
1492
+ if (!serialized.trim()) {
1493
1493
  return null;
1494
1494
  }
1495
- const startIdx = matches[0];
1496
- const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
1497
- const offsets = buildLineOffsets(content);
1498
- return {
1499
- matchedText,
1500
- index: offsets[startIdx],
1501
- line: startIdx + 1
1502
- // 1-based
1503
- };
1495
+ log2.info("Generating summary", {
1496
+ name,
1497
+ messageCount: messagesToSummarize.length,
1498
+ cacheReuse: !!mainSystem
1499
+ });
1500
+ let summaryText = "";
1501
+ const useMainCache = !!mainSystem;
1502
+ const system = useMainCache ? mainSystem : compactionPrompt;
1503
+ const tools2 = useMainCache ? mainTools ?? [] : [];
1504
+ const userContent = useMainCache ? `${compactionPrompt}
1505
+
1506
+ ---
1507
+
1508
+ Conversation to summarize:
1509
+
1510
+ ${serialized}` : serialized;
1511
+ for await (const event of streamChat({
1512
+ ...apiConfig,
1513
+ subAgentId: "conversationSummarizer",
1514
+ system,
1515
+ messages: [{ role: "user", content: userContent }],
1516
+ tools: tools2
1517
+ })) {
1518
+ if (event.type === "text") {
1519
+ summaryText += event.text;
1520
+ } else if (event.type === "error") {
1521
+ log2.error("Summary generation failed", { name, error: event.error });
1522
+ return null;
1523
+ }
1524
+ }
1525
+ if (!summaryText.trim()) {
1526
+ log2.warn("Empty summary generated", { name });
1527
+ return null;
1528
+ }
1529
+ log2.info("Summary generated", { name, summaryLength: summaryText.length });
1530
+ return summaryText.trim();
1504
1531
  }
1505
- function replaceAt(content, index, oldLength, newString) {
1506
- return content.slice(0, index) + newString + content.slice(index + oldLength);
1532
+ var log2, CONVERSATION_SUMMARY_PROMPT, SUBAGENT_SUMMARY_PROMPT, SUMMARIZABLE_SUBAGENTS;
1533
+ var init_compaction = __esm({
1534
+ "src/compaction/index.ts"() {
1535
+ "use strict";
1536
+ init_api();
1537
+ init_assets();
1538
+ init_logger();
1539
+ log2 = createLogger("compaction");
1540
+ CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
1541
+ SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
1542
+ SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
1543
+ }
1544
+ });
1545
+
1546
+ // src/tools/_helpers/sidecar.ts
1547
+ function setSidecarBaseUrl(url) {
1548
+ baseUrl = url;
1549
+ log3.info("Configured", { url });
1507
1550
  }
1508
- function formatOccurrenceError(count, lines, filePath) {
1509
- return `old_string found ${count} times in ${filePath} (at lines ${lines.join(", ")}) \u2014 must be unique. Include more surrounding context to disambiguate, or use replace_all to replace every occurrence.`;
1551
+ function isSidecarConfigured() {
1552
+ return baseUrl !== null;
1510
1553
  }
1511
- var init_helpers2 = __esm({
1512
- "src/tools/code/editFile/_helpers.ts"() {
1554
+ async function sidecarRequest(endpoint, body = {}, options) {
1555
+ if (!baseUrl) {
1556
+ throw new Error("Sidecar not available");
1557
+ }
1558
+ const url = `${baseUrl}${endpoint}`;
1559
+ try {
1560
+ const res = await fetch(url, {
1561
+ method: "POST",
1562
+ headers: { "Content-Type": "application/json" },
1563
+ body: JSON.stringify(body),
1564
+ signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
1565
+ });
1566
+ if (!res.ok) {
1567
+ log3.error("Sidecar error", { endpoint, status: res.status });
1568
+ throw new Error(`Sidecar error: ${res.status}`);
1569
+ }
1570
+ const data = await res.json();
1571
+ if (data?.success === false) {
1572
+ const code = data.errorCode ? ` [${data.errorCode}]` : "";
1573
+ throw new Error(`${data.error || "Unknown error"}${code}`);
1574
+ }
1575
+ return data;
1576
+ } catch (err) {
1577
+ if (err.message.startsWith("Sidecar error")) {
1578
+ throw err;
1579
+ }
1580
+ log3.error("Sidecar connection error", { endpoint, error: err.message });
1581
+ throw new Error(`Sidecar connection error: ${err.message}`);
1582
+ }
1583
+ }
1584
+ var log3, baseUrl;
1585
+ var init_sidecar = __esm({
1586
+ "src/tools/_helpers/sidecar.ts"() {
1513
1587
  "use strict";
1588
+ init_logger();
1589
+ log3 = createLogger("sidecar");
1590
+ baseUrl = null;
1514
1591
  }
1515
1592
  });
1516
1593
 
1517
- // src/tools/code/editFile/index.ts
1518
- import fs8 from "fs/promises";
1519
- var editFileTool;
1520
- var init_editFile = __esm({
1521
- "src/tools/code/editFile/index.ts"() {
1594
+ // src/tools/_helpers/lsp.ts
1595
+ async function lspRequest(endpoint, body) {
1596
+ return sidecarRequest(endpoint, body);
1597
+ }
1598
+ var setLspBaseUrl, isLspConfigured;
1599
+ var init_lsp = __esm({
1600
+ "src/tools/_helpers/lsp.ts"() {
1522
1601
  "use strict";
1523
- init_diff();
1524
- init_fileLock();
1525
- init_helpers2();
1526
- editFileTool = {
1527
- clearable: true,
1528
- definition: {
1529
- name: "editFile",
1530
- description: "Replace a string in a file. old_string must appear exactly once (minor indentation differences are handled automatically). Set replace_all to true to replace every occurrence at once. For bulk mechanical substitutions (renaming a variable, swapping colors), prefer replace_all. Always read the file first so you know the exact text to match. When editing nested structures (objects, function bodies, arrays, template literals), always include the full enclosing structure in old_string rather than just an inner fragment. Replacing a partial slice from the middle of nested code is the most common source of syntax errors.",
1531
- inputSchema: {
1532
- type: "object",
1533
- properties: {
1534
- path: {
1535
- type: "string",
1536
- description: "The file path to edit, relative to the project root."
1537
- },
1538
- old_string: {
1539
- type: "string",
1540
- description: "The exact string to find and replace. Must be unique in the file unless replace_all is true."
1541
- },
1542
- new_string: {
1543
- type: "string",
1544
- description: "The replacement string."
1545
- },
1546
- replace_all: {
1547
- type: "boolean",
1548
- description: "If true, replace every occurrence of old_string in the file. Defaults to false."
1549
- }
1550
- },
1551
- required: ["path", "old_string", "new_string"]
1552
- }
1553
- },
1554
- async execute(input) {
1555
- const release = await acquireFileLock(input.path);
1556
- try {
1557
- const content = await fs8.readFile(input.path, "utf-8");
1558
- const { old_string, new_string, replace_all } = input;
1559
- const occurrences = findOccurrences(content, old_string);
1560
- if (replace_all) {
1561
- if (occurrences.length === 0) {
1562
- return `Error: old_string not found in ${input.path}.`;
1563
- }
1564
- let updated = content;
1565
- for (let i = occurrences.length - 1; i >= 0; i--) {
1566
- updated = replaceAt(
1567
- updated,
1568
- occurrences[i].index,
1569
- old_string.length,
1570
- new_string
1571
- );
1572
- }
1573
- await fs8.writeFile(input.path, updated, "utf-8");
1574
- return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
1575
- ${unifiedDiff(input.path, content, updated)}`;
1576
- }
1577
- if (occurrences.length === 1) {
1578
- const updated = replaceAt(
1579
- content,
1580
- occurrences[0].index,
1581
- old_string.length,
1582
- new_string
1583
- );
1584
- await fs8.writeFile(input.path, updated, "utf-8");
1585
- return `Updated ${input.path}
1586
- ${unifiedDiff(input.path, content, updated)}`;
1587
- }
1588
- if (occurrences.length > 1) {
1589
- const lines = occurrences.map((o) => o.line);
1590
- return `Error: ${formatOccurrenceError(occurrences.length, lines, input.path)}`;
1591
- }
1592
- const flex = flexibleMatch(content, old_string);
1593
- if (flex) {
1594
- const updated = replaceAt(
1595
- content,
1596
- flex.index,
1597
- flex.matchedText.length,
1598
- new_string
1599
- );
1600
- await fs8.writeFile(input.path, updated, "utf-8");
1601
- return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
1602
- ${unifiedDiff(input.path, content, updated)}`;
1603
- }
1604
- return `Error: old_string not found in ${input.path}. Make sure you've read the file first and copied the exact text.`;
1605
- } catch (err) {
1606
- return `Error editing file: ${err.message}`;
1607
- } finally {
1608
- release();
1609
- }
1610
- }
1611
- };
1602
+ init_sidecar();
1603
+ setLspBaseUrl = setSidecarBaseUrl;
1604
+ isLspConfigured = isSidecarConfigured;
1612
1605
  }
1613
1606
  });
1614
1607
 
1615
- // src/tools/code/bash.ts
1616
- import { spawn as spawn2 } from "child_process";
1617
- var DEFAULT_TIMEOUT_MS, DEFAULT_MAX_LINES3, bashTool;
1618
- var init_bash = __esm({
1619
- "src/tools/code/bash.ts"() {
1620
- "use strict";
1621
- DEFAULT_TIMEOUT_MS = 12e4;
1622
- DEFAULT_MAX_LINES3 = 500;
1623
- bashTool = {
1624
- clearable: true,
1625
- definition: {
1626
- name: "bash",
1627
- description: "Run a shell command and return stdout + stderr. 120-second timeout by default (configurable). Use for: npm install/build/test, git operations, tsc --noEmit, or any CLI tool. Prefer dedicated tools over bash when available (use grep instead of bash + rg, readFile instead of bash + cat). Output is truncated to 500 lines by default.",
1628
- inputSchema: {
1629
- type: "object",
1630
- properties: {
1631
- command: {
1632
- type: "string",
1633
- description: "The shell command to execute."
1634
- },
1635
- cwd: {
1636
- type: "string",
1637
- description: "Working directory to run the command in. Defaults to the project root."
1638
- },
1639
- timeout: {
1640
- type: "number",
1641
- description: "Timeout in seconds. Defaults to 120. Use higher values for long-running commands like builds or test suites."
1642
- },
1643
- maxLines: {
1644
- type: "number",
1645
- description: "Maximum number of output lines to return. Defaults to 500. Set to 0 for no limit."
1646
- }
1647
- },
1648
- required: ["command"]
1649
- }
1650
- },
1651
- async execute(input, context) {
1652
- const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1653
- const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1654
- return new Promise((resolve2) => {
1655
- const child = spawn2("sh", ["-c", input.command], {
1656
- cwd: input.cwd || void 0,
1657
- env: { ...process.env, FORCE_COLOR: "1" }
1658
- });
1659
- let output = "";
1660
- child.stdout.on("data", (chunk) => {
1661
- const text = chunk.toString();
1662
- output += text;
1663
- context?.onLog?.(text);
1664
- });
1665
- child.stderr.on("data", (chunk) => {
1666
- const text = chunk.toString();
1667
- output += text;
1668
- context?.onLog?.(text);
1669
- });
1670
- const timer = setTimeout(() => {
1671
- child.kill("SIGTERM");
1672
- }, timeoutMs);
1673
- child.on("close", (code) => {
1674
- clearTimeout(timer);
1675
- if (!output) {
1676
- if (code && code !== 0) {
1677
- resolve2(`Error: process exited with code ${code}`);
1678
- } else {
1679
- resolve2("(no output)");
1680
- }
1681
- return;
1682
- }
1683
- const lines = output.split("\n");
1684
- if (lines.length > maxLines) {
1685
- resolve2(
1686
- lines.slice(0, maxLines).join("\n") + `
1687
-
1688
- (truncated at ${maxLines} lines of ${lines.length} total \u2014 increase maxLines to see more)`
1689
- );
1690
- } else {
1691
- resolve2(output);
1692
- }
1693
- });
1694
- child.on("error", (err) => {
1695
- clearTimeout(timer);
1696
- resolve2(`Error: ${err.message}`);
1697
- });
1698
- });
1608
+ // src/prompt/static/projectContext.ts
1609
+ import fs7 from "fs";
1610
+ import path4 from "path";
1611
+ function loadProjectInstructions() {
1612
+ for (const file of AGENT_INSTRUCTION_FILES) {
1613
+ try {
1614
+ const content = fs7.readFileSync(file, "utf-8").trim();
1615
+ if (content) {
1616
+ return `
1617
+ ## Project Instructions (${file})
1618
+ ${content}`;
1699
1619
  }
1700
- };
1620
+ } catch {
1621
+ }
1701
1622
  }
1702
- });
1703
-
1704
- // src/tools/code/grep.ts
1705
- import { exec } from "child_process";
1706
- function formatResults(stdout, max) {
1707
- const lines = stdout.trim().split("\n");
1708
- let result = lines.join("\n");
1709
- if (lines.length >= max) {
1710
- result += `
1711
-
1712
- (truncated at ${max} results \u2014 increase maxResults to see more)`;
1623
+ return "";
1624
+ }
1625
+ function loadProjectManifest() {
1626
+ try {
1627
+ const manifest = fs7.readFileSync("mindstudio.json", "utf-8");
1628
+ return `
1629
+ ## Project Manifest (mindstudio.json)
1630
+ \`\`\`json
1631
+ ${manifest}
1632
+ \`\`\``;
1633
+ } catch {
1634
+ return "";
1713
1635
  }
1714
- return result;
1715
1636
  }
1716
- var DEFAULT_MAX, grepTool;
1717
- var init_grep = __esm({
1718
- "src/tools/code/grep.ts"() {
1719
- "use strict";
1720
- DEFAULT_MAX = 50;
1721
- grepTool = {
1722
- clearable: true,
1723
- definition: {
1724
- name: "grep",
1725
- description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers (default 50 results). Use this to find where something is used, locate function definitions, or search for patterns across the codebase. For finding a symbol's definition precisely, prefer the definition tool if LSP is available. Automatically excludes node_modules and .git.",
1726
- inputSchema: {
1727
- type: "object",
1728
- properties: {
1729
- pattern: {
1730
- type: "string",
1731
- description: "The search pattern (regex supported)."
1732
- },
1733
- path: {
1734
- type: "string",
1735
- description: "Directory or file to search in. Defaults to current directory."
1736
- },
1737
- glob: {
1738
- type: "string",
1739
- description: 'File glob to filter (e.g., "*.ts"). Only used with ripgrep.'
1740
- },
1741
- maxResults: {
1742
- type: "number",
1743
- description: "Maximum number of matching lines to return. Defaults to 50. Increase if you need more comprehensive results."
1744
- }
1745
- },
1746
- required: ["pattern"]
1747
- }
1748
- },
1749
- async execute(input) {
1750
- const searchPath = input.path || ".";
1751
- const max = input.maxResults || DEFAULT_MAX;
1752
- const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
1753
- const escaped = input.pattern.replace(/'/g, "'\\''");
1754
- const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1755
- const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1756
- return new Promise((resolve2) => {
1757
- exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1758
- if (stdout?.trim()) {
1759
- resolve2(formatResults(stdout, max));
1760
- return;
1761
- }
1762
- exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1763
- if (grepStdout?.trim()) {
1764
- resolve2(formatResults(grepStdout, max));
1765
- } else {
1766
- resolve2("No matches found.");
1767
- }
1768
- });
1769
- });
1770
- });
1637
+ function loadSpecFileMetadata() {
1638
+ try {
1639
+ const files = walkMdFiles("src");
1640
+ if (files.length === 0) {
1641
+ return "";
1642
+ }
1643
+ const entries = [];
1644
+ for (const filePath of files) {
1645
+ const { name, description, type } = parseFrontmatter(filePath);
1646
+ let line = `- ${filePath}`;
1647
+ if (name) {
1648
+ line += ` \u2014 "${name}"`;
1771
1649
  }
1772
- };
1650
+ if (type) {
1651
+ line += ` (${type})`;
1652
+ }
1653
+ if (description) {
1654
+ line += ` \u2014 ${description}`;
1655
+ }
1656
+ entries.push(line);
1657
+ }
1658
+ return `
1659
+ ## Spec Files
1660
+ ${entries.join("\n")}`;
1661
+ } catch {
1662
+ return "";
1773
1663
  }
1774
- });
1775
-
1776
- // src/tools/code/glob.ts
1777
- import fg from "fast-glob";
1778
- var DEFAULT_MAX2, globTool;
1779
- var init_glob = __esm({
1780
- "src/tools/code/glob.ts"() {
1781
- "use strict";
1782
- DEFAULT_MAX2 = 200;
1783
- globTool = {
1784
- clearable: true,
1785
- definition: {
1786
- name: "glob",
1787
- description: 'Find files matching a glob pattern. Returns matching file paths sorted alphabetically (default 200 results). Use this to discover project structure, find files by name or extension, or check if a file exists. Common patterns: "**/*.ts" (all TypeScript files), "src/**/*.tsx" (React components in src), "*.json" (root-level JSON files). Automatically excludes node_modules and .git.',
1788
- inputSchema: {
1789
- type: "object",
1790
- properties: {
1791
- pattern: {
1792
- type: "string",
1793
- description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.tsx", "*.json").'
1794
- },
1795
- maxResults: {
1796
- type: "number",
1797
- description: "Maximum number of file paths to return. Defaults to 200. Increase if you need the complete list."
1798
- }
1799
- },
1800
- required: ["pattern"]
1801
- }
1802
- },
1803
- async execute(input) {
1804
- try {
1805
- const max = input.maxResults || DEFAULT_MAX2;
1806
- const files = await fg(input.pattern, {
1807
- ignore: ["**/node_modules/**", "**/.git/**"],
1808
- dot: false
1809
- });
1810
- if (files.length === 0) {
1811
- return "No files found.";
1812
- }
1813
- const sorted = files.sort();
1814
- const truncated = sorted.slice(0, max);
1815
- let result = truncated.join("\n");
1816
- if (sorted.length > max) {
1817
- result += `
1818
-
1819
- (showing ${max} of ${sorted.length} matches \u2014 increase maxResults to see all)`;
1820
- }
1821
- return result;
1822
- } catch (err) {
1823
- return `Error: ${err.message}`;
1824
- }
1825
- }
1826
- };
1827
- }
1828
- });
1829
-
1830
- // src/tools/code/listDir.ts
1831
- import fs9 from "fs/promises";
1832
- import path4 from "path";
1833
- async function readAndSort(dirPath) {
1834
- const entries = await fs9.readdir(dirPath, { withFileTypes: true });
1835
- return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
1836
- if (a.isDirectory() && !b.isDirectory()) {
1837
- return -1;
1838
- }
1839
- if (!a.isDirectory() && b.isDirectory()) {
1840
- return 1;
1841
- }
1842
- return a.name.localeCompare(b.name);
1843
- });
1844
1664
  }
1845
- async function collapsePath(basePath, name) {
1846
- let display = name;
1847
- let current = path4.join(basePath, name);
1848
- for (; ; ) {
1849
- let children;
1850
- try {
1851
- children = await readAndSort(current);
1852
- } catch {
1853
- break;
1854
- }
1855
- if (children.length === 1 && children[0].isDirectory()) {
1856
- display += "/" + children[0].name;
1857
- current = path4.join(current, children[0].name);
1858
- } else {
1859
- break;
1665
+ function walkMdFiles(dir) {
1666
+ const results = [];
1667
+ try {
1668
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
1669
+ for (const entry of entries) {
1670
+ const full = path4.join(dir, entry.name);
1671
+ if (entry.isDirectory()) {
1672
+ results.push(...walkMdFiles(full));
1673
+ } else if (entry.name.endsWith(".md")) {
1674
+ results.push(full);
1675
+ }
1860
1676
  }
1677
+ } catch {
1861
1678
  }
1862
- return [display, current];
1863
- }
1864
- function formatSize(bytes) {
1865
- if (bytes < 1e3) {
1866
- return `${bytes} B`;
1867
- }
1868
- if (bytes < 1e6) {
1869
- return `${(bytes / 1e3).toFixed(1)} kB`;
1870
- }
1871
- return `${(bytes / 1e6).toFixed(1)} MB`;
1679
+ return results.sort();
1872
1680
  }
1873
- async function formatFile(dirPath, name, indent) {
1681
+ function parseFrontmatter(filePath) {
1874
1682
  try {
1875
- const stat = await fs9.stat(path4.join(dirPath, name));
1876
- return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
1683
+ const content = fs7.readFileSync(filePath, "utf-8");
1684
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1685
+ if (!match) {
1686
+ return { name: "", description: "", type: "" };
1687
+ }
1688
+ const fm = match[1];
1689
+ const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
1690
+ const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
1691
+ const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
1692
+ return { name, description, type };
1877
1693
  } catch {
1878
- return `${indent}${name}`;
1694
+ return { name: "", description: "", type: "" };
1879
1695
  }
1880
1696
  }
1881
- var EXCLUDE, MAX_CHILDREN, listDirTool;
1882
- var init_listDir = __esm({
1883
- "src/tools/code/listDir.ts"() {
1884
- "use strict";
1885
- EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
1886
- MAX_CHILDREN = 15;
1887
- listDirTool = {
1888
- clearable: true,
1889
- definition: {
1890
- name: "listDir",
1891
- description: "List the contents of a directory with one level of subdirectory expansion. Shows file sizes and collapses single-child directory chains (a/b/c/ shown as one entry). Use this for a quick overview of a directory's structure. For finding files across the whole project, use glob instead.",
1892
- inputSchema: {
1893
- type: "object",
1894
- properties: {
1895
- path: {
1896
- type: "string",
1897
- description: 'Directory path to list, relative to project root. Defaults to ".".'
1898
- }
1899
- }
1900
- }
1901
- },
1902
- async execute(input) {
1903
- const dirPath = input.path || ".";
1904
- try {
1905
- const entries = await readAndSort(dirPath);
1906
- const lines = [];
1907
- for (const entry of entries) {
1908
- if (entry.isDirectory()) {
1909
- const [displayName, finalPath] = await collapsePath(
1910
- dirPath,
1911
- entry.name
1912
- );
1913
- lines.push(`${displayName}/`);
1914
- try {
1915
- const children = await readAndSort(finalPath);
1916
- const capped = children.slice(0, MAX_CHILDREN);
1917
- for (const child of capped) {
1918
- if (child.isDirectory()) {
1919
- lines.push(` ${child.name}/`);
1920
- } else {
1921
- lines.push(await formatFile(finalPath, child.name, " "));
1922
- }
1923
- }
1924
- if (children.length > MAX_CHILDREN) {
1925
- lines.push(` ... and ${children.length - MAX_CHILDREN} more`);
1926
- }
1927
- } catch {
1928
- }
1929
- } else {
1930
- lines.push(await formatFile(dirPath, entry.name, ""));
1931
- }
1932
- }
1933
- return lines.join("\n") || "(empty directory)";
1934
- } catch (err) {
1935
- return `Error listing directory: ${err.message}`;
1936
- }
1697
+ function loadProjectFileListing() {
1698
+ try {
1699
+ const entries = fs7.readdirSync(".", { withFileTypes: true });
1700
+ const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
1701
+ if (a.isDirectory() && !b.isDirectory()) {
1702
+ return -1;
1937
1703
  }
1938
- };
1939
- }
1940
- });
1941
-
1942
- // src/tools/code/editsFinished.ts
1943
- var editsFinishedTool;
1944
- var init_editsFinished = __esm({
1945
- "src/tools/code/editsFinished.ts"() {
1946
- "use strict";
1947
- editsFinishedTool = {
1948
- clearable: false,
1949
- definition: {
1950
- name: "editsFinished",
1951
- description: "Signal that file edits are complete. Call this after you finish writing/editing files so the live preview updates cleanly. The preview is paused while you edit to avoid showing broken intermediate states \u2014 this unpauses it. If you forget to call this, the preview updates when your turn ends.",
1952
- inputSchema: {
1953
- type: "object",
1954
- properties: {},
1955
- required: []
1956
- }
1957
- },
1958
- async execute() {
1959
- return "Preview updated.";
1704
+ if (!a.isDirectory() && b.isDirectory()) {
1705
+ return 1;
1960
1706
  }
1961
- };
1962
- }
1963
- });
1964
-
1965
- // src/tools/_helpers/sidecar.ts
1966
- function setSidecarBaseUrl(url) {
1967
- baseUrl = url;
1968
- log2.info("Configured", { url });
1969
- }
1970
- function isSidecarConfigured() {
1971
- return baseUrl !== null;
1972
- }
1973
- async function sidecarRequest(endpoint, body = {}, options) {
1974
- if (!baseUrl) {
1975
- throw new Error("Sidecar not available");
1976
- }
1977
- const url = `${baseUrl}${endpoint}`;
1978
- try {
1979
- const res = await fetch(url, {
1980
- method: "POST",
1981
- headers: { "Content-Type": "application/json" },
1982
- body: JSON.stringify(body),
1983
- signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
1984
- });
1985
- if (!res.ok) {
1986
- log2.error("Sidecar error", { endpoint, status: res.status });
1987
- throw new Error(`Sidecar error: ${res.status}`);
1988
- }
1989
- const data = await res.json();
1990
- if (data?.success === false) {
1991
- const code = data.errorCode ? ` [${data.errorCode}]` : "";
1992
- throw new Error(`${data.error || "Unknown error"}${code}`);
1993
- }
1994
- return data;
1995
- } catch (err) {
1996
- if (err.message.startsWith("Sidecar error")) {
1997
- throw err;
1998
- }
1999
- log2.error("Sidecar connection error", { endpoint, error: err.message });
2000
- throw new Error(`Sidecar connection error: ${err.message}`);
1707
+ return a.name.localeCompare(b.name);
1708
+ }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
1709
+ return `
1710
+ ## Project Files
1711
+ \`\`\`
1712
+ ${listing}
1713
+ \`\`\``;
1714
+ } catch {
1715
+ return "";
2001
1716
  }
2002
1717
  }
2003
- var log2, baseUrl;
2004
- var init_sidecar = __esm({
2005
- "src/tools/_helpers/sidecar.ts"() {
1718
+ var AGENT_INSTRUCTION_FILES;
1719
+ var init_projectContext = __esm({
1720
+ "src/prompt/static/projectContext.ts"() {
2006
1721
  "use strict";
2007
- init_logger();
2008
- log2 = createLogger("sidecar");
2009
- baseUrl = null;
1722
+ AGENT_INSTRUCTION_FILES = [
1723
+ "CLAUDE.md",
1724
+ "claude.md",
1725
+ ".claude/instructions.md",
1726
+ "AGENTS.md",
1727
+ "agents.md",
1728
+ ".agents.md",
1729
+ "COPILOT.md",
1730
+ "copilot.md",
1731
+ ".copilot-instructions.md",
1732
+ ".github/copilot-instructions.md",
1733
+ "REMY.md",
1734
+ "remy.md",
1735
+ ".cursorrules",
1736
+ ".cursorules"
1737
+ ];
2010
1738
  }
2011
1739
  });
2012
1740
 
2013
- // src/tools/_helpers/lsp.ts
2014
- async function lspRequest(endpoint, body) {
2015
- return sidecarRequest(endpoint, body);
1741
+ // src/prompt/index.ts
1742
+ function resolveIncludes(template) {
1743
+ const result = template.replace(
1744
+ /\{\{([^}]+)\}\}/g,
1745
+ (_, filePath) => readAsset("prompt", filePath.trim())
1746
+ );
1747
+ return result.replace(/\n{3,}/g, "\n\n").trim();
2016
1748
  }
2017
- var setLspBaseUrl, isLspConfigured;
2018
- var init_lsp = __esm({
2019
- "src/tools/_helpers/lsp.ts"() {
1749
+ function buildSystemPrompt(onboardingState, viewContext) {
1750
+ const projectContext = [
1751
+ loadProjectInstructions(),
1752
+ loadProjectManifest(),
1753
+ loadSpecFileMetadata(),
1754
+ loadProjectFileListing()
1755
+ ].filter(Boolean).join("\n");
1756
+ const now = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
1757
+ month: "long",
1758
+ day: "numeric",
1759
+ year: "numeric"
1760
+ });
1761
+ const template = `
1762
+ {{static/identity.md}}
1763
+
1764
+ Current date: ${now}
1765
+
1766
+ <platform_docs>
1767
+ <platform>
1768
+ {{compiled/platform.md}}
1769
+ </platform>
1770
+
1771
+ <manifest>
1772
+ {{compiled/manifest.md}}
1773
+ </manifest>
1774
+
1775
+ <tables>
1776
+ {{compiled/tables.md}}
1777
+ </tables>
1778
+
1779
+ <methods>
1780
+ {{compiled/methods.md}}
1781
+ </methods>
1782
+
1783
+ <auth>
1784
+ {{compiled/auth.md}}
1785
+ </auth>
1786
+
1787
+ <dev_and_deploy>
1788
+ {{compiled/dev-and-deploy.md}}
1789
+ </dev_and_deploy>
1790
+
1791
+ <design>
1792
+ {{compiled/design.md}}
1793
+ </design>
1794
+
1795
+ <building_agent_interfaces>
1796
+ {{compiled/agent-interfaces.md}}
1797
+ </building_agent_interfaces>
1798
+
1799
+ <media_cdn>
1800
+ {{compiled/media-cdn.md}}
1801
+ </media_cdn>
1802
+
1803
+ <interfaces>
1804
+ {{compiled/interfaces.md}}
1805
+ </interfaces>
1806
+
1807
+ <scenarios>
1808
+ {{compiled/scenarios.md}}
1809
+ </scenarios>
1810
+
1811
+ <secrets>
1812
+ {{compiled/secrets.md}}
1813
+ </secrets>
1814
+ </platform_docs>
1815
+
1816
+ <mindstudio_agent_sdk_docs>
1817
+ {{compiled/sdk-actions.md}}
1818
+
1819
+ {{compiled/task-agents.md}}
1820
+ </mindstudio_agent_sdk_docs>
1821
+
1822
+ <mindstudio_flavored_markdown_spec_docs>
1823
+ {{compiled/msfm.md}}
1824
+ </mindstudio_flavored_markdown_spec_docs>
1825
+
1826
+ <project_context>
1827
+ ${projectContext}
1828
+ </project_context>
1829
+
1830
+ <intake_mode_instructions>
1831
+ {{static/intake.md}}
1832
+ </intake_mode_instructions>
1833
+
1834
+ <spec_authoring_instructions>
1835
+ {{static/authoring.md}}
1836
+ </spec_authoring_instructions>
1837
+
1838
+ {{static/team.md}}
1839
+
1840
+ <code_authoring_instructions>
1841
+ {{static/coding.md}}
1842
+ ${isLspConfigured() ? `<typescript_lsp>
1843
+ {{static/lsp.md}}
1844
+ </typescript_lsp>` : ""}
1845
+ </code_authoring_instructions>
1846
+
1847
+ {{static/instructions.md}}
1848
+
1849
+ <conversation_summaries>
1850
+ Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
1851
+
1852
+ Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query.
1853
+ </conversation_summaries>
1854
+
1855
+ <project_onboarding>
1856
+ New projects progress through four onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
1857
+
1858
+ - **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code.
1859
+ - **initialSpecAuthoring**: Writing and refining the first spec. The user can see it in the editor as it streams in and can give feedback to iterate on it. This phase covers both the initial draft and any back-and-forth refinement before code generation.
1860
+ - **initialCodegen**: First code generation from the spec. The agent is generating methods, tables, interfaces, manifest updates, and scenarios. This can take a while and involves heavy tool use. The user sees a full-screen build progress view.
1861
+ - **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
1862
+
1863
+ <!-- cache_breakpoint -->
1864
+
1865
+ <current_project_onboarding_state>
1866
+ ${onboardingState ?? "onboardingFinished"}
1867
+ </current_project_onboarding_state>
1868
+ </project_onboarding>
1869
+
1870
+ <view_context>
1871
+ The user is currently in ${viewContext?.mode ?? "code"} mode.
1872
+ ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
1873
+ </view_context>
1874
+ `;
1875
+ return resolveIncludes(template);
1876
+ }
1877
+ var init_prompt = __esm({
1878
+ "src/prompt/index.ts"() {
2020
1879
  "use strict";
2021
- init_sidecar();
2022
- setLspBaseUrl = setSidecarBaseUrl;
2023
- isLspConfigured = isSidecarConfigured;
1880
+ init_assets();
1881
+ init_lsp();
1882
+ init_projectContext();
2024
1883
  }
2025
1884
  });
2026
1885
 
2027
- // src/tools/code/lspDiagnostics.ts
2028
- var lspDiagnosticsTool;
2029
- var init_lspDiagnostics = __esm({
2030
- "src/tools/code/lspDiagnostics.ts"() {
1886
+ // src/session.ts
1887
+ import fs8 from "fs";
1888
+ function loadSession(state) {
1889
+ try {
1890
+ const raw = fs8.readFileSync(SESSION_FILE, "utf-8");
1891
+ const data = JSON.parse(raw);
1892
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
1893
+ state.messages = sanitizeMessages(data.messages);
1894
+ log4.info("Session loaded", { messageCount: state.messages.length });
1895
+ return true;
1896
+ }
1897
+ } catch {
1898
+ }
1899
+ return false;
1900
+ }
1901
+ function sanitizeMessages(messages) {
1902
+ const result = [];
1903
+ for (let i = 0; i < messages.length; i++) {
1904
+ result.push(messages[i]);
1905
+ const msg = messages[i];
1906
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
1907
+ continue;
1908
+ }
1909
+ const toolBlocks = msg.content.filter(
1910
+ (b) => b.type === "tool"
1911
+ );
1912
+ if (toolBlocks.length === 0) {
1913
+ continue;
1914
+ }
1915
+ const resultIds = /* @__PURE__ */ new Set();
1916
+ for (let j = i + 1; j < messages.length; j++) {
1917
+ const next = messages[j];
1918
+ if (next.role === "user" && next.toolCallId) {
1919
+ resultIds.add(next.toolCallId);
1920
+ } else {
1921
+ break;
1922
+ }
1923
+ }
1924
+ for (const tc of toolBlocks) {
1925
+ if (!resultIds.has(tc.id)) {
1926
+ result.push({
1927
+ role: "user",
1928
+ content: "Error: tool result lost (session recovered)",
1929
+ toolCallId: tc.id,
1930
+ isToolError: true
1931
+ });
1932
+ }
1933
+ }
1934
+ }
1935
+ return result;
1936
+ }
1937
+ function saveSession(state) {
1938
+ try {
1939
+ fs8.writeFileSync(
1940
+ SESSION_FILE,
1941
+ JSON.stringify({ messages: state.messages }, null, 2),
1942
+ "utf-8"
1943
+ );
1944
+ log4.info("Session saved", { messageCount: state.messages.length });
1945
+ } catch (err) {
1946
+ log4.warn("Session save failed", { error: err.message });
1947
+ }
1948
+ }
1949
+ function clearSession(state) {
1950
+ state.messages = [];
1951
+ try {
1952
+ fs8.unlinkSync(SESSION_FILE);
1953
+ } catch {
1954
+ }
1955
+ }
1956
+ var log4, SESSION_FILE;
1957
+ var init_session = __esm({
1958
+ "src/session.ts"() {
2031
1959
  "use strict";
2032
- init_lsp();
2033
- lspDiagnosticsTool = {
2034
- clearable: true,
1960
+ init_logger();
1961
+ log4 = createLogger("session");
1962
+ SESSION_FILE = ".remy-session.json";
1963
+ }
1964
+ });
1965
+
1966
+ // src/compaction/trigger.ts
1967
+ function triggerCompaction(state, apiConfig, callbacks) {
1968
+ callbacks?.onStart?.();
1969
+ const system = buildSystemPrompt("onboardingFinished");
1970
+ const tools2 = getToolDefinitions("onboardingFinished");
1971
+ compactConversation(state, apiConfig, system, tools2).then(() => {
1972
+ saveSession(state);
1973
+ callbacks?.onComplete?.();
1974
+ log5.info("Compaction complete");
1975
+ }).catch((err) => {
1976
+ callbacks?.onError?.(err.message || "Compaction failed");
1977
+ log5.error("Compaction failed", { error: err.message });
1978
+ }).finally(() => {
1979
+ callbacks?.onFinally?.();
1980
+ });
1981
+ }
1982
+ var log5;
1983
+ var init_trigger = __esm({
1984
+ "src/compaction/trigger.ts"() {
1985
+ "use strict";
1986
+ init_compaction();
1987
+ init_prompt();
1988
+ init_tools6();
1989
+ init_session();
1990
+ init_logger();
1991
+ log5 = createLogger("compaction:trigger");
1992
+ }
1993
+ });
1994
+
1995
+ // src/tools/common/compactConversation.ts
1996
+ var compactConversationTool;
1997
+ var init_compactConversation = __esm({
1998
+ "src/tools/common/compactConversation.ts"() {
1999
+ "use strict";
2000
+ init_trigger();
2001
+ compactConversationTool = {
2002
+ clearable: false,
2035
2003
  definition: {
2036
- name: "lspDiagnostics",
2037
- description: "Get TypeScript diagnostics (type errors, warnings) for a file, with suggested fixes when available. Use this after editing a file to check for errors.",
2004
+ name: "compactConversation",
2005
+ description: "Compact the conversation history by summarizing older messages into a checkpoint. The summary preserves key decisions, what was built, and the current state of the project, but drops the verbose tool results, diffs, and intermediate steps that are no longer useful. Use this when you have just finished a large block of mechanical work (building, refactoring, debugging) and are about to shift back into conversational mode with the user. Runs in the background. Do not use after small changes like fixing a bug or editing copy.",
2038
2006
  inputSchema: {
2039
2007
  type: "object",
2040
- properties: {
2041
- file: {
2042
- type: "string",
2043
- description: "File path relative to workspace root."
2044
- }
2045
- },
2046
- required: ["file"]
2008
+ properties: {}
2047
2009
  }
2048
2010
  },
2049
- async execute(input) {
2050
- const data = await lspRequest("/diagnostics", { file: input.file });
2051
- const diags = data.diagnostics || [];
2052
- if (diags.length === 0) {
2053
- return "No diagnostics \u2014 file is clean.";
2011
+ async execute(_input, context) {
2012
+ if (!context?.conversationMessages || !context.apiConfig) {
2013
+ return "Error: compaction requires execution context.";
2054
2014
  }
2055
- const lines = [];
2056
- for (const d of diags) {
2057
- let line = `${d.severity}: ${d.file}:${d.line}:${d.column} \u2014 ${d.message}`;
2058
- try {
2059
- const actionsData = await lspRequest("/code-actions", {
2060
- file: d.file,
2061
- startLine: d.line,
2062
- startColumn: d.column,
2063
- endLine: d.endLine ?? d.line,
2064
- endColumn: d.endColumn ?? d.column,
2065
- diagnostics: [d]
2066
- });
2067
- const actions = actionsData.actions || [];
2068
- if (actions.length > 0) {
2069
- const fixes = actions.map((a) => a.title).join("; ");
2070
- line += `
2071
- Quick fixes: ${fixes}`;
2072
- }
2073
- } catch {
2074
- }
2075
- lines.push(line);
2076
- }
2077
- return lines.join("\n");
2015
+ triggerCompaction(
2016
+ { messages: context.conversationMessages },
2017
+ context.apiConfig
2018
+ );
2019
+ return "Compaction started in the background.";
2078
2020
  }
2079
2021
  };
2080
2022
  }
2081
2023
  });
2082
2024
 
2083
- // src/tools/code/restartProcess.ts
2084
- var restartProcessTool;
2085
- var init_restartProcess = __esm({
2086
- "src/tools/code/restartProcess.ts"() {
2087
- "use strict";
2088
- init_lsp();
2089
- restartProcessTool = {
2090
- clearable: false,
2091
- definition: {
2092
- name: "restartProcess",
2093
- description: "Restart a managed sandbox process. Use this after running npm install or changing package.json to restart the dev server so it picks up new dependencies.",
2094
- inputSchema: {
2095
- type: "object",
2096
- properties: {
2097
- name: {
2098
- type: "string",
2099
- description: 'Process name to restart. Currently supported: "devServer".'
2100
- }
2101
- },
2102
- required: ["name"]
2103
- }
2104
- },
2105
- async execute(input) {
2106
- const data = await lspRequest("/restart-process", { name: input.name });
2107
- if (data.ok) {
2108
- await new Promise((resolve2) => setTimeout(resolve2, 5e3));
2109
- return `Restarted ${input.name}.`;
2110
- }
2111
- return `Error: unexpected response: ${JSON.stringify(data)}`;
2112
- }
2113
- };
2025
+ // src/tools/code/readFile.ts
2026
+ import fs9 from "fs/promises";
2027
+ function isBinary(buffer) {
2028
+ const sample = buffer.subarray(0, 8192);
2029
+ for (let i = 0; i < sample.length; i++) {
2030
+ if (sample[i] === 0) {
2031
+ return true;
2032
+ }
2114
2033
  }
2115
- });
2116
-
2117
- // src/tools/code/runScenario.ts
2118
- var runScenarioTool;
2119
- var init_runScenario = __esm({
2120
- "src/tools/code/runScenario.ts"() {
2034
+ return false;
2035
+ }
2036
+ var DEFAULT_MAX_LINES2, readFileTool;
2037
+ var init_readFile = __esm({
2038
+ "src/tools/code/readFile.ts"() {
2121
2039
  "use strict";
2122
- runScenarioTool = {
2040
+ DEFAULT_MAX_LINES2 = 500;
2041
+ readFileTool = {
2123
2042
  clearable: true,
2124
2043
  definition: {
2125
- name: "runScenario",
2126
- description: "Run a scenario to seed the dev database with test data. By default truncates all tables first, then executes the seed function and impersonates the scenario roles. Use skipTruncate to run the seed function against existing data without resetting. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details. Return synchronously - no need to sleep before checking results.",
2044
+ name: "readFile",
2045
+ description: "Read a file's contents with line numbers. Always read a file before editing it \u2014 never guess at contents. For large files, consider using symbols first to identify the relevant section, then use offset and maxLines to read just that section. Line numbers in the output correspond to what editFile expects. Defaults to first 500 lines. Use a negative offset to read from the end of the file (e.g., offset: -50 reads the last 50 lines).",
2127
2046
  inputSchema: {
2128
2047
  type: "object",
2129
2048
  properties: {
2130
- scenarioId: {
2049
+ path: {
2131
2050
  type: "string",
2132
- description: "The scenario ID from mindstudio.json."
2051
+ description: "The file path to read, relative to the project root."
2133
2052
  },
2134
- skipTruncate: {
2135
- type: "boolean",
2136
- description: "When true, skip the database reset step and run the seed function against existing data. Defaults to false (clean-slate)."
2053
+ offset: {
2054
+ type: "number",
2055
+ description: "Line number to start reading from (1-indexed). Use a negative number to read from the end (e.g., -50 reads the last 50 lines). Defaults to 1."
2056
+ },
2057
+ maxLines: {
2058
+ type: "number",
2059
+ description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
2137
2060
  }
2138
2061
  },
2139
- required: ["scenarioId"]
2062
+ required: ["path"]
2140
2063
  }
2141
2064
  },
2142
- async execute() {
2143
- return "ok";
2065
+ async execute(input) {
2066
+ try {
2067
+ const buffer = await fs9.readFile(input.path);
2068
+ if (isBinary(buffer)) {
2069
+ const size = buffer.length;
2070
+ const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
2071
+ return `Error: ${input.path} appears to be a binary file (${unit}). Use bash to inspect it if needed.`;
2072
+ }
2073
+ const content = buffer.toString("utf-8");
2074
+ const allLines = content.split("\n");
2075
+ const totalLines = allLines.length;
2076
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
2077
+ let startIdx;
2078
+ if (input.offset && input.offset < 0) {
2079
+ startIdx = Math.max(0, totalLines + input.offset);
2080
+ } else {
2081
+ startIdx = Math.max(0, (input.offset || 1) - 1);
2082
+ }
2083
+ const sliced = allLines.slice(startIdx, startIdx + maxLines);
2084
+ const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
2085
+ let result = numbered;
2086
+ const endLine = startIdx + sliced.length;
2087
+ const displayStart = startIdx + 1;
2088
+ if (endLine < totalLines) {
2089
+ result += `
2090
+
2091
+ (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
2092
+ }
2093
+ return result;
2094
+ } catch (err) {
2095
+ return `Error reading file: ${err.message}`;
2096
+ }
2144
2097
  }
2145
2098
  };
2146
2099
  }
2147
2100
  });
2148
2101
 
2149
- // src/tools/code/runMethod.ts
2150
- var runMethodTool;
2151
- var init_runMethod = __esm({
2152
- "src/tools/code/runMethod.ts"() {
2102
+ // src/tools/code/writeFile.ts
2103
+ import fs10 from "fs/promises";
2104
+ import path5 from "path";
2105
+ var writeFileTool;
2106
+ var init_writeFile = __esm({
2107
+ "src/tools/code/writeFile.ts"() {
2153
2108
  "use strict";
2154
- runMethodTool = {
2109
+ init_diff();
2110
+ init_fileLock();
2111
+ writeFileTool = {
2155
2112
  clearable: true,
2156
2113
  definition: {
2157
- name: "runMethod",
2158
- description: "Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Return synchronously - no need to sleep before checking results.",
2114
+ name: "writeFile",
2115
+ description: "Create a new file or completely overwrite an existing one. Parent directories are created automatically. Use this for new files or full rewrites. For targeted changes to existing files, use editFile instead \u2014 it preserves the parts you don't want to change and avoids errors from forgetting to include unchanged code.",
2159
2116
  inputSchema: {
2160
2117
  type: "object",
2161
2118
  properties: {
2162
- method: {
2119
+ path: {
2163
2120
  type: "string",
2164
- description: 'The method export name (camelCase, e.g. "listHaikus").'
2165
- },
2166
- input: {
2167
- type: "object",
2168
- description: "The input payload to pass to the method. Omit for methods that take no input."
2169
- },
2170
- roles: {
2171
- type: "array",
2172
- items: { type: "string" },
2173
- description: 'Optional. Role names for this request (e.g. ["admin"]). Can be used without userId to test role-gated logic. Overrides session-level impersonation for this call only.'
2121
+ description: "The file path to write, relative to the project root."
2174
2122
  },
2175
- userId: {
2123
+ content: {
2176
2124
  type: "string",
2177
- description: "Optional. User ID for this request \u2014 use a managed user's ID to simulate their identity. Overrides session-level impersonation for this call only."
2125
+ description: "The full content to write to the file."
2178
2126
  }
2179
2127
  },
2180
- required: ["method"]
2128
+ required: ["path", "content"]
2181
2129
  }
2182
2130
  },
2183
- async execute() {
2184
- return "ok";
2185
- }
2186
- };
2187
- }
2188
- });
2189
-
2190
- // src/tools/code/queryDatabase.ts
2191
- var queryDatabaseTool;
2192
- var init_queryDatabase = __esm({
2193
- "src/tools/code/queryDatabase.ts"() {
2194
- "use strict";
2195
- queryDatabaseTool = {
2196
- clearable: true,
2197
- definition: {
2198
- name: "queryDatabase",
2199
- description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
2200
- inputSchema: {
2201
- type: "object",
2202
- properties: {
2203
- sql: {
2204
- type: "string",
2205
- description: "The SQL query to execute."
2131
+ streaming: /* @__PURE__ */ (() => {
2132
+ let lastNewlineCount = 0;
2133
+ let lastPath = "";
2134
+ return {
2135
+ transform: async (partial) => {
2136
+ const newlineCount = partial.content.split("\n").length - 1;
2137
+ if (partial.path !== lastPath || newlineCount < lastNewlineCount) {
2138
+ lastNewlineCount = 0;
2139
+ lastPath = partial.path;
2206
2140
  }
2207
- },
2208
- required: ["sql"]
2141
+ if (newlineCount <= lastNewlineCount) {
2142
+ return null;
2143
+ }
2144
+ lastNewlineCount = newlineCount;
2145
+ const lastNewline = partial.content.lastIndexOf("\n");
2146
+ const completeContent = partial.content.substring(0, lastNewline + 1);
2147
+ const oldContent = await fs10.readFile(partial.path, "utf-8").catch(() => "");
2148
+ return `Writing ${partial.path} (${newlineCount} lines)
2149
+ ${unifiedDiff(partial.path, oldContent, completeContent)}`;
2150
+ }
2151
+ };
2152
+ })(),
2153
+ async execute(input) {
2154
+ const release = await acquireFileLock(input.path);
2155
+ try {
2156
+ await fs10.mkdir(path5.dirname(input.path), { recursive: true });
2157
+ let oldContent = null;
2158
+ try {
2159
+ oldContent = await fs10.readFile(input.path, "utf-8");
2160
+ } catch {
2161
+ }
2162
+ await fs10.writeFile(input.path, input.content, "utf-8");
2163
+ const lineCount = input.content.split("\n").length;
2164
+ const label = oldContent !== null ? "Wrote" : "Created";
2165
+ return `${label} ${input.path} (${lineCount} lines)
2166
+ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
2167
+ } catch (err) {
2168
+ return `Error writing file: ${err.message}`;
2169
+ } finally {
2170
+ release();
2209
2171
  }
2210
- },
2211
- async execute() {
2212
- return "ok";
2213
2172
  }
2214
2173
  };
2215
2174
  }
2216
2175
  });
2217
2176
 
2218
- // src/subagents/common/analyzeImage.ts
2219
- async function analyzeImage(params) {
2220
- const { prompt, imageUrl, timeout = 2e5, onLog } = params;
2221
- return runCli(
2222
- `mindstudio analyze-image --prompt ${JSON.stringify(prompt)} --image-url ${JSON.stringify(imageUrl)} --vision-model-override ${JSON.stringify(VISION_MODEL_OVERRIDE)} --output-key analysis --no-meta`,
2223
- { timeout, onLog }
2224
- );
2225
- }
2226
- var VISION_MODEL, VISION_MODEL_OVERRIDE;
2227
- var init_analyzeImage = __esm({
2228
- "src/subagents/common/analyzeImage.ts"() {
2229
- "use strict";
2230
- init_runCli();
2231
- VISION_MODEL = "claude-4-6-sonnet";
2232
- VISION_MODEL_OVERRIDE = JSON.stringify({
2233
- model: VISION_MODEL,
2234
- config: { thinkingBudget: "off" }
2235
- });
2177
+ // src/tools/code/editFile/_helpers.ts
2178
+ function buildLineOffsets(content) {
2179
+ const offsets = [0];
2180
+ for (let i = 0; i < content.length; i++) {
2181
+ if (content[i] === "\n") {
2182
+ offsets.push(i + 1);
2183
+ }
2236
2184
  }
2237
- });
2238
-
2239
- // src/tools/_helpers/screenshot.ts
2240
- function buildScreenshotAnalysisPrompt(opts) {
2241
- let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
2242
- if (opts?.styleMap) {
2243
- p += `
2244
-
2245
- The following styleMap describes the computed layout state at the moment of capture. Use it to verify typography, spacing, overflow, and element dimensions \u2014 it is more accurate than visual estimation from the image.
2246
-
2247
- <style_map>
2248
- ${opts.styleMap}
2249
- </style_map>`;
2250
- }
2251
- p += `
2252
-
2253
- ${TEXT_WRAP_DISCLAIMER}`;
2254
- return p;
2185
+ return offsets;
2255
2186
  }
2256
- async function captureAndAnalyzeScreenshot(promptOrOptions) {
2257
- let prompt;
2258
- let existingUrl;
2259
- let onLog;
2260
- let path11;
2261
- if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2262
- prompt = promptOrOptions.prompt;
2263
- existingUrl = promptOrOptions.imageUrl;
2264
- path11 = promptOrOptions.path;
2265
- onLog = promptOrOptions.onLog;
2266
- } else {
2267
- prompt = promptOrOptions;
2268
- }
2269
- let url;
2270
- let styleMap;
2271
- if (existingUrl) {
2272
- url = existingUrl;
2273
- } else {
2274
- const ssResult = await sidecarRequest(
2275
- "/screenshot-full-page",
2276
- path11 ? { path: path11 } : void 0,
2277
- { timeout: 12e4 }
2278
- );
2279
- url = ssResult?.url || ssResult?.screenshotUrl;
2280
- if (!url) {
2281
- throw new Error(
2282
- `No URL in sidecar response. The browser may not be ready yet. Response: ${JSON.stringify(ssResult)}`
2283
- );
2187
+ function lineAtOffset(offsets, charIndex) {
2188
+ let lo = 0;
2189
+ let hi = offsets.length - 1;
2190
+ while (lo < hi) {
2191
+ const mid = lo + hi + 1 >> 1;
2192
+ if (offsets[mid] <= charIndex) {
2193
+ lo = mid;
2194
+ } else {
2195
+ hi = mid - 1;
2284
2196
  }
2285
- styleMap = ssResult?.styleMap;
2286
- }
2287
- if (prompt === false) {
2288
- return url;
2289
2197
  }
2290
- const analysisPrompt = buildScreenshotAnalysisPrompt({
2291
- prompt: prompt || void 0,
2292
- styleMap
2293
- });
2294
- const analysis = await analyzeImage({
2295
- prompt: analysisPrompt,
2296
- imageUrl: url,
2297
- onLog
2298
- });
2299
- return JSON.stringify({ url, analysis, ...styleMap ? { styleMap } : {} });
2198
+ return lo + 1;
2300
2199
  }
2301
- var SCREENSHOT_ANALYSIS_PROMPT, TEXT_WRAP_DISCLAIMER;
2302
- var init_screenshot = __esm({
2303
- "src/tools/_helpers/screenshot.ts"() {
2304
- "use strict";
2305
- init_sidecar();
2306
- init_analyzeImage();
2307
- SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).
2308
-
2309
- Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2310
- TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.`;
2200
+ function findOccurrences(content, searchString) {
2201
+ if (!searchString) {
2202
+ return [];
2311
2203
  }
2312
- });
2313
-
2314
- // src/tools/_helpers/browserLock.ts
2315
- function acquireBrowserLock() {
2316
- let release;
2317
- const next = new Promise((res) => {
2318
- release = res;
2319
- });
2320
- const wait = lockQueue;
2321
- lockQueue = next;
2322
- return wait.then(() => release);
2323
- }
2324
- async function checkBrowserConnected() {
2325
- try {
2326
- const status = await sidecarRequest(
2327
- "/browser-status",
2328
- {},
2329
- { timeout: 5e3 }
2330
- );
2331
- if (!status.connected) {
2332
- return {
2333
- connected: false,
2334
- error: "The browser preview is not connected. The user needs to open the preview."
2335
- };
2204
+ const offsets = buildLineOffsets(content);
2205
+ const results = [];
2206
+ let pos = 0;
2207
+ while (pos <= content.length - searchString.length) {
2208
+ const idx = content.indexOf(searchString, pos);
2209
+ if (idx === -1) {
2210
+ break;
2336
2211
  }
2337
- return { connected: true };
2338
- } catch (err) {
2339
- return {
2340
- connected: false,
2341
- error: err?.message || "Could not check browser status. The dev environment may not be running."
2342
- };
2212
+ results.push({ index: idx, line: lineAtOffset(offsets, idx) });
2213
+ pos = idx + 1;
2343
2214
  }
2215
+ return results;
2344
2216
  }
2345
- var lockQueue;
2346
- var init_browserLock = __esm({
2347
- "src/tools/_helpers/browserLock.ts"() {
2348
- "use strict";
2349
- init_sidecar();
2350
- lockQueue = Promise.resolve();
2217
+ function flexibleMatch(content, searchString) {
2218
+ const contentLines = content.split("\n");
2219
+ const searchLines = searchString.split("\n").map((l) => l.trimStart());
2220
+ if (searchLines.length === 0) {
2221
+ return null;
2351
2222
  }
2352
- });
2353
-
2354
- // src/statusWatcher.ts
2355
- function startStatusWatcher(config) {
2356
- const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
2357
- let lastLabel = "";
2358
- let lastContext = "";
2359
- let inflight = false;
2360
- let stopped = false;
2361
- const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
2362
- async function tick() {
2363
- if (stopped || signal?.aborted || inflight) {
2364
- return;
2365
- }
2366
- inflight = true;
2367
- try {
2368
- const context = getContext();
2369
- if (!context || context === lastContext) {
2370
- return;
2371
- }
2372
- lastContext = context;
2373
- const res = await fetch(url, {
2374
- method: "POST",
2375
- headers: {
2376
- "Content-Type": "application/json",
2377
- Authorization: `Bearer ${apiConfig.apiKey}`
2378
- },
2379
- body: JSON.stringify({ context }),
2380
- signal
2381
- });
2382
- if (!res.ok) {
2383
- return;
2384
- }
2385
- const data = await res.json();
2386
- if (!data.label || data.label === lastLabel) {
2387
- return;
2388
- }
2389
- lastLabel = data.label;
2390
- if (stopped) {
2391
- return;
2223
+ const matches = [];
2224
+ for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
2225
+ let allMatch = true;
2226
+ for (let j = 0; j < searchLines.length; j++) {
2227
+ if (contentLines[i + j].trimStart() !== searchLines[j]) {
2228
+ allMatch = false;
2229
+ break;
2392
2230
  }
2393
- onStatus(data.label);
2394
- } catch {
2395
- } finally {
2396
- inflight = false;
2231
+ }
2232
+ if (allMatch) {
2233
+ matches.push(i);
2397
2234
  }
2398
2235
  }
2399
- const timer = setInterval(tick, interval);
2400
- tick().catch(() => {
2401
- });
2236
+ if (matches.length !== 1) {
2237
+ return null;
2238
+ }
2239
+ const startIdx = matches[0];
2240
+ const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
2241
+ const offsets = buildLineOffsets(content);
2402
2242
  return {
2403
- stop() {
2404
- stopped = true;
2405
- clearInterval(timer);
2406
- }
2243
+ matchedText,
2244
+ index: offsets[startIdx],
2245
+ line: startIdx + 1
2246
+ // 1-based
2407
2247
  };
2408
2248
  }
2409
- var init_statusWatcher = __esm({
2410
- "src/statusWatcher.ts"() {
2249
+ function replaceAt(content, index, oldLength, newString) {
2250
+ return content.slice(0, index) + newString + content.slice(index + oldLength);
2251
+ }
2252
+ function formatOccurrenceError(count, lines, filePath) {
2253
+ return `old_string found ${count} times in ${filePath} (at lines ${lines.join(", ")}) \u2014 must be unique. Include more surrounding context to disambiguate, or use replace_all to replace every occurrence.`;
2254
+ }
2255
+ var init_helpers2 = __esm({
2256
+ "src/tools/code/editFile/_helpers.ts"() {
2411
2257
  "use strict";
2412
2258
  }
2413
2259
  });
2414
2260
 
2415
- // src/subagents/common/cleanMessages.ts
2416
- function findLastSummaryCheckpoint(messages, name) {
2417
- for (let i = messages.length - 1; i >= 0; i--) {
2418
- const msg = messages[i];
2419
- if (!Array.isArray(msg.content)) {
2420
- continue;
2421
- }
2422
- for (const block of msg.content) {
2423
- if (block.type === "summary" && block.name === name) {
2424
- return i;
2425
- }
2426
- }
2427
- }
2428
- return -1;
2429
- }
2430
- function fixOrphanedToolCalls(messages) {
2431
- const toolResultIds = /* @__PURE__ */ new Set();
2432
- for (const msg of messages) {
2433
- if (msg.role === "user" && msg.toolCallId) {
2434
- toolResultIds.add(msg.toolCallId);
2435
- }
2436
- }
2437
- const result = [...messages];
2438
- for (let i = result.length - 1; i >= 0; i--) {
2439
- const msg = result[i];
2440
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
2441
- continue;
2442
- }
2443
- const toolBlocks = msg.content.filter(
2444
- (b) => b.type === "tool"
2445
- );
2446
- const orphans = toolBlocks.filter((tc) => !toolResultIds.has(tc.id));
2447
- if (orphans.length === 0) {
2448
- continue;
2449
- }
2450
- const synthetics = orphans.map((tc) => ({
2451
- role: "user",
2452
- content: "Error: tool result lost (session recovered)",
2453
- toolCallId: tc.id,
2454
- isToolError: true
2455
- }));
2456
- result.splice(i + 1, 0, ...synthetics);
2457
- break;
2458
- }
2459
- return result;
2460
- }
2461
- function cleanMessagesForApi(messages) {
2462
- const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
2463
- let startIdx = 0;
2464
- const prefix = [];
2465
- if (checkpointIdx !== -1) {
2466
- const checkpointMsg = messages[checkpointIdx];
2467
- const blocks = checkpointMsg.content;
2468
- const summaryBlock = blocks.find(
2469
- (b) => b.type === "summary" && b.name === "conversation"
2470
- );
2471
- if (summaryBlock && summaryBlock.type === "summary") {
2472
- prefix.push({
2473
- role: "user",
2474
- content: `<conversation_summary>
2475
- ${summaryBlock.text}
2476
- </conversation_summary>`,
2477
- hidden: true
2478
- });
2479
- }
2480
- startIdx = checkpointIdx + 1;
2481
- }
2482
- const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
2483
- const toolUseIds = /* @__PURE__ */ new Set();
2484
- for (const msg of messagesToProcess) {
2485
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
2486
- for (const block of msg.content) {
2487
- if (block.type === "tool") {
2488
- toolUseIds.add(block.id);
2261
+ // src/tools/code/editFile/index.ts
2262
+ import fs11 from "fs/promises";
2263
+ var editFileTool;
2264
+ var init_editFile = __esm({
2265
+ "src/tools/code/editFile/index.ts"() {
2266
+ "use strict";
2267
+ init_diff();
2268
+ init_fileLock();
2269
+ init_helpers2();
2270
+ editFileTool = {
2271
+ clearable: true,
2272
+ definition: {
2273
+ name: "editFile",
2274
+ description: "Replace a string in a file. old_string must appear exactly once (minor indentation differences are handled automatically). Set replace_all to true to replace every occurrence at once. For bulk mechanical substitutions (renaming a variable, swapping colors), prefer replace_all. Always read the file first so you know the exact text to match. When editing nested structures (objects, function bodies, arrays, template literals), always include the full enclosing structure in old_string rather than just an inner fragment. Replacing a partial slice from the middle of nested code is the most common source of syntax errors.",
2275
+ inputSchema: {
2276
+ type: "object",
2277
+ properties: {
2278
+ path: {
2279
+ type: "string",
2280
+ description: "The file path to edit, relative to the project root."
2281
+ },
2282
+ old_string: {
2283
+ type: "string",
2284
+ description: "The exact string to find and replace. Must be unique in the file unless replace_all is true."
2285
+ },
2286
+ new_string: {
2287
+ type: "string",
2288
+ description: "The replacement string."
2289
+ },
2290
+ replace_all: {
2291
+ type: "boolean",
2292
+ description: "If true, replace every occurrence of old_string in the file. Defaults to false."
2293
+ }
2294
+ },
2295
+ required: ["path", "old_string", "new_string"]
2296
+ }
2297
+ },
2298
+ async execute(input) {
2299
+ const release = await acquireFileLock(input.path);
2300
+ try {
2301
+ const content = await fs11.readFile(input.path, "utf-8");
2302
+ const { old_string, new_string, replace_all } = input;
2303
+ const occurrences = findOccurrences(content, old_string);
2304
+ if (replace_all) {
2305
+ if (occurrences.length === 0) {
2306
+ return `Error: old_string not found in ${input.path}.`;
2307
+ }
2308
+ let updated = content;
2309
+ for (let i = occurrences.length - 1; i >= 0; i--) {
2310
+ updated = replaceAt(
2311
+ updated,
2312
+ occurrences[i].index,
2313
+ old_string.length,
2314
+ new_string
2315
+ );
2316
+ }
2317
+ await fs11.writeFile(input.path, updated, "utf-8");
2318
+ return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
2319
+ ${unifiedDiff(input.path, content, updated)}`;
2320
+ }
2321
+ if (occurrences.length === 1) {
2322
+ const updated = replaceAt(
2323
+ content,
2324
+ occurrences[0].index,
2325
+ old_string.length,
2326
+ new_string
2327
+ );
2328
+ await fs11.writeFile(input.path, updated, "utf-8");
2329
+ return `Updated ${input.path}
2330
+ ${unifiedDiff(input.path, content, updated)}`;
2331
+ }
2332
+ if (occurrences.length > 1) {
2333
+ const lines = occurrences.map((o) => o.line);
2334
+ return `Error: ${formatOccurrenceError(occurrences.length, lines, input.path)}`;
2335
+ }
2336
+ const flex = flexibleMatch(content, old_string);
2337
+ if (flex) {
2338
+ const updated = replaceAt(
2339
+ content,
2340
+ flex.index,
2341
+ flex.matchedText.length,
2342
+ new_string
2343
+ );
2344
+ await fs11.writeFile(input.path, updated, "utf-8");
2345
+ return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
2346
+ ${unifiedDiff(input.path, content, updated)}`;
2347
+ }
2348
+ return `Error: old_string not found in ${input.path}. Make sure you've read the file first and copied the exact text.`;
2349
+ } catch (err) {
2350
+ return `Error editing file: ${err.message}`;
2351
+ } finally {
2352
+ release();
2489
2353
  }
2490
2354
  }
2491
- }
2355
+ };
2492
2356
  }
2493
- const cleaned = messagesToProcess.filter((msg) => {
2494
- if (Array.isArray(msg.content)) {
2495
- const blocks = msg.content;
2496
- if (blocks.some((b) => b.type === "summary")) {
2497
- return false;
2357
+ });
2358
+
2359
+ // src/tools/code/bash.ts
2360
+ import { spawn as spawn2 } from "child_process";
2361
+ var DEFAULT_TIMEOUT_MS, DEFAULT_MAX_LINES3, bashTool;
2362
+ var init_bash = __esm({
2363
+ "src/tools/code/bash.ts"() {
2364
+ "use strict";
2365
+ DEFAULT_TIMEOUT_MS = 12e4;
2366
+ DEFAULT_MAX_LINES3 = 500;
2367
+ bashTool = {
2368
+ clearable: true,
2369
+ definition: {
2370
+ name: "bash",
2371
+ description: "Run a shell command and return stdout + stderr. 120-second timeout by default (configurable). Use for: npm install/build/test, git operations, tsc --noEmit, or any CLI tool. Prefer dedicated tools over bash when available (use grep instead of bash + rg, readFile instead of bash + cat). Output is truncated to 500 lines by default.",
2372
+ inputSchema: {
2373
+ type: "object",
2374
+ properties: {
2375
+ command: {
2376
+ type: "string",
2377
+ description: "The shell command to execute."
2378
+ },
2379
+ cwd: {
2380
+ type: "string",
2381
+ description: "Working directory to run the command in. Defaults to the project root."
2382
+ },
2383
+ timeout: {
2384
+ type: "number",
2385
+ description: "Timeout in seconds. Defaults to 120. Use higher values for long-running commands like builds or test suites."
2386
+ },
2387
+ maxLines: {
2388
+ type: "number",
2389
+ description: "Maximum number of output lines to return. Defaults to 500. Set to 0 for no limit."
2390
+ }
2391
+ },
2392
+ required: ["command"]
2393
+ }
2394
+ },
2395
+ async execute(input, context) {
2396
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
2397
+ const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
2398
+ return new Promise((resolve2) => {
2399
+ const child = spawn2("sh", ["-c", input.command], {
2400
+ cwd: input.cwd || void 0,
2401
+ env: { ...process.env, FORCE_COLOR: "1" }
2402
+ });
2403
+ let output = "";
2404
+ child.stdout.on("data", (chunk) => {
2405
+ const text = chunk.toString();
2406
+ output += text;
2407
+ context?.onLog?.(text);
2408
+ });
2409
+ child.stderr.on("data", (chunk) => {
2410
+ const text = chunk.toString();
2411
+ output += text;
2412
+ context?.onLog?.(text);
2413
+ });
2414
+ const timer = setTimeout(() => {
2415
+ child.kill("SIGTERM");
2416
+ }, timeoutMs);
2417
+ child.on("close", (code) => {
2418
+ clearTimeout(timer);
2419
+ if (!output) {
2420
+ if (code && code !== 0) {
2421
+ resolve2(`Error: process exited with code ${code}`);
2422
+ } else {
2423
+ resolve2("(no output)");
2424
+ }
2425
+ return;
2426
+ }
2427
+ const lines = output.split("\n");
2428
+ if (lines.length > maxLines) {
2429
+ resolve2(
2430
+ lines.slice(0, maxLines).join("\n") + `
2431
+
2432
+ (truncated at ${maxLines} lines of ${lines.length} total \u2014 increase maxLines to see more)`
2433
+ );
2434
+ } else {
2435
+ resolve2(output);
2436
+ }
2437
+ });
2438
+ child.on("error", (err) => {
2439
+ clearTimeout(timer);
2440
+ resolve2(`Error: ${err.message}`);
2441
+ });
2442
+ });
2498
2443
  }
2499
- }
2500
- if (msg.role === "assistant" && Array.isArray(msg.content) && msg.content.length === 0) {
2501
- return false;
2502
- }
2503
- if (msg.role === "user" && msg.toolCallId && !toolUseIds.has(msg.toolCallId)) {
2504
- return false;
2505
- }
2506
- return true;
2507
- }).map((msg) => {
2508
- if (msg.role === "user" && typeof msg.content === "string" && msg.content.startsWith("@@automated::")) {
2509
- return {
2510
- ...msg,
2511
- content: msg.content.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "")
2512
- };
2513
- }
2514
- if (!Array.isArray(msg.content)) {
2515
- return msg;
2516
- }
2517
- const blocks = msg.content;
2518
- const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2519
- const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
2520
- const thinking = blocks.filter(
2521
- (b) => b.type === "thinking"
2522
- ).map((b) => ({ thinking: b.thinking, signature: b.signature }));
2523
- const cleaned2 = {
2524
- role: msg.role,
2525
- content: text
2526
2444
  };
2527
- if (toolCalls.length > 0) {
2528
- cleaned2.toolCalls = toolCalls;
2529
- }
2530
- if (thinking.length > 0) {
2531
- cleaned2.thinking = thinking;
2532
- }
2533
- if (msg.hidden) {
2534
- cleaned2.hidden = true;
2535
- }
2536
- return cleaned2;
2537
- });
2538
- return [...prefix, ...cleaned];
2539
- }
2540
- var init_cleanMessages = __esm({
2541
- "src/subagents/common/cleanMessages.ts"() {
2542
- "use strict";
2543
2445
  }
2544
2446
  });
2545
2447
 
2546
- // src/subagents/runner.ts
2547
- async function runSubAgent(config) {
2548
- const {
2549
- system,
2550
- task,
2448
+ // src/tools/code/grep.ts
2449
+ import { exec } from "child_process";
2450
+ function formatResults(stdout, max) {
2451
+ const lines = stdout.trim().split("\n");
2452
+ let result = lines.join("\n");
2453
+ if (lines.length >= max) {
2454
+ result += `
2455
+
2456
+ (truncated at ${max} results \u2014 increase maxResults to see more)`;
2457
+ }
2458
+ return result;
2459
+ }
2460
+ var DEFAULT_MAX, grepTool;
2461
+ var init_grep = __esm({
2462
+ "src/tools/code/grep.ts"() {
2463
+ "use strict";
2464
+ DEFAULT_MAX = 50;
2465
+ grepTool = {
2466
+ clearable: true,
2467
+ definition: {
2468
+ name: "grep",
2469
+ description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers (default 50 results). Use this to find where something is used, locate function definitions, or search for patterns across the codebase. For finding a symbol's definition precisely, prefer the definition tool if LSP is available. Automatically excludes node_modules and .git.",
2470
+ inputSchema: {
2471
+ type: "object",
2472
+ properties: {
2473
+ pattern: {
2474
+ type: "string",
2475
+ description: "The search pattern (regex supported)."
2476
+ },
2477
+ path: {
2478
+ type: "string",
2479
+ description: "Directory or file to search in. Defaults to current directory."
2480
+ },
2481
+ glob: {
2482
+ type: "string",
2483
+ description: 'File glob to filter (e.g., "*.ts"). Only used with ripgrep.'
2484
+ },
2485
+ maxResults: {
2486
+ type: "number",
2487
+ description: "Maximum number of matching lines to return. Defaults to 50. Increase if you need more comprehensive results."
2488
+ }
2489
+ },
2490
+ required: ["pattern"]
2491
+ }
2492
+ },
2493
+ async execute(input) {
2494
+ const searchPath = input.path || ".";
2495
+ const max = input.maxResults || DEFAULT_MAX;
2496
+ const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
2497
+ const escaped = input.pattern.replace(/'/g, "'\\''");
2498
+ const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
2499
+ const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
2500
+ return new Promise((resolve2) => {
2501
+ exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
2502
+ if (stdout?.trim()) {
2503
+ resolve2(formatResults(stdout, max));
2504
+ return;
2505
+ }
2506
+ exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
2507
+ if (grepStdout?.trim()) {
2508
+ resolve2(formatResults(grepStdout, max));
2509
+ } else {
2510
+ resolve2("No matches found.");
2511
+ }
2512
+ });
2513
+ });
2514
+ });
2515
+ }
2516
+ };
2517
+ }
2518
+ });
2519
+
2520
+ // src/tools/code/glob.ts
2521
+ import fg from "fast-glob";
2522
+ var DEFAULT_MAX2, globTool;
2523
+ var init_glob = __esm({
2524
+ "src/tools/code/glob.ts"() {
2525
+ "use strict";
2526
+ DEFAULT_MAX2 = 200;
2527
+ globTool = {
2528
+ clearable: true,
2529
+ definition: {
2530
+ name: "glob",
2531
+ description: 'Find files matching a glob pattern. Returns matching file paths sorted alphabetically (default 200 results). Use this to discover project structure, find files by name or extension, or check if a file exists. Common patterns: "**/*.ts" (all TypeScript files), "src/**/*.tsx" (React components in src), "*.json" (root-level JSON files). Automatically excludes node_modules and .git.',
2532
+ inputSchema: {
2533
+ type: "object",
2534
+ properties: {
2535
+ pattern: {
2536
+ type: "string",
2537
+ description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.tsx", "*.json").'
2538
+ },
2539
+ maxResults: {
2540
+ type: "number",
2541
+ description: "Maximum number of file paths to return. Defaults to 200. Increase if you need the complete list."
2542
+ }
2543
+ },
2544
+ required: ["pattern"]
2545
+ }
2546
+ },
2547
+ async execute(input) {
2548
+ try {
2549
+ const max = input.maxResults || DEFAULT_MAX2;
2550
+ const files = await fg(input.pattern, {
2551
+ ignore: ["**/node_modules/**", "**/.git/**"],
2552
+ dot: false
2553
+ });
2554
+ if (files.length === 0) {
2555
+ return "No files found.";
2556
+ }
2557
+ const sorted = files.sort();
2558
+ const truncated = sorted.slice(0, max);
2559
+ let result = truncated.join("\n");
2560
+ if (sorted.length > max) {
2561
+ result += `
2562
+
2563
+ (showing ${max} of ${sorted.length} matches \u2014 increase maxResults to see all)`;
2564
+ }
2565
+ return result;
2566
+ } catch (err) {
2567
+ return `Error: ${err.message}`;
2568
+ }
2569
+ }
2570
+ };
2571
+ }
2572
+ });
2573
+
2574
+ // src/tools/code/listDir.ts
2575
+ import fs12 from "fs/promises";
2576
+ import path6 from "path";
2577
+ async function readAndSort(dirPath) {
2578
+ const entries = await fs12.readdir(dirPath, { withFileTypes: true });
2579
+ return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
2580
+ if (a.isDirectory() && !b.isDirectory()) {
2581
+ return -1;
2582
+ }
2583
+ if (!a.isDirectory() && b.isDirectory()) {
2584
+ return 1;
2585
+ }
2586
+ return a.name.localeCompare(b.name);
2587
+ });
2588
+ }
2589
+ async function collapsePath(basePath, name) {
2590
+ let display = name;
2591
+ let current = path6.join(basePath, name);
2592
+ for (; ; ) {
2593
+ let children;
2594
+ try {
2595
+ children = await readAndSort(current);
2596
+ } catch {
2597
+ break;
2598
+ }
2599
+ if (children.length === 1 && children[0].isDirectory()) {
2600
+ display += "/" + children[0].name;
2601
+ current = path6.join(current, children[0].name);
2602
+ } else {
2603
+ break;
2604
+ }
2605
+ }
2606
+ return [display, current];
2607
+ }
2608
+ function formatSize(bytes) {
2609
+ if (bytes < 1e3) {
2610
+ return `${bytes} B`;
2611
+ }
2612
+ if (bytes < 1e6) {
2613
+ return `${(bytes / 1e3).toFixed(1)} kB`;
2614
+ }
2615
+ return `${(bytes / 1e6).toFixed(1)} MB`;
2616
+ }
2617
+ async function formatFile(dirPath, name, indent) {
2618
+ try {
2619
+ const stat = await fs12.stat(path6.join(dirPath, name));
2620
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
2621
+ } catch {
2622
+ return `${indent}${name}`;
2623
+ }
2624
+ }
2625
+ var EXCLUDE, MAX_CHILDREN, listDirTool;
2626
+ var init_listDir = __esm({
2627
+ "src/tools/code/listDir.ts"() {
2628
+ "use strict";
2629
+ EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
2630
+ MAX_CHILDREN = 15;
2631
+ listDirTool = {
2632
+ clearable: true,
2633
+ definition: {
2634
+ name: "listDir",
2635
+ description: "List the contents of a directory with one level of subdirectory expansion. Shows file sizes and collapses single-child directory chains (a/b/c/ shown as one entry). Use this for a quick overview of a directory's structure. For finding files across the whole project, use glob instead.",
2636
+ inputSchema: {
2637
+ type: "object",
2638
+ properties: {
2639
+ path: {
2640
+ type: "string",
2641
+ description: 'Directory path to list, relative to project root. Defaults to ".".'
2642
+ }
2643
+ }
2644
+ }
2645
+ },
2646
+ async execute(input) {
2647
+ const dirPath = input.path || ".";
2648
+ try {
2649
+ const entries = await readAndSort(dirPath);
2650
+ const lines = [];
2651
+ for (const entry of entries) {
2652
+ if (entry.isDirectory()) {
2653
+ const [displayName, finalPath] = await collapsePath(
2654
+ dirPath,
2655
+ entry.name
2656
+ );
2657
+ lines.push(`${displayName}/`);
2658
+ try {
2659
+ const children = await readAndSort(finalPath);
2660
+ const capped = children.slice(0, MAX_CHILDREN);
2661
+ for (const child of capped) {
2662
+ if (child.isDirectory()) {
2663
+ lines.push(` ${child.name}/`);
2664
+ } else {
2665
+ lines.push(await formatFile(finalPath, child.name, " "));
2666
+ }
2667
+ }
2668
+ if (children.length > MAX_CHILDREN) {
2669
+ lines.push(` ... and ${children.length - MAX_CHILDREN} more`);
2670
+ }
2671
+ } catch {
2672
+ }
2673
+ } else {
2674
+ lines.push(await formatFile(dirPath, entry.name, ""));
2675
+ }
2676
+ }
2677
+ return lines.join("\n") || "(empty directory)";
2678
+ } catch (err) {
2679
+ return `Error listing directory: ${err.message}`;
2680
+ }
2681
+ }
2682
+ };
2683
+ }
2684
+ });
2685
+
2686
+ // src/tools/code/editsFinished.ts
2687
+ var editsFinishedTool;
2688
+ var init_editsFinished = __esm({
2689
+ "src/tools/code/editsFinished.ts"() {
2690
+ "use strict";
2691
+ editsFinishedTool = {
2692
+ clearable: false,
2693
+ definition: {
2694
+ name: "editsFinished",
2695
+ description: "Signal that file edits are complete. Call this after you finish writing/editing files so the live preview updates cleanly. The preview is paused while you edit to avoid showing broken intermediate states \u2014 this unpauses it. If you forget to call this, the preview updates when your turn ends.",
2696
+ inputSchema: {
2697
+ type: "object",
2698
+ properties: {},
2699
+ required: []
2700
+ }
2701
+ },
2702
+ async execute() {
2703
+ return "Preview updated.";
2704
+ }
2705
+ };
2706
+ }
2707
+ });
2708
+
2709
+ // src/tools/code/lspDiagnostics.ts
2710
+ var lspDiagnosticsTool;
2711
+ var init_lspDiagnostics = __esm({
2712
+ "src/tools/code/lspDiagnostics.ts"() {
2713
+ "use strict";
2714
+ init_lsp();
2715
+ lspDiagnosticsTool = {
2716
+ clearable: true,
2717
+ definition: {
2718
+ name: "lspDiagnostics",
2719
+ description: "Get TypeScript diagnostics (type errors, warnings) for a file, with suggested fixes when available. Use this after editing a file to check for errors.",
2720
+ inputSchema: {
2721
+ type: "object",
2722
+ properties: {
2723
+ file: {
2724
+ type: "string",
2725
+ description: "File path relative to workspace root."
2726
+ }
2727
+ },
2728
+ required: ["file"]
2729
+ }
2730
+ },
2731
+ async execute(input) {
2732
+ const data = await lspRequest("/diagnostics", { file: input.file });
2733
+ const diags = data.diagnostics || [];
2734
+ if (diags.length === 0) {
2735
+ return "No diagnostics \u2014 file is clean.";
2736
+ }
2737
+ const lines = [];
2738
+ for (const d of diags) {
2739
+ let line = `${d.severity}: ${d.file}:${d.line}:${d.column} \u2014 ${d.message}`;
2740
+ try {
2741
+ const actionsData = await lspRequest("/code-actions", {
2742
+ file: d.file,
2743
+ startLine: d.line,
2744
+ startColumn: d.column,
2745
+ endLine: d.endLine ?? d.line,
2746
+ endColumn: d.endColumn ?? d.column,
2747
+ diagnostics: [d]
2748
+ });
2749
+ const actions = actionsData.actions || [];
2750
+ if (actions.length > 0) {
2751
+ const fixes = actions.map((a) => a.title).join("; ");
2752
+ line += `
2753
+ Quick fixes: ${fixes}`;
2754
+ }
2755
+ } catch {
2756
+ }
2757
+ lines.push(line);
2758
+ }
2759
+ return lines.join("\n");
2760
+ }
2761
+ };
2762
+ }
2763
+ });
2764
+
2765
+ // src/tools/code/restartProcess.ts
2766
+ var restartProcessTool;
2767
+ var init_restartProcess = __esm({
2768
+ "src/tools/code/restartProcess.ts"() {
2769
+ "use strict";
2770
+ init_lsp();
2771
+ restartProcessTool = {
2772
+ clearable: false,
2773
+ definition: {
2774
+ name: "restartProcess",
2775
+ description: "Restart a managed sandbox process. Use this after running npm install or changing package.json to restart the dev server so it picks up new dependencies.",
2776
+ inputSchema: {
2777
+ type: "object",
2778
+ properties: {
2779
+ name: {
2780
+ type: "string",
2781
+ description: 'Process name to restart. Currently supported: "devServer".'
2782
+ }
2783
+ },
2784
+ required: ["name"]
2785
+ }
2786
+ },
2787
+ async execute(input) {
2788
+ const data = await lspRequest("/restart-process", { name: input.name });
2789
+ if (data.ok) {
2790
+ await new Promise((resolve2) => setTimeout(resolve2, 5e3));
2791
+ return `Restarted ${input.name}.`;
2792
+ }
2793
+ return `Error: unexpected response: ${JSON.stringify(data)}`;
2794
+ }
2795
+ };
2796
+ }
2797
+ });
2798
+
2799
+ // src/tools/code/runScenario.ts
2800
+ var runScenarioTool;
2801
+ var init_runScenario = __esm({
2802
+ "src/tools/code/runScenario.ts"() {
2803
+ "use strict";
2804
+ runScenarioTool = {
2805
+ clearable: true,
2806
+ definition: {
2807
+ name: "runScenario",
2808
+ description: "Run a scenario to seed the dev database with test data. By default truncates all tables first, then executes the seed function and impersonates the scenario roles. Use skipTruncate to run the seed function against existing data without resetting. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details. Return synchronously - no need to sleep before checking results.",
2809
+ inputSchema: {
2810
+ type: "object",
2811
+ properties: {
2812
+ scenarioId: {
2813
+ type: "string",
2814
+ description: "The scenario ID from mindstudio.json."
2815
+ },
2816
+ skipTruncate: {
2817
+ type: "boolean",
2818
+ description: "When true, skip the database reset step and run the seed function against existing data. Defaults to false (clean-slate)."
2819
+ }
2820
+ },
2821
+ required: ["scenarioId"]
2822
+ }
2823
+ },
2824
+ async execute() {
2825
+ return "ok";
2826
+ }
2827
+ };
2828
+ }
2829
+ });
2830
+
2831
+ // src/tools/code/runMethod.ts
2832
+ var runMethodTool;
2833
+ var init_runMethod = __esm({
2834
+ "src/tools/code/runMethod.ts"() {
2835
+ "use strict";
2836
+ runMethodTool = {
2837
+ clearable: true,
2838
+ definition: {
2839
+ name: "runMethod",
2840
+ description: "Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Return synchronously - no need to sleep before checking results.",
2841
+ inputSchema: {
2842
+ type: "object",
2843
+ properties: {
2844
+ method: {
2845
+ type: "string",
2846
+ description: 'The method export name (camelCase, e.g. "listHaikus").'
2847
+ },
2848
+ input: {
2849
+ type: "object",
2850
+ description: "The input payload to pass to the method. Omit for methods that take no input."
2851
+ },
2852
+ roles: {
2853
+ type: "array",
2854
+ items: { type: "string" },
2855
+ description: 'Optional. Role names for this request (e.g. ["admin"]). Can be used without userId to test role-gated logic. Overrides session-level impersonation for this call only.'
2856
+ },
2857
+ userId: {
2858
+ type: "string",
2859
+ description: "Optional. User ID for this request \u2014 use a managed user's ID to simulate their identity. Overrides session-level impersonation for this call only."
2860
+ }
2861
+ },
2862
+ required: ["method"]
2863
+ }
2864
+ },
2865
+ async execute() {
2866
+ return "ok";
2867
+ }
2868
+ };
2869
+ }
2870
+ });
2871
+
2872
+ // src/tools/code/queryDatabase.ts
2873
+ var queryDatabaseTool;
2874
+ var init_queryDatabase = __esm({
2875
+ "src/tools/code/queryDatabase.ts"() {
2876
+ "use strict";
2877
+ queryDatabaseTool = {
2878
+ clearable: true,
2879
+ definition: {
2880
+ name: "queryDatabase",
2881
+ description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
2882
+ inputSchema: {
2883
+ type: "object",
2884
+ properties: {
2885
+ sql: {
2886
+ type: "string",
2887
+ description: "The SQL query to execute."
2888
+ }
2889
+ },
2890
+ required: ["sql"]
2891
+ }
2892
+ },
2893
+ async execute() {
2894
+ return "ok";
2895
+ }
2896
+ };
2897
+ }
2898
+ });
2899
+
2900
+ // src/subagents/common/analyzeImage.ts
2901
+ async function analyzeImage(params) {
2902
+ const { prompt, imageUrl, timeout = 2e5, onLog } = params;
2903
+ return runCli(
2904
+ `mindstudio analyze-image --prompt ${JSON.stringify(prompt)} --image-url ${JSON.stringify(imageUrl)} --vision-model-override ${JSON.stringify(VISION_MODEL_OVERRIDE)} --output-key analysis --no-meta`,
2905
+ { timeout, onLog }
2906
+ );
2907
+ }
2908
+ var VISION_MODEL, VISION_MODEL_OVERRIDE;
2909
+ var init_analyzeImage = __esm({
2910
+ "src/subagents/common/analyzeImage.ts"() {
2911
+ "use strict";
2912
+ init_runCli();
2913
+ VISION_MODEL = "claude-4-6-sonnet";
2914
+ VISION_MODEL_OVERRIDE = JSON.stringify({
2915
+ model: VISION_MODEL,
2916
+ config: { thinkingBudget: "off" }
2917
+ });
2918
+ }
2919
+ });
2920
+
2921
+ // src/tools/_helpers/screenshot.ts
2922
+ function buildScreenshotAnalysisPrompt(opts) {
2923
+ let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
2924
+ if (opts?.styleMap) {
2925
+ p += `
2926
+
2927
+ The following styleMap describes the computed layout state at the moment of capture. Use it to verify typography, spacing, overflow, and element dimensions \u2014 it is more accurate than visual estimation from the image.
2928
+
2929
+ <style_map>
2930
+ ${opts.styleMap}
2931
+ </style_map>`;
2932
+ }
2933
+ p += `
2934
+
2935
+ ${TEXT_WRAP_DISCLAIMER}`;
2936
+ return p;
2937
+ }
2938
+ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2939
+ let prompt;
2940
+ let existingUrl;
2941
+ let onLog;
2942
+ let path11;
2943
+ if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2944
+ prompt = promptOrOptions.prompt;
2945
+ existingUrl = promptOrOptions.imageUrl;
2946
+ path11 = promptOrOptions.path;
2947
+ onLog = promptOrOptions.onLog;
2948
+ } else {
2949
+ prompt = promptOrOptions;
2950
+ }
2951
+ let url;
2952
+ let styleMap;
2953
+ if (existingUrl) {
2954
+ url = existingUrl;
2955
+ } else {
2956
+ const ssResult = await sidecarRequest(
2957
+ "/screenshot-full-page",
2958
+ path11 ? { path: path11 } : void 0,
2959
+ { timeout: 12e4 }
2960
+ );
2961
+ url = ssResult?.url || ssResult?.screenshotUrl;
2962
+ if (!url) {
2963
+ throw new Error(
2964
+ `No URL in sidecar response. The browser may not be ready yet. Response: ${JSON.stringify(ssResult)}`
2965
+ );
2966
+ }
2967
+ styleMap = ssResult?.styleMap;
2968
+ }
2969
+ if (prompt === false) {
2970
+ return url;
2971
+ }
2972
+ const analysisPrompt = buildScreenshotAnalysisPrompt({
2973
+ prompt: prompt || void 0,
2974
+ styleMap
2975
+ });
2976
+ const analysis = await analyzeImage({
2977
+ prompt: analysisPrompt,
2978
+ imageUrl: url,
2979
+ onLog
2980
+ });
2981
+ return JSON.stringify({ url, analysis, ...styleMap ? { styleMap } : {} });
2982
+ }
2983
+ var SCREENSHOT_ANALYSIS_PROMPT, TEXT_WRAP_DISCLAIMER;
2984
+ var init_screenshot = __esm({
2985
+ "src/tools/_helpers/screenshot.ts"() {
2986
+ "use strict";
2987
+ init_sidecar();
2988
+ init_analyzeImage();
2989
+ SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).
2990
+
2991
+ Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2992
+ TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.`;
2993
+ }
2994
+ });
2995
+
2996
+ // src/tools/_helpers/browserLock.ts
2997
+ function acquireBrowserLock() {
2998
+ let release;
2999
+ const next = new Promise((res) => {
3000
+ release = res;
3001
+ });
3002
+ const wait = lockQueue;
3003
+ lockQueue = next;
3004
+ return wait.then(() => release);
3005
+ }
3006
+ async function checkBrowserConnected() {
3007
+ try {
3008
+ const status = await sidecarRequest(
3009
+ "/browser-status",
3010
+ {},
3011
+ { timeout: 5e3 }
3012
+ );
3013
+ if (!status.connected) {
3014
+ return {
3015
+ connected: false,
3016
+ error: "The browser preview is not connected. The user needs to open the preview."
3017
+ };
3018
+ }
3019
+ return { connected: true };
3020
+ } catch (err) {
3021
+ return {
3022
+ connected: false,
3023
+ error: err?.message || "Could not check browser status. The dev environment may not be running."
3024
+ };
3025
+ }
3026
+ }
3027
+ var lockQueue;
3028
+ var init_browserLock = __esm({
3029
+ "src/tools/_helpers/browserLock.ts"() {
3030
+ "use strict";
3031
+ init_sidecar();
3032
+ lockQueue = Promise.resolve();
3033
+ }
3034
+ });
3035
+
3036
+ // src/statusWatcher.ts
3037
+ function startStatusWatcher(config) {
3038
+ const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
3039
+ let lastLabel = "";
3040
+ let lastContext = "";
3041
+ let inflight = false;
3042
+ let stopped = false;
3043
+ const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
3044
+ async function tick() {
3045
+ if (stopped || signal?.aborted || inflight) {
3046
+ return;
3047
+ }
3048
+ inflight = true;
3049
+ try {
3050
+ const context = getContext();
3051
+ if (!context || context === lastContext) {
3052
+ return;
3053
+ }
3054
+ lastContext = context;
3055
+ const res = await fetch(url, {
3056
+ method: "POST",
3057
+ headers: {
3058
+ "Content-Type": "application/json",
3059
+ Authorization: `Bearer ${apiConfig.apiKey}`
3060
+ },
3061
+ body: JSON.stringify({ context }),
3062
+ signal
3063
+ });
3064
+ if (!res.ok) {
3065
+ return;
3066
+ }
3067
+ const data = await res.json();
3068
+ if (!data.label || data.label === lastLabel) {
3069
+ return;
3070
+ }
3071
+ lastLabel = data.label;
3072
+ if (stopped) {
3073
+ return;
3074
+ }
3075
+ onStatus(data.label);
3076
+ } catch {
3077
+ } finally {
3078
+ inflight = false;
3079
+ }
3080
+ }
3081
+ const timer = setInterval(tick, interval);
3082
+ tick().catch(() => {
3083
+ });
3084
+ return {
3085
+ stop() {
3086
+ stopped = true;
3087
+ clearInterval(timer);
3088
+ }
3089
+ };
3090
+ }
3091
+ var init_statusWatcher = __esm({
3092
+ "src/statusWatcher.ts"() {
3093
+ "use strict";
3094
+ }
3095
+ });
3096
+
3097
+ // src/subagents/common/cleanMessages.ts
3098
+ function findLastSummaryCheckpoint(messages, name) {
3099
+ for (let i = messages.length - 1; i >= 0; i--) {
3100
+ const msg = messages[i];
3101
+ if (!Array.isArray(msg.content)) {
3102
+ continue;
3103
+ }
3104
+ for (const block of msg.content) {
3105
+ if (block.type === "summary" && block.name === name) {
3106
+ return i;
3107
+ }
3108
+ }
3109
+ }
3110
+ return -1;
3111
+ }
3112
+ function fixOrphanedToolCalls(messages) {
3113
+ const toolResultIds = /* @__PURE__ */ new Set();
3114
+ for (const msg of messages) {
3115
+ if (msg.role === "user" && msg.toolCallId) {
3116
+ toolResultIds.add(msg.toolCallId);
3117
+ }
3118
+ }
3119
+ const result = [...messages];
3120
+ for (let i = result.length - 1; i >= 0; i--) {
3121
+ const msg = result[i];
3122
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
3123
+ continue;
3124
+ }
3125
+ const toolBlocks = msg.content.filter(
3126
+ (b) => b.type === "tool"
3127
+ );
3128
+ const orphans = toolBlocks.filter((tc) => !toolResultIds.has(tc.id));
3129
+ if (orphans.length === 0) {
3130
+ continue;
3131
+ }
3132
+ const synthetics = orphans.map((tc) => ({
3133
+ role: "user",
3134
+ content: "Error: tool result lost (session recovered)",
3135
+ toolCallId: tc.id,
3136
+ isToolError: true
3137
+ }));
3138
+ result.splice(i + 1, 0, ...synthetics);
3139
+ break;
3140
+ }
3141
+ return result;
3142
+ }
3143
+ function cleanMessagesForApi(messages) {
3144
+ const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
3145
+ let startIdx = 0;
3146
+ const prefix = [];
3147
+ if (checkpointIdx !== -1) {
3148
+ const checkpointMsg = messages[checkpointIdx];
3149
+ const blocks = checkpointMsg.content;
3150
+ const summaryBlock = blocks.find(
3151
+ (b) => b.type === "summary" && b.name === "conversation"
3152
+ );
3153
+ if (summaryBlock && summaryBlock.type === "summary") {
3154
+ prefix.push({
3155
+ role: "user",
3156
+ content: `<conversation_summary>
3157
+ ${summaryBlock.text}
3158
+ </conversation_summary>`,
3159
+ hidden: true
3160
+ });
3161
+ }
3162
+ startIdx = checkpointIdx + 1;
3163
+ }
3164
+ const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
3165
+ const toolUseIds = /* @__PURE__ */ new Set();
3166
+ for (const msg of messagesToProcess) {
3167
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
3168
+ for (const block of msg.content) {
3169
+ if (block.type === "tool") {
3170
+ toolUseIds.add(block.id);
3171
+ }
3172
+ }
3173
+ }
3174
+ }
3175
+ const cleaned = messagesToProcess.filter((msg) => {
3176
+ if (Array.isArray(msg.content)) {
3177
+ const blocks = msg.content;
3178
+ if (blocks.some((b) => b.type === "summary")) {
3179
+ return false;
3180
+ }
3181
+ }
3182
+ if (msg.role === "assistant" && Array.isArray(msg.content) && msg.content.length === 0) {
3183
+ return false;
3184
+ }
3185
+ if (msg.role === "user" && msg.toolCallId && !toolUseIds.has(msg.toolCallId)) {
3186
+ return false;
3187
+ }
3188
+ return true;
3189
+ }).map((msg) => {
3190
+ if (msg.role === "user" && typeof msg.content === "string" && msg.content.startsWith("@@automated::")) {
3191
+ return {
3192
+ ...msg,
3193
+ content: msg.content.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "")
3194
+ };
3195
+ }
3196
+ if (!Array.isArray(msg.content)) {
3197
+ return msg;
3198
+ }
3199
+ const blocks = msg.content;
3200
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3201
+ const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
3202
+ const thinking = blocks.filter(
3203
+ (b) => b.type === "thinking"
3204
+ ).map((b) => ({ thinking: b.thinking, signature: b.signature }));
3205
+ const cleaned2 = {
3206
+ role: msg.role,
3207
+ content: text
3208
+ };
3209
+ if (toolCalls.length > 0) {
3210
+ cleaned2.toolCalls = toolCalls;
3211
+ }
3212
+ if (thinking.length > 0) {
3213
+ cleaned2.thinking = thinking;
3214
+ }
3215
+ if (msg.hidden) {
3216
+ cleaned2.hidden = true;
3217
+ }
3218
+ return cleaned2;
3219
+ });
3220
+ return [...prefix, ...cleaned];
3221
+ }
3222
+ var init_cleanMessages = __esm({
3223
+ "src/subagents/common/cleanMessages.ts"() {
3224
+ "use strict";
3225
+ }
3226
+ });
3227
+
3228
+ // src/subagents/runner.ts
3229
+ async function runSubAgent(config) {
3230
+ const {
3231
+ system,
3232
+ task,
2551
3233
  tools: tools2,
2552
3234
  externalTools,
2553
3235
  executeTool: executeTool2,
@@ -2568,7 +3250,7 @@ async function runSubAgent(config) {
2568
3250
  const signal = background ? bgAbort.signal : parentSignal;
2569
3251
  const agentName = subAgentId || "sub-agent";
2570
3252
  const runStart = Date.now();
2571
- log3.info("Sub-agent started", { requestId, parentToolId, agentName });
3253
+ log6.info("Sub-agent started", { requestId, parentToolId, agentName });
2572
3254
  const emit2 = (e) => {
2573
3255
  onEvent({ ...e, parentToolId });
2574
3256
  };
@@ -2737,7 +3419,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2737
3419
  const text = getPartialText(contentBlocks);
2738
3420
  return { text, messages: thisInvocation() };
2739
3421
  }
2740
- log3.info("Tools executing", {
3422
+ log6.info("Tools executing", {
2741
3423
  requestId,
2742
3424
  parentToolId,
2743
3425
  count: toolCalls.length,
@@ -2814,7 +3496,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2814
3496
  run2(tc.input);
2815
3497
  const r = await resultPromise;
2816
3498
  toolRegistry?.unregister(tc.id);
2817
- log3.info("Tool completed", {
3499
+ log6.info("Tool completed", {
2818
3500
  requestId,
2819
3501
  parentToolId,
2820
3502
  toolCallId: tc.id,
@@ -2859,7 +3541,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2859
3541
  const wrapRun = async () => {
2860
3542
  try {
2861
3543
  const result = await run();
2862
- log3.info("Sub-agent complete", {
3544
+ log6.info("Sub-agent complete", {
2863
3545
  requestId,
2864
3546
  parentToolId,
2865
3547
  agentName,
@@ -2868,7 +3550,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2868
3550
  });
2869
3551
  return result;
2870
3552
  } catch (err) {
2871
- log3.warn("Sub-agent error", {
3553
+ log6.warn("Sub-agent error", {
2872
3554
  requestId,
2873
3555
  parentToolId,
2874
3556
  agentName,
@@ -2880,7 +3562,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2880
3562
  if (!background) {
2881
3563
  return wrapRun();
2882
3564
  }
2883
- log3.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3565
+ log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
2884
3566
  toolRegistry?.register({
2885
3567
  id: parentToolId,
2886
3568
  name: agentName,
@@ -2907,7 +3589,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2907
3589
  });
2908
3590
  return { text: ack, messages: [], backgrounded: true };
2909
3591
  }
2910
- var log3;
3592
+ var log6;
2911
3593
  var init_runner = __esm({
2912
3594
  "src/subagents/runner.ts"() {
2913
3595
  "use strict";
@@ -2915,7 +3597,7 @@ var init_runner = __esm({
2915
3597
  init_logger();
2916
3598
  init_statusWatcher();
2917
3599
  init_cleanMessages();
2918
- log3 = createLogger("sub-agent");
3600
+ log6 = createLogger("sub-agent");
2919
3601
  }
2920
3602
  });
2921
3603
 
@@ -3058,54 +3740,11 @@ var init_tools = __esm({
3058
3740
  }
3059
3741
  });
3060
3742
 
3061
- // src/assets.ts
3062
- import fs10 from "fs";
3063
- import path5 from "path";
3064
- function findRoot(start) {
3065
- let dir = start;
3066
- while (dir !== path5.dirname(dir)) {
3067
- if (fs10.existsSync(path5.join(dir, "package.json"))) {
3068
- return dir;
3069
- }
3070
- dir = path5.dirname(dir);
3071
- }
3072
- return start;
3073
- }
3074
- function assetPath(...segments) {
3075
- return path5.join(ASSETS_BASE, ...segments);
3076
- }
3077
- function readAsset(...segments) {
3078
- const full = assetPath(...segments);
3079
- try {
3080
- return fs10.readFileSync(full, "utf-8").trim();
3081
- } catch {
3082
- throw new Error(`Required asset missing: ${full}`);
3083
- }
3084
- }
3085
- function readJsonAsset(fallback, ...segments) {
3086
- const full = assetPath(...segments);
3087
- try {
3088
- return JSON.parse(fs10.readFileSync(full, "utf-8"));
3089
- } catch {
3090
- return fallback;
3091
- }
3092
- }
3093
- var ROOT, ASSETS_BASE;
3094
- var init_assets = __esm({
3095
- "src/assets.ts"() {
3096
- "use strict";
3097
- ROOT = findRoot(
3098
- import.meta.dirname ?? path5.dirname(new URL(import.meta.url).pathname)
3099
- );
3100
- ASSETS_BASE = fs10.existsSync(path5.join(ROOT, "dist", "prompt")) ? path5.join(ROOT, "dist") : path5.join(ROOT, "src");
3101
- }
3102
- });
3103
-
3104
3743
  // src/subagents/browserAutomation/prompt.ts
3105
- import fs11 from "fs";
3744
+ import fs13 from "fs";
3106
3745
  function getBrowserAutomationPrompt() {
3107
3746
  try {
3108
- const appSpec = fs11.readFileSync("src/app.md", "utf-8").trim();
3747
+ const appSpec = fs13.readFileSync("src/app.md", "utf-8").trim();
3109
3748
  return `${BASE_PROMPT}
3110
3749
 
3111
3750
  <!-- cache_breakpoint -->
@@ -3118,7 +3757,7 @@ ${appSpec}
3118
3757
  }
3119
3758
  }
3120
3759
  var BASE_PROMPT;
3121
- var init_prompt = __esm({
3760
+ var init_prompt2 = __esm({
3122
3761
  "src/subagents/browserAutomation/prompt.ts"() {
3123
3762
  "use strict";
3124
3763
  init_assets();
@@ -3127,19 +3766,19 @@ var init_prompt = __esm({
3127
3766
  });
3128
3767
 
3129
3768
  // src/subagents/browserAutomation/index.ts
3130
- var log4, browserAutomationTool;
3769
+ var log7, browserAutomationTool;
3131
3770
  var init_browserAutomation = __esm({
3132
3771
  "src/subagents/browserAutomation/index.ts"() {
3133
3772
  "use strict";
3134
3773
  init_runner();
3135
3774
  init_tools();
3136
- init_prompt();
3775
+ init_prompt2();
3137
3776
  init_sidecar();
3138
3777
  init_browserLock();
3139
3778
  init_screenshot();
3140
3779
  init_runCli();
3141
3780
  init_logger();
3142
- log4 = createLogger("browser-automation");
3781
+ log7 = createLogger("browser-automation");
3143
3782
  browserAutomationTool = {
3144
3783
  clearable: true,
3145
3784
  definition: {
@@ -3245,7 +3884,7 @@ var init_browserAutomation = __esm({
3245
3884
  }
3246
3885
  }
3247
3886
  } catch {
3248
- log4.debug("Failed to parse batch analysis result", {
3887
+ log7.debug("Failed to parse batch analysis result", {
3249
3888
  batchResult
3250
3889
  });
3251
3890
  }
@@ -4062,16 +4701,16 @@ var init_tools3 = __esm({
4062
4701
  });
4063
4702
 
4064
4703
  // src/subagents/common/context.ts
4065
- import fs12 from "fs";
4066
- import path6 from "path";
4067
- function walkMdFiles(dir, skip) {
4704
+ import fs14 from "fs";
4705
+ import path7 from "path";
4706
+ function walkMdFiles2(dir, skip) {
4068
4707
  const files = [];
4069
4708
  try {
4070
- for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
4071
- const full = path6.join(dir, entry.name);
4709
+ for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
4710
+ const full = path7.join(dir, entry.name);
4072
4711
  if (entry.isDirectory()) {
4073
4712
  if (!skip?.has(entry.name)) {
4074
- files.push(...walkMdFiles(full, skip));
4713
+ files.push(...walkMdFiles2(full, skip));
4075
4714
  }
4076
4715
  } else if (entry.name.endsWith(".md")) {
4077
4716
  files.push(full);
@@ -4081,9 +4720,9 @@ function walkMdFiles(dir, skip) {
4081
4720
  }
4082
4721
  return files.sort();
4083
4722
  }
4084
- function parseFrontmatter(filePath) {
4723
+ function parseFrontmatter2(filePath) {
4085
4724
  try {
4086
- const content = fs12.readFileSync(filePath, "utf-8");
4725
+ const content = fs14.readFileSync(filePath, "utf-8");
4087
4726
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4088
4727
  if (!match) {
4089
4728
  return {};
@@ -4103,12 +4742,12 @@ function parseFrontmatter(filePath) {
4103
4742
  }
4104
4743
  }
4105
4744
  function loadSpecIndex() {
4106
- const files = walkMdFiles("src", /* @__PURE__ */ new Set(["roadmap"]));
4745
+ const files = walkMdFiles2("src", /* @__PURE__ */ new Set(["roadmap"]));
4107
4746
  if (files.length === 0) {
4108
4747
  return "";
4109
4748
  }
4110
4749
  const lines = files.map((f) => {
4111
- const fm = parseFrontmatter(f);
4750
+ const fm = parseFrontmatter2(f);
4112
4751
  let line = `- ${f}`;
4113
4752
  if (fm.name) {
4114
4753
  line += ` \u2014 "${fm.name}"`;
@@ -4129,7 +4768,7 @@ function loadRoadmapIndex() {
4129
4768
  const parts = [];
4130
4769
  try {
4131
4770
  const indexJson = JSON.parse(
4132
- fs12.readFileSync("src/roadmap/index.json", "utf-8")
4771
+ fs14.readFileSync("src/roadmap/index.json", "utf-8")
4133
4772
  );
4134
4773
  if (indexJson.lanes?.length > 0) {
4135
4774
  const laneLines = indexJson.lanes.map(
@@ -4146,10 +4785,10 @@ ${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
4146
4785
  }
4147
4786
  } catch {
4148
4787
  }
4149
- const files = walkMdFiles("src/roadmap");
4788
+ const files = walkMdFiles2("src/roadmap");
4150
4789
  if (files.length > 0) {
4151
4790
  const lines = files.map((f) => {
4152
- const fm = parseFrontmatter(f);
4791
+ const fm = parseFrontmatter2(f);
4153
4792
  let line = `- ${f}`;
4154
4793
  if (fm.name) {
4155
4794
  line += ` \u2014 "${fm.name}"`;
@@ -4237,7 +4876,7 @@ var init_context = __esm({
4237
4876
  });
4238
4877
 
4239
4878
  // src/subagents/designExpert/data/sampleCache.ts
4240
- import fs13 from "fs";
4879
+ import fs15 from "fs";
4241
4880
  function generateIndices(poolSize, sampleSize) {
4242
4881
  const n = Math.min(sampleSize, poolSize);
4243
4882
  const indices = Array.from({ length: poolSize }, (_, i) => i);
@@ -4249,14 +4888,14 @@ function generateIndices(poolSize, sampleSize) {
4249
4888
  }
4250
4889
  function load() {
4251
4890
  try {
4252
- return JSON.parse(fs13.readFileSync(SAMPLE_FILE, "utf-8"));
4891
+ return JSON.parse(fs15.readFileSync(SAMPLE_FILE, "utf-8"));
4253
4892
  } catch {
4254
4893
  return null;
4255
4894
  }
4256
4895
  }
4257
4896
  function save(indices) {
4258
4897
  try {
4259
- fs13.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
4898
+ fs15.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
4260
4899
  } catch {
4261
4900
  }
4262
4901
  }
@@ -4439,7 +5078,7 @@ This project is in the "${state}" phase. The codebase is a placeholder scaffold
4439
5078
  return prompt;
4440
5079
  }
4441
5080
  var SUBAGENT, RUNTIME_PLACEHOLDERS, PROMPT_TEMPLATE;
4442
- var init_prompt2 = __esm({
5081
+ var init_prompt3 = __esm({
4443
5082
  "src/subagents/designExpert/prompt.ts"() {
4444
5083
  "use strict";
4445
5084
  init_assets();
@@ -4519,7 +5158,7 @@ var init_designExpert = __esm({
4519
5158
  init_runner();
4520
5159
  init_tools3();
4521
5160
  init_tools2();
4522
- init_prompt2();
5161
+ init_prompt3();
4523
5162
  init_history();
4524
5163
  DESCRIPTION = `
4525
5164
  Visual design expert. Describe the situation and what you need \u2014 the agent decides what to deliver. It reads the spec files automatically. Include relevant user requirements and context it can't get from the spec, but do not list specific deliverables or tell it how to do its job. Do not suggest implementation details or ideas - only relay what is needed.
@@ -4653,23 +5292,23 @@ var init_tools4 = __esm({
4653
5292
  });
4654
5293
 
4655
5294
  // src/subagents/productVision/executor.ts
4656
- import fs14 from "fs";
4657
- import path7 from "path";
5295
+ import fs16 from "fs";
5296
+ import path8 from "path";
4658
5297
  function resolve(filePath) {
4659
- return path7.join(ROADMAP_DIR, filePath);
5298
+ return path8.join(ROADMAP_DIR, filePath);
4660
5299
  }
4661
5300
  async function executeVisionTool(name, input, context) {
4662
5301
  switch (name) {
4663
5302
  case "writeFile": {
4664
5303
  const filePath = resolve(input.path);
4665
5304
  try {
4666
- fs14.mkdirSync(ROADMAP_DIR, { recursive: true });
5305
+ fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
4667
5306
  let oldContent = null;
4668
5307
  try {
4669
- oldContent = fs14.readFileSync(filePath, "utf-8");
5308
+ oldContent = fs16.readFileSync(filePath, "utf-8");
4670
5309
  } catch {
4671
5310
  }
4672
- fs14.writeFileSync(filePath, input.content, "utf-8");
5311
+ fs16.writeFileSync(filePath, input.content, "utf-8");
4673
5312
  const lineCount = input.content.split("\n").length;
4674
5313
  const label = oldContent !== null ? "Wrote" : "Created";
4675
5314
  return `${label} ${filePath} (${lineCount} lines)
@@ -4681,11 +5320,11 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
4681
5320
  case "deleteFile": {
4682
5321
  const filePath = resolve(input.path);
4683
5322
  try {
4684
- if (!fs14.existsSync(filePath)) {
5323
+ if (!fs16.existsSync(filePath)) {
4685
5324
  return `Error: ${filePath} does not exist`;
4686
5325
  }
4687
- const oldContent = fs14.readFileSync(filePath, "utf-8");
4688
- fs14.unlinkSync(filePath);
5326
+ const oldContent = fs16.readFileSync(filePath, "utf-8");
5327
+ fs16.unlinkSync(filePath);
4689
5328
  return `Deleted ${filePath}
4690
5329
  ${unifiedDiff(filePath, oldContent, "")}`;
4691
5330
  } catch (err) {
@@ -4698,8 +5337,8 @@ ${unifiedDiff(filePath, oldContent, "")}`;
4698
5337
  }
4699
5338
  const filePath = resolve("pitch.html");
4700
5339
  try {
4701
- fs14.mkdirSync(ROADMAP_DIR, { recursive: true });
4702
- const existing = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf-8").trim() : "";
5340
+ fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
5341
+ const existing = fs16.existsSync(filePath) ? fs16.readFileSync(filePath, "utf-8").trim() : "";
4703
5342
  const currentDeck = existing || PITCH_DECK_SHELL;
4704
5343
  const task = `
4705
5344
  <pitch_content>${input.task}</pitch_content>
@@ -4724,7 +5363,7 @@ Respond only with the complete HTML file and absolutely no other text. Your resp
4724
5363
  /```(?:html|wireframe)\n([\s\S]*?)```/
4725
5364
  );
4726
5365
  const html = htmlMatch ? htmlMatch[1].trim() : result;
4727
- fs14.writeFileSync(filePath, html, "utf-8");
5366
+ fs16.writeFileSync(filePath, html, "utf-8");
4728
5367
  return `Pitch deck written successfully.`;
4729
5368
  } catch (err) {
4730
5369
  return `Error generating pitch deck: ${err.message}`;
@@ -4765,7 +5404,7 @@ function getProductVisionPrompt() {
4765
5404
  return parts.join("\n\n");
4766
5405
  }
4767
5406
  var BASE_PROMPT2;
4768
- var init_prompt3 = __esm({
5407
+ var init_prompt4 = __esm({
4769
5408
  "src/subagents/productVision/prompt.ts"() {
4770
5409
  "use strict";
4771
5410
  init_assets();
@@ -4785,7 +5424,7 @@ var init_productVision = __esm({
4785
5424
  init_tools4();
4786
5425
  init_tools2();
4787
5426
  init_executor();
4788
- init_prompt3();
5427
+ init_prompt4();
4789
5428
  init_history();
4790
5429
  productVisionTool = {
4791
5430
  clearable: false,
@@ -5045,6 +5684,7 @@ var init_tools6 = __esm({
5045
5684
  init_sdkConsultant();
5046
5685
  init_searchGoogle();
5047
5686
  init_setProjectMetadata();
5687
+ init_compactConversation();
5048
5688
  init_readFile();
5049
5689
  init_writeFile();
5050
5690
  init_editFile();
@@ -5076,6 +5716,7 @@ var init_tools6 = __esm({
5076
5716
  designExpertTool,
5077
5717
  productVisionTool,
5078
5718
  codeSanityCheckTool,
5719
+ compactConversationTool,
5079
5720
  // Post-onboarding
5080
5721
  clearSyncStatusTool,
5081
5722
  presentSyncPlanTool,
@@ -5110,86 +5751,6 @@ var init_tools6 = __esm({
5110
5751
  }
5111
5752
  });
5112
5753
 
5113
- // src/session.ts
5114
- import fs15 from "fs";
5115
- function loadSession(state) {
5116
- try {
5117
- const raw = fs15.readFileSync(SESSION_FILE, "utf-8");
5118
- const data = JSON.parse(raw);
5119
- if (Array.isArray(data.messages) && data.messages.length > 0) {
5120
- state.messages = sanitizeMessages(data.messages);
5121
- log5.info("Session loaded", { messageCount: state.messages.length });
5122
- return true;
5123
- }
5124
- } catch {
5125
- }
5126
- return false;
5127
- }
5128
- function sanitizeMessages(messages) {
5129
- const result = [];
5130
- for (let i = 0; i < messages.length; i++) {
5131
- result.push(messages[i]);
5132
- const msg = messages[i];
5133
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
5134
- continue;
5135
- }
5136
- const toolBlocks = msg.content.filter(
5137
- (b) => b.type === "tool"
5138
- );
5139
- if (toolBlocks.length === 0) {
5140
- continue;
5141
- }
5142
- const resultIds = /* @__PURE__ */ new Set();
5143
- for (let j = i + 1; j < messages.length; j++) {
5144
- const next = messages[j];
5145
- if (next.role === "user" && next.toolCallId) {
5146
- resultIds.add(next.toolCallId);
5147
- } else {
5148
- break;
5149
- }
5150
- }
5151
- for (const tc of toolBlocks) {
5152
- if (!resultIds.has(tc.id)) {
5153
- result.push({
5154
- role: "user",
5155
- content: "Error: tool result lost (session recovered)",
5156
- toolCallId: tc.id,
5157
- isToolError: true
5158
- });
5159
- }
5160
- }
5161
- }
5162
- return result;
5163
- }
5164
- function saveSession(state) {
5165
- try {
5166
- fs15.writeFileSync(
5167
- SESSION_FILE,
5168
- JSON.stringify({ messages: state.messages }, null, 2),
5169
- "utf-8"
5170
- );
5171
- log5.info("Session saved", { messageCount: state.messages.length });
5172
- } catch (err) {
5173
- log5.warn("Session save failed", { error: err.message });
5174
- }
5175
- }
5176
- function clearSession(state) {
5177
- state.messages = [];
5178
- try {
5179
- fs15.unlinkSync(SESSION_FILE);
5180
- } catch {
5181
- }
5182
- }
5183
- var log5, SESSION_FILE;
5184
- var init_session = __esm({
5185
- "src/session.ts"() {
5186
- "use strict";
5187
- init_logger();
5188
- log5 = createLogger("session");
5189
- SESSION_FILE = ".remy-session.json";
5190
- }
5191
- });
5192
-
5193
5754
  // src/parsePartialJson.ts
5194
5755
  function parsePartialJson(jsonString) {
5195
5756
  const length = jsonString.length;
@@ -5422,7 +5983,7 @@ async function runTurn(params) {
5422
5983
  } = params;
5423
5984
  const tools2 = getToolDefinitions(onboardingState);
5424
5985
  const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
5425
- log6.info("Turn started", {
5986
+ log8.info("Turn started", {
5426
5987
  requestId,
5427
5988
  model,
5428
5989
  toolCount: tools2.length,
@@ -5646,7 +6207,7 @@ async function runTurn(params) {
5646
6207
  const tool = getToolByName(event.name);
5647
6208
  const wasStreamed = acc?.started ?? false;
5648
6209
  const isInputStreaming = !!tool?.streaming?.partialInput;
5649
- log6.info("Tool received", {
6210
+ log8.info("Tool received", {
5650
6211
  requestId,
5651
6212
  toolCallId: event.id,
5652
6213
  name: event.name
@@ -5693,7 +6254,14 @@ async function runTurn(params) {
5693
6254
  });
5694
6255
  state.messages.push({
5695
6256
  role: "assistant",
5696
- content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt)
6257
+ content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
6258
+ usage: {
6259
+ inputTokens: turnInputTokens,
6260
+ outputTokens: turnOutputTokens,
6261
+ cacheCreationTokens: turnCacheCreation || void 0,
6262
+ cacheReadTokens: turnCacheRead || void 0,
6263
+ llmCalls: turnLlmCalls
6264
+ }
5697
6265
  });
5698
6266
  }
5699
6267
  onEvent({ type: "turn_cancelled" });
@@ -5703,7 +6271,14 @@ async function runTurn(params) {
5703
6271
  if (contentBlocks.length > 0) {
5704
6272
  state.messages.push({
5705
6273
  role: "assistant",
5706
- content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt)
6274
+ content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
6275
+ usage: {
6276
+ inputTokens: turnInputTokens,
6277
+ outputTokens: turnOutputTokens,
6278
+ cacheCreationTokens: turnCacheCreation || void 0,
6279
+ cacheReadTokens: turnCacheRead || void 0,
6280
+ llmCalls: turnLlmCalls
6281
+ }
5707
6282
  });
5708
6283
  }
5709
6284
  const toolCalls = getToolCalls(contentBlocks);
@@ -5725,7 +6300,7 @@ async function runTurn(params) {
5725
6300
  });
5726
6301
  return;
5727
6302
  }
5728
- log6.info("Tools executing", {
6303
+ log8.info("Tools executing", {
5729
6304
  requestId,
5730
6305
  count: toolCalls.length,
5731
6306
  tools: toolCalls.map((tc) => tc.name)
@@ -5772,7 +6347,7 @@ async function runTurn(params) {
5772
6347
  let result;
5773
6348
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
5774
6349
  saveSession(state);
5775
- log6.info("Waiting for external tool result", {
6350
+ log8.info("Waiting for external tool result", {
5776
6351
  requestId,
5777
6352
  toolCallId: tc.id,
5778
6353
  name: tc.name
@@ -5829,7 +6404,7 @@ async function runTurn(params) {
5829
6404
  if (!tc.input.background) {
5830
6405
  toolRegistry?.unregister(tc.id);
5831
6406
  }
5832
- log6.info("Tool completed", {
6407
+ log8.info("Tool completed", {
5833
6408
  requestId,
5834
6409
  toolCallId: tc.id,
5835
6410
  name: tc.name,
@@ -5848,346 +6423,68 @@ async function runTurn(params) {
5848
6423
  );
5849
6424
  statusWatcher.stop();
5850
6425
  for (const r of results) {
5851
- const block = contentBlocks.find(
5852
- (b) => b.type === "tool" && b.id === r.id
5853
- );
5854
- if (block?.type === "tool") {
5855
- block.result = r.result;
5856
- block.isError = r.isError;
5857
- block.completedAt = Date.now();
5858
- const msgs = subAgentMessages.get(r.id);
5859
- if (msgs) {
5860
- block.subAgentMessages = msgs;
5861
- }
5862
- }
5863
- }
5864
- const lastNonExcluded = toolCalls.filter(
5865
- (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
5866
- );
5867
- lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
5868
- lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
5869
- lastCompletedResult = results.at(-1)?.result ?? "";
5870
- for (const r of results) {
5871
- state.messages.push({
5872
- role: "user",
5873
- content: r.result,
5874
- toolCallId: r.id,
5875
- isToolError: r.isError
5876
- });
5877
- }
5878
- if (signal?.aborted) {
5879
- onEvent({ type: "turn_cancelled" });
5880
- saveSession(state);
5881
- return;
5882
- }
5883
- }
5884
- }
5885
- var log6, EXTERNAL_TOOLS;
5886
- var init_agent = __esm({
5887
- "src/agent.ts"() {
5888
- "use strict";
5889
- init_api();
5890
- init_tools6();
5891
- init_session();
5892
- init_logger();
5893
- init_parsePartialJson();
5894
- init_statusWatcher();
5895
- init_errors();
5896
- init_cleanMessages();
5897
- init_tools6();
5898
- log6 = createLogger("agent");
5899
- EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
5900
- "promptUser",
5901
- "setProjectOnboardingState",
5902
- "clearSyncStatus",
5903
- "presentSyncPlan",
5904
- "presentPublishPlan",
5905
- "presentPlan",
5906
- "confirmDestructiveAction",
5907
- "runScenario",
5908
- "runMethod",
5909
- "queryDatabase",
5910
- "browserCommand",
5911
- "setProjectMetadata"
5912
- ]);
5913
- }
5914
- });
5915
-
5916
- // src/prompt/static/projectContext.ts
5917
- import fs16 from "fs";
5918
- import path8 from "path";
5919
- function loadProjectInstructions() {
5920
- for (const file of AGENT_INSTRUCTION_FILES) {
5921
- try {
5922
- const content = fs16.readFileSync(file, "utf-8").trim();
5923
- if (content) {
5924
- return `
5925
- ## Project Instructions (${file})
5926
- ${content}`;
5927
- }
5928
- } catch {
5929
- }
5930
- }
5931
- return "";
5932
- }
5933
- function loadProjectManifest() {
5934
- try {
5935
- const manifest = fs16.readFileSync("mindstudio.json", "utf-8");
5936
- return `
5937
- ## Project Manifest (mindstudio.json)
5938
- \`\`\`json
5939
- ${manifest}
5940
- \`\`\``;
5941
- } catch {
5942
- return "";
5943
- }
5944
- }
5945
- function loadSpecFileMetadata() {
5946
- try {
5947
- const files = walkMdFiles2("src");
5948
- if (files.length === 0) {
5949
- return "";
5950
- }
5951
- const entries = [];
5952
- for (const filePath of files) {
5953
- const { name, description, type } = parseFrontmatter2(filePath);
5954
- let line = `- ${filePath}`;
5955
- if (name) {
5956
- line += ` \u2014 "${name}"`;
5957
- }
5958
- if (type) {
5959
- line += ` (${type})`;
5960
- }
5961
- if (description) {
5962
- line += ` \u2014 ${description}`;
6426
+ const block = contentBlocks.find(
6427
+ (b) => b.type === "tool" && b.id === r.id
6428
+ );
6429
+ if (block?.type === "tool") {
6430
+ block.result = r.result;
6431
+ block.isError = r.isError;
6432
+ block.completedAt = Date.now();
6433
+ const msgs = subAgentMessages.get(r.id);
6434
+ if (msgs) {
6435
+ block.subAgentMessages = msgs;
6436
+ }
5963
6437
  }
5964
- entries.push(line);
5965
6438
  }
5966
- return `
5967
- ## Spec Files
5968
- ${entries.join("\n")}`;
5969
- } catch {
5970
- return "";
5971
- }
5972
- }
5973
- function walkMdFiles2(dir) {
5974
- const results = [];
5975
- try {
5976
- const entries = fs16.readdirSync(dir, { withFileTypes: true });
5977
- for (const entry of entries) {
5978
- const full = path8.join(dir, entry.name);
5979
- if (entry.isDirectory()) {
5980
- results.push(...walkMdFiles2(full));
5981
- } else if (entry.name.endsWith(".md")) {
5982
- results.push(full);
5983
- }
6439
+ const lastNonExcluded = toolCalls.filter(
6440
+ (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
6441
+ );
6442
+ lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
6443
+ lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
6444
+ lastCompletedResult = results.at(-1)?.result ?? "";
6445
+ for (const r of results) {
6446
+ state.messages.push({
6447
+ role: "user",
6448
+ content: r.result,
6449
+ toolCallId: r.id,
6450
+ isToolError: r.isError
6451
+ });
5984
6452
  }
5985
- } catch {
5986
- }
5987
- return results.sort();
5988
- }
5989
- function parseFrontmatter2(filePath) {
5990
- try {
5991
- const content = fs16.readFileSync(filePath, "utf-8");
5992
- const match = content.match(/^---\n([\s\S]*?)\n---/);
5993
- if (!match) {
5994
- return { name: "", description: "", type: "" };
6453
+ if (signal?.aborted) {
6454
+ onEvent({ type: "turn_cancelled" });
6455
+ saveSession(state);
6456
+ return;
5995
6457
  }
5996
- const fm = match[1];
5997
- const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
5998
- const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
5999
- const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
6000
- return { name, description, type };
6001
- } catch {
6002
- return { name: "", description: "", type: "" };
6003
- }
6004
- }
6005
- function loadProjectFileListing() {
6006
- try {
6007
- const entries = fs16.readdirSync(".", { withFileTypes: true });
6008
- const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
6009
- if (a.isDirectory() && !b.isDirectory()) {
6010
- return -1;
6011
- }
6012
- if (!a.isDirectory() && b.isDirectory()) {
6013
- return 1;
6014
- }
6015
- return a.name.localeCompare(b.name);
6016
- }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
6017
- return `
6018
- ## Project Files
6019
- \`\`\`
6020
- ${listing}
6021
- \`\`\``;
6022
- } catch {
6023
- return "";
6024
- }
6025
- }
6026
- var AGENT_INSTRUCTION_FILES;
6027
- var init_projectContext = __esm({
6028
- "src/prompt/static/projectContext.ts"() {
6029
- "use strict";
6030
- AGENT_INSTRUCTION_FILES = [
6031
- "CLAUDE.md",
6032
- "claude.md",
6033
- ".claude/instructions.md",
6034
- "AGENTS.md",
6035
- "agents.md",
6036
- ".agents.md",
6037
- "COPILOT.md",
6038
- "copilot.md",
6039
- ".copilot-instructions.md",
6040
- ".github/copilot-instructions.md",
6041
- "REMY.md",
6042
- "remy.md",
6043
- ".cursorrules",
6044
- ".cursorules"
6045
- ];
6046
6458
  }
6047
- });
6048
-
6049
- // src/prompt/index.ts
6050
- function resolveIncludes(template) {
6051
- const result = template.replace(
6052
- /\{\{([^}]+)\}\}/g,
6053
- (_, filePath) => readAsset("prompt", filePath.trim())
6054
- );
6055
- return result.replace(/\n{3,}/g, "\n\n").trim();
6056
- }
6057
- function buildSystemPrompt(onboardingState, viewContext) {
6058
- const projectContext = [
6059
- loadProjectInstructions(),
6060
- loadProjectManifest(),
6061
- loadSpecFileMetadata(),
6062
- loadProjectFileListing()
6063
- ].filter(Boolean).join("\n");
6064
- const now = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
6065
- month: "long",
6066
- day: "numeric",
6067
- year: "numeric"
6068
- });
6069
- const template = `
6070
- {{static/identity.md}}
6071
-
6072
- Current date: ${now}
6073
-
6074
- <platform_docs>
6075
- <platform>
6076
- {{compiled/platform.md}}
6077
- </platform>
6078
-
6079
- <manifest>
6080
- {{compiled/manifest.md}}
6081
- </manifest>
6082
-
6083
- <tables>
6084
- {{compiled/tables.md}}
6085
- </tables>
6086
-
6087
- <methods>
6088
- {{compiled/methods.md}}
6089
- </methods>
6090
-
6091
- <auth>
6092
- {{compiled/auth.md}}
6093
- </auth>
6094
-
6095
- <dev_and_deploy>
6096
- {{compiled/dev-and-deploy.md}}
6097
- </dev_and_deploy>
6098
-
6099
- <design>
6100
- {{compiled/design.md}}
6101
- </design>
6102
-
6103
- <building_agent_interfaces>
6104
- {{compiled/agent-interfaces.md}}
6105
- </building_agent_interfaces>
6106
-
6107
- <media_cdn>
6108
- {{compiled/media-cdn.md}}
6109
- </media_cdn>
6110
-
6111
- <interfaces>
6112
- {{compiled/interfaces.md}}
6113
- </interfaces>
6114
-
6115
- <scenarios>
6116
- {{compiled/scenarios.md}}
6117
- </scenarios>
6118
-
6119
- <secrets>
6120
- {{compiled/secrets.md}}
6121
- </secrets>
6122
- </platform_docs>
6123
-
6124
- <mindstudio_agent_sdk_docs>
6125
- {{compiled/sdk-actions.md}}
6126
-
6127
- {{compiled/task-agents.md}}
6128
- </mindstudio_agent_sdk_docs>
6129
-
6130
- <mindstudio_flavored_markdown_spec_docs>
6131
- {{compiled/msfm.md}}
6132
- </mindstudio_flavored_markdown_spec_docs>
6133
-
6134
- <project_context>
6135
- ${projectContext}
6136
- </project_context>
6137
-
6138
- <intake_mode_instructions>
6139
- {{static/intake.md}}
6140
- </intake_mode_instructions>
6141
-
6142
- <spec_authoring_instructions>
6143
- {{static/authoring.md}}
6144
- </spec_authoring_instructions>
6145
-
6146
- {{static/team.md}}
6147
-
6148
- <code_authoring_instructions>
6149
- {{static/coding.md}}
6150
- ${isLspConfigured() ? `<typescript_lsp>
6151
- {{static/lsp.md}}
6152
- </typescript_lsp>` : ""}
6153
- </code_authoring_instructions>
6154
-
6155
- {{static/instructions.md}}
6156
-
6157
- <conversation_summaries>
6158
- Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
6159
-
6160
- Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query.
6161
- </conversation_summaries>
6162
-
6163
- <project_onboarding>
6164
- New projects progress through four onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
6165
-
6166
- - **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code.
6167
- - **initialSpecAuthoring**: Writing and refining the first spec. The user can see it in the editor as it streams in and can give feedback to iterate on it. This phase covers both the initial draft and any back-and-forth refinement before code generation.
6168
- - **initialCodegen**: First code generation from the spec. The agent is generating methods, tables, interfaces, manifest updates, and scenarios. This can take a while and involves heavy tool use. The user sees a full-screen build progress view.
6169
- - **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
6170
-
6171
- <!-- cache_breakpoint -->
6172
-
6173
- <current_project_onboarding_state>
6174
- ${onboardingState ?? "onboardingFinished"}
6175
- </current_project_onboarding_state>
6176
- </project_onboarding>
6177
-
6178
- <view_context>
6179
- The user is currently in ${viewContext?.mode ?? "code"} mode.
6180
- ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
6181
- </view_context>
6182
- `;
6183
- return resolveIncludes(template);
6184
6459
  }
6185
- var init_prompt4 = __esm({
6186
- "src/prompt/index.ts"() {
6460
+ var log8, EXTERNAL_TOOLS;
6461
+ var init_agent = __esm({
6462
+ "src/agent.ts"() {
6187
6463
  "use strict";
6188
- init_assets();
6189
- init_lsp();
6190
- init_projectContext();
6464
+ init_api();
6465
+ init_tools6();
6466
+ init_session();
6467
+ init_logger();
6468
+ init_parsePartialJson();
6469
+ init_statusWatcher();
6470
+ init_errors();
6471
+ init_cleanMessages();
6472
+ init_tools6();
6473
+ log8 = createLogger("agent");
6474
+ EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
6475
+ "promptUser",
6476
+ "setProjectOnboardingState",
6477
+ "clearSyncStatus",
6478
+ "presentSyncPlan",
6479
+ "presentPublishPlan",
6480
+ "presentPlan",
6481
+ "confirmDestructiveAction",
6482
+ "runScenario",
6483
+ "runMethod",
6484
+ "queryDatabase",
6485
+ "browserCommand",
6486
+ "setProjectMetadata"
6487
+ ]);
6191
6488
  }
6192
6489
  });
6193
6490
 
@@ -6198,10 +6495,10 @@ import os from "os";
6198
6495
  function loadConfigFile() {
6199
6496
  try {
6200
6497
  const raw = fs17.readFileSync(CONFIG_PATH, "utf-8");
6201
- log7.debug("Loaded config file", { path: CONFIG_PATH });
6498
+ log9.debug("Loaded config file", { path: CONFIG_PATH });
6202
6499
  return JSON.parse(raw);
6203
6500
  } catch (err) {
6204
- log7.debug("No config file found", {
6501
+ log9.debug("No config file found", {
6205
6502
  path: CONFIG_PATH,
6206
6503
  error: err.message
6207
6504
  });
@@ -6215,25 +6512,25 @@ function resolveConfig(flags2) {
6215
6512
  const apiKey = flags2?.apiKey || process.env.MINDSTUDIO_API_KEY || env?.apiKey || "";
6216
6513
  const baseUrl2 = flags2?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
6217
6514
  if (!apiKey) {
6218
- log7.error("No API key found");
6515
+ log9.error("No API key found");
6219
6516
  throw new Error(
6220
6517
  "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
6221
6518
  );
6222
6519
  }
6223
6520
  const keySource = flags2?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
6224
- log7.info("Config resolved", {
6521
+ log9.info("Config resolved", {
6225
6522
  baseUrl: baseUrl2,
6226
6523
  keySource,
6227
6524
  environment: activeEnv
6228
6525
  });
6229
6526
  return { apiKey, baseUrl: baseUrl2 };
6230
6527
  }
6231
- var log7, CONFIG_PATH, DEFAULT_BASE_URL;
6528
+ var log9, CONFIG_PATH, DEFAULT_BASE_URL;
6232
6529
  var init_config = __esm({
6233
6530
  "src/config.ts"() {
6234
6531
  "use strict";
6235
6532
  init_logger();
6236
- log7 = createLogger("config");
6533
+ log9 = createLogger("config");
6237
6534
  CONFIG_PATH = path9.join(
6238
6535
  os.homedir(),
6239
6536
  ".mindstudio-local-tunnel",
@@ -6243,235 +6540,13 @@ var init_config = __esm({
6243
6540
  }
6244
6541
  });
6245
6542
 
6246
- // src/compaction/index.ts
6247
- async function compactConversation(state, apiConfig, system, tools2) {
6248
- const insertionIndex = findSafeInsertionPoint(state.messages);
6249
- const summaries = [];
6250
- const tasks = [];
6251
- const conversationMessages = getConversationMessagesForSummary(
6252
- state.messages,
6253
- insertionIndex
6254
- );
6255
- if (conversationMessages.length > 0) {
6256
- tasks.push(
6257
- generateSummary(
6258
- apiConfig,
6259
- "conversation",
6260
- CONVERSATION_SUMMARY_PROMPT,
6261
- conversationMessages,
6262
- system,
6263
- tools2
6264
- ).then((text) => {
6265
- if (text) {
6266
- summaries.push({ name: "conversation", text });
6267
- }
6268
- })
6269
- );
6270
- }
6271
- for (const name of SUMMARIZABLE_SUBAGENTS) {
6272
- const subagentMessages = getSubAgentMessagesForSummary(
6273
- state.messages,
6274
- name,
6275
- insertionIndex
6276
- );
6277
- if (subagentMessages.length > 0) {
6278
- tasks.push(
6279
- generateSummary(
6280
- apiConfig,
6281
- name,
6282
- SUBAGENT_SUMMARY_PROMPT,
6283
- subagentMessages,
6284
- system,
6285
- tools2
6286
- ).then((text) => {
6287
- if (text) {
6288
- summaries.push({ name, text });
6289
- }
6290
- })
6291
- );
6292
- }
6293
- }
6294
- await Promise.all(tasks);
6295
- const checkpointMessages = summaries.map((s) => ({
6296
- role: "user",
6297
- hidden: true,
6298
- content: [
6299
- {
6300
- type: "summary",
6301
- name: s.name,
6302
- text: s.text,
6303
- startedAt: Date.now()
6304
- }
6305
- ]
6306
- }));
6307
- if (checkpointMessages.length > 0) {
6308
- state.messages.splice(insertionIndex, 0, ...checkpointMessages);
6309
- }
6310
- log8.info("Compaction complete", {
6311
- summaries: summaries.length,
6312
- insertionIndex,
6313
- messagesAfter: state.messages.length - insertionIndex - checkpointMessages.length
6314
- });
6315
- }
6316
- function findSafeInsertionPoint(messages) {
6317
- let idx = messages.length;
6318
- while (idx > 0) {
6319
- const msg = messages[idx - 1];
6320
- if (msg.role === "user" && msg.toolCallId) {
6321
- idx--;
6322
- } else {
6323
- break;
6324
- }
6325
- }
6326
- if (idx < messages.length && idx > 0) {
6327
- const msg = messages[idx - 1];
6328
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
6329
- const hasToolUse = msg.content.some(
6330
- (b) => b.type === "tool"
6331
- );
6332
- if (hasToolUse) {
6333
- idx--;
6334
- }
6335
- }
6336
- }
6337
- return idx;
6338
- }
6339
- function getConversationMessagesForSummary(messages, endIndex) {
6340
- let startIdx = 0;
6341
- for (let i = endIndex - 1; i >= 0; i--) {
6342
- const msg = messages[i];
6343
- if (!Array.isArray(msg.content)) {
6344
- continue;
6345
- }
6346
- for (const block of msg.content) {
6347
- if (block.type === "summary" && block.name === "conversation") {
6348
- startIdx = i + 1;
6349
- break;
6350
- }
6351
- }
6352
- if (startIdx > 0) {
6353
- break;
6354
- }
6355
- }
6356
- return messages.slice(startIdx, endIndex);
6357
- }
6358
- function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
6359
- let checkpointIdx = -1;
6360
- for (let i = endIndex - 1; i >= 0; i--) {
6361
- const msg = messages[i];
6362
- if (!Array.isArray(msg.content)) {
6363
- continue;
6364
- }
6365
- for (const block of msg.content) {
6366
- if (block.type === "summary" && block.name === subAgentName) {
6367
- checkpointIdx = i;
6368
- break;
6369
- }
6370
- }
6371
- if (checkpointIdx !== -1) {
6372
- break;
6373
- }
6374
- }
6375
- const startIdx = checkpointIdx !== -1 ? checkpointIdx + 1 : 0;
6376
- const collected = [];
6377
- for (let i = startIdx; i < endIndex; i++) {
6378
- const msg = messages[i];
6379
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
6380
- continue;
6381
- }
6382
- for (const block of msg.content) {
6383
- if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
6384
- collected.push(...block.subAgentMessages);
6385
- }
6386
- }
6387
- }
6388
- return collected;
6389
- }
6390
- function serializeForSummary(messages) {
6391
- return messages.map((msg) => {
6392
- if (typeof msg.content === "string") {
6393
- return `[${msg.role}]: ${msg.content}`;
6394
- }
6395
- if (!Array.isArray(msg.content)) {
6396
- return `[${msg.role}]: (empty)`;
6397
- }
6398
- const blocks = msg.content;
6399
- const parts = [];
6400
- for (const block of blocks) {
6401
- if (block.type === "text") {
6402
- parts.push(block.text);
6403
- } else if (block.type === "tool") {
6404
- parts.push(
6405
- `[tool: ${block.name}(${JSON.stringify(block.input).slice(0, 200)})] \u2192 ${(block.result ?? "").slice(0, 500)}`
6406
- );
6407
- }
6408
- }
6409
- return `[${msg.role}]: ${parts.join("\n")}`;
6410
- }).join("\n\n");
6411
- }
6412
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools) {
6413
- const serialized = serializeForSummary(messagesToSummarize);
6414
- if (!serialized.trim()) {
6415
- return null;
6416
- }
6417
- log8.info("Generating summary", {
6418
- name,
6419
- messageCount: messagesToSummarize.length,
6420
- cacheReuse: !!mainSystem
6421
- });
6422
- let summaryText = "";
6423
- const useMainCache = !!mainSystem;
6424
- const system = useMainCache ? mainSystem : compactionPrompt;
6425
- const tools2 = useMainCache ? mainTools ?? [] : [];
6426
- const userContent = useMainCache ? `${compactionPrompt}
6427
-
6428
- ---
6429
-
6430
- Conversation to summarize:
6431
-
6432
- ${serialized}` : serialized;
6433
- for await (const event of streamChat({
6434
- ...apiConfig,
6435
- subAgentId: "conversationSummarizer",
6436
- system,
6437
- messages: [{ role: "user", content: userContent }],
6438
- tools: tools2
6439
- })) {
6440
- if (event.type === "text") {
6441
- summaryText += event.text;
6442
- } else if (event.type === "error") {
6443
- log8.error("Summary generation failed", { name, error: event.error });
6444
- return null;
6445
- }
6446
- }
6447
- if (!summaryText.trim()) {
6448
- log8.warn("Empty summary generated", { name });
6449
- return null;
6450
- }
6451
- log8.info("Summary generated", { name, summaryLength: summaryText.length });
6452
- return summaryText.trim();
6453
- }
6454
- var log8, CONVERSATION_SUMMARY_PROMPT, SUBAGENT_SUMMARY_PROMPT, SUMMARIZABLE_SUBAGENTS;
6455
- var init_compaction = __esm({
6456
- "src/compaction/index.ts"() {
6457
- "use strict";
6458
- init_api();
6459
- init_assets();
6460
- init_logger();
6461
- log8 = createLogger("compaction");
6462
- CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
6463
- SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
6464
- SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
6465
- }
6466
- });
6467
-
6468
6543
  // src/toolRegistry.ts
6469
- var log9, ToolRegistry;
6544
+ var log10, ToolRegistry;
6470
6545
  var init_toolRegistry = __esm({
6471
6546
  "src/toolRegistry.ts"() {
6472
6547
  "use strict";
6473
6548
  init_logger();
6474
- log9 = createLogger("tool-registry");
6549
+ log10 = createLogger("tool-registry");
6475
6550
  ToolRegistry = class {
6476
6551
  entries = /* @__PURE__ */ new Map();
6477
6552
  onEvent;
@@ -6497,7 +6572,7 @@ var init_toolRegistry = __esm({
6497
6572
  if (!entry) {
6498
6573
  return false;
6499
6574
  }
6500
- log9.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6575
+ log10.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6501
6576
  entry.abortController.abort(mode);
6502
6577
  if (mode === "graceful") {
6503
6578
  const partial = entry.getPartialResult?.() ?? "";
@@ -6530,7 +6605,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6530
6605
  if (!entry) {
6531
6606
  return false;
6532
6607
  }
6533
- log9.info("Tool restarted", { toolCallId: id, name: entry.name });
6608
+ log10.info("Tool restarted", { toolCallId: id, name: entry.name });
6534
6609
  entry.abortController.abort("restart");
6535
6610
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6536
6611
  this.onEvent?.({
@@ -6705,7 +6780,7 @@ ${xmlParts}
6705
6780
  }
6706
6781
  function onBackgroundComplete(toolCallId, name, result, subAgentMessages) {
6707
6782
  pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6708
- log10.info("Background complete", {
6783
+ log11.info("Background complete", {
6709
6784
  toolCallId,
6710
6785
  name,
6711
6786
  requestId: currentRequestId
@@ -6977,7 +7052,7 @@ ${xmlParts}
6977
7052
  requestId
6978
7053
  );
6979
7054
  }
6980
- log10.info("Turn complete", {
7055
+ log11.info("Turn complete", {
6981
7056
  requestId,
6982
7057
  durationMs: Date.now() - turnStart
6983
7058
  });
@@ -6986,7 +7061,7 @@ ${xmlParts}
6986
7061
  emit("error", { error: err.message }, requestId);
6987
7062
  emit("completed", { success: false, error: err.message }, requestId);
6988
7063
  }
6989
- log10.warn("Command failed", {
7064
+ log11.warn("Command failed", {
6990
7065
  action: "message",
6991
7066
  requestId,
6992
7067
  error: err.message
@@ -7006,7 +7081,7 @@ ${xmlParts}
7006
7081
  return;
7007
7082
  }
7008
7083
  const { action, requestId } = parsed;
7009
- log10.info("Command received", { action, requestId });
7084
+ log11.info("Command received", { action, requestId });
7010
7085
  if (action === "tool_result" && parsed.id) {
7011
7086
  const id = parsed.id;
7012
7087
  const result = parsed.result ?? "";
@@ -7015,7 +7090,7 @@ ${xmlParts}
7015
7090
  pendingTools.delete(id);
7016
7091
  pending.resolve(result);
7017
7092
  } else if (!running) {
7018
- log10.info("Late tool_result while idle, dismissing", { id });
7093
+ log11.info("Late tool_result while idle, dismissing", { id });
7019
7094
  emit("completed", { success: true }, requestId);
7020
7095
  } else {
7021
7096
  earlyResults.set(id, result);
@@ -7071,36 +7146,31 @@ ${xmlParts}
7071
7146
  return;
7072
7147
  }
7073
7148
  if (action === "compact") {
7074
- sessionStats.compactionInProgress = true;
7075
- sessionStats.updatedAt = Date.now();
7076
- try {
7077
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
7078
- } catch {
7079
- }
7080
- const compactSystem = buildSystemPrompt("onboardingFinished");
7081
- const compactTools = getToolDefinitions("onboardingFinished");
7082
- compactConversation(state, config, compactSystem, compactTools).then(() => {
7083
- saveSession(state);
7084
- emit("compaction_complete", {}, requestId);
7085
- emit("completed", { success: true }, requestId);
7086
- }).catch((err) => {
7087
- emit(
7088
- "compaction_complete",
7089
- { error: err.message || "Compaction failed" },
7090
- requestId
7091
- );
7092
- emit(
7093
- "completed",
7094
- { success: false, error: err.message || "Compaction failed" },
7095
- requestId
7096
- );
7097
- }).finally(() => {
7098
- sessionStats.compactionInProgress = false;
7099
- sessionStats.messageCount = state.messages.length;
7100
- sessionStats.updatedAt = Date.now();
7101
- try {
7102
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
7103
- } catch {
7149
+ triggerCompaction(state, config, {
7150
+ onStart: () => {
7151
+ sessionStats.compactionInProgress = true;
7152
+ sessionStats.updatedAt = Date.now();
7153
+ try {
7154
+ writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
7155
+ } catch {
7156
+ }
7157
+ },
7158
+ onComplete: () => {
7159
+ emit("compaction_complete", {}, requestId);
7160
+ emit("completed", { success: true }, requestId);
7161
+ },
7162
+ onError: (error) => {
7163
+ emit("compaction_complete", { error }, requestId);
7164
+ emit("completed", { success: false, error }, requestId);
7165
+ },
7166
+ onFinally: () => {
7167
+ sessionStats.compactionInProgress = false;
7168
+ sessionStats.messageCount = state.messages.length;
7169
+ sessionStats.updatedAt = Date.now();
7170
+ try {
7171
+ writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
7172
+ } catch {
7173
+ }
7104
7174
  }
7105
7175
  });
7106
7176
  return;
@@ -7130,21 +7200,20 @@ ${xmlParts}
7130
7200
  process.on("SIGINT", shutdown);
7131
7201
  emit("ready");
7132
7202
  }
7133
- var log10;
7203
+ var log11;
7134
7204
  var init_headless = __esm({
7135
7205
  "src/headless.ts"() {
7136
7206
  "use strict";
7137
7207
  init_logger();
7138
7208
  init_config();
7139
- init_prompt4();
7140
- init_compaction();
7141
- init_tools6();
7209
+ init_prompt();
7210
+ init_trigger();
7142
7211
  init_lsp();
7143
7212
  init_agent();
7144
7213
  init_session();
7145
7214
  init_toolRegistry();
7146
7215
  init_resolve();
7147
- log10 = createLogger("headless");
7216
+ log11 = createLogger("headless");
7148
7217
  }
7149
7218
  });
7150
7219
 
@@ -7287,7 +7356,7 @@ function MessageList({ turns }) {
7287
7356
 
7288
7357
  // src/tui/App.tsx
7289
7358
  init_agent();
7290
- init_prompt4();
7359
+ init_prompt();
7291
7360
  init_session();
7292
7361
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
7293
7362
  function App({ apiConfig, model }) {