@cortexkit/aft-pi 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1163 -45
- package/dist/tools/ast.d.ts +27 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/conflicts.d.ts +9 -0
- package/dist/tools/conflicts.d.ts.map +1 -1
- package/dist/tools/diff-format.d.ts +24 -0
- package/dist/tools/diff-format.d.ts.map +1 -0
- package/dist/tools/fs.d.ts +15 -1
- package/dist/tools/fs.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts +37 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/imports.d.ts +21 -1
- package/dist/tools/imports.d.ts.map +1 -1
- package/dist/tools/lsp.d.ts +16 -1
- package/dist/tools/lsp.d.ts.map +1 -1
- package/dist/tools/navigate.d.ts +17 -1
- package/dist/tools/navigate.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +27 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactor.d.ts +22 -1
- package/dist/tools/refactor.d.ts.map +1 -1
- package/dist/tools/render-helpers.d.ts +36 -0
- package/dist/tools/render-helpers.d.ts.map +1 -0
- package/dist/tools/safety.d.ts +16 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts +14 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/tools/structure.d.ts +26 -1
- package/dist/tools/structure.d.ts.map +1 -1
- package/package.json +12 -9
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { createRequire as createRequire3 } from "node:module";
|
|
6
|
-
import { homedir as
|
|
6
|
+
import { homedir as homedir7 } from "node:os";
|
|
7
7
|
import { join as join8 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// src/shared/status.ts
|
|
@@ -1342,6 +1342,197 @@ function registerShutdownCleanup(fn) {
|
|
|
1342
1342
|
// src/tools/ast.ts
|
|
1343
1343
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
1344
1344
|
import { Type } from "@sinclair/typebox";
|
|
1345
|
+
|
|
1346
|
+
// src/tools/render-helpers.ts
|
|
1347
|
+
import { homedir as homedir5 } from "node:os";
|
|
1348
|
+
import { renderDiff } from "@mariozechner/pi-coding-agent";
|
|
1349
|
+
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
1350
|
+
function reuseText(last) {
|
|
1351
|
+
return last instanceof Text ? last : new Text("", 0, 0);
|
|
1352
|
+
}
|
|
1353
|
+
function reuseContainer(last) {
|
|
1354
|
+
return last instanceof Container ? last : new Container;
|
|
1355
|
+
}
|
|
1356
|
+
function shortenPath(path2) {
|
|
1357
|
+
const home = homedir5();
|
|
1358
|
+
if (path2.startsWith(home))
|
|
1359
|
+
return `~${path2.slice(home.length)}`;
|
|
1360
|
+
return path2;
|
|
1361
|
+
}
|
|
1362
|
+
function renderToolCall(toolName, summary, theme, context) {
|
|
1363
|
+
const text = reuseText(context.lastComponent);
|
|
1364
|
+
const suffix = summary ? ` ${summary}` : "";
|
|
1365
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
|
|
1366
|
+
return text;
|
|
1367
|
+
}
|
|
1368
|
+
function accentPath(theme, path2) {
|
|
1369
|
+
if (!path2)
|
|
1370
|
+
return theme.fg("toolOutput", "...");
|
|
1371
|
+
return theme.fg("accent", shortenPath(path2));
|
|
1372
|
+
}
|
|
1373
|
+
function collectTextContent(result) {
|
|
1374
|
+
return result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
|
|
1375
|
+
`).trim();
|
|
1376
|
+
}
|
|
1377
|
+
function extractStructuredPayload(result) {
|
|
1378
|
+
if (result.details !== undefined)
|
|
1379
|
+
return result.details;
|
|
1380
|
+
const text = collectTextContent(result);
|
|
1381
|
+
if (!text)
|
|
1382
|
+
return;
|
|
1383
|
+
try {
|
|
1384
|
+
return JSON.parse(text);
|
|
1385
|
+
} catch {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function renderErrorResult(result, fallback, theme, context) {
|
|
1390
|
+
const text = reuseText(context.lastComponent);
|
|
1391
|
+
const message = collectTextContent(result) || fallback;
|
|
1392
|
+
text.setText(`
|
|
1393
|
+
${theme.fg("error", message)}`);
|
|
1394
|
+
return text;
|
|
1395
|
+
}
|
|
1396
|
+
function renderSections(sections, context) {
|
|
1397
|
+
const container = reuseContainer(context.lastComponent);
|
|
1398
|
+
container.clear();
|
|
1399
|
+
const visibleSections = sections.filter((section) => section.trim().length > 0);
|
|
1400
|
+
if (visibleSections.length === 0)
|
|
1401
|
+
return container;
|
|
1402
|
+
container.addChild(new Spacer(1));
|
|
1403
|
+
visibleSections.forEach((section, index) => {
|
|
1404
|
+
if (index > 0)
|
|
1405
|
+
container.addChild(new Spacer(1));
|
|
1406
|
+
container.addChild(new Text(section, 0, 0));
|
|
1407
|
+
});
|
|
1408
|
+
return container;
|
|
1409
|
+
}
|
|
1410
|
+
function asRecord2(value) {
|
|
1411
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1412
|
+
return;
|
|
1413
|
+
return value;
|
|
1414
|
+
}
|
|
1415
|
+
function asRecords(value) {
|
|
1416
|
+
return Array.isArray(value) ? value.map(asRecord2).filter(Boolean) : [];
|
|
1417
|
+
}
|
|
1418
|
+
function asString(value) {
|
|
1419
|
+
return typeof value === "string" ? value : undefined;
|
|
1420
|
+
}
|
|
1421
|
+
function asNumber(value) {
|
|
1422
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1423
|
+
}
|
|
1424
|
+
function asBoolean(value) {
|
|
1425
|
+
return typeof value === "boolean" ? value : undefined;
|
|
1426
|
+
}
|
|
1427
|
+
function formatValue(value) {
|
|
1428
|
+
if (Array.isArray(value))
|
|
1429
|
+
return value.map(formatValue).join(", ");
|
|
1430
|
+
if (typeof value === "string")
|
|
1431
|
+
return value;
|
|
1432
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
1433
|
+
return String(value);
|
|
1434
|
+
if (value === null || value === undefined)
|
|
1435
|
+
return "—";
|
|
1436
|
+
try {
|
|
1437
|
+
return JSON.stringify(value);
|
|
1438
|
+
} catch {
|
|
1439
|
+
return String(value);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
function groupByFile(items, getFile) {
|
|
1443
|
+
const groups = new Map;
|
|
1444
|
+
items.forEach((item) => {
|
|
1445
|
+
const file = getFile(item) ?? "(unknown file)";
|
|
1446
|
+
const current = groups.get(file) ?? [];
|
|
1447
|
+
current.push(item);
|
|
1448
|
+
groups.set(file, current);
|
|
1449
|
+
});
|
|
1450
|
+
return groups;
|
|
1451
|
+
}
|
|
1452
|
+
function distinctCount(values) {
|
|
1453
|
+
return new Set(values.filter((value) => Boolean(value))).size;
|
|
1454
|
+
}
|
|
1455
|
+
function severityBadge(theme, severity) {
|
|
1456
|
+
const label = severity === "information" ? "info" : severity;
|
|
1457
|
+
switch (severity) {
|
|
1458
|
+
case "error":
|
|
1459
|
+
return theme.fg("error", `[${label}]`);
|
|
1460
|
+
case "warning":
|
|
1461
|
+
return theme.fg("warning", `[${label}]`);
|
|
1462
|
+
case "information":
|
|
1463
|
+
return theme.fg("accent", `[${label}]`);
|
|
1464
|
+
case "hint":
|
|
1465
|
+
return theme.fg("muted", `[${label}]`);
|
|
1466
|
+
default:
|
|
1467
|
+
return theme.fg("muted", `[${label}]`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function formatTimestamp(value) {
|
|
1471
|
+
if (typeof value === "string" && value.length > 0)
|
|
1472
|
+
return value;
|
|
1473
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1474
|
+
return;
|
|
1475
|
+
const millis = value > 1000000000000 ? value : value * 1000;
|
|
1476
|
+
const date = new Date(millis);
|
|
1477
|
+
if (Number.isNaN(date.getTime()))
|
|
1478
|
+
return String(value);
|
|
1479
|
+
return date.toISOString().replace("T", " ").replace(".000Z", "Z");
|
|
1480
|
+
}
|
|
1481
|
+
function formatUnifiedDiffForPi(unifiedDiff) {
|
|
1482
|
+
if (!unifiedDiff.trim())
|
|
1483
|
+
return "";
|
|
1484
|
+
const entries = [];
|
|
1485
|
+
const hunkHeader = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
1486
|
+
let oldLine = 1;
|
|
1487
|
+
let newLine = 1;
|
|
1488
|
+
for (const line of unifiedDiff.split(`
|
|
1489
|
+
`)) {
|
|
1490
|
+
if (!line)
|
|
1491
|
+
continue;
|
|
1492
|
+
if (line.startsWith("--- ") || line.startsWith("+++ "))
|
|
1493
|
+
continue;
|
|
1494
|
+
if (line.startsWith("\"))
|
|
1495
|
+
continue;
|
|
1496
|
+
const headerMatch = line.match(hunkHeader);
|
|
1497
|
+
if (headerMatch) {
|
|
1498
|
+
oldLine = Number(headerMatch[1]);
|
|
1499
|
+
newLine = Number(headerMatch[2]);
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1503
|
+
entries.push({ prefix: "+", line: newLine, text: line.slice(1) });
|
|
1504
|
+
newLine += 1;
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1508
|
+
entries.push({ prefix: "-", line: oldLine, text: line.slice(1) });
|
|
1509
|
+
oldLine += 1;
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (line.startsWith(" ")) {
|
|
1513
|
+
entries.push({ prefix: " ", line: oldLine, text: line.slice(1) });
|
|
1514
|
+
oldLine += 1;
|
|
1515
|
+
newLine += 1;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (entries.length === 0)
|
|
1519
|
+
return "";
|
|
1520
|
+
const width = String(entries.reduce((max, entry) => Math.max(max, entry.line), 1)).length;
|
|
1521
|
+
return entries.map((entry) => `${entry.prefix}${String(entry.line).padStart(width, " ")} ${entry.text}`).join(`
|
|
1522
|
+
`);
|
|
1523
|
+
}
|
|
1524
|
+
function renderUnifiedDiff(unifiedDiff) {
|
|
1525
|
+
const piDiff = formatUnifiedDiffForPi(unifiedDiff);
|
|
1526
|
+
if (!piDiff)
|
|
1527
|
+
return "";
|
|
1528
|
+
try {
|
|
1529
|
+
return renderDiff(piDiff);
|
|
1530
|
+
} catch {
|
|
1531
|
+
return piDiff;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// src/tools/ast.ts
|
|
1345
1536
|
var AstLang = StringEnum(["typescript", "tsx", "javascript", "python", "rust", "go"], {
|
|
1346
1537
|
description: "Target language"
|
|
1347
1538
|
});
|
|
@@ -1362,6 +1553,107 @@ var ReplaceParams = Type.Object({
|
|
|
1362
1553
|
globs: Type.Optional(Type.Array(Type.String(), { description: "Include/exclude globs" })),
|
|
1363
1554
|
dryRun: Type.Optional(Type.Boolean({ description: "Preview without applying (default: false)" }))
|
|
1364
1555
|
});
|
|
1556
|
+
function buildAstSearchSections(payload, theme) {
|
|
1557
|
+
const response = asRecord2(payload);
|
|
1558
|
+
if (!response)
|
|
1559
|
+
return [theme.fg("muted", "No AST search results.")];
|
|
1560
|
+
const matches = asRecords(response.matches);
|
|
1561
|
+
const totalMatches = asNumber(response.total_matches) ?? matches.length;
|
|
1562
|
+
const filesWithMatches = asNumber(response.files_with_matches) ?? groupByFile(matches, (match) => asString(match.file)).size;
|
|
1563
|
+
const filesSearched = asNumber(response.files_searched);
|
|
1564
|
+
const header = [
|
|
1565
|
+
theme.fg("success", `${totalMatches} match${totalMatches === 1 ? "" : "es"}`),
|
|
1566
|
+
theme.fg("accent", `${filesWithMatches} file${filesWithMatches === 1 ? "" : "s"}`),
|
|
1567
|
+
filesSearched !== undefined ? theme.fg("muted", `${filesSearched} searched`) : undefined
|
|
1568
|
+
].filter(Boolean).join(" · ");
|
|
1569
|
+
if (matches.length === 0)
|
|
1570
|
+
return [header, theme.fg("muted", "No AST matches found.")];
|
|
1571
|
+
const grouped = groupByFile(matches, (match) => asString(match.file));
|
|
1572
|
+
const sections = [header];
|
|
1573
|
+
for (const [file, fileMatches] of grouped.entries()) {
|
|
1574
|
+
const lines = [theme.fg("accent", shortenPath(file))];
|
|
1575
|
+
fileMatches.forEach((match, index) => {
|
|
1576
|
+
const line = asNumber(match.line) ?? 0;
|
|
1577
|
+
const column = asNumber(match.column) ?? 0;
|
|
1578
|
+
const snippet = asString(match.text)?.trim() || "(empty match)";
|
|
1579
|
+
lines.push(` ${index + 1}. ${theme.fg("muted", `${line}:${column}`)} ${snippet}`);
|
|
1580
|
+
const metaVars = asRecord2(match.meta_variables);
|
|
1581
|
+
if (metaVars && Object.keys(metaVars).length > 0) {
|
|
1582
|
+
Object.entries(metaVars).forEach(([name, value]) => {
|
|
1583
|
+
lines.push(` ${theme.fg("muted", `${name} =`)} ${formatValue(value)}`);
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
const context = asRecords(match.context);
|
|
1587
|
+
context.forEach((ctxLine) => {
|
|
1588
|
+
const ctxNumber = asNumber(ctxLine.line) ?? 0;
|
|
1589
|
+
const prefix = ctxLine.is_match === true ? theme.fg("accent", ">") : theme.fg("muted", "|");
|
|
1590
|
+
lines.push(` ${prefix} ${ctxNumber}: ${asString(ctxLine.text) ?? ""}`);
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
sections.push(lines.join(`
|
|
1594
|
+
`));
|
|
1595
|
+
}
|
|
1596
|
+
return sections;
|
|
1597
|
+
}
|
|
1598
|
+
function buildAstReplaceSections(payload, theme) {
|
|
1599
|
+
const response = asRecord2(payload);
|
|
1600
|
+
if (!response)
|
|
1601
|
+
return [theme.fg("muted", "No AST replace results.")];
|
|
1602
|
+
const files = asRecords(response.files);
|
|
1603
|
+
const totalReplacements = asNumber(response.total_replacements) ?? 0;
|
|
1604
|
+
const totalFiles = asNumber(response.total_files) ?? files.length;
|
|
1605
|
+
const filesWithMatches = asNumber(response.files_with_matches);
|
|
1606
|
+
const dryRun = response.dry_run === true;
|
|
1607
|
+
const headerParts = [
|
|
1608
|
+
dryRun ? theme.fg("warning", "[dry run]") : theme.fg("success", "[applied]"),
|
|
1609
|
+
`${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}`,
|
|
1610
|
+
`${totalFiles} file${totalFiles === 1 ? "" : "s"}`,
|
|
1611
|
+
filesWithMatches !== undefined ? theme.fg("muted", `${filesWithMatches} matched`) : undefined
|
|
1612
|
+
];
|
|
1613
|
+
const sections = [headerParts.filter(Boolean).join(" ")];
|
|
1614
|
+
if (files.length === 0) {
|
|
1615
|
+
sections.push(theme.fg("muted", "No files changed."));
|
|
1616
|
+
return sections;
|
|
1617
|
+
}
|
|
1618
|
+
files.forEach((fileResult) => {
|
|
1619
|
+
const file = shortenPath(asString(fileResult.file) ?? "(unknown file)");
|
|
1620
|
+
const replacements = asNumber(fileResult.replacements) ?? 0;
|
|
1621
|
+
const error2 = asString(fileResult.error);
|
|
1622
|
+
const diff = asString(fileResult.diff);
|
|
1623
|
+
const lines = [
|
|
1624
|
+
`${theme.fg("accent", file)} ${theme.fg("muted", `(${replacements} replacement${replacements === 1 ? "" : "s"})`)}`
|
|
1625
|
+
];
|
|
1626
|
+
if (error2) {
|
|
1627
|
+
lines.push(theme.fg("error", error2));
|
|
1628
|
+
} else if (diff) {
|
|
1629
|
+
const rendered = renderUnifiedDiff(diff);
|
|
1630
|
+
lines.push(rendered || theme.fg("muted", "No diff available."));
|
|
1631
|
+
} else {
|
|
1632
|
+
const backupId = asString(fileResult.backup_id);
|
|
1633
|
+
lines.push(backupId ? `${theme.fg("success", "saved")} ${theme.fg("muted", backupId)}` : theme.fg("success", "saved"));
|
|
1634
|
+
}
|
|
1635
|
+
sections.push(lines.join(`
|
|
1636
|
+
`));
|
|
1637
|
+
});
|
|
1638
|
+
return sections;
|
|
1639
|
+
}
|
|
1640
|
+
function renderAstCall(toolName, args, theme, context) {
|
|
1641
|
+
const lang = theme.fg("accent", args.lang);
|
|
1642
|
+
const summary = toolName === "ast_grep_replace" ? `${lang} ${theme.fg("toolOutput", `${args.pattern} → ${args.rewrite}`)}` : `${lang} ${theme.fg("toolOutput", args.pattern)}`;
|
|
1643
|
+
return renderToolCall(toolName === "ast_grep_replace" ? "ast replace" : "ast search", summary, theme, context);
|
|
1644
|
+
}
|
|
1645
|
+
function renderAstResult(toolName, result, theme, context) {
|
|
1646
|
+
if (context.isError) {
|
|
1647
|
+
return renderErrorResult(result, `${toolName} failed`, theme, context);
|
|
1648
|
+
}
|
|
1649
|
+
const payload = extractStructuredPayload(result);
|
|
1650
|
+
if (!payload) {
|
|
1651
|
+
const text = collectTextContent(result);
|
|
1652
|
+
return renderSections([text || theme.fg("muted", "No result.")], context);
|
|
1653
|
+
}
|
|
1654
|
+
const sections = toolName === "ast_grep_replace" ? buildAstReplaceSections(payload, theme) : buildAstSearchSections(payload, theme);
|
|
1655
|
+
return renderSections(sections, context);
|
|
1656
|
+
}
|
|
1365
1657
|
function registerAstTools(pi, ctx, surface) {
|
|
1366
1658
|
if (surface.astSearch) {
|
|
1367
1659
|
pi.registerTool({
|
|
@@ -1383,6 +1675,12 @@ function registerAstTools(pi, ctx, surface) {
|
|
|
1383
1675
|
req.context_lines = params.contextLines;
|
|
1384
1676
|
const response = await callBridge(bridge, "ast_search", req);
|
|
1385
1677
|
return textResult(response.text ?? JSON.stringify(response));
|
|
1678
|
+
},
|
|
1679
|
+
renderCall(args, theme, context) {
|
|
1680
|
+
return renderAstCall("ast_grep_search", args, theme, context);
|
|
1681
|
+
},
|
|
1682
|
+
renderResult(result, _options, theme, context) {
|
|
1683
|
+
return renderAstResult("ast_grep_search", result, theme, context);
|
|
1386
1684
|
}
|
|
1387
1685
|
});
|
|
1388
1686
|
}
|
|
@@ -1406,6 +1704,12 @@ function registerAstTools(pi, ctx, surface) {
|
|
|
1406
1704
|
req.dry_run = params.dryRun === true;
|
|
1407
1705
|
const response = await callBridge(bridge, "ast_replace", req);
|
|
1408
1706
|
return textResult(response.text ?? JSON.stringify(response));
|
|
1707
|
+
},
|
|
1708
|
+
renderCall(args, theme, context) {
|
|
1709
|
+
return renderAstCall("ast_grep_replace", args, theme, context);
|
|
1710
|
+
},
|
|
1711
|
+
renderResult(result, _options, theme, context) {
|
|
1712
|
+
return renderAstResult("ast_grep_replace", result, theme, context);
|
|
1409
1713
|
}
|
|
1410
1714
|
});
|
|
1411
1715
|
}
|
|
@@ -1414,6 +1718,32 @@ function registerAstTools(pi, ctx, surface) {
|
|
|
1414
1718
|
// src/tools/conflicts.ts
|
|
1415
1719
|
import { Type as Type2 } from "@sinclair/typebox";
|
|
1416
1720
|
var ConflictsParams = Type2.Object({});
|
|
1721
|
+
function renderConflictCall(theme, context) {
|
|
1722
|
+
return renderToolCall("conflicts", undefined, theme, context);
|
|
1723
|
+
}
|
|
1724
|
+
function buildConflictSections(text) {
|
|
1725
|
+
const trimmed = text.trim();
|
|
1726
|
+
if (!trimmed)
|
|
1727
|
+
return ["No merge conflicts found."];
|
|
1728
|
+
const [header, ...rest] = trimmed.split(/\n\n+/);
|
|
1729
|
+
const match = header.match(/^(\d+)\s+files?,\s+(\d+)\s+conflicts?/i);
|
|
1730
|
+
const sections = [
|
|
1731
|
+
match ? `${match[1]} conflicted file${match[1] === "1" ? "" : "s"} · ${match[2]} region${match[2] === "1" ? "" : "s"}` : header
|
|
1732
|
+
];
|
|
1733
|
+
if (rest.length === 0)
|
|
1734
|
+
return sections;
|
|
1735
|
+
sections.push(...rest.map((section) => section.trim()).filter(Boolean));
|
|
1736
|
+
return sections;
|
|
1737
|
+
}
|
|
1738
|
+
function renderConflictResult(text, theme, context) {
|
|
1739
|
+
const sections = buildConflictSections(text).map((section, index) => index === 0 ? theme.fg("warning", section) : section);
|
|
1740
|
+
return renderSections(sections, context);
|
|
1741
|
+
}
|
|
1742
|
+
function renderConflictToolResult(result, theme, context) {
|
|
1743
|
+
if (context.isError)
|
|
1744
|
+
return renderErrorResult(result, "conflicts failed", theme, context);
|
|
1745
|
+
return renderConflictResult(collectTextContent(result), theme, context);
|
|
1746
|
+
}
|
|
1417
1747
|
function registerConflictsTool(pi, ctx) {
|
|
1418
1748
|
pi.registerTool({
|
|
1419
1749
|
name: "aft_conflicts",
|
|
@@ -1424,6 +1754,12 @@ function registerConflictsTool(pi, ctx) {
|
|
|
1424
1754
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
1425
1755
|
const response = await callBridge(bridge, "git_conflicts");
|
|
1426
1756
|
return textResult(response.text ?? JSON.stringify(response, null, 2));
|
|
1757
|
+
},
|
|
1758
|
+
renderCall(_args, theme, context) {
|
|
1759
|
+
return renderConflictCall(theme, context);
|
|
1760
|
+
},
|
|
1761
|
+
renderResult(result, _options, theme, context) {
|
|
1762
|
+
return renderConflictToolResult(result, theme, context);
|
|
1427
1763
|
}
|
|
1428
1764
|
});
|
|
1429
1765
|
}
|
|
@@ -1437,6 +1773,27 @@ var MoveParams = Type3.Object({
|
|
|
1437
1773
|
filePath: Type3.String({ description: "Source file path to move" }),
|
|
1438
1774
|
destination: Type3.String({ description: "Destination file path" })
|
|
1439
1775
|
});
|
|
1776
|
+
function renderFsCall(toolName, args, theme, context) {
|
|
1777
|
+
if (toolName === "aft_delete") {
|
|
1778
|
+
return renderToolCall("delete", accentPath(theme, args.filePath), theme, context);
|
|
1779
|
+
}
|
|
1780
|
+
const moveArgs = args;
|
|
1781
|
+
return renderToolCall("move", `${accentPath(theme, moveArgs.filePath)} ${theme.fg("muted", "→")} ${accentPath(theme, moveArgs.destination)}`, theme, context);
|
|
1782
|
+
}
|
|
1783
|
+
function renderFsResult(toolName, args, result, theme, context) {
|
|
1784
|
+
if (context.isError) {
|
|
1785
|
+
return renderErrorResult(result, `${toolName} failed`, theme, context);
|
|
1786
|
+
}
|
|
1787
|
+
if (toolName === "aft_delete") {
|
|
1788
|
+
const filePath = shortenPath(args.filePath);
|
|
1789
|
+
return renderSections([`${theme.fg("success", "✓ deleted")} ${theme.fg("accent", filePath)}`], context);
|
|
1790
|
+
}
|
|
1791
|
+
const moveArgs = args;
|
|
1792
|
+
return renderSections([
|
|
1793
|
+
`${theme.fg("success", "✓ moved")} ${theme.fg("accent", shortenPath(moveArgs.filePath))}`,
|
|
1794
|
+
`${theme.fg("muted", "to")} ${theme.fg("accent", shortenPath(moveArgs.destination))}`
|
|
1795
|
+
], context);
|
|
1796
|
+
}
|
|
1440
1797
|
function registerFsTools(pi, ctx, surface) {
|
|
1441
1798
|
if (surface.delete) {
|
|
1442
1799
|
pi.registerTool({
|
|
@@ -1448,6 +1805,12 @@ function registerFsTools(pi, ctx, surface) {
|
|
|
1448
1805
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
1449
1806
|
const response = await callBridge(bridge, "delete_file", { file: params.filePath });
|
|
1450
1807
|
return textResult(`Deleted ${params.filePath}`, response);
|
|
1808
|
+
},
|
|
1809
|
+
renderCall(args, theme, context) {
|
|
1810
|
+
return renderFsCall("aft_delete", args, theme, context);
|
|
1811
|
+
},
|
|
1812
|
+
renderResult(result, _options, theme, context) {
|
|
1813
|
+
return renderFsResult("aft_delete", context.args, result, theme, context);
|
|
1451
1814
|
}
|
|
1452
1815
|
});
|
|
1453
1816
|
}
|
|
@@ -1464,6 +1827,12 @@ function registerFsTools(pi, ctx, surface) {
|
|
|
1464
1827
|
destination: params.destination
|
|
1465
1828
|
});
|
|
1466
1829
|
return textResult(`Moved ${params.filePath} → ${params.destination}`, response);
|
|
1830
|
+
},
|
|
1831
|
+
renderCall(args, theme, context) {
|
|
1832
|
+
return renderFsCall("aft_move", args, theme, context);
|
|
1833
|
+
},
|
|
1834
|
+
renderResult(result, _options, theme, context) {
|
|
1835
|
+
return renderFsResult("aft_move", context.args, result, theme, context);
|
|
1467
1836
|
}
|
|
1468
1837
|
});
|
|
1469
1838
|
}
|
|
@@ -1471,8 +1840,117 @@ function registerFsTools(pi, ctx, surface) {
|
|
|
1471
1840
|
|
|
1472
1841
|
// src/tools/hoisted.ts
|
|
1473
1842
|
import { stat } from "node:fs/promises";
|
|
1843
|
+
import { homedir as homedir6 } from "node:os";
|
|
1474
1844
|
import { resolve } from "node:path";
|
|
1845
|
+
import {
|
|
1846
|
+
renderDiff as renderDiff2
|
|
1847
|
+
} from "@mariozechner/pi-coding-agent";
|
|
1848
|
+
import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@mariozechner/pi-tui";
|
|
1475
1849
|
import { Type as Type4 } from "@sinclair/typebox";
|
|
1850
|
+
|
|
1851
|
+
// src/tools/diff-format.ts
|
|
1852
|
+
import { diffLines } from "diff";
|
|
1853
|
+
var DEFAULT_CONTEXT_LINES = 4;
|
|
1854
|
+
function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_LINES) {
|
|
1855
|
+
const parts = diffLines(oldContent, newContent);
|
|
1856
|
+
const output = [];
|
|
1857
|
+
const oldLines = oldContent.split(`
|
|
1858
|
+
`);
|
|
1859
|
+
const newLines = newContent.split(`
|
|
1860
|
+
`);
|
|
1861
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
1862
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
1863
|
+
const pad = (n) => String(n).padStart(lineNumWidth, " ");
|
|
1864
|
+
const blank = " ".repeat(lineNumWidth);
|
|
1865
|
+
let oldLineNum = 1;
|
|
1866
|
+
let newLineNum = 1;
|
|
1867
|
+
let lastWasChange = false;
|
|
1868
|
+
let firstChangedLine;
|
|
1869
|
+
for (let i = 0;i < parts.length; i++) {
|
|
1870
|
+
const part = parts[i];
|
|
1871
|
+
const raw = part.value.split(`
|
|
1872
|
+
`);
|
|
1873
|
+
if (raw[raw.length - 1] === "")
|
|
1874
|
+
raw.pop();
|
|
1875
|
+
if (part.added || part.removed) {
|
|
1876
|
+
if (firstChangedLine === undefined)
|
|
1877
|
+
firstChangedLine = newLineNum;
|
|
1878
|
+
for (const line of raw) {
|
|
1879
|
+
if (part.added) {
|
|
1880
|
+
output.push(`+${pad(newLineNum)} ${line}`);
|
|
1881
|
+
newLineNum++;
|
|
1882
|
+
} else {
|
|
1883
|
+
output.push(`-${pad(oldLineNum)} ${line}`);
|
|
1884
|
+
oldLineNum++;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
lastWasChange = true;
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
const nextIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
1891
|
+
const hasLeading = lastWasChange;
|
|
1892
|
+
const hasTrailing = nextIsChange;
|
|
1893
|
+
if (hasLeading && hasTrailing) {
|
|
1894
|
+
if (raw.length <= contextLines * 2) {
|
|
1895
|
+
for (const line of raw) {
|
|
1896
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
1897
|
+
oldLineNum++;
|
|
1898
|
+
newLineNum++;
|
|
1899
|
+
}
|
|
1900
|
+
} else {
|
|
1901
|
+
for (const line of raw.slice(0, contextLines)) {
|
|
1902
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
1903
|
+
oldLineNum++;
|
|
1904
|
+
newLineNum++;
|
|
1905
|
+
}
|
|
1906
|
+
const skipped = raw.length - contextLines * 2;
|
|
1907
|
+
output.push(` ${blank} ...`);
|
|
1908
|
+
oldLineNum += skipped;
|
|
1909
|
+
newLineNum += skipped;
|
|
1910
|
+
for (const line of raw.slice(raw.length - contextLines)) {
|
|
1911
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
1912
|
+
oldLineNum++;
|
|
1913
|
+
newLineNum++;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
} else if (hasLeading) {
|
|
1917
|
+
const shown = raw.slice(0, contextLines);
|
|
1918
|
+
for (const line of shown) {
|
|
1919
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
1920
|
+
oldLineNum++;
|
|
1921
|
+
newLineNum++;
|
|
1922
|
+
}
|
|
1923
|
+
const skipped = raw.length - shown.length;
|
|
1924
|
+
if (skipped > 0) {
|
|
1925
|
+
output.push(` ${blank} ...`);
|
|
1926
|
+
oldLineNum += skipped;
|
|
1927
|
+
newLineNum += skipped;
|
|
1928
|
+
}
|
|
1929
|
+
} else if (hasTrailing) {
|
|
1930
|
+
const shownCount = Math.min(contextLines, raw.length);
|
|
1931
|
+
const shown = raw.slice(raw.length - shownCount);
|
|
1932
|
+
const skipped = raw.length - shown.length;
|
|
1933
|
+
if (skipped > 0) {
|
|
1934
|
+
output.push(` ${blank} ...`);
|
|
1935
|
+
oldLineNum += skipped;
|
|
1936
|
+
newLineNum += skipped;
|
|
1937
|
+
}
|
|
1938
|
+
for (const line of shown) {
|
|
1939
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
1940
|
+
oldLineNum++;
|
|
1941
|
+
newLineNum++;
|
|
1942
|
+
}
|
|
1943
|
+
} else {
|
|
1944
|
+
oldLineNum += raw.length;
|
|
1945
|
+
newLineNum += raw.length;
|
|
1946
|
+
}
|
|
1947
|
+
lastWasChange = false;
|
|
1948
|
+
}
|
|
1949
|
+
return { diff: output.join(`
|
|
1950
|
+
`), firstChangedLine };
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/tools/hoisted.ts
|
|
1476
1954
|
var ReadParams = Type4.Object({
|
|
1477
1955
|
path: Type4.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
1478
1956
|
offset: Type4.Optional(Type4.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
@@ -1504,6 +1982,8 @@ function registerHoistedTools(pi, ctx, surface) {
|
|
|
1504
1982
|
name: "read",
|
|
1505
1983
|
label: "read",
|
|
1506
1984
|
description: "Read file contents with line numbers. Backed by AFT's indexed Rust reader — faster than the built-in `read` on large repos and correctly handles images/PDFs as attachments.",
|
|
1985
|
+
promptSnippet: "Read file contents (supports offset/limit for large files)",
|
|
1986
|
+
promptGuidelines: ["Use read to examine files instead of cat or sed."],
|
|
1507
1987
|
parameters: ReadParams,
|
|
1508
1988
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
1509
1989
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
@@ -1530,30 +2010,24 @@ function registerHoistedTools(pi, ctx, surface) {
|
|
|
1530
2010
|
pi.registerTool({
|
|
1531
2011
|
name: "write",
|
|
1532
2012
|
label: "write",
|
|
1533
|
-
description: "Write a file atomically with per-file backup, optional auto-format, and inline LSP diagnostics. Parent directories are created automatically. Overwrites existing files.",
|
|
2013
|
+
description: "Write a file atomically with per-file backup, optional auto-format, and inline LSP diagnostics. Parent directories are created automatically. Overwrites existing files. Uses `filePath` (not `path`).",
|
|
2014
|
+
promptSnippet: "Create or overwrite files (uses filePath; auto-formats; returns LSP diagnostics inline)",
|
|
2015
|
+
promptGuidelines: ["Use write only for new files or complete rewrites."],
|
|
1534
2016
|
parameters: WriteParams,
|
|
1535
2017
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
1536
2018
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
1537
2019
|
const response = await callBridge(bridge, "write", {
|
|
1538
2020
|
file: params.filePath,
|
|
1539
|
-
content: params.content
|
|
1540
|
-
|
|
1541
|
-
const diffAdd = response.diff?.additions ?? 0;
|
|
1542
|
-
const diffDel = response.diff?.deletions ?? 0;
|
|
1543
|
-
const diagnostics = response.lsp_diagnostics;
|
|
1544
|
-
let summary = `Wrote ${params.filePath} (+${diffAdd}/-${diffDel})`;
|
|
1545
|
-
if (diagnostics && diagnostics.length > 0) {
|
|
1546
|
-
summary += `
|
|
1547
|
-
|
|
1548
|
-
LSP diagnostics:
|
|
1549
|
-
${JSON.stringify(diagnostics, null, 2)}`;
|
|
1550
|
-
}
|
|
1551
|
-
return textResult(summary, {
|
|
1552
|
-
filePath: params.filePath,
|
|
1553
|
-
additions: diffAdd,
|
|
1554
|
-
deletions: diffDel,
|
|
1555
|
-
diagnostics
|
|
2021
|
+
content: params.content,
|
|
2022
|
+
include_diff: true
|
|
1556
2023
|
});
|
|
2024
|
+
return buildMutationResult(params.filePath, response);
|
|
2025
|
+
},
|
|
2026
|
+
renderCall(args, theme, context) {
|
|
2027
|
+
return renderMutationCall("write", args?.filePath, theme, context);
|
|
2028
|
+
},
|
|
2029
|
+
renderResult(result, _options, theme, context) {
|
|
2030
|
+
return renderMutationResult(result, theme, context);
|
|
1557
2031
|
}
|
|
1558
2032
|
});
|
|
1559
2033
|
}
|
|
@@ -1561,39 +2035,33 @@ ${JSON.stringify(diagnostics, null, 2)}`;
|
|
|
1561
2035
|
pi.registerTool({
|
|
1562
2036
|
name: "edit",
|
|
1563
2037
|
label: "edit",
|
|
1564
|
-
description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift).
|
|
2038
|
+
description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches — use `occurrence` to pick one, or `replaceAll: true`. Always returns LSP diagnostics inline.",
|
|
2039
|
+
promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching)",
|
|
2040
|
+
promptGuidelines: [
|
|
2041
|
+
"Prefer edit over write when changing part of an existing file.",
|
|
2042
|
+
"Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly."
|
|
2043
|
+
],
|
|
1565
2044
|
parameters: EditParams,
|
|
1566
2045
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
1567
2046
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
1568
2047
|
const req = {
|
|
1569
2048
|
file: params.filePath,
|
|
1570
2049
|
match: params.oldString ?? "",
|
|
1571
|
-
replacement: params.newString ?? ""
|
|
2050
|
+
replacement: params.newString ?? "",
|
|
2051
|
+
include_diff: true
|
|
1572
2052
|
};
|
|
1573
2053
|
if (params.replaceAll === true)
|
|
1574
2054
|
req.replace_all = true;
|
|
1575
2055
|
if (params.occurrence !== undefined)
|
|
1576
2056
|
req.occurrence = params.occurrence;
|
|
1577
2057
|
const response = await callBridge(bridge, "edit_match", req);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
summary += `
|
|
1586
|
-
|
|
1587
|
-
LSP diagnostics:
|
|
1588
|
-
${JSON.stringify(diagnostics, null, 2)}`;
|
|
1589
|
-
}
|
|
1590
|
-
return textResult(summary, {
|
|
1591
|
-
filePath: params.filePath,
|
|
1592
|
-
additions: diffAdd,
|
|
1593
|
-
deletions: diffDel,
|
|
1594
|
-
replacements,
|
|
1595
|
-
diagnostics
|
|
1596
|
-
});
|
|
2058
|
+
return buildMutationResult(params.filePath, response);
|
|
2059
|
+
},
|
|
2060
|
+
renderCall(args, theme, context) {
|
|
2061
|
+
return renderMutationCall("edit", args?.filePath, theme, context);
|
|
2062
|
+
},
|
|
2063
|
+
renderResult(result, _options, theme, context) {
|
|
2064
|
+
return renderMutationResult(result, theme, context);
|
|
1597
2065
|
}
|
|
1598
2066
|
});
|
|
1599
2067
|
}
|
|
@@ -1602,6 +2070,8 @@ ${JSON.stringify(diagnostics, null, 2)}`;
|
|
|
1602
2070
|
name: "grep",
|
|
1603
2071
|
label: "grep",
|
|
1604
2072
|
description: "Search for a regex pattern across files. Uses AFT's trigram index inside the project root for fast repeated queries, and falls back to ripgrep for paths outside the project root.",
|
|
2073
|
+
promptSnippet: "Fast regex search across files (trigram-indexed inside the project root)",
|
|
2074
|
+
promptGuidelines: ["Prefer grep over bash-invoked find/rg for in-project searches."],
|
|
1605
2075
|
parameters: GrepParams,
|
|
1606
2076
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
1607
2077
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
@@ -1621,6 +2091,112 @@ ${JSON.stringify(diagnostics, null, 2)}`;
|
|
|
1621
2091
|
});
|
|
1622
2092
|
}
|
|
1623
2093
|
}
|
|
2094
|
+
function buildMutationResult(filePath, response) {
|
|
2095
|
+
const diffObj = response.diff;
|
|
2096
|
+
const additions = diffObj?.additions ?? 0;
|
|
2097
|
+
const deletions = diffObj?.deletions ?? 0;
|
|
2098
|
+
const replacements = response.replacements;
|
|
2099
|
+
const diagnostics = response.lsp_diagnostics;
|
|
2100
|
+
const truncated = diffObj?.truncated === true;
|
|
2101
|
+
let diffText;
|
|
2102
|
+
let firstChangedLine;
|
|
2103
|
+
if (diffObj && !truncated && typeof diffObj.before === "string" && typeof diffObj.after === "string") {
|
|
2104
|
+
const formatted = formatDiffForPi(diffObj.before, diffObj.after);
|
|
2105
|
+
diffText = formatted.diff;
|
|
2106
|
+
firstChangedLine = formatted.firstChangedLine;
|
|
2107
|
+
}
|
|
2108
|
+
const summaryHeader = replacements !== undefined ? `Edited ${filePath} (+${additions}/-${deletions}, ${replacements} replacement${replacements === 1 ? "" : "s"})` : `Wrote ${filePath} (+${additions}/-${deletions})`;
|
|
2109
|
+
let text = summaryHeader;
|
|
2110
|
+
if (diffText)
|
|
2111
|
+
text += `
|
|
2112
|
+
|
|
2113
|
+
${diffText}`;
|
|
2114
|
+
if (truncated) {
|
|
2115
|
+
text += `
|
|
2116
|
+
|
|
2117
|
+
(diff truncated — file too large to include before/after content)`;
|
|
2118
|
+
}
|
|
2119
|
+
if (diagnostics && diagnostics.length > 0) {
|
|
2120
|
+
text += `
|
|
2121
|
+
|
|
2122
|
+
LSP diagnostics:
|
|
2123
|
+
${formatDiagnosticsText(diagnostics)}`;
|
|
2124
|
+
}
|
|
2125
|
+
return {
|
|
2126
|
+
content: [{ type: "text", text }],
|
|
2127
|
+
details: {
|
|
2128
|
+
diff: diffText,
|
|
2129
|
+
firstChangedLine,
|
|
2130
|
+
additions,
|
|
2131
|
+
deletions,
|
|
2132
|
+
replacements,
|
|
2133
|
+
diagnostics,
|
|
2134
|
+
truncated: truncated || undefined
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
function formatDiagnosticsText(diagnostics) {
|
|
2139
|
+
try {
|
|
2140
|
+
return diagnostics.map((d) => {
|
|
2141
|
+
if (d && typeof d === "object") {
|
|
2142
|
+
const obj = d;
|
|
2143
|
+
const line = obj.line ?? obj.startLine ?? "?";
|
|
2144
|
+
const severity = obj.severity ?? "info";
|
|
2145
|
+
const msg = obj.message ?? JSON.stringify(obj);
|
|
2146
|
+
return ` [${severity}] line ${line}: ${msg}`;
|
|
2147
|
+
}
|
|
2148
|
+
return ` ${String(d)}`;
|
|
2149
|
+
}).join(`
|
|
2150
|
+
`);
|
|
2151
|
+
} catch {
|
|
2152
|
+
return JSON.stringify(diagnostics, null, 2);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
function reuseText2(last) {
|
|
2156
|
+
return last instanceof Text2 ? last : new Text2("", 0, 0);
|
|
2157
|
+
}
|
|
2158
|
+
function reuseContainer2(last) {
|
|
2159
|
+
return last instanceof Container2 ? last : new Container2;
|
|
2160
|
+
}
|
|
2161
|
+
function renderMutationCall(toolName, filePath, theme, context) {
|
|
2162
|
+
const text = reuseText2(context.lastComponent);
|
|
2163
|
+
const pathDisplay = filePath ? theme.fg("accent", shortenPath2(filePath)) : theme.fg("toolOutput", "...");
|
|
2164
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))} ${pathDisplay}`);
|
|
2165
|
+
return text;
|
|
2166
|
+
}
|
|
2167
|
+
function renderMutationResult(result, theme, context) {
|
|
2168
|
+
if (context.isError) {
|
|
2169
|
+
const errorText = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
2170
|
+
`).trim();
|
|
2171
|
+
const text = reuseText2(context.lastComponent);
|
|
2172
|
+
text.setText(`
|
|
2173
|
+
${theme.fg("error", errorText || "edit failed")}`);
|
|
2174
|
+
return text;
|
|
2175
|
+
}
|
|
2176
|
+
const details = result.details;
|
|
2177
|
+
const diff = typeof details?.diff === "string" ? details.diff : undefined;
|
|
2178
|
+
if (!diff) {
|
|
2179
|
+
const additions = details?.additions ?? 0;
|
|
2180
|
+
const deletions = details?.deletions ?? 0;
|
|
2181
|
+
const text = reuseText2(context.lastComponent);
|
|
2182
|
+
const summary = theme.fg("success", `+${additions}/-${deletions}`);
|
|
2183
|
+
const suffix = details?.truncated ? ` ${theme.fg("muted", "(diff truncated)")}` : "";
|
|
2184
|
+
text.setText(`
|
|
2185
|
+
${summary}${suffix}`);
|
|
2186
|
+
return text;
|
|
2187
|
+
}
|
|
2188
|
+
const container = reuseContainer2(context.lastComponent);
|
|
2189
|
+
container.clear();
|
|
2190
|
+
container.addChild(new Spacer2(1));
|
|
2191
|
+
container.addChild(new Text2(renderDiff2(diff), 1, 0));
|
|
2192
|
+
return container;
|
|
2193
|
+
}
|
|
2194
|
+
function shortenPath2(path2) {
|
|
2195
|
+
const home = homedir6();
|
|
2196
|
+
if (path2.startsWith(home))
|
|
2197
|
+
return `~${path2.slice(home.length)}`;
|
|
2198
|
+
return path2;
|
|
2199
|
+
}
|
|
1624
2200
|
async function resolvePathArg(cwd, path2) {
|
|
1625
2201
|
const abs = resolve(cwd, path2);
|
|
1626
2202
|
try {
|
|
@@ -1650,6 +2226,54 @@ var ImportParams = Type5.Object({
|
|
|
1650
2226
|
description: "Post-edit validation level (default: syntax)"
|
|
1651
2227
|
}))
|
|
1652
2228
|
});
|
|
2229
|
+
function buildImportSections(args, payload, theme) {
|
|
2230
|
+
const response = asRecord2(payload);
|
|
2231
|
+
if (!response)
|
|
2232
|
+
return [theme.fg("muted", "No import result.")];
|
|
2233
|
+
if (response.dry_run === true) {
|
|
2234
|
+
return [
|
|
2235
|
+
theme.fg("warning", `[dry run] ${args.op}`),
|
|
2236
|
+
asString(response.diff) || theme.fg("muted", "No diff available.")
|
|
2237
|
+
];
|
|
2238
|
+
}
|
|
2239
|
+
if (args.op === "organize") {
|
|
2240
|
+
const groups = asRecords(response.groups);
|
|
2241
|
+
const groupText = groups.length > 0 ? groups.map((group) => `${asString(group.name) ?? "unknown"}: ${asNumber(group.count) ?? 0}`).join(" · ") : "No imports found";
|
|
2242
|
+
return [
|
|
2243
|
+
`${theme.fg("success", "organized")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
2244
|
+
`${theme.fg("muted", "groups")} ${groupText}`,
|
|
2245
|
+
`${theme.fg("muted", "duplicates removed")} ${asNumber(response.removed_duplicates) ?? 0}`
|
|
2246
|
+
];
|
|
2247
|
+
}
|
|
2248
|
+
if (args.op === "add") {
|
|
2249
|
+
const moduleName = asString(response.module) ?? args.module ?? "(module)";
|
|
2250
|
+
const status = response.already_present === true ? theme.fg("warning", "already present") : theme.fg("success", "added");
|
|
2251
|
+
return [
|
|
2252
|
+
`${status} ${theme.fg("accent", moduleName)}`,
|
|
2253
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
2254
|
+
`${theme.fg("muted", "group")} ${asString(response.group) ?? "—"}`
|
|
2255
|
+
];
|
|
2256
|
+
}
|
|
2257
|
+
return [
|
|
2258
|
+
`${theme.fg("success", "removed")} ${theme.fg("accent", asString(response.module) ?? args.module ?? "(module)")}`,
|
|
2259
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
2260
|
+
args.removeName ? `${theme.fg("muted", "name")} ${args.removeName}` : `${theme.fg("muted", "scope")} entire import`
|
|
2261
|
+
];
|
|
2262
|
+
}
|
|
2263
|
+
function renderImportCall(args, theme, context) {
|
|
2264
|
+
const summary = [
|
|
2265
|
+
theme.fg("accent", args.op),
|
|
2266
|
+
accentPath(theme, args.filePath),
|
|
2267
|
+
args.module ? theme.fg("toolOutput", args.module) : undefined
|
|
2268
|
+
].filter(Boolean).join(" ");
|
|
2269
|
+
return renderToolCall("import", summary, theme, context);
|
|
2270
|
+
}
|
|
2271
|
+
function renderImportResult(result, args, theme, context) {
|
|
2272
|
+
if (context.isError)
|
|
2273
|
+
return renderErrorResult(result, "import failed", theme, context);
|
|
2274
|
+
const payload = extractStructuredPayload(result);
|
|
2275
|
+
return renderSections(buildImportSections(args, payload, theme), context);
|
|
2276
|
+
}
|
|
1653
2277
|
function registerImportTools(pi, ctx) {
|
|
1654
2278
|
pi.registerTool({
|
|
1655
2279
|
name: "aft_import",
|
|
@@ -1683,6 +2307,12 @@ function registerImportTools(pi, ctx) {
|
|
|
1683
2307
|
req.validate = params.validate;
|
|
1684
2308
|
const response = await callBridge(bridge, commandMap[params.op], req);
|
|
1685
2309
|
return textResult(JSON.stringify(response, null, 2));
|
|
2310
|
+
},
|
|
2311
|
+
renderCall(args, theme, context) {
|
|
2312
|
+
return renderImportCall(args, theme, context);
|
|
2313
|
+
},
|
|
2314
|
+
renderResult(result, _options, theme, context) {
|
|
2315
|
+
return renderImportResult(result, context.args, theme, context);
|
|
1686
2316
|
}
|
|
1687
2317
|
});
|
|
1688
2318
|
}
|
|
@@ -1702,6 +2332,51 @@ var LspDiagnosticsParams = Type6.Object({
|
|
|
1702
2332
|
description: "Wait N ms for fresh diagnostics (max 10000, default: 0)"
|
|
1703
2333
|
}))
|
|
1704
2334
|
});
|
|
2335
|
+
function buildDiagnosticsSections(payload, theme) {
|
|
2336
|
+
const response = asRecord2(payload);
|
|
2337
|
+
if (!response)
|
|
2338
|
+
return [theme.fg("muted", "No diagnostics available.")];
|
|
2339
|
+
const diagnostics = asRecords(response.diagnostics);
|
|
2340
|
+
const total = asNumber(response.total) ?? diagnostics.length;
|
|
2341
|
+
const filesWithErrors = asNumber(response.files_with_errors) ?? distinctCount(diagnostics.filter((diag) => asString(diag.severity) === "error").map((diag) => asString(diag.file)));
|
|
2342
|
+
const filesCount = distinctCount(diagnostics.map((diag) => asString(diag.file)));
|
|
2343
|
+
const sections = [
|
|
2344
|
+
`${theme.fg(total > 0 ? "warning" : "success", `${total} diagnostic${total === 1 ? "" : "s"}`)} ${theme.fg("muted", `across ${filesCount} file${filesCount === 1 ? "" : "s"}, ${filesWithErrors} error file${filesWithErrors === 1 ? "" : "s"}`)}`
|
|
2345
|
+
];
|
|
2346
|
+
if (diagnostics.length === 0) {
|
|
2347
|
+
sections.push(theme.fg("muted", "No diagnostics found."));
|
|
2348
|
+
return sections;
|
|
2349
|
+
}
|
|
2350
|
+
const grouped = groupByFile(diagnostics, (diag) => asString(diag.file));
|
|
2351
|
+
for (const [file, fileDiagnostics] of grouped.entries()) {
|
|
2352
|
+
const lines = [theme.fg("accent", shortenPath(file))];
|
|
2353
|
+
fileDiagnostics.forEach((diagnostic) => {
|
|
2354
|
+
const severity = asString(diagnostic.severity) ?? "information";
|
|
2355
|
+
const line = asNumber(diagnostic.line) ?? 0;
|
|
2356
|
+
const column = asNumber(diagnostic.column) ?? 0;
|
|
2357
|
+
const code = asString(diagnostic.code);
|
|
2358
|
+
const message = asString(diagnostic.message) ?? "(no message)";
|
|
2359
|
+
const location = `${line}:${column}`;
|
|
2360
|
+
lines.push(` ${severityBadge(theme, severity)} ${location}${code ? ` ${theme.fg("muted", code)}` : ""} ${message}`);
|
|
2361
|
+
});
|
|
2362
|
+
sections.push(lines.join(`
|
|
2363
|
+
`));
|
|
2364
|
+
}
|
|
2365
|
+
return sections;
|
|
2366
|
+
}
|
|
2367
|
+
function renderDiagnosticsCall(args, theme, context) {
|
|
2368
|
+
const target = args.filePath ?? args.directory;
|
|
2369
|
+
const summary = [
|
|
2370
|
+
target ? accentPath(theme, target) : undefined,
|
|
2371
|
+
args.severity ? theme.fg("toolOutput", args.severity) : undefined
|
|
2372
|
+
].filter(Boolean).join(" ");
|
|
2373
|
+
return renderToolCall("lsp diagnostics", summary, theme, context);
|
|
2374
|
+
}
|
|
2375
|
+
function renderDiagnosticsResult(result, theme, context) {
|
|
2376
|
+
if (context.isError)
|
|
2377
|
+
return renderErrorResult(result, "lsp diagnostics failed", theme, context);
|
|
2378
|
+
return renderSections(buildDiagnosticsSections(extractStructuredPayload(result), theme), context);
|
|
2379
|
+
}
|
|
1705
2380
|
function registerLspTools(pi, ctx) {
|
|
1706
2381
|
pi.registerTool({
|
|
1707
2382
|
name: "lsp_diagnostics",
|
|
@@ -1726,6 +2401,12 @@ function registerLspTools(pi, ctx) {
|
|
|
1726
2401
|
req.wait_ms = params.waitMs;
|
|
1727
2402
|
const response = await callBridge(bridge, "lsp_diagnostics", req);
|
|
1728
2403
|
return textResult(JSON.stringify(response, null, 2));
|
|
2404
|
+
},
|
|
2405
|
+
renderCall(args, theme, context) {
|
|
2406
|
+
return renderDiagnosticsCall(args, theme, context);
|
|
2407
|
+
},
|
|
2408
|
+
renderResult(result, _options, theme, context) {
|
|
2409
|
+
return renderDiagnosticsResult(result, theme, context);
|
|
1729
2410
|
}
|
|
1730
2411
|
});
|
|
1731
2412
|
}
|
|
@@ -1742,6 +2423,121 @@ var NavigateParams = Type7.Object({
|
|
|
1742
2423
|
depth: Type7.Optional(Type7.Number({ description: "Max traversal depth" })),
|
|
1743
2424
|
expression: Type7.Optional(Type7.String({ description: "Expression to track (required for trace_data)" }))
|
|
1744
2425
|
});
|
|
2426
|
+
function treeLine(depth, text) {
|
|
2427
|
+
return `${" ".repeat(depth)}${depth === 0 ? "" : "↳ "}${text}`;
|
|
2428
|
+
}
|
|
2429
|
+
function renderCallTreeNode(node, depth, lines) {
|
|
2430
|
+
const name = asString(node.name) ?? "(unknown)";
|
|
2431
|
+
const file = shortenPath(asString(node.file) ?? "(unknown file)");
|
|
2432
|
+
const line = asNumber(node.line);
|
|
2433
|
+
lines.push(treeLine(depth, `${name} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}`));
|
|
2434
|
+
asRecords(node.children).forEach((child) => {
|
|
2435
|
+
renderCallTreeNode(child, depth + 1, lines);
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
function renderTracePath(path2, index, lines) {
|
|
2439
|
+
lines.push(`Path ${index + 1}`);
|
|
2440
|
+
asRecords(path2.hops).forEach((hop, hopIndex) => {
|
|
2441
|
+
const symbol = asString(hop.symbol) ?? "(unknown)";
|
|
2442
|
+
const file = shortenPath(asString(hop.file) ?? "(unknown file)");
|
|
2443
|
+
const line = asNumber(hop.line);
|
|
2444
|
+
const entry = hop.is_entry_point === true ? " [entry]" : "";
|
|
2445
|
+
lines.push(treeLine(hopIndex + 1, `${symbol}${entry} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}`));
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
function buildNavigateSections(args, payload, theme) {
|
|
2449
|
+
const response = asRecord2(payload);
|
|
2450
|
+
if (!response)
|
|
2451
|
+
return [theme.fg("muted", "No navigation result.")];
|
|
2452
|
+
if (args.op === "call_tree") {
|
|
2453
|
+
const lines = [];
|
|
2454
|
+
renderCallTreeNode(response, 0, lines);
|
|
2455
|
+
return lines.length > 0 ? lines : [theme.fg("muted", "No call tree available.")];
|
|
2456
|
+
}
|
|
2457
|
+
if (args.op === "callers") {
|
|
2458
|
+
const groups = asRecords(response.callers);
|
|
2459
|
+
const sections2 = [
|
|
2460
|
+
`${theme.fg("success", `${asNumber(response.total_callers) ?? 0} caller${(asNumber(response.total_callers) ?? 0) === 1 ? "" : "s"}`)} ${theme.fg("muted", `${groups.length} file group${groups.length === 1 ? "" : "s"}`)}`
|
|
2461
|
+
];
|
|
2462
|
+
groups.forEach((group) => {
|
|
2463
|
+
const file = shortenPath(asString(group.file) ?? "(unknown file)");
|
|
2464
|
+
const lines = [theme.fg("accent", file)];
|
|
2465
|
+
asRecords(group.callers).forEach((caller) => {
|
|
2466
|
+
lines.push(` ↳ ${asString(caller.symbol) ?? "(unknown)"} ${theme.fg("muted", `line ${asNumber(caller.line) ?? "?"}`)}`);
|
|
2467
|
+
});
|
|
2468
|
+
sections2.push(lines.join(`
|
|
2469
|
+
`));
|
|
2470
|
+
});
|
|
2471
|
+
return sections2;
|
|
2472
|
+
}
|
|
2473
|
+
if (args.op === "trace_to") {
|
|
2474
|
+
const paths = asRecords(response.paths);
|
|
2475
|
+
const sections2 = [
|
|
2476
|
+
`${theme.fg("success", `${asNumber(response.total_paths) ?? paths.length} path${(asNumber(response.total_paths) ?? paths.length) === 1 ? "" : "s"}`)} ${theme.fg("muted", `${asNumber(response.entry_points_found) ?? 0} entry point${(asNumber(response.entry_points_found) ?? 0) === 1 ? "" : "s"}`)}`
|
|
2477
|
+
];
|
|
2478
|
+
if (paths.length === 0)
|
|
2479
|
+
sections2.push(theme.fg("muted", "No entry paths found."));
|
|
2480
|
+
paths.forEach((path2, index) => {
|
|
2481
|
+
const lines = [];
|
|
2482
|
+
renderTracePath(path2, index, lines);
|
|
2483
|
+
sections2.push(lines.join(`
|
|
2484
|
+
`));
|
|
2485
|
+
});
|
|
2486
|
+
return sections2;
|
|
2487
|
+
}
|
|
2488
|
+
if (args.op === "impact") {
|
|
2489
|
+
const callers = asRecords(response.callers);
|
|
2490
|
+
const sections2 = [
|
|
2491
|
+
`${theme.fg("warning", `${asNumber(response.total_affected) ?? callers.length} affected call site${(asNumber(response.total_affected) ?? callers.length) === 1 ? "" : "s"}`)} ${theme.fg("muted", `${asNumber(response.affected_files) ?? 0} file${(asNumber(response.affected_files) ?? 0) === 1 ? "" : "s"}`)}`
|
|
2492
|
+
];
|
|
2493
|
+
if (callers.length === 0)
|
|
2494
|
+
sections2.push(theme.fg("muted", "No impacted callers found."));
|
|
2495
|
+
callers.forEach((caller) => {
|
|
2496
|
+
const file = shortenPath(asString(caller.caller_file) ?? "(unknown file)");
|
|
2497
|
+
const symbol = asString(caller.caller_symbol) ?? "(unknown)";
|
|
2498
|
+
const line = asNumber(caller.line) ?? 0;
|
|
2499
|
+
const entry = caller.is_entry_point === true ? ` ${theme.fg("warning", "[entry]")}` : "";
|
|
2500
|
+
const expression = asString(caller.call_expression);
|
|
2501
|
+
const params = Array.isArray(caller.parameters) ? caller.parameters.map(String).join(", ") : "";
|
|
2502
|
+
sections2.push([
|
|
2503
|
+
`${theme.fg("accent", file)}:${line}`,
|
|
2504
|
+
` ↳ ${symbol}${entry}`,
|
|
2505
|
+
expression ? ` ${theme.fg("muted", expression)}` : undefined,
|
|
2506
|
+
params ? ` ${theme.fg("muted", `params: ${params}`)}` : undefined
|
|
2507
|
+
].filter(Boolean).join(`
|
|
2508
|
+
`));
|
|
2509
|
+
});
|
|
2510
|
+
return sections2;
|
|
2511
|
+
}
|
|
2512
|
+
const hops = asRecords(response.hops);
|
|
2513
|
+
const sections = [
|
|
2514
|
+
`${theme.fg("success", `${hops.length} hop${hops.length === 1 ? "" : "s"}`)} ${asBoolean(response.depth_limited) ? theme.fg("warning", "(depth limited)") : ""}`.trim()
|
|
2515
|
+
];
|
|
2516
|
+
if (hops.length === 0)
|
|
2517
|
+
sections.push(theme.fg("muted", "No data-flow hops found."));
|
|
2518
|
+
hops.forEach((hop, index) => {
|
|
2519
|
+
const file = shortenPath(asString(hop.file) ?? "(unknown file)");
|
|
2520
|
+
const symbol = asString(hop.symbol) ?? "(unknown)";
|
|
2521
|
+
const variable = asString(hop.variable) ?? "(unknown)";
|
|
2522
|
+
const line = asNumber(hop.line) ?? 0;
|
|
2523
|
+
const approximate = hop.approximate === true ? ` ${theme.fg("warning", "[approx]")}` : "";
|
|
2524
|
+
sections.push(treeLine(index, `${variable} ${theme.fg("muted", `${asString(hop.flow_type) ?? "flow"}`)} ${symbol} [${file}:${line}]${approximate}`));
|
|
2525
|
+
});
|
|
2526
|
+
return sections;
|
|
2527
|
+
}
|
|
2528
|
+
function renderNavigateCall(args, theme, context) {
|
|
2529
|
+
const summary = [
|
|
2530
|
+
theme.fg("accent", args.op),
|
|
2531
|
+
accentPath(theme, args.filePath),
|
|
2532
|
+
theme.fg("toolOutput", args.symbol)
|
|
2533
|
+
].filter(Boolean).join(" ");
|
|
2534
|
+
return renderToolCall("navigate", summary, theme, context);
|
|
2535
|
+
}
|
|
2536
|
+
function renderNavigateResult(result, args, theme, context) {
|
|
2537
|
+
if (context.isError)
|
|
2538
|
+
return renderErrorResult(result, "navigate failed", theme, context);
|
|
2539
|
+
return renderSections(buildNavigateSections(args, extractStructuredPayload(result), theme), context);
|
|
2540
|
+
}
|
|
1745
2541
|
function registerNavigateTool(pi, ctx) {
|
|
1746
2542
|
pi.registerTool({
|
|
1747
2543
|
name: "aft_navigate",
|
|
@@ -1764,6 +2560,12 @@ function registerNavigateTool(pi, ctx) {
|
|
|
1764
2560
|
req.expression = params.expression;
|
|
1765
2561
|
const response = await callBridge(bridge, params.op, req);
|
|
1766
2562
|
return textResult(JSON.stringify(response, null, 2));
|
|
2563
|
+
},
|
|
2564
|
+
renderCall(args, theme, context) {
|
|
2565
|
+
return renderNavigateCall(args, theme, context);
|
|
2566
|
+
},
|
|
2567
|
+
renderResult(result, _options, theme, context) {
|
|
2568
|
+
return renderNavigateResult(result, context.args, theme, context);
|
|
1767
2569
|
}
|
|
1768
2570
|
});
|
|
1769
2571
|
}
|
|
@@ -1876,6 +2678,71 @@ var ZoomParams = Type8.Object({
|
|
|
1876
2678
|
symbols: Type8.Optional(Type8.Array(Type8.String(), { description: "Multiple symbols — returns array of matches" })),
|
|
1877
2679
|
contextLines: Type8.Optional(Type8.Number({ description: "Lines of context before/after (default: 3)" }))
|
|
1878
2680
|
});
|
|
2681
|
+
function buildOutlineSections(text, theme) {
|
|
2682
|
+
const trimmed = text.trim();
|
|
2683
|
+
if (!trimmed)
|
|
2684
|
+
return [theme.fg("muted", "No outline available.")];
|
|
2685
|
+
const lines = trimmed.split(`
|
|
2686
|
+
`);
|
|
2687
|
+
if (lines.length === 1)
|
|
2688
|
+
return [theme.fg("accent", lines[0])];
|
|
2689
|
+
return [theme.fg("accent", lines[0]), lines.slice(1).join(`
|
|
2690
|
+
`)];
|
|
2691
|
+
}
|
|
2692
|
+
function buildZoomSections(args, payload, theme) {
|
|
2693
|
+
const items = Array.isArray(payload) ? payload : payload ? [payload] : [];
|
|
2694
|
+
if (items.length === 0)
|
|
2695
|
+
return [theme.fg("muted", "No zoom result available.")];
|
|
2696
|
+
return items.map((item) => {
|
|
2697
|
+
const record = asRecord2(item);
|
|
2698
|
+
if (!record)
|
|
2699
|
+
return theme.fg("muted", "No zoom result available.");
|
|
2700
|
+
const name = asString(record.name) ?? "(unknown symbol)";
|
|
2701
|
+
const kind = asString(record.kind) ?? "symbol";
|
|
2702
|
+
const range = asRecord2(record.range);
|
|
2703
|
+
const startLine = range && typeof range.start_line === "number" ? range.start_line : undefined;
|
|
2704
|
+
const endLine = range && typeof range.end_line === "number" ? range.end_line : undefined;
|
|
2705
|
+
const location = startLine !== undefined ? `${shortenPath(args.filePath)}:${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : shortenPath(args.filePath);
|
|
2706
|
+
const lines = [`${theme.fg("accent", name)} ${theme.fg("muted", `[${kind}] ${location}`)}`];
|
|
2707
|
+
const content = asString(record.content);
|
|
2708
|
+
if (content) {
|
|
2709
|
+
lines.push(content.split(`
|
|
2710
|
+
`).map((line) => ` ${line}`).join(`
|
|
2711
|
+
`));
|
|
2712
|
+
}
|
|
2713
|
+
const annotations = asRecord2(record.annotations);
|
|
2714
|
+
const callsOut = annotations ? asRecords(annotations.calls_out) : [];
|
|
2715
|
+
const calledBy = annotations ? asRecords(annotations.called_by) : [];
|
|
2716
|
+
if (callsOut.length > 0) {
|
|
2717
|
+
lines.push(`${theme.fg("muted", "calls out")}`, callsOut.map((call) => ` ↳ ${asString(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}`).join(`
|
|
2718
|
+
`));
|
|
2719
|
+
}
|
|
2720
|
+
if (calledBy.length > 0) {
|
|
2721
|
+
lines.push(`${theme.fg("muted", "called by")}`, calledBy.map((call) => ` ↳ ${asString(call.name) ?? "(unknown)"}${typeof call.line === "number" ? `:${call.line}` : ""}`).join(`
|
|
2722
|
+
`));
|
|
2723
|
+
}
|
|
2724
|
+
return lines.join(`
|
|
2725
|
+
`);
|
|
2726
|
+
}).filter(Boolean);
|
|
2727
|
+
}
|
|
2728
|
+
function renderOutlineCall(args, theme, context) {
|
|
2729
|
+
const summary = args.filePath ? accentPath(theme, args.filePath) : args.directory ? `${theme.fg("muted", "dir")} ${accentPath(theme, args.directory)}` : args.files && args.files.length > 0 ? theme.fg("accent", `${args.files.length} files`) : undefined;
|
|
2730
|
+
return renderToolCall("outline", summary, theme, context);
|
|
2731
|
+
}
|
|
2732
|
+
function renderOutlineResult(result, theme, context) {
|
|
2733
|
+
if (context.isError)
|
|
2734
|
+
return renderErrorResult(result, "outline failed", theme, context);
|
|
2735
|
+
return renderSections(buildOutlineSections(collectTextContent(result), theme), context);
|
|
2736
|
+
}
|
|
2737
|
+
function renderZoomCall(args, theme, context) {
|
|
2738
|
+
const target = args.symbol ? theme.fg("toolOutput", args.symbol) : args.symbols && args.symbols.length > 0 ? theme.fg("toolOutput", `${args.symbols.length} symbols`) : theme.fg("toolOutput", "lines");
|
|
2739
|
+
return renderToolCall("zoom", `${accentPath(theme, args.filePath)} ${target}`, theme, context);
|
|
2740
|
+
}
|
|
2741
|
+
function renderZoomResult(result, args, theme, context) {
|
|
2742
|
+
if (context.isError)
|
|
2743
|
+
return renderErrorResult(result, "zoom failed", theme, context);
|
|
2744
|
+
return renderSections(buildZoomSections(args, extractStructuredPayload(result), theme), context);
|
|
2745
|
+
}
|
|
1879
2746
|
function registerReadingTools(pi, ctx, surface) {
|
|
1880
2747
|
if (surface.outline) {
|
|
1881
2748
|
pi.registerTool({
|
|
@@ -1919,6 +2786,12 @@ function registerReadingTools(pi, ctx, surface) {
|
|
|
1919
2786
|
}
|
|
1920
2787
|
const response = await callBridge(bridge, "outline", { file: params.filePath });
|
|
1921
2788
|
return textResult(response.text ?? "");
|
|
2789
|
+
},
|
|
2790
|
+
renderCall(args, theme, context) {
|
|
2791
|
+
return renderOutlineCall(args, theme, context);
|
|
2792
|
+
},
|
|
2793
|
+
renderResult(result, _options, theme, context) {
|
|
2794
|
+
return renderOutlineResult(result, theme, context);
|
|
1922
2795
|
}
|
|
1923
2796
|
});
|
|
1924
2797
|
}
|
|
@@ -1946,6 +2819,12 @@ function registerReadingTools(pi, ctx, surface) {
|
|
|
1946
2819
|
req.context_lines = params.contextLines;
|
|
1947
2820
|
const response = await callBridge(bridge, "zoom", req);
|
|
1948
2821
|
return textResult(JSON.stringify(response, null, 2));
|
|
2822
|
+
},
|
|
2823
|
+
renderCall(args, theme, context) {
|
|
2824
|
+
return renderZoomCall(args, theme, context);
|
|
2825
|
+
},
|
|
2826
|
+
renderResult(result, _options, theme, context) {
|
|
2827
|
+
return renderZoomResult(result, context.args, theme, context);
|
|
1949
2828
|
}
|
|
1950
2829
|
});
|
|
1951
2830
|
}
|
|
@@ -1966,6 +2845,63 @@ var RefactorParams = Type9.Object({
|
|
|
1966
2845
|
callSiteLine: Type9.Optional(Type9.Number({ description: "1-based call site line (for inline)" })),
|
|
1967
2846
|
dryRun: Type9.Optional(Type9.Boolean({ description: "Preview as diff" }))
|
|
1968
2847
|
});
|
|
2848
|
+
function buildRefactorSections(args, payload, theme) {
|
|
2849
|
+
const response = asRecord2(payload);
|
|
2850
|
+
if (!response)
|
|
2851
|
+
return [theme.fg("muted", "No refactor result.")];
|
|
2852
|
+
if (response.dry_run === true) {
|
|
2853
|
+
const diffs = asRecords(response.diffs);
|
|
2854
|
+
const sections = [theme.fg("warning", `[dry run] ${args.op}`)];
|
|
2855
|
+
if (diffs.length === 0) {
|
|
2856
|
+
sections.push(theme.fg("muted", "No diff available."));
|
|
2857
|
+
return sections;
|
|
2858
|
+
}
|
|
2859
|
+
diffs.forEach((diff) => {
|
|
2860
|
+
const file = shortenPath(asString(diff.file) ?? "(unknown file)");
|
|
2861
|
+
const rendered = renderUnifiedDiff(asString(diff.diff) ?? "") || theme.fg("muted", "No diff available.");
|
|
2862
|
+
sections.push(`${theme.fg("accent", file)}
|
|
2863
|
+
${rendered}`);
|
|
2864
|
+
});
|
|
2865
|
+
return sections;
|
|
2866
|
+
}
|
|
2867
|
+
if (args.op === "move") {
|
|
2868
|
+
const results = asRecords(response.results);
|
|
2869
|
+
return [
|
|
2870
|
+
`${theme.fg("success", "moved symbol")} ${theme.fg("toolOutput", args.symbol ?? "(symbol)")}`,
|
|
2871
|
+
`${theme.fg("muted", "files modified")} ${asNumber(response.files_modified) ?? results.length}`,
|
|
2872
|
+
`${theme.fg("muted", "consumers updated")} ${asNumber(response.consumers_updated) ?? 0}`,
|
|
2873
|
+
results.length > 0 ? results.map((entry) => ` ↳ ${shortenPath(asString(entry.file) ?? "(unknown file)")}`).join(`
|
|
2874
|
+
`) : theme.fg("muted", "No files reported.")
|
|
2875
|
+
];
|
|
2876
|
+
}
|
|
2877
|
+
if (args.op === "extract") {
|
|
2878
|
+
return [
|
|
2879
|
+
`${theme.fg("success", "extracted")} ${theme.fg("toolOutput", asString(response.name) ?? args.name ?? "(function)")}`,
|
|
2880
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath(asString(response.file) ?? args.filePath))}`,
|
|
2881
|
+
`${theme.fg("muted", "params")} ${Array.isArray(response.parameters) ? response.parameters.join(", ") || "none" : "none"}`,
|
|
2882
|
+
`${theme.fg("muted", "return type")} ${asString(response.return_type) ?? "unknown"}`
|
|
2883
|
+
];
|
|
2884
|
+
}
|
|
2885
|
+
return [
|
|
2886
|
+
`${theme.fg("success", "inlined")} ${theme.fg("toolOutput", asString(response.symbol) ?? args.symbol ?? "(symbol)")}`,
|
|
2887
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath(asString(response.file) ?? args.filePath))}`,
|
|
2888
|
+
`${theme.fg("muted", "context")} ${asString(response.call_context) ?? "unknown"}`,
|
|
2889
|
+
`${theme.fg("muted", "substitutions")} ${asNumber(response.substitutions) ?? 0}`
|
|
2890
|
+
];
|
|
2891
|
+
}
|
|
2892
|
+
function renderRefactorCall(args, theme, context) {
|
|
2893
|
+
const summary = [
|
|
2894
|
+
theme.fg("accent", args.op),
|
|
2895
|
+
accentPath(theme, args.filePath),
|
|
2896
|
+
args.symbol ? theme.fg("toolOutput", args.symbol) : undefined
|
|
2897
|
+
].filter(Boolean).join(" ");
|
|
2898
|
+
return renderToolCall("refactor", summary, theme, context);
|
|
2899
|
+
}
|
|
2900
|
+
function renderRefactorResult(result, args, theme, context) {
|
|
2901
|
+
if (context.isError)
|
|
2902
|
+
return renderErrorResult(result, "refactor failed", theme, context);
|
|
2903
|
+
return renderSections(buildRefactorSections(args, extractStructuredPayload(result), theme), context);
|
|
2904
|
+
}
|
|
1969
2905
|
function registerRefactorTool(pi, ctx) {
|
|
1970
2906
|
pi.registerTool({
|
|
1971
2907
|
name: "aft_refactor",
|
|
@@ -1999,6 +2935,12 @@ function registerRefactorTool(pi, ctx) {
|
|
|
1999
2935
|
req.dry_run = params.dryRun;
|
|
2000
2936
|
const response = await callBridge(bridge, commandMap[params.op], req);
|
|
2001
2937
|
return textResult(JSON.stringify(response, null, 2));
|
|
2938
|
+
},
|
|
2939
|
+
renderCall(args, theme, context) {
|
|
2940
|
+
return renderRefactorCall(args, theme, context);
|
|
2941
|
+
},
|
|
2942
|
+
renderResult(result, _options, theme, context) {
|
|
2943
|
+
return renderRefactorResult(result, context.args, theme, context);
|
|
2002
2944
|
}
|
|
2003
2945
|
});
|
|
2004
2946
|
}
|
|
@@ -2016,6 +2958,78 @@ var SafetyParams = Type10.Object({
|
|
|
2016
2958
|
description: "Specific files for checkpoint (optional, defaults to all tracked)"
|
|
2017
2959
|
}))
|
|
2018
2960
|
});
|
|
2961
|
+
function buildSafetySections(args, payload, theme) {
|
|
2962
|
+
const response = asRecord2(payload);
|
|
2963
|
+
if (!response)
|
|
2964
|
+
return [theme.fg("muted", "No safety result.")];
|
|
2965
|
+
if (args.op === "undo") {
|
|
2966
|
+
return [
|
|
2967
|
+
`${theme.fg("success", "restored")} ${theme.fg("accent", shortenPath(asString(response.path) ?? args.filePath ?? "(file)"))}`,
|
|
2968
|
+
`${theme.fg("muted", "backup")} ${asString(response.backup_id) ?? "—"}`
|
|
2969
|
+
];
|
|
2970
|
+
}
|
|
2971
|
+
if (args.op === "history") {
|
|
2972
|
+
const entries = asRecords(response.entries);
|
|
2973
|
+
const sections2 = [
|
|
2974
|
+
theme.fg("accent", shortenPath(asString(response.file) ?? args.filePath ?? "(file)"))
|
|
2975
|
+
];
|
|
2976
|
+
if (entries.length === 0) {
|
|
2977
|
+
sections2.push(theme.fg("muted", "No history entries."));
|
|
2978
|
+
return sections2;
|
|
2979
|
+
}
|
|
2980
|
+
sections2.push(entries.map((entry, index) => {
|
|
2981
|
+
const backupId = asString(entry.backup_id) ?? `entry-${index + 1}`;
|
|
2982
|
+
const timestamp = formatTimestamp(entry.timestamp) ?? "unknown time";
|
|
2983
|
+
const description = asString(entry.description) ?? "";
|
|
2984
|
+
return `${index + 1}. ${backupId} ${theme.fg("muted", timestamp)}${description ? `
|
|
2985
|
+
${description}` : ""}`;
|
|
2986
|
+
}).join(`
|
|
2987
|
+
`));
|
|
2988
|
+
return sections2;
|
|
2989
|
+
}
|
|
2990
|
+
if (args.op === "checkpoint") {
|
|
2991
|
+
const skipped = asRecords(response.skipped);
|
|
2992
|
+
return [
|
|
2993
|
+
`${theme.fg("success", "checkpoint created")} ${theme.fg("accent", asString(response.name) ?? args.name ?? "(checkpoint)")}`,
|
|
2994
|
+
`${theme.fg("muted", "files")} ${asNumber(response.file_count) ?? 0}`,
|
|
2995
|
+
skipped.length > 0 ? `${theme.fg("warning", "skipped")}
|
|
2996
|
+
${skipped.map((entry) => ` ↳ ${shortenPath(asString(entry.file) ?? "(file)")}: ${asString(entry.error) ?? "unknown error"}`).join(`
|
|
2997
|
+
`)}` : theme.fg("muted", "No skipped files.")
|
|
2998
|
+
];
|
|
2999
|
+
}
|
|
3000
|
+
if (args.op === "restore") {
|
|
3001
|
+
return [
|
|
3002
|
+
`${theme.fg("success", "checkpoint restored")} ${theme.fg("accent", asString(response.name) ?? args.name ?? "(checkpoint)")}`,
|
|
3003
|
+
`${theme.fg("muted", "files")} ${asNumber(response.file_count) ?? 0}`
|
|
3004
|
+
];
|
|
3005
|
+
}
|
|
3006
|
+
const checkpoints = asRecords(response.checkpoints);
|
|
3007
|
+
const sections = [
|
|
3008
|
+
theme.fg("accent", `${checkpoints.length} checkpoint${checkpoints.length === 1 ? "" : "s"}`)
|
|
3009
|
+
];
|
|
3010
|
+
if (checkpoints.length === 0) {
|
|
3011
|
+
sections.push(theme.fg("muted", "No checkpoints saved."));
|
|
3012
|
+
return sections;
|
|
3013
|
+
}
|
|
3014
|
+
sections.push(checkpoints.map((checkpoint, index) => {
|
|
3015
|
+
const name = asString(checkpoint.name) ?? `checkpoint-${index + 1}`;
|
|
3016
|
+
const count = asNumber(checkpoint.file_count) ?? 0;
|
|
3017
|
+
const created = formatTimestamp(checkpoint.created_at) ?? "unknown time";
|
|
3018
|
+
return `${index + 1}. ${name} ${theme.fg("muted", `${count} file${count === 1 ? "" : "s"} · ${created}`)}`;
|
|
3019
|
+
}).join(`
|
|
3020
|
+
`));
|
|
3021
|
+
return sections;
|
|
3022
|
+
}
|
|
3023
|
+
function renderSafetyCall(args, theme, context) {
|
|
3024
|
+
const target = args.filePath ?? args.name;
|
|
3025
|
+
const summary = [theme.fg("accent", args.op), target ? accentPath(theme, target) : undefined].filter(Boolean).join(" ");
|
|
3026
|
+
return renderToolCall("safety", summary, theme, context);
|
|
3027
|
+
}
|
|
3028
|
+
function renderSafetyResult(result, args, theme, context) {
|
|
3029
|
+
if (context.isError)
|
|
3030
|
+
return renderErrorResult(result, "safety failed", theme, context);
|
|
3031
|
+
return renderSections(buildSafetySections(args, extractStructuredPayload(result), theme), context);
|
|
3032
|
+
}
|
|
2019
3033
|
function registerSafetyTool(pi, ctx) {
|
|
2020
3034
|
pi.registerTool({
|
|
2021
3035
|
name: "aft_safety",
|
|
@@ -2038,14 +3052,28 @@ function registerSafetyTool(pi, ctx) {
|
|
|
2038
3052
|
list: "list_checkpoints"
|
|
2039
3053
|
};
|
|
2040
3054
|
const req = {};
|
|
2041
|
-
if (params.filePath)
|
|
2042
|
-
req.file = params.filePath;
|
|
2043
3055
|
if (params.name)
|
|
2044
3056
|
req.name = params.name;
|
|
2045
|
-
if (params.
|
|
2046
|
-
|
|
3057
|
+
if (params.op === "checkpoint") {
|
|
3058
|
+
if (params.files) {
|
|
3059
|
+
req.files = params.files;
|
|
3060
|
+
} else if (params.filePath) {
|
|
3061
|
+
req.files = [params.filePath];
|
|
3062
|
+
}
|
|
3063
|
+
} else {
|
|
3064
|
+
if (params.filePath)
|
|
3065
|
+
req.file = params.filePath;
|
|
3066
|
+
if (params.files)
|
|
3067
|
+
req.files = params.files;
|
|
3068
|
+
}
|
|
2047
3069
|
const response = await callBridge(bridge, commandMap[params.op], req);
|
|
2048
3070
|
return textResult(JSON.stringify(response, null, 2));
|
|
3071
|
+
},
|
|
3072
|
+
renderCall(args, theme, context) {
|
|
3073
|
+
return renderSafetyCall(args, theme, context);
|
|
3074
|
+
},
|
|
3075
|
+
renderResult(result, _options, theme, context) {
|
|
3076
|
+
return renderSafetyResult(result, context.args, theme, context);
|
|
2049
3077
|
}
|
|
2050
3078
|
});
|
|
2051
3079
|
}
|
|
@@ -2056,6 +3084,53 @@ var SearchParams2 = Type11.Object({
|
|
|
2056
3084
|
query: Type11.String({ description: "Natural-language description of the code to find" }),
|
|
2057
3085
|
topK: Type11.Optional(Type11.Number({ description: "Maximum number of results (default: 10, max: 100)" }))
|
|
2058
3086
|
});
|
|
3087
|
+
function buildSemanticSections(args, payload, theme) {
|
|
3088
|
+
const response = asRecord2(payload);
|
|
3089
|
+
if (!response)
|
|
3090
|
+
return [theme.fg("muted", "No semantic search result.")];
|
|
3091
|
+
const status = asString(response.status) ?? "unknown";
|
|
3092
|
+
const sections = [
|
|
3093
|
+
`${theme.fg(status === "ready" ? "success" : "warning", `index: ${status}`)} ${theme.fg("muted", `query=${JSON.stringify(args.query)} topK=${args.topK ?? 10}`)}`
|
|
3094
|
+
];
|
|
3095
|
+
if (status !== "ready") {
|
|
3096
|
+
sections.push(asString(response.text) ?? theme.fg("muted", "Semantic index is not ready."));
|
|
3097
|
+
return sections;
|
|
3098
|
+
}
|
|
3099
|
+
const results = asRecords(response.results);
|
|
3100
|
+
if (results.length === 0) {
|
|
3101
|
+
sections.push(theme.fg("muted", "No semantic matches found."));
|
|
3102
|
+
return sections;
|
|
3103
|
+
}
|
|
3104
|
+
const grouped = groupByFile(results, (result) => asString(result.file));
|
|
3105
|
+
for (const [file, fileResults] of grouped.entries()) {
|
|
3106
|
+
const lines = [theme.fg("accent", shortenPath(file))];
|
|
3107
|
+
fileResults.forEach((result) => {
|
|
3108
|
+
const score = asNumber(result.score);
|
|
3109
|
+
const startLine = asNumber(result.start_line);
|
|
3110
|
+
const endLine = asNumber(result.end_line);
|
|
3111
|
+
const range = startLine !== undefined ? `${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : "?";
|
|
3112
|
+
const kind = asString(result.kind) ?? "symbol";
|
|
3113
|
+
const name = asString(result.name) ?? "(unknown)";
|
|
3114
|
+
lines.push(` ↳ ${name} ${theme.fg("muted", `[${kind}] lines ${range}${score !== undefined ? ` score ${score.toFixed(3)}` : ""}`)}`);
|
|
3115
|
+
const snippet = asString(result.snippet);
|
|
3116
|
+
if (snippet) {
|
|
3117
|
+
lines.push(...snippet.split(`
|
|
3118
|
+
`).map((line) => ` ${line}`));
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
sections.push(lines.join(`
|
|
3122
|
+
`));
|
|
3123
|
+
}
|
|
3124
|
+
return sections;
|
|
3125
|
+
}
|
|
3126
|
+
function renderSemanticCall(args, theme, context) {
|
|
3127
|
+
return renderToolCall("semantic search", theme.fg("toolOutput", args.query), theme, context);
|
|
3128
|
+
}
|
|
3129
|
+
function renderSemanticResult(result, args, theme, context) {
|
|
3130
|
+
if (context.isError)
|
|
3131
|
+
return renderErrorResult(result, "semantic search failed", theme, context);
|
|
3132
|
+
return renderSections(buildSemanticSections(args, extractStructuredPayload(result), theme), context);
|
|
3133
|
+
}
|
|
2059
3134
|
function registerSemanticTool(pi, ctx) {
|
|
2060
3135
|
pi.registerTool({
|
|
2061
3136
|
name: "aft_search",
|
|
@@ -2069,6 +3144,12 @@ function registerSemanticTool(pi, ctx) {
|
|
|
2069
3144
|
req.top_k = params.topK;
|
|
2070
3145
|
const response = await callBridge(bridge, "semantic_search", req);
|
|
2071
3146
|
return textResult(response.text ?? JSON.stringify(response, null, 2));
|
|
3147
|
+
},
|
|
3148
|
+
renderCall(args, theme, context) {
|
|
3149
|
+
return renderSemanticCall(args, theme, context);
|
|
3150
|
+
},
|
|
3151
|
+
renderResult(result, _options, theme, context) {
|
|
3152
|
+
return renderSemanticResult(result, context.args, theme, context);
|
|
2072
3153
|
}
|
|
2073
3154
|
});
|
|
2074
3155
|
}
|
|
@@ -2096,6 +3177,37 @@ var TransformParams = Type12.Object({
|
|
|
2096
3177
|
description: "Validation level (default: syntax)"
|
|
2097
3178
|
}))
|
|
2098
3179
|
});
|
|
3180
|
+
function buildTransformSections(args, payload, theme) {
|
|
3181
|
+
const response = asRecord2(payload);
|
|
3182
|
+
if (!response)
|
|
3183
|
+
return [theme.fg("muted", "No transform result.")];
|
|
3184
|
+
if (response.dry_run === true) {
|
|
3185
|
+
return [
|
|
3186
|
+
theme.fg("warning", `[dry run] ${args.op}`),
|
|
3187
|
+
asString(response.diff) ?? theme.fg("muted", "No diff available.")
|
|
3188
|
+
];
|
|
3189
|
+
}
|
|
3190
|
+
const target = asString(response.target) ?? asString(response.scope) ?? args.target ?? args.container ?? args.field ?? args.filePath;
|
|
3191
|
+
return [
|
|
3192
|
+
`${theme.fg("success", "transformed")} ${theme.fg("accent", args.op)}`,
|
|
3193
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
3194
|
+
target ? `${theme.fg("muted", "target")} ${target}` : theme.fg("muted", "No target metadata.")
|
|
3195
|
+
];
|
|
3196
|
+
}
|
|
3197
|
+
function renderTransformCall(args, theme, context) {
|
|
3198
|
+
const target = args.target ?? args.container ?? args.field;
|
|
3199
|
+
const summary = [
|
|
3200
|
+
theme.fg("accent", args.op),
|
|
3201
|
+
accentPath(theme, args.filePath),
|
|
3202
|
+
target ? theme.fg("toolOutput", target) : undefined
|
|
3203
|
+
].filter(Boolean).join(" ");
|
|
3204
|
+
return renderToolCall("transform", summary, theme, context);
|
|
3205
|
+
}
|
|
3206
|
+
function renderTransformResult(result, args, theme, context) {
|
|
3207
|
+
if (context.isError)
|
|
3208
|
+
return renderErrorResult(result, "transform failed", theme, context);
|
|
3209
|
+
return renderSections(buildTransformSections(args, extractStructuredPayload(result), theme), context);
|
|
3210
|
+
}
|
|
2099
3211
|
function registerStructureTool(pi, ctx) {
|
|
2100
3212
|
pi.registerTool({
|
|
2101
3213
|
name: "aft_transform",
|
|
@@ -2131,6 +3243,12 @@ function registerStructureTool(pi, ctx) {
|
|
|
2131
3243
|
req.validate = params.validate;
|
|
2132
3244
|
const response = await callBridge(bridge, params.op, req);
|
|
2133
3245
|
return textResult(JSON.stringify(response, null, 2));
|
|
3246
|
+
},
|
|
3247
|
+
renderCall(args, theme, context) {
|
|
3248
|
+
return renderTransformCall(args, theme, context);
|
|
3249
|
+
},
|
|
3250
|
+
renderResult(result, _options, theme, context) {
|
|
3251
|
+
return renderTransformResult(result, context.args, theme, context);
|
|
2134
3252
|
}
|
|
2135
3253
|
});
|
|
2136
3254
|
}
|
|
@@ -2145,7 +3263,7 @@ var PLUGIN_VERSION = (() => {
|
|
|
2145
3263
|
}
|
|
2146
3264
|
})();
|
|
2147
3265
|
function resolveStorageDir() {
|
|
2148
|
-
return join8(
|
|
3266
|
+
return join8(homedir7(), ".pi", "agent", "aft");
|
|
2149
3267
|
}
|
|
2150
3268
|
function resolveToolSurface(config) {
|
|
2151
3269
|
const surface = config.tool_surface ?? "recommended";
|