pentesting 0.21.7 → 0.21.9

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.
Files changed (2) hide show
  1. package/dist/main.js +1171 -975
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -13,11 +13,11 @@ import chalk from "chalk";
13
13
  import gradient from "gradient-string";
14
14
 
15
15
  // src/platform/tui/app.tsx
16
- import { useState as useState3, useCallback as useCallback2, useEffect as useEffect3 } from "react";
16
+ import { useState as useState4, useCallback as useCallback3, useEffect as useEffect4 } from "react";
17
17
  import { Box as Box6, useInput, useApp } from "ink";
18
18
 
19
19
  // src/platform/tui/hooks/useAgent.ts
20
- import { useState, useEffect, useCallback, useRef } from "react";
20
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
21
21
 
22
22
  // src/shared/constants/timing.ts
23
23
  var TOOL_TIMEOUTS = {
@@ -43,6 +43,18 @@ var DISPLAY_LIMITS = {
43
43
  TOOL_INPUT_TRUNCATED: 30,
44
44
  /** Max characters to show in tool output preview */
45
45
  TOOL_OUTPUT_PREVIEW: 200,
46
+ /** Max characters to show in tool output slice (events) */
47
+ TOOL_OUTPUT_SLICE: 80,
48
+ /** Max characters to show in tool error preview (events) */
49
+ TOOL_ERROR_SLICE: 120,
50
+ /** Max characters for delegate output preview */
51
+ DELEGATE_OUTPUT_SLICE: 60,
52
+ /** Max characters for command preview in logs */
53
+ COMMAND_PREVIEW: 100,
54
+ /** Max characters for output summary in agent loop */
55
+ OUTPUT_SUMMARY: 200,
56
+ /** Max characters for error preview in logs */
57
+ ERROR_PREVIEW: 100,
46
58
  /** Max characters for status thought preview */
47
59
  STATUS_THOUGHT_PREVIEW: 60,
48
60
  /** Max characters after truncation for status */
@@ -66,7 +78,19 @@ var DISPLAY_LIMITS = {
66
78
  /** Number of targets to preview in state summary */
67
79
  TARGET_PREVIEW: 3,
68
80
  /** Number of important findings to preview */
69
- FINDING_PREVIEW: 5
81
+ FINDING_PREVIEW: 5,
82
+ /** Number of hashes to preview */
83
+ HASH_PREVIEW: 20,
84
+ /** Number of loot items to preview */
85
+ LOOT_PREVIEW: 30,
86
+ /** Number of recent process events to show */
87
+ RECENT_EVENT_COUNT: 5,
88
+ /** Max links to preview in browser output */
89
+ LINKS_PREVIEW: 20,
90
+ /** Max endpoints to sample */
91
+ ENDPOINT_SAMPLE: 5,
92
+ /** Purpose truncation length */
93
+ PURPOSE_TRUNCATE: 27
70
94
  };
71
95
  var AGENT_LIMITS = {
72
96
  /** Maximum agent loop iterations — generous to allow complex tasks.
@@ -113,11 +137,27 @@ var INPUT_PROMPT_PATTERNS = [
113
137
  /\(Y\/n\)/i
114
138
  ];
115
139
 
140
+ // src/shared/constants/exit-codes.ts
141
+ var EXIT_CODES = {
142
+ /** Successful execution */
143
+ SUCCESS: 0,
144
+ /** General error */
145
+ GENERAL_ERROR: 1,
146
+ /** Command not found */
147
+ COMMAND_NOT_FOUND: 127,
148
+ /** Process killed by SIGALRM (timeout) */
149
+ TIMEOUT: 124,
150
+ /** Process killed by SIGINT (Ctrl+C) */
151
+ SIGINT: 130,
152
+ /** Process killed by SIGTERM */
153
+ SIGTERM: 143
154
+ };
155
+
116
156
  // src/shared/constants/agent.ts
117
157
  var ID_LENGTH = AGENT_LIMITS.ID_LENGTH;
118
158
  var ID_RADIX = AGENT_LIMITS.ID_RADIX;
119
159
  var APP_NAME = "Pentest AI";
120
- var APP_VERSION = "0.21.7";
160
+ var APP_VERSION = "0.21.9";
121
161
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
122
162
  var LLM_ROLES = {
123
163
  SYSTEM: "system",
@@ -146,7 +186,11 @@ var SENSITIVE_INPUT_TYPES = [
146
186
  ];
147
187
 
148
188
  // src/shared/utils/id.ts
189
+ var ID_DEFAULT_RADIX = 36;
190
+ var ID_DEFAULT_LENGTH = 6;
149
191
  var generateId = (radix = 36, length = 8) => Math.random().toString(radix).substring(2, 2 + length);
192
+ var generatePrefixedId = (prefix, radix = ID_DEFAULT_RADIX, randomLength = ID_DEFAULT_LENGTH) => `${prefix}_${Date.now()}_${Math.random().toString(radix).slice(2, 2 + randomLength)}`;
193
+ var generateTempFilename = (suffix = "", prefix = "pentest") => `${prefix}-${Date.now()}-${Math.random().toString(ID_DEFAULT_RADIX).slice(2, 2 + ID_DEFAULT_LENGTH)}${suffix}`;
150
194
 
151
195
  // src/shared/constants/protocol.ts
152
196
  var PHASES = {
@@ -410,15 +454,23 @@ var DEFAULTS = {
410
454
  };
411
455
 
412
456
  // src/engine/process-manager.ts
413
- import { spawn as spawn2, execSync } from "child_process";
414
- import { readFileSync as readFileSync2, existsSync as existsSync2, unlinkSync, writeFileSync as writeFileSync2, appendFileSync } from "fs";
457
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
458
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync, writeFileSync as writeFileSync2, appendFileSync } from "fs";
415
459
 
416
460
  // src/engine/tools-base.ts
417
461
  import { spawn } from "child_process";
418
- import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
419
- import { join } from "path";
462
+ import { readFileSync, existsSync as existsSync2, writeFileSync } from "fs";
463
+ import { join, dirname } from "path";
420
464
  import { tmpdir } from "os";
421
465
 
466
+ // src/shared/utils/file-utils.ts
467
+ import { existsSync, mkdirSync } from "fs";
468
+ function ensureDirExists(dirPath) {
469
+ if (!existsSync(dirPath)) {
470
+ mkdirSync(dirPath, { recursive: true });
471
+ }
472
+ }
473
+
422
474
  // src/shared/constants/system.ts
423
475
  var PROCESS_ROLES = {
424
476
  LISTENER: "listener",
@@ -469,6 +521,10 @@ var SYSTEM_LIMITS = {
469
521
  MAX_STDOUT_SLICE: 3e3,
470
522
  /** Maximum characters to show from stderr */
471
523
  MAX_STDERR_SLICE: 500,
524
+ /** Maximum characters for error detail messages */
525
+ MAX_ERROR_DETAIL_SLICE: 200,
526
+ /** Maximum characters for input prompt previews */
527
+ MAX_PROMPT_PREVIEW: 50,
472
528
  /** Maximum characters for input snippets in logs */
473
529
  MAX_INPUT_SLICE: 100,
474
530
  /** Maximum events to keep in process event log */
@@ -486,12 +542,6 @@ var SYSTEM_LIMITS = {
486
542
  /** Port range for API services */
487
543
  API_PORT_RANGE: { MIN: 3e3, MAX: 3500 }
488
544
  };
489
- var EXIT_CODES2 = {
490
- /** SIGINT (Ctrl+C) - 128 + 2 */
491
- SIGINT: 130,
492
- /** SIGTERM - 128 + 15 */
493
- SIGTERM: 143
494
- };
495
545
  var DETECTION_PATTERNS = {
496
546
  LISTENER: /-(?:lvnp|nlvp|lp|p)\s+(\d+)/,
497
547
  HTTP_SERVER: /(?:http\.server|SimpleHTTPServer)\s+(\d+)/,
@@ -1115,11 +1165,11 @@ async function installTool(toolName) {
1115
1165
  globalEventEmitter?.({
1116
1166
  type: COMMAND_EVENT_TYPES.TOOL_INSTALL_FAILED,
1117
1167
  message: `Failed to install: ${toolName}`,
1118
- detail: stderr.slice(0, 200)
1168
+ detail: stderr.slice(0, SYSTEM_LIMITS.MAX_ERROR_DETAIL_SLICE)
1119
1169
  });
1120
1170
  resolve({
1121
1171
  success: false,
1122
- output: `Failed to install ${toolName}: ${stderr.slice(0, 200)}`
1172
+ output: `Failed to install ${toolName}: ${stderr.slice(0, SYSTEM_LIMITS.MAX_ERROR_DETAIL_SLICE)}`
1123
1173
  });
1124
1174
  }
1125
1175
  });
@@ -1169,7 +1219,7 @@ async function runCommand(command, args = [], options = {}) {
1169
1219
  globalEventEmitter?.({
1170
1220
  type: COMMAND_EVENT_TYPES.TOOL_RETRY,
1171
1221
  message: `Retrying command after installing ${toolName}...`,
1172
- detail: command.slice(0, 100)
1222
+ detail: command.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)
1173
1223
  });
1174
1224
  }
1175
1225
  return lastResult;
@@ -1179,7 +1229,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1179
1229
  const timeout = options.timeout ?? TOOL_TIMEOUTS.DEFAULT_COMMAND;
1180
1230
  globalEventEmitter?.({
1181
1231
  type: COMMAND_EVENT_TYPES.COMMAND_START,
1182
- message: `Executing: ${command.slice(0, 100)}${command.length > 100 ? "..." : ""}`
1232
+ message: `Executing: ${command.slice(0, DISPLAY_LIMITS.COMMAND_PREVIEW)}${command.length > DISPLAY_LIMITS.COMMAND_PREVIEW ? "..." : ""}`
1183
1233
  });
1184
1234
  const child = spawn("sh", ["-c", command], {
1185
1235
  timeout,
@@ -1197,7 +1247,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1197
1247
  inputHandled = true;
1198
1248
  globalEventEmitter?.({
1199
1249
  type: COMMAND_EVENT_TYPES.INPUT_REQUIRED,
1200
- message: `Input required: ${data.trim().slice(0, 50)}`,
1250
+ message: `Input required: ${data.trim().slice(0, SYSTEM_LIMITS.MAX_PROMPT_PREVIEW)}`,
1201
1251
  detail: "Waiting for user input..."
1202
1252
  });
1203
1253
  const promptText = data.trim();
@@ -1237,7 +1287,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1237
1287
  globalEventEmitter?.({
1238
1288
  type: COMMAND_EVENT_TYPES.COMMAND_FAILED,
1239
1289
  message: `Command failed (exit ${code})`,
1240
- detail: stderr.slice(0, 100)
1290
+ detail: stderr.slice(0, SYSTEM_LIMITS.MAX_INPUT_SLICE)
1241
1291
  });
1242
1292
  resolve({
1243
1293
  success: false,
@@ -1262,7 +1312,7 @@ async function executeCommandOnce(command, args = [], options = {}) {
1262
1312
  }
1263
1313
  async function readFileContent(filePath) {
1264
1314
  try {
1265
- if (!existsSync(filePath)) {
1315
+ if (!existsSync2(filePath)) {
1266
1316
  return {
1267
1317
  success: false,
1268
1318
  output: "",
@@ -1285,10 +1335,8 @@ async function readFileContent(filePath) {
1285
1335
  }
1286
1336
  async function writeFileContent(filePath, content) {
1287
1337
  try {
1288
- const dir = join(filePath, "..");
1289
- if (!existsSync(dir)) {
1290
- mkdirSync(dir, { recursive: true });
1291
- }
1338
+ const dir = dirname(filePath);
1339
+ ensureDirExists(dir);
1292
1340
  writeFileSync(filePath, content, "utf-8");
1293
1341
  return {
1294
1342
  success: true,
@@ -1304,20 +1352,11 @@ async function writeFileContent(filePath, content) {
1304
1352
  }
1305
1353
  }
1306
1354
  function createTempFile(suffix = "") {
1307
- return join(tmpdir(), `pentest-${Date.now()}-${Math.random().toString(ID_RADIX).substring(ID_LENGTH)}${suffix}`);
1355
+ return join(tmpdir(), generateTempFilename(suffix));
1308
1356
  }
1309
1357
 
1310
- // src/engine/process-manager.ts
1311
- var backgroundProcesses = /* @__PURE__ */ new Map();
1312
- var cleanupDone = false;
1313
- var processEventLog = [];
1314
- function logEvent(processId, event, detail) {
1315
- processEventLog.push({ timestamp: Date.now(), processId, event, detail });
1316
- if (processEventLog.length > SYSTEM_LIMITS.MAX_EVENT_LOG) {
1317
- processEventLog.splice(0, processEventLog.length - SYSTEM_LIMITS.MAX_EVENT_LOG);
1318
- }
1319
- }
1320
- function autoDetect(command) {
1358
+ // src/engine/process-detector.ts
1359
+ function detectProcessRole(command) {
1321
1360
  const tags = [];
1322
1361
  let port;
1323
1362
  let role = PROCESS_ROLES.BACKGROUND;
@@ -1365,6 +1404,12 @@ function autoDetect(command) {
1365
1404
  if (tags.length === 0) tags.push(PROCESS_ROLES.BACKGROUND);
1366
1405
  return { tags, port, role, isInteractive };
1367
1406
  }
1407
+ function detectConnection(stdout) {
1408
+ return DETECTION_PATTERNS.CONNECTION.some((p) => p.test(stdout));
1409
+ }
1410
+
1411
+ // src/engine/process-tree.ts
1412
+ import { execSync } from "child_process";
1368
1413
  function discoverChildPids(parentPid) {
1369
1414
  try {
1370
1415
  const result2 = execSync(`pgrep -P ${parentPid} 2>/dev/null || true`, {
@@ -1395,15 +1440,74 @@ function isPidAlive(pid) {
1395
1440
  return false;
1396
1441
  }
1397
1442
  }
1398
- function detectConnection(stdout) {
1399
- return DETECTION_PATTERNS.CONNECTION.some((p) => p.test(stdout));
1443
+ async function killProcessTree(pid, childPids, waitMs = SYSTEM_LIMITS.SHUTDOWN_WAIT_MS) {
1444
+ try {
1445
+ process.kill(-pid, "SIGTERM");
1446
+ } catch {
1447
+ try {
1448
+ process.kill(pid, "SIGTERM");
1449
+ } catch {
1450
+ }
1451
+ }
1452
+ for (const childPid of childPids) {
1453
+ try {
1454
+ process.kill(childPid, "SIGTERM");
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ await new Promise((r) => setTimeout(r, waitMs));
1459
+ if (isPidAlive(pid)) {
1460
+ try {
1461
+ process.kill(-pid, "SIGKILL");
1462
+ } catch {
1463
+ }
1464
+ try {
1465
+ process.kill(pid, "SIGKILL");
1466
+ } catch {
1467
+ }
1468
+ }
1469
+ for (const childPid of childPids) {
1470
+ if (isPidAlive(childPid)) {
1471
+ try {
1472
+ process.kill(childPid, "SIGKILL");
1473
+ } catch {
1474
+ }
1475
+ }
1476
+ }
1477
+ }
1478
+ function killProcessTreeSync(pid, childPids) {
1479
+ try {
1480
+ process.kill(-pid, "SIGKILL");
1481
+ } catch {
1482
+ }
1483
+ try {
1484
+ process.kill(pid, "SIGKILL");
1485
+ } catch {
1486
+ }
1487
+ for (const childPid of childPids) {
1488
+ try {
1489
+ process.kill(childPid, "SIGKILL");
1490
+ } catch {
1491
+ }
1492
+ }
1493
+ }
1494
+
1495
+ // src/engine/process-manager.ts
1496
+ var backgroundProcesses = /* @__PURE__ */ new Map();
1497
+ var cleanupDone = false;
1498
+ var processEventLog = [];
1499
+ function logEvent(processId, event, detail) {
1500
+ processEventLog.push({ timestamp: Date.now(), processId, event, detail });
1501
+ if (processEventLog.length > SYSTEM_LIMITS.MAX_EVENT_LOG) {
1502
+ processEventLog.splice(0, processEventLog.length - SYSTEM_LIMITS.MAX_EVENT_LOG);
1503
+ }
1400
1504
  }
1401
1505
  function startBackgroundProcess(command, options = {}) {
1402
- const processId = `bg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1506
+ const processId = generatePrefixedId("bg");
1403
1507
  const stdoutFile = createTempFile(".stdout");
1404
1508
  const stderrFile = createTempFile(".stderr");
1405
1509
  const stdinFile = createTempFile(".stdin");
1406
- const { tags, port, role, isInteractive } = autoDetect(command);
1510
+ const { tags, port, role, isInteractive } = detectProcessRole(command);
1407
1511
  let wrappedCmd;
1408
1512
  if (isInteractive) {
1409
1513
  writeFileSync2(stdinFile, "", "utf-8");
@@ -1459,7 +1563,7 @@ async function sendToProcess(processId, input, waitMs = SYSTEM_LIMITS.DEFAULT_WA
1459
1563
  if (proc.hasExited) return { success: false, output: `Process ${processId} has exited`, newOutput: "" };
1460
1564
  let currentLen = 0;
1461
1565
  try {
1462
- if (existsSync2(proc.stdoutFile)) {
1566
+ if (existsSync3(proc.stdoutFile)) {
1463
1567
  currentLen = readFileSync2(proc.stdoutFile, "utf-8").length;
1464
1568
  }
1465
1569
  } catch {
@@ -1473,7 +1577,7 @@ async function sendToProcess(processId, input, waitMs = SYSTEM_LIMITS.DEFAULT_WA
1473
1577
  await new Promise((r) => setTimeout(r, waitMs));
1474
1578
  let fullStdout = "";
1475
1579
  try {
1476
- if (existsSync2(proc.stdoutFile)) {
1580
+ if (existsSync3(proc.stdoutFile)) {
1477
1581
  fullStdout = readFileSync2(proc.stdoutFile, "utf-8");
1478
1582
  }
1479
1583
  } catch {
@@ -1506,7 +1610,7 @@ function isProcessRunning(processId) {
1506
1610
  proc.childPids = discoverAllDescendants(proc.pid);
1507
1611
  if (proc.role === "listener") {
1508
1612
  try {
1509
- if (existsSync2(proc.stdoutFile)) {
1613
+ if (existsSync3(proc.stdoutFile)) {
1510
1614
  const stdout = readFileSync2(proc.stdoutFile, "utf-8");
1511
1615
  if (detectConnection(stdout)) {
1512
1616
  promoteToShell(processId, "Reverse shell connected (auto-detected)");
@@ -1525,7 +1629,7 @@ function getProcessOutput(processId) {
1525
1629
  let stdout = "";
1526
1630
  let stderr = "";
1527
1631
  try {
1528
- if (existsSync2(proc.stdoutFile)) {
1632
+ if (existsSync3(proc.stdoutFile)) {
1529
1633
  const content = readFileSync2(proc.stdoutFile, "utf-8");
1530
1634
  stdout = content.length > SYSTEM_LIMITS.MAX_STDOUT_SLICE ? `... [truncated ${content.length - SYSTEM_LIMITS.MAX_STDOUT_SLICE} chars] ...
1531
1635
  ` + content.slice(-SYSTEM_LIMITS.MAX_STDOUT_SLICE) : content;
@@ -1533,7 +1637,7 @@ function getProcessOutput(processId) {
1533
1637
  } catch {
1534
1638
  }
1535
1639
  try {
1536
- if (existsSync2(proc.stderrFile)) {
1640
+ if (existsSync3(proc.stderrFile)) {
1537
1641
  const content = readFileSync2(proc.stderrFile, "utf-8");
1538
1642
  stderr = content.length > SYSTEM_LIMITS.MAX_STDERR_SLICE ? `... [truncated ${content.length - SYSTEM_LIMITS.MAX_STDERR_SLICE} chars] ...
1539
1643
  ` + content.slice(-SYSTEM_LIMITS.MAX_STDERR_SLICE) : content;
@@ -1554,46 +1658,14 @@ function getProcessOutput(processId) {
1554
1658
  isInteractive: proc.isInteractive
1555
1659
  };
1556
1660
  }
1557
- async function stopProcess(processId) {
1661
+ async function stopBackgroundProcess(processId) {
1558
1662
  const proc = backgroundProcesses.get(processId);
1559
1663
  if (!proc) return { success: false, output: `Process ${processId} not found` };
1560
1664
  const wasActiveShell = proc.role === PROCESS_ROLES.ACTIVE_SHELL;
1561
1665
  const finalOutput = getProcessOutput(processId);
1562
1666
  if (!proc.hasExited) {
1563
1667
  proc.childPids = discoverAllDescendants(proc.pid);
1564
- try {
1565
- process.kill(-proc.pid, "SIGTERM");
1566
- } catch {
1567
- try {
1568
- process.kill(proc.pid, "SIGTERM");
1569
- } catch {
1570
- }
1571
- }
1572
- for (const childPid of proc.childPids) {
1573
- try {
1574
- process.kill(childPid, "SIGTERM");
1575
- } catch {
1576
- }
1577
- }
1578
- await new Promise((r) => setTimeout(r, SYSTEM_LIMITS.SHUTDOWN_WAIT_MS));
1579
- if (isPidAlive(proc.pid)) {
1580
- try {
1581
- process.kill(-proc.pid, "SIGKILL");
1582
- } catch {
1583
- }
1584
- try {
1585
- process.kill(proc.pid, "SIGKILL");
1586
- } catch {
1587
- }
1588
- }
1589
- for (const childPid of proc.childPids) {
1590
- if (isPidAlive(childPid)) {
1591
- try {
1592
- process.kill(childPid, "SIGKILL");
1593
- } catch {
1594
- }
1595
- }
1596
- }
1668
+ await killProcessTree(proc.pid, proc.childPids);
1597
1669
  }
1598
1670
  try {
1599
1671
  unlinkSync(proc.stdoutFile);
@@ -1717,7 +1789,7 @@ async function cleanupAllProcesses() {
1717
1789
  backgroundProcesses.clear();
1718
1790
  for (const name of ORPHAN_PROCESS_NAMES) {
1719
1791
  try {
1720
- execSync(`pkill -f "${name}" 2>/dev/null || true`, { stdio: "ignore", timeout: SYSTEM_LIMITS.PROCESS_OP_TIMEOUT_MS });
1792
+ execSync2(`pkill -f "${name}" 2>/dev/null || true`, { stdio: "ignore", timeout: SYSTEM_LIMITS.PROCESS_OP_TIMEOUT_MS });
1721
1793
  } catch {
1722
1794
  }
1723
1795
  }
@@ -1740,10 +1812,10 @@ function getResourceSummary() {
1740
1812
  const roleIcon = PROCESS_ICONS[p.role] || PROCESS_ICONS[PROCESS_ROLES.BACKGROUND];
1741
1813
  let lastOutput = "";
1742
1814
  try {
1743
- if (p.stdoutFile && existsSync2(p.stdoutFile)) {
1815
+ if (p.stdoutFile && existsSync3(p.stdoutFile)) {
1744
1816
  const content = readFileSync2(p.stdoutFile, "utf-8");
1745
- const lines2 = content.trim().split("\n");
1746
- lastOutput = lines2.slice(-3).join(" | ").replace(/\n/g, " ");
1817
+ const outputLines = content.trim().split("\n");
1818
+ lastOutput = outputLines.slice(-3).join(" | ").replace(/\n/g, " ");
1747
1819
  }
1748
1820
  } catch {
1749
1821
  }
@@ -1767,7 +1839,7 @@ ${STATUS_MARKERS.WARNING} SUSPECTED ZOMBIES (Orphaned Children):`);
1767
1839
  for (const o of orphans) {
1768
1840
  lines.push(` [${o.id}] Exited parent has alive children: ${o.childPids.join(", ")}`);
1769
1841
  }
1770
- lines.push(` \u2192 Recommendation: Run bg_process({ action: "stop", process_id: "ID" }) to clean up entire tree.`);
1842
+ lines.push(` \u2192 Recommendation: Run bg_process({ action: "stop", process_id: "ID" }) to clean up.`);
1771
1843
  }
1772
1844
  if (ports.length > 0) {
1773
1845
  lines.push(`
@@ -1799,20 +1871,7 @@ Ports In Use: ${ports.join(", ")}`);
1799
1871
  function syncCleanupAllProcesses() {
1800
1872
  for (const [, proc] of backgroundProcesses) {
1801
1873
  if (!proc.hasExited) {
1802
- try {
1803
- process.kill(-proc.pid, "SIGKILL");
1804
- } catch {
1805
- }
1806
- try {
1807
- process.kill(proc.pid, "SIGKILL");
1808
- } catch {
1809
- }
1810
- for (const childPid of proc.childPids) {
1811
- try {
1812
- process.kill(childPid, "SIGKILL");
1813
- } catch {
1814
- }
1815
- }
1874
+ killProcessTreeSync(proc.pid, proc.childPids);
1816
1875
  }
1817
1876
  try {
1818
1877
  unlinkSync(proc.stdoutFile);
@@ -1834,13 +1893,14 @@ process.on("exit", () => {
1834
1893
  process.on("SIGINT", () => {
1835
1894
  syncCleanupAllProcesses();
1836
1895
  backgroundProcesses.clear();
1837
- process.exit(EXIT_CODES2.SIGINT);
1896
+ process.exit(EXIT_CODES.SIGINT);
1838
1897
  });
1839
1898
  process.on("SIGTERM", () => {
1840
1899
  syncCleanupAllProcesses();
1841
1900
  backgroundProcesses.clear();
1842
- process.exit(EXIT_CODES2.SIGTERM);
1901
+ process.exit(EXIT_CODES.SIGTERM);
1843
1902
  });
1903
+ var stopProcess = stopBackgroundProcess;
1844
1904
 
1845
1905
  // src/engine/state-serializer.ts
1846
1906
  var StateSerializer = class {
@@ -1952,11 +2012,11 @@ var SharedState = class {
1952
2012
  for (const update of updates) {
1953
2013
  const index = this.data.missionChecklist.findIndex((item) => item.id === update.id);
1954
2014
  if (index !== -1) {
1955
- if (update.remove) {
2015
+ if (update.shouldRemove) {
1956
2016
  this.data.missionChecklist.splice(index, 1);
1957
2017
  } else {
1958
2018
  if (update.text !== void 0) this.data.missionChecklist[index].text = update.text;
1959
- if (update.completed !== void 0) this.data.missionChecklist[index].isCompleted = update.completed;
2019
+ if (update.isCompleted !== void 0) this.data.missionChecklist[index].isCompleted = update.isCompleted;
1960
2020
  }
1961
2021
  }
1962
2022
  }
@@ -2349,23 +2409,23 @@ function getApprovalLevel(toolCall) {
2349
2409
  return APPROVAL_LEVELS.CONFIRM;
2350
2410
  }
2351
2411
  var ApprovalGate = class {
2352
- constructor(autoApprove = false) {
2353
- this.autoApprove = autoApprove;
2412
+ constructor(shouldAutoApprove = false) {
2413
+ this.shouldAutoApprove = shouldAutoApprove;
2354
2414
  }
2355
2415
  /**
2356
2416
  * Set auto-approve mode
2357
2417
  */
2358
2418
  setAutoApprove(enabled) {
2359
- this.autoApprove = enabled;
2419
+ this.shouldAutoApprove = enabled;
2360
2420
  }
2361
2421
  /**
2362
2422
  * Get current auto-approve mode
2363
2423
  */
2364
2424
  isAutoApprove() {
2365
- return this.autoApprove;
2425
+ return this.shouldAutoApprove;
2366
2426
  }
2367
2427
  async request(toolCall) {
2368
- if (this.autoApprove) return { isApproved: true, reason: "Auto-approve enabled" };
2428
+ if (this.shouldAutoApprove) return { isApproved: true, reason: "Auto-approve enabled" };
2369
2429
  const level = getApprovalLevel(toolCall);
2370
2430
  if (level === APPROVAL_LEVELS.AUTO) return { isApproved: true };
2371
2431
  if (level === APPROVAL_LEVELS.BLOCK) return { isApproved: false, reason: "Policy blocked execution" };
@@ -2696,105 +2756,284 @@ All ports freed. All children killed.`
2696
2756
  }
2697
2757
  ];
2698
2758
 
2699
- // src/engine/tools-mid.ts
2700
- import { execFileSync } from "child_process";
2701
-
2702
- // src/shared/utils/config.ts
2703
- import path from "path";
2704
- import { fileURLToPath } from "url";
2705
- var __filename = fileURLToPath(import.meta.url);
2706
- var __dirname = path.dirname(__filename);
2707
- var ENV_KEYS = {
2708
- API_KEY: "PENTEST_API_KEY",
2709
- BASE_URL: "PENTEST_BASE_URL",
2710
- MODEL: "PENTEST_MODEL",
2711
- SEARCH_API_KEY: "SEARCH_API_KEY",
2712
- SEARCH_API_URL: "SEARCH_API_URL"
2713
- };
2714
- var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
2715
- function getApiKey() {
2716
- return process.env[ENV_KEYS.API_KEY] || "";
2717
- }
2718
- function getBaseUrl() {
2719
- return process.env[ENV_KEYS.BASE_URL] || void 0;
2720
- }
2721
- function getModel() {
2722
- return process.env[ENV_KEYS.MODEL] || "";
2723
- }
2724
- function getSearchApiKey() {
2725
- return process.env[ENV_KEYS.SEARCH_API_KEY];
2726
- }
2727
- function getSearchApiUrl() {
2728
- return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
2729
- }
2730
- function isBrowserHeadless() {
2731
- return true;
2732
- }
2733
-
2734
- // src/shared/constants/search-api.const.ts
2735
- var SEARCH_URL_PATTERN = {
2736
- GLM: "bigmodel.cn",
2737
- ZHIPU: "zhipuai",
2738
- BRAVE: "brave.com",
2739
- SERPER: "serper.dev"
2740
- };
2741
- var SEARCH_HEADER = {
2742
- CONTENT_TYPE: "Content-Type",
2743
- AUTHORIZATION: "Authorization",
2744
- ACCEPT: "Accept",
2745
- X_SUBSCRIPTION_TOKEN: "X-Subscription-Token",
2746
- X_API_KEY: "X-API-KEY"
2747
- };
2748
- var SEARCH_LIMIT = {
2749
- DEFAULT_RESULT_COUNT: 10,
2750
- MAX_OUTPUT_LINES: 20,
2751
- TIMEOUT_MS: 1e4
2752
- };
2753
-
2754
- // src/shared/utils/debug-logger.ts
2755
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3, writeFileSync as writeFileSync3 } from "fs";
2756
- import { join as join2 } from "path";
2757
- var DebugLogger = class _DebugLogger {
2758
- static instance;
2759
- logPath;
2760
- initialized = false;
2761
- constructor(clearOnInit = false) {
2762
- const debugDir = join2(process.cwd(), ".pentesting", "debug");
2763
- try {
2764
- if (!existsSync3(debugDir)) {
2765
- mkdirSync2(debugDir, { recursive: true });
2766
- }
2767
- this.logPath = join2(debugDir, "debug.log");
2768
- if (clearOnInit) {
2769
- this.clear();
2759
+ // src/engine/tools/pentest-state-tools.ts
2760
+ var createStateTools = (state) => [
2761
+ {
2762
+ name: TOOL_NAMES.UPDATE_MISSION,
2763
+ description: `Update the mission summary and checklist.
2764
+ Use this for strategic planning and maintaining long-term context during complex engagements.
2765
+ The mission summary acts as your "long-term memory" \u2014 keep it updated with critical findings and overall progress.
2766
+ The checklist tracks granular steps (e.g., "[ ] Crack hashes", "[x] Recon 10.10.10.5").
2767
+ Mandatory when:
2768
+ - Big goals change
2769
+ - You encounter a major obstacle (firewall, port conflict)
2770
+ - You gain a significant new access point (reverse shell)
2771
+ - You need to summarize findings to prevent context overflow`,
2772
+ parameters: {
2773
+ summary: { type: "string", description: "Updated mission summary (concise overview)" },
2774
+ checklist_updates: {
2775
+ type: "array",
2776
+ items: {
2777
+ type: "object",
2778
+ properties: {
2779
+ id: { type: "string", description: "Item ID (required for update/remove)" },
2780
+ text: { type: "string", description: "Updated text" },
2781
+ completed: { type: "boolean", description: "Completion status" },
2782
+ remove: { type: "boolean", description: "Remove item if true" }
2783
+ },
2784
+ required: ["id"]
2785
+ },
2786
+ description: "Updates to existing checklist items"
2787
+ },
2788
+ add_items: {
2789
+ type: "array",
2790
+ items: { type: "string" },
2791
+ description: "New checklist items to add"
2770
2792
  }
2771
- this.initialized = true;
2772
- this.log("general", "=== DEBUG LOGGER INITIALIZED ===");
2773
- this.log("general", `Log file: ${this.logPath}`);
2774
- } catch (e) {
2775
- console.error("[DebugLogger] Failed to initialize:", e);
2776
- this.logPath = "";
2777
- }
2778
- }
2779
- static getInstance(clearOnInit = false) {
2780
- if (!_DebugLogger.instance) {
2781
- _DebugLogger.instance = new _DebugLogger(clearOnInit);
2793
+ },
2794
+ execute: async (p) => {
2795
+ if (p.summary) state.setMissionSummary(p.summary);
2796
+ if (p.checklist_updates) state.updateMissionChecklist(p.checklist_updates);
2797
+ if (p.add_items) state.addMissionChecklistItems(p.add_items);
2798
+ return {
2799
+ success: true,
2800
+ output: `Mission updated.
2801
+ Summary: ${state.getMissionSummary()}
2802
+ Checklist Items: ${state.getMissionChecklist().length}`
2803
+ };
2782
2804
  }
2783
- return _DebugLogger.instance;
2784
- }
2785
- /** Reset the singleton instance (used by initDebugLogger) */
2786
- static resetInstance(clearOnInit = false) {
2787
- _DebugLogger.instance = new _DebugLogger(clearOnInit);
2788
- return _DebugLogger.instance;
2805
+ },
2806
+ {
2807
+ name: TOOL_NAMES.GET_STATE,
2808
+ description: "Get current engagement state summary",
2809
+ parameters: {},
2810
+ execute: async () => ({ success: true, output: state.toPrompt() })
2789
2811
  }
2790
- log(category, message, data) {
2791
- if (!this.initialized || !this.logPath) return;
2792
- try {
2793
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2794
- let logLine = `[${timestamp}] [${category.toUpperCase()}] ${message}`;
2795
- if (data !== void 0) {
2796
- logLine += ` | ${JSON.stringify(data)}`;
2797
- }
2812
+ ];
2813
+
2814
+ // src/engine/tools/pentest-target-tools.ts
2815
+ var createTargetTools = (state) => [
2816
+ {
2817
+ name: TOOL_NAMES.ADD_TARGET,
2818
+ description: `Register a new target for the engagement.
2819
+ Use this when you discover new hosts during recon, pivoting, or post-exploitation.
2820
+ The target will be tracked in SharedState and available for all agents.`,
2821
+ parameters: {
2822
+ ip: { type: "string", description: "Target IP address" },
2823
+ hostname: { type: "string", description: "Target hostname (optional)" },
2824
+ ports: {
2825
+ type: "array",
2826
+ items: {
2827
+ type: "object",
2828
+ properties: {
2829
+ port: { type: "number", description: "Port number" },
2830
+ service: { type: "string", description: "Service name (e.g., http, ssh)" },
2831
+ version: { type: "string", description: "Service version" },
2832
+ state: { type: "string", description: "Port state (default: open)" }
2833
+ },
2834
+ required: ["port", "service"]
2835
+ },
2836
+ description: "Discovered ports (optional, can add later via recon)"
2837
+ },
2838
+ tags: {
2839
+ type: "array",
2840
+ items: { type: "string" },
2841
+ description: 'Tags for categorization (e.g., "internal", "dmz", "pivot")'
2842
+ }
2843
+ },
2844
+ required: ["ip"],
2845
+ execute: async (p) => {
2846
+ const ip = p.ip;
2847
+ const existing = state.getTarget(ip);
2848
+ if (existing) {
2849
+ const newPorts = p.ports || [];
2850
+ for (const np of newPorts) {
2851
+ const exists = existing.ports.some((ep) => ep.port === np.port);
2852
+ if (!exists) {
2853
+ existing.ports.push({
2854
+ port: np.port,
2855
+ service: np.service || "unknown",
2856
+ version: np.version,
2857
+ state: np.state || "open",
2858
+ notes: []
2859
+ });
2860
+ }
2861
+ }
2862
+ if (p.hostname) existing.hostname = p.hostname;
2863
+ if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...p.tags])];
2864
+ return { success: true, output: `Target ${ip} updated. Total ports: ${existing.ports.length}` };
2865
+ }
2866
+ const ports = (p.ports || []).map((port) => ({
2867
+ port: port.port,
2868
+ service: port.service || "unknown",
2869
+ version: port.version,
2870
+ state: port.state || "open",
2871
+ notes: []
2872
+ }));
2873
+ state.addTarget({
2874
+ ip,
2875
+ hostname: p.hostname,
2876
+ ports,
2877
+ tags: p.tags || [],
2878
+ firstSeen: Date.now()
2879
+ });
2880
+ return { success: true, output: `Target ${ip} added.${p.hostname ? ` Hostname: ${p.hostname}` : ""} Ports: ${ports.length}` };
2881
+ }
2882
+ },
2883
+ {
2884
+ name: TOOL_NAMES.ADD_LOOT,
2885
+ description: `Record captured loot (credentials, hashes, tokens, SSH keys, files, etc).
2886
+ Use this whenever you discover sensitive data during the engagement.
2887
+ Loot is tracked in SharedState and visible to all agents for credential reuse and lateral movement.
2888
+ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate`,
2889
+ parameters: {
2890
+ type: { type: "string", description: "Loot type: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate" },
2891
+ host: { type: "string", description: "Source host where loot was found" },
2892
+ detail: { type: "string", description: 'Loot detail (e.g., "admin:Password123", "root:$6$...", "JWT admin token")' }
2893
+ },
2894
+ required: ["type", "host", "detail"],
2895
+ execute: async (p) => {
2896
+ const lootType = p.type;
2897
+ const crackableTypes = ["hash"];
2898
+ state.addLoot({
2899
+ type: lootType,
2900
+ host: p.host,
2901
+ detail: p.detail,
2902
+ obtainedAt: Date.now(),
2903
+ isCrackable: crackableTypes.includes(lootType),
2904
+ isCracked: false
2905
+ });
2906
+ return {
2907
+ success: true,
2908
+ output: `Loot recorded: [${lootType}] from ${p.host}
2909
+ Detail: ${p.detail}
2910
+ ` + (crackableTypes.includes(lootType) ? `This is crackable. Consider: hash_crack({ hashes: "${p.detail.slice(0, 30)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
2911
+ };
2912
+ }
2913
+ },
2914
+ {
2915
+ name: TOOL_NAMES.ADD_FINDING,
2916
+ description: "Add a security finding",
2917
+ parameters: {
2918
+ title: { type: "string", description: "Finding title" },
2919
+ severity: { type: "string", description: "Severity" },
2920
+ affected: { type: "array", items: { type: "string" }, description: "Affected host:port" }
2921
+ },
2922
+ required: ["title", "severity"],
2923
+ execute: async (p) => {
2924
+ state.addFinding({
2925
+ id: generateId(ID_RADIX, ID_LENGTH),
2926
+ title: p.title,
2927
+ severity: p.severity,
2928
+ affected: p.affected || [],
2929
+ description: p.description || "",
2930
+ evidence: [],
2931
+ isVerified: false,
2932
+ remediation: "",
2933
+ foundAt: Date.now()
2934
+ });
2935
+ return { success: true, output: `Added: ${p.title}` };
2936
+ }
2937
+ }
2938
+ ];
2939
+
2940
+ // src/engine/tools-mid.ts
2941
+ import { execFileSync } from "child_process";
2942
+
2943
+ // src/shared/utils/config.ts
2944
+ import path from "path";
2945
+ import { fileURLToPath } from "url";
2946
+ var __filename = fileURLToPath(import.meta.url);
2947
+ var __dirname = path.dirname(__filename);
2948
+ var ENV_KEYS = {
2949
+ API_KEY: "PENTEST_API_KEY",
2950
+ BASE_URL: "PENTEST_BASE_URL",
2951
+ MODEL: "PENTEST_MODEL",
2952
+ SEARCH_API_KEY: "SEARCH_API_KEY",
2953
+ SEARCH_API_URL: "SEARCH_API_URL"
2954
+ };
2955
+ var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
2956
+ function getApiKey() {
2957
+ return process.env[ENV_KEYS.API_KEY] || "";
2958
+ }
2959
+ function getBaseUrl() {
2960
+ return process.env[ENV_KEYS.BASE_URL] || void 0;
2961
+ }
2962
+ function getModel() {
2963
+ return process.env[ENV_KEYS.MODEL] || "";
2964
+ }
2965
+ function getSearchApiKey() {
2966
+ return process.env[ENV_KEYS.SEARCH_API_KEY];
2967
+ }
2968
+ function getSearchApiUrl() {
2969
+ return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
2970
+ }
2971
+ function isBrowserHeadless() {
2972
+ return true;
2973
+ }
2974
+
2975
+ // src/shared/constants/search-api.const.ts
2976
+ var SEARCH_URL_PATTERN = {
2977
+ GLM: "bigmodel.cn",
2978
+ ZHIPU: "zhipuai",
2979
+ BRAVE: "brave.com",
2980
+ SERPER: "serper.dev"
2981
+ };
2982
+ var SEARCH_HEADER = {
2983
+ CONTENT_TYPE: "Content-Type",
2984
+ AUTHORIZATION: "Authorization",
2985
+ ACCEPT: "Accept",
2986
+ X_SUBSCRIPTION_TOKEN: "X-Subscription-Token",
2987
+ X_API_KEY: "X-API-KEY"
2988
+ };
2989
+ var SEARCH_LIMIT = {
2990
+ DEFAULT_RESULT_COUNT: 10,
2991
+ MAX_OUTPUT_LINES: 20,
2992
+ TIMEOUT_MS: 1e4
2993
+ };
2994
+
2995
+ // src/shared/utils/debug-logger.ts
2996
+ import { appendFileSync as appendFileSync2, writeFileSync as writeFileSync3 } from "fs";
2997
+ import { join as join2 } from "path";
2998
+ var DebugLogger = class _DebugLogger {
2999
+ static instance;
3000
+ logPath;
3001
+ initialized = false;
3002
+ constructor(clearOnInit = false) {
3003
+ const debugDir = join2(process.cwd(), ".pentesting", "debug");
3004
+ try {
3005
+ ensureDirExists(debugDir);
3006
+ this.logPath = join2(debugDir, "debug.log");
3007
+ if (clearOnInit) {
3008
+ this.clear();
3009
+ }
3010
+ this.initialized = true;
3011
+ this.log("general", "=== DEBUG LOGGER INITIALIZED ===");
3012
+ this.log("general", `Log file: ${this.logPath}`);
3013
+ } catch (e) {
3014
+ console.error("[DebugLogger] Failed to initialize:", e);
3015
+ this.logPath = "";
3016
+ }
3017
+ }
3018
+ static getInstance(clearOnInit = false) {
3019
+ if (!_DebugLogger.instance) {
3020
+ _DebugLogger.instance = new _DebugLogger(clearOnInit);
3021
+ }
3022
+ return _DebugLogger.instance;
3023
+ }
3024
+ /** Reset the singleton instance (used by initDebugLogger) */
3025
+ static resetInstance(clearOnInit = false) {
3026
+ _DebugLogger.instance = new _DebugLogger(clearOnInit);
3027
+ return _DebugLogger.instance;
3028
+ }
3029
+ log(category, message, data) {
3030
+ if (!this.initialized || !this.logPath) return;
3031
+ try {
3032
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3033
+ let logLine = `[${timestamp}] [${category.toUpperCase()}] ${message}`;
3034
+ if (data !== void 0) {
3035
+ logLine += ` | ${JSON.stringify(data)}`;
3036
+ }
2798
3037
  logLine += "\n";
2799
3038
  appendFileSync2(this.logPath, logLine);
2800
3039
  } catch (e) {
@@ -2832,10 +3071,7 @@ function debugLog(category, message, data) {
2832
3071
  }
2833
3072
 
2834
3073
  // src/engine/tools/web-browser.ts
2835
- import { spawn as spawn3 } from "child_process";
2836
- import { writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "fs";
2837
- import { join as join3, dirname } from "path";
2838
- import { tmpdir as tmpdir2 } from "os";
3074
+ import { join as join5, tmpdir as tmpdir3 } from "path";
2839
3075
 
2840
3076
  // src/shared/constants/browser/config.ts
2841
3077
  var BROWSER_CONFIG = {
@@ -2883,11 +3119,14 @@ var PLAYWRIGHT_SCRIPT = {
2883
3119
  SPAWN_TIMEOUT_BUFFER_MS: 1e4
2884
3120
  };
2885
3121
 
2886
- // src/engine/tools/web-browser.ts
3122
+ // src/engine/tools/web-browser-setup.ts
3123
+ import { spawn as spawn3 } from "child_process";
3124
+ import { existsSync as existsSync4 } from "fs";
3125
+ import { join as join3, dirname as dirname2 } from "path";
2887
3126
  function getPlaywrightPath() {
2888
3127
  try {
2889
3128
  const mainPath = __require.resolve("playwright");
2890
- return dirname(mainPath);
3129
+ return dirname2(mainPath);
2891
3130
  } catch {
2892
3131
  }
2893
3132
  const dockerPath = "/app/node_modules/playwright";
@@ -2899,16 +3138,6 @@ function getPlaywrightPath() {
2899
3138
  }
2900
3139
  return join3(process.cwd(), "node_modules", "playwright");
2901
3140
  }
2902
- var DEFAULT_OPTIONS = {
2903
- timeout: BROWSER_CONFIG.DEFAULT_TIMEOUT,
2904
- userAgent: BROWSER_CONFIG.DEFAULT_USER_AGENT,
2905
- viewport: BROWSER_CONFIG.VIEWPORT,
2906
- waitAfterLoad: BROWSER_CONFIG.DEFAULT_WAIT_AFTER_LOAD,
2907
- screenshot: false,
2908
- extractContent: true,
2909
- extractLinks: true,
2910
- extractForms: true
2911
- };
2912
3141
  async function checkPlaywright() {
2913
3142
  try {
2914
3143
  const result2 = await new Promise((resolve) => {
@@ -2955,53 +3184,19 @@ async function installPlaywright() {
2955
3184
  });
2956
3185
  });
2957
3186
  }
2958
- async function browseUrl(url, options = {}) {
2959
- const opts = { ...DEFAULT_OPTIONS, ...options };
2960
- const { installed, browserInstalled } = await checkPlaywright();
2961
- if (!installed || !browserInstalled) {
2962
- const installResult = await installPlaywright();
2963
- if (!installResult.success) {
2964
- return {
2965
- success: false,
2966
- output: "",
2967
- error: `Playwright not available and auto-install failed: ${installResult.output}`
2968
- };
2969
- }
2970
- }
2971
- const screenshotPath = opts.screenshot ? join3(join3(tmpdir2(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
2972
- const script = buildBrowseScript(url, opts, screenshotPath);
2973
- const result2 = await runPlaywrightScript(script, opts.timeout, "browse");
2974
- if (!result2.success) {
2975
- return {
2976
- success: false,
2977
- output: result2.output,
2978
- error: result2.error
2979
- };
2980
- }
2981
- if (result2.parsedData) {
2982
- return {
2983
- success: true,
2984
- output: formatBrowserOutput(result2.parsedData, opts),
2985
- screenshots: screenshotPath ? [screenshotPath] : void 0,
2986
- extractedData: result2.parsedData
2987
- };
2988
- }
2989
- return {
2990
- success: true,
2991
- output: result2.output || "Navigation completed",
2992
- screenshots: screenshotPath ? [screenshotPath] : void 0
2993
- };
2994
- }
3187
+
3188
+ // src/engine/tools/web-browser-script.ts
3189
+ import { spawn as spawn4 } from "child_process";
3190
+ import { writeFileSync as writeFileSync4, unlinkSync as unlinkSync2 } from "fs";
3191
+ import { join as join4, tmpdir as tmpdir2 } from "path";
2995
3192
  function safeJsString(str) {
2996
3193
  return JSON.stringify(str);
2997
3194
  }
2998
3195
  function runPlaywrightScript(script, timeout, scriptPrefix) {
2999
3196
  return new Promise((resolve) => {
3000
- const tempDir = join3(tmpdir2(), BROWSER_PATHS.TEMP_DIR_NAME);
3001
- if (!existsSync4(tempDir)) {
3002
- mkdirSync3(tempDir, { recursive: true });
3003
- }
3004
- const scriptPath = join3(tempDir, `${scriptPrefix}-${Date.now()}${PLAYWRIGHT_SCRIPT.EXTENSION}`);
3197
+ const tempDir = join4(tmpdir2(), BROWSER_PATHS.TEMP_DIR_NAME);
3198
+ ensureDirExists(tempDir);
3199
+ const scriptPath = join4(tempDir, `${scriptPrefix}-${Date.now()}${PLAYWRIGHT_SCRIPT.EXTENSION}`);
3005
3200
  try {
3006
3201
  writeFileSync4(scriptPath, script, "utf-8");
3007
3202
  } catch (err) {
@@ -3014,10 +3209,10 @@ function runPlaywrightScript(script, timeout, scriptPrefix) {
3014
3209
  }
3015
3210
  const nodePathDirs = [
3016
3211
  "/app/node_modules",
3017
- join3(process.cwd(), "node_modules"),
3212
+ join4(process.cwd(), "node_modules"),
3018
3213
  process.env.NODE_PATH || ""
3019
3214
  ].filter(Boolean).join(":");
3020
- const child = spawn3(PLAYWRIGHT_CMD.NODE, [scriptPath], {
3215
+ const child = spawn4(PLAYWRIGHT_CMD.NODE, [scriptPath], {
3021
3216
  timeout: timeout + PLAYWRIGHT_SCRIPT.SPAWN_TIMEOUT_BUFFER_MS,
3022
3217
  env: { ...process.env, NODE_PATH: nodePathDirs }
3023
3218
  });
@@ -3192,15 +3387,66 @@ function formatBrowserOutput(data, options) {
3192
3387
  lines.push("");
3193
3388
  });
3194
3389
  }
3195
- return lines.join("\n");
3390
+ return lines.join("\n");
3391
+ }
3392
+
3393
+ // src/engine/tools/web-browser-types.ts
3394
+ var DEFAULT_BROWSER_OPTIONS = {
3395
+ timeout: BROWSER_CONFIG.DEFAULT_TIMEOUT,
3396
+ userAgent: BROWSER_CONFIG.DEFAULT_USER_AGENT,
3397
+ viewport: BROWSER_CONFIG.VIEWPORT,
3398
+ waitAfterLoad: BROWSER_CONFIG.DEFAULT_WAIT_AFTER_LOAD,
3399
+ screenshot: false,
3400
+ extractContent: true,
3401
+ extractLinks: true,
3402
+ extractForms: true
3403
+ };
3404
+
3405
+ // src/engine/tools/web-browser.ts
3406
+ async function browseUrl(url, options = {}) {
3407
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
3408
+ const { installed, browserInstalled } = await checkPlaywright();
3409
+ if (!installed || !browserInstalled) {
3410
+ const installResult = await installPlaywright();
3411
+ if (!installResult.success) {
3412
+ return {
3413
+ success: false,
3414
+ output: "",
3415
+ error: `Playwright not available and auto-install failed: ${installResult.output}`
3416
+ };
3417
+ }
3418
+ }
3419
+ const screenshotPath = browserOptions.screenshot ? join5(join5(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
3420
+ const script = buildBrowseScript(url, browserOptions, screenshotPath);
3421
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "browse");
3422
+ if (!result2.success) {
3423
+ return {
3424
+ success: false,
3425
+ output: result2.output,
3426
+ error: result2.error
3427
+ };
3428
+ }
3429
+ if (result2.parsedData) {
3430
+ return {
3431
+ success: true,
3432
+ output: formatBrowserOutput(result2.parsedData, browserOptions),
3433
+ screenshots: screenshotPath ? [screenshotPath] : void 0,
3434
+ extractedData: result2.parsedData
3435
+ };
3436
+ }
3437
+ return {
3438
+ success: true,
3439
+ output: result2.output || "Navigation completed",
3440
+ screenshots: screenshotPath ? [screenshotPath] : void 0
3441
+ };
3196
3442
  }
3197
3443
  async function fillAndSubmitForm(url, formData, options = {}) {
3198
- const opts = { ...DEFAULT_OPTIONS, ...options };
3444
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
3199
3445
  const safeUrl = safeJsString(url);
3200
3446
  const safeFormData = JSON.stringify(formData);
3201
3447
  const playwrightPath = getPlaywrightPath();
3202
3448
  const safePlaywrightPath = safeJsString(playwrightPath);
3203
- const headlessMode = isBrowserHeadless();
3449
+ const headlessMode = process.env.HEADLESS !== "false";
3204
3450
  const script = `
3205
3451
  const { chromium } = require(${safePlaywrightPath});
3206
3452
 
@@ -3209,7 +3455,7 @@ const { chromium } = require(${safePlaywrightPath});
3209
3455
  const page = await browser.newPage();
3210
3456
 
3211
3457
  try {
3212
- await page.goto(${safeUrl}, { waitUntil: 'networkidle', timeout: ${opts.timeout} });
3458
+ await page.goto(${safeUrl}, { waitUntil: 'networkidle', timeout: ${browserOptions.timeout} });
3213
3459
 
3214
3460
  // Fill form fields
3215
3461
  const formData = ${safeFormData};
@@ -3241,7 +3487,7 @@ const { chromium } = require(${safePlaywrightPath});
3241
3487
  }
3242
3488
  })();
3243
3489
  `;
3244
- const result2 = await runPlaywrightScript(script, opts.timeout, "form");
3490
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
3245
3491
  if (!result2.success) {
3246
3492
  return {
3247
3493
  success: false,
@@ -3764,6 +4010,260 @@ Use 'get_owasp_knowledge' with edition="2023" or similar to see specific details
3764
4010
  `.trim();
3765
4011
  }
3766
4012
 
4013
+ // src/engine/tools/pentest-intel-tools.ts
4014
+ var createIntelTools = (_state) => [
4015
+ {
4016
+ name: TOOL_NAMES.PARSE_NMAP,
4017
+ description: "Parse nmap XML output to structured JSON",
4018
+ parameters: { path: { type: "string", description: "Path to nmap XML" } },
4019
+ required: ["path"],
4020
+ execute: async (p) => parseNmap(p.path)
4021
+ },
4022
+ {
4023
+ name: TOOL_NAMES.SEARCH_CVE,
4024
+ description: "Search CVE and Exploit-DB for vulnerabilities",
4025
+ parameters: {
4026
+ service: { type: "string", description: "Service name" },
4027
+ version: { type: "string", description: "Version number" }
4028
+ },
4029
+ required: ["service"],
4030
+ execute: async (p) => searchCVE(p.service, p.version)
4031
+ },
4032
+ {
4033
+ name: TOOL_NAMES.WEB_SEARCH,
4034
+ description: `Search the web for information. Use this to:
4035
+ - Find CVE details and exploit code
4036
+ - Research security advisories
4037
+ - Look up OWASP vulnerabilities
4038
+ - Find documentation for tools and techniques
4039
+ - Search for latest security news and exploits
4040
+ - Research unknown services, parameters, or errors
4041
+
4042
+ IMPORTANT: When you encounter an error or unknown behavior, use this tool to research it.
4043
+ Returns search results with links and summaries.`,
4044
+ parameters: {
4045
+ query: { type: "string", description: "Search query" },
4046
+ use_browser: {
4047
+ type: "boolean",
4048
+ description: "Use headless browser for JavaScript-heavy pages (slower but more complete)"
4049
+ }
4050
+ },
4051
+ required: ["query"],
4052
+ execute: async (p) => {
4053
+ const query = p.query;
4054
+ const useBrowser = p.use_browser;
4055
+ if (useBrowser) {
4056
+ const result2 = await webSearchWithBrowser(query, "google");
4057
+ return {
4058
+ success: result2.success,
4059
+ output: result2.output,
4060
+ error: result2.error
4061
+ };
4062
+ }
4063
+ return webSearch(query);
4064
+ }
4065
+ },
4066
+ {
4067
+ name: TOOL_NAMES.BROWSE_URL,
4068
+ description: `Navigate to a URL using a headless browser (Playwright).
4069
+ Use this for:
4070
+ - Browsing JavaScript-heavy websites
4071
+ - Extracting links, forms, and content
4072
+ - Viewing security advisories
4073
+ - Accessing documentation pages
4074
+ - Analyzing web application structure
4075
+ - Discovering web attack surface (forms, inputs, API endpoints)
4076
+
4077
+ Can extract forms and inputs for security testing.`,
4078
+ parameters: {
4079
+ url: { type: "string", description: "URL to browse" },
4080
+ extract_forms: {
4081
+ type: "boolean",
4082
+ description: "Extract form information (inputs, actions, methods)"
4083
+ },
4084
+ extract_links: {
4085
+ type: "boolean",
4086
+ description: "Extract all links from the page"
4087
+ },
4088
+ screenshot: {
4089
+ type: "boolean",
4090
+ description: "Take a screenshot of the page"
4091
+ },
4092
+ extra_headers: {
4093
+ type: "object",
4094
+ description: 'Custom HTTP headers (e.g., {"X-Forwarded-For": "127.0.0.1"})',
4095
+ additionalProperties: { type: "string" }
4096
+ }
4097
+ },
4098
+ required: ["url"],
4099
+ execute: async (p) => {
4100
+ const result2 = await browseUrl(p.url, {
4101
+ extractForms: p.extract_forms,
4102
+ extractLinks: p.extract_links,
4103
+ screenshot: p.screenshot,
4104
+ extraHeaders: p.extra_headers
4105
+ });
4106
+ return {
4107
+ success: result2.success,
4108
+ output: result2.output,
4109
+ error: result2.error
4110
+ };
4111
+ }
4112
+ },
4113
+ {
4114
+ name: TOOL_NAMES.FILL_FORM,
4115
+ description: `Fill and submit a web form. Use this for:
4116
+ - Testing form-based authentication
4117
+ - Submitting search forms
4118
+ - Testing for form injection vulnerabilities
4119
+ - Automated form interaction`,
4120
+ parameters: {
4121
+ url: { type: "string", description: "URL containing the form" },
4122
+ fields: {
4123
+ type: "object",
4124
+ description: "Form field names and values to fill",
4125
+ additionalProperties: { type: "string" }
4126
+ }
4127
+ },
4128
+ required: ["url", "fields"],
4129
+ execute: async (p) => {
4130
+ const result2 = await fillAndSubmitForm(p.url, p.fields);
4131
+ return {
4132
+ success: result2.success,
4133
+ output: result2.output,
4134
+ error: result2.error
4135
+ };
4136
+ }
4137
+ },
4138
+ {
4139
+ name: TOOL_NAMES.GET_OWASP_KNOWLEDGE,
4140
+ description: `Get OWASP Top 10 vulnerability knowledge (2017, 2021, 2025 editions).
4141
+ Returns structured information about:
4142
+ - OWASP Top 10 category details (detection methods, test payloads, tools)
4143
+ - Common vulnerability patterns
4144
+ - Attack methodology per category
4145
+ - Recommended tools for each category
4146
+
4147
+ Available editions: "2025" (latest), "2021", "2017", "all"
4148
+ When attacking web services, ALWAYS start with get_web_attack_surface first, then use this.`,
4149
+ parameters: {
4150
+ edition: {
4151
+ type: "string",
4152
+ description: 'OWASP edition or year: "2017" through "2025", or "all"',
4153
+ enum: ["2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025", "all"]
4154
+ },
4155
+ category: {
4156
+ type: "string",
4157
+ description: 'Specific category (e.g., "A01", "API1", "LLM01", "K01").'
4158
+ }
4159
+ },
4160
+ execute: async (p) => {
4161
+ const edition = p.edition || "all";
4162
+ const category = p.category;
4163
+ if (category) {
4164
+ const results = [];
4165
+ for (const [year, data2] of Object.entries(OWASP_FULL_HISTORY)) {
4166
+ const catData = data2[category];
4167
+ if (catData) {
4168
+ results.push(`## OWASP ${year} ${category}
4169
+ ${JSON.stringify(catData, null, 2)}`);
4170
+ }
4171
+ }
4172
+ if (results.length > 0) {
4173
+ return {
4174
+ success: true,
4175
+ output: `# OWASP Category: ${category}
4176
+
4177
+ ${results.join("\n\n")}`
4178
+ };
4179
+ }
4180
+ return {
4181
+ success: false,
4182
+ output: `Category ${category} not found in any edition.`
4183
+ };
4184
+ }
4185
+ if (edition === "all") {
4186
+ return {
4187
+ success: true,
4188
+ output: getOWASPSummary()
4189
+ };
4190
+ }
4191
+ const data = OWASP_FULL_HISTORY[edition];
4192
+ if (!data) {
4193
+ return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.` };
4194
+ }
4195
+ return {
4196
+ success: true,
4197
+ output: getOWASPForYear(edition)
4198
+ };
4199
+ }
4200
+ },
4201
+ {
4202
+ name: TOOL_NAMES.GET_WEB_ATTACK_SURFACE,
4203
+ description: `Get the web attack surface discovery protocol.
4204
+ ALWAYS call this FIRST when the target is a web service (HTTP/HTTPS).
4205
+ Returns a step-by-step guide for:
4206
+ - Fingerprinting the web server and technology stack
4207
+ - Discovering directories, files, and API endpoints
4208
+ - Using Playwright headless browser for deep inspection
4209
+ - Systematic OWASP 2025 testing methodology
4210
+ - When and how to use web_search for unknown things`,
4211
+ parameters: {},
4212
+ execute: async () => ({
4213
+ success: true,
4214
+ output: getWebAttackSurface()
4215
+ })
4216
+ },
4217
+ {
4218
+ name: TOOL_NAMES.GET_CVE_INFO,
4219
+ description: `Get information about known CVEs. Use this to:
4220
+ - Look up vulnerability details
4221
+ - Check if a service version has known vulnerabilities
4222
+ - Get exploit information
4223
+
4224
+ Includes critical CVEs like Log4Shell, Spring4Shell, EternalBlue, XZ Backdoor, etc.
4225
+ If CVE not found locally, use web_search to find it online.`,
4226
+ parameters: {
4227
+ cve_id: {
4228
+ type: "string",
4229
+ description: 'CVE identifier (e.g., "CVE-2021-44228") or "list" to see all known CVEs'
4230
+ }
4231
+ },
4232
+ required: ["cve_id"],
4233
+ execute: async (p) => {
4234
+ const cveId = p.cve_id;
4235
+ if (cveId.toLowerCase() === "list") {
4236
+ const list = Object.entries(CVE_DATABASE).map(([id, info]) => `- ${id}: ${info.name} (${info.severity}) - ${info.affected}`).join("\n");
4237
+ return {
4238
+ success: true,
4239
+ output: `# Known CVEs in Database
4240
+
4241
+ ${list}
4242
+
4243
+ Use the specific CVE ID to get more details.
4244
+ For CVEs not in this list, use web_search to find them.`
4245
+ };
4246
+ }
4247
+ const cve = CVE_DATABASE[cveId.toUpperCase()];
4248
+ if (cve) {
4249
+ return {
4250
+ success: true,
4251
+ output: `# ${cveId}
4252
+
4253
+ **Name:** ${cve.name}
4254
+ **Severity:** ${cve.severity}
4255
+ **Affected:** ${cve.affected}
4256
+ **Description:** ${cve.description}`
4257
+ };
4258
+ }
4259
+ return {
4260
+ success: true,
4261
+ output: `CVE ${cveId} not in local database. Use web_search({ query: "${cveId} exploit POC" }) to find more information online.`
4262
+ };
4263
+ }
4264
+ }
4265
+ ];
4266
+
3767
4267
  // src/shared/utils/payload-mutator.ts
3768
4268
  function urlEncode(s) {
3769
4269
  return [...s].map((c) => {
@@ -4058,464 +4558,47 @@ function getContextRecommendations(context, variantCount) {
4058
4558
  recs.push("Try: echo PAYLOAD_BASE64 | base64 -d | sh");
4059
4559
  recs.push("Variable concatenation: a=c;b=at;$a$b /etc/passwd");
4060
4560
  break;
4061
- }
4062
- recs.push("If all variants fail: web_search for latest bypass techniques for this specific filter type");
4063
- return recs;
4064
- }
4065
-
4066
- // src/engine/tools/pentest.ts
4067
- var createPentestTools = (state) => [
4068
- {
4069
- name: TOOL_NAMES.UPDATE_MISSION,
4070
- description: `Update the mission summary and checklist.
4071
- Use this for strategic planning and maintaining long-term context during complex engagements.
4072
- The mission summary acts as your "long-term memory" \u2014 keep it updated with critical findings and overall progress.
4073
- The checklist tracks granular steps (e.g., "[ ] Crack hashes", "[x] Recon 10.10.10.5").
4074
- Mandatory when:
4075
- - Big goals change
4076
- - You encounter a major obstacle (firewall, port conflict)
4077
- - You gain a significant new access point (reverse shell)
4078
- - You need to summarize findings to prevent context overflow`,
4079
- parameters: {
4080
- summary: { type: "string", description: "Updated mission summary (concise overview)" },
4081
- checklist_updates: {
4082
- type: "array",
4083
- items: {
4084
- type: "object",
4085
- properties: {
4086
- id: { type: "string", description: "Item ID (required for update/remove)" },
4087
- text: { type: "string", description: "Updated text" },
4088
- completed: { type: "boolean", description: "Completion status" },
4089
- remove: { type: "boolean", description: "Remove item if true" }
4090
- },
4091
- required: ["id"]
4092
- },
4093
- description: "Updates to existing checklist items"
4094
- },
4095
- add_items: {
4096
- type: "array",
4097
- items: { type: "string" },
4098
- description: "New checklist items to add"
4099
- }
4100
- },
4101
- execute: async (p) => {
4102
- if (p.summary) state.setMissionSummary(p.summary);
4103
- if (p.checklist_updates) state.updateMissionChecklist(p.checklist_updates);
4104
- if (p.add_items) state.addMissionChecklistItems(p.add_items);
4105
- return {
4106
- success: true,
4107
- output: `Mission updated.
4108
- Summary: ${state.getMissionSummary()}
4109
- Checklist Items: ${state.getMissionChecklist().length}`
4110
- };
4111
- }
4112
- },
4113
- {
4114
- name: TOOL_NAMES.HASH_CRACK,
4115
- description: `Crack password hashes using hashcat or john.
4116
- Runs as a background process by default. Use bg_process status to check progress.
4117
- Common wordlists are automatically searched in /usr/share/wordlists (rockyou.txt, etc).`,
4118
- parameters: {
4119
- hashes: { type: "string", description: "Hash(es) to crack (raw or file path)" },
4120
- format: { type: "string", description: "Hash format (e.g., md5, sha1, nt, mscash2). Omit for auto-detect." },
4121
- wordlist: { type: "string", description: "Wordlist name or path (default: rockyou)" },
4122
- background: { type: "boolean", description: "Run in background. Default: true" }
4123
- },
4124
- required: ["hashes"],
4125
- execute: async (p) => {
4126
- const hashes = p.hashes;
4127
- const format = p.format;
4128
- const wordlist = p.wordlist || "rockyou";
4129
- const background = p.background !== false;
4130
- let wordlistPath = wordlist;
4131
- if (wordlist === "rockyou") wordlistPath = WORDLISTS.ROCKYOU;
4132
- const cmd = `hashcat -m ${format || HASHCAT_MODES.MD5} "${hashes}" "${wordlistPath}" --force`;
4133
- if (background) {
4134
- const proc = startBackgroundProcess(cmd, {
4135
- description: `Cracking hashes: ${hashes.slice(0, 20)}...`,
4136
- purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
4137
- });
4138
- return {
4139
- success: true,
4140
- output: `Hash cracking started in background (ID: ${proc.id}).
4141
- Command: ${cmd}
4142
- Check status with: bg_process({ action: "status", process_id: "${proc.id}" })`
4143
- };
4144
- } else {
4145
- return runCommand(cmd);
4146
- }
4147
- }
4148
- },
4149
- {
4150
- name: TOOL_NAMES.PARSE_NMAP,
4151
- description: "Parse nmap XML output to structured JSON",
4152
- parameters: { path: { type: "string", description: "Path to nmap XML" } },
4153
- required: ["path"],
4154
- execute: async (p) => parseNmap(p.path)
4155
- },
4156
- {
4157
- name: TOOL_NAMES.SEARCH_CVE,
4158
- description: "Search CVE and Exploit-DB for vulnerabilities",
4159
- parameters: {
4160
- service: { type: "string", description: "Service name" },
4161
- version: { type: "string", description: "Version number" }
4162
- },
4163
- required: ["service"],
4164
- execute: async (p) => searchCVE(p.service, p.version)
4165
- },
4166
- {
4167
- name: TOOL_NAMES.WEB_SEARCH,
4168
- description: `Search the web for information. Use this to:
4169
- - Find CVE details and exploit code
4170
- - Research security advisories
4171
- - Look up OWASP vulnerabilities
4172
- - Find documentation for tools and techniques
4173
- - Search for latest security news and exploits
4174
- - Research unknown services, parameters, or errors
4175
-
4176
- IMPORTANT: When you encounter an error or unknown behavior, use this tool to research it.
4177
- Returns search results with links and summaries.`,
4178
- parameters: {
4179
- query: { type: "string", description: "Search query" },
4180
- use_browser: {
4181
- type: "boolean",
4182
- description: "Use headless browser for JavaScript-heavy pages (slower but more complete)"
4183
- }
4184
- },
4185
- required: ["query"],
4186
- execute: async (p) => {
4187
- const query = p.query;
4188
- const useBrowser = p.use_browser;
4189
- if (useBrowser) {
4190
- const result2 = await webSearchWithBrowser(query, "google");
4191
- return {
4192
- success: result2.success,
4193
- output: result2.output,
4194
- error: result2.error
4195
- };
4196
- }
4197
- return webSearch(query);
4198
- }
4199
- },
4200
- {
4201
- name: TOOL_NAMES.BROWSE_URL,
4202
- description: `Navigate to a URL using a headless browser (Playwright).
4203
- Use this for:
4204
- - Browsing JavaScript-heavy websites
4205
- - Extracting links, forms, and content
4206
- - Viewing security advisories
4207
- - Accessing documentation pages
4208
- - Analyzing web application structure
4209
- - Discovering web attack surface (forms, inputs, API endpoints)
4210
-
4211
- Can extract forms and inputs for security testing.`,
4212
- parameters: {
4213
- url: { type: "string", description: "URL to browse" },
4214
- extract_forms: {
4215
- type: "boolean",
4216
- description: "Extract form information (inputs, actions, methods)"
4217
- },
4218
- extract_links: {
4219
- type: "boolean",
4220
- description: "Extract all links from the page"
4221
- },
4222
- screenshot: {
4223
- type: "boolean",
4224
- description: "Take a screenshot of the page"
4225
- },
4226
- extra_headers: {
4227
- type: "object",
4228
- description: 'Custom HTTP headers (e.g., {"X-Forwarded-For": "127.0.0.1"})',
4229
- additionalProperties: { type: "string" }
4230
- }
4231
- },
4232
- required: ["url"],
4233
- execute: async (p) => {
4234
- const result2 = await browseUrl(p.url, {
4235
- extractForms: p.extract_forms,
4236
- extractLinks: p.extract_links,
4237
- screenshot: p.screenshot,
4238
- extraHeaders: p.extra_headers
4239
- });
4240
- return {
4241
- success: result2.success,
4242
- output: result2.output,
4243
- error: result2.error
4244
- };
4245
- }
4246
- },
4247
- {
4248
- name: TOOL_NAMES.FILL_FORM,
4249
- description: `Fill and submit a web form. Use this for:
4250
- - Testing form-based authentication
4251
- - Submitting search forms
4252
- - Testing for form injection vulnerabilities
4253
- - Automated form interaction`,
4254
- parameters: {
4255
- url: { type: "string", description: "URL containing the form" },
4256
- fields: {
4257
- type: "object",
4258
- description: "Form field names and values to fill",
4259
- additionalProperties: { type: "string" }
4260
- }
4261
- },
4262
- required: ["url", "fields"],
4263
- execute: async (p) => {
4264
- const result2 = await fillAndSubmitForm(p.url, p.fields);
4265
- return {
4266
- success: result2.success,
4267
- output: result2.output,
4268
- error: result2.error
4269
- };
4270
- }
4271
- },
4272
- {
4273
- name: TOOL_NAMES.GET_OWASP_KNOWLEDGE,
4274
- description: `Get OWASP Top 10 vulnerability knowledge (2017, 2021, 2025 editions).
4275
- Returns structured information about:
4276
- - OWASP Top 10 category details (detection methods, test payloads, tools)
4277
- - Common vulnerability patterns
4278
- - Attack methodology per category
4279
- - Recommended tools for each category
4280
-
4281
- Available editions: "2025" (latest), "2021", "2017", "all"
4282
- When attacking web services, ALWAYS start with get_web_attack_surface first, then use this.`,
4283
- parameters: {
4284
- edition: {
4285
- type: "string",
4286
- description: 'OWASP edition or year: "2017" through "2025", or "all"',
4287
- enum: ["2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025", "all"]
4288
- },
4289
- category: {
4290
- type: "string",
4291
- description: 'Specific category (e.g., "A01", "API1", "LLM01", "K01").'
4292
- }
4293
- },
4294
- execute: async (p) => {
4295
- const edition = p.edition || "all";
4296
- const category = p.category;
4297
- if (category) {
4298
- const results = [];
4299
- for (const [year, data2] of Object.entries(OWASP_FULL_HISTORY)) {
4300
- const catData = data2[category];
4301
- if (catData) {
4302
- results.push(`## OWASP ${year} ${category}
4303
- ${JSON.stringify(catData, null, 2)}`);
4304
- }
4305
- }
4306
- if (results.length > 0) {
4307
- return {
4308
- success: true,
4309
- output: `# OWASP Category: ${category}
4561
+ }
4562
+ recs.push("If all variants fail: web_search for latest bypass techniques for this specific filter type");
4563
+ return recs;
4564
+ }
4310
4565
 
4311
- ${results.join("\n\n")}`
4312
- };
4313
- }
4314
- return {
4315
- success: false,
4316
- output: `Category ${category} not found in any edition.`
4317
- };
4318
- }
4319
- if (edition === "all") {
4320
- return {
4321
- success: true,
4322
- output: getOWASPSummary()
4323
- };
4324
- }
4325
- const data = OWASP_FULL_HISTORY[edition];
4326
- if (!data) {
4327
- return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.` };
4328
- }
4329
- return {
4330
- success: true,
4331
- output: getOWASPForYear(edition)
4332
- };
4333
- }
4334
- },
4335
- {
4336
- name: TOOL_NAMES.GET_WEB_ATTACK_SURFACE,
4337
- description: `Get the web attack surface discovery protocol.
4338
- ALWAYS call this FIRST when the target is a web service (HTTP/HTTPS).
4339
- Returns a step-by-step guide for:
4340
- - Fingerprinting the web server and technology stack
4341
- - Discovering directories, files, and API endpoints
4342
- - Using Playwright headless browser for deep inspection
4343
- - Systematic OWASP 2025 testing methodology
4344
- - When and how to use web_search for unknown things`,
4345
- parameters: {},
4346
- execute: async () => ({
4347
- success: true,
4348
- output: getWebAttackSurface()
4349
- })
4350
- },
4566
+ // src/engine/tools/pentest-attack-tools.ts
4567
+ var createAttackTools = (_state) => [
4351
4568
  {
4352
- name: TOOL_NAMES.GET_CVE_INFO,
4353
- description: `Get information about known CVEs. Use this to:
4354
- - Look up vulnerability details
4355
- - Check if a service version has known vulnerabilities
4356
- - Get exploit information
4357
-
4358
- Includes critical CVEs like Log4Shell, Spring4Shell, EternalBlue, XZ Backdoor, etc.
4359
- If CVE not found locally, use web_search to find it online.`,
4569
+ name: TOOL_NAMES.HASH_CRACK,
4570
+ description: `Crack password hashes using hashcat or john.
4571
+ Runs as a background process by default. Use bg_process status to check progress.
4572
+ Common wordlists are automatically searched in /usr/share/wordlists (rockyou.txt, etc).`,
4360
4573
  parameters: {
4361
- cve_id: {
4362
- type: "string",
4363
- description: 'CVE identifier (e.g., "CVE-2021-44228") or "list" to see all known CVEs'
4364
- }
4574
+ hashes: { type: "string", description: "Hash(es) to crack (raw or file path)" },
4575
+ format: { type: "string", description: "Hash format (e.g., md5, sha1, nt, mscash2). Omit for auto-detect." },
4576
+ wordlist: { type: "string", description: "Wordlist name or path (default: rockyou)" },
4577
+ background: { type: "boolean", description: "Run in background. Default: true" }
4365
4578
  },
4366
- required: ["cve_id"],
4579
+ required: ["hashes"],
4367
4580
  execute: async (p) => {
4368
- const cveId = p.cve_id;
4369
- if (cveId.toLowerCase() === "list") {
4370
- const list = Object.entries(CVE_DATABASE).map(([id, info]) => `- ${id}: ${info.name} (${info.severity}) - ${info.affected}`).join("\n");
4371
- return {
4372
- success: true,
4373
- output: `# Known CVEs in Database
4374
-
4375
- ${list}
4376
-
4377
- Use the specific CVE ID to get more details.
4378
- For CVEs not in this list, use web_search to find them.`
4379
- };
4380
- }
4381
- const cve = CVE_DATABASE[cveId.toUpperCase()];
4382
- if (cve) {
4581
+ const hashes = p.hashes;
4582
+ const format = p.format;
4583
+ const wordlist = p.wordlist || "rockyou";
4584
+ const background = p.background !== false;
4585
+ let wordlistPath = wordlist;
4586
+ if (wordlist === "rockyou") wordlistPath = WORDLISTS.ROCKYOU;
4587
+ const cmd = `hashcat -m ${format || HASHCAT_MODES.MD5} "${hashes}" "${wordlistPath}" --force`;
4588
+ if (background) {
4589
+ const proc = startBackgroundProcess(cmd, {
4590
+ description: `Cracking hashes: ${hashes.slice(0, 20)}...`,
4591
+ purpose: `Attempting to crack ${format || "unknown"} hashes using ${wordlist}`
4592
+ });
4383
4593
  return {
4384
4594
  success: true,
4385
- output: `# ${cveId}
4386
-
4387
- **Name:** ${cve.name}
4388
- **Severity:** ${cve.severity}
4389
- **Affected:** ${cve.affected}
4390
- **Description:** ${cve.description}`
4595
+ output: `Hash cracking started in background (ID: ${proc.id}).
4596
+ Command: ${cmd}
4597
+ Check status with: bg_process({ action: "status", process_id: "${proc.id}" })`
4391
4598
  };
4599
+ } else {
4600
+ return runCommand(cmd);
4392
4601
  }
4393
- return {
4394
- success: true,
4395
- output: `CVE ${cveId} not in local database. Use web_search({ query: "${cveId} exploit POC" }) to find more information online.`
4396
- };
4397
- }
4398
- },
4399
- {
4400
- name: TOOL_NAMES.ADD_TARGET,
4401
- description: `Register a new target for the engagement.
4402
- Use this when you discover new hosts during recon, pivoting, or post-exploitation.
4403
- The target will be tracked in SharedState and available for all agents.`,
4404
- parameters: {
4405
- ip: { type: "string", description: "Target IP address" },
4406
- hostname: { type: "string", description: "Target hostname (optional)" },
4407
- ports: {
4408
- type: "array",
4409
- items: {
4410
- type: "object",
4411
- properties: {
4412
- port: { type: "number", description: "Port number" },
4413
- service: { type: "string", description: "Service name (e.g., http, ssh)" },
4414
- version: { type: "string", description: "Service version" },
4415
- state: { type: "string", description: "Port state (default: open)" }
4416
- },
4417
- required: ["port", "service"]
4418
- },
4419
- description: "Discovered ports (optional, can add later via recon)"
4420
- },
4421
- tags: {
4422
- type: "array",
4423
- items: { type: "string" },
4424
- description: 'Tags for categorization (e.g., "internal", "dmz", "pivot")'
4425
- }
4426
- },
4427
- required: ["ip"],
4428
- execute: async (p) => {
4429
- const ip = p.ip;
4430
- const existing = state.getTarget(ip);
4431
- if (existing) {
4432
- const newPorts = p.ports || [];
4433
- for (const np of newPorts) {
4434
- const exists = existing.ports.some((ep) => ep.port === np.port);
4435
- if (!exists) {
4436
- existing.ports.push({
4437
- port: np.port,
4438
- service: np.service || "unknown",
4439
- version: np.version,
4440
- state: np.state || "open",
4441
- notes: []
4442
- });
4443
- }
4444
- }
4445
- if (p.hostname) existing.hostname = p.hostname;
4446
- if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...p.tags])];
4447
- return { success: true, output: `Target ${ip} updated. Total ports: ${existing.ports.length}` };
4448
- }
4449
- const ports = (p.ports || []).map((port) => ({
4450
- port: port.port,
4451
- service: port.service || "unknown",
4452
- version: port.version,
4453
- state: port.state || "open",
4454
- notes: []
4455
- }));
4456
- state.addTarget({
4457
- ip,
4458
- hostname: p.hostname,
4459
- ports,
4460
- tags: p.tags || [],
4461
- firstSeen: Date.now()
4462
- });
4463
- return { success: true, output: `Target ${ip} added.${p.hostname ? ` Hostname: ${p.hostname}` : ""} Ports: ${ports.length}` };
4464
- }
4465
- },
4466
- {
4467
- name: TOOL_NAMES.ADD_LOOT,
4468
- description: `Record captured loot (credentials, hashes, tokens, SSH keys, files, etc).
4469
- Use this whenever you discover sensitive data during the engagement.
4470
- Loot is tracked in SharedState and visible to all agents for credential reuse and lateral movement.
4471
- Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate`,
4472
- parameters: {
4473
- type: { type: "string", description: "Loot type: credential, hash, token, ssh_key, api_key, file, session, ticket, certificate" },
4474
- host: { type: "string", description: "Source host where loot was found" },
4475
- detail: { type: "string", description: 'Loot detail (e.g., "admin:Password123", "root:$6$...", "JWT admin token")' }
4476
- },
4477
- required: ["type", "host", "detail"],
4478
- execute: async (p) => {
4479
- const lootType = p.type;
4480
- const crackableTypes = ["hash"];
4481
- state.addLoot({
4482
- type: lootType,
4483
- host: p.host,
4484
- detail: p.detail,
4485
- obtainedAt: Date.now(),
4486
- isCrackable: crackableTypes.includes(lootType),
4487
- isCracked: false
4488
- });
4489
- return {
4490
- success: true,
4491
- output: `Loot recorded: [${lootType}] from ${p.host}
4492
- Detail: ${p.detail}
4493
- ` + (crackableTypes.includes(lootType) ? `This is crackable. Consider: hash_crack({ hashes: "${p.detail.slice(0, 30)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
4494
- };
4495
- }
4496
- },
4497
- {
4498
- name: TOOL_NAMES.ADD_FINDING,
4499
- description: "Add a security finding",
4500
- parameters: {
4501
- title: { type: "string", description: "Finding title" },
4502
- severity: { type: "string", description: "Severity" },
4503
- affected: { type: "array", items: { type: "string" }, description: "Affected host:port" }
4504
- },
4505
- required: ["title", "severity"],
4506
- execute: async (p) => {
4507
- state.addFinding({
4508
- id: generateId(ID_RADIX, ID_LENGTH),
4509
- title: p.title,
4510
- severity: p.severity,
4511
- affected: p.affected || [],
4512
- description: p.description || "",
4513
- evidence: [],
4514
- isVerified: false,
4515
- remediation: "",
4516
- foundAt: Date.now()
4517
- });
4518
- return { success: true, output: `Added: ${p.title}` };
4519
4602
  }
4520
4603
  },
4521
4604
  {
@@ -4568,12 +4651,6 @@ ${variantList}
4568
4651
  };
4569
4652
  }
4570
4653
  },
4571
- {
4572
- name: TOOL_NAMES.GET_STATE,
4573
- description: "Get current engagement state summary",
4574
- parameters: {},
4575
- execute: async () => ({ success: true, output: state.toPrompt() })
4576
- },
4577
4654
  {
4578
4655
  name: TOOL_NAMES.GET_WORDLISTS,
4579
4656
  description: `Discover available wordlists on the system by scanning actual filesystem paths.
@@ -4607,7 +4684,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4607
4684
  },
4608
4685
  execute: async (p) => {
4609
4686
  const { existsSync: existsSync6, statSync, readdirSync: readdirSync2 } = await import("fs");
4610
- const { join: join7 } = await import("path");
4687
+ const { join: join9 } = await import("path");
4611
4688
  const category = p.category || "";
4612
4689
  const search = p.search || "";
4613
4690
  const minSize = p.min_size || 0;
@@ -4626,7 +4703,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4626
4703
  try {
4627
4704
  const entries = readdirSync2(dirPath, { withFileTypes: true });
4628
4705
  for (const entry of entries) {
4629
- const fullPath = join7(dirPath, entry.name);
4706
+ const fullPath = join9(dirPath, entry.name);
4630
4707
  if (entry.isDirectory()) {
4631
4708
  if (entry.name.startsWith(".")) continue;
4632
4709
  if (entry.name === "doc") continue;
@@ -4712,6 +4789,14 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4712
4789
  }
4713
4790
  ];
4714
4791
 
4792
+ // src/engine/tools/pentest.ts
4793
+ var createPentestTools = (state) => [
4794
+ ...createStateTools(state),
4795
+ ...createTargetTools(state),
4796
+ ...createIntelTools(state),
4797
+ ...createAttackTools(state)
4798
+ ];
4799
+
4715
4800
  // src/engine/tools/agents.ts
4716
4801
  var globalCredentialHandler = null;
4717
4802
  function setCredentialHandler(handler) {
@@ -4727,7 +4812,7 @@ async function requestCredential(request) {
4727
4812
  try {
4728
4813
  const value = await globalCredentialHandler(request);
4729
4814
  if (value === null) {
4730
- if (request.optional) {
4815
+ if (request.isOptional) {
4731
4816
  return {
4732
4817
  success: true,
4733
4818
  output: `User skipped: ${request.prompt}${request.default ? ` (using default)` : ""}`,
@@ -4779,7 +4864,7 @@ Always provide context about why you need this input.`,
4779
4864
  type: "string",
4780
4865
  description: 'Additional context (e.g., "for SSH connection to 192.168.1.1")'
4781
4866
  },
4782
- optional: {
4867
+ isOptional: {
4783
4868
  type: "boolean",
4784
4869
  description: "Whether this input is optional"
4785
4870
  },
@@ -4795,7 +4880,7 @@ Always provide context about why you need this input.`,
4795
4880
  type: p.input_type || INPUT_TYPES.TEXT,
4796
4881
  prompt: p.question,
4797
4882
  context: p.context,
4798
- optional: p.optional,
4883
+ isOptional: p.isOptional,
4799
4884
  options: p.options
4800
4885
  };
4801
4886
  const result2 = await requestCredential(request);
@@ -5742,81 +5827,81 @@ var ServiceParser = class {
5742
5827
  };
5743
5828
 
5744
5829
  // src/domains/registry.ts
5745
- import { join as join4, dirname as dirname2 } from "path";
5830
+ import { join as join6, dirname as dirname3 } from "path";
5746
5831
  import { fileURLToPath as fileURLToPath2 } from "url";
5747
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
5832
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
5748
5833
  var DOMAINS = {
5749
5834
  [SERVICE_CATEGORIES.NETWORK]: {
5750
5835
  id: SERVICE_CATEGORIES.NETWORK,
5751
5836
  name: "Network Infrastructure",
5752
5837
  description: "Vulnerability scanning, port mapping, and network service exploitation.",
5753
- promptPath: join4(__dirname2, "network/prompt.md")
5838
+ promptPath: join6(__dirname2, "network/prompt.md")
5754
5839
  },
5755
5840
  [SERVICE_CATEGORIES.WEB]: {
5756
5841
  id: SERVICE_CATEGORIES.WEB,
5757
5842
  name: "Web Application",
5758
5843
  description: "Web app security testing, injection attacks, and auth bypass.",
5759
- promptPath: join4(__dirname2, "web/prompt.md")
5844
+ promptPath: join6(__dirname2, "web/prompt.md")
5760
5845
  },
5761
5846
  [SERVICE_CATEGORIES.DATABASE]: {
5762
5847
  id: SERVICE_CATEGORIES.DATABASE,
5763
5848
  name: "Database Security",
5764
5849
  description: "SQL injection, database enumeration, and data extraction.",
5765
- promptPath: join4(__dirname2, "database/prompt.md")
5850
+ promptPath: join6(__dirname2, "database/prompt.md")
5766
5851
  },
5767
5852
  [SERVICE_CATEGORIES.AD]: {
5768
5853
  id: SERVICE_CATEGORIES.AD,
5769
5854
  name: "Active Directory",
5770
5855
  description: "Kerberos, LDAP, and Windows domain privilege escalation.",
5771
- promptPath: join4(__dirname2, "ad/prompt.md")
5856
+ promptPath: join6(__dirname2, "ad/prompt.md")
5772
5857
  },
5773
5858
  [SERVICE_CATEGORIES.EMAIL]: {
5774
5859
  id: SERVICE_CATEGORIES.EMAIL,
5775
5860
  name: "Email Services",
5776
5861
  description: "SMTP, IMAP, POP3 security and user enumeration.",
5777
- promptPath: join4(__dirname2, "email/prompt.md")
5862
+ promptPath: join6(__dirname2, "email/prompt.md")
5778
5863
  },
5779
5864
  [SERVICE_CATEGORIES.REMOTE_ACCESS]: {
5780
5865
  id: SERVICE_CATEGORIES.REMOTE_ACCESS,
5781
5866
  name: "Remote Access",
5782
5867
  description: "SSH, RDP, VNC and other remote control protocols.",
5783
- promptPath: join4(__dirname2, "remote-access/prompt.md")
5868
+ promptPath: join6(__dirname2, "remote-access/prompt.md")
5784
5869
  },
5785
5870
  [SERVICE_CATEGORIES.FILE_SHARING]: {
5786
5871
  id: SERVICE_CATEGORIES.FILE_SHARING,
5787
5872
  name: "File Sharing",
5788
5873
  description: "SMB, NFS, FTP and shared resource security.",
5789
- promptPath: join4(__dirname2, "file-sharing/prompt.md")
5874
+ promptPath: join6(__dirname2, "file-sharing/prompt.md")
5790
5875
  },
5791
5876
  [SERVICE_CATEGORIES.CLOUD]: {
5792
5877
  id: SERVICE_CATEGORIES.CLOUD,
5793
5878
  name: "Cloud Infrastructure",
5794
5879
  description: "AWS, Azure, and GCP security and misconfiguration.",
5795
- promptPath: join4(__dirname2, "cloud/prompt.md")
5880
+ promptPath: join6(__dirname2, "cloud/prompt.md")
5796
5881
  },
5797
5882
  [SERVICE_CATEGORIES.CONTAINER]: {
5798
5883
  id: SERVICE_CATEGORIES.CONTAINER,
5799
5884
  name: "Container Systems",
5800
5885
  description: "Docker and Kubernetes security testing.",
5801
- promptPath: join4(__dirname2, "container/prompt.md")
5886
+ promptPath: join6(__dirname2, "container/prompt.md")
5802
5887
  },
5803
5888
  [SERVICE_CATEGORIES.API]: {
5804
5889
  id: SERVICE_CATEGORIES.API,
5805
5890
  name: "API Security",
5806
5891
  description: "REST, GraphQL, and SOAP API security testing.",
5807
- promptPath: join4(__dirname2, "api/prompt.md")
5892
+ promptPath: join6(__dirname2, "api/prompt.md")
5808
5893
  },
5809
5894
  [SERVICE_CATEGORIES.WIRELESS]: {
5810
5895
  id: SERVICE_CATEGORIES.WIRELESS,
5811
5896
  name: "Wireless Networks",
5812
5897
  description: "WiFi and Bluetooth security testing.",
5813
- promptPath: join4(__dirname2, "wireless/prompt.md")
5898
+ promptPath: join6(__dirname2, "wireless/prompt.md")
5814
5899
  },
5815
5900
  [SERVICE_CATEGORIES.ICS]: {
5816
5901
  id: SERVICE_CATEGORIES.ICS,
5817
5902
  name: "Industrial Systems",
5818
5903
  description: "Critical infrastructure - Modbus, DNP3, ENIP.",
5819
- promptPath: join4(__dirname2, "ics/prompt.md")
5904
+ promptPath: join6(__dirname2, "ics/prompt.md")
5820
5905
  }
5821
5906
  };
5822
5907
 
@@ -6597,9 +6682,9 @@ function logLLM(message, data) {
6597
6682
 
6598
6683
  // src/engine/orchestrator/orchestrator.ts
6599
6684
  import { fileURLToPath as fileURLToPath3 } from "url";
6600
- import { dirname as dirname3, join as join5 } from "path";
6685
+ import { dirname as dirname4, join as join7 } from "path";
6601
6686
  var __filename2 = fileURLToPath3(import.meta.url);
6602
- var __dirname3 = dirname3(__filename2);
6687
+ var __dirname3 = dirname4(__filename2);
6603
6688
 
6604
6689
  // src/agents/core-agent.ts
6605
6690
  var CoreAgent = class _CoreAgent {
@@ -6869,16 +6954,44 @@ Please decide how to handle this error and continue.`;
6869
6954
  emitReasoningEnd(phase) {
6870
6955
  this.events.emit({ type: EVENT_TYPES.REASONING_END, timestamp: Date.now(), data: { phase } });
6871
6956
  }
6872
- emitComplete(output, iteration, toolsExecuted, durationMs, tokens) {
6957
+ emitComplete(output, iteration, toolsExecuted, durationMs, tokens) {
6958
+ this.events.emit({
6959
+ type: EVENT_TYPES.COMPLETE,
6960
+ timestamp: Date.now(),
6961
+ data: {
6962
+ finalOutput: output,
6963
+ iterations: iteration + 1,
6964
+ toolsExecuted,
6965
+ durationMs,
6966
+ tokens
6967
+ }
6968
+ });
6969
+ }
6970
+ /** Emit tool call event for TUI tracking */
6971
+ emitToolCall(toolName, input) {
6972
+ this.events.emit({
6973
+ type: EVENT_TYPES.TOOL_CALL,
6974
+ timestamp: Date.now(),
6975
+ data: {
6976
+ toolName,
6977
+ input,
6978
+ approvalLevel: APPROVAL_LEVELS.AUTO,
6979
+ needsApproval: false
6980
+ }
6981
+ });
6982
+ }
6983
+ /** Emit tool result event for TUI tracking */
6984
+ emitToolResult(toolName, success, output, error, duration) {
6873
6985
  this.events.emit({
6874
- type: EVENT_TYPES.COMPLETE,
6986
+ type: EVENT_TYPES.TOOL_RESULT,
6875
6987
  timestamp: Date.now(),
6876
6988
  data: {
6877
- finalOutput: output,
6878
- iterations: iteration + 1,
6879
- toolsExecuted,
6880
- durationMs,
6881
- tokens
6989
+ toolName,
6990
+ success,
6991
+ output,
6992
+ outputSummary: output.slice(0, 200),
6993
+ error,
6994
+ duration
6882
6995
  }
6883
6996
  });
6884
6997
  }
@@ -6941,16 +7054,7 @@ Please decide how to handle this error and continue.`;
6941
7054
  /** Execute tool calls in parallel via Promise.allSettled */
6942
7055
  async executeToolCallsInParallel(toolCalls, progress) {
6943
7056
  for (const call of toolCalls) {
6944
- this.events.emit({
6945
- type: EVENT_TYPES.TOOL_CALL,
6946
- timestamp: Date.now(),
6947
- data: {
6948
- toolName: call.name,
6949
- input: call.input,
6950
- approvalLevel: APPROVAL_LEVELS.AUTO,
6951
- needsApproval: false
6952
- }
6953
- });
7057
+ this.emitToolCall(call.name, call.input);
6954
7058
  }
6955
7059
  const promises = toolCalls.map((call) => this.executeSingleTool(call, progress));
6956
7060
  const settled = await Promise.allSettled(promises);
@@ -6964,16 +7068,7 @@ Please decide how to handle this error and continue.`;
6964
7068
  async executeToolCallsSequentially(toolCalls, progress) {
6965
7069
  const results = [];
6966
7070
  for (const call of toolCalls) {
6967
- this.events.emit({
6968
- type: EVENT_TYPES.TOOL_CALL,
6969
- timestamp: Date.now(),
6970
- data: {
6971
- toolName: call.name,
6972
- input: call.input,
6973
- approvalLevel: APPROVAL_LEVELS.AUTO,
6974
- needsApproval: false
6975
- }
6976
- });
7071
+ this.emitToolCall(call.name, call.input);
6977
7072
  const result2 = await this.executeSingleTool(call, progress);
6978
7073
  results.push(result2);
6979
7074
  }
@@ -7013,35 +7108,13 @@ Please decide how to handle this error and continue.`;
7013
7108
  progress.blockedCommandPatterns.clear();
7014
7109
  }
7015
7110
  }
7016
- this.events.emit({
7017
- type: EVENT_TYPES.TOOL_RESULT,
7018
- timestamp: Date.now(),
7019
- data: {
7020
- toolName: call.name,
7021
- success: result2.success,
7022
- output: outputText,
7023
- outputSummary: outputText.slice(0, 200),
7024
- error: result2.error,
7025
- duration: Date.now() - toolStartTime
7026
- }
7027
- });
7111
+ this.emitToolResult(call.name, result2.success, outputText, result2.error, Date.now() - toolStartTime);
7028
7112
  return { toolCallId: call.id, output: outputText, error: result2.error };
7029
7113
  } catch (error) {
7030
7114
  const errorMsg = String(error);
7031
7115
  const enrichedError = this.enrichToolErrorContext({ toolName: call.name, input: call.input, error: errorMsg, originalOutput: "", progress });
7032
7116
  if (progress) progress.toolErrors++;
7033
- this.events.emit({
7034
- type: EVENT_TYPES.TOOL_RESULT,
7035
- timestamp: Date.now(),
7036
- data: {
7037
- toolName: call.name,
7038
- success: false,
7039
- output: enrichedError,
7040
- outputSummary: enrichedError.slice(0, 200),
7041
- error: errorMsg,
7042
- duration: Date.now() - toolStartTime
7043
- }
7044
- });
7117
+ this.emitToolResult(call.name, false, enrichedError, errorMsg, Date.now() - toolStartTime);
7045
7118
  return { toolCallId: call.id, output: enrichedError, error: errorMsg };
7046
7119
  }
7047
7120
  }
@@ -7141,7 +7214,7 @@ Please decide how to handle this error and continue.`;
7141
7214
 
7142
7215
  // src/agents/prompt-builder.ts
7143
7216
  import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
7144
- import { join as join6, dirname as dirname4 } from "path";
7217
+ import { join as join8, dirname as dirname5 } from "path";
7145
7218
  import { fileURLToPath as fileURLToPath4 } from "url";
7146
7219
 
7147
7220
  // src/shared/constants/prompts.ts
@@ -7195,9 +7268,9 @@ var INITIAL_TASKS = {
7195
7268
  };
7196
7269
 
7197
7270
  // src/agents/prompt-builder.ts
7198
- var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
7199
- var PROMPTS_DIR = join6(__dirname4, "prompts");
7200
- var TECHNIQUES_DIR = join6(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
7271
+ var __dirname4 = dirname5(fileURLToPath4(import.meta.url));
7272
+ var PROMPTS_DIR = join8(__dirname4, "prompts");
7273
+ var TECHNIQUES_DIR = join8(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
7201
7274
  var { AGENT_FILES } = PROMPT_PATHS;
7202
7275
  var PHASE_PROMPT_MAP = {
7203
7276
  // Direct mappings — phase has its own prompt file
@@ -7273,7 +7346,7 @@ var PromptBuilder = class {
7273
7346
  * Load a prompt file from src/agents/prompts/
7274
7347
  */
7275
7348
  loadPromptFile(filename) {
7276
- const path2 = join6(PROMPTS_DIR, filename);
7349
+ const path2 = join8(PROMPTS_DIR, filename);
7277
7350
  return existsSync5(path2) ? readFileSync3(path2, PROMPT_CONFIG.ENCODING) : "";
7278
7351
  }
7279
7352
  /**
@@ -7322,7 +7395,7 @@ ${content}
7322
7395
  if (relevantTechniques.length === 0) return "";
7323
7396
  const fragments = [];
7324
7397
  for (const technique of relevantTechniques) {
7325
- const filePath = join6(TECHNIQUES_DIR, `${technique}.md`);
7398
+ const filePath = join8(TECHNIQUES_DIR, `${technique}.md`);
7326
7399
  try {
7327
7400
  if (!existsSync5(filePath)) continue;
7328
7401
  const content = readFileSync3(filePath, PROMPT_CONFIG.ENCODING);
@@ -7469,10 +7542,10 @@ var AgentFactory = class {
7469
7542
  * Architecture: Single agent with all tools.
7470
7543
  * No sub-agents, no spawn_sub, no nested loops.
7471
7544
  */
7472
- static createMainAgent(autoApprove = false) {
7545
+ static createMainAgent(shouldAutoApprove = false) {
7473
7546
  const state = new SharedState();
7474
7547
  const events = new AgentEventEmitter();
7475
- const approvalGate = new ApprovalGate(autoApprove);
7548
+ const approvalGate = new ApprovalGate(shouldAutoApprove);
7476
7549
  const scopeGuard = new ScopeGuard(state);
7477
7550
  const toolRegistry = new CategorizedToolRegistry(
7478
7551
  state,
@@ -7484,6 +7557,54 @@ var AgentFactory = class {
7484
7557
  }
7485
7558
  };
7486
7559
 
7560
+ // src/platform/tui/utils/format.ts
7561
+ var formatDuration = (ms) => {
7562
+ const totalSec = ms / 1e3;
7563
+ if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
7564
+ const minutes = Math.floor(totalSec / 60);
7565
+ const seconds = Math.floor(totalSec % 60);
7566
+ return `${minutes}m ${seconds}s`;
7567
+ };
7568
+ var formatTokens = (count) => {
7569
+ if (count >= 1e6) return (count / 1e6).toFixed(1) + "M";
7570
+ if (count >= 1e3) return (count / 1e3).toFixed(1) + "K";
7571
+ return String(count);
7572
+ };
7573
+ var formatMeta = (ms, tokens) => {
7574
+ const parts = [];
7575
+ if (ms > 0) parts.push(formatDuration(ms));
7576
+ if (tokens > 0) parts.push(`\u2191 ${formatTokens(tokens)} tokens`);
7577
+ return parts.length > 0 ? `(${parts.join(" \xB7 ")})` : "";
7578
+ };
7579
+ var formatInlineStatus = () => {
7580
+ const zombieHunter2 = new ZombieHunter();
7581
+ const healthMonitor2 = new HealthMonitor();
7582
+ const processes = listBackgroundProcesses();
7583
+ const zombies = zombieHunter2.scan();
7584
+ const health = healthMonitor2.check();
7585
+ const statusData = {
7586
+ processes: processes.map((p) => ({
7587
+ id: p.id,
7588
+ role: p.role,
7589
+ description: p.description,
7590
+ purpose: p.purpose,
7591
+ running: p.isRunning,
7592
+ durationMs: p.durationMs,
7593
+ listeningPort: p.listeningPort,
7594
+ exitCode: p.exitCode
7595
+ })),
7596
+ zombies: zombies.map((z) => ({
7597
+ processId: z.processId,
7598
+ orphanedChildren: z.orphanedChildren
7599
+ })),
7600
+ health: health.overall
7601
+ };
7602
+ return JSON.stringify(statusData);
7603
+ };
7604
+
7605
+ // src/platform/tui/hooks/useAgentState.ts
7606
+ import { useState, useRef, useCallback } from "react";
7607
+
7487
7608
  // src/shared/constants/thought.ts
7488
7609
  var THOUGHT_TYPE = {
7489
7610
  THINKING: "thinking",
@@ -7742,53 +7863,8 @@ var HELP_TEXT = `
7742
7863
  /auto (toggle approval mode)
7743
7864
  `;
7744
7865
 
7745
- // src/platform/tui/utils/format.ts
7746
- var formatDuration = (ms) => {
7747
- const totalSec = ms / 1e3;
7748
- if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
7749
- const minutes = Math.floor(totalSec / 60);
7750
- const seconds = Math.floor(totalSec % 60);
7751
- return `${minutes}m ${seconds}s`;
7752
- };
7753
- var formatTokens = (count) => {
7754
- if (count >= 1e6) return (count / 1e6).toFixed(1) + "M";
7755
- if (count >= 1e3) return (count / 1e3).toFixed(1) + "K";
7756
- return String(count);
7757
- };
7758
- var formatMeta = (ms, tokens) => {
7759
- const parts = [];
7760
- if (ms > 0) parts.push(formatDuration(ms));
7761
- if (tokens > 0) parts.push(`\u2191 ${formatTokens(tokens)} tokens`);
7762
- return parts.length > 0 ? `(${parts.join(" \xB7 ")})` : "";
7763
- };
7764
- var formatInlineStatus = () => {
7765
- const zombieHunter2 = new ZombieHunter();
7766
- const healthMonitor2 = new HealthMonitor();
7767
- const processes = listBackgroundProcesses();
7768
- const zombies = zombieHunter2.scan();
7769
- const health = healthMonitor2.check();
7770
- const statusData = {
7771
- processes: processes.map((p) => ({
7772
- id: p.id,
7773
- role: p.role,
7774
- description: p.description,
7775
- purpose: p.purpose,
7776
- running: p.isRunning,
7777
- durationMs: p.durationMs,
7778
- listeningPort: p.listeningPort,
7779
- exitCode: p.exitCode
7780
- })),
7781
- zombies: zombies.map((z) => ({
7782
- processId: z.processId,
7783
- orphanedChildren: z.orphanedChildren
7784
- })),
7785
- health: health.overall
7786
- };
7787
- return JSON.stringify(statusData);
7788
- };
7789
-
7790
- // src/platform/tui/hooks/useAgent.ts
7791
- var useAgent = (autoApprove, target) => {
7866
+ // src/platform/tui/hooks/useAgentState.ts
7867
+ var useAgentState = () => {
7792
7868
  const [messages, setMessages] = useState([]);
7793
7869
  const [isProcessing, setIsProcessing] = useState(false);
7794
7870
  const [currentStatus, setCurrentStatus] = useState("");
@@ -7816,14 +7892,6 @@ var useAgent = (autoApprove, target) => {
7816
7892
  const retryCountRef = useRef(0);
7817
7893
  const tokenAccumRef = useRef(0);
7818
7894
  const lastStepTokensRef = useRef(0);
7819
- const [agent] = useState(() => AgentFactory.createMainAgent(autoApprove));
7820
- useEffect(() => {
7821
- if (target) {
7822
- agent.addTarget(target);
7823
- agent.setScope([target]);
7824
- }
7825
- }, [agent, target]);
7826
- const eventsRef = useRef(agent.getEventEmitter());
7827
7895
  const addMessage = useCallback((type, content) => {
7828
7896
  const id = Math.random().toString(36).substring(7);
7829
7897
  setMessages((prev) => [...prev, { id, type, content, timestamp: /* @__PURE__ */ new Date() }]);
@@ -7847,56 +7915,63 @@ var useAgent = (autoApprove, target) => {
7847
7915
  setElapsedTime(0);
7848
7916
  }
7849
7917
  }, []);
7850
- const executeTask = useCallback(async (task) => {
7851
- setIsProcessing(true);
7852
- manageTimer("start");
7853
- setCurrentStatus("Thinking");
7854
- resetCumulativeCounters();
7855
- try {
7856
- const response = await agent.execute(task);
7857
- const meta = lastResponseMetaRef.current;
7858
- const suffix = meta ? ` ${formatMeta(meta.durationMs || 0, (meta.tokens?.input || 0) + (meta.tokens?.output || 0))}` : "";
7859
- addMessage("ai", response + suffix);
7860
- } catch (e) {
7861
- addMessage("error", e instanceof Error ? e.message : String(e));
7862
- } finally {
7863
- manageTimer("stop");
7864
- setIsProcessing(false);
7865
- setCurrentStatus("");
7866
- }
7867
- }, [agent, addMessage, manageTimer, resetCumulativeCounters]);
7868
- const abort = useCallback(() => {
7869
- agent.abort();
7870
- setIsProcessing(false);
7871
- manageTimer("stop");
7872
- setCurrentStatus("");
7873
- addMessage("system", "Interrupted");
7874
- }, [agent, addMessage, manageTimer]);
7875
- const cancelInputRequest = useCallback(() => {
7876
- if (inputRequest.isActive && inputRequest.resolve) {
7877
- inputRequest.resolve(null);
7878
- setInputRequest({ isActive: false, prompt: "", isPassword: false, resolve: null });
7879
- addMessage("system", "Input cancelled");
7880
- }
7881
- }, [inputRequest, addMessage]);
7918
+ const clearAllTimers = useCallback(() => {
7919
+ if (timerRef.current) clearInterval(timerRef.current);
7920
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
7921
+ }, []);
7922
+ return {
7923
+ // State
7924
+ messages,
7925
+ setMessages,
7926
+ isProcessing,
7927
+ setIsProcessing,
7928
+ currentStatus,
7929
+ setCurrentStatus,
7930
+ elapsedTime,
7931
+ retryState,
7932
+ setRetryState,
7933
+ currentTokens,
7934
+ setCurrentTokens,
7935
+ inputRequest,
7936
+ setInputRequest,
7937
+ stats,
7938
+ setStats,
7939
+ // Refs
7940
+ lastResponseMetaRef,
7941
+ timerRef,
7942
+ retryCountdownRef,
7943
+ retryCountRef,
7944
+ tokenAccumRef,
7945
+ lastStepTokensRef,
7946
+ // Helpers
7947
+ addMessage,
7948
+ resetCumulativeCounters,
7949
+ manageTimer,
7950
+ clearAllTimers
7951
+ };
7952
+ };
7953
+
7954
+ // src/platform/tui/hooks/useAgentEvents.ts
7955
+ import { useEffect } from "react";
7956
+ var useAgentEvents = (agent, eventsRef, state) => {
7957
+ const {
7958
+ addMessage,
7959
+ setCurrentStatus,
7960
+ setRetryState,
7961
+ setCurrentTokens,
7962
+ setStats,
7963
+ lastResponseMetaRef,
7964
+ retryCountdownRef,
7965
+ retryCountRef,
7966
+ tokenAccumRef,
7967
+ lastStepTokensRef,
7968
+ clearAllTimers
7969
+ } = state;
7882
7970
  useEffect(() => {
7883
7971
  const events = eventsRef.current;
7884
7972
  const onToolCall = (e) => {
7885
7973
  setCurrentStatus(`Executing: ${e.data.toolName}`);
7886
- let inputStr = "";
7887
- try {
7888
- if (e.data.input && Object.keys(e.data.input).length > 0) {
7889
- if (e.data.toolName === TOOL_NAMES.RUN_CMD && e.data.input.command) {
7890
- inputStr = String(e.data.input.command);
7891
- } else {
7892
- const str = JSON.stringify(e.data.input);
7893
- if (str !== "{}") {
7894
- inputStr = str.length > TUI_DISPLAY_LIMITS.toolInputPreview ? str.substring(0, TUI_DISPLAY_LIMITS.toolInputTruncated) + "..." : str;
7895
- }
7896
- }
7897
- }
7898
- } catch {
7899
- }
7974
+ const inputStr = formatToolInput(e.data.toolName, e.data.input);
7900
7975
  addMessage("tool", inputStr ? `${e.data.toolName} ${inputStr}` : e.data.toolName);
7901
7976
  };
7902
7977
  const onToolResult = (e) => {
@@ -7912,55 +7987,42 @@ var useAgent = (autoApprove, target) => {
7912
7987
  lastResponseMetaRef.current = { durationMs: e.data.durationMs, tokens: e.data.tokens };
7913
7988
  };
7914
7989
  const onRetry = (e) => {
7915
- const delaySec = Math.ceil(e.data.delayMs / 1e3);
7916
- retryCountRef.current += 1;
7917
- const retryNum = retryCountRef.current;
7918
- addMessage("system", `\u27F3 Retry #${retryNum} \xB7 ${e.data.error} \xB7 waiting ${delaySec}s...`);
7919
- setRetryState({ isRetrying: true, attempt: retryNum, maxRetries: e.data.maxRetries, delayMs: e.data.delayMs, error: e.data.error, countdown: delaySec });
7920
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
7921
- let remaining = delaySec;
7922
- retryCountdownRef.current = setInterval(() => {
7923
- remaining -= 1;
7924
- if (remaining <= 0) {
7925
- setRetryState((prev) => ({ ...prev, isRetrying: false, countdown: 0 }));
7926
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
7927
- } else {
7928
- setRetryState((prev) => ({ ...prev, countdown: remaining }));
7929
- }
7930
- }, 1e3);
7990
+ handleRetry(e, addMessage, setRetryState, retryCountdownRef, retryCountRef);
7991
+ };
7992
+ const onThink = (e) => {
7993
+ const t = e.data.thought;
7994
+ setCurrentStatus(t.length > TUI_DISPLAY_LIMITS.statusThoughtPreview ? t.substring(0, TUI_DISPLAY_LIMITS.statusThoughtTruncated) + "..." : t);
7995
+ };
7996
+ const onError = (e) => {
7997
+ addMessage("error", e.data.message || "An error occurred");
7931
7998
  };
7932
7999
  const onUsageUpdate = (e) => {
7933
8000
  const stepTokens = e.data.inputTokens + e.data.outputTokens;
7934
- if (stepTokens < lastStepTokensRef.current) tokenAccumRef.current += lastStepTokensRef.current;
8001
+ if (stepTokens < lastStepTokensRef.current) {
8002
+ tokenAccumRef.current += lastStepTokensRef.current;
8003
+ }
7935
8004
  lastStepTokensRef.current = stepTokens;
7936
8005
  setCurrentTokens(tokenAccumRef.current + stepTokens);
7937
8006
  };
7938
8007
  setInputHandler((p) => {
7939
- return new Promise((resolve) => setInputRequest({
7940
- isActive: true,
7941
- prompt: p.trim(),
7942
- isPassword: /password|passphrase/i.test(p),
7943
- inputType: /sudo/i.test(p) ? "sudo_password" : /password|passphrase/i.test(p) ? "password" : "text",
7944
- resolve
7945
- }));
8008
+ return new Promise((resolve) => {
8009
+ const isPassword = /password|passphrase/i.test(p);
8010
+ const inputType = /sudo/i.test(p) ? "sudo_password" : isPassword ? "password" : "text";
8011
+ state.setInputRequest({
8012
+ isActive: true,
8013
+ prompt: p.trim(),
8014
+ isPassword,
8015
+ inputType,
8016
+ resolve
8017
+ });
8018
+ });
7946
8019
  });
7947
8020
  setCredentialHandler((request) => {
7948
8021
  return new Promise((resolve) => {
7949
8022
  const hiddenTypes = ["password", "sudo_password", "ssh_password", "passphrase", "api_key", "credential"];
7950
8023
  const isPassword = hiddenTypes.includes(request.type);
7951
- let displayPrompt = request.prompt || "Enter input";
7952
- if (request.context) {
7953
- displayPrompt = `${displayPrompt}
7954
- Context: ${request.context}`;
7955
- }
7956
- if (request.optional) {
7957
- displayPrompt += " (optional - press Enter to skip)";
7958
- }
7959
- if (request.options && request.options.length > 0) {
7960
- displayPrompt += `
7961
- Options: ${request.options.join(", ")}`;
7962
- }
7963
- setInputRequest({
8024
+ const displayPrompt = buildCredentialPrompt(request);
8025
+ state.setInputRequest({
7964
8026
  isActive: true,
7965
8027
  prompt: displayPrompt,
7966
8028
  isPassword,
@@ -7973,35 +8035,25 @@ Options: ${request.options.join(", ")}`;
7973
8035
  });
7974
8036
  });
7975
8037
  setCommandEventEmitter((event) => {
7976
- const icons = {
7977
- [COMMAND_EVENT_TYPES.TOOL_MISSING]: "\u26A0\uFE0F",
7978
- [COMMAND_EVENT_TYPES.TOOL_INSTALL]: "\u{1F4E6}",
7979
- [COMMAND_EVENT_TYPES.TOOL_INSTALLED]: "\u2705",
7980
- [COMMAND_EVENT_TYPES.TOOL_INSTALL_FAILED]: "\u274C",
7981
- [COMMAND_EVENT_TYPES.TOOL_RETRY]: "\u{1F504}",
7982
- [COMMAND_EVENT_TYPES.COMMAND_START]: "\u25B6",
7983
- [COMMAND_EVENT_TYPES.COMMAND_SUCCESS]: "\u2713",
7984
- [COMMAND_EVENT_TYPES.COMMAND_FAILED]: "\u2717",
7985
- [COMMAND_EVENT_TYPES.COMMAND_ERROR]: "\u274C",
7986
- [COMMAND_EVENT_TYPES.INPUT_REQUIRED]: "\u{1F510}"
7987
- };
7988
- const icon = icons[event.type] || "\u2022";
8038
+ const icon = getCommandEventIcon(event.type);
7989
8039
  const msg = event.detail ? `${icon} ${event.message}
7990
8040
  ${event.detail}` : `${icon} ${event.message}`;
7991
8041
  addMessage("system", msg);
7992
8042
  });
7993
8043
  const updateStats = () => {
7994
8044
  const s = agent.getState();
7995
- setStats({ phase: agent.getPhase(), targets: s.getTargets().size, findings: s.getFindings().length, todo: s.getTodo().length });
8045
+ setStats({
8046
+ phase: agent.getPhase(),
8047
+ targets: s.getTargets().size,
8048
+ findings: s.getFindings().length,
8049
+ todo: s.getTodo().length
8050
+ });
7996
8051
  };
7997
8052
  events.on(EVENT_TYPES.TOOL_CALL, onToolCall);
7998
8053
  events.on(EVENT_TYPES.TOOL_RESULT, onToolResult);
7999
- events.on(EVENT_TYPES.THINK, (e) => {
8000
- const t = e.data.thought;
8001
- setCurrentStatus(t.length > TUI_DISPLAY_LIMITS.statusThoughtPreview ? t.substring(0, TUI_DISPLAY_LIMITS.statusThoughtTruncated) + "..." : t);
8002
- });
8054
+ events.on(EVENT_TYPES.THINK, onThink);
8003
8055
  events.on(EVENT_TYPES.COMPLETE, onComplete);
8004
- events.on(EVENT_TYPES.ERROR, (e) => addMessage("error", e.data.message || "An error occurred"));
8056
+ events.on(EVENT_TYPES.ERROR, onError);
8005
8057
  events.on(EVENT_TYPES.RETRY, onRetry);
8006
8058
  events.on(EVENT_TYPES.USAGE_UPDATE, onUsageUpdate);
8007
8059
  events.on(EVENT_TYPES.STATE_CHANGE, updateStats);
@@ -8009,10 +8061,154 @@ Options: ${request.options.join(", ")}`;
8009
8061
  updateStats();
8010
8062
  return () => {
8011
8063
  events.removeAllListeners();
8012
- if (timerRef.current) clearInterval(timerRef.current);
8013
- if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
8064
+ clearAllTimers();
8014
8065
  };
8015
- }, [agent, addMessage]);
8066
+ }, [
8067
+ agent,
8068
+ addMessage,
8069
+ setCurrentStatus,
8070
+ setRetryState,
8071
+ setCurrentTokens,
8072
+ setStats,
8073
+ lastResponseMetaRef,
8074
+ retryCountdownRef,
8075
+ retryCountRef,
8076
+ tokenAccumRef,
8077
+ lastStepTokensRef,
8078
+ clearAllTimers,
8079
+ state
8080
+ ]);
8081
+ };
8082
+ function formatToolInput(toolName, input) {
8083
+ if (!input || Object.keys(input).length === 0) return "";
8084
+ try {
8085
+ if (toolName === TOOL_NAMES.RUN_CMD && input.command) {
8086
+ return String(input.command);
8087
+ }
8088
+ const str = JSON.stringify(input);
8089
+ if (str === "{}") return "";
8090
+ return str.length > TUI_DISPLAY_LIMITS.toolInputPreview ? str.substring(0, TUI_DISPLAY_LIMITS.toolInputTruncated) + "..." : str;
8091
+ } catch {
8092
+ return "";
8093
+ }
8094
+ }
8095
+ function handleRetry(e, addMessage, setRetryState, retryCountdownRef, retryCountRef) {
8096
+ const delaySec = Math.ceil(e.data.delayMs / 1e3);
8097
+ retryCountRef.current += 1;
8098
+ const retryNum = retryCountRef.current;
8099
+ addMessage("system", `\u27F3 Retry #${retryNum} \xB7 ${e.data.error} \xB7 waiting ${delaySec}s...`);
8100
+ setRetryState({
8101
+ isRetrying: true,
8102
+ attempt: retryNum,
8103
+ maxRetries: e.data.maxRetries,
8104
+ delayMs: e.data.delayMs,
8105
+ error: e.data.error,
8106
+ countdown: delaySec
8107
+ });
8108
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
8109
+ let remaining = delaySec;
8110
+ retryCountdownRef.current = setInterval(() => {
8111
+ remaining -= 1;
8112
+ if (remaining <= 0) {
8113
+ setRetryState((prev) => ({ ...prev, isRetrying: false, countdown: 0 }));
8114
+ if (retryCountdownRef.current) clearInterval(retryCountdownRef.current);
8115
+ } else {
8116
+ setRetryState((prev) => ({ ...prev, countdown: remaining }));
8117
+ }
8118
+ }, 1e3);
8119
+ }
8120
+ function buildCredentialPrompt(request) {
8121
+ let displayPrompt = request.prompt || "Enter input";
8122
+ if (request.context) {
8123
+ displayPrompt = `${displayPrompt}
8124
+ Context: ${request.context}`;
8125
+ }
8126
+ if (request.optional) {
8127
+ displayPrompt += " (optional - press Enter to skip)";
8128
+ }
8129
+ if (request.options && request.options.length > 0) {
8130
+ displayPrompt += `
8131
+ Options: ${request.options.join(", ")}`;
8132
+ }
8133
+ return displayPrompt;
8134
+ }
8135
+ function getCommandEventIcon(eventType) {
8136
+ const icons = {
8137
+ [COMMAND_EVENT_TYPES.TOOL_MISSING]: "\u26A0\uFE0F",
8138
+ [COMMAND_EVENT_TYPES.TOOL_INSTALL]: "\u{1F4E6}",
8139
+ [COMMAND_EVENT_TYPES.TOOL_INSTALLED]: "\u2705",
8140
+ [COMMAND_EVENT_TYPES.TOOL_INSTALL_FAILED]: "\u274C",
8141
+ [COMMAND_EVENT_TYPES.TOOL_RETRY]: "\u{1F504}",
8142
+ [COMMAND_EVENT_TYPES.COMMAND_START]: "\u25B6",
8143
+ [COMMAND_EVENT_TYPES.COMMAND_SUCCESS]: "\u2713",
8144
+ [COMMAND_EVENT_TYPES.COMMAND_FAILED]: "\u2717",
8145
+ [COMMAND_EVENT_TYPES.COMMAND_ERROR]: "\u274C",
8146
+ [COMMAND_EVENT_TYPES.INPUT_REQUIRED]: "\u{1F510}"
8147
+ };
8148
+ return icons[eventType] || "\u2022";
8149
+ }
8150
+
8151
+ // src/platform/tui/hooks/useAgent.ts
8152
+ var useAgent = (shouldAutoApprove, target) => {
8153
+ const [agent] = useState2(() => AgentFactory.createMainAgent(shouldAutoApprove));
8154
+ const eventsRef = useRef2(agent.getEventEmitter());
8155
+ const state = useAgentState();
8156
+ const {
8157
+ messages,
8158
+ setMessages,
8159
+ isProcessing,
8160
+ setIsProcessing,
8161
+ currentStatus,
8162
+ elapsedTime,
8163
+ retryState,
8164
+ currentTokens,
8165
+ inputRequest,
8166
+ setInputRequest,
8167
+ stats,
8168
+ lastResponseMetaRef,
8169
+ addMessage,
8170
+ manageTimer,
8171
+ resetCumulativeCounters
8172
+ } = state;
8173
+ useEffect2(() => {
8174
+ if (target) {
8175
+ agent.addTarget(target);
8176
+ agent.setScope([target]);
8177
+ }
8178
+ }, [agent, target]);
8179
+ useAgentEvents(agent, eventsRef, state);
8180
+ const executeTask = useCallback2(async (task) => {
8181
+ setIsProcessing(true);
8182
+ manageTimer("start");
8183
+ state.setCurrentStatus("Thinking");
8184
+ resetCumulativeCounters();
8185
+ try {
8186
+ const response = await agent.execute(task);
8187
+ const meta = lastResponseMetaRef.current;
8188
+ const suffix = meta ? ` ${formatMeta(meta.durationMs || 0, (meta.tokens?.input || 0) + (meta.tokens?.output || 0))}` : "";
8189
+ addMessage("ai", response + suffix);
8190
+ } catch (e) {
8191
+ addMessage("error", e instanceof Error ? e.message : String(e));
8192
+ } finally {
8193
+ manageTimer("stop");
8194
+ setIsProcessing(false);
8195
+ state.setCurrentStatus("");
8196
+ }
8197
+ }, [agent, addMessage, manageTimer, resetCumulativeCounters, setIsProcessing, lastResponseMetaRef, state]);
8198
+ const abort = useCallback2(() => {
8199
+ agent.abort();
8200
+ setIsProcessing(false);
8201
+ manageTimer("stop");
8202
+ state.setCurrentStatus("");
8203
+ addMessage("system", "Interrupted");
8204
+ }, [agent, addMessage, manageTimer, setIsProcessing, state]);
8205
+ const cancelInputRequest = useCallback2(() => {
8206
+ if (inputRequest.isActive && inputRequest.resolve) {
8207
+ inputRequest.resolve(null);
8208
+ setInputRequest({ isActive: false, prompt: "", isPassword: false, resolve: null });
8209
+ addMessage("system", "Input cancelled");
8210
+ }
8211
+ }, [inputRequest, setInputRequest, addMessage]);
8016
8212
  return {
8017
8213
  agent,
8018
8214
  messages,
@@ -8222,14 +8418,14 @@ var MessageList = ({ messages }) => {
8222
8418
  import { Box as Box3, Text as Text4 } from "ink";
8223
8419
 
8224
8420
  // src/platform/tui/components/MusicSpinner.tsx
8225
- import { useState as useState2, useEffect as useEffect2 } from "react";
8421
+ import { useState as useState3, useEffect as useEffect3 } from "react";
8226
8422
  import { Text as Text3 } from "ink";
8227
8423
  import { jsx as jsx3 } from "react/jsx-runtime";
8228
8424
  var FRAMES = ["\u2669", "\u266A", "\u266B", "\u266C", "\u266B", "\u266A"];
8229
8425
  var INTERVAL = 150;
8230
8426
  var MusicSpinner = ({ color }) => {
8231
- const [index, setIndex] = useState2(0);
8232
- useEffect2(() => {
8427
+ const [index, setIndex] = useState3(0);
8428
+ useEffect3(() => {
8233
8429
  const timer = setInterval(() => {
8234
8430
  setIndex((i) => (i + 1) % FRAMES.length);
8235
8431
  }, INTERVAL);
@@ -8388,9 +8584,9 @@ var footer_default = Footer;
8388
8584
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
8389
8585
  var App = ({ autoApprove = false, target }) => {
8390
8586
  const { exit } = useApp();
8391
- const [input, setInput] = useState3("");
8392
- const [secretInput, setSecretInput] = useState3("");
8393
- const [autoApproveMode, setAutoApproveMode] = useState3(autoApprove);
8587
+ const [input, setInput] = useState4("");
8588
+ const [secretInput, setSecretInput] = useState4("");
8589
+ const [autoApproveMode, setAutoApproveMode] = useState4(autoApprove);
8394
8590
  const {
8395
8591
  agent,
8396
8592
  messages,
@@ -8408,14 +8604,14 @@ var App = ({ autoApprove = false, target }) => {
8408
8604
  cancelInputRequest,
8409
8605
  addMessage
8410
8606
  } = useAgent(autoApproveMode, target);
8411
- const handleExit = useCallback2(() => {
8607
+ const handleExit = useCallback3(() => {
8412
8608
  if (inputRequest.isActive && inputRequest.resolve) inputRequest.resolve(null);
8413
8609
  cleanupAllProcesses().catch(() => {
8414
8610
  });
8415
8611
  exit();
8416
8612
  setTimeout(() => process.exit(0), DISPLAY_LIMITS.EXIT_DELAY);
8417
8613
  }, [exit, inputRequest, cancelInputRequest]);
8418
- const handleCommand = useCallback2(async (cmd, args) => {
8614
+ const handleCommand = useCallback3(async (cmd, args) => {
8419
8615
  switch (cmd) {
8420
8616
  case UI_COMMANDS.HELP:
8421
8617
  case UI_COMMANDS.HELP_SHORT:
@@ -8497,7 +8693,7 @@ ${procData.stdout || "(no output)"}
8497
8693
  addMessage("error", `Unknown command: /${cmd}`);
8498
8694
  }
8499
8695
  }, [agent, addMessage, executeTask, setMessages, handleExit, autoApproveMode]);
8500
- const handleSubmit = useCallback2(async (value) => {
8696
+ const handleSubmit = useCallback3(async (value) => {
8501
8697
  const trimmed = value.trim();
8502
8698
  if (!trimmed) return;
8503
8699
  setInput("");
@@ -8509,7 +8705,7 @@ ${procData.stdout || "(no output)"}
8509
8705
  await executeTask(trimmed);
8510
8706
  }
8511
8707
  }, [addMessage, executeTask, handleCommand]);
8512
- const handleSecretSubmit = useCallback2((value) => {
8708
+ const handleSecretSubmit = useCallback3((value) => {
8513
8709
  if (!inputRequest.isActive || !inputRequest.resolve) return;
8514
8710
  const displayText = inputRequest.isPassword ? "\u2022".repeat(value.length) : value;
8515
8711
  const promptLabel = inputRequest.prompt || "Input";
@@ -8525,7 +8721,7 @@ ${procData.stdout || "(no output)"}
8525
8721
  }
8526
8722
  if (key.ctrl && ch === "c") handleExit();
8527
8723
  });
8528
- useEffect3(() => {
8724
+ useEffect4(() => {
8529
8725
  const onSignal = () => handleExit();
8530
8726
  process.on("SIGINT", onSignal);
8531
8727
  process.on("SIGTERM", onSignal);