@pikaa-ai/pikaa 0.3.28 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pikaa.js +5 -2
- package/dist/cli.js +972 -179
- package/dist/index.js +956 -168
- package/package.json +9 -9
package/dist/cli.js
CHANGED
|
@@ -244,6 +244,7 @@ class DefaultModelClientSession {
|
|
|
244
244
|
systemMessage.cache_control = { type: "ephemeral" };
|
|
245
245
|
}
|
|
246
246
|
const messages = [systemMessage];
|
|
247
|
+
const declaredToolCallIds = new Set;
|
|
247
248
|
for (let i = 0;i < params.history.length; i++) {
|
|
248
249
|
const item = params.history[i];
|
|
249
250
|
if (item.type === "user_message") {
|
|
@@ -268,6 +269,9 @@ class DefaultModelClientSession {
|
|
|
268
269
|
j++;
|
|
269
270
|
}
|
|
270
271
|
if (toolCalls.length > 0) {
|
|
272
|
+
for (const tc of toolCalls) {
|
|
273
|
+
declaredToolCallIds.add(tc.id);
|
|
274
|
+
}
|
|
271
275
|
messages.push({
|
|
272
276
|
role: "assistant",
|
|
273
277
|
content: cleanedContent || null,
|
|
@@ -304,6 +308,9 @@ class DefaultModelClientSession {
|
|
|
304
308
|
});
|
|
305
309
|
j++;
|
|
306
310
|
}
|
|
311
|
+
for (const tc of toolCalls) {
|
|
312
|
+
declaredToolCallIds.add(tc.id);
|
|
313
|
+
}
|
|
307
314
|
messages.push({
|
|
308
315
|
role: "assistant",
|
|
309
316
|
content: null,
|
|
@@ -311,11 +318,13 @@ class DefaultModelClientSession {
|
|
|
311
318
|
});
|
|
312
319
|
i = j - 1;
|
|
313
320
|
} else if (item.type === "function_call_output") {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
if (declaredToolCallIds.has(item.callId)) {
|
|
322
|
+
messages.push({
|
|
323
|
+
role: "tool",
|
|
324
|
+
tool_call_id: item.callId,
|
|
325
|
+
content: item.output
|
|
326
|
+
});
|
|
327
|
+
}
|
|
319
328
|
}
|
|
320
329
|
}
|
|
321
330
|
const toolsPayload = typeof params.tools?.toModelToolsSchema === "function" ? params.tools.toModelToolsSchema() : typeof params.tools?.toOpenAISpec === "function" ? params.tools.toOpenAISpec() : Array.isArray(params.tools) ? params.tools : [];
|
|
@@ -705,14 +714,39 @@ function compactHistory(history, retainedRecentItems = 6) {
|
|
|
705
714
|
if (history.length <= retainedRecentItems) {
|
|
706
715
|
return [...history];
|
|
707
716
|
}
|
|
708
|
-
const
|
|
709
|
-
|
|
717
|
+
const targetCutoff = history.length - retainedRecentItems;
|
|
718
|
+
let bestCutoff = -1;
|
|
719
|
+
for (let i = targetCutoff;i >= 0; i--) {
|
|
720
|
+
if (history[i]?.type === "user_message") {
|
|
721
|
+
bestCutoff = i;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (bestCutoff <= 0) {
|
|
726
|
+
for (let i = targetCutoff + 1;i < history.length; i++) {
|
|
727
|
+
if (history[i]?.type === "user_message") {
|
|
728
|
+
bestCutoff = i;
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
let cutoffIndex = bestCutoff > 0 ? bestCutoff : targetCutoff;
|
|
734
|
+
while (cutoffIndex < history.length && history[cutoffIndex]?.type === "function_call_output") {
|
|
735
|
+
cutoffIndex++;
|
|
736
|
+
}
|
|
737
|
+
if (cutoffIndex <= 0 || cutoffIndex >= history.length) {
|
|
738
|
+
return [...history];
|
|
739
|
+
}
|
|
740
|
+
const itemsToCompact = history.slice(0, cutoffIndex);
|
|
741
|
+
const recentItems = history.slice(cutoffIndex);
|
|
710
742
|
const summaryParts = ["### Summary of previous conversation context:"];
|
|
711
743
|
for (const item of itemsToCompact) {
|
|
712
744
|
if (item.type === "user_message") {
|
|
713
745
|
summaryParts.push(`- User: ${item.content.slice(0, 200)}`);
|
|
714
746
|
} else if (item.type === "function_call") {
|
|
715
747
|
summaryParts.push(`- Executed tool: ${item.name}`);
|
|
748
|
+
} else if (item.type === "function_call_output") {
|
|
749
|
+
summaryParts.push(` Tool output: ${item.output.slice(0, 100)}`);
|
|
716
750
|
} else if (item.type === "agent_message") {
|
|
717
751
|
summaryParts.push(`- Assistant: ${item.content.slice(0, 200)}`);
|
|
718
752
|
}
|
|
@@ -1110,7 +1144,7 @@ class EphemeralWorkspaceManager {
|
|
|
1110
1144
|
}
|
|
1111
1145
|
var globalEphemeralWorkspace = new EphemeralWorkspaceManager;
|
|
1112
1146
|
// src/verification/verifier.ts
|
|
1113
|
-
import { spawnSync } from "child_process";
|
|
1147
|
+
import { spawn, spawnSync } from "child_process";
|
|
1114
1148
|
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1115
1149
|
import { join as join6 } from "path";
|
|
1116
1150
|
|
|
@@ -1487,36 +1521,120 @@ class AutoVerifier {
|
|
|
1487
1521
|
this.customCommand = options.customCommand;
|
|
1488
1522
|
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
1489
1523
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1524
|
+
findTargetedTests(modifiedFiles = []) {
|
|
1525
|
+
const matchedTests = new Set;
|
|
1526
|
+
for (const rawFile of modifiedFiles) {
|
|
1527
|
+
const normalized = rawFile.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1528
|
+
if (/\.(test|spec)\.[jt]sx?$/i.test(normalized) || /(^|\/)test_[^/]+\.py$/i.test(normalized) || /_test\.py$/i.test(normalized) || /_test\.go$/i.test(normalized)) {
|
|
1529
|
+
if (existsSync7(join6(this.cwd, normalized))) {
|
|
1530
|
+
matchedTests.add(normalized);
|
|
1531
|
+
}
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const extMatch = normalized.match(/\.[^.]+$/);
|
|
1535
|
+
if (!extMatch)
|
|
1536
|
+
continue;
|
|
1537
|
+
const withoutExt = normalized.slice(0, -extMatch[0].length);
|
|
1538
|
+
const parts = withoutExt.split("/");
|
|
1539
|
+
const baseName = parts[parts.length - 1];
|
|
1540
|
+
const subPath = normalized.startsWith("src/") ? normalized.slice(4, -extMatch[0].length) : normalized.startsWith("lib/") ? normalized.slice(4, -extMatch[0].length) : withoutExt;
|
|
1541
|
+
const candidatePaths = [
|
|
1542
|
+
`tests/${subPath}.test.ts`,
|
|
1543
|
+
`tests/${subPath}.test.js`,
|
|
1544
|
+
`tests/${subPath}.test.tsx`,
|
|
1545
|
+
`tests/${subPath}.spec.ts`,
|
|
1546
|
+
`tests/${subPath}.spec.js`,
|
|
1547
|
+
`tests/${baseName}.test.ts`,
|
|
1548
|
+
`tests/${baseName}.test.js`,
|
|
1549
|
+
`tests/${baseName}.spec.ts`,
|
|
1550
|
+
`test/${subPath}.test.ts`,
|
|
1551
|
+
`test/${subPath}.test.js`,
|
|
1552
|
+
`test/${baseName}.test.ts`,
|
|
1553
|
+
`test/${baseName}.test.js`,
|
|
1554
|
+
`src/${subPath}.test.ts`,
|
|
1555
|
+
`src/${subPath}.spec.ts`,
|
|
1556
|
+
`${withoutExt}.test.ts`,
|
|
1557
|
+
`${withoutExt}.spec.ts`,
|
|
1558
|
+
`tests/test_${baseName}.py`,
|
|
1559
|
+
`test_${baseName}.py`,
|
|
1560
|
+
`tests/${baseName}_test.go`
|
|
1561
|
+
];
|
|
1562
|
+
for (const candidate of candidatePaths) {
|
|
1563
|
+
if (existsSync7(join6(this.cwd, candidate))) {
|
|
1564
|
+
matchedTests.add(candidate);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return Array.from(matchedTests);
|
|
1569
|
+
}
|
|
1570
|
+
resolveTargetedTestCommand(testFiles) {
|
|
1571
|
+
if (testFiles.length === 0)
|
|
1572
|
+
return null;
|
|
1573
|
+
const quotedFiles = testFiles.map((f) => f.includes(" ") ? `"${f}"` : f).join(" ");
|
|
1574
|
+
if (existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock"))) {
|
|
1575
|
+
return `bun test ${quotedFiles}`;
|
|
1576
|
+
}
|
|
1577
|
+
const pkgPath = join6(this.cwd, "package.json");
|
|
1578
|
+
if (existsSync7(pkgPath)) {
|
|
1579
|
+
try {
|
|
1580
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1581
|
+
const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
1582
|
+
if (allDeps.vitest) {
|
|
1583
|
+
return `npx vitest run ${quotedFiles}`;
|
|
1584
|
+
}
|
|
1585
|
+
if (allDeps.jest) {
|
|
1586
|
+
return `npx jest ${quotedFiles}`;
|
|
1587
|
+
}
|
|
1588
|
+
const pm = existsSync7(join6(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync7(join6(this.cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
1589
|
+
if (pkg.scripts?.test) {
|
|
1590
|
+
if (pkg.scripts.test.includes("bun test")) {
|
|
1591
|
+
return `bun test ${quotedFiles}`;
|
|
1592
|
+
}
|
|
1593
|
+
return `${pm} test -- ${quotedFiles}`;
|
|
1594
|
+
}
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
console.warn(`[AutoVerifier] Failed to parse package.json for test runner:`, err);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (existsSync7(join6(this.cwd, "pyproject.toml")) || existsSync7(join6(this.cwd, "requirements.txt"))) {
|
|
1600
|
+
if (existsSync7(join6(this.cwd, "uv.lock"))) {
|
|
1601
|
+
return `uv run pytest ${quotedFiles}`;
|
|
1602
|
+
}
|
|
1603
|
+
return `pytest ${quotedFiles}`;
|
|
1493
1604
|
}
|
|
1605
|
+
if (existsSync7(join6(this.cwd, "go.mod"))) {
|
|
1606
|
+
return `go test ${quotedFiles}`;
|
|
1607
|
+
}
|
|
1608
|
+
if (existsSync7(join6(this.cwd, "Cargo.toml"))) {
|
|
1609
|
+
return `cargo test ${quotedFiles}`;
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
resolveStaticCommand() {
|
|
1494
1614
|
try {
|
|
1495
1615
|
const analyzer = new ProjectAnalyzer(this.cwd);
|
|
1496
1616
|
const analysis = analyzer.analyze();
|
|
1497
1617
|
if (analysis.commands.typecheck) {
|
|
1498
1618
|
return analysis.commands.typecheck;
|
|
1499
1619
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
if (analysis.commands.test) {
|
|
1504
|
-
return analysis.commands.test;
|
|
1505
|
-
}
|
|
1506
|
-
} catch {}
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
console.warn(`[AutoVerifier] ProjectAnalyzer analysis error:`, err);
|
|
1622
|
+
}
|
|
1507
1623
|
const pkgPath = join6(this.cwd, "package.json");
|
|
1508
1624
|
if (existsSync7(pkgPath)) {
|
|
1509
1625
|
try {
|
|
1510
1626
|
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1511
1627
|
if (pkg.scripts) {
|
|
1628
|
+
const pm = existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock")) ? "bun" : existsSync7(join6(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync7(join6(this.cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
1629
|
+
const runCmd = pm === "bun" || pm === "pnpm" || pm === "yarn" ? `${pm} run` : "npm run";
|
|
1512
1630
|
if (pkg.scripts.typecheck)
|
|
1513
|
-
return
|
|
1631
|
+
return `${runCmd} typecheck`;
|
|
1514
1632
|
if (pkg.scripts.check)
|
|
1515
|
-
return
|
|
1516
|
-
if (pkg.scripts.test)
|
|
1517
|
-
return "npm test";
|
|
1633
|
+
return `${runCmd} check`;
|
|
1518
1634
|
}
|
|
1519
|
-
} catch {
|
|
1635
|
+
} catch (err) {
|
|
1636
|
+
console.warn(`[AutoVerifier] Failed to parse package.json for static check:`, err);
|
|
1637
|
+
}
|
|
1520
1638
|
}
|
|
1521
1639
|
if (existsSync7(join6(this.cwd, "tsconfig.json"))) {
|
|
1522
1640
|
return "npx tsc --noEmit";
|
|
@@ -1527,17 +1645,76 @@ class AutoVerifier {
|
|
|
1527
1645
|
if (existsSync7(join6(this.cwd, "go.mod"))) {
|
|
1528
1646
|
return "go vet ./...";
|
|
1529
1647
|
}
|
|
1530
|
-
if (existsSync7(join6(this.cwd, "
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1648
|
+
if (existsSync7(join6(this.cwd, "mypy.ini")) || existsSync7(join6(this.cwd, ".mypy.ini"))) {
|
|
1649
|
+
return "mypy .";
|
|
1650
|
+
}
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
resolveVerificationCommand(modifiedFiles = []) {
|
|
1654
|
+
if (this.customCommand && this.customCommand.trim()) {
|
|
1655
|
+
return this.customCommand.trim();
|
|
1656
|
+
}
|
|
1657
|
+
const targetedTests = this.findTargetedTests(modifiedFiles);
|
|
1658
|
+
const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
|
|
1659
|
+
const staticCmd = this.resolveStaticCommand();
|
|
1660
|
+
if (targetedTestCmd && staticCmd) {
|
|
1661
|
+
return `${staticCmd} && ${targetedTestCmd}`;
|
|
1662
|
+
}
|
|
1663
|
+
if (targetedTestCmd) {
|
|
1664
|
+
return targetedTestCmd;
|
|
1665
|
+
}
|
|
1666
|
+
if (staticCmd) {
|
|
1667
|
+
return staticCmd;
|
|
1668
|
+
}
|
|
1669
|
+
try {
|
|
1670
|
+
const analyzer = new ProjectAnalyzer(this.cwd);
|
|
1671
|
+
const analysis = analyzer.analyze();
|
|
1672
|
+
if (analysis.commands.test)
|
|
1673
|
+
return analysis.commands.test;
|
|
1674
|
+
if (analysis.commands.lint)
|
|
1675
|
+
return analysis.commands.lint;
|
|
1676
|
+
} catch {}
|
|
1677
|
+
const pkgPath = join6(this.cwd, "package.json");
|
|
1678
|
+
if (existsSync7(pkgPath)) {
|
|
1679
|
+
try {
|
|
1680
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
|
|
1681
|
+
if (pkg.scripts?.test) {
|
|
1682
|
+
const pm = existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock")) ? "bun" : "npm";
|
|
1683
|
+
return pm === "bun" ? "bun test" : "npm test";
|
|
1684
|
+
}
|
|
1685
|
+
if (pkg.scripts?.lint) {
|
|
1686
|
+
return "npm run lint";
|
|
1687
|
+
}
|
|
1688
|
+
} catch {}
|
|
1534
1689
|
}
|
|
1535
1690
|
return null;
|
|
1536
1691
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1692
|
+
resolveVerificationStages(modifiedFiles = []) {
|
|
1693
|
+
if (this.customCommand && this.customCommand.trim()) {
|
|
1694
|
+
return [this.customCommand.trim()];
|
|
1695
|
+
}
|
|
1696
|
+
const stages = [];
|
|
1697
|
+
const staticCmd = this.resolveStaticCommand();
|
|
1698
|
+
const targetedTests = this.findTargetedTests(modifiedFiles);
|
|
1699
|
+
const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
|
|
1700
|
+
if (staticCmd) {
|
|
1701
|
+
stages.push(staticCmd);
|
|
1702
|
+
}
|
|
1703
|
+
if (targetedTestCmd) {
|
|
1704
|
+
stages.push(targetedTestCmd);
|
|
1705
|
+
}
|
|
1706
|
+
if (stages.length === 0) {
|
|
1707
|
+
const fallback = this.resolveVerificationCommand(modifiedFiles);
|
|
1708
|
+
if (fallback) {
|
|
1709
|
+
stages.push(fallback);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return stages;
|
|
1713
|
+
}
|
|
1714
|
+
async verify(modifiedFiles = [], signal) {
|
|
1715
|
+
const stages = this.resolveVerificationStages(modifiedFiles);
|
|
1539
1716
|
const startTime = performance.now();
|
|
1540
|
-
if (
|
|
1717
|
+
if (stages.length === 0) {
|
|
1541
1718
|
return {
|
|
1542
1719
|
command: "none",
|
|
1543
1720
|
success: true,
|
|
@@ -1547,59 +1724,158 @@ class AutoVerifier {
|
|
|
1547
1724
|
reason: "NO_VERIFIER_DETECTED"
|
|
1548
1725
|
};
|
|
1549
1726
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1727
|
+
const combinedOutputs = [];
|
|
1728
|
+
let executedCommands = [];
|
|
1729
|
+
for (const cmd of stages) {
|
|
1730
|
+
if (signal?.aborted) {
|
|
1731
|
+
return {
|
|
1732
|
+
command: cmd,
|
|
1733
|
+
success: false,
|
|
1734
|
+
exitCode: 1,
|
|
1735
|
+
output: "Verification was aborted.",
|
|
1736
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
1737
|
+
reason: "ABORTED"
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
executedCommands.push(cmd);
|
|
1741
|
+
const stageResult = await this.executeCommandAsync(cmd, signal);
|
|
1742
|
+
combinedOutputs.push(`[${cmd}]
|
|
1743
|
+
${stageResult.output}`);
|
|
1744
|
+
if (!stageResult.success) {
|
|
1745
|
+
return {
|
|
1746
|
+
command: cmd,
|
|
1747
|
+
success: false,
|
|
1748
|
+
exitCode: stageResult.exitCode,
|
|
1749
|
+
output: this.truncateOutput(combinedOutputs.join(`
|
|
1750
|
+
|
|
1751
|
+
`)),
|
|
1752
|
+
durationMs: Math.round(performance.now() - startTime)
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return {
|
|
1757
|
+
command: executedCommands.join(" && "),
|
|
1758
|
+
success: true,
|
|
1759
|
+
exitCode: 0,
|
|
1760
|
+
output: this.truncateOutput(combinedOutputs.join(`
|
|
1761
|
+
|
|
1762
|
+
`)),
|
|
1763
|
+
durationMs: Math.round(performance.now() - startTime)
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
executeCommandAsync(command, signal) {
|
|
1767
|
+
return new Promise((resolve) => {
|
|
1768
|
+
let proc = null;
|
|
1769
|
+
let stdoutData = "";
|
|
1770
|
+
let stderrData = "";
|
|
1771
|
+
let isSettled = false;
|
|
1772
|
+
const finish = (success, exitCode, output) => {
|
|
1773
|
+
if (isSettled)
|
|
1774
|
+
return;
|
|
1775
|
+
isSettled = true;
|
|
1776
|
+
cleanup();
|
|
1777
|
+
resolve({ success, exitCode, output: output.trim() });
|
|
1778
|
+
};
|
|
1779
|
+
const killProcessTree = () => {
|
|
1780
|
+
if (!proc || !proc.pid)
|
|
1781
|
+
return;
|
|
1782
|
+
try {
|
|
1783
|
+
if (process.platform === "win32") {
|
|
1784
|
+
spawnSync("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
|
|
1785
|
+
} else {
|
|
1786
|
+
proc.kill("SIGTERM");
|
|
1787
|
+
setTimeout(() => {
|
|
1788
|
+
try {
|
|
1789
|
+
proc?.kill("SIGKILL");
|
|
1790
|
+
} catch {}
|
|
1791
|
+
}, 500);
|
|
1792
|
+
}
|
|
1793
|
+
} catch {}
|
|
1794
|
+
};
|
|
1795
|
+
const onAbort = () => {
|
|
1796
|
+
killProcessTree();
|
|
1797
|
+
finish(false, 1, "Verification command aborted by user or session signal.");
|
|
1798
|
+
};
|
|
1799
|
+
if (signal?.aborted) {
|
|
1800
|
+
return finish(false, 1, "Verification aborted before launch.");
|
|
1801
|
+
}
|
|
1802
|
+
if (signal) {
|
|
1803
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1804
|
+
}
|
|
1805
|
+
const timer = setTimeout(() => {
|
|
1806
|
+
killProcessTree();
|
|
1807
|
+
const partial = (stdoutData + `
|
|
1808
|
+
` + stderrData).trim();
|
|
1809
|
+
const timeoutMsg = `[Verification Timeout Error]: Verification command '${command}' exceeded timeout limit of ${this.timeoutMs}ms and was terminated.`;
|
|
1810
|
+
finish(false, 1, partial ? `${partial}
|
|
1811
|
+
|
|
1812
|
+
${timeoutMsg}` : timeoutMsg);
|
|
1813
|
+
}, this.timeoutMs);
|
|
1814
|
+
const cleanup = () => {
|
|
1815
|
+
clearTimeout(timer);
|
|
1816
|
+
if (signal) {
|
|
1817
|
+
signal.removeEventListener("abort", onAbort);
|
|
1562
1818
|
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1819
|
+
};
|
|
1820
|
+
try {
|
|
1821
|
+
proc = spawn(command, {
|
|
1822
|
+
cwd: this.cwd,
|
|
1823
|
+
shell: true,
|
|
1824
|
+
env: {
|
|
1825
|
+
...process.env,
|
|
1826
|
+
CI: "true",
|
|
1827
|
+
FORCE_COLOR: "0"
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1831
|
+
stdoutData += chunk.toString();
|
|
1832
|
+
});
|
|
1833
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1834
|
+
stderrData += chunk.toString();
|
|
1835
|
+
});
|
|
1836
|
+
proc.on("error", (err) => {
|
|
1837
|
+
const partial = (stdoutData + `
|
|
1838
|
+
` + stderrData).trim();
|
|
1839
|
+
const spawnMsg = `[Verification Process Spawn Error]: Failed to spawn command '${command}': ${err.message}`;
|
|
1840
|
+
finish(false, 1, partial ? `${partial}
|
|
1841
|
+
|
|
1842
|
+
${spawnMsg}` : spawnMsg);
|
|
1843
|
+
});
|
|
1844
|
+
proc.on("close", (code) => {
|
|
1845
|
+
const exitCode = code ?? 0;
|
|
1846
|
+
const combined = (stdoutData + `
|
|
1847
|
+
` + stderrData).trim();
|
|
1848
|
+
if (exitCode === 0) {
|
|
1849
|
+
finish(true, 0, combined || "Verification passed cleanly.");
|
|
1850
|
+
} else {
|
|
1851
|
+
const errorFallback = `[Verification Failure]: Command '${command}' exited with code ${exitCode} and produced no output.`;
|
|
1852
|
+
finish(false, exitCode, combined ? `${combined}
|
|
1853
|
+
|
|
1854
|
+
[Process exited with non-zero code ${exitCode}]` : errorFallback);
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
finish(false, 1, `[Verification Execution Exception]: ${err.message || String(err)}`);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
truncateOutput(output) {
|
|
1863
|
+
if (output.length > 4000) {
|
|
1864
|
+
const lines = output.split(`
|
|
1571
1865
|
`);
|
|
1572
|
-
|
|
1573
|
-
|
|
1866
|
+
if (lines.length > 70) {
|
|
1867
|
+
const head = lines.slice(0, 35).join(`
|
|
1574
1868
|
`);
|
|
1575
|
-
|
|
1869
|
+
const tail = lines.slice(-30).join(`
|
|
1576
1870
|
`);
|
|
1577
|
-
|
|
1871
|
+
return `${head}
|
|
1578
1872
|
|
|
1579
|
-
... [${lines.length -
|
|
1873
|
+
... [${lines.length - 65} lines truncated for context efficiency] ...
|
|
1580
1874
|
|
|
1581
1875
|
${tail}`;
|
|
1582
|
-
}
|
|
1583
1876
|
}
|
|
1584
|
-
const exitCode = proc.status ?? (proc.error ? 1 : 0);
|
|
1585
|
-
const success = exitCode === 0;
|
|
1586
|
-
return {
|
|
1587
|
-
command,
|
|
1588
|
-
success,
|
|
1589
|
-
exitCode,
|
|
1590
|
-
output: combined || (success ? "Verification succeeded cleanly." : "Command failed with empty output."),
|
|
1591
|
-
durationMs
|
|
1592
|
-
};
|
|
1593
|
-
} catch (err) {
|
|
1594
|
-
const durationMs = Math.round(performance.now() - startTime);
|
|
1595
|
-
return {
|
|
1596
|
-
command,
|
|
1597
|
-
success: false,
|
|
1598
|
-
exitCode: 1,
|
|
1599
|
-
output: `Verification execution error: ${err.message || String(err)}`,
|
|
1600
|
-
durationMs
|
|
1601
|
-
};
|
|
1602
1877
|
}
|
|
1878
|
+
return output;
|
|
1603
1879
|
}
|
|
1604
1880
|
}
|
|
1605
1881
|
// src/session/turn.ts
|
|
@@ -1723,6 +1999,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
1723
1999
|
});
|
|
1724
2000
|
}
|
|
1725
2001
|
if (toolCallRequests.length > 0) {
|
|
2002
|
+
const validCalls = [];
|
|
1726
2003
|
for (const toolCall of toolCallRequests) {
|
|
1727
2004
|
if (!toolCall.name || !toolCall.name.trim())
|
|
1728
2005
|
continue;
|
|
@@ -1746,6 +2023,14 @@ async function runTurn(session, turnContext, input) {
|
|
|
1746
2023
|
toolName: toolCall.name,
|
|
1747
2024
|
arguments: toolCall.arguments
|
|
1748
2025
|
});
|
|
2026
|
+
validCalls.push({
|
|
2027
|
+
callId: toolCall.callId,
|
|
2028
|
+
name: toolCall.name,
|
|
2029
|
+
arguments: toolCall.arguments,
|
|
2030
|
+
functionCallItem
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
for (const toolCall of validCalls) {
|
|
1749
2034
|
const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
|
|
1750
2035
|
cwd: turnContext.environment.cwd,
|
|
1751
2036
|
turnId,
|
|
@@ -1753,6 +2038,12 @@ async function runTurn(session, turnContext, input) {
|
|
|
1753
2038
|
execPolicy: session.execPolicy,
|
|
1754
2039
|
mode: session.collaborationMode,
|
|
1755
2040
|
permissionMode: session.permissionMode,
|
|
2041
|
+
onFileModified: (p) => {
|
|
2042
|
+
if (p) {
|
|
2043
|
+
modifiedFiles.add(p);
|
|
2044
|
+
hasRunVerification = false;
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
1756
2047
|
onPlanUpdate: (plan, explanation) => {
|
|
1757
2048
|
session.emitEvent({
|
|
1758
2049
|
type: "PlanUpdated",
|
|
@@ -1768,7 +2059,8 @@ async function runTurn(session, turnContext, input) {
|
|
|
1768
2059
|
turnId,
|
|
1769
2060
|
toolName: toolCall.name,
|
|
1770
2061
|
description,
|
|
1771
|
-
command
|
|
2062
|
+
command,
|
|
2063
|
+
prefixRule
|
|
1772
2064
|
});
|
|
1773
2065
|
},
|
|
1774
2066
|
requestInput: async (question, options) => {
|
|
@@ -1819,7 +2111,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
1819
2111
|
cwd,
|
|
1820
2112
|
customCommand: session.autoVerificationCommand
|
|
1821
2113
|
});
|
|
1822
|
-
const command = verifier.resolveVerificationCommand();
|
|
2114
|
+
const command = verifier.resolveVerificationCommand(Array.from(modifiedFiles));
|
|
1823
2115
|
if (command) {
|
|
1824
2116
|
session.emitEvent({
|
|
1825
2117
|
type: "VerificationStarted",
|
|
@@ -1827,7 +2119,7 @@ async function runTurn(session, turnContext, input) {
|
|
|
1827
2119
|
command,
|
|
1828
2120
|
modifiedFiles: Array.from(modifiedFiles)
|
|
1829
2121
|
});
|
|
1830
|
-
const vResult = verifier.verify(Array.from(modifiedFiles));
|
|
2122
|
+
const vResult = await verifier.verify(Array.from(modifiedFiles), signal);
|
|
1831
2123
|
session.emitEvent({
|
|
1832
2124
|
type: "VerificationCompleted",
|
|
1833
2125
|
turnId,
|
|
@@ -1990,8 +2282,133 @@ async function submissionLoop(session, queue) {
|
|
|
1990
2282
|
}
|
|
1991
2283
|
}
|
|
1992
2284
|
|
|
2285
|
+
// src/security/shell-parser.ts
|
|
2286
|
+
function parseShellCommand(commandLine) {
|
|
2287
|
+
const commands = [];
|
|
2288
|
+
const subshellCommands = [];
|
|
2289
|
+
let hasPipes = false;
|
|
2290
|
+
let currentSegment = "";
|
|
2291
|
+
let inSingleQuote = false;
|
|
2292
|
+
let inDoubleQuote = false;
|
|
2293
|
+
let isEscaped = false;
|
|
2294
|
+
const len = commandLine.length;
|
|
2295
|
+
for (let i = 0;i < len; i++) {
|
|
2296
|
+
const char = commandLine[i];
|
|
2297
|
+
if (isEscaped) {
|
|
2298
|
+
currentSegment += char;
|
|
2299
|
+
isEscaped = false;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
if (char === "\\") {
|
|
2303
|
+
isEscaped = true;
|
|
2304
|
+
currentSegment += char;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
if (char === "'" && !inDoubleQuote) {
|
|
2308
|
+
inSingleQuote = !inSingleQuote;
|
|
2309
|
+
currentSegment += char;
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
if (char === '"' && !inSingleQuote) {
|
|
2313
|
+
inDoubleQuote = !inDoubleQuote;
|
|
2314
|
+
currentSegment += char;
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
if (inSingleQuote) {
|
|
2318
|
+
currentSegment += char;
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
if (char === "`") {
|
|
2322
|
+
let endIdx = -1;
|
|
2323
|
+
for (let j = i + 1;j < len; j++) {
|
|
2324
|
+
if (commandLine[j] === "\\" && j + 1 < len) {
|
|
2325
|
+
j++;
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (commandLine[j] === "`") {
|
|
2329
|
+
endIdx = j;
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (endIdx !== -1) {
|
|
2334
|
+
const innerCmd = commandLine.slice(i + 1, endIdx);
|
|
2335
|
+
if (innerCmd.trim()) {
|
|
2336
|
+
subshellCommands.push(innerCmd.trim());
|
|
2337
|
+
}
|
|
2338
|
+
currentSegment += commandLine.slice(i, endIdx + 1);
|
|
2339
|
+
i = endIdx;
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
if (char === "$" && i + 1 < len && commandLine[i + 1] === "(") {
|
|
2344
|
+
let depth = 1;
|
|
2345
|
+
let endIdx = -1;
|
|
2346
|
+
for (let j = i + 2;j < len; j++) {
|
|
2347
|
+
if (commandLine[j] === "\\" && j + 1 < len) {
|
|
2348
|
+
j++;
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (commandLine[j] === "(")
|
|
2352
|
+
depth++;
|
|
2353
|
+
else if (commandLine[j] === ")") {
|
|
2354
|
+
depth--;
|
|
2355
|
+
if (depth === 0) {
|
|
2356
|
+
endIdx = j;
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
if (endIdx !== -1) {
|
|
2362
|
+
const innerCmd = commandLine.slice(i + 2, endIdx);
|
|
2363
|
+
if (innerCmd.trim()) {
|
|
2364
|
+
subshellCommands.push(innerCmd.trim());
|
|
2365
|
+
}
|
|
2366
|
+
currentSegment += commandLine.slice(i, endIdx + 1);
|
|
2367
|
+
i = endIdx;
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (!inDoubleQuote) {
|
|
2372
|
+
if (char === "&" && commandLine[i + 1] === "&" || char === "|" && commandLine[i + 1] === "|") {
|
|
2373
|
+
if (currentSegment.trim()) {
|
|
2374
|
+
commands.push(currentSegment.trim());
|
|
2375
|
+
}
|
|
2376
|
+
currentSegment = "";
|
|
2377
|
+
i++;
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
if (char === "|") {
|
|
2381
|
+
hasPipes = true;
|
|
2382
|
+
if (currentSegment.trim()) {
|
|
2383
|
+
commands.push(currentSegment.trim());
|
|
2384
|
+
}
|
|
2385
|
+
currentSegment = "";
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
if (char === ";" || char === `
|
|
2389
|
+
` || char === "&") {
|
|
2390
|
+
if (currentSegment.trim()) {
|
|
2391
|
+
commands.push(currentSegment.trim());
|
|
2392
|
+
}
|
|
2393
|
+
currentSegment = "";
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
currentSegment += char;
|
|
2398
|
+
}
|
|
2399
|
+
if (currentSegment.trim()) {
|
|
2400
|
+
commands.push(currentSegment.trim());
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
commands: commands.filter(Boolean),
|
|
2404
|
+
subshellCommands: subshellCommands.filter(Boolean),
|
|
2405
|
+
hasPipes
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
|
|
1993
2409
|
// src/security/exec-policy.ts
|
|
1994
2410
|
class ExecPolicy {
|
|
2411
|
+
denyRules = [];
|
|
1995
2412
|
rules = [];
|
|
1996
2413
|
mode = "auto";
|
|
1997
2414
|
constructor(initialMode = "auto") {
|
|
@@ -2005,12 +2422,24 @@ class ExecPolicy {
|
|
|
2005
2422
|
this.mode = mode;
|
|
2006
2423
|
}
|
|
2007
2424
|
initDefaultRules() {
|
|
2008
|
-
this.
|
|
2009
|
-
this.
|
|
2010
|
-
this.
|
|
2011
|
-
this.addRule(/^(
|
|
2012
|
-
this.addRule(/^(
|
|
2013
|
-
this.addRule(/^(
|
|
2425
|
+
this.addDenyRule(/(?:^|[/\\])(mkfs|format|fdisk|parted)\b/i, "Destructive filesystem formatting operation");
|
|
2426
|
+
this.addDenyRule(/^dd\s+.*(of=\/dev\/|\/dev\/sd|\/dev\/nvme)/i, "Raw block device write attempt");
|
|
2427
|
+
this.addDenyRule(/(?:^|[/\\])(reboot|shutdown|poweroff|init\s+0)\b/i, "System power manipulation");
|
|
2428
|
+
this.addRule(/^(sudo|su|doas|runas)\b/i, "prompt", "Privilege escalation attempt");
|
|
2429
|
+
this.addRule(/^(rm|del|rmdir|shred|unlink)\b/i, "prompt", "Destructive file removal");
|
|
2430
|
+
this.addRule(/^(chmod|chown|kill|pkill|killall|systemctl|service|crontab)\b/i, "prompt", "System administration & process control");
|
|
2431
|
+
this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase|branch\s+-D|checkout\s+-f))\b/i, "prompt", "Destructive git operation");
|
|
2432
|
+
this.addRule(/^(curl|wget|fetch|ssh|scp|sftp|ftp|rsync|nc|ncat|netcat|socat|telnet)\b/i, "prompt", "Network & remote transfer");
|
|
2433
|
+
this.addRule(/^(bash|sh|zsh|dash|ksh|cmd(\.exe)?|powershell(\.exe)?|pwsh)\s+(-c|-command|\/c)\b/i, "prompt", "Arbitrary subshell command execution");
|
|
2434
|
+
this.addRule(/^(python|python3|node|bun|perl|ruby)\s+(-c|-e)\b/i, "prompt", "Inline arbitrary code evaluation");
|
|
2435
|
+
this.addRule(/^(eval|exec)\b/i, "prompt", "Dynamic code execution");
|
|
2436
|
+
this.addRule(/^(npm\s+publish|bun\s+publish|cargo\s+publish)\b/i, "prompt", "Package registry publication");
|
|
2437
|
+
this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote|describe))\b/i, "allow", "Safe git query");
|
|
2438
|
+
this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where|stat|file|du|df)\b/i, "allow", "Safe read-only shell command");
|
|
2439
|
+
this.addRule(/^(bun\s+(test|run|--version|-v)|npm\s+(test|run|--version|-v)|npx\s+(tsc|eslint|oxlint)|tsc|cargo\s+(check|test|build)|go\s+(test|vet|build)|pytest|python\s+-m\s+unittest|node\s+(-v|--version|--test))\b/i, "allow", "Testing, typechecking & build verification");
|
|
2440
|
+
}
|
|
2441
|
+
addDenyRule(pattern, description) {
|
|
2442
|
+
this.denyRules.push({ pattern, decision: "deny", description });
|
|
2014
2443
|
}
|
|
2015
2444
|
addRule(pattern, decision, description) {
|
|
2016
2445
|
this.rules.unshift({ pattern, decision, description });
|
|
@@ -2033,8 +2462,49 @@ class ExecPolicy {
|
|
|
2033
2462
|
}
|
|
2034
2463
|
evaluate(command) {
|
|
2035
2464
|
const trimmed = command.trim();
|
|
2465
|
+
if (!trimmed) {
|
|
2466
|
+
return { decision: "allow" };
|
|
2467
|
+
}
|
|
2468
|
+
const parsed = parseShellCommand(trimmed);
|
|
2469
|
+
const subCommands = [...parsed.commands, ...parsed.subshellCommands].map((c) => c.trim()).filter(Boolean);
|
|
2470
|
+
if (subCommands.length > 1 || parsed.subshellCommands.length > 0) {
|
|
2471
|
+
for (const subCmd of subCommands) {
|
|
2472
|
+
const subResult = this.evaluateSingle(subCmd);
|
|
2473
|
+
if (subResult.decision === "deny") {
|
|
2474
|
+
return {
|
|
2475
|
+
decision: "deny",
|
|
2476
|
+
reason: `Chained command contains denied operation: '${subCmd}' (${subResult.reason || "Forbidden"})`
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
if (subResult.decision === "prompt") {
|
|
2480
|
+
return {
|
|
2481
|
+
decision: "prompt",
|
|
2482
|
+
reason: `Chained command contains operation requiring confirmation: '${subCmd}' (${subResult.reason || "Requires approval"})`
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
if (parsed.hasPipes && /\|\s*(ba|z|k|c)?sh\b/i.test(trimmed)) {
|
|
2487
|
+
return {
|
|
2488
|
+
decision: "prompt",
|
|
2489
|
+
reason: "Pipeline executes piped input directly into shell interpreter (| sh)"
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
return { decision: "allow", reason: "All chained sub-commands are permitted" };
|
|
2493
|
+
}
|
|
2494
|
+
return this.evaluateSingle(trimmed);
|
|
2495
|
+
}
|
|
2496
|
+
evaluateSingle(command) {
|
|
2497
|
+
const trimmed = command.trim();
|
|
2498
|
+
for (const rule of this.denyRules) {
|
|
2499
|
+
if (rule.pattern.test(trimmed)) {
|
|
2500
|
+
return {
|
|
2501
|
+
decision: "deny",
|
|
2502
|
+
reason: rule.description
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2036
2506
|
if (this.mode === "plan") {
|
|
2037
|
-
const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
|
|
2507
|
+
const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
|
|
2038
2508
|
if (isReadOnly) {
|
|
2039
2509
|
return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
|
|
2040
2510
|
}
|
|
@@ -2050,7 +2520,7 @@ class ExecPolicy {
|
|
|
2050
2520
|
};
|
|
2051
2521
|
}
|
|
2052
2522
|
if (this.mode === "accept-edits") {
|
|
2053
|
-
const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
|
|
2523
|
+
const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
|
|
2054
2524
|
if (isReadOnly) {
|
|
2055
2525
|
return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
|
|
2056
2526
|
}
|
|
@@ -2067,11 +2537,21 @@ class ExecPolicy {
|
|
|
2067
2537
|
};
|
|
2068
2538
|
}
|
|
2069
2539
|
}
|
|
2540
|
+
if (this.isRecognizedSafeDevCommand(trimmed)) {
|
|
2541
|
+
return {
|
|
2542
|
+
decision: "allow",
|
|
2543
|
+
reason: "Safe development workspace command"
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
const firstWord = trimmed.split(/\s+/)[0] || trimmed;
|
|
2070
2547
|
return {
|
|
2071
|
-
decision: "
|
|
2072
|
-
reason:
|
|
2548
|
+
decision: "prompt",
|
|
2549
|
+
reason: `Command '${firstWord}' is unclassified and requires confirmation in auto mode`
|
|
2073
2550
|
};
|
|
2074
2551
|
}
|
|
2552
|
+
isRecognizedSafeDevCommand(command) {
|
|
2553
|
+
return /^(git\s+(checkout|add|commit|stash|merge|pull|init)|bun\s+(install|add|remove)|npm\s+(install|i|add|remove)|yarn\s+(add|remove)|pnpm\s+(add|remove|install)|mkdir|touch|cp|copy|mv|move|clear|cls|echo|printf|node|bun|python|python3|cargo|go)\b/i.test(command);
|
|
2554
|
+
}
|
|
2075
2555
|
}
|
|
2076
2556
|
|
|
2077
2557
|
// src/session/session.ts
|
|
@@ -2204,7 +2684,8 @@ class Session {
|
|
|
2204
2684
|
turnId: params.turnId,
|
|
2205
2685
|
toolName: params.toolName,
|
|
2206
2686
|
description: params.description,
|
|
2207
|
-
command: params.command
|
|
2687
|
+
command: params.command,
|
|
2688
|
+
prefixRule: params.prefixRule
|
|
2208
2689
|
});
|
|
2209
2690
|
this.emitEvent({
|
|
2210
2691
|
type: "StatusChanged",
|
|
@@ -2382,6 +2863,7 @@ var applyPatchTool = {
|
|
|
2382
2863
|
try {
|
|
2383
2864
|
mkdirSync4(dirname3(filePath), { recursive: true });
|
|
2384
2865
|
writeFileSync2(filePath, replacementContent, "utf8");
|
|
2866
|
+
ctx.onFileModified?.(rawPath);
|
|
2385
2867
|
return { output: `Successfully created new file '${rawPath}'` };
|
|
2386
2868
|
} catch (err) {
|
|
2387
2869
|
return {
|
|
@@ -2402,7 +2884,31 @@ var applyPatchTool = {
|
|
|
2402
2884
|
isError: true
|
|
2403
2885
|
};
|
|
2404
2886
|
}
|
|
2405
|
-
|
|
2887
|
+
let targetToFind = targetContent;
|
|
2888
|
+
let replacementToUse = replacementContent;
|
|
2889
|
+
const fileHasCrlf = originalFileContent.includes(`\r
|
|
2890
|
+
`);
|
|
2891
|
+
let firstIndex = originalFileContent.indexOf(targetToFind);
|
|
2892
|
+
if (firstIndex === -1 && fileHasCrlf) {
|
|
2893
|
+
const crlfTarget = targetContent.replace(/\r?\n/g, `\r
|
|
2894
|
+
`);
|
|
2895
|
+
firstIndex = originalFileContent.indexOf(crlfTarget);
|
|
2896
|
+
if (firstIndex !== -1) {
|
|
2897
|
+
targetToFind = crlfTarget;
|
|
2898
|
+
replacementToUse = replacementContent.replace(/\r?\n/g, `\r
|
|
2899
|
+
`);
|
|
2900
|
+
}
|
|
2901
|
+
} else if (firstIndex === -1 && !fileHasCrlf && targetContent.includes(`\r
|
|
2902
|
+
`)) {
|
|
2903
|
+
const lfTarget = targetContent.replace(/\r\n/g, `
|
|
2904
|
+
`);
|
|
2905
|
+
firstIndex = originalFileContent.indexOf(lfTarget);
|
|
2906
|
+
if (firstIndex !== -1) {
|
|
2907
|
+
targetToFind = lfTarget;
|
|
2908
|
+
replacementToUse = replacementContent.replace(/\r\n/g, `
|
|
2909
|
+
`);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2406
2912
|
if (firstIndex === -1) {
|
|
2407
2913
|
return {
|
|
2408
2914
|
output: `Error: targetContent was not found in '${rawPath}'.
|
|
@@ -2413,7 +2919,7 @@ var applyPatchTool = {
|
|
|
2413
2919
|
isError: true
|
|
2414
2920
|
};
|
|
2415
2921
|
}
|
|
2416
|
-
const secondIndex = originalFileContent.indexOf(
|
|
2922
|
+
const secondIndex = originalFileContent.indexOf(targetToFind, firstIndex + 1);
|
|
2417
2923
|
if (secondIndex !== -1) {
|
|
2418
2924
|
return {
|
|
2419
2925
|
output: `Error: targetContent matched multiple locations in '${rawPath}'.
|
|
@@ -2424,8 +2930,9 @@ var applyPatchTool = {
|
|
|
2424
2930
|
isError: true
|
|
2425
2931
|
};
|
|
2426
2932
|
}
|
|
2427
|
-
const newFileContent = originalFileContent.slice(0, firstIndex) +
|
|
2933
|
+
const newFileContent = originalFileContent.slice(0, firstIndex) + replacementToUse + originalFileContent.slice(firstIndex + targetToFind.length);
|
|
2428
2934
|
writeFileSync2(filePath, newFileContent, "utf8");
|
|
2935
|
+
ctx.onFileModified?.(rawPath);
|
|
2429
2936
|
return {
|
|
2430
2937
|
output: `Successfully applied patch to '${rawPath}'`
|
|
2431
2938
|
};
|
|
@@ -2537,8 +3044,12 @@ class LinuxSandbox {
|
|
|
2537
3044
|
}
|
|
2538
3045
|
this.hasBwrap = existsSync9("/usr/bin/bwrap") || existsSync9("/bin/bwrap") || existsSync9("/usr/local/bin/bwrap");
|
|
2539
3046
|
}
|
|
2540
|
-
wrapCommand(cmd, profile) {
|
|
2541
|
-
if (
|
|
3047
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
3048
|
+
if (profile.kind === "danger-unrestricted") {
|
|
3049
|
+
return cmd;
|
|
3050
|
+
}
|
|
3051
|
+
if (!this.hasBwrap) {
|
|
3052
|
+
onWarning?.("Linux kernel sandbox (Bubblewrap / bwrap) is not available. Command will execute without OS-level namespace isolation.");
|
|
2542
3053
|
return cmd;
|
|
2543
3054
|
}
|
|
2544
3055
|
const bwrapArgs = [
|
|
@@ -2554,6 +3065,23 @@ class LinuxSandbox {
|
|
|
2554
3065
|
"--tmpfs",
|
|
2555
3066
|
"/tmp"
|
|
2556
3067
|
];
|
|
3068
|
+
const homeDir = process.env.HOME || "/root";
|
|
3069
|
+
const sensitivePaths = [
|
|
3070
|
+
`${homeDir}/.ssh`,
|
|
3071
|
+
`${homeDir}/.aws`,
|
|
3072
|
+
`${homeDir}/.gnupg`,
|
|
3073
|
+
`${homeDir}/.azure`,
|
|
3074
|
+
`${homeDir}/.kube`,
|
|
3075
|
+
`${homeDir}/.docker`,
|
|
3076
|
+
`${homeDir}/.netrc`,
|
|
3077
|
+
"/etc/shadow",
|
|
3078
|
+
"/etc/sudoers"
|
|
3079
|
+
];
|
|
3080
|
+
for (const sensitive of sensitivePaths) {
|
|
3081
|
+
if (existsSync9(sensitive)) {
|
|
3082
|
+
bwrapArgs.push("--tmpfs", sensitive);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
2557
3085
|
for (const writableRoot of profile.writableRoots) {
|
|
2558
3086
|
bwrapArgs.push("--bind", writableRoot, writableRoot);
|
|
2559
3087
|
}
|
|
@@ -2603,11 +3131,21 @@ class MacOSSandbox {
|
|
|
2603
3131
|
} else {
|
|
2604
3132
|
rules.push("(deny network*)");
|
|
2605
3133
|
}
|
|
3134
|
+
const home = process.env.HOME || "/Users/Shared";
|
|
3135
|
+
rules.push(`(deny file-read* (subpath "${home}/.ssh"))`);
|
|
3136
|
+
rules.push(`(deny file-read* (subpath "${home}/.aws"))`);
|
|
3137
|
+
rules.push(`(deny file-read* (subpath "${home}/.gnupg"))`);
|
|
3138
|
+
rules.push(`(deny file-read* (subpath "${home}/.kube"))`);
|
|
3139
|
+
rules.push(`(deny file-read* (subpath "${home}/.docker"))`);
|
|
2606
3140
|
return rules.join(`
|
|
2607
3141
|
`);
|
|
2608
3142
|
}
|
|
2609
|
-
wrapCommand(cmd, profile) {
|
|
2610
|
-
if (
|
|
3143
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
3144
|
+
if (profile.kind === "danger-unrestricted") {
|
|
3145
|
+
return cmd;
|
|
3146
|
+
}
|
|
3147
|
+
if (!this.hasSandboxExec) {
|
|
3148
|
+
onWarning?.("macOS Seatbelt (sandbox-exec) is not available. Command will execute without OS-level namespace isolation.");
|
|
2611
3149
|
return cmd;
|
|
2612
3150
|
}
|
|
2613
3151
|
const policy = this.generateProfile(profile);
|
|
@@ -2640,7 +3178,7 @@ class KernelSandboxManager {
|
|
|
2640
3178
|
isSandboxingActive: this.windowsSandbox.isSupported() || this.linuxSandbox.isSupported() || this.macOsSandbox.isSupported()
|
|
2641
3179
|
};
|
|
2642
3180
|
}
|
|
2643
|
-
buildDefaultProfile(cwd, allowNetwork =
|
|
3181
|
+
buildDefaultProfile(cwd, allowNetwork = false) {
|
|
2644
3182
|
const normCwd = normalize(resolve6(cwd));
|
|
2645
3183
|
return {
|
|
2646
3184
|
kind: "workspace-write",
|
|
@@ -2655,11 +3193,15 @@ class KernelSandboxManager {
|
|
|
2655
3193
|
}
|
|
2656
3194
|
};
|
|
2657
3195
|
}
|
|
2658
|
-
wrapCommand(cmd, profile) {
|
|
3196
|
+
wrapCommand(cmd, profile, onWarning) {
|
|
2659
3197
|
if (process.platform === "linux") {
|
|
2660
|
-
return this.linuxSandbox.wrapCommand(cmd, profile);
|
|
3198
|
+
return this.linuxSandbox.wrapCommand(cmd, profile, onWarning);
|
|
2661
3199
|
} else if (process.platform === "darwin") {
|
|
2662
|
-
return this.macOsSandbox.wrapCommand(cmd, profile);
|
|
3200
|
+
return this.macOsSandbox.wrapCommand(cmd, profile, onWarning);
|
|
3201
|
+
} else if (process.platform === "win32") {
|
|
3202
|
+
if (!this.windowsSandbox.isSupported() && profile.kind !== "danger-unrestricted") {
|
|
3203
|
+
onWarning?.("Windows JobObject isolation is not available in this environment. Command will execute without OS-level process limits.");
|
|
3204
|
+
}
|
|
2663
3205
|
}
|
|
2664
3206
|
return cmd;
|
|
2665
3207
|
}
|
|
@@ -2791,6 +3333,12 @@ class PrefixRulesStore {
|
|
|
2791
3333
|
matchesPrefix(cmdTokens, prefixTokens) {
|
|
2792
3334
|
if (prefixTokens.length > cmdTokens.length)
|
|
2793
3335
|
return false;
|
|
3336
|
+
const SHELL_OPERATORS = new Set([";", "&&", "||", "|", "&", ";;", "&|"]);
|
|
3337
|
+
for (const token of cmdTokens) {
|
|
3338
|
+
if (SHELL_OPERATORS.has(token) || token.includes(";") || token.includes("&&") || token.includes("||")) {
|
|
3339
|
+
return false;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
2794
3342
|
for (let i = 0;i < prefixTokens.length; i++) {
|
|
2795
3343
|
const cmd = cmdTokens[i];
|
|
2796
3344
|
const prefix = prefixTokens[i];
|
|
@@ -2846,11 +3394,29 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2846
3394
|
}
|
|
2847
3395
|
const args = rawArgs;
|
|
2848
3396
|
const rulesStore = ctx.prefixRulesStore || globalPrefixRulesStore;
|
|
2849
|
-
const
|
|
3397
|
+
const activePolicy = ctx.execPolicy || policy;
|
|
3398
|
+
const policyDecision = activePolicy.evaluate(command);
|
|
3399
|
+
if (policyDecision.decision === "deny") {
|
|
3400
|
+
return {
|
|
3401
|
+
output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
|
|
3402
|
+
isError: true
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
const parsed = parseShellCommand(command);
|
|
3406
|
+
const isCompound = parsed.commands.length > 1 || parsed.subshellCommands.length > 0 || parsed.hasPipes;
|
|
2850
3407
|
let isEscalated = false;
|
|
2851
|
-
if (
|
|
2852
|
-
|
|
2853
|
-
|
|
3408
|
+
if (!isCompound) {
|
|
3409
|
+
const cmdTokens = command.split(/\s+/).filter(Boolean);
|
|
3410
|
+
if (rulesStore.isApproved(ctx.cwd, cmdTokens)) {
|
|
3411
|
+
isEscalated = true;
|
|
3412
|
+
}
|
|
3413
|
+
} else {
|
|
3414
|
+
const allSubCommands = [...parsed.commands, ...parsed.subshellCommands];
|
|
3415
|
+
if (allSubCommands.length > 0 && allSubCommands.every((sub) => rulesStore.isApproved(ctx.cwd, sub.split(/\s+/).filter(Boolean)))) {
|
|
3416
|
+
isEscalated = true;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
if (!isEscalated && args.sandbox_permissions === "require_escalated") {
|
|
2854
3420
|
if (ctx.requestApproval) {
|
|
2855
3421
|
const promptDesc = args.justification || "Executing command with escalated permissions";
|
|
2856
3422
|
const approvalResult = await ctx.requestApproval(promptDesc, command, args.prefix_rule);
|
|
@@ -2868,16 +3434,8 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2868
3434
|
isEscalated = true;
|
|
2869
3435
|
}
|
|
2870
3436
|
}
|
|
2871
|
-
if (!isEscalated) {
|
|
2872
|
-
|
|
2873
|
-
const policyDecision = activePolicy.evaluate(command);
|
|
2874
|
-
if (policyDecision.decision === "deny") {
|
|
2875
|
-
return {
|
|
2876
|
-
output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
|
|
2877
|
-
isError: true
|
|
2878
|
-
};
|
|
2879
|
-
}
|
|
2880
|
-
if (policyDecision.decision === "prompt" && ctx.requestApproval) {
|
|
3437
|
+
if (policyDecision.decision === "prompt" && !isEscalated) {
|
|
3438
|
+
if (ctx.requestApproval) {
|
|
2881
3439
|
const approvalResult = await ctx.requestApproval(policyDecision.reason || "Executing external command", command);
|
|
2882
3440
|
const isAllowed = typeof approvalResult === "boolean" ? approvalResult : approvalResult?.allowed;
|
|
2883
3441
|
if (!isAllowed) {
|
|
@@ -2886,17 +3444,22 @@ function createShellTool(policy = new ExecPolicy) {
|
|
|
2886
3444
|
isError: true
|
|
2887
3445
|
};
|
|
2888
3446
|
}
|
|
3447
|
+
} else {
|
|
3448
|
+
return {
|
|
3449
|
+
output: `Error: Command execution requires user confirmation: '${command}' (${policyDecision.reason || "Requires approval"})`,
|
|
3450
|
+
isError: true
|
|
3451
|
+
};
|
|
2889
3452
|
}
|
|
2890
3453
|
}
|
|
2891
3454
|
const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : 30000;
|
|
2892
3455
|
const isWindows = process.platform === "win32";
|
|
2893
3456
|
const baseCmd = isWindows ? ["cmd.exe", "/d", "/s", "/c", command] : ["/bin/sh", "-c", command];
|
|
2894
3457
|
const ephemeralScratchpad = globalEphemeralWorkspace.createScratchpad(ctx.turnId);
|
|
2895
|
-
const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd);
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3458
|
+
const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd, isEscalated);
|
|
3459
|
+
let sandboxNotice = null;
|
|
3460
|
+
const wrappedCmd = globalKernelSandbox.wrapCommand(baseCmd, sandboxProfile, (w) => {
|
|
3461
|
+
sandboxNotice = w;
|
|
3462
|
+
});
|
|
2900
3463
|
try {
|
|
2901
3464
|
const proc = Bun.spawn(wrappedCmd, {
|
|
2902
3465
|
cwd: ctx.cwd,
|
|
@@ -2950,10 +3513,38 @@ ${result.stderr.trim()}`);
|
|
|
2950
3513
|
2. If this is a test failure, trace the failure in source code and fix the root cause before re-running.
|
|
2951
3514
|
3. If this is a missing command/module, install or configure the prerequisite.`);
|
|
2952
3515
|
}
|
|
2953
|
-
|
|
3516
|
+
let rawOutput = outputParts.join(`
|
|
2954
3517
|
`) || "[Command completed with no output]";
|
|
3518
|
+
const lines = rawOutput.split(`
|
|
3519
|
+
`);
|
|
3520
|
+
const MAX_LINES = 250;
|
|
3521
|
+
const MAX_CHARS = 30000;
|
|
3522
|
+
if (lines.length > MAX_LINES) {
|
|
3523
|
+
const headLines = lines.slice(0, 125).join(`
|
|
3524
|
+
`);
|
|
3525
|
+
const tailLines = lines.slice(-100).join(`
|
|
3526
|
+
`);
|
|
3527
|
+
rawOutput = `${headLines}
|
|
3528
|
+
|
|
3529
|
+
... [${lines.length - 225} lines truncated for context efficiency] ...
|
|
3530
|
+
|
|
3531
|
+
${tailLines}`;
|
|
3532
|
+
}
|
|
3533
|
+
if (rawOutput.length > MAX_CHARS) {
|
|
3534
|
+
const headChars = rawOutput.slice(0, 16000);
|
|
3535
|
+
const tailChars = rawOutput.slice(-12000);
|
|
3536
|
+
rawOutput = `${headChars}
|
|
3537
|
+
|
|
3538
|
+
... [${rawOutput.length - 28000} characters truncated for context efficiency] ...
|
|
3539
|
+
|
|
3540
|
+
${tailChars}`;
|
|
3541
|
+
}
|
|
3542
|
+
if (sandboxNotice) {
|
|
3543
|
+
rawOutput = `[Sandbox Notice]: ${sandboxNotice}
|
|
3544
|
+
${rawOutput}`;
|
|
3545
|
+
}
|
|
2955
3546
|
return {
|
|
2956
|
-
output,
|
|
3547
|
+
output: rawOutput,
|
|
2957
3548
|
isError: result.code !== 0
|
|
2958
3549
|
};
|
|
2959
3550
|
} catch (err) {
|
|
@@ -2972,7 +3563,7 @@ ${result.stderr.trim()}`);
|
|
|
2972
3563
|
}
|
|
2973
3564
|
var shellTool = createShellTool();
|
|
2974
3565
|
// src/tools/handlers/file-ops.ts
|
|
2975
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, statSync as statSync4, mkdirSync as mkdirSync6 } from "fs";
|
|
3566
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, statSync as statSync4, lstatSync, mkdirSync as mkdirSync6 } from "fs";
|
|
2976
3567
|
import { resolve as resolve8, dirname as dirname5 } from "path";
|
|
2977
3568
|
var DEFAULT_MAX_UNPAGINATED_LINES = 250;
|
|
2978
3569
|
var readFileTool = {
|
|
@@ -3084,6 +3675,7 @@ var viewFileTool = {
|
|
|
3084
3675
|
name: "view_file",
|
|
3085
3676
|
description: "View file content with surgical line-range support. Alias for read_file matching Antigravity & Claude Code conventions."
|
|
3086
3677
|
};
|
|
3678
|
+
var DEFAULT_MAX_DIR_ENTRIES = 500;
|
|
3087
3679
|
var listDirTool = {
|
|
3088
3680
|
name: "list_dir",
|
|
3089
3681
|
description: "List contents of a directory with file names and types.",
|
|
@@ -3100,13 +3692,31 @@ var listDirTool = {
|
|
|
3100
3692
|
}
|
|
3101
3693
|
try {
|
|
3102
3694
|
const entries = readdirSync4(dirPath);
|
|
3103
|
-
const
|
|
3695
|
+
const totalEntries = entries.length;
|
|
3696
|
+
const displayEntries = entries.slice(0, DEFAULT_MAX_DIR_ENTRIES);
|
|
3697
|
+
const formatted = displayEntries.map((entry) => {
|
|
3104
3698
|
const full = resolve8(dirPath, entry);
|
|
3105
|
-
|
|
3699
|
+
let isDir = false;
|
|
3700
|
+
try {
|
|
3701
|
+
isDir = statSync4(full).isDirectory();
|
|
3702
|
+
} catch {
|
|
3703
|
+
try {
|
|
3704
|
+
const lst = lstatSync(full);
|
|
3705
|
+
if (lst.isSymbolicLink())
|
|
3706
|
+
return `[SYMLINK] ${entry}`;
|
|
3707
|
+
} catch {}
|
|
3708
|
+
return `[FILE] ${entry}`;
|
|
3709
|
+
}
|
|
3106
3710
|
return `${isDir ? "[DIR]" : "[FILE]"} ${entry}`;
|
|
3107
3711
|
});
|
|
3108
|
-
|
|
3109
|
-
`) || "[Empty directory]"
|
|
3712
|
+
let output = formatted.join(`
|
|
3713
|
+
`) || "[Empty directory]";
|
|
3714
|
+
if (totalEntries > DEFAULT_MAX_DIR_ENTRIES) {
|
|
3715
|
+
output += `
|
|
3716
|
+
|
|
3717
|
+
... [Truncated: ${totalEntries - DEFAULT_MAX_DIR_ENTRIES} more entries. Showing first ${DEFAULT_MAX_DIR_ENTRIES} of ${totalEntries}]`;
|
|
3718
|
+
}
|
|
3719
|
+
return { output };
|
|
3110
3720
|
} catch (err) {
|
|
3111
3721
|
return { output: `Failed to list directory: ${err instanceof Error ? err.message : String(err)}`, isError: true };
|
|
3112
3722
|
}
|
|
@@ -3147,6 +3757,7 @@ var writeFileTool = {
|
|
|
3147
3757
|
try {
|
|
3148
3758
|
mkdirSync6(dirname5(filePath), { recursive: true });
|
|
3149
3759
|
writeFileSync3(filePath, String(args.content ?? ""), "utf8");
|
|
3760
|
+
ctx.onFileModified?.(rawPath);
|
|
3150
3761
|
return { output: `Successfully wrote to '${args.path}'` };
|
|
3151
3762
|
} catch (err) {
|
|
3152
3763
|
return {
|
|
@@ -3640,6 +4251,12 @@ class CodeModeToolsProxy {
|
|
|
3640
4251
|
if (result.isError) {
|
|
3641
4252
|
throw new Error(`Tool '${tool.name}' failed: ${result.output}`);
|
|
3642
4253
|
}
|
|
4254
|
+
if (normalizedName === "write_file" || normalizedName === "apply_patch" || tool.name === "write_file" || tool.name === "apply_patch") {
|
|
4255
|
+
const pathArg = String(args?.path || "");
|
|
4256
|
+
if (pathArg) {
|
|
4257
|
+
this.context.onFileModified?.(pathArg);
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
3643
4260
|
return result.output;
|
|
3644
4261
|
};
|
|
3645
4262
|
}
|
|
@@ -3713,6 +4330,9 @@ class SandboxedWorkerHost {
|
|
|
3713
4330
|
const fetch = undefined;
|
|
3714
4331
|
const XMLHttpRequest = undefined;
|
|
3715
4332
|
const WebSocket = undefined;
|
|
4333
|
+
const globalThis = Object.freeze(Object.create(null));
|
|
4334
|
+
const global = undefined;
|
|
4335
|
+
const window = undefined;
|
|
3716
4336
|
|
|
3717
4337
|
return (async () => {
|
|
3718
4338
|
${cleanCode}
|
|
@@ -4164,7 +4784,30 @@ class AgentRoleRegistry {
|
|
|
4164
4784
|
name: "default",
|
|
4165
4785
|
description: "Generalist autonomous developer agent capable of coding, testing, and debugging.",
|
|
4166
4786
|
systemPrompt: "You are Groupy, an expert autonomous software engineer. Think carefully, use tools surgically, and verify every step.",
|
|
4167
|
-
nicknameCandidates: ["Pikaa", "Heca", "Bankli", "Moli"]
|
|
4787
|
+
nicknameCandidates: ["Pikaa", "Heca", "Bankli", "Moli"],
|
|
4788
|
+
allowedToolNames: [
|
|
4789
|
+
"read_file",
|
|
4790
|
+
"view_file",
|
|
4791
|
+
"write_file",
|
|
4792
|
+
"apply_patch",
|
|
4793
|
+
"list_dir",
|
|
4794
|
+
"shell",
|
|
4795
|
+
"update_plan",
|
|
4796
|
+
"ask_question",
|
|
4797
|
+
"request_user_input",
|
|
4798
|
+
"remember",
|
|
4799
|
+
"list_memories",
|
|
4800
|
+
"read_memory",
|
|
4801
|
+
"save_memory",
|
|
4802
|
+
"spawn_agent",
|
|
4803
|
+
"wait_agent",
|
|
4804
|
+
"send_input",
|
|
4805
|
+
"close_agent",
|
|
4806
|
+
"list_agents",
|
|
4807
|
+
"create_worktree",
|
|
4808
|
+
"list_worktrees",
|
|
4809
|
+
"merge_worktree"
|
|
4810
|
+
]
|
|
4168
4811
|
});
|
|
4169
4812
|
this.registerRole({
|
|
4170
4813
|
name: "reviewer",
|
|
@@ -4261,7 +4904,13 @@ class AgentRoleRegistry {
|
|
|
4261
4904
|
if (!role || !role.allowedToolNames)
|
|
4262
4905
|
return sourceRouter;
|
|
4263
4906
|
const filtered = sourceRouter.list().filter((t) => {
|
|
4264
|
-
return role.allowedToolNames.some((allowed) =>
|
|
4907
|
+
return role.allowedToolNames.some((allowed) => {
|
|
4908
|
+
if (t.name === allowed)
|
|
4909
|
+
return true;
|
|
4910
|
+
if (allowed.endsWith("*") && t.name.startsWith(allowed.slice(0, -1)))
|
|
4911
|
+
return true;
|
|
4912
|
+
return false;
|
|
4913
|
+
});
|
|
4265
4914
|
});
|
|
4266
4915
|
const newRouter = new sourceRouter.constructor;
|
|
4267
4916
|
for (const tool of filtered) {
|
|
@@ -4307,7 +4956,11 @@ class AgentGraphStore {
|
|
|
4307
4956
|
if (dbPath !== ":memory:") {
|
|
4308
4957
|
const dir = resolve11(dbPath, "..");
|
|
4309
4958
|
if (!existsSync15(dir)) {
|
|
4310
|
-
|
|
4959
|
+
try {
|
|
4960
|
+
mkdirSync7(dir, { recursive: true });
|
|
4961
|
+
} catch (err) {
|
|
4962
|
+
console.warn(`[AgentGraphStore] Failed to create database directory '${dir}':`, err);
|
|
4963
|
+
}
|
|
4311
4964
|
}
|
|
4312
4965
|
}
|
|
4313
4966
|
this.db = new Database2(dbPath);
|
|
@@ -4389,25 +5042,45 @@ class AgentGraphStore {
|
|
|
4389
5042
|
close() {
|
|
4390
5043
|
try {
|
|
4391
5044
|
this.db.close();
|
|
4392
|
-
} catch {
|
|
5045
|
+
} catch (err) {
|
|
5046
|
+
console.warn("[AgentGraphStore] Failed to close database cleanly:", err);
|
|
5047
|
+
}
|
|
4393
5048
|
}
|
|
4394
5049
|
}
|
|
4395
5050
|
|
|
4396
5051
|
// src/agents/spawner.ts
|
|
4397
5052
|
class AgentSpawner {
|
|
4398
5053
|
parentSession;
|
|
5054
|
+
depth;
|
|
4399
5055
|
subAgents = new Map;
|
|
4400
5056
|
nextAgentId = 1;
|
|
4401
5057
|
roleRegistry;
|
|
4402
5058
|
parentIdentity;
|
|
4403
5059
|
graphStore;
|
|
4404
|
-
|
|
5060
|
+
maxConcurrentAgents;
|
|
5061
|
+
maxDepth;
|
|
5062
|
+
maxRetainedCompleted;
|
|
5063
|
+
defaultTokenBudget;
|
|
5064
|
+
constructor(parentSession, roleRegistry, parentIdentity, graphStore, options, depth = 0) {
|
|
4405
5065
|
this.parentSession = parentSession;
|
|
5066
|
+
this.depth = depth;
|
|
4406
5067
|
this.roleRegistry = roleRegistry || new AgentRoleRegistry;
|
|
4407
5068
|
this.parentIdentity = parentIdentity || createAgentIdentity(undefined, "groupy-main");
|
|
4408
5069
|
this.graphStore = graphStore || new AgentGraphStore;
|
|
5070
|
+
const envMax = process.env.PIKAA_MAX_SUBAGENTS ? parseInt(process.env.PIKAA_MAX_SUBAGENTS, 10) : NaN;
|
|
5071
|
+
this.maxConcurrentAgents = options?.maxConcurrentAgents ?? (!isNaN(envMax) && envMax > 0 ? envMax : 5);
|
|
5072
|
+
this.maxDepth = options?.maxDepth ?? 2;
|
|
5073
|
+
this.maxRetainedCompleted = options?.maxRetainedCompleted ?? 20;
|
|
5074
|
+
this.defaultTokenBudget = options?.defaultTokenBudget ?? 50000;
|
|
4409
5075
|
}
|
|
4410
5076
|
async spawnAgent(params) {
|
|
5077
|
+
if (this.depth >= this.maxDepth) {
|
|
5078
|
+
throw new GroupyError(`Recursion limit exceeded: Maximum sub-agent nesting depth (${this.maxDepth}) reached.`);
|
|
5079
|
+
}
|
|
5080
|
+
const activeRunningCount = Array.from(this.subAgents.values()).filter((h) => h.status === "running").length;
|
|
5081
|
+
if (activeRunningCount >= this.maxConcurrentAgents) {
|
|
5082
|
+
throw new GroupyError(`Resource limit exceeded: Maximum concurrent sub-agents limit (${this.maxConcurrentAgents}) reached. Please wait for running sub-agents to complete or close them.`);
|
|
5083
|
+
}
|
|
4411
5084
|
const roleName = params.role || "default";
|
|
4412
5085
|
const roleConfig = this.roleRegistry.getRole(roleName);
|
|
4413
5086
|
const agentIndex = this.nextAgentId++;
|
|
@@ -4419,7 +5092,18 @@ class AgentSpawner {
|
|
|
4419
5092
|
|
|
4420
5093
|
Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus strictly on this task.` : `You are a specialized sub-agent named ${nickname} tasked with: '${params.taskName}'. Focus strictly on this task.`);
|
|
4421
5094
|
const baseTools = params.tools || this.parentSession.tools;
|
|
4422
|
-
|
|
5095
|
+
let effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
|
|
5096
|
+
if (this.depth + 1 >= this.maxDepth) {
|
|
5097
|
+
const sanitizedRouter = new ToolRouter;
|
|
5098
|
+
for (const t of effectiveTools.list()) {
|
|
5099
|
+
if (t.name !== "spawn_agent") {
|
|
5100
|
+
sanitizedRouter.register(t);
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
effectiveTools = sanitizedRouter;
|
|
5104
|
+
}
|
|
5105
|
+
const tokenBudget = params.maxTokens ?? this.defaultTokenBudget;
|
|
5106
|
+
let accumulatedTokens = 0;
|
|
4423
5107
|
const childSession = new Session({
|
|
4424
5108
|
threadId: agentId,
|
|
4425
5109
|
model: effectiveModel,
|
|
@@ -4443,45 +5127,87 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4443
5127
|
createdAt: Date.now(),
|
|
4444
5128
|
identity: childIdentity,
|
|
4445
5129
|
session: childSession,
|
|
4446
|
-
promise: taskPromise
|
|
5130
|
+
promise: taskPromise,
|
|
5131
|
+
depth: this.depth + 1,
|
|
5132
|
+
tokenBudget,
|
|
5133
|
+
totalTokens: 0
|
|
4447
5134
|
};
|
|
4448
5135
|
try {
|
|
4449
5136
|
this.graphStore.upsertEdge(this.parentSession.threadId, agentId, "open");
|
|
4450
|
-
} catch {
|
|
5137
|
+
} catch (err) {
|
|
5138
|
+
console.warn(`[AgentSpawner] Failed to record edge in graph store for agent '${agentId}':`, err);
|
|
5139
|
+
}
|
|
4451
5140
|
let collectedAgentText = "";
|
|
4452
5141
|
childSession.onEvent((event) => {
|
|
4453
5142
|
if (event.msg.type === "AgentMessageDelta") {
|
|
4454
5143
|
collectedAgentText += event.msg.delta;
|
|
5144
|
+
accumulatedTokens += Math.ceil(event.msg.delta.length / 4);
|
|
5145
|
+
handle.totalTokens = accumulatedTokens;
|
|
5146
|
+
if (accumulatedTokens > tokenBudget && handle.status === "running") {
|
|
5147
|
+
handle.status = "error";
|
|
5148
|
+
handle.error = `Token budget limit exceeded (${tokenBudget} tokens).`;
|
|
5149
|
+
childSession.interrupt();
|
|
5150
|
+
try {
|
|
5151
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5152
|
+
} catch (err) {
|
|
5153
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5154
|
+
}
|
|
5155
|
+
rejectPromise(new Error(handle.error));
|
|
5156
|
+
}
|
|
4455
5157
|
} else if (event.msg.type === "TurnCompleted") {
|
|
4456
|
-
handle.status
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
5158
|
+
if (handle.status === "running") {
|
|
5159
|
+
handle.status = "completed";
|
|
5160
|
+
handle.lastOutput = collectedAgentText.trim();
|
|
5161
|
+
handle.totalTokens = accumulatedTokens;
|
|
5162
|
+
try {
|
|
5163
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5164
|
+
} catch (err) {
|
|
5165
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5166
|
+
}
|
|
5167
|
+
this.pruneCompletedAgents();
|
|
5168
|
+
resolvePromise(handle.lastOutput);
|
|
5169
|
+
}
|
|
4462
5170
|
} else if (event.msg.type === "Error") {
|
|
4463
|
-
handle.status
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
5171
|
+
if (handle.status === "running") {
|
|
5172
|
+
handle.status = "error";
|
|
5173
|
+
handle.error = event.msg.message;
|
|
5174
|
+
handle.totalTokens = accumulatedTokens;
|
|
5175
|
+
try {
|
|
5176
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5177
|
+
} catch (err) {
|
|
5178
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5179
|
+
}
|
|
5180
|
+
this.pruneCompletedAgents();
|
|
5181
|
+
rejectPromise(new Error(event.msg.message));
|
|
5182
|
+
}
|
|
4469
5183
|
} else if (event.msg.type === "StatusChanged" && event.msg.status === "interrupted") {
|
|
4470
|
-
handle.status
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
5184
|
+
if (handle.status === "running") {
|
|
5185
|
+
handle.status = "interrupted";
|
|
5186
|
+
handle.totalTokens = accumulatedTokens;
|
|
5187
|
+
try {
|
|
5188
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5189
|
+
} catch (err) {
|
|
5190
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
|
|
5191
|
+
}
|
|
5192
|
+
this.pruneCompletedAgents();
|
|
5193
|
+
resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
|
|
5194
|
+
}
|
|
4475
5195
|
}
|
|
4476
5196
|
});
|
|
4477
5197
|
this.subAgents.set(agentId, handle);
|
|
4478
5198
|
childSession.prompt(params.message).catch((err) => {
|
|
4479
|
-
handle.status
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
5199
|
+
if (handle.status === "running") {
|
|
5200
|
+
handle.status = "error";
|
|
5201
|
+
handle.error = err instanceof Error ? err.message : String(err);
|
|
5202
|
+
handle.totalTokens = accumulatedTokens;
|
|
5203
|
+
try {
|
|
5204
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5205
|
+
} catch (storeErr) {
|
|
5206
|
+
console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, storeErr);
|
|
5207
|
+
}
|
|
5208
|
+
this.pruneCompletedAgents();
|
|
5209
|
+
rejectPromise(err);
|
|
5210
|
+
}
|
|
4485
5211
|
});
|
|
4486
5212
|
return {
|
|
4487
5213
|
id: handle.id,
|
|
@@ -4490,9 +5216,22 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4490
5216
|
role: handle.role,
|
|
4491
5217
|
status: handle.status,
|
|
4492
5218
|
createdAt: handle.createdAt,
|
|
4493
|
-
agentRuntimeId: childIdentity.agentRuntimeId
|
|
5219
|
+
agentRuntimeId: childIdentity.agentRuntimeId,
|
|
5220
|
+
depth: handle.depth,
|
|
5221
|
+
tokenBudget: handle.tokenBudget,
|
|
5222
|
+
totalTokens: handle.totalTokens
|
|
4494
5223
|
};
|
|
4495
5224
|
}
|
|
5225
|
+
pruneCompletedAgents() {
|
|
5226
|
+
const finishedHandles = Array.from(this.subAgents.values()).filter((h) => h.status !== "running");
|
|
5227
|
+
if (finishedHandles.length > this.maxRetainedCompleted) {
|
|
5228
|
+
finishedHandles.sort((a, b) => a.createdAt - b.createdAt);
|
|
5229
|
+
const toRemove = finishedHandles.slice(0, finishedHandles.length - this.maxRetainedCompleted);
|
|
5230
|
+
for (const h of toRemove) {
|
|
5231
|
+
this.subAgents.delete(h.id);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
4496
5235
|
async waitAgent(agentIdOrTaskName, timeoutMs = 60000) {
|
|
4497
5236
|
if (!agentIdOrTaskName) {
|
|
4498
5237
|
const handles = Array.from(this.subAgents.values());
|
|
@@ -4546,9 +5285,36 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4546
5285
|
handle.status = "interrupted";
|
|
4547
5286
|
try {
|
|
4548
5287
|
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
4549
|
-
} catch {
|
|
5288
|
+
} catch (err) {
|
|
5289
|
+
console.warn(`[AgentSpawner] Failed to close edge for agent '${agentId}':`, err);
|
|
5290
|
+
}
|
|
5291
|
+
this.pruneCompletedAgents();
|
|
4550
5292
|
return `Sub-agent ${handle.nickname} (${agentId}) interrupted and closed.`;
|
|
4551
5293
|
}
|
|
5294
|
+
removeAgent(agentId) {
|
|
5295
|
+
const handle = this.subAgents.get(agentId);
|
|
5296
|
+
if (!handle)
|
|
5297
|
+
return false;
|
|
5298
|
+
if (handle.status === "running") {
|
|
5299
|
+
handle.session.interrupt();
|
|
5300
|
+
}
|
|
5301
|
+
try {
|
|
5302
|
+
this.graphStore.setEdgeStatus(agentId, "closed");
|
|
5303
|
+
} catch (err) {
|
|
5304
|
+
console.warn(`[AgentSpawner] Failed to close edge on removal for '${agentId}':`, err);
|
|
5305
|
+
}
|
|
5306
|
+
return this.subAgents.delete(agentId);
|
|
5307
|
+
}
|
|
5308
|
+
clearCompleted() {
|
|
5309
|
+
let cleared = 0;
|
|
5310
|
+
for (const [id, handle] of this.subAgents.entries()) {
|
|
5311
|
+
if (handle.status !== "running") {
|
|
5312
|
+
this.subAgents.delete(id);
|
|
5313
|
+
cleared++;
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
return cleared;
|
|
5317
|
+
}
|
|
4552
5318
|
listAgents() {
|
|
4553
5319
|
const list = [];
|
|
4554
5320
|
for (const handle of this.subAgents.values()) {
|
|
@@ -4560,7 +5326,10 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
|
|
|
4560
5326
|
status: handle.status,
|
|
4561
5327
|
createdAt: handle.createdAt,
|
|
4562
5328
|
agentRuntimeId: handle.identity.agentRuntimeId,
|
|
4563
|
-
lastOutput: handle.lastOutput
|
|
5329
|
+
lastOutput: handle.lastOutput,
|
|
5330
|
+
depth: handle.depth,
|
|
5331
|
+
tokenBudget: handle.tokenBudget,
|
|
5332
|
+
totalTokens: handle.totalTokens
|
|
4564
5333
|
});
|
|
4565
5334
|
}
|
|
4566
5335
|
return list;
|
|
@@ -4594,6 +5363,10 @@ function createMultiAgentTools(spawner) {
|
|
|
4594
5363
|
model: {
|
|
4595
5364
|
type: "string",
|
|
4596
5365
|
description: "Optional model override. Defaults to inheriting parent model."
|
|
5366
|
+
},
|
|
5367
|
+
max_tokens: {
|
|
5368
|
+
type: "number",
|
|
5369
|
+
description: "Optional token budget limit for the sub-agent (default: 50000)."
|
|
4597
5370
|
}
|
|
4598
5371
|
},
|
|
4599
5372
|
required: ["task_name", "message"]
|
|
@@ -4603,8 +5376,9 @@ function createMultiAgentTools(spawner) {
|
|
|
4603
5376
|
const message = String(args.message || "");
|
|
4604
5377
|
const role = args.role ? String(args.role) : undefined;
|
|
4605
5378
|
const model = args.model ? String(args.model) : undefined;
|
|
5379
|
+
const maxTokens = typeof args.max_tokens === "number" ? args.max_tokens : undefined;
|
|
4606
5380
|
try {
|
|
4607
|
-
const handle = await spawner.spawnAgent({ taskName, message, role, model });
|
|
5381
|
+
const handle = await spawner.spawnAgent({ taskName, message, role, model, maxTokens });
|
|
4608
5382
|
return {
|
|
4609
5383
|
output: `Successfully spawned sub-agent '${handle.id}' with role '${handle.role}' for task '${handle.taskName}' (status: ${handle.status}). Use 'wait_agent' to collect results.`
|
|
4610
5384
|
};
|
|
@@ -5898,7 +6672,8 @@ class SessionPersistenceManager {
|
|
|
5898
6672
|
loadSession(threadId) {
|
|
5899
6673
|
try {
|
|
5900
6674
|
return this.store.restoreSession(threadId);
|
|
5901
|
-
} catch {
|
|
6675
|
+
} catch (err) {
|
|
6676
|
+
console.warn(`[SessionPersistenceManager] Failed to restore session '${threadId}':`, err);
|
|
5902
6677
|
return null;
|
|
5903
6678
|
}
|
|
5904
6679
|
}
|
|
@@ -5906,7 +6681,9 @@ class SessionPersistenceManager {
|
|
|
5906
6681
|
for (const unsub of this.unsubscribers) {
|
|
5907
6682
|
try {
|
|
5908
6683
|
unsub();
|
|
5909
|
-
} catch {
|
|
6684
|
+
} catch (err) {
|
|
6685
|
+
console.warn("[SessionPersistenceManager] Error in unbindSession callback:", err);
|
|
6686
|
+
}
|
|
5910
6687
|
}
|
|
5911
6688
|
this.unsubscribers = [];
|
|
5912
6689
|
}
|
|
@@ -5933,7 +6710,9 @@ class SessionPersistenceManager {
|
|
|
5933
6710
|
for (const unsub of this.unsubscribers) {
|
|
5934
6711
|
try {
|
|
5935
6712
|
unsub();
|
|
5936
|
-
} catch {
|
|
6713
|
+
} catch (err) {
|
|
6714
|
+
console.warn("[SessionPersistenceManager] Error closing session persistence listener:", err);
|
|
6715
|
+
}
|
|
5937
6716
|
}
|
|
5938
6717
|
this.unsubscribers = [];
|
|
5939
6718
|
this.store.close();
|
|
@@ -6268,7 +7047,9 @@ class MemoryStore {
|
|
|
6268
7047
|
if (!existsSync20(dir)) {
|
|
6269
7048
|
try {
|
|
6270
7049
|
mkdirSync11(dir, { recursive: true });
|
|
6271
|
-
} catch {
|
|
7050
|
+
} catch (err) {
|
|
7051
|
+
console.warn(`[MemoryStore] Failed to create custom memory directory '${dir}':`, err);
|
|
7052
|
+
}
|
|
6272
7053
|
}
|
|
6273
7054
|
return dir;
|
|
6274
7055
|
}
|
|
@@ -6277,7 +7058,9 @@ class MemoryStore {
|
|
|
6277
7058
|
if (!existsSync20(dir)) {
|
|
6278
7059
|
try {
|
|
6279
7060
|
mkdirSync11(dir, { recursive: true });
|
|
6280
|
-
} catch {
|
|
7061
|
+
} catch (err) {
|
|
7062
|
+
console.warn(`[MemoryStore] Failed to create project memory directory '${dir}':`, err);
|
|
7063
|
+
}
|
|
6281
7064
|
}
|
|
6282
7065
|
return dir;
|
|
6283
7066
|
}
|
|
@@ -6421,7 +7204,9 @@ class MemoryStore {
|
|
|
6421
7204
|
`)[0] || parsed.name,
|
|
6422
7205
|
file: f
|
|
6423
7206
|
});
|
|
6424
|
-
} catch {
|
|
7207
|
+
} catch (err) {
|
|
7208
|
+
console.warn(`[MemoryStore] Failed to parse topic memory file '${f}':`, err);
|
|
7209
|
+
}
|
|
6425
7210
|
}
|
|
6426
7211
|
const indexLines = [
|
|
6427
7212
|
"# Project Auto-Memory Index",
|
|
@@ -6463,7 +7248,9 @@ class MemoryStore {
|
|
|
6463
7248
|
try {
|
|
6464
7249
|
const full = join12(memoryDir, f);
|
|
6465
7250
|
list.push(this.parseTopicFile(readFileSync12(full, "utf8"), full));
|
|
6466
|
-
} catch {
|
|
7251
|
+
} catch (err) {
|
|
7252
|
+
console.warn(`[MemoryStore] Failed to read topic memory file '${f}':`, err);
|
|
7253
|
+
}
|
|
6467
7254
|
}
|
|
6468
7255
|
return list;
|
|
6469
7256
|
}
|
|
@@ -7397,7 +8184,7 @@ function parsePatch(oldSrc, newSrc, contextLines = 3) {
|
|
|
7397
8184
|
// package.json
|
|
7398
8185
|
var package_default = {
|
|
7399
8186
|
name: "@pikaa-ai/pikaa",
|
|
7400
|
-
version: "0.
|
|
8187
|
+
version: "0.4.1",
|
|
7401
8188
|
description: "PIKAA CLI - AI coding agent that runs locally in your terminal.",
|
|
7402
8189
|
main: "./dist/index.js",
|
|
7403
8190
|
module: "./dist/index.js",
|
|
@@ -7429,14 +8216,14 @@ var package_default = {
|
|
|
7429
8216
|
prepublishOnly: "bun run build:js"
|
|
7430
8217
|
},
|
|
7431
8218
|
optionalDependencies: {
|
|
7432
|
-
"@pikaa-ai/pikaa-linux-x64": "0.
|
|
7433
|
-
"@pikaa-ai/pikaa-linux-x64-musl": "0.
|
|
7434
|
-
"@pikaa-ai/pikaa-linux-arm64": "0.
|
|
7435
|
-
"@pikaa-ai/pikaa-linux-arm64-musl": "0.
|
|
7436
|
-
"@pikaa-ai/pikaa-darwin-x64": "0.
|
|
7437
|
-
"@pikaa-ai/pikaa-darwin-arm64": "0.
|
|
7438
|
-
"@pikaa-ai/pikaa-windows-x64": "0.
|
|
7439
|
-
"@pikaa-ai/pikaa-windows-arm64": "0.
|
|
8219
|
+
"@pikaa-ai/pikaa-linux-x64": "0.4.1",
|
|
8220
|
+
"@pikaa-ai/pikaa-linux-x64-musl": "0.4.1",
|
|
8221
|
+
"@pikaa-ai/pikaa-linux-arm64": "0.4.1",
|
|
8222
|
+
"@pikaa-ai/pikaa-linux-arm64-musl": "0.4.1",
|
|
8223
|
+
"@pikaa-ai/pikaa-darwin-x64": "0.4.1",
|
|
8224
|
+
"@pikaa-ai/pikaa-darwin-arm64": "0.4.1",
|
|
8225
|
+
"@pikaa-ai/pikaa-windows-x64": "0.4.1",
|
|
8226
|
+
"@pikaa-ai/pikaa-windows-arm64": "0.4.1"
|
|
7440
8227
|
},
|
|
7441
8228
|
keywords: [
|
|
7442
8229
|
"ai",
|
|
@@ -11068,9 +11855,15 @@ class CliRepl {
|
|
|
11068
11855
|
try {
|
|
11069
11856
|
const decision = await promptToolApproval(msg);
|
|
11070
11857
|
if (decision === "always") {
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11858
|
+
if (msg.prefixRule && Array.isArray(msg.prefixRule) && msg.prefixRule.length > 0) {
|
|
11859
|
+
globalPrefixRulesStore.addRule(this.session.cwd, msg.prefixRule);
|
|
11860
|
+
this.session.resolveApproval(msg.approvalId, { allowed: true, rememberPrefix: true });
|
|
11861
|
+
this.spinner.start(`Executing approved action (saved prefix rule)...`, this.turnStartTime);
|
|
11862
|
+
} else {
|
|
11863
|
+
this.session.execPolicy.addRule(/.*/, "allow", "User allowed all standard actions for this session");
|
|
11864
|
+
this.session.resolveApproval(msg.approvalId, true);
|
|
11865
|
+
this.spinner.start(`Executing approved action (auto-approved for session)...`, this.turnStartTime);
|
|
11866
|
+
}
|
|
11074
11867
|
} else if (decision === "yes") {
|
|
11075
11868
|
this.session.resolveApproval(msg.approvalId, true);
|
|
11076
11869
|
this.spinner.start(`Executing approved action...`, this.turnStartTime);
|