pentesting 0.47.2 → 0.47.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -105,7 +105,27 @@ var DISPLAY_LIMITS = {
105
105
  /** Prefix length for sensitive value redaction */
106
106
  REDACT_PREFIX: 4,
107
107
  /** Max characters for raw JSON error preview */
108
- RAW_JSON_ERROR_PREVIEW: 500
108
+ RAW_JSON_ERROR_PREVIEW: 500,
109
+ // ─── Memory/History Slice Sizes (§4-3 Extract Magic Values) ─────
110
+ /** Recent credentials to include in context */
111
+ RECENT_CREDENTIALS: 5,
112
+ /** Recent failures to analyze for patterns */
113
+ RECENT_FAILURES: 5,
114
+ /** Recent successes to include in summary */
115
+ RECENT_SUCCESSES: 3,
116
+ /** Recent insights to display */
117
+ RECENT_INSIGHTS: 3,
118
+ /** Recent memory events to include */
119
+ RECENT_MEMORY_EVENTS: 10,
120
+ /** Summary window - small (for quick summaries) */
121
+ SUMMARY_WINDOW_SMALL: 100,
122
+ /** Summary window - large (for detailed summaries) */
123
+ SUMMARY_WINDOW_LARGE: 500,
124
+ // ─── Evidence Display ────────────────────────────────────────
125
+ /** Max evidence items to show in finding preview */
126
+ EVIDENCE_ITEMS_PREVIEW: 3,
127
+ /** Max characters per evidence item in preview */
128
+ EVIDENCE_PREVIEW_LENGTH: 120
109
129
  };
110
130
  var AGENT_LIMITS = {
111
131
  /** Maximum agent loop iterations — generous to allow complex tasks.
@@ -311,7 +331,7 @@ var ORPHAN_PROCESS_NAMES = [
311
331
 
312
332
  // src/shared/constants/agent.ts
313
333
  var APP_NAME = "Pentest AI";
314
- var APP_VERSION = "0.47.2";
334
+ var APP_VERSION = "0.47.4";
315
335
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
316
336
  var LLM_ROLES = {
317
337
  SYSTEM: "system",
@@ -653,12 +673,44 @@ var TODO_STATUSES = {
653
673
  DONE: "done",
654
674
  SKIPPED: "skipped"
655
675
  };
676
+ var LOOT_TYPES = {
677
+ CREDENTIAL: "credential",
678
+ SESSION: "session",
679
+ HASH: "hash",
680
+ TOKEN: "token",
681
+ FILE: "file",
682
+ TICKET: "ticket",
683
+ SSH_KEY: "ssh_key",
684
+ CERTIFICATE: "certificate",
685
+ API_KEY: "api_key"
686
+ };
687
+ var ATTACK_TACTICS = {
688
+ INITIAL_ACCESS: "initial_access",
689
+ EXECUTION: "execution",
690
+ PERSISTENCE: "persistence",
691
+ PRIV_ESC: "privilege_escalation",
692
+ DEFENSE_EVASION: "defense_evasion",
693
+ CREDENTIAL_ACCESS: "credential_access",
694
+ DISCOVERY: "discovery",
695
+ LATERAL_MOVEMENT: "lateral_movement",
696
+ COLLECTION: "collection",
697
+ EXFILTRATION: "exfiltration",
698
+ C2: "command_and_control",
699
+ IMPACT: "impact"
700
+ };
656
701
  var APPROVAL_STATUSES = {
657
702
  AUTO: "auto",
658
703
  USER_CONFIRMED: "user_confirmed",
659
704
  USER_REVIEWED: "user_reviewed",
660
705
  DENIED: "denied"
661
706
  };
707
+ var SEVERITIES = {
708
+ CRITICAL: "critical",
709
+ HIGH: "high",
710
+ MEDIUM: "medium",
711
+ LOW: "low",
712
+ INFO: "info"
713
+ };
662
714
  var PRIORITIES = {
663
715
  HIGH: "high",
664
716
  MEDIUM: "medium",
@@ -757,6 +809,73 @@ function ensureDirExists(dirPath) {
757
809
  }
758
810
  }
759
811
 
812
+ // src/shared/constants/time.ts
813
+ var MS_PER_MINUTE = 6e4;
814
+ var SECONDS_PER_MINUTE = 60;
815
+ var SECONDS_PER_HOUR = 3600;
816
+
817
+ // src/shared/constants/paths.ts
818
+ import path from "path";
819
+ import { fileURLToPath } from "url";
820
+ var __filename = fileURLToPath(import.meta.url);
821
+ var __dirname = path.dirname(__filename);
822
+ var PROJECT_ROOT = path.resolve(__dirname, "../../../");
823
+ var PENTESTING_ROOT = ".pentesting";
824
+ var WORK_DIR = `${PENTESTING_ROOT}/tmp`;
825
+ var MEMORY_DIR = `${PENTESTING_ROOT}/memory`;
826
+ var REPORTS_DIR = `${PENTESTING_ROOT}/reports`;
827
+ var SESSIONS_DIR = `${PENTESTING_ROOT}/sessions`;
828
+ var LOOT_DIR = `${PENTESTING_ROOT}/loot`;
829
+ var OUTPUTS_DIR = `${PENTESTING_ROOT}/outputs`;
830
+ var DEBUG_DIR = `${PENTESTING_ROOT}/debug`;
831
+ var JOURNAL_DIR = `${PENTESTING_ROOT}/journal`;
832
+ var WORKSPACE = {
833
+ /** Root directory */
834
+ get ROOT() {
835
+ return path.resolve(PENTESTING_ROOT);
836
+ },
837
+ /** Temporary files */
838
+ get TMP() {
839
+ return path.resolve(WORK_DIR);
840
+ },
841
+ /** Persistent memory */
842
+ get MEMORY() {
843
+ return path.resolve(MEMORY_DIR);
844
+ },
845
+ /** Generated reports */
846
+ get REPORTS() {
847
+ return path.resolve(REPORTS_DIR);
848
+ },
849
+ /** Session snapshots */
850
+ get SESSIONS() {
851
+ return path.resolve(SESSIONS_DIR);
852
+ },
853
+ /** Captured loot */
854
+ get LOOT() {
855
+ return path.resolve(LOOT_DIR);
856
+ },
857
+ /** Full tool outputs */
858
+ get OUTPUTS() {
859
+ return path.resolve(OUTPUTS_DIR);
860
+ },
861
+ /** Debug logs */
862
+ get DEBUG() {
863
+ return path.resolve(DEBUG_DIR);
864
+ },
865
+ /** Persistent per-turn journal (§13 memo system) */
866
+ get JOURNAL() {
867
+ return path.resolve(JOURNAL_DIR);
868
+ }
869
+ };
870
+
871
+ // src/shared/constants/orchestrator.ts
872
+ var GRACEFUL_SHUTDOWN_WAIT_MS = 200;
873
+ var PROCESS_OUTPUT_TRUNCATION_LIMIT = 1e4;
874
+ var LONG_RUNNING_THRESHOLD_MS = 5 * MS_PER_MINUTE;
875
+ var VERY_LONG_RUNNING_THRESHOLD_MS = 15 * MS_PER_MINUTE;
876
+ var MAX_RECOMMENDATIONS_FOR_HEALTHY = 2;
877
+ var DEFAULT_DIRECTIVE_FOCUS = PHASES.RECON;
878
+
760
879
  // src/shared/utils/command-security-lists.ts
761
880
  var ALLOWED_BINARIES = /* @__PURE__ */ new Set([
762
881
  // Network scanning
@@ -964,7 +1083,7 @@ var SAFE_PIPE_TARGETS = /* @__PURE__ */ new Set([
964
1083
  "python3",
965
1084
  "python"
966
1085
  ]);
967
- var SAFE_REDIRECT_PATHS = ["/tmp/", "/root/", "/var/log/", "/opt/"];
1086
+ var SAFE_REDIRECT_PATHS = [`${WORK_DIR}/`];
968
1087
  var BLOCKED_BINARIES = /* @__PURE__ */ new Set([
969
1088
  "rm",
970
1089
  "shred",
@@ -989,50 +1108,6 @@ var BLOCKED_BINARIES = /* @__PURE__ */ new Set([
989
1108
  // src/shared/utils/debug-logger.ts
990
1109
  import { appendFileSync, writeFileSync } from "fs";
991
1110
  import { join } from "path";
992
-
993
- // src/shared/constants/paths.ts
994
- import path from "path";
995
- import { fileURLToPath } from "url";
996
- import { homedir } from "os";
997
- var __filename = fileURLToPath(import.meta.url);
998
- var __dirname = path.dirname(__filename);
999
- var PROJECT_ROOT = path.resolve(__dirname, "../../../");
1000
- var WORKSPACE_DIR_NAME = ".pentesting";
1001
- function getWorkspaceRoot() {
1002
- return path.join(homedir(), WORKSPACE_DIR_NAME);
1003
- }
1004
- var WORKSPACE = {
1005
- /** Root directory (resolved lazily via getWorkspaceRoot) */
1006
- get ROOT() {
1007
- return getWorkspaceRoot();
1008
- },
1009
- /** Per-session state snapshots */
1010
- get SESSIONS() {
1011
- return path.join(getWorkspaceRoot(), "sessions");
1012
- },
1013
- /** Debug logs */
1014
- get DEBUG() {
1015
- return path.join(getWorkspaceRoot(), "debug");
1016
- },
1017
- /** Generated reports */
1018
- get REPORTS() {
1019
- return path.join(getWorkspaceRoot(), "reports");
1020
- },
1021
- /** Downloaded loot, captured files */
1022
- get LOOT() {
1023
- return path.join(getWorkspaceRoot(), "loot");
1024
- },
1025
- /** Temporary files for active operations */
1026
- get TEMP() {
1027
- return path.join(getWorkspaceRoot(), "temp");
1028
- },
1029
- /** Full tool output files saved by Context Digest */
1030
- get OUTPUTS() {
1031
- return path.join(getWorkspaceRoot(), "outputs");
1032
- }
1033
- };
1034
-
1035
- // src/shared/utils/debug-logger.ts
1036
1111
  var DebugLogger = class _DebugLogger {
1037
1112
  static instance;
1038
1113
  logPath;
@@ -1123,16 +1198,16 @@ var SHELL_CHARS = {
1123
1198
  };
1124
1199
  function validateCommand(command) {
1125
1200
  if (!command || typeof command !== "string") {
1126
- return { safe: false, error: "Empty or invalid command" };
1201
+ return { isSafe: false, error: "Empty or invalid command" };
1127
1202
  }
1128
1203
  const normalizedCommand = command.trim();
1129
1204
  if (normalizedCommand.length === 0) {
1130
- return { safe: false, error: "Empty command" };
1205
+ return { isSafe: false, error: "Empty command" };
1131
1206
  }
1132
1207
  for (const pattern of INJECTION_PATTERNS) {
1133
1208
  if (pattern.test(normalizedCommand)) {
1134
1209
  return {
1135
- safe: false,
1210
+ isSafe: false,
1136
1211
  error: `Injection pattern detected: ${pattern.source}`,
1137
1212
  suggestion: "Avoid command substitution ($(), ``), variable expansion (${}) and control characters."
1138
1213
  };
@@ -1141,13 +1216,13 @@ function validateCommand(command) {
1141
1216
  const subCommands = splitChainedCommands(normalizedCommand);
1142
1217
  for (const subCmd of subCommands) {
1143
1218
  const result2 = validateSingleCommand(subCmd.trim());
1144
- if (!result2.safe) {
1219
+ if (!result2.isSafe) {
1145
1220
  return result2;
1146
1221
  }
1147
1222
  }
1148
1223
  const primaryBinary = extractBinary(subCommands[0].trim());
1149
1224
  return {
1150
- safe: true,
1225
+ isSafe: true,
1151
1226
  binary: primaryBinary || void 0,
1152
1227
  args: normalizedCommand.split(/\s+/).slice(1)
1153
1228
  };
@@ -1202,14 +1277,14 @@ function validateSingleCommand(command) {
1202
1277
  if (!binary) continue;
1203
1278
  if (BLOCKED_BINARIES.has(binary)) {
1204
1279
  return {
1205
- safe: false,
1280
+ isSafe: false,
1206
1281
  error: `Binary '${binary}' is blocked for security reasons`,
1207
1282
  suggestion: `'${binary}' is not allowed. Use a different approach.`
1208
1283
  };
1209
1284
  }
1210
1285
  if (idx > 0 && !SAFE_PIPE_TARGETS.has(binary) && !ALLOWED_BINARIES.has(binary)) {
1211
1286
  return {
1212
- safe: false,
1287
+ isSafe: false,
1213
1288
  error: `Pipe target '${binary}' is not in the allowed list`,
1214
1289
  suggestion: `Piping to '${binary}' is not allowed. Safe pipe targets: head, tail, grep, awk, sed, cut, sort, uniq, wc, jq, tee.`
1215
1290
  };
@@ -1219,10 +1294,10 @@ function validateSingleCommand(command) {
1219
1294
  }
1220
1295
  }
1221
1296
  const redirectResult = validateRedirects(command);
1222
- if (!redirectResult.safe) {
1297
+ if (!redirectResult.isSafe) {
1223
1298
  return redirectResult;
1224
1299
  }
1225
- return { safe: true };
1300
+ return { isSafe: true };
1226
1301
  }
1227
1302
  function splitByPipe(command) {
1228
1303
  const parts = [];
@@ -1265,33 +1340,91 @@ function stripRedirects(segment) {
1265
1340
  return segment.replace(/\d*>\s*&\d+/g, "").replace(/\d*>>\s*\S+/g, "").replace(/\d*>\s*\S+/g, "").replace(/<\s*\S+/g, "").trim();
1266
1341
  }
1267
1342
  function validateRedirects(command) {
1268
- const redirectPattern = /(\d*)>{1,2}\s*([^\s|;&]+)/g;
1269
- let match;
1270
- while ((match = redirectPattern.exec(command)) !== null) {
1271
- const target = match[2];
1272
- if (target.startsWith("&") || target === "/dev/null") continue;
1273
- const isAllowed = SAFE_REDIRECT_PATHS.some((p) => target.startsWith(p));
1274
- if (!isAllowed) {
1275
- return {
1276
- safe: false,
1277
- error: `Redirect to '${target}' is not in allowed paths`,
1278
- suggestion: `Redirect output to /tmp/ or /root/ paths. Example: > /tmp/output.txt`
1279
- };
1343
+ const redirects = extractRedirectTargets(command);
1344
+ for (const { type, fd, target } of redirects) {
1345
+ if (type === ">" || type === ">>") {
1346
+ if (target.startsWith("&") || target === "/dev/null") continue;
1347
+ const isAllowed = SAFE_REDIRECT_PATHS.some((p) => target.startsWith(p));
1348
+ if (!isAllowed) {
1349
+ return {
1350
+ isSafe: false,
1351
+ error: `Redirect to '${target}' is not in allowed paths`,
1352
+ suggestion: `Redirect output to ${WORK_DIR}/ paths. Example: > ${WORK_DIR}/output.txt`
1353
+ };
1354
+ }
1355
+ } else if (type === "<") {
1356
+ const isAllowed = SAFE_REDIRECT_PATHS.some((p) => target.startsWith(p));
1357
+ if (!isAllowed) {
1358
+ return {
1359
+ isSafe: false,
1360
+ error: `Input redirect from '${target}' is not in allowed paths`,
1361
+ suggestion: `Input redirect only from ${WORK_DIR}/ paths.`
1362
+ };
1363
+ }
1280
1364
  }
1281
1365
  }
1282
- const inputRedirectPattern = /<\s*([^\s|;&]+)/g;
1283
- while ((match = inputRedirectPattern.exec(command)) !== null) {
1284
- const source = match[1];
1285
- const isAllowed = SAFE_REDIRECT_PATHS.some((p) => source.startsWith(p));
1286
- if (!isAllowed) {
1287
- return {
1288
- safe: false,
1289
- error: `Input redirect from '${source}' is not in allowed paths`,
1290
- suggestion: `Input redirect only from /tmp/ or /root/ paths.`
1291
- };
1366
+ return { isSafe: true };
1367
+ }
1368
+ function extractRedirectTargets(command) {
1369
+ const redirects = [];
1370
+ let inSingle = false;
1371
+ let inDouble = false;
1372
+ let i = 0;
1373
+ while (i < command.length) {
1374
+ const ch = command[i];
1375
+ if (ch === "'" && !inDouble) {
1376
+ inSingle = !inSingle;
1377
+ i++;
1378
+ continue;
1292
1379
  }
1380
+ if (ch === '"' && !inSingle) {
1381
+ inDouble = !inDouble;
1382
+ i++;
1383
+ continue;
1384
+ }
1385
+ if (!inSingle && !inDouble) {
1386
+ if (ch === ">" || ch === "<") {
1387
+ const isAppend = ch === ">" && command[i + 1] === ">";
1388
+ const type = isAppend ? ">>" : ch;
1389
+ const jump = isAppend ? 2 : 1;
1390
+ let fd = "";
1391
+ if (ch === ">") {
1392
+ let b = i - 1;
1393
+ while (b >= 0 && /\d/.test(command[b])) {
1394
+ fd = command[b] + fd;
1395
+ b--;
1396
+ }
1397
+ }
1398
+ i += jump;
1399
+ while (i < command.length && /\s/.test(command[i])) i++;
1400
+ let target = "";
1401
+ let targetInSingle = false;
1402
+ let targetInDouble = false;
1403
+ while (i < command.length) {
1404
+ const tc = command[i];
1405
+ if (tc === "'" && !targetInDouble) {
1406
+ targetInSingle = !targetInSingle;
1407
+ i++;
1408
+ continue;
1409
+ }
1410
+ if (tc === '"' && !targetInSingle) {
1411
+ targetInDouble = !targetInDouble;
1412
+ i++;
1413
+ continue;
1414
+ }
1415
+ if (!targetInSingle && !targetInDouble && /[\s|;&<>]/.test(tc)) break;
1416
+ target += tc;
1417
+ i++;
1418
+ }
1419
+ if (target) {
1420
+ redirects.push({ type, fd, target });
1421
+ }
1422
+ continue;
1423
+ }
1424
+ }
1425
+ i++;
1293
1426
  }
1294
- return { safe: true };
1427
+ return redirects;
1295
1428
  }
1296
1429
  function extractBinary(command) {
1297
1430
  const parts = command.trim().split(/\s+/);
@@ -1514,7 +1647,7 @@ function clearCommandEventEmitter() {
1514
1647
  async function runCommand(command, args = [], options = {}) {
1515
1648
  const fullCommand = args.length > 0 ? `${command} ${args.join(" ")}` : command;
1516
1649
  const validation = validateCommand(fullCommand);
1517
- if (!validation.safe) {
1650
+ if (!validation.isSafe) {
1518
1651
  return {
1519
1652
  success: false,
1520
1653
  output: "",
@@ -1688,6 +1821,34 @@ function createTempFile(suffix = "") {
1688
1821
  return join2(tmpdir(), generateTempFilename(suffix));
1689
1822
  }
1690
1823
 
1824
+ // src/shared/constants/files.ts
1825
+ var FILE_EXTENSIONS = {
1826
+ // Data formats
1827
+ JSON: ".json",
1828
+ MARKDOWN: ".md",
1829
+ TXT: ".txt",
1830
+ XML: ".xml",
1831
+ // Network capture
1832
+ PCAP: ".pcap",
1833
+ HOSTS: ".hosts",
1834
+ MITM: ".mitm",
1835
+ // Process I/O
1836
+ STDOUT: ".stdout",
1837
+ STDERR: ".stderr",
1838
+ STDIN: ".stdin",
1839
+ // Scripts
1840
+ SH: ".sh",
1841
+ PY: ".py",
1842
+ CJS: ".cjs",
1843
+ // Config
1844
+ ENV: ".env",
1845
+ BAK: ".bak"
1846
+ };
1847
+ var SPECIAL_FILES = {
1848
+ LATEST_STATE: "latest.json",
1849
+ README: "README.md"
1850
+ };
1851
+
1691
1852
  // src/engine/process-cleanup.ts
1692
1853
  import { unlinkSync } from "fs";
1693
1854
 
@@ -1877,9 +2038,9 @@ function logEvent(processId, event, detail) {
1877
2038
  }
1878
2039
  function startBackgroundProcess(command, options = {}) {
1879
2040
  const processId = generatePrefixedId("bg");
1880
- const stdoutFile = createTempFile(".stdout");
1881
- const stderrFile = createTempFile(".stderr");
1882
- const stdinFile = createTempFile(".stdin");
2041
+ const stdoutFile = createTempFile(FILE_EXTENSIONS.STDOUT);
2042
+ const stderrFile = createTempFile(FILE_EXTENSIONS.STDERR);
2043
+ const stdinFile = createTempFile(FILE_EXTENSIONS.STDIN);
1883
2044
  const { tags, port, role, isInteractive } = detectProcessRole(command);
1884
2045
  let wrappedCmd;
1885
2046
  if (isInteractive) {
@@ -2531,13 +2692,13 @@ var AttackGraph = class {
2531
2692
  * Record a credential discovery and create spray edges.
2532
2693
  */
2533
2694
  addCredential(username, password, source) {
2534
- const credId = this.addNode("credential", `${username}:***`, {
2695
+ const credId = this.addNode(NODE_TYPE.CREDENTIAL, `${username}:***`, {
2535
2696
  username,
2536
2697
  password,
2537
2698
  source
2538
2699
  });
2539
2700
  for (const [id, node] of this.nodes) {
2540
- if (node.type === "service") {
2701
+ if (node.type === NODE_TYPE.SERVICE) {
2541
2702
  const svc = String(node.data.service || "");
2542
2703
  if (["ssh", "ftp", "rdp", "smb", "http", "mysql", "postgresql", "mssql", "winrm", "vnc", "telnet"].some((s) => svc.includes(s))) {
2543
2704
  this.addEdge(credId, id, "can_try_on", 0.6);
@@ -2550,7 +2711,7 @@ var AttackGraph = class {
2550
2711
  * Record a vulnerability finding.
2551
2712
  */
2552
2713
  addVulnerability(title, target, severity, hasExploit = false) {
2553
- const vulnId = this.addNode("vulnerability", title, {
2714
+ const vulnId = this.addNode(NODE_TYPE.VULNERABILITY, title, {
2554
2715
  target,
2555
2716
  severity,
2556
2717
  hasExploit
@@ -2561,7 +2722,7 @@ var AttackGraph = class {
2561
2722
  }
2562
2723
  }
2563
2724
  if (hasExploit) {
2564
- const accessId = this.addNode("access", `shell via ${title}`, {
2725
+ const accessId = this.addNode(NODE_TYPE.ACCESS, `shell via ${title}`, {
2565
2726
  via: title,
2566
2727
  status: GRAPH_STATUS.POTENTIAL
2567
2728
  });
@@ -2573,14 +2734,14 @@ var AttackGraph = class {
2573
2734
  * Record gained access.
2574
2735
  */
2575
2736
  addAccess(host, level, via) {
2576
- const accessId = this.addNode("access", `${level}@${host}`, {
2737
+ const accessId = this.addNode(NODE_TYPE.ACCESS, `${level}@${host}`, {
2577
2738
  host,
2578
2739
  level,
2579
2740
  via
2580
2741
  });
2581
2742
  this.markSucceeded(accessId);
2582
2743
  if (["root", "admin", "SYSTEM", "Administrator"].includes(level)) {
2583
- const lootId = this.addNode("loot", `flags on ${host}`, {
2744
+ const lootId = this.addNode(NODE_TYPE.LOOT, `flags on ${host}`, {
2584
2745
  host,
2585
2746
  status: GRAPH_STATUS.NEEDS_SEARCH
2586
2747
  });
@@ -2592,7 +2753,7 @@ var AttackGraph = class {
2592
2753
  * Record OSINT discovery (Docker image, GitHub repo, company info, etc.)
2593
2754
  */
2594
2755
  addOSINT(category, detail, data = {}) {
2595
- const osintId = this.addNode("osint", `${category}: ${detail}`, {
2756
+ const osintId = this.addNode(NODE_TYPE.OSINT, `${category}: ${detail}`, {
2596
2757
  category,
2597
2758
  detail,
2598
2759
  ...data
@@ -2979,7 +3140,7 @@ var WorkingMemory = class {
2979
3140
  const lines = ["<working-memory>"];
2980
3141
  if (failures.length > 0) {
2981
3142
  lines.push(`\u26A0\uFE0F FAILED ATTEMPTS (${failures.length} \u2014 DO NOT REPEAT):`);
2982
- for (const f of failures.slice(-5)) {
3143
+ for (const f of failures.slice(-DISPLAY_LIMITS.RECENT_FAILURES)) {
2983
3144
  lines.push(` \u2717 ${f.content}`);
2984
3145
  }
2985
3146
  }
@@ -2989,13 +3150,13 @@ var WorkingMemory = class {
2989
3150
  }
2990
3151
  if (successes.length > 0) {
2991
3152
  lines.push(`\u2705 RECENT SUCCESSES (${successes.length}):`);
2992
- for (const s of successes.slice(-3)) {
3153
+ for (const s of successes.slice(-DISPLAY_LIMITS.RECENT_SUCCESSES)) {
2993
3154
  lines.push(` \u2713 ${s.content}`);
2994
3155
  }
2995
3156
  }
2996
3157
  if (insights.length > 0) {
2997
3158
  lines.push(`\u{1F4A1} INSIGHTS:`);
2998
- for (const i of insights.slice(-3)) {
3159
+ for (const i of insights.slice(-DISPLAY_LIMITS.RECENT_INSIGHTS)) {
2999
3160
  lines.push(` \u2192 ${i.content}`);
3000
3161
  }
3001
3162
  }
@@ -3048,9 +3209,9 @@ var EpisodicMemory = class {
3048
3209
  toPrompt() {
3049
3210
  if (this.events.length === 0) return "";
3050
3211
  const lines = ["<session-timeline>"];
3051
- const recent = this.events.slice(-10);
3212
+ const recent = this.events.slice(-DISPLAY_LIMITS.RECENT_MEMORY_EVENTS);
3052
3213
  for (const e of recent) {
3053
- const mins = Math.floor((Date.now() - e.timestamp) / 6e4);
3214
+ const mins = Math.floor((Date.now() - e.timestamp) / MS_PER_MINUTE);
3054
3215
  const icon = {
3055
3216
  tool_success: "\u2705",
3056
3217
  tool_failure: "\u274C",
@@ -3069,7 +3230,6 @@ var EpisodicMemory = class {
3069
3230
  this.events = [];
3070
3231
  }
3071
3232
  };
3072
- var MEMORY_DIR = "/tmp/pentesting-memory";
3073
3233
  var MEMORY_FILE = join3(MEMORY_DIR, "persistent-knowledge.json");
3074
3234
  var PersistentMemory = class {
3075
3235
  knowledge;
@@ -3186,7 +3346,7 @@ var DynamicTechniqueLibrary = class {
3186
3346
  });
3187
3347
  if (this.techniques.length > this.maxTechniques) {
3188
3348
  this.techniques.sort((a, b) => {
3189
- if (a.verified !== b.verified) return a.verified ? -1 : 1;
3349
+ if (a.isVerified !== b.isVerified) return a.isVerified ? -1 : 1;
3190
3350
  return b.learnedAt - a.learnedAt;
3191
3351
  });
3192
3352
  this.techniques = this.techniques.slice(0, this.maxTechniques);
@@ -3222,7 +3382,7 @@ var DynamicTechniqueLibrary = class {
3222
3382
  source: `Web search: "${query}"`,
3223
3383
  technique: tech,
3224
3384
  applicableTo,
3225
- verified: false,
3385
+ isVerified: false,
3226
3386
  fromQuery: query
3227
3387
  });
3228
3388
  }
@@ -3233,7 +3393,7 @@ var DynamicTechniqueLibrary = class {
3233
3393
  verify(techniqueSubstring) {
3234
3394
  for (const t of this.techniques) {
3235
3395
  if (t.technique.toLowerCase().includes(techniqueSubstring.toLowerCase())) {
3236
- t.verified = true;
3396
+ t.isVerified = true;
3237
3397
  }
3238
3398
  }
3239
3399
  }
@@ -3261,8 +3421,8 @@ var DynamicTechniqueLibrary = class {
3261
3421
  */
3262
3422
  toPrompt() {
3263
3423
  if (this.techniques.length === 0) return "";
3264
- const verified = this.techniques.filter((t) => t.verified);
3265
- const unverified = this.techniques.filter((t) => !t.verified);
3424
+ const verified = this.techniques.filter((t) => t.isVerified);
3425
+ const unverified = this.techniques.filter((t) => !t.isVerified);
3266
3426
  const lines = ["<learned-techniques>"];
3267
3427
  if (verified.length > 0) {
3268
3428
  lines.push("VERIFIED (worked in this session):");
@@ -3539,8 +3699,8 @@ var SharedState = class {
3539
3699
  return this.data.currentPhase;
3540
3700
  }
3541
3701
  // --- CTF Mode ---
3542
- setCtfMode(enabled) {
3543
- this.data.ctfMode = enabled;
3702
+ setCtfMode(shouldEnable) {
3703
+ this.data.ctfMode = shouldEnable;
3544
3704
  }
3545
3705
  isCtfMode() {
3546
3706
  return this.data.ctfMode;
@@ -3826,20 +3986,6 @@ var ScopeGuard = class {
3826
3986
  };
3827
3987
 
3828
3988
  // src/engine/approval.ts
3829
- var CATEGORY_APPROVAL = {
3830
- [SERVICE_CATEGORIES.NETWORK]: APPROVAL_LEVELS.CONFIRM,
3831
- [SERVICE_CATEGORIES.WEB]: APPROVAL_LEVELS.CONFIRM,
3832
- [SERVICE_CATEGORIES.DATABASE]: APPROVAL_LEVELS.REVIEW,
3833
- [SERVICE_CATEGORIES.AD]: APPROVAL_LEVELS.REVIEW,
3834
- [SERVICE_CATEGORIES.EMAIL]: APPROVAL_LEVELS.CONFIRM,
3835
- [SERVICE_CATEGORIES.REMOTE_ACCESS]: APPROVAL_LEVELS.REVIEW,
3836
- [SERVICE_CATEGORIES.FILE_SHARING]: APPROVAL_LEVELS.CONFIRM,
3837
- [SERVICE_CATEGORIES.CLOUD]: APPROVAL_LEVELS.REVIEW,
3838
- [SERVICE_CATEGORIES.CONTAINER]: APPROVAL_LEVELS.REVIEW,
3839
- [SERVICE_CATEGORIES.API]: APPROVAL_LEVELS.CONFIRM,
3840
- [SERVICE_CATEGORIES.WIRELESS]: APPROVAL_LEVELS.REVIEW,
3841
- [SERVICE_CATEGORIES.ICS]: APPROVAL_LEVELS.BLOCK
3842
- };
3843
3989
  var ApprovalGate = class {
3844
3990
  constructor(shouldAutoApprove = false) {
3845
3991
  this.shouldAutoApprove = shouldAutoApprove;
@@ -3847,8 +3993,8 @@ var ApprovalGate = class {
3847
3993
  /**
3848
3994
  * Set auto-approve mode
3849
3995
  */
3850
- setAutoApprove(enabled) {
3851
- this.shouldAutoApprove = enabled;
3996
+ setAutoApprove(shouldEnable) {
3997
+ this.shouldAutoApprove = shouldEnable;
3852
3998
  }
3853
3999
  /**
3854
4000
  * Get current auto-approve mode
@@ -4143,7 +4289,7 @@ function autoExtractStructured(toolName, output) {
4143
4289
  data.vulnerabilities = vulns;
4144
4290
  hasData = true;
4145
4291
  }
4146
- if (toolName === "parse_nmap" || /nmap scan report/i.test(output)) {
4292
+ if (toolName === TOOL_NAMES.PARSE_NMAP || /nmap scan report/i.test(output)) {
4147
4293
  const nmap = extractNmapStructured(output);
4148
4294
  if (nmap.structured.openPorts && nmap.structured.openPorts.length > 0) {
4149
4295
  data.openPorts = nmap.structured.openPorts;
@@ -4548,7 +4694,8 @@ Used ports: ${usedPorts.join(", ")}
4548
4694
  [!] STRATEGY ADAPTATION REQUIRED:
4549
4695
  1. Try the next available port (e.g., ${nextPort} or 4445, 9001)
4550
4696
  2. If this is for a listener, update your Mission/Checklist with the NEW port so other agents know.
4551
- 3. Check bg_process({ action: "list" }) to see if you can stop the conflicting process.`
4697
+ 3. Check bg_process({ action: "list" }) to see if you can stop the conflicting process.`,
4698
+ error: `Port ${requestedPort} already in use`
4552
4699
  };
4553
4700
  }
4554
4701
  }
@@ -4695,7 +4842,7 @@ ${output.stderr.slice(-SYSTEM_LIMITS.MAX_STDERR_SLICE) || "(empty)"}` + connecti
4695
4842
  if (!cmd) return { success: false, output: "", error: "Missing command for interact. Provide the command to execute on the target." };
4696
4843
  const waitMs = Math.min(params.wait_ms || SYSTEM_LIMITS.DEFAULT_WAIT_MS_INTERACT, SYSTEM_LIMITS.MAX_WAIT_MS_INTERACT);
4697
4844
  const result2 = await sendToProcess(processId, cmd, waitMs);
4698
- if (!result2.success) return { success: false, output: result2.output };
4845
+ if (!result2.success) return { success: false, output: result2.output, error: result2.output };
4699
4846
  return {
4700
4847
  success: true,
4701
4848
  output: `Command sent: ${cmd}
@@ -4711,7 +4858,7 @@ ${result2.output}`
4711
4858
  if (!processId) return { success: false, output: "", error: "Missing process_id for promote" };
4712
4859
  const desc = params.description;
4713
4860
  const success = promoteToShell(processId, desc);
4714
- if (!success) return { success: false, output: `Process ${processId} not found` };
4861
+ if (!success) return { success: false, output: `Process ${processId} not found`, error: `Process ${processId} not found` };
4715
4862
  return {
4716
4863
  success: true,
4717
4864
  output: `[OK] Process ${processId} promoted to ACTIVE SHELL.
@@ -4861,7 +5008,8 @@ Examples:
4861
5008
  if (!validPhases.includes(newPhase)) {
4862
5009
  return {
4863
5010
  success: false,
4864
- output: `Invalid phase. Valid phases: ${validPhases.join(", ")}`
5011
+ output: `Invalid phase. Valid phases: ${validPhases.join(", ")}`,
5012
+ error: `Invalid phase: ${newPhase}`
4865
5013
  };
4866
5014
  }
4867
5015
  state.setPhase(newPhase);
@@ -4989,6 +5137,36 @@ function formatValidation(result2) {
4989
5137
  }
4990
5138
 
4991
5139
  // src/engine/tools/pentest-target-tools.ts
5140
+ function isPortArray(value) {
5141
+ if (!Array.isArray(value)) return false;
5142
+ return value.every(
5143
+ (item) => typeof item === "object" && item !== null && typeof item.port === "number"
5144
+ );
5145
+ }
5146
+ function parsePorts(value) {
5147
+ return isPortArray(value) ? value : [];
5148
+ }
5149
+ function isValidSeverity(value) {
5150
+ return typeof value === "string" && Object.values(SEVERITIES).includes(value);
5151
+ }
5152
+ function parseSeverity(value) {
5153
+ return isValidSeverity(value) ? value : "medium";
5154
+ }
5155
+ function isValidLootType(value) {
5156
+ return typeof value === "string" && Object.values(LOOT_TYPES).includes(value);
5157
+ }
5158
+ function parseLootType(value) {
5159
+ return isValidLootType(value) ? value : "file";
5160
+ }
5161
+ function isValidAttackTactic(value) {
5162
+ return typeof value === "string" && Object.values(ATTACK_TACTICS).includes(value);
5163
+ }
5164
+ function parseStringArray(value) {
5165
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : [];
5166
+ }
5167
+ function parseString(value, defaultValue = "") {
5168
+ return typeof value === "string" ? value : defaultValue;
5169
+ }
4992
5170
  var createTargetTools = (state) => [
4993
5171
  {
4994
5172
  name: TOOL_NAMES.ADD_TARGET,
@@ -5020,10 +5198,10 @@ The target will be tracked in SharedState and available for all agents.`,
5020
5198
  },
5021
5199
  required: ["ip"],
5022
5200
  execute: async (p) => {
5023
- const ip = p.ip;
5201
+ const ip = parseString(p.ip);
5024
5202
  const existing = state.getTarget(ip);
5025
5203
  if (existing) {
5026
- const newPorts = p.ports || [];
5204
+ const newPorts = parsePorts(p.ports);
5027
5205
  for (const np of newPorts) {
5028
5206
  const exists = existing.ports.some((ep) => ep.port === np.port);
5029
5207
  if (!exists) {
@@ -5037,11 +5215,11 @@ The target will be tracked in SharedState and available for all agents.`,
5037
5215
  state.attackGraph.addService(ip, np.port, np.service || "unknown", np.version);
5038
5216
  }
5039
5217
  }
5040
- if (p.hostname) existing.hostname = p.hostname;
5041
- if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...p.tags])];
5218
+ if (p.hostname) existing.hostname = parseString(p.hostname);
5219
+ if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...parseStringArray(p.tags)])];
5042
5220
  return { success: true, output: `Target ${ip} updated. Total ports: ${existing.ports.length}` };
5043
5221
  }
5044
- const ports = (p.ports || []).map((port) => ({
5222
+ const ports = parsePorts(p.ports).map((port) => ({
5045
5223
  port: port.port,
5046
5224
  service: port.service || "unknown",
5047
5225
  version: port.version,
@@ -5050,9 +5228,9 @@ The target will be tracked in SharedState and available for all agents.`,
5050
5228
  }));
5051
5229
  state.addTarget({
5052
5230
  ip,
5053
- hostname: p.hostname,
5231
+ hostname: parseString(p.hostname) || void 0,
5054
5232
  ports,
5055
- tags: p.tags || [],
5233
+ tags: parseStringArray(p.tags),
5056
5234
  firstSeen: Date.now()
5057
5235
  });
5058
5236
  return { success: true, output: `Target ${ip} added.${p.hostname ? ` Hostname: ${p.hostname}` : ""} Ports: ${ports.length}` };
@@ -5071,30 +5249,30 @@ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certifi
5071
5249
  },
5072
5250
  required: ["type", "host", "detail"],
5073
5251
  execute: async (p) => {
5074
- const lootType = p.type;
5252
+ const lootTypeStr = parseString(p.type);
5075
5253
  const crackableTypes = ["hash"];
5076
- const detail = p.detail;
5077
- const host = p.host;
5254
+ const detail = parseString(p.detail);
5255
+ const host = parseString(p.host);
5078
5256
  state.addLoot({
5079
- type: lootType,
5257
+ type: parseLootType(lootTypeStr),
5080
5258
  host,
5081
5259
  detail,
5082
5260
  obtainedAt: Date.now(),
5083
- isCrackable: crackableTypes.includes(lootType),
5261
+ isCrackable: crackableTypes.includes(lootTypeStr),
5084
5262
  isCracked: false
5085
5263
  });
5086
- if (["credential", "token", "ssh_key", "api_key"].includes(lootType)) {
5264
+ if (["credential", "token", "ssh_key", "api_key"].includes(lootTypeStr)) {
5087
5265
  const parts = detail.split(":");
5088
5266
  if (parts.length >= 2) {
5089
5267
  state.attackGraph.addCredential(parts[0], parts.slice(1).join(":"), host);
5090
5268
  }
5091
5269
  }
5092
- state.episodicMemory.record("access_gained", `Loot [${lootType}] from ${host}: ${detail.slice(0, DISPLAY_LIMITS.MEMORY_EVENT_PREVIEW)}`);
5270
+ state.episodicMemory.record("access_gained", `Loot [${lootTypeStr}] from ${host}: ${detail.slice(0, DISPLAY_LIMITS.MEMORY_EVENT_PREVIEW)}`);
5093
5271
  return {
5094
5272
  success: true,
5095
- output: `Loot recorded: [${lootType}] from ${host}
5273
+ output: `Loot recorded: [${lootTypeStr}] from ${host}
5096
5274
  Detail: ${detail}
5097
- ` + (crackableTypes.includes(lootType) ? `This is crackable. Consider: hash_crack({ hashes: "${detail.slice(0, DISPLAY_LIMITS.LOOT_DETAIL_PREVIEW)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
5275
+ ` + (crackableTypes.includes(lootTypeStr) ? `This is crackable. Consider: hash_crack({ hashes: "${detail.slice(0, DISPLAY_LIMITS.LOOT_DETAIL_PREVIEW)}..." })` : `Consider credential reuse / lateral movement with this loot.`)
5098
5276
  };
5099
5277
  }
5100
5278
  },
@@ -5113,12 +5291,13 @@ Findings without evidence are marked as UNVERIFIED and have low credibility.`,
5113
5291
  },
5114
5292
  required: ["title", "severity", "description", "evidence"],
5115
5293
  execute: async (p) => {
5116
- const evidence = p.evidence || [];
5117
- const title = p.title;
5118
- const severity = p.severity;
5119
- const affected = p.affected || [];
5120
- const description = p.description || "";
5294
+ const evidence = parseStringArray(p.evidence);
5295
+ const title = parseString(p.title);
5296
+ const severity = parseSeverity(p.severity);
5297
+ const affected = parseStringArray(p.affected);
5298
+ const description = parseString(p.description);
5121
5299
  const validation = validateFinding(evidence, severity);
5300
+ const attackPattern = parseString(p.attackPattern);
5122
5301
  state.addFinding({
5123
5302
  id: generateId(AGENT_LIMITS.ID_RADIX, AGENT_LIMITS.ID_LENGTH),
5124
5303
  title,
@@ -5129,7 +5308,7 @@ Findings without evidence are marked as UNVERIFIED and have low credibility.`,
5129
5308
  isVerified: validation.isVerified,
5130
5309
  remediation: "",
5131
5310
  foundAt: Date.now(),
5132
- ...p.attackPattern ? { attackPattern: p.attackPattern } : {}
5311
+ ...attackPattern && isValidAttackTactic(attackPattern) ? { attackPattern } : {}
5133
5312
  });
5134
5313
  const hasExploit = validation.isVerified;
5135
5314
  const target = affected[0] || "unknown";
@@ -5147,7 +5326,7 @@ ${formatValidation(validation)}`
5147
5326
  }
5148
5327
  ];
5149
5328
 
5150
- // src/engine/tools-mid.ts
5329
+ // src/engine/tools/intel-utils.ts
5151
5330
  import { execFileSync } from "child_process";
5152
5331
 
5153
5332
  // src/shared/utils/config.ts
@@ -5160,7 +5339,7 @@ var ENV_KEYS = {
5160
5339
  THINKING: "PENTEST_THINKING",
5161
5340
  THINKING_BUDGET: "PENTEST_THINKING_BUDGET"
5162
5341
  };
5163
- var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
5342
+ var DEFAULT_SEARCH_API_URL = "https://open.bigmodel.cn/api/paas/v4/tools/web-search-pro";
5164
5343
  function getApiKey() {
5165
5344
  return process.env[ENV_KEYS.API_KEY] || "";
5166
5345
  }
@@ -5171,7 +5350,10 @@ function getModel() {
5171
5350
  return process.env[ENV_KEYS.MODEL] || "";
5172
5351
  }
5173
5352
  function getSearchApiKey() {
5174
- return process.env[ENV_KEYS.SEARCH_API_KEY];
5353
+ if (process.env[ENV_KEYS.SEARCH_API_KEY]) {
5354
+ return process.env[ENV_KEYS.SEARCH_API_KEY];
5355
+ }
5356
+ return process.env[ENV_KEYS.API_KEY];
5175
5357
  }
5176
5358
  function getSearchApiUrl() {
5177
5359
  return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
@@ -5543,50 +5725,60 @@ var DEFAULT_BROWSER_OPTIONS = {
5543
5725
 
5544
5726
  // src/engine/tools/web-browser.ts
5545
5727
  async function browseUrl(url, options = {}) {
5546
- const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5547
- const { installed, browserInstalled } = await checkPlaywright();
5548
- if (!installed || !browserInstalled) {
5549
- const installResult = await installPlaywright();
5550
- if (!installResult.success) {
5728
+ try {
5729
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5730
+ const { installed, browserInstalled } = await checkPlaywright();
5731
+ if (!installed || !browserInstalled) {
5732
+ const installResult = await installPlaywright();
5733
+ if (!installResult.success) {
5734
+ return {
5735
+ success: false,
5736
+ output: "",
5737
+ error: `Playwright not available and auto-install failed: ${installResult.output}`
5738
+ };
5739
+ }
5740
+ }
5741
+ const screenshotPath = browserOptions.screenshot ? join6(join6(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
5742
+ const script = buildBrowseScript(url, browserOptions, screenshotPath);
5743
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "browse");
5744
+ if (!result2.success) {
5551
5745
  return {
5552
5746
  success: false,
5553
- output: "",
5554
- error: `Playwright not available and auto-install failed: ${installResult.output}`
5747
+ output: result2.output,
5748
+ error: result2.error
5749
+ };
5750
+ }
5751
+ if (result2.parsedData) {
5752
+ return {
5753
+ success: true,
5754
+ output: formatBrowserOutput(result2.parsedData, browserOptions),
5755
+ screenshots: screenshotPath ? [screenshotPath] : void 0,
5756
+ extractedData: result2.parsedData
5555
5757
  };
5556
5758
  }
5557
- }
5558
- const screenshotPath = browserOptions.screenshot ? join6(join6(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
5559
- const script = buildBrowseScript(url, browserOptions, screenshotPath);
5560
- const result2 = await runPlaywrightScript(script, browserOptions.timeout, "browse");
5561
- if (!result2.success) {
5562
5759
  return {
5563
- success: false,
5564
- output: result2.output,
5565
- error: result2.error
5760
+ success: true,
5761
+ output: result2.output || "Navigation completed",
5762
+ screenshots: screenshotPath ? [screenshotPath] : void 0
5566
5763
  };
5567
- }
5568
- if (result2.parsedData) {
5764
+ } catch (error) {
5765
+ const msg = error instanceof Error ? error.message : String(error);
5569
5766
  return {
5570
- success: true,
5571
- output: formatBrowserOutput(result2.parsedData, browserOptions),
5572
- screenshots: screenshotPath ? [screenshotPath] : void 0,
5573
- extractedData: result2.parsedData
5767
+ success: false,
5768
+ output: "",
5769
+ error: `Browser error: ${msg}`
5574
5770
  };
5575
5771
  }
5576
- return {
5577
- success: true,
5578
- output: result2.output || "Navigation completed",
5579
- screenshots: screenshotPath ? [screenshotPath] : void 0
5580
- };
5581
5772
  }
5582
5773
  async function fillAndSubmitForm(url, formData, options = {}) {
5583
- const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5584
- const safeUrl = safeJsString(url);
5585
- const safeFormData = JSON.stringify(formData);
5586
- const playwrightPath = getPlaywrightPath();
5587
- const safePlaywrightPath = safeJsString(playwrightPath);
5588
- const headlessMode = process.env.HEADLESS !== "false";
5589
- const script = `
5774
+ try {
5775
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5776
+ const safeUrl = safeJsString(url);
5777
+ const safeFormData = JSON.stringify(formData);
5778
+ const playwrightPath = getPlaywrightPath();
5779
+ const safePlaywrightPath = safeJsString(playwrightPath);
5780
+ const headlessMode = process.env.HEADLESS !== "false";
5781
+ const script = `
5590
5782
  const { chromium } = require(${safePlaywrightPath});
5591
5783
 
5592
5784
  (async () => {
@@ -5626,27 +5818,35 @@ const { chromium } = require(${safePlaywrightPath});
5626
5818
  }
5627
5819
  })();
5628
5820
  `;
5629
- const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
5630
- if (!result2.success) {
5821
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
5822
+ if (!result2.success) {
5823
+ return {
5824
+ success: false,
5825
+ output: result2.output,
5826
+ error: result2.error
5827
+ };
5828
+ }
5829
+ if (result2.parsedData) {
5830
+ const data = result2.parsedData;
5831
+ return {
5832
+ success: true,
5833
+ output: `Form submitted. Current URL: ${data.url}
5834
+ Title: ${data.title}`,
5835
+ extractedData: result2.parsedData
5836
+ };
5837
+ }
5631
5838
  return {
5632
- success: false,
5633
- output: result2.output,
5634
- error: result2.error
5839
+ success: true,
5840
+ output: result2.output || "Form submitted"
5635
5841
  };
5636
- }
5637
- if (result2.parsedData) {
5638
- const data = result2.parsedData;
5842
+ } catch (error) {
5843
+ const msg = error instanceof Error ? error.message : String(error);
5639
5844
  return {
5640
- success: true,
5641
- output: `Form submitted. Current URL: ${data.url}
5642
- Title: ${data.title}`,
5643
- extractedData: result2.parsedData
5845
+ success: false,
5846
+ output: "",
5847
+ error: `Form submission error: ${msg}`
5644
5848
  };
5645
5849
  }
5646
- return {
5647
- success: true,
5648
- output: result2.output || "Form submitted"
5649
- };
5650
5850
  }
5651
5851
  async function webSearchWithBrowser(query, engine = "google") {
5652
5852
  const searchUrls = {
@@ -5663,6 +5863,7 @@ async function webSearchWithBrowser(query, engine = "google") {
5663
5863
  }
5664
5864
 
5665
5865
  // src/engine/web-search-providers.ts
5866
+ var SEARCH_TIMEOUT_MS = 15e3;
5666
5867
  function getErrorMessage(error) {
5667
5868
  return error instanceof Error ? error.message : String(error);
5668
5869
  }
@@ -5670,25 +5871,43 @@ async function searchWithGLM(query, apiKey, apiUrl) {
5670
5871
  debugLog("search", "GLM request START", { apiUrl, query });
5671
5872
  const requestBody = {
5672
5873
  model: "web-search-pro",
5673
- messages: [{ role: "user", content: query }],
5874
+ messages: [{ role: LLM_ROLES.USER, content: query }],
5674
5875
  stream: false
5675
5876
  };
5676
5877
  debugLog("search", "GLM request body", requestBody);
5677
- const response = await fetch(apiUrl, {
5678
- method: "POST",
5679
- headers: {
5680
- [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5681
- [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`
5682
- },
5683
- body: JSON.stringify(requestBody)
5684
- });
5878
+ let response;
5879
+ try {
5880
+ response = await fetch(apiUrl, {
5881
+ method: "POST",
5882
+ headers: {
5883
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5884
+ [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`
5885
+ },
5886
+ body: JSON.stringify(requestBody),
5887
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5888
+ });
5889
+ } catch (err) {
5890
+ const msg = getErrorMessage(err);
5891
+ debugLog("search", "GLM fetch FAILED (network)", { error: msg });
5892
+ return { success: false, output: "", error: `GLM search network error: ${msg}. Check internet connection or API endpoint.` };
5893
+ }
5685
5894
  debugLog("search", "GLM response status", { status: response.status, ok: response.ok });
5686
5895
  if (!response.ok) {
5687
- const errorText = await response.text();
5896
+ let errorText = "";
5897
+ try {
5898
+ errorText = await response.text();
5899
+ } catch {
5900
+ }
5688
5901
  debugLog("search", "GLM response ERROR", { status: response.status, error: errorText });
5689
- throw new Error(`GLM Search API error: ${response.status} - ${errorText}`);
5902
+ return { success: false, output: "", error: `GLM Search API error ${response.status}: ${errorText.slice(0, 500)}` };
5903
+ }
5904
+ let data;
5905
+ try {
5906
+ data = await response.json();
5907
+ } catch (err) {
5908
+ debugLog("search", "GLM JSON parse FAILED", { error: getErrorMessage(err) });
5909
+ return { success: false, output: "", error: `GLM search returned invalid JSON: ${getErrorMessage(err)}` };
5690
5910
  }
5691
- const data = await response.json();
5692
5911
  debugLog("search", "GLM response data", { hasChoices: !!data.choices, choicesCount: data.choices?.length });
5693
5912
  let results = "";
5694
5913
  if (data.choices?.[0]?.message?.content) {
@@ -5716,19 +5935,37 @@ async function searchWithBrave(query, apiKey, apiUrl) {
5716
5935
  debugLog("search", "Brave request START", { apiUrl, query });
5717
5936
  const url = `${apiUrl}?q=${encodeURIComponent(query)}&count=${SEARCH_LIMIT.DEFAULT_RESULT_COUNT}`;
5718
5937
  debugLog("search", "Brave request URL", { url });
5719
- const response = await fetch(url, {
5720
- headers: {
5721
- [SEARCH_HEADER.ACCEPT]: "application/json",
5722
- [SEARCH_HEADER.X_SUBSCRIPTION_TOKEN]: apiKey
5723
- }
5724
- });
5938
+ let response;
5939
+ try {
5940
+ response = await fetch(url, {
5941
+ headers: {
5942
+ [SEARCH_HEADER.ACCEPT]: "application/json",
5943
+ [SEARCH_HEADER.X_SUBSCRIPTION_TOKEN]: apiKey
5944
+ },
5945
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5946
+ });
5947
+ } catch (err) {
5948
+ const msg = getErrorMessage(err);
5949
+ debugLog("search", "Brave fetch FAILED (network)", { error: msg });
5950
+ return { success: false, output: "", error: `Brave search network error: ${msg}. Check internet connection.` };
5951
+ }
5725
5952
  debugLog("search", "Brave response status", { status: response.status, ok: response.ok });
5726
5953
  if (!response.ok) {
5727
- const errorText = await response.text();
5954
+ let errorText = "";
5955
+ try {
5956
+ errorText = await response.text();
5957
+ } catch {
5958
+ }
5728
5959
  debugLog("search", "Brave response ERROR", { status: response.status, error: errorText });
5729
- throw new Error(`Brave API error: ${response.status}`);
5960
+ return { success: false, output: "", error: `Brave API error ${response.status}: ${errorText.slice(0, 500)}` };
5961
+ }
5962
+ let data;
5963
+ try {
5964
+ data = await response.json();
5965
+ } catch (err) {
5966
+ debugLog("search", "Brave JSON parse FAILED", { error: getErrorMessage(err) });
5967
+ return { success: false, output: "", error: `Brave search returned invalid JSON: ${getErrorMessage(err)}` };
5730
5968
  }
5731
- const data = await response.json();
5732
5969
  const results = data.web?.results || [];
5733
5970
  debugLog("search", "Brave results count", { count: results.length });
5734
5971
  if (results.length === 0) {
@@ -5744,21 +5981,39 @@ async function searchWithBrave(query, apiKey, apiUrl) {
5744
5981
  }
5745
5982
  async function searchWithSerper(query, apiKey, apiUrl) {
5746
5983
  debugLog("search", "Serper request START", { apiUrl, query });
5747
- const response = await fetch(apiUrl, {
5748
- method: "POST",
5749
- headers: {
5750
- [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5751
- [SEARCH_HEADER.X_API_KEY]: apiKey
5752
- },
5753
- body: JSON.stringify({ q: query })
5754
- });
5984
+ let response;
5985
+ try {
5986
+ response = await fetch(apiUrl, {
5987
+ method: "POST",
5988
+ headers: {
5989
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5990
+ [SEARCH_HEADER.X_API_KEY]: apiKey
5991
+ },
5992
+ body: JSON.stringify({ q: query }),
5993
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5994
+ });
5995
+ } catch (err) {
5996
+ const msg = getErrorMessage(err);
5997
+ debugLog("search", "Serper fetch FAILED (network)", { error: msg });
5998
+ return { success: false, output: "", error: `Serper search network error: ${msg}. Check internet connection.` };
5999
+ }
5755
6000
  debugLog("search", "Serper response status", { status: response.status, ok: response.ok });
5756
6001
  if (!response.ok) {
5757
- const errorText = await response.text();
6002
+ let errorText = "";
6003
+ try {
6004
+ errorText = await response.text();
6005
+ } catch {
6006
+ }
5758
6007
  debugLog("search", "Serper response ERROR", { status: response.status, error: errorText });
5759
- throw new Error(`Serper API error: ${response.status}`);
6008
+ return { success: false, output: "", error: `Serper API error ${response.status}: ${errorText.slice(0, 500)}` };
6009
+ }
6010
+ let data;
6011
+ try {
6012
+ data = await response.json();
6013
+ } catch (err) {
6014
+ debugLog("search", "Serper JSON parse FAILED", { error: getErrorMessage(err) });
6015
+ return { success: false, output: "", error: `Serper search returned invalid JSON: ${getErrorMessage(err)}` };
5760
6016
  }
5761
- const data = await response.json();
5762
6017
  const results = data.organic || [];
5763
6018
  debugLog("search", "Serper results count", { count: results.length });
5764
6019
  if (results.length === 0) {
@@ -5773,48 +6028,40 @@ async function searchWithSerper(query, apiKey, apiUrl) {
5773
6028
  return { success: true, output: formatted };
5774
6029
  }
5775
6030
  async function searchWithGenericApi(query, apiKey, apiUrl) {
5776
- const response = await fetch(apiUrl, {
5777
- method: "POST",
5778
- headers: {
5779
- [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5780
- [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`,
5781
- [SEARCH_HEADER.X_API_KEY]: apiKey
5782
- },
5783
- body: JSON.stringify({ query, q: query })
5784
- });
6031
+ let response;
6032
+ try {
6033
+ response = await fetch(apiUrl, {
6034
+ method: "POST",
6035
+ headers: {
6036
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
6037
+ [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`,
6038
+ [SEARCH_HEADER.X_API_KEY]: apiKey
6039
+ },
6040
+ body: JSON.stringify({ query, q: query }),
6041
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
6042
+ });
6043
+ } catch (err) {
6044
+ const msg = getErrorMessage(err);
6045
+ return { success: false, output: "", error: `Search API network error: ${msg}. Check internet connection or API endpoint.` };
6046
+ }
5785
6047
  if (!response.ok) {
5786
- const errorText = await response.text();
5787
- throw new Error(`Search API error: ${response.status} - ${errorText}`);
6048
+ let errorText = "";
6049
+ try {
6050
+ errorText = await response.text();
6051
+ } catch {
6052
+ }
6053
+ return { success: false, output: "", error: `Search API error ${response.status}: ${errorText.slice(0, 500)}` };
6054
+ }
6055
+ let data;
6056
+ try {
6057
+ data = await response.json();
6058
+ } catch (err) {
6059
+ return { success: false, output: "", error: `Search API returned invalid JSON: ${getErrorMessage(err)}` };
5788
6060
  }
5789
- const data = await response.json();
5790
6061
  return { success: true, output: JSON.stringify(data, null, 2) };
5791
6062
  }
5792
- async function searchWithPlaywright(query) {
5793
- debugLog("search", "Playwright Google search START", { query });
5794
- try {
5795
- const result2 = await webSearchWithBrowser(query, "google");
5796
- if (!result2.success) {
5797
- return {
5798
- success: false,
5799
- output: "",
5800
- error: `Playwright search failed: ${result2.error}. Set SEARCH_API_KEY for reliable search (Brave API by default).`
5801
- };
5802
- }
5803
- debugLog("search", "Playwright Google search COMPLETE", { outputLength: result2.output.length });
5804
- return {
5805
- success: true,
5806
- output: result2.output || `No results found for: ${query}`
5807
- };
5808
- } catch (error) {
5809
- return {
5810
- success: false,
5811
- output: "",
5812
- error: `Playwright search error: ${getErrorMessage(error)}. Set SEARCH_API_KEY for reliable search (Brave API by default).`
5813
- };
5814
- }
5815
- }
5816
6063
 
5817
- // src/engine/tools-mid.ts
6064
+ // src/engine/tools/intel-utils.ts
5818
6065
  var PORT_STATE2 = {
5819
6066
  OPEN: "open",
5820
6067
  CLOSED: "closed",
@@ -5925,31 +6172,46 @@ async function searchExploitDB(service, version) {
5925
6172
  }
5926
6173
  async function webSearch(query, _engine) {
5927
6174
  debugLog("search", "webSearch START", { query });
6175
+ const apiKey = getSearchApiKey();
6176
+ const apiUrl = getSearchApiUrl();
6177
+ debugLog("search", "Search API config", {
6178
+ hasApiKey: !!apiKey,
6179
+ apiUrl,
6180
+ apiKeyPrefix: apiKey ? apiKey.slice(0, DISPLAY_LIMITS.API_KEY_PREFIX) + "..." : null
6181
+ });
6182
+ if (!apiKey) {
6183
+ return {
6184
+ success: false,
6185
+ output: "",
6186
+ error: `SEARCH_API_KEY is required for web search. Set it in your environment:
6187
+
6188
+ # Brave Search (recommended - free tier available)
6189
+ export SEARCH_API_KEY=your_brave_api_key
6190
+ # Get key at: https://brave.com/search/api/
6191
+
6192
+ # Or GLM Web Search (\u667A\u8C31 AI)
6193
+ export SEARCH_API_KEY=your_glm_api_key
6194
+ export SEARCH_API_URL=https://open.bigmodel.cn/api/paas/v4/tools/web-search-pro
6195
+
6196
+ # Or Serper (Google)
6197
+ export SEARCH_API_KEY=your_serper_key
6198
+ export SEARCH_API_URL=https://google.serper.dev/search`
6199
+ };
6200
+ }
5928
6201
  try {
5929
- const apiKey = getSearchApiKey();
5930
- const apiUrl = getSearchApiUrl();
5931
- debugLog("search", "Search API config", {
5932
- hasApiKey: !!apiKey,
5933
- apiUrl,
5934
- apiKeyPrefix: apiKey ? apiKey.slice(0, DISPLAY_LIMITS.API_KEY_PREFIX) + "..." : null
5935
- });
5936
- if (apiKey) {
5937
- if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU)) {
5938
- debugLog("search", "Using GLM search");
5939
- return await searchWithGLM(query, apiKey, apiUrl);
5940
- } else if (apiUrl.includes(SEARCH_URL_PATTERN.BRAVE)) {
5941
- debugLog("search", "Using Brave search");
5942
- return await searchWithBrave(query, apiKey, apiUrl);
5943
- } else if (apiUrl.includes(SEARCH_URL_PATTERN.SERPER)) {
5944
- debugLog("search", "Using Serper search");
5945
- return await searchWithSerper(query, apiKey, apiUrl);
5946
- } else {
5947
- debugLog("search", "Using generic search API");
5948
- return await searchWithGenericApi(query, apiKey, apiUrl);
5949
- }
6202
+ if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU)) {
6203
+ debugLog("search", "Using GLM search");
6204
+ return await searchWithGLM(query, apiKey, apiUrl);
6205
+ } else if (apiUrl.includes(SEARCH_URL_PATTERN.BRAVE)) {
6206
+ debugLog("search", "Using Brave search");
6207
+ return await searchWithBrave(query, apiKey, apiUrl);
6208
+ } else if (apiUrl.includes(SEARCH_URL_PATTERN.SERPER)) {
6209
+ debugLog("search", "Using Serper search");
6210
+ return await searchWithSerper(query, apiKey, apiUrl);
6211
+ } else {
6212
+ debugLog("search", "Using generic search API");
6213
+ return await searchWithGenericApi(query, apiKey, apiUrl);
5950
6214
  }
5951
- debugLog("search", "SEARCH_API_KEY not set \u2014 falling back to Playwright Google search");
5952
- return await searchWithPlaywright(query);
5953
6215
  } catch (error) {
5954
6216
  debugLog("search", "webSearch ERROR", { error: getErrorMessage2(error) });
5955
6217
  return {
@@ -6332,7 +6594,8 @@ ${results.join("\n\n")}`
6332
6594
  }
6333
6595
  return {
6334
6596
  success: false,
6335
- output: `Category ${category} not found in any edition.`
6597
+ output: `Category ${category} not found in any edition.`,
6598
+ error: `Category ${category} not found`
6336
6599
  };
6337
6600
  }
6338
6601
  if (edition === "all") {
@@ -6343,7 +6606,7 @@ ${results.join("\n\n")}`
6343
6606
  }
6344
6607
  const data = OWASP_FULL_HISTORY[edition];
6345
6608
  if (!data) {
6346
- return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.` };
6609
+ return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.`, error: `Edition ${edition} not found` };
6347
6610
  }
6348
6611
  return {
6349
6612
  success: true,
@@ -6888,8 +7151,8 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
6888
7151
  }
6889
7152
  },
6890
7153
  execute: async (p) => {
6891
- const { existsSync: existsSync10, statSync: statSync2, readdirSync: readdirSync3 } = await import("fs");
6892
- const { join: join12 } = await import("path");
7154
+ const { existsSync: existsSync11, statSync: statSync3, readdirSync: readdirSync4 } = await import("fs");
7155
+ const { join: join13 } = await import("path");
6893
7156
  const category = p.category || "";
6894
7157
  const search = p.search || "";
6895
7158
  const minSize = p.min_size || 0;
@@ -6925,7 +7188,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
6925
7188
  const processFile = (fullPath, fileName) => {
6926
7189
  const ext = fileName.split(".").pop()?.toLowerCase();
6927
7190
  if (!WORDLIST_EXTENSIONS.has(ext || "")) return;
6928
- const stats = statSync2(fullPath);
7191
+ const stats = statSync3(fullPath);
6929
7192
  if (stats.size < minSize) return;
6930
7193
  if (!matchesCategory(fullPath)) return;
6931
7194
  if (!matchesSearch(fullPath, fileName)) return;
@@ -6935,16 +7198,16 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
6935
7198
  results.push("");
6936
7199
  };
6937
7200
  const scanDir = (dirPath, maxDepth = 3, depth = 0) => {
6938
- if (depth > maxDepth || !existsSync10(dirPath)) return;
7201
+ if (depth > maxDepth || !existsSync11(dirPath)) return;
6939
7202
  let entries;
6940
7203
  try {
6941
- entries = readdirSync3(dirPath, { withFileTypes: true });
7204
+ entries = readdirSync4(dirPath, { withFileTypes: true });
6942
7205
  } catch {
6943
7206
  return;
6944
7207
  }
6945
7208
  for (const entry of entries) {
6946
7209
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
6947
- const fullPath = join12(dirPath, entry.name);
7210
+ const fullPath = join13(dirPath, entry.name);
6948
7211
  if (entry.isDirectory()) {
6949
7212
  scanDir(fullPath, maxDepth, depth + 1);
6950
7213
  continue;
@@ -7235,7 +7498,7 @@ Requires root/sudo privileges.`,
7235
7498
  const iface = p.interface || "any";
7236
7499
  const filter = p.filter || "";
7237
7500
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SNIFF_DURATION;
7238
- const outputFile = p.output_file || createTempFile(".pcap");
7501
+ const outputFile = p.output_file || createTempFile(FILE_EXTENSIONS.PCAP);
7239
7502
  const count = p.count || NETWORK_CONFIG.DEFAULT_PACKET_COUNT;
7240
7503
  const extractCreds = p.extract_creds !== false;
7241
7504
  const filterFlag = filter ? `"${filter}"` : "";
@@ -7320,9 +7583,9 @@ Requires root/sudo privileges.`,
7320
7583
  const spoofIp = p.spoof_ip;
7321
7584
  const iface = p.interface || "";
7322
7585
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
7323
- const hostsFile = createTempFile(".hosts");
7324
- const { writeFileSync: writeFileSync8 } = await import("fs");
7325
- writeFileSync8(hostsFile, `${spoofIp} ${domain}
7586
+ const hostsFile = createTempFile(FILE_EXTENSIONS.HOSTS);
7587
+ const { writeFileSync: writeFileSync9 } = await import("fs");
7588
+ writeFileSync9(hostsFile, `${spoofIp} ${domain}
7326
7589
  ${spoofIp} *.${domain}
7327
7590
  `);
7328
7591
  const ifaceFlag = iface ? `-i ${iface}` : "";
@@ -7372,7 +7635,7 @@ Combine with arp_spoof for transparent proxying.`,
7372
7635
  const port = p.port || NETWORK_CONFIG.DEFAULT_PROXY_PORT;
7373
7636
  const targetHost = p.target_host;
7374
7637
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
7375
- const outputFile = p.output_file || createTempFile(".mitm");
7638
+ const outputFile = p.output_file || createTempFile(FILE_EXTENSIONS.MITM);
7376
7639
  const sslInsecure = p.ssl_insecure !== false;
7377
7640
  let cmd;
7378
7641
  const sslFlag = sslInsecure ? "--ssl-insecure" : "";
@@ -7436,7 +7699,7 @@ This is a high-level tool that combines tcpdump capture with protocol analysis.`
7436
7699
  const iface = p.interface || "any";
7437
7700
  const protocols = p.protocols || "all";
7438
7701
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SNIFF_DURATION;
7439
- const outputFile = createTempFile(".pcap");
7702
+ const outputFile = createTempFile(FILE_EXTENSIONS.PCAP);
7440
7703
  let bpfFilter = `host ${target}`;
7441
7704
  if (protocols !== "all") {
7442
7705
  const protoFilters = [];
@@ -7587,15 +7850,6 @@ var ZombieHunter = class {
7587
7850
  }
7588
7851
  };
7589
7852
 
7590
- // src/shared/constants/orchestrator.ts
7591
- var GRACEFUL_SHUTDOWN_WAIT_MS = 200;
7592
- var PROCESS_OUTPUT_TRUNCATION_LIMIT = 1e4;
7593
- var MS_PER_MINUTE = 6e4;
7594
- var LONG_RUNNING_THRESHOLD_MS = 5 * MS_PER_MINUTE;
7595
- var VERY_LONG_RUNNING_THRESHOLD_MS = 15 * MS_PER_MINUTE;
7596
- var MAX_RECOMMENDATIONS_FOR_HEALTHY = 2;
7597
- var DEFAULT_DIRECTIVE_FOCUS = PHASES.RECON;
7598
-
7599
7853
  // src/engine/resource/health-monitor.ts
7600
7854
  var HEALTH_STATUS = {
7601
7855
  HEALTHY: "healthy",
@@ -7838,6 +8092,86 @@ Returns recommendations on process status, port conflicts, long-running tasks, e
7838
8092
  }
7839
8093
  ];
7840
8094
 
8095
+ // src/shared/constants/service-ports.ts
8096
+ var SERVICE_PORTS = {
8097
+ SSH: 22,
8098
+ FTP: 21,
8099
+ TELNET: 23,
8100
+ SMTP: 25,
8101
+ DNS: 53,
8102
+ HTTP: 80,
8103
+ POP3: 110,
8104
+ IMAP: 143,
8105
+ SMB_NETBIOS: 139,
8106
+ KERBEROS: 88,
8107
+ LDAP: 389,
8108
+ SMB: 445,
8109
+ HTTPS: 443,
8110
+ SMTPS: 465,
8111
+ SMTP_TLS: 587,
8112
+ MODBUS: 502,
8113
+ IMAPS: 993,
8114
+ POP3S: 995,
8115
+ MSSQL: 1433,
8116
+ MYSQL: 3306,
8117
+ RDP: 3389,
8118
+ POSTGRESQL: 5432,
8119
+ VNC: 5900,
8120
+ REDIS: 6379,
8121
+ DOCKER_HTTP: 2375,
8122
+ DOCKER_HTTPS: 2376,
8123
+ KUBERNETES_API: 6443,
8124
+ HTTP_ALT: 8080,
8125
+ HTTPS_ALT: 8443,
8126
+ NFS: 2049,
8127
+ DNP3: 2e4,
8128
+ MONGODB: 27017,
8129
+ ELASTICSEARCH: 9200,
8130
+ MEMCACHED: 11211,
8131
+ NODE_DEFAULT: 3e3,
8132
+ FLASK_DEFAULT: 5e3,
8133
+ DJANGO_DEFAULT: 8e3
8134
+ };
8135
+ var CRITICAL_SERVICE_PORTS = [
8136
+ SERVICE_PORTS.SSH,
8137
+ SERVICE_PORTS.RDP,
8138
+ SERVICE_PORTS.MYSQL,
8139
+ SERVICE_PORTS.POSTGRESQL,
8140
+ SERVICE_PORTS.REDIS,
8141
+ SERVICE_PORTS.MONGODB
8142
+ ];
8143
+ var NO_AUTH_CRITICAL_PORTS = [
8144
+ SERVICE_PORTS.REDIS,
8145
+ SERVICE_PORTS.MONGODB,
8146
+ SERVICE_PORTS.ELASTICSEARCH,
8147
+ SERVICE_PORTS.MEMCACHED
8148
+ ];
8149
+ var WEB_SERVICE_PORTS = [
8150
+ SERVICE_PORTS.HTTP,
8151
+ SERVICE_PORTS.HTTPS,
8152
+ SERVICE_PORTS.HTTP_ALT,
8153
+ SERVICE_PORTS.HTTPS_ALT,
8154
+ SERVICE_PORTS.NODE_DEFAULT,
8155
+ SERVICE_PORTS.FLASK_DEFAULT,
8156
+ SERVICE_PORTS.DJANGO_DEFAULT
8157
+ ];
8158
+ var PLAINTEXT_HTTP_PORTS = [
8159
+ SERVICE_PORTS.HTTP,
8160
+ SERVICE_PORTS.HTTP_ALT,
8161
+ SERVICE_PORTS.NODE_DEFAULT
8162
+ ];
8163
+ var DATABASE_PORTS = [
8164
+ SERVICE_PORTS.MYSQL,
8165
+ SERVICE_PORTS.POSTGRESQL,
8166
+ SERVICE_PORTS.MSSQL,
8167
+ SERVICE_PORTS.MONGODB,
8168
+ SERVICE_PORTS.REDIS
8169
+ ];
8170
+ var SMB_PORTS = [
8171
+ SERVICE_PORTS.SMB,
8172
+ SERVICE_PORTS.SMB_NETBIOS
8173
+ ];
8174
+
7841
8175
  // src/domains/network/tools.ts
7842
8176
  var NETWORK_TOOLS = [
7843
8177
  {
@@ -7883,7 +8217,7 @@ var NETWORK_CONFIG2 = {
7883
8217
  tools: NETWORK_TOOLS,
7884
8218
  dangerLevel: DANGER_LEVELS.ACTIVE,
7885
8219
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
7886
- commonPorts: [21, 22, 80, 443, 445, 3389, 8080],
8220
+ commonPorts: [SERVICE_PORTS.FTP, SERVICE_PORTS.SSH, SERVICE_PORTS.HTTP, SERVICE_PORTS.HTTPS, SERVICE_PORTS.SMB, SERVICE_PORTS.RDP, SERVICE_PORTS.HTTP_ALT],
7887
8221
  commonServices: [SERVICES.FTP, SERVICES.SSH, SERVICES.HTTP, SERVICES.HTTPS, SERVICES.SMB]
7888
8222
  };
7889
8223
 
@@ -7946,7 +8280,7 @@ var WEB_CONFIG = {
7946
8280
  tools: WEB_TOOLS,
7947
8281
  dangerLevel: DANGER_LEVELS.ACTIVE,
7948
8282
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
7949
- commonPorts: [80, 443, 8080],
8283
+ commonPorts: [SERVICE_PORTS.HTTP, SERVICE_PORTS.HTTPS, SERVICE_PORTS.HTTP_ALT],
7950
8284
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
7951
8285
  };
7952
8286
 
@@ -7981,12 +8315,12 @@ var DATABASE_TOOLS = [
7981
8315
  description: "MySQL enumeration - version, users, databases",
7982
8316
  parameters: {
7983
8317
  target: { type: "string", description: "Target IP/hostname" },
7984
- port: { type: "string", description: "Port (default 3306)" }
8318
+ port: { type: "string", description: `Port (default ${SERVICE_PORTS.MYSQL})` }
7985
8319
  },
7986
8320
  required: ["target"],
7987
8321
  execute: async (params) => {
7988
8322
  const target = params.target;
7989
- const port = params.port || "3306";
8323
+ const port = params.port || String(SERVICE_PORTS.MYSQL);
7990
8324
  return await runCommand("mysql", ["-h", target, "-P", port, "-e", "SELECT VERSION(), USER(), DATABASE();"]);
7991
8325
  }
7992
8326
  },
@@ -8007,12 +8341,12 @@ var DATABASE_TOOLS = [
8007
8341
  description: "Redis enumeration",
8008
8342
  parameters: {
8009
8343
  target: { type: "string", description: "Target IP" },
8010
- port: { type: "string", description: "Port (default 6379)" }
8344
+ port: { type: "string", description: `Port (default ${SERVICE_PORTS.REDIS})` }
8011
8345
  },
8012
8346
  required: ["target"],
8013
8347
  execute: async (params) => {
8014
8348
  const target = params.target;
8015
- const port = params.port || "6379";
8349
+ const port = params.port || String(SERVICE_PORTS.REDIS);
8016
8350
  return await runCommand("redis-cli", ["-h", target, "-p", port, "INFO"]);
8017
8351
  }
8018
8352
  },
@@ -8044,7 +8378,7 @@ var DATABASE_CONFIG = {
8044
8378
  tools: DATABASE_TOOLS,
8045
8379
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8046
8380
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8047
- commonPorts: [1433, 3306, 5432, 6379, 27017],
8381
+ commonPorts: [SERVICE_PORTS.MSSQL, SERVICE_PORTS.MYSQL, SERVICE_PORTS.POSTGRESQL, SERVICE_PORTS.REDIS, SERVICE_PORTS.MONGODB],
8048
8382
  commonServices: [SERVICES.MYSQL, SERVICES.POSTGRES, SERVICES.REDIS, SERVICES.MONGODB]
8049
8383
  };
8050
8384
 
@@ -8097,7 +8431,7 @@ var AD_CONFIG = {
8097
8431
  tools: AD_TOOLS,
8098
8432
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8099
8433
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8100
- commonPorts: [88, 389, 445],
8434
+ commonPorts: [SERVICE_PORTS.KERBEROS, SERVICE_PORTS.LDAP, SERVICE_PORTS.SMB],
8101
8435
  commonServices: [SERVICES.AD, SERVICES.SMB]
8102
8436
  };
8103
8437
 
@@ -8121,7 +8455,7 @@ var EMAIL_CONFIG = {
8121
8455
  tools: EMAIL_TOOLS,
8122
8456
  dangerLevel: DANGER_LEVELS.ACTIVE,
8123
8457
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8124
- commonPorts: [25, 110, 143, 465, 587, 993, 995],
8458
+ commonPorts: [SERVICE_PORTS.SMTP, SERVICE_PORTS.POP3, SERVICE_PORTS.IMAP, SERVICE_PORTS.SMTPS, SERVICE_PORTS.SMTP_TLS, SERVICE_PORTS.IMAPS, SERVICE_PORTS.POP3S],
8125
8459
  commonServices: [SERVICES.SMTP, SERVICES.POP3, SERVICES.IMAP]
8126
8460
  };
8127
8461
 
@@ -8146,7 +8480,7 @@ var REMOTE_ACCESS_TOOLS = [
8146
8480
  },
8147
8481
  required: ["target"],
8148
8482
  execute: async (params) => {
8149
- return await runCommand("nmap", ["-p", "3389", "--script", "rdp-enum-encryption,rdp-ntlm-info", params.target]);
8483
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.RDP), "--script", "rdp-enum-encryption,rdp-ntlm-info", params.target]);
8150
8484
  }
8151
8485
  }
8152
8486
  ];
@@ -8156,7 +8490,7 @@ var REMOTE_ACCESS_CONFIG = {
8156
8490
  tools: REMOTE_ACCESS_TOOLS,
8157
8491
  dangerLevel: DANGER_LEVELS.ACTIVE,
8158
8492
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8159
- commonPorts: [22, 3389, 5900],
8493
+ commonPorts: [SERVICE_PORTS.SSH, SERVICE_PORTS.RDP, SERVICE_PORTS.VNC],
8160
8494
  commonServices: [SERVICES.SSH, SERVICES.RDP, SERVICES.VNC]
8161
8495
  };
8162
8496
 
@@ -8170,7 +8504,7 @@ var FILE_SHARING_TOOLS = [
8170
8504
  },
8171
8505
  required: ["target"],
8172
8506
  execute: async (params) => {
8173
- return await runCommand("nmap", ["-p", "21", "--script", "ftp-anon,ftp-syst", params.target]);
8507
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.FTP), "--script", "ftp-anon,ftp-syst", params.target]);
8174
8508
  }
8175
8509
  },
8176
8510
  {
@@ -8191,7 +8525,7 @@ var FILE_SHARING_CONFIG = {
8191
8525
  tools: FILE_SHARING_TOOLS,
8192
8526
  dangerLevel: DANGER_LEVELS.ACTIVE,
8193
8527
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8194
- commonPorts: [21, 139, 445, 2049],
8528
+ commonPorts: [SERVICE_PORTS.FTP, SERVICE_PORTS.SMB_NETBIOS, SERVICE_PORTS.SMB, SERVICE_PORTS.NFS],
8195
8529
  commonServices: [SERVICES.FTP, SERVICES.SMB, SERVICES.NFS]
8196
8530
  };
8197
8531
 
@@ -8235,7 +8569,7 @@ var CLOUD_CONFIG = {
8235
8569
  tools: CLOUD_TOOLS,
8236
8570
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8237
8571
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8238
- commonPorts: [443],
8572
+ commonPorts: [SERVICE_PORTS.HTTPS],
8239
8573
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8240
8574
  };
8241
8575
 
@@ -8270,7 +8604,7 @@ var CONTAINER_CONFIG = {
8270
8604
  tools: CONTAINER_TOOLS,
8271
8605
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8272
8606
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8273
- commonPorts: [2375, 2376, 5e3, 6443],
8607
+ commonPorts: [SERVICE_PORTS.DOCKER_HTTP, SERVICE_PORTS.DOCKER_HTTPS, SERVICE_PORTS.FLASK_DEFAULT, SERVICE_PORTS.KUBERNETES_API],
8274
8608
  commonServices: [SERVICES.DOCKER, SERVICES.KUBERNETES]
8275
8609
  };
8276
8610
 
@@ -8333,7 +8667,7 @@ var API_CONFIG = {
8333
8667
  tools: API_TOOLS,
8334
8668
  dangerLevel: DANGER_LEVELS.ACTIVE,
8335
8669
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8336
- commonPorts: [3e3, 5e3, 8e3, 8080],
8670
+ commonPorts: [SERVICE_PORTS.NODE_DEFAULT, SERVICE_PORTS.FLASK_DEFAULT, SERVICE_PORTS.DJANGO_DEFAULT, SERVICE_PORTS.HTTP_ALT],
8337
8671
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8338
8672
  };
8339
8673
 
@@ -8371,7 +8705,7 @@ var ICS_TOOLS = [
8371
8705
  },
8372
8706
  required: ["target"],
8373
8707
  execute: async (params) => {
8374
- return await runCommand("nmap", ["-p", "502", "--script", "modbus-discover", params.target]);
8708
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.MODBUS), "--script", "modbus-discover", params.target]);
8375
8709
  }
8376
8710
  }
8377
8711
  ];
@@ -8381,7 +8715,7 @@ var ICS_CONFIG = {
8381
8715
  tools: ICS_TOOLS,
8382
8716
  dangerLevel: DANGER_LEVELS.ACTIVE,
8383
8717
  defaultApproval: APPROVAL_LEVELS.BLOCK,
8384
- commonPorts: [502, 2e4],
8718
+ commonPorts: [SERVICE_PORTS.MODBUS, SERVICE_PORTS.DNP3],
8385
8719
  commonServices: [SERVICES.MODBUS, SERVICES.DNP3]
8386
8720
  };
8387
8721
 
@@ -8443,8 +8777,18 @@ var ToolRegistry = class {
8443
8777
  this.logDeniedAction(toolCall, approval.reason || "Execution denied");
8444
8778
  return { success: false, output: "", error: approval.reason || "Denied by policy" };
8445
8779
  }
8446
- const result2 = await tool.execute(toolCall.input);
8447
- const command = String(toolCall.input.command || toolCall.input.url || toolCall.input.query || "");
8780
+ let result2;
8781
+ try {
8782
+ result2 = await tool.execute(toolCall.input);
8783
+ } catch (execError) {
8784
+ const errMsg = execError instanceof Error ? execError.message : String(execError);
8785
+ result2 = {
8786
+ success: false,
8787
+ output: "",
8788
+ error: `Tool execution error: ${errMsg}`
8789
+ };
8790
+ }
8791
+ const command = String(toolCall.input.command || toolCall.input.url || toolCall.input.query || JSON.stringify(toolCall.input));
8448
8792
  if (result2.success) {
8449
8793
  this.state.workingMemory.recordSuccess(toolCall.name, command, result2.output || "");
8450
8794
  } else {
@@ -8547,7 +8891,7 @@ var SERVICE_CATEGORY_MAP = {
8547
8891
  "docker": SERVICE_CATEGORIES.CONTAINER,
8548
8892
  "modbus": SERVICE_CATEGORIES.ICS
8549
8893
  };
8550
- var CATEGORY_APPROVAL2 = {
8894
+ var CATEGORY_APPROVAL = {
8551
8895
  [SERVICE_CATEGORIES.NETWORK]: APPROVAL_LEVELS.CONFIRM,
8552
8896
  [SERVICE_CATEGORIES.WEB]: APPROVAL_LEVELS.CONFIRM,
8553
8897
  [SERVICE_CATEGORIES.DATABASE]: APPROVAL_LEVELS.REVIEW,
@@ -8696,6 +9040,10 @@ var DOMAINS = {
8696
9040
  };
8697
9041
 
8698
9042
  // src/engine/tools-registry.ts
9043
+ var VALID_CATEGORIES = Object.values(SERVICE_CATEGORIES);
9044
+ function isValidCategory(id) {
9045
+ return VALID_CATEGORIES.includes(id);
9046
+ }
8699
9047
  var CategorizedToolRegistry = class extends ToolRegistry {
8700
9048
  categories = /* @__PURE__ */ new Map();
8701
9049
  constructor(state, scopeGuard, approvalGate, events) {
@@ -8727,13 +9075,13 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8727
9075
  ];
8728
9076
  const coreTools = coreToolNames.map((name) => this.getTool(name)).filter((t) => !!t);
8729
9077
  Object.keys(DOMAINS).forEach((id) => {
8730
- const cat = id;
8731
- this.categories.set(cat, {
8732
- name: cat,
8733
- description: DOMAINS[cat]?.description || "",
9078
+ if (!isValidCategory(id)) return;
9079
+ this.categories.set(id, {
9080
+ name: id,
9081
+ description: DOMAINS[id]?.description || "",
8734
9082
  tools: [...coreTools],
8735
- dangerLevel: this.calculateDanger(cat),
8736
- defaultApproval: CATEGORY_APPROVAL2[cat] || "confirm"
9083
+ dangerLevel: this.calculateDanger(id),
9084
+ defaultApproval: CATEGORY_APPROVAL[id] || "confirm"
8737
9085
  });
8738
9086
  });
8739
9087
  }
@@ -8750,19 +9098,17 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8750
9098
  const cat = ServiceParser.detectCategory(p.port, p.service);
8751
9099
  if (cat && !seen.has(cat)) {
8752
9100
  seen.add(cat);
8753
- const suggestion = {
9101
+ results.push({
8754
9102
  category: cat,
8755
9103
  tools: this.getByCategory(cat).map((t) => t.name)
8756
- };
8757
- results.push(suggestion);
9104
+ });
8758
9105
  }
8759
9106
  });
8760
9107
  if (target.hostname && ServiceParser.isCloudHostname(target.hostname) && !seen.has(SERVICE_CATEGORIES.CLOUD)) {
8761
- const cloudSuggestion = {
9108
+ results.push({
8762
9109
  category: SERVICE_CATEGORIES.CLOUD,
8763
9110
  tools: this.getByCategory(SERVICE_CATEGORIES.CLOUD).map((t) => t.name)
8764
- };
8765
- results.push(cloudSuggestion);
9111
+ });
8766
9112
  }
8767
9113
  return results;
8768
9114
  }
@@ -8801,7 +9147,7 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8801
9147
  return Array.from(this.categories.values());
8802
9148
  }
8803
9149
  getApprovalForCategory(cat) {
8804
- return CATEGORY_APPROVAL2[cat] || "confirm";
9150
+ return CATEGORY_APPROVAL[cat] || "confirm";
8805
9151
  }
8806
9152
  };
8807
9153
 
@@ -8886,14 +9232,28 @@ var LLM_ERROR_TYPES = {
8886
9232
  var HTTP_STATUS = { BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, RATE_LIMIT: 429 };
8887
9233
  var NETWORK_ERROR_CODES = {
8888
9234
  ECONNRESET: "ECONNRESET",
9235
+ ECONNREFUSED: "ECONNREFUSED",
8889
9236
  ETIMEDOUT: "ETIMEDOUT",
8890
9237
  ENOTFOUND: "ENOTFOUND",
8891
- CONNECT_TIMEOUT: "UND_ERR_CONNECT_TIMEOUT"
9238
+ CONNECT_TIMEOUT: "UND_ERR_CONNECT_TIMEOUT",
9239
+ SOCKET_TIMEOUT: "UND_ERR_SOCKET"
8892
9240
  };
8893
9241
  var TRANSIENT_NETWORK_ERRORS = [
8894
9242
  NETWORK_ERROR_CODES.ECONNRESET,
9243
+ NETWORK_ERROR_CODES.ECONNREFUSED,
8895
9244
  NETWORK_ERROR_CODES.ETIMEDOUT,
8896
- NETWORK_ERROR_CODES.ENOTFOUND
9245
+ NETWORK_ERROR_CODES.ENOTFOUND,
9246
+ NETWORK_ERROR_CODES.SOCKET_TIMEOUT
9247
+ ];
9248
+ var NETWORK_ERROR_PATTERNS = [
9249
+ "fetch failed",
9250
+ "network error",
9251
+ "econnrefused",
9252
+ "econnreset",
9253
+ "enotfound",
9254
+ "etimedout",
9255
+ "socket hang up",
9256
+ "dns lookup failed"
8897
9257
  ];
8898
9258
  var LLMError = class extends Error {
8899
9259
  /** Structured error information */
@@ -8920,12 +9280,17 @@ function classifyError(error) {
8920
9280
  if (statusCode === HTTP_STATUS.BAD_REQUEST) {
8921
9281
  return { type: LLM_ERROR_TYPES.INVALID_REQUEST, message: errorMessage, statusCode, isRetryable: false, suggestedAction: "Modify request" };
8922
9282
  }
8923
- if (e.code && TRANSIENT_NETWORK_ERRORS.includes(e.code)) {
9283
+ const errorCode = e.code || e.cause?.code;
9284
+ if (errorCode && TRANSIENT_NETWORK_ERRORS.includes(errorCode)) {
8924
9285
  return { type: LLM_ERROR_TYPES.NETWORK_ERROR, message: errorMessage, isRetryable: true, suggestedAction: "Check network" };
8925
9286
  }
8926
- if (errorMessage.toLowerCase().includes("timeout") || e.code === NETWORK_ERROR_CODES.CONNECT_TIMEOUT) {
9287
+ if (errorMessage.toLowerCase().includes("timeout") || errorCode === NETWORK_ERROR_CODES.CONNECT_TIMEOUT) {
8927
9288
  return { type: LLM_ERROR_TYPES.TIMEOUT, message: errorMessage, isRetryable: true, suggestedAction: "Retry" };
8928
9289
  }
9290
+ const lowerMsg = errorMessage.toLowerCase();
9291
+ if (NETWORK_ERROR_PATTERNS.some((pattern) => lowerMsg.includes(pattern))) {
9292
+ return { type: LLM_ERROR_TYPES.NETWORK_ERROR, message: errorMessage, isRetryable: true, suggestedAction: "Check network" };
9293
+ }
8929
9294
  return { type: LLM_ERROR_TYPES.UNKNOWN, message: errorMessage, statusCode, isRetryable: false, suggestedAction: "Analyze error" };
8930
9295
  }
8931
9296
 
@@ -9004,7 +9369,11 @@ var LLMClient = class {
9004
9369
  signal
9005
9370
  });
9006
9371
  if (!response.ok) {
9007
- const errorBody = await response.text();
9372
+ let errorBody = `HTTP ${response.status}`;
9373
+ try {
9374
+ errorBody = await response.text();
9375
+ } catch {
9376
+ }
9008
9377
  const error = new Error(errorBody);
9009
9378
  error.status = response.status;
9010
9379
  throw error;
@@ -9340,7 +9709,7 @@ function saveState(state) {
9340
9709
  }
9341
9710
  function pruneOldSessions(sessionsDir) {
9342
9711
  try {
9343
- const sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json") && f !== "latest.json").map((f) => ({
9712
+ const sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(FILE_EXTENSIONS.JSON) && f !== SPECIAL_FILES.LATEST_STATE).map((f) => ({
9344
9713
  name: f,
9345
9714
  path: join9(sessionsDir, f),
9346
9715
  mtime: statSync(join9(sessionsDir, f)).mtimeMs
@@ -9377,7 +9746,10 @@ function loadState(state) {
9377
9746
  state.addLoot(loot);
9378
9747
  }
9379
9748
  for (const item of snapshot.todo) {
9380
- state.addTodo(item.content, item.priority);
9749
+ const id = state.addTodo(item.content, item.priority);
9750
+ if (item.status && item.status !== "pending") {
9751
+ state.updateTodo(id, { status: item.status });
9752
+ }
9381
9753
  }
9382
9754
  const validPhases = new Set(Object.values(PHASES));
9383
9755
  const restoredPhase = validPhases.has(snapshot.currentPhase) ? snapshot.currentPhase : PHASES.RECON;
@@ -9387,6 +9759,20 @@ function loadState(state) {
9387
9759
  }
9388
9760
  if (snapshot.missionChecklist?.length > 0) {
9389
9761
  state.addMissionChecklistItems(snapshot.missionChecklist.map((c) => c.text));
9762
+ const restoredChecklist = state.getMissionChecklist();
9763
+ const baseIndex = restoredChecklist.length - snapshot.missionChecklist.length;
9764
+ const completedUpdates = [];
9765
+ for (let i = 0; i < snapshot.missionChecklist.length; i++) {
9766
+ if (snapshot.missionChecklist[i].isCompleted && restoredChecklist[baseIndex + i]) {
9767
+ completedUpdates.push({
9768
+ id: restoredChecklist[baseIndex + i].id,
9769
+ isCompleted: true
9770
+ });
9771
+ }
9772
+ }
9773
+ if (completedUpdates.length > 0) {
9774
+ state.updateMissionChecklist(completedUpdates);
9775
+ }
9390
9776
  }
9391
9777
  return true;
9392
9778
  } catch (err) {
@@ -9400,8 +9786,9 @@ function clearWorkspace() {
9400
9786
  const dirsToClean = [
9401
9787
  { path: WORKSPACE.SESSIONS, label: "sessions" },
9402
9788
  { path: WORKSPACE.DEBUG, label: "debug logs" },
9403
- { path: WORKSPACE.TEMP, label: "temp files" },
9404
- { path: WORKSPACE.OUTPUTS, label: "outputs" }
9789
+ { path: WORKSPACE.TMP, label: "temp files" },
9790
+ { path: WORKSPACE.OUTPUTS, label: "outputs" },
9791
+ { path: WORKSPACE.JOURNAL, label: "journal" }
9405
9792
  ];
9406
9793
  for (const dir of dirsToClean) {
9407
9794
  try {
@@ -9481,387 +9868,97 @@ function appendBlockedCommandHints(lines, errorLower) {
9481
9868
  if (errorLower.includes("pipe target") || errorLower.includes("injection")) {
9482
9869
  lines.push(`Fix: Use the tool's built-in output options instead of shell pipes.`);
9483
9870
  lines.push(`Alternative approaches:`);
9484
- lines.push(` 1. Save output to file: command > /tmp/output.txt, then use read_file("/tmp/output.txt")`);
9485
- lines.push(` 2. Use tool-specific flags: nmap -oN /tmp/scan.txt, curl -o /tmp/page.html`);
9871
+ lines.push(` 1. Save output to file: command > ${WORK_DIR}/output.txt, then use read_file("${WORK_DIR}/output.txt")`);
9872
+ lines.push(` 2. Use tool-specific flags: nmap -oN ${WORK_DIR}/scan.txt, curl -o ${WORK_DIR}/page.html`);
9486
9873
  lines.push(` 3. Use browse_url for web content instead of curl | grep`);
9487
9874
  lines.push(` 4. If filtering output: run the command first, then read_file + parse results`);
9488
9875
  } else if (errorLower.includes("redirect")) {
9489
- lines.push(`Fix: Redirect to /tmp/ or /root/ paths only.`);
9490
- lines.push(`Example: command > /tmp/output.txt or command 2>&1 > /tmp/errors.txt`);
9876
+ lines.push(`Fix: Redirect to ${WORK_DIR}/ path.`);
9877
+ lines.push(`Example: command > ${WORK_DIR}/output.txt or command 2>&1 > ${WORK_DIR}/errors.txt`);
9491
9878
  } else {
9492
9879
  lines.push(`Fix: Simplify the command. Avoid chaining complex operations.`);
9493
9880
  lines.push(`Break into multiple steps: run_cmd \u2192 read_file \u2192 run_cmd.`);
9494
9881
  }
9495
9882
  }
9496
9883
 
9497
- // src/shared/utils/output-compressor.ts
9498
- var MIN_COMPRESS_LENGTH = 3e3;
9499
- var SUMMARY_HEADER = "\u2550\u2550\u2550 INTELLIGENCE SUMMARY (auto-extracted) \u2550\u2550\u2550";
9500
- var SUMMARY_FOOTER = "\u2550\u2550\u2550 END SUMMARY \u2014 Full output follows \u2550\u2550\u2550";
9501
- var EXTRACT_LIMITS = {
9502
- NMAP_PORTS: 30,
9503
- NMAP_VULNS: 10,
9504
- LINPEAS_SUDO: 500,
9505
- LINPEAS_WRITABLE: 300,
9506
- LINPEAS_CRON: 5,
9507
- LINPEAS_PASSWORDS: 5,
9508
- ENUM4LINUX_SHARES: 10,
9509
- DIRBUST_PATHS: 20,
9510
- SQLMAP_INJECTIONS: 5,
9511
- HASH_NTLM: 5,
9512
- HASH_PREVIEW_LEN: 100,
9513
- GENERIC_CREDS: 5,
9514
- GENERIC_PATHS: 10
9515
- };
9516
- var TOOL_SIGNATURES = [
9517
- {
9518
- name: "nmap",
9519
- signatures: [/Nmap scan report/i, /PORT\s+STATE\s+SERVICE/i, /Nmap done:/i],
9520
- extract: extractNmapIntel
9521
- },
9522
- {
9523
- name: "linpeas",
9524
- signatures: [/linpeas/i, /╔══.*╗/, /Linux Privilege Escalation/i],
9525
- extract: extractLinpeasIntel
9526
- },
9527
- {
9528
- name: "enum4linux",
9529
- signatures: [/enum4linux/i, /Starting enum4linux/i, /\|\s+Target\s+Information/i],
9530
- extract: extractEnum4linuxIntel
9531
- },
9532
- {
9533
- name: "gobuster/ffuf/feroxbuster",
9534
- signatures: [/Gobuster/i, /FFUF/i, /feroxbuster/i, /Status:\s*\d{3}/],
9535
- extract: extractDirBustIntel
9536
- },
9537
- {
9538
- name: "sqlmap",
9539
- signatures: [/sqlmap/i, /\[INFO\]\s+testing/i, /Parameter:\s+/i],
9540
- extract: extractSqlmapIntel
9541
- },
9542
- {
9543
- name: "hash_dump",
9544
- signatures: [/\$[0-9]\$/, /\$2[aby]\$/, /:[0-9]+:[a-f0-9]{32}:/i],
9545
- extract: extractHashIntel
9546
- }
9547
- ];
9548
- function compressToolOutput(output, toolName) {
9549
- if (!output || output.length < MIN_COMPRESS_LENGTH) {
9550
- return output;
9551
- }
9552
- let intel = [];
9553
- let detectedTool = "";
9554
- for (const sig of TOOL_SIGNATURES) {
9555
- const matched = sig.signatures.some((s) => s.test(output));
9556
- if (matched) {
9557
- detectedTool = sig.name;
9558
- intel = sig.extract(output);
9559
- break;
9560
- }
9561
- }
9562
- if (intel.length === 0) {
9563
- intel = extractGenericIntel(output);
9564
- detectedTool = toolName || "unknown";
9565
- }
9566
- if (intel.length === 0) {
9567
- return output;
9568
- }
9569
- const summary = [
9570
- SUMMARY_HEADER,
9571
- `Tool: ${detectedTool} | Output length: ${output.length} chars`,
9572
- "",
9573
- ...intel,
9574
- "",
9575
- SUMMARY_FOOTER,
9576
- ""
9577
- ].join("\n");
9578
- return summary + output;
9579
- }
9580
- function extractNmapIntel(output) {
9581
- const intel = [];
9582
- const lines = output.split("\n");
9583
- const hostMatches = output.match(/Nmap scan report for\s+(\S+)/gi);
9584
- const openPorts = output.match(/^\d+\/\w+\s+open\s+/gm);
9585
- if (hostMatches) intel.push(`Hosts scanned: ${hostMatches.length}`);
9586
- if (openPorts) intel.push(`Open ports found: ${openPorts.length}`);
9587
- const portLines = lines.filter((l) => /^\d+\/\w+\s+open\s+/.test(l.trim()));
9588
- if (portLines.length > 0) {
9589
- intel.push("Open ports:");
9590
- for (const pl of portLines.slice(0, EXTRACT_LIMITS.NMAP_PORTS)) {
9591
- intel.push(` ${pl.trim()}`);
9592
- }
9593
- }
9594
- const vulnLines = lines.filter(
9595
- (l) => /VULNERABLE|CVE-\d{4}-\d+|exploit|CRITICAL/i.test(l)
9596
- );
9597
- if (vulnLines.length > 0) {
9598
- intel.push("\u26A0\uFE0F Vulnerability indicators:");
9599
- for (const vl of vulnLines.slice(0, EXTRACT_LIMITS.NMAP_VULNS)) {
9600
- intel.push(` ${vl.trim()}`);
9601
- }
9602
- }
9603
- const osMatch = output.match(/OS details:\s*(.+)/i) || output.match(/Running:\s*(.+)/i);
9604
- if (osMatch) intel.push(`OS: ${osMatch[1].trim()}`);
9605
- return intel;
9606
- }
9607
- function extractLinpeasIntel(output) {
9608
- const intel = [];
9609
- const suidSection = output.match(/SUID[\s\S]*?(?=\n[═╔╗━]+|\n\n\n)/i);
9610
- if (suidSection) {
9611
- const suidBins = suidSection[0].match(/\/\S+/g);
9612
- if (suidBins) {
9613
- const interestingSuid = suidBins.filter(
9614
- (b) => /python|perl|ruby|node|bash|vim|less|more|nano|find|nmap|awk|env|php|gcc|gdb|docker|strace|ltrace/i.test(b)
9615
- );
9616
- if (interestingSuid.length > 0) {
9617
- intel.push(`\u{1F534} Exploitable SUID binaries: ${interestingSuid.join(", ")}`);
9618
- }
9619
- }
9620
- }
9621
- const sudoMatch = output.match(/User \S+ may run[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
9622
- if (sudoMatch) {
9623
- intel.push(`\u{1F534} sudo -l: ${sudoMatch[0].trim().slice(0, EXTRACT_LIMITS.LINPEAS_SUDO)}`);
9624
- }
9625
- const writableMatch = output.match(/Interesting writable[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
9626
- if (writableMatch) {
9627
- intel.push(`\u{1F4DD} Writable: ${writableMatch[0].trim().slice(0, EXTRACT_LIMITS.LINPEAS_WRITABLE)}`);
9628
- }
9629
- const cronMatch = output.match(/Cron[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
9630
- if (cronMatch) {
9631
- const cronLines = cronMatch[0].split("\n").filter((l) => l.includes("*") || /\/(root|cron)/i.test(l));
9632
- if (cronLines.length > 0) {
9633
- intel.push("\u23F0 Cron entries:");
9634
- cronLines.slice(0, EXTRACT_LIMITS.LINPEAS_CRON).forEach((c) => intel.push(` ${c.trim()}`));
9635
- }
9636
- }
9637
- const kernelMatch = output.match(/Linux version\s+(\S+)/i) || output.match(/Kernel:\s*(\S+)/i);
9638
- if (kernelMatch) intel.push(`\u{1F427} Kernel: ${kernelMatch[1]}`);
9639
- const passLines = output.split("\n").filter(
9640
- (l) => /password\s*[=:]\s*\S+/i.test(l) && !/\*\*\*|example|sample/i.test(l)
9641
- );
9642
- if (passLines.length > 0) {
9643
- intel.push("\u{1F511} Potential credentials found:");
9644
- passLines.slice(0, EXTRACT_LIMITS.LINPEAS_PASSWORDS).forEach((p) => intel.push(` ${p.trim()}`));
9645
- }
9646
- const cveMatches = output.match(/CVE-\d{4}-\d+/gi);
9647
- if (cveMatches) {
9648
- const uniqueCves = [...new Set(cveMatches)];
9649
- intel.push(`\u26A0\uFE0F CVEs mentioned: ${uniqueCves.join(", ")}`);
9650
- }
9651
- return intel;
9652
- }
9653
- function extractEnum4linuxIntel(output) {
9654
- const intel = [];
9655
- const userMatches = output.match(/user:\[(\S+?)\]/gi);
9656
- if (userMatches) {
9657
- const users = userMatches.map((u) => u.replace(/user:\[|\]/gi, ""));
9658
- intel.push(`\u{1F464} Users found: ${users.join(", ")}`);
9659
- }
9660
- const shareMatches = output.match(/Mapping: (\S+),\s*Listing: (\S+)/gi) || output.match(/\\\\\S+\\\\\S+/g);
9661
- if (shareMatches) {
9662
- intel.push(`\u{1F4C2} Shares: ${shareMatches.slice(0, EXTRACT_LIMITS.ENUM4LINUX_SHARES).join(", ")}`);
9663
- }
9664
- const domainMatch = output.match(/Domain:\s*\[(\S+?)\]/i) || output.match(/Workgroup:\s*\[(\S+?)\]/i);
9665
- if (domainMatch) intel.push(`\u{1F3E2} Domain: ${domainMatch[1]}`);
9666
- const policyMatch = output.match(/Password Complexity|Account Lockout/i);
9667
- if (policyMatch) intel.push("\u{1F510} Password policy information found");
9668
- return intel;
9669
- }
9670
- function extractDirBustIntel(output) {
9671
- const intel = [];
9672
- const lines = output.split("\n");
9673
- const interestingPaths = [];
9674
- for (const line of lines) {
9675
- const match = line.match(/(?:Status:\s*(\d{3})|(\d{3})\s+\d+\s+\d+).*?(\/\S+)/);
9676
- if (match) {
9677
- const status = match[1] || match[2];
9678
- const path2 = match[3];
9679
- if (["200", "301", "302", "403"].includes(status)) {
9680
- interestingPaths.push(`[${status}] ${path2}`);
9681
- }
9682
- }
9683
- const ffufMatch = line.match(/(\S+)\s+\[Status:\s*(\d+),?\s*Size:\s*(\d+)/);
9684
- if (ffufMatch && ["200", "301", "302", "403"].includes(ffufMatch[2])) {
9685
- interestingPaths.push(`[${ffufMatch[2]}] ${ffufMatch[1]} (${ffufMatch[3]}b)`);
9686
- }
9687
- }
9688
- if (interestingPaths.length > 0) {
9689
- intel.push(`\u{1F4C1} Discovered paths (${interestingPaths.length}):`);
9690
- interestingPaths.slice(0, EXTRACT_LIMITS.DIRBUST_PATHS).forEach((p) => intel.push(` ${p}`));
9691
- if (interestingPaths.length > EXTRACT_LIMITS.DIRBUST_PATHS) {
9692
- intel.push(` ... and ${interestingPaths.length - EXTRACT_LIMITS.DIRBUST_PATHS} more`);
9693
- }
9694
- }
9695
- return intel;
9696
- }
9697
- function extractSqlmapIntel(output) {
9698
- const intel = [];
9699
- const injectionTypes = output.match(/Type:\s*(\S.*?)(?:\n|$)/gi);
9700
- if (injectionTypes) {
9701
- intel.push("\u{1F489} SQL injection found:");
9702
- injectionTypes.slice(0, EXTRACT_LIMITS.SQLMAP_INJECTIONS).forEach((t) => intel.push(` ${t.trim()}`));
9703
- }
9704
- const dbMatch = output.match(/back-end DBMS:\s*(.+)/i);
9705
- if (dbMatch) intel.push(`\u{1F5C4}\uFE0F DBMS: ${dbMatch[1].trim()}`);
9706
- const dbListMatch = output.match(/available databases.*?:\s*([\s\S]*?)(?=\n\[|\n$)/i);
9707
- if (dbListMatch) {
9708
- intel.push(`\u{1F4CA} Databases: ${dbListMatch[1].trim().replace(/\n/g, ", ")}`);
9709
- }
9710
- const tableMatches = output.match(/Database:\s*\S+\s+Table:\s*\S+/gi);
9711
- if (tableMatches) {
9712
- intel.push(`\u{1F4CB} Tables dumped: ${tableMatches.length}`);
9713
- }
9714
- return intel;
9715
- }
9716
- function extractHashIntel(output) {
9717
- const intel = [];
9718
- const lines = output.split("\n");
9719
- const md5Hashes = lines.filter((l) => /\b[a-f0-9]{32}\b/i.test(l) && !l.includes("{"));
9720
- const sha256Hashes = lines.filter((l) => /\b[a-f0-9]{64}\b/i.test(l));
9721
- const unixHashes = lines.filter((l) => /\$[0-9]\$|\$2[aby]\$|\$6\$|\$5\$/i.test(l));
9722
- const ntlmHashes = lines.filter((l) => /:[0-9]+:[a-f0-9]{32}:[a-f0-9]{32}:::/i.test(l));
9723
- if (md5Hashes.length > 0) intel.push(`#\uFE0F\u20E3 MD5 hashes: ${md5Hashes.length}`);
9724
- if (sha256Hashes.length > 0) intel.push(`#\uFE0F\u20E3 SHA256 hashes: ${sha256Hashes.length}`);
9725
- if (unixHashes.length > 0) intel.push(`#\uFE0F\u20E3 Unix crypt hashes: ${unixHashes.length}`);
9726
- if (ntlmHashes.length > 0) {
9727
- intel.push(`#\uFE0F\u20E3 NTLM hashes: ${ntlmHashes.length}`);
9728
- ntlmHashes.slice(0, EXTRACT_LIMITS.HASH_NTLM).forEach((h) => intel.push(` ${h.trim().slice(0, EXTRACT_LIMITS.HASH_PREVIEW_LEN)}`));
9729
- }
9730
- return intel;
9731
- }
9732
- function extractGenericIntel(output) {
9733
- const intel = [];
9734
- const credPatterns = output.match(/(?:password|passwd|pwd|credentials?)\s*[=:]\s*\S+/gi);
9735
- if (credPatterns) {
9736
- intel.push("\u{1F511} Potential credentials detected:");
9737
- credPatterns.slice(0, EXTRACT_LIMITS.GENERIC_CREDS).forEach((c) => intel.push(` ${c.trim()}`));
9738
- }
9739
- const cves = output.match(/CVE-\d{4}-\d+/gi);
9740
- if (cves) {
9741
- const unique = [...new Set(cves)];
9742
- intel.push(`\u26A0\uFE0F CVEs mentioned: ${unique.join(", ")}`);
9743
- }
9744
- const ips = output.match(/\b(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\b/g);
9745
- if (ips) {
9746
- const uniqueIps = [...new Set(ips)].filter(
9747
- (ip) => !ip.startsWith("0.") && !ip.startsWith("255.") && ip !== "127.0.0.1"
9748
- );
9749
- if (uniqueIps.length > 0 && uniqueIps.length <= 20) {
9750
- intel.push(`\u{1F310} IP addresses found: ${uniqueIps.join(", ")}`);
9751
- }
9752
- }
9753
- const paths = output.match(/\/(?:etc\/(?:shadow|passwd|sudoers)|root\/|home\/\S+|var\/www\/\S+|opt\/\S+)\S*/g);
9754
- if (paths) {
9755
- const uniquePaths = [...new Set(paths)].slice(0, EXTRACT_LIMITS.GENERIC_PATHS);
9756
- intel.push(`\u{1F4C2} Interesting paths: ${uniquePaths.join(", ")}`);
9757
- }
9758
- const flagPatterns = output.match(/(?:flag|secret|key|token)\{[^}]+\}/gi);
9759
- if (flagPatterns) {
9760
- intel.push("\u{1F3F4} FLAG/SECRET patterns detected:");
9761
- flagPatterns.forEach((f) => intel.push(` ${f}`));
9762
- }
9763
- return intel;
9764
- }
9765
-
9766
9884
  // src/shared/utils/context-digest.ts
9767
9885
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "fs";
9768
- var PASSTHROUGH_THRESHOLD = 3e3;
9769
- var LAYER2_THRESHOLD = 8e3;
9770
- var LAYER3_THRESHOLD = 5e4;
9771
- var MAX_REDUCED_LINES = 500;
9886
+ var PASSTHROUGH_THRESHOLD = 500;
9887
+ var PREPROCESS_THRESHOLD = 3e3;
9888
+ var MAX_PREPROCESSED_LINES = 800;
9772
9889
  var getOutputDir = () => WORKSPACE.OUTPUTS;
9773
9890
  var MAX_DUPLICATE_DISPLAY = 3;
9774
- var LAYER3_MAX_INPUT_CHARS = 8e4;
9891
+ var ANALYST_MAX_INPUT_CHARS = 8e4;
9775
9892
  var FALLBACK_MAX_CHARS = 3e4;
9776
- var SIGNAL_PATTERNS = [
9777
- /error|fail|denied|refused|timeout|exception/i,
9778
- /warning|warn|deprecated|insecure/i,
9779
- /success|found|detected|discovered|vulnerable|VULNERABLE/i,
9780
- /password|passwd|credential|secret|key|token|hash/i,
9781
- /CVE-\d{4}-\d+/i,
9782
- /\d+\/\w+\s+open\s+/,
9783
- // nmap open port
9784
- /flag\{|ctf\{|HTB\{|THM\{/i,
9785
- // CTF flags
9786
- /root:|admin:|www-data:|nobody:/,
9787
- // /etc/passwd entries
9788
- /\$[0-9]\$|\$2[aby]\$/,
9789
- // password hashes
9790
- /\b(?:192\.168|10\.|172\.(?:1[6-9]|2\d|3[01]))\.\d+\.\d+\b/,
9791
- // internal IPs
9792
- /BEGIN\s+(?:RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY/i,
9793
- // private keys
9794
- /Authorization:|Bearer\s+|Basic\s+/i
9795
- // auth tokens
9796
- ];
9797
- var NOISE_PATTERNS = [
9798
- /^\s*$/,
9799
- // blank lines
9800
- /^\[[\d:]+\]\s*$/,
9801
- // timestamp-only lines
9802
- /^#+\s*$/,
9803
- // separator lines
9804
- /^\s*Progress:\s*\[?[#=\-\s>]+\]?\s*\d+/i,
9805
- // progress bars
9806
- /\d+\s+requests?\s+(?:sent|made)/i,
9807
- // request counters
9808
- /^\s*(?:\.{3,}|={5,}|-{5,})\s*$/
9809
- // decoration lines
9810
- ];
9811
- async function digestToolOutput(output, toolName, toolInput, llmDigestFn) {
9893
+ async function digestToolOutput(output, toolName, toolInput, analystFn) {
9812
9894
  const originalLength = output.length;
9813
9895
  if (originalLength < PASSTHROUGH_THRESHOLD) {
9814
9896
  return {
9815
9897
  digestedOutput: output,
9816
9898
  fullOutputPath: null,
9817
- layersApplied: [],
9899
+ analystUsed: false,
9900
+ memo: null,
9818
9901
  originalLength,
9819
9902
  digestedLength: originalLength,
9820
9903
  compressionRatio: 1
9821
9904
  };
9822
9905
  }
9823
- const layersApplied = [];
9824
- let processed = output;
9825
- let savedOutputPath = null;
9826
- processed = compressToolOutput(processed, toolName);
9827
- layersApplied.push(1);
9828
- if (processed.length > LAYER2_THRESHOLD) {
9829
- processed = structuralReduce(processed);
9830
- layersApplied.push(2);
9831
- }
9832
- if (processed.length > LAYER3_THRESHOLD) {
9833
- savedOutputPath = saveFullOutput(output, toolName);
9834
- if (llmDigestFn) {
9835
- try {
9836
- const context = `Tool: ${toolName}${toolInput ? ` | Input: ${toolInput}` : ""}`;
9837
- const digest = await llmDigestFn(processed, context);
9838
- processed = formatLLMDigest(digest, savedOutputPath, originalLength);
9839
- layersApplied.push(3);
9840
- } catch (err) {
9841
- debugLog("general", "Context Digest Layer 3 failed, falling back", { toolName, error: String(err) });
9842
- processed = formatFallbackDigest(processed, savedOutputPath, originalLength);
9843
- layersApplied.push(3);
9844
- }
9845
- } else {
9846
- processed = formatFallbackDigest(processed, savedOutputPath, originalLength);
9906
+ const savedOutputPath = saveFullOutput(output, toolName);
9907
+ let preprocessed = output;
9908
+ if (originalLength > PREPROCESS_THRESHOLD) {
9909
+ preprocessed = structuralPreprocess(output);
9910
+ }
9911
+ if (analystFn) {
9912
+ try {
9913
+ const context = `Tool: ${toolName}${toolInput ? ` | Input: ${toolInput}` : ""}`;
9914
+ const rawAnalystResponse = await analystFn(preprocessed, context);
9915
+ const memo6 = parseAnalystMemo(rawAnalystResponse);
9916
+ const formatted = formatAnalystDigest(rawAnalystResponse, savedOutputPath, originalLength);
9917
+ return {
9918
+ digestedOutput: formatted,
9919
+ fullOutputPath: savedOutputPath,
9920
+ analystUsed: true,
9921
+ memo: memo6,
9922
+ originalLength,
9923
+ digestedLength: formatted.length,
9924
+ compressionRatio: formatted.length / originalLength
9925
+ };
9926
+ } catch (err) {
9927
+ debugLog("general", "Analyst LLM failed, falling back to truncation", {
9928
+ toolName,
9929
+ error: String(err)
9930
+ });
9847
9931
  }
9848
- } else if (layersApplied.includes(2)) {
9849
- savedOutputPath = saveFullOutput(output, toolName);
9850
9932
  }
9933
+ const fallback = formatFallbackDigest(preprocessed, savedOutputPath, originalLength);
9851
9934
  return {
9852
- digestedOutput: processed,
9935
+ digestedOutput: fallback,
9853
9936
  fullOutputPath: savedOutputPath,
9854
- layersApplied,
9937
+ analystUsed: false,
9938
+ memo: null,
9855
9939
  originalLength,
9856
- digestedLength: processed.length,
9857
- compressionRatio: processed.length / originalLength
9940
+ digestedLength: fallback.length,
9941
+ compressionRatio: fallback.length / originalLength
9858
9942
  };
9859
9943
  }
9860
- function structuralReduce(output) {
9944
+ var NOISE_PATTERNS = [
9945
+ /^\s*$/,
9946
+ // blank lines
9947
+ /^\[[\d:]+\]\s*$/,
9948
+ // timestamp-only lines
9949
+ /^#+\s*$/,
9950
+ // separator lines
9951
+ /^\s*Progress:\s*\[?[#=\-\s>]+\]?\s*\d/i,
9952
+ // progress bars
9953
+ /\d+\s+requests?\s+(?:sent|made)/i,
9954
+ // request counters
9955
+ /^\s*(?:\.{3,}|={5,}|-{5,})\s*$/
9956
+ // decoration lines
9957
+ ];
9958
+ function structuralPreprocess(output) {
9861
9959
  let cleaned = stripAnsi(output);
9862
9960
  const lines = cleaned.split("\n");
9863
9961
  const result2 = [];
9864
- const duplicateCounts = /* @__PURE__ */ new Map();
9865
9962
  let lastLine = "";
9866
9963
  let consecutiveDupes = 0;
9867
9964
  for (const line of lines) {
@@ -9869,11 +9966,9 @@ function structuralReduce(output) {
9869
9966
  if (NOISE_PATTERNS.some((p) => p.test(trimmed))) {
9870
9967
  continue;
9871
9968
  }
9872
- const isSignal = SIGNAL_PATTERNS.some((p) => p.test(trimmed));
9873
9969
  const normalized = normalizeLine(trimmed);
9874
- if (normalized === normalizeLine(lastLine) && !isSignal) {
9970
+ if (normalized === normalizeLine(lastLine)) {
9875
9971
  consecutiveDupes++;
9876
- duplicateCounts.set(normalized, (duplicateCounts.get(normalized) || 1) + 1);
9877
9972
  continue;
9878
9973
  }
9879
9974
  if (consecutiveDupes > 0) {
@@ -9896,57 +9991,124 @@ function structuralReduce(output) {
9896
9991
  result2.push(lastLine);
9897
9992
  }
9898
9993
  }
9899
- if (result2.length > MAX_REDUCED_LINES) {
9900
- const headSize = Math.floor(MAX_REDUCED_LINES * 0.4);
9901
- const tailSize = Math.floor(MAX_REDUCED_LINES * 0.3);
9902
- const signalBudget = MAX_REDUCED_LINES - headSize - tailSize;
9994
+ if (result2.length > MAX_PREPROCESSED_LINES) {
9995
+ const headSize = Math.floor(MAX_PREPROCESSED_LINES * 0.5);
9996
+ const tailSize = Math.floor(MAX_PREPROCESSED_LINES * 0.3);
9903
9997
  const head = result2.slice(0, headSize);
9904
9998
  const tail = result2.slice(-tailSize);
9905
- const middle = result2.slice(headSize, -tailSize);
9906
- const middleSignals = middle.filter((line) => SIGNAL_PATTERNS.some((p) => p.test(line))).slice(0, signalBudget);
9907
- const skipped = middle.length - middleSignals.length;
9999
+ const skipped = result2.length - headSize - tailSize;
9908
10000
  cleaned = [
9909
10001
  ...head,
9910
10002
  "",
9911
- `... [${skipped} routine lines skipped \u2014 ${middleSignals.length} important lines preserved] ...`,
9912
- "",
9913
- ...middleSignals,
9914
- "",
9915
- `... [resuming last ${tailSize} lines] ...`,
10003
+ `... [${skipped} lines skipped for Analyst LLM context \u2014 full output saved to file] ...`,
9916
10004
  "",
9917
10005
  ...tail
9918
10006
  ].join("\n");
9919
10007
  } else {
9920
10008
  cleaned = result2.join("\n");
9921
10009
  }
10010
+ if (cleaned.length > ANALYST_MAX_INPUT_CHARS) {
10011
+ cleaned = cleaned.slice(0, ANALYST_MAX_INPUT_CHARS) + `
10012
+ ... [truncated at ${ANALYST_MAX_INPUT_CHARS} chars for Analyst LLM \u2014 full output saved to file]`;
10013
+ }
9922
10014
  return cleaned;
9923
10015
  }
9924
- var DIGEST_SYSTEM_PROMPT = `You are a pentesting output analyst. Given raw tool output, extract ONLY actionable intelligence. Be terse and structured.
10016
+ var ANALYST_SYSTEM_PROMPT = `You are an independent pentesting output analyst. You receive raw tool output and must extract ONLY actionable intelligence for the main attack agent.
9925
10017
 
9926
10018
  FORMAT YOUR RESPONSE EXACTLY LIKE THIS:
10019
+
9927
10020
  ## Key Findings
9928
- - [finding 1]
10021
+ - [finding 1 with exact values: ports, versions, paths]
9929
10022
  - [finding 2]
9930
10023
 
9931
10024
  ## Credentials/Secrets
9932
- - [any discovered credentials, hashes, tokens, keys]
10025
+ - [any discovered credentials, hashes, tokens, keys, certificates]
10026
+ - (write "None found" if none)
9933
10027
 
9934
10028
  ## Attack Vectors
9935
- - [exploitable services, vulnerabilities, misconfigurations]
10029
+ - [exploitable services, vulnerabilities, misconfigurations, CVEs]
10030
+ - (write "None identified" if none)
10031
+
10032
+ ## Failures/Errors
10033
+ - [what was attempted and FAILED \u2014 include the FULL command, wordlist, target, and the reason WHY it failed]
10034
+ - [e.g.: "SSH brute force: hydra -l admin -P /usr/share/wordlists/rockyou.txt ssh://10.0.0.1 \u2014 connection refused (port filtered)"]
10035
+ - [e.g.: "SQLi on /login with sqlmap --tamper=space2comment \u2014 input sanitized, WAF detected (ModSecurity)"]
10036
+ - (write "No failures" if everything succeeded)
10037
+
10038
+ ## Suspicious Signals
10039
+ - [anomalies that are NOT confirmed vulnerabilities but suggest exploitable surface]
10040
+ - [e.g.: "Response time 3x slower on /admin path \u2014 possible auth check or backend processing"]
10041
+ - [e.g.: "X-Debug-Token header present \u2014 debug mode may be enabled"]
10042
+ - [e.g.: "Verbose error message reveals stack trace / internal path / DB schema"]
10043
+ - [e.g.: "Unexpected 302 redirect with session param leaked in URL"]
10044
+ - (write "No suspicious signals" if nothing anomalous)
10045
+
10046
+ ## Attack Value
10047
+ - [ONE word: HIGH / MED / LOW / NONE]
10048
+ - Reasoning: [1 sentence why \u2014 what makes this worth pursuing or abandoning]
9936
10049
 
9937
10050
  ## Next Steps
9938
10051
  - [recommended immediate actions based on findings]
9939
10052
 
9940
10053
  RULES:
9941
- - Be EXTREMELY concise \u2014 max 30 lines total
9942
- - Only include ACTIONABLE findings \u2014 skip routine/expected results
10054
+ - Include EXACT values: port numbers, versions, usernames, file paths, IPs, full commands used
10055
+ - For failures: include the COMPLETE command with all flags, wordlists, and targets \u2014 "brute force failed" alone is USELESS
10056
+ - Look for the UNEXPECTED \u2014 non-standard ports, unusual banners, timing anomalies, error leaks
10057
+ - Credentials include: passwords, hashes, API keys, tokens, private keys, cookies, session IDs
10058
+ - Flag any information disclosure: server versions, internal paths, stack traces, debug output
9943
10059
  - If nothing interesting found, say "No actionable findings in this output"
9944
- - Include exact values: port numbers, versions, usernames, file paths
9945
- - Never include decorative output, banners, or progress information`;
9946
- function formatLLMDigest(digest, filePath, originalChars) {
10060
+ - Never include decorative output, banners, or progress information
10061
+ - Do NOT miss subtle signals: unusual HTTP headers, non-standard responses, timing differences
10062
+ - Write as much detail as needed \u2014 do NOT artificially shorten. Every detail matters for strategy.
10063
+
10064
+ ## Reflection
10065
+ - What this output tells us: [1-line assessment]
10066
+ - Recommended next action: [1-2 specific follow-up actions]`;
10067
+ function parseAnalystMemo(response) {
10068
+ const sections = {};
10069
+ let currentSection = "";
10070
+ let reflectionLines = [];
10071
+ let attackValueLine = "NONE";
10072
+ let attackValueReasoning = "";
10073
+ for (const line of response.split("\n")) {
10074
+ if (line.startsWith("## ")) {
10075
+ currentSection = line.replace("## ", "").trim().toLowerCase();
10076
+ sections[currentSection] = [];
10077
+ } else if (currentSection === "reflection") {
10078
+ if (line.trim()) reflectionLines.push(line.trim());
10079
+ } else if (currentSection === "attack value") {
10080
+ const match = line.match(/\b(HIGH|MED|LOW|NONE)\b/);
10081
+ if (match) attackValueLine = match[1];
10082
+ const reasonMatch = line.match(/[Rr]easoning:\s*(.+)/);
10083
+ if (reasonMatch) attackValueReasoning = reasonMatch[1].trim();
10084
+ } else if (currentSection) {
10085
+ const trimmed = line.trim();
10086
+ if (!trimmed) continue;
10087
+ const content = trimmed.replace(/^(?:-|\*|\d+[.)]\s*)\s*/, "").trim();
10088
+ if (content) sections[currentSection].push(content);
10089
+ }
10090
+ }
10091
+ const filterNone = (items) => items.filter((i) => !/(^none|^no )/i.test(i.trim()));
10092
+ const rawValue = attackValueLine.toUpperCase();
10093
+ const attackValue = ["HIGH", "MED", "LOW", "NONE"].includes(rawValue) ? rawValue : "LOW";
10094
+ return {
10095
+ keyFindings: filterNone(sections["key findings"] || []),
10096
+ credentials: filterNone(sections["credentials/secrets"] || []),
10097
+ attackVectors: filterNone(sections["attack vectors"] || []),
10098
+ failures: filterNone(sections["failures/errors"] || []),
10099
+ suspicions: filterNone(sections["suspicious signals"] || []),
10100
+ attackValue,
10101
+ nextSteps: filterNone(sections["next steps"] || []),
10102
+ reflection: [
10103
+ attackValueReasoning ? `[${attackValue}] ${attackValueReasoning}` : "",
10104
+ ...reflectionLines
10105
+ ].filter(Boolean).join(" | ")
10106
+ };
10107
+ }
10108
+ function formatAnalystDigest(digest, filePath, originalChars) {
9947
10109
  return [
9948
10110
  "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
9949
- "\u2551 CONTEXT DIGEST (LLM-summarized) \u2551",
10111
+ "\u2551 ANALYST DIGEST (Independent LLM analysis) \u2551",
9950
10112
  "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
9951
10113
  "",
9952
10114
  digest,
@@ -9956,29 +10118,26 @@ function formatLLMDigest(digest, filePath, originalChars) {
9956
10118
  ].join("\n");
9957
10119
  }
9958
10120
  function formatFallbackDigest(processed, filePath, originalChars) {
9959
- const lines = processed.split("\n");
9960
- const summaryEndIdx = lines.findIndex((l) => l.includes("END SUMMARY"));
9961
- const summaryBlock = summaryEndIdx > 0 ? lines.slice(0, summaryEndIdx + 1).join("\n") : "";
9962
- const remaining = summaryEndIdx > 0 ? lines.slice(summaryEndIdx + 1).join("\n") : processed;
9963
10121
  const maxChars = FALLBACK_MAX_CHARS;
9964
- let truncatedRemaining = remaining;
9965
- if (remaining.length > maxChars) {
10122
+ let truncated = processed;
10123
+ if (processed.length > maxChars) {
9966
10124
  const headChars = Math.floor(maxChars * 0.6);
9967
10125
  const tailChars = Math.floor(maxChars * 0.4);
9968
- const skipped = remaining.length - headChars - tailChars;
9969
- truncatedRemaining = remaining.slice(0, headChars) + `
10126
+ const skipped = processed.length - headChars - tailChars;
10127
+ truncated = processed.slice(0, headChars) + `
9970
10128
 
9971
- ... [${skipped} chars omitted \u2014 read full output from file] ...
10129
+ ... [${skipped} chars omitted \u2014 Analyst LLM unavailable, read full output from file] ...
9972
10130
 
9973
- ` + remaining.slice(-tailChars);
10131
+ ` + processed.slice(-tailChars);
9974
10132
  }
9975
10133
  return [
9976
- summaryBlock,
9977
- truncatedRemaining,
10134
+ "\u26A0\uFE0F ANALYST UNAVAILABLE \u2014 showing truncated raw output:",
10135
+ "",
10136
+ truncated,
9978
10137
  "",
9979
10138
  `\u{1F4C2} Full output saved: ${filePath} (${originalChars} chars)`,
9980
10139
  `\u{1F4A1} Use read_file("${filePath}") to see the complete raw output.`
9981
- ].filter(Boolean).join("\n");
10140
+ ].join("\n");
9982
10141
  }
9983
10142
  function stripAnsi(text) {
9984
10143
  return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1B\[[\d;]*m/g, "");
@@ -10004,20 +10163,21 @@ function saveFullOutput(output, toolName) {
10004
10163
  }
10005
10164
  function createLLMDigestFn(llmClient) {
10006
10165
  return async (text, context) => {
10007
- const truncatedText = text.length > LAYER3_MAX_INPUT_CHARS ? text.slice(0, LAYER3_MAX_INPUT_CHARS) + `
10008
- ... [truncated for summarization, ${text.length - LAYER3_MAX_INPUT_CHARS} chars omitted]` : text;
10009
- const messages = [{ role: "user", content: `Analyze this pentesting tool output and extract actionable intelligence.
10166
+ const messages = [{
10167
+ role: LLM_ROLES.USER,
10168
+ content: `Analyze this pentesting tool output and extract actionable intelligence.
10010
10169
 
10011
10170
  Context: ${context}
10012
10171
 
10013
10172
  --- OUTPUT START ---
10014
- ${truncatedText}
10015
- --- OUTPUT END ---` }];
10173
+ ${text}
10174
+ --- OUTPUT END ---`
10175
+ }];
10016
10176
  const response = await llmClient.generateResponse(
10017
10177
  messages,
10018
10178
  void 0,
10019
- // no tools — summarization only
10020
- DIGEST_SYSTEM_PROMPT
10179
+ // no tools — analysis only
10180
+ ANALYST_SYSTEM_PROMPT
10021
10181
  );
10022
10182
  return response.content || "No actionable findings extracted.";
10023
10183
  };
@@ -10032,6 +10192,16 @@ var CoreAgent = class _CoreAgent {
10032
10192
  agentType;
10033
10193
  maxIterations;
10034
10194
  abortController = null;
10195
+ /**
10196
+ * Collected tool execution records for the current turn.
10197
+ * MainAgent reads this after each step to write journal entries.
10198
+ * Cleared at the start of each step.
10199
+ */
10200
+ turnToolJournal = [];
10201
+ /** Aggregated memo from all tools in the current turn */
10202
+ turnMemo = { keyFindings: [], credentials: [], attackVectors: [], failures: [], suspicions: [], attackValue: "LOW", nextSteps: [] };
10203
+ /** Analyst reflections collected during this turn (1-line assessments) */
10204
+ turnReflections = [];
10035
10205
  constructor(agentType, state, events, toolRegistry, maxIterations) {
10036
10206
  this.agentType = agentType;
10037
10207
  this.state = state;
@@ -10159,7 +10329,22 @@ RULES:
10159
10329
  });
10160
10330
  continue;
10161
10331
  }
10162
- throw error;
10332
+ const unexpectedMsg = error instanceof Error ? error.message : String(error);
10333
+ this.events.emit({
10334
+ type: EVENT_TYPES.ERROR,
10335
+ timestamp: Date.now(),
10336
+ data: {
10337
+ message: `Unexpected error: ${unexpectedMsg}`,
10338
+ phase: this.state.getPhase(),
10339
+ isRecoverable: true
10340
+ }
10341
+ });
10342
+ messages.push({
10343
+ role: LLM_ROLES.USER,
10344
+ content: `\u26A0\uFE0F UNEXPECTED ERROR: ${unexpectedMsg}
10345
+ This may be a transient issue. Continue your task \u2014 retry the last action or try an alternative approach.`
10346
+ });
10347
+ continue;
10163
10348
  }
10164
10349
  }
10165
10350
  const summary = `Max iterations (${this.maxIterations}) reached. Progress: ${progress.totalToolsExecuted} tools executed (${progress.toolSuccesses} succeeded, ${progress.toolErrors} failed). Current phase: ${this.state.getPhase()}. Findings: ${this.state.getFindings?.()?.length ?? "unknown"}.`;
@@ -10477,27 +10662,62 @@ ${firstLine}`, phase }
10477
10662
  progress.blockedCommandPatterns.clear();
10478
10663
  }
10479
10664
  }
10665
+ const rawOutputForTUI = outputText;
10666
+ let digestedOutputForLLM = outputText;
10667
+ let digestResult = null;
10480
10668
  try {
10481
10669
  const llmDigestFn = createLLMDigestFn(this.llm);
10482
- const digestResult = await digestToolOutput(
10670
+ digestResult = await digestToolOutput(
10483
10671
  outputText,
10484
10672
  call.name,
10485
10673
  JSON.stringify(call.input).slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
10486
10674
  llmDigestFn
10487
10675
  );
10488
- outputText = digestResult.digestedOutput;
10676
+ digestedOutputForLLM = digestResult.digestedOutput;
10489
10677
  } catch {
10490
- if (outputText.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
10491
- const truncated = outputText.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
10492
- const remaining = outputText.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
10493
- outputText = `${truncated}
10678
+ if (digestedOutputForLLM.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
10679
+ const truncated = digestedOutputForLLM.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
10680
+ const remaining = digestedOutputForLLM.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
10681
+ digestedOutputForLLM = `${truncated}
10494
10682
 
10495
10683
  ... [TRUNCATED ${remaining} characters for context hygiene] ...
10496
10684
  \u{1F4A1} TIP: If you need to see the full output, use a tool to read the file directly or run the command with | head, | tail, or | grep.`;
10497
10685
  }
10498
10686
  }
10499
- this.emitToolResult(call.name, result2.success, outputText, result2.error, Date.now() - toolStartTime);
10500
- return { toolCallId: call.id, output: outputText, error: result2.error };
10687
+ this.emitToolResult(call.name, result2.success, rawOutputForTUI, result2.error, Date.now() - toolStartTime);
10688
+ const inputSummary = JSON.stringify(call.input);
10689
+ this.turnToolJournal.push({
10690
+ name: call.name,
10691
+ inputSummary,
10692
+ success: result2.success,
10693
+ analystSummary: digestResult?.memo ? digestResult.memo.keyFindings.join("; ") || "No key findings" : digestedOutputForLLM,
10694
+ outputFile: digestResult?.fullOutputPath ?? null
10695
+ });
10696
+ if (digestResult?.memo) {
10697
+ const m = digestResult.memo;
10698
+ this.turnMemo.keyFindings.push(...m.keyFindings);
10699
+ this.turnMemo.credentials.push(...m.credentials);
10700
+ this.turnMemo.attackVectors.push(...m.attackVectors);
10701
+ this.turnMemo.failures.push(...m.failures);
10702
+ this.turnMemo.suspicions.push(...m.suspicions);
10703
+ const VALUE_RANK = { HIGH: 3, MED: 2, LOW: 1, NONE: 0 };
10704
+ if ((VALUE_RANK[m.attackValue] ?? 0) > (VALUE_RANK[this.turnMemo.attackValue] ?? 0)) {
10705
+ this.turnMemo.attackValue = m.attackValue;
10706
+ }
10707
+ this.turnMemo.nextSteps.push(...m.nextSteps);
10708
+ if (m.reflection) this.turnReflections.push(m.reflection);
10709
+ }
10710
+ if (digestResult?.memo?.credentials.length) {
10711
+ for (const cred of digestResult.memo.credentials) {
10712
+ this.state.addLoot({
10713
+ type: "credential",
10714
+ host: "auto-extracted",
10715
+ detail: cred,
10716
+ obtainedAt: Date.now()
10717
+ });
10718
+ }
10719
+ }
10720
+ return { toolCallId: call.id, output: digestedOutputForLLM, error: result2.error };
10501
10721
  } catch (error) {
10502
10722
  const errorMsg = String(error);
10503
10723
  const enrichedError = this.enrichToolError({ toolName: call.name, input: call.input, error: errorMsg, originalOutput: "", progress });
@@ -10572,8 +10792,8 @@ ${firstLine}`, phase }
10572
10792
  };
10573
10793
 
10574
10794
  // src/agents/prompt-builder.ts
10575
- import { readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
10576
- import { join as join10, dirname as dirname5 } from "path";
10795
+ import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
10796
+ import { join as join11, dirname as dirname5 } from "path";
10577
10797
  import { fileURLToPath as fileURLToPath4 } from "url";
10578
10798
 
10579
10799
  // src/shared/constants/prompts.ts
@@ -10634,73 +10854,6 @@ var INITIAL_TASKS = {
10634
10854
  RECON: "Initial reconnaissance and target discovery"
10635
10855
  };
10636
10856
 
10637
- // src/shared/constants/service-ports.ts
10638
- var SERVICE_PORTS = {
10639
- SSH: 22,
10640
- FTP: 21,
10641
- TELNET: 23,
10642
- SMTP: 25,
10643
- DNS: 53,
10644
- HTTP: 80,
10645
- POP3: 110,
10646
- IMAP: 143,
10647
- SMB_NETBIOS: 139,
10648
- SMB: 445,
10649
- HTTPS: 443,
10650
- MSSQL: 1433,
10651
- MYSQL: 3306,
10652
- RDP: 3389,
10653
- POSTGRESQL: 5432,
10654
- REDIS: 6379,
10655
- HTTP_ALT: 8080,
10656
- HTTPS_ALT: 8443,
10657
- MONGODB: 27017,
10658
- ELASTICSEARCH: 9200,
10659
- MEMCACHED: 11211,
10660
- NODE_DEFAULT: 3e3,
10661
- FLASK_DEFAULT: 5e3,
10662
- DJANGO_DEFAULT: 8e3
10663
- };
10664
- var CRITICAL_SERVICE_PORTS = [
10665
- SERVICE_PORTS.SSH,
10666
- SERVICE_PORTS.RDP,
10667
- SERVICE_PORTS.MYSQL,
10668
- SERVICE_PORTS.POSTGRESQL,
10669
- SERVICE_PORTS.REDIS,
10670
- SERVICE_PORTS.MONGODB
10671
- ];
10672
- var NO_AUTH_CRITICAL_PORTS = [
10673
- SERVICE_PORTS.REDIS,
10674
- SERVICE_PORTS.MONGODB,
10675
- SERVICE_PORTS.ELASTICSEARCH,
10676
- SERVICE_PORTS.MEMCACHED
10677
- ];
10678
- var WEB_SERVICE_PORTS = [
10679
- SERVICE_PORTS.HTTP,
10680
- SERVICE_PORTS.HTTPS,
10681
- SERVICE_PORTS.HTTP_ALT,
10682
- SERVICE_PORTS.HTTPS_ALT,
10683
- SERVICE_PORTS.NODE_DEFAULT,
10684
- SERVICE_PORTS.FLASK_DEFAULT,
10685
- SERVICE_PORTS.DJANGO_DEFAULT
10686
- ];
10687
- var PLAINTEXT_HTTP_PORTS = [
10688
- SERVICE_PORTS.HTTP,
10689
- SERVICE_PORTS.HTTP_ALT,
10690
- SERVICE_PORTS.NODE_DEFAULT
10691
- ];
10692
- var DATABASE_PORTS = [
10693
- SERVICE_PORTS.MYSQL,
10694
- SERVICE_PORTS.POSTGRESQL,
10695
- SERVICE_PORTS.MSSQL,
10696
- SERVICE_PORTS.MONGODB,
10697
- SERVICE_PORTS.REDIS
10698
- ];
10699
- var SMB_PORTS = [
10700
- SERVICE_PORTS.SMB,
10701
- SERVICE_PORTS.SMB_NETBIOS
10702
- ];
10703
-
10704
10857
  // src/shared/constants/scoring.ts
10705
10858
  var ATTACK_SCORING = {
10706
10859
  /** Base score for all attack prioritization */
@@ -10861,10 +11014,229 @@ function getAttacksForService(service, port) {
10861
11014
  return attacks;
10862
11015
  }
10863
11016
 
11017
+ // src/shared/utils/journal.ts
11018
+ import { writeFileSync as writeFileSync8, readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync5 } from "fs";
11019
+ import { join as join10 } from "path";
11020
+ var MAX_JOURNAL_ENTRIES = 50;
11021
+ var SUMMARY_REGEN_INTERVAL = 10;
11022
+ var MAX_OUTPUT_FILES = 30;
11023
+ var TURN_PREFIX = "turn-";
11024
+ var SUMMARY_FILE = "summary.md";
11025
+ function writeJournalEntry(entry) {
11026
+ try {
11027
+ const journalDir = WORKSPACE.JOURNAL;
11028
+ ensureDirExists(journalDir);
11029
+ const padded = String(entry.turn).padStart(4, "0");
11030
+ const filePath = join10(journalDir, `${TURN_PREFIX}${padded}.json`);
11031
+ writeFileSync8(filePath, JSON.stringify(entry, null, 2), "utf-8");
11032
+ return filePath;
11033
+ } catch (err) {
11034
+ debugLog("general", "Failed to write journal entry", { turn: entry.turn, error: String(err) });
11035
+ return null;
11036
+ }
11037
+ }
11038
+ function readJournalSummary() {
11039
+ try {
11040
+ const summaryPath = join10(WORKSPACE.JOURNAL, SUMMARY_FILE);
11041
+ if (!existsSync8(summaryPath)) return "";
11042
+ return readFileSync5(summaryPath, "utf-8");
11043
+ } catch {
11044
+ return "";
11045
+ }
11046
+ }
11047
+ function getRecentEntries(count = MAX_JOURNAL_ENTRIES) {
11048
+ try {
11049
+ const journalDir = WORKSPACE.JOURNAL;
11050
+ if (!existsSync8(journalDir)) return [];
11051
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort().slice(-count);
11052
+ const entries = [];
11053
+ for (const file of files) {
11054
+ try {
11055
+ const raw = readFileSync5(join10(journalDir, file), "utf-8");
11056
+ entries.push(JSON.parse(raw));
11057
+ } catch {
11058
+ }
11059
+ }
11060
+ return entries;
11061
+ } catch {
11062
+ return [];
11063
+ }
11064
+ }
11065
+ function getNextTurnNumber() {
11066
+ try {
11067
+ const journalDir = WORKSPACE.JOURNAL;
11068
+ if (!existsSync8(journalDir)) return 1;
11069
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort();
11070
+ if (files.length === 0) return 1;
11071
+ const lastFile = files[files.length - 1];
11072
+ const match = lastFile.match(/turn-(\d+)\.json/);
11073
+ return match ? parseInt(match[1], 10) + 1 : 1;
11074
+ } catch {
11075
+ return 1;
11076
+ }
11077
+ }
11078
+ function shouldRegenerateSummary(currentTurn) {
11079
+ return currentTurn > 0 && currentTurn % SUMMARY_REGEN_INTERVAL === 0;
11080
+ }
11081
+ function regenerateJournalSummary() {
11082
+ try {
11083
+ const entries = getRecentEntries();
11084
+ if (entries.length === 0) return;
11085
+ const journalDir = WORKSPACE.JOURNAL;
11086
+ ensureDirExists(journalDir);
11087
+ const summary = buildSummaryFromEntries(entries);
11088
+ const summaryPath = join10(journalDir, SUMMARY_FILE);
11089
+ writeFileSync8(summaryPath, summary, "utf-8");
11090
+ debugLog("general", "Journal summary regenerated", {
11091
+ entries: entries.length,
11092
+ summaryLength: summary.length
11093
+ });
11094
+ } catch (err) {
11095
+ debugLog("general", "Failed to regenerate journal summary", { error: String(err) });
11096
+ }
11097
+ }
11098
+ function buildSummaryFromEntries(entries) {
11099
+ const attempts = [];
11100
+ const findings = [];
11101
+ const credentials = [];
11102
+ const successes = [];
11103
+ const failures = [];
11104
+ const suspicions = [];
11105
+ const nextSteps = [];
11106
+ const reflections = [];
11107
+ const VALUE_ORDER = { HIGH: 0, MED: 1, LOW: 2, NONE: 3 };
11108
+ const reversed = [...entries].reverse();
11109
+ for (const entry of reversed) {
11110
+ const value = entry.memo.attackValue || "LOW";
11111
+ for (const tool of entry.tools) {
11112
+ attempts.push({
11113
+ turn: entry.turn,
11114
+ phase: entry.phase,
11115
+ ok: tool.success,
11116
+ name: tool.name,
11117
+ input: tool.inputSummary,
11118
+ value
11119
+ });
11120
+ }
11121
+ for (const finding of entry.memo.keyFindings) {
11122
+ const line = `- [T${entry.turn}|\u26A1${value}] ${finding}`;
11123
+ if (!findings.includes(line)) findings.push(line);
11124
+ }
11125
+ for (const cred of entry.memo.credentials) {
11126
+ const line = `- [T${entry.turn}] ${cred}`;
11127
+ if (!credentials.includes(line)) credentials.push(line);
11128
+ }
11129
+ for (const vector of entry.memo.attackVectors) {
11130
+ const line = `- [T${entry.turn}] ${vector}`;
11131
+ if (!successes.includes(line)) successes.push(line);
11132
+ }
11133
+ for (const fail of entry.memo.failures) {
11134
+ const line = `- [T${entry.turn}] ${fail}`;
11135
+ if (!failures.includes(line)) failures.push(line);
11136
+ }
11137
+ for (const tool of entry.tools) {
11138
+ if (!tool.success) {
11139
+ const detail = `${tool.name}(${tool.inputSummary}): ${tool.analystSummary}`;
11140
+ const line = `- [T${entry.turn}] ${detail}`;
11141
+ if (!failures.includes(line)) failures.push(line);
11142
+ }
11143
+ }
11144
+ for (const s of entry.memo.suspicions || []) {
11145
+ const line = `- [T${entry.turn}] ${s}`;
11146
+ if (!suspicions.includes(line)) suspicions.push(line);
11147
+ }
11148
+ if (nextSteps.length < 5) {
11149
+ for (const step of entry.memo.nextSteps) {
11150
+ if (!nextSteps.includes(`- ${step}`)) nextSteps.push(`- ${step}`);
11151
+ }
11152
+ }
11153
+ if (entry.reflection) {
11154
+ reflections.push(`- [T${entry.turn}|\u26A1${value}] ${entry.reflection}`);
11155
+ }
11156
+ }
11157
+ attempts.sort((a, b) => {
11158
+ const vd = (VALUE_ORDER[a.value] ?? 3) - (VALUE_ORDER[b.value] ?? 3);
11159
+ return vd !== 0 ? vd : b.turn - a.turn;
11160
+ });
11161
+ const attemptLines = attempts.map(
11162
+ (a) => `- [T${a.turn}|${a.phase}|\u26A1${a.value}] ${a.ok ? "\u2705" : "\u274C"} ${a.name}: ${a.input}`
11163
+ );
11164
+ const lastTurn = entries[entries.length - 1]?.turn || 0;
11165
+ const sections = [
11166
+ `# Session Journal Summary`,
11167
+ `> Turn ${lastTurn} / ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19)}`,
11168
+ ""
11169
+ ];
11170
+ const addSection = (title, items) => {
11171
+ if (items.length === 0) return;
11172
+ sections.push(`## ${title}`);
11173
+ sections.push(...items);
11174
+ sections.push("");
11175
+ };
11176
+ if (attemptLines.length > 0) {
11177
+ sections.push("## Techniques Tried (by attack value)");
11178
+ sections.push("> \u26A1HIGH=keep drilling \u26A1MED=worth exploring \u26A1LOW=low priority \u26A1NONE=abandon");
11179
+ sections.push(...attemptLines);
11180
+ sections.push("");
11181
+ }
11182
+ addSection("\u{1F9E0} Analyst Analysis (attack value rationale)", reflections);
11183
+ addSection("\u{1F50D} Suspicious Signals (unconfirmed, needs investigation)", suspicions);
11184
+ addSection("\u{1F4CB} Key Findings", findings);
11185
+ addSection("\u{1F511} Credentials Obtained", credentials);
11186
+ addSection("\u2705 Successful Attack Vectors", successes);
11187
+ addSection("\u274C Failure Causes (do not repeat)", failures);
11188
+ addSection("\u27A1\uFE0F Next Recommendations", nextSteps);
11189
+ return sections.join("\n");
11190
+ }
11191
+ function rotateJournalEntries() {
11192
+ try {
11193
+ const journalDir = WORKSPACE.JOURNAL;
11194
+ if (!existsSync8(journalDir)) return;
11195
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort();
11196
+ if (files.length <= MAX_JOURNAL_ENTRIES) return;
11197
+ const toDelete = files.slice(0, files.length - MAX_JOURNAL_ENTRIES);
11198
+ for (const file of toDelete) {
11199
+ try {
11200
+ unlinkSync5(join10(journalDir, file));
11201
+ } catch {
11202
+ }
11203
+ }
11204
+ debugLog("general", "Journal entries rotated", {
11205
+ deleted: toDelete.length,
11206
+ remaining: MAX_JOURNAL_ENTRIES
11207
+ });
11208
+ } catch {
11209
+ }
11210
+ }
11211
+ function rotateOutputFiles() {
11212
+ try {
11213
+ const outputDir = WORKSPACE.OUTPUTS;
11214
+ if (!existsSync8(outputDir)) return;
11215
+ const files = readdirSync2(outputDir).filter((f) => f.endsWith(".txt")).map((f) => ({
11216
+ name: f,
11217
+ path: join10(outputDir, f),
11218
+ mtime: statSync2(join10(outputDir, f)).mtimeMs
11219
+ })).sort((a, b) => b.mtime - a.mtime);
11220
+ if (files.length <= MAX_OUTPUT_FILES) return;
11221
+ const toDelete = files.slice(MAX_OUTPUT_FILES);
11222
+ for (const file of toDelete) {
11223
+ try {
11224
+ unlinkSync5(file.path);
11225
+ } catch {
11226
+ }
11227
+ }
11228
+ debugLog("general", "Output files rotated", {
11229
+ deleted: toDelete.length,
11230
+ remaining: MAX_OUTPUT_FILES
11231
+ });
11232
+ } catch {
11233
+ }
11234
+ }
11235
+
10864
11236
  // src/agents/prompt-builder.ts
10865
11237
  var __dirname4 = dirname5(fileURLToPath4(import.meta.url));
10866
- var PROMPTS_DIR = join10(__dirname4, "prompts");
10867
- var TECHNIQUES_DIR = join10(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
11238
+ var PROMPTS_DIR = join11(__dirname4, "prompts");
11239
+ var TECHNIQUES_DIR = join11(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
10868
11240
  var { AGENT_FILES } = PROMPT_PATHS;
10869
11241
  var PHASE_PROMPT_MAP = {
10870
11242
  // Direct mappings — phase has its own prompt file
@@ -10938,6 +11310,7 @@ var PromptBuilder = class {
10938
11310
  * 13. Learned techniques (#7: dynamic technique library)
10939
11311
  * 14. Persistent memory (#12: cross-session knowledge)
10940
11312
  * ★ 15. STRATEGIC DIRECTIVE — LLM-generated tactical instructions (D-CIPHER)
11313
+ * ★ 15b. SESSION JOURNAL — compressed history of past turns (§13 memo system)
10941
11314
  * 16. User context
10942
11315
  */
10943
11316
  async build(userInput, phase) {
@@ -10963,8 +11336,10 @@ var PromptBuilder = class {
10963
11336
  // #12
10964
11337
  this.getDynamicTechniquesFragment(),
10965
11338
  // #7
10966
- this.getPersistentMemoryFragment()
11339
+ this.getPersistentMemoryFragment(),
10967
11340
  // #12
11341
+ this.getJournalFragment()
11342
+ // §13 session journal
10968
11343
  ];
10969
11344
  const strategistDirective = await this.getStrategistFragment();
10970
11345
  if (strategistDirective) {
@@ -10988,8 +11363,8 @@ ${content}
10988
11363
  * Load a prompt file from src/agents/prompts/
10989
11364
  */
10990
11365
  loadPromptFile(filename) {
10991
- const path2 = join10(PROMPTS_DIR, filename);
10992
- return existsSync8(path2) ? readFileSync5(path2, PROMPT_CONFIG.ENCODING) : "";
11366
+ const path2 = join11(PROMPTS_DIR, filename);
11367
+ return existsSync9(path2) ? readFileSync6(path2, PROMPT_CONFIG.ENCODING) : "";
10993
11368
  }
10994
11369
  /**
10995
11370
  * Load phase-specific prompt.
@@ -11032,18 +11407,18 @@ ${content}
11032
11407
  * as general reference — NO code change needed to add new techniques.
11033
11408
  *
11034
11409
  * The map is an optimization (priority ordering), not a gate.
11035
- * "마크다운 파일 하나를 폴더에 넣으면, PromptBuilder 자동으로 발견하고 로드한다."
11410
+ * "Drop a markdown file in the folder, PromptBuilder auto-discovers and loads it."
11036
11411
  */
11037
11412
  loadPhaseRelevantTechniques(phase) {
11038
- if (!existsSync8(TECHNIQUES_DIR)) return "";
11413
+ if (!existsSync9(TECHNIQUES_DIR)) return "";
11039
11414
  const priorityTechniques = PHASE_TECHNIQUE_MAP[phase] || [];
11040
11415
  const loadedSet = /* @__PURE__ */ new Set();
11041
11416
  const fragments = [];
11042
11417
  for (const technique of priorityTechniques) {
11043
- const filePath = join10(TECHNIQUES_DIR, `${technique}.md`);
11418
+ const filePath = join11(TECHNIQUES_DIR, `${technique}.md`);
11044
11419
  try {
11045
- if (!existsSync8(filePath)) continue;
11046
- const content = readFileSync5(filePath, PROMPT_CONFIG.ENCODING);
11420
+ if (!existsSync9(filePath)) continue;
11421
+ const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
11047
11422
  if (content) {
11048
11423
  fragments.push(`<technique-reference category="${technique}">
11049
11424
  ${content}
@@ -11054,10 +11429,10 @@ ${content}
11054
11429
  }
11055
11430
  }
11056
11431
  try {
11057
- const allFiles = readdirSync2(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
11432
+ const allFiles = readdirSync3(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
11058
11433
  for (const file of allFiles) {
11059
- const filePath = join10(TECHNIQUES_DIR, file);
11060
- const content = readFileSync5(filePath, PROMPT_CONFIG.ENCODING);
11434
+ const filePath = join11(TECHNIQUES_DIR, file);
11435
+ const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
11061
11436
  if (content) {
11062
11437
  const category = file.replace(".md", "");
11063
11438
  fragments.push(`<technique-reference category="${category}">
@@ -11160,6 +11535,23 @@ ${lines.join("\n")}
11160
11535
  }
11161
11536
  return this.state.persistentMemory.toPrompt(services);
11162
11537
  }
11538
+ // --- §13: Session Journal Summary ---
11539
+ /**
11540
+ * Load journal summary from .pentesting/journal/summary.md
11541
+ * Provides compressed history of past turns — what worked, what failed,
11542
+ * what was discovered. Main LLM uses this for continuity across many turns.
11543
+ */
11544
+ getJournalFragment() {
11545
+ try {
11546
+ const summary = readJournalSummary();
11547
+ if (!summary) return "";
11548
+ return `<session-journal>
11549
+ ${summary}
11550
+ </session-journal>`;
11551
+ } catch {
11552
+ return "";
11553
+ }
11554
+ }
11163
11555
  // --- D-CIPHER: Strategist Meta-Prompting ---
11164
11556
  /**
11165
11557
  * Generate strategic directive via Strategist LLM.
@@ -11172,29 +11564,11 @@ ${lines.join("\n")}
11172
11564
  };
11173
11565
 
11174
11566
  // src/agents/strategist.ts
11175
- import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
11176
- import { join as join11, dirname as dirname6 } from "path";
11567
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
11568
+ import { join as join12, dirname as dirname6 } from "path";
11177
11569
  import { fileURLToPath as fileURLToPath5 } from "url";
11178
-
11179
- // src/shared/constants/strategist.ts
11180
- var STRATEGIST_LIMITS = {
11181
- /** Maximum characters of state context sent to Strategist LLM.
11182
- * WHY: Keeps Strategist input focused and affordable (~3-5K tokens).
11183
- * Full state can be 20K+; Strategist needs summary, not everything. */
11184
- MAX_INPUT_CHARS: 15e3,
11185
- /** Maximum characters for the Strategist's response.
11186
- * WHY: Directives should be terse and actionable (~800-1500 tokens).
11187
- * Enhanced format includes SITUATION, priorities, EXHAUSTED, and SEARCH ORDERS. */
11188
- MAX_OUTPUT_CHARS: 5e3,
11189
- /** Maximum lines in the directive output.
11190
- * WHY: Forces concise, prioritized directives while allowing
11191
- * structured format (priorities + exhausted + search orders). */
11192
- MAX_DIRECTIVE_LINES: 60
11193
- };
11194
-
11195
- // src/agents/strategist.ts
11196
11570
  var __dirname5 = dirname6(fileURLToPath5(import.meta.url));
11197
- var STRATEGIST_PROMPT_PATH = join11(__dirname5, "prompts", "strategist-system.md");
11571
+ var STRATEGIST_PROMPT_PATH = join12(__dirname5, "prompts", "strategist-system.md");
11198
11572
  var Strategist = class {
11199
11573
  llm;
11200
11574
  state;
@@ -11245,24 +11619,33 @@ var Strategist = class {
11245
11619
  const sections = [];
11246
11620
  sections.push("## Engagement State");
11247
11621
  sections.push(this.state.toPrompt());
11248
- const timeline = this.state.episodicMemory.toPrompt();
11249
- if (timeline) {
11250
- sections.push("");
11251
- sections.push("## Recent Actions");
11252
- sections.push(timeline);
11253
- }
11254
11622
  const failures = this.state.workingMemory.toPrompt();
11255
11623
  if (failures) {
11256
11624
  sections.push("");
11257
11625
  sections.push("## Failed Attempts (DO NOT REPEAT THESE)");
11258
11626
  sections.push(failures);
11259
11627
  }
11628
+ try {
11629
+ const journalSummary = readJournalSummary();
11630
+ if (journalSummary) {
11631
+ sections.push("");
11632
+ sections.push("## Session Journal (past turns summary)");
11633
+ sections.push(journalSummary);
11634
+ }
11635
+ } catch {
11636
+ }
11260
11637
  const graph = this.state.attackGraph.toPrompt();
11261
11638
  if (graph) {
11262
11639
  sections.push("");
11263
11640
  sections.push("## Attack Graph");
11264
11641
  sections.push(graph);
11265
11642
  }
11643
+ const timeline = this.state.episodicMemory.toPrompt();
11644
+ if (timeline) {
11645
+ sections.push("");
11646
+ sections.push("## Recent Actions");
11647
+ sections.push(timeline);
11648
+ }
11266
11649
  const techniques = this.state.dynamicTechniques.toPrompt();
11267
11650
  if (techniques) {
11268
11651
  sections.push("");
@@ -11278,16 +11661,12 @@ var Strategist = class {
11278
11661
  sections.push(`## Challenge Type: ${analysis.primaryType.toUpperCase()} (${(analysis.confidence * 100).toFixed(0)}%)`);
11279
11662
  sections.push(analysis.strategySuggestion);
11280
11663
  }
11281
- let input = sections.join("\n");
11282
- if (input.length > STRATEGIST_LIMITS.MAX_INPUT_CHARS) {
11283
- input = input.slice(0, STRATEGIST_LIMITS.MAX_INPUT_CHARS) + "\n\n... [state truncated for Strategist context]";
11284
- }
11285
- return input;
11664
+ return sections.join("\n");
11286
11665
  }
11287
11666
  // ─── LLM Call ───────────────────────────────────────────────
11288
11667
  async callLLM(input) {
11289
11668
  const messages = [{
11290
- role: "user",
11669
+ role: LLM_ROLES.USER,
11291
11670
  content: `Analyze this penetration test situation and write a tactical directive for the attack agent.
11292
11671
 
11293
11672
  ${input}`
@@ -11299,9 +11678,6 @@ ${input}`
11299
11678
  this.systemPrompt
11300
11679
  );
11301
11680
  let content = response.content || "";
11302
- if (content.length > STRATEGIST_LIMITS.MAX_OUTPUT_CHARS) {
11303
- content = content.slice(0, STRATEGIST_LIMITS.MAX_OUTPUT_CHARS) + "\n... [directive truncated]";
11304
- }
11305
11681
  const cost = response.usage ? response.usage.input_tokens + response.usage.output_tokens : 0;
11306
11682
  this.totalTokenCost += cost;
11307
11683
  return {
@@ -11316,7 +11692,7 @@ ${input}`
11316
11692
  */
11317
11693
  formatForPrompt(directive, isStale = false) {
11318
11694
  if (!directive.content) return "";
11319
- const age = Math.floor((Date.now() - directive.generatedAt) / 6e4);
11695
+ const age = Math.floor((Date.now() - directive.generatedAt) / MS_PER_MINUTE);
11320
11696
  const staleWarning = isStale ? `
11321
11697
  NOTE: This directive is from ${age}min ago (Strategist call failed this turn). Verify assumptions are still valid.` : "";
11322
11698
  return [
@@ -11331,8 +11707,8 @@ NOTE: This directive is from ${age}min ago (Strategist call failed this turn). V
11331
11707
  // ─── System Prompt Loading ──────────────────────────────────
11332
11708
  loadSystemPrompt() {
11333
11709
  try {
11334
- if (existsSync9(STRATEGIST_PROMPT_PATH)) {
11335
- return readFileSync6(STRATEGIST_PROMPT_PATH, "utf-8");
11710
+ if (existsSync10(STRATEGIST_PROMPT_PATH)) {
11711
+ return readFileSync7(STRATEGIST_PROMPT_PATH, "utf-8");
11336
11712
  }
11337
11713
  } catch {
11338
11714
  }
@@ -11374,6 +11750,8 @@ var MainAgent = class extends CoreAgent {
11374
11750
  approvalGate;
11375
11751
  scopeGuard;
11376
11752
  userInput = "";
11753
+ /** Monotonic turn counter for journal entries */
11754
+ turnCounter = 0;
11377
11755
  constructor(state, events, toolRegistry, approvalGate, scopeGuard) {
11378
11756
  super(AGENT_ROLES.ORCHESTRATOR, state, events, toolRegistry);
11379
11757
  this.approvalGate = approvalGate;
@@ -11411,8 +11789,34 @@ var MainAgent = class extends CoreAgent {
11411
11789
  * The Strategist LLM generates a fresh tactical directive every turn.
11412
11790
  */
11413
11791
  async step(iteration, messages, _unusedPrompt, progress) {
11792
+ if (this.turnCounter === 0) {
11793
+ this.turnCounter = getNextTurnNumber();
11794
+ }
11795
+ this.turnToolJournal = [];
11796
+ this.turnMemo = { keyFindings: [], credentials: [], attackVectors: [], failures: [], suspicions: [], attackValue: "LOW", nextSteps: [] };
11797
+ this.turnReflections = [];
11414
11798
  const dynamicPrompt = await this.getCurrentPrompt();
11415
11799
  const result2 = await super.step(iteration, messages, dynamicPrompt, progress);
11800
+ if (this.turnToolJournal.length > 0) {
11801
+ try {
11802
+ const entry = {
11803
+ turn: this.turnCounter,
11804
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11805
+ phase: this.state.getPhase(),
11806
+ tools: this.turnToolJournal,
11807
+ memo: this.turnMemo,
11808
+ reflection: this.turnReflections.length > 0 ? this.turnReflections.join(" | ") : this.turnMemo.nextSteps.join("; ")
11809
+ };
11810
+ writeJournalEntry(entry);
11811
+ if (shouldRegenerateSummary(this.turnCounter)) {
11812
+ regenerateJournalSummary();
11813
+ }
11814
+ rotateJournalEntries();
11815
+ rotateOutputFiles();
11816
+ } catch {
11817
+ }
11818
+ this.turnCounter++;
11819
+ }
11416
11820
  this.emitStateChange();
11417
11821
  return result2;
11418
11822
  }
@@ -11465,8 +11869,8 @@ var MainAgent = class extends CoreAgent {
11465
11869
  });
11466
11870
  }
11467
11871
  // ─── Public API Surface ─────────────────────────────────────
11468
- setAutoApprove(enabled) {
11469
- this.approvalGate.setAutoApprove(enabled);
11872
+ setAutoApprove(shouldEnable) {
11873
+ this.approvalGate.setAutoApprove(shouldEnable);
11470
11874
  }
11471
11875
  getState() {
11472
11876
  return this.state;
@@ -11978,9 +12382,10 @@ var useAgentEvents = (agent, eventsRef, state) => {
11978
12382
  const onReasoningDelta = (e) => {
11979
12383
  reasoningBufferRef.current += e.data.content;
11980
12384
  const chars = reasoningBufferRef.current.length;
11981
- const estTokens = Math.round(chars / 4);
12385
+ const estTokens = Math.round(chars / LLM_LIMITS.charsPerTokenEstimate);
12386
+ const firstLine = reasoningBufferRef.current.split("\n")[0]?.slice(0, TUI_DISPLAY_LIMITS.reasoningPreviewChars) || "";
11982
12387
  setCurrentStatus(`Reasoning (~${estTokens} tokens)
11983
- ${reasoningBufferRef.current}`);
12388
+ ${firstLine}`);
11984
12389
  };
11985
12390
  const onReasoningEnd = () => {
11986
12391
  const text = reasoningBufferRef.current.trim();
@@ -12439,7 +12844,7 @@ var MessageList = memo(({ messages }) => {
12439
12844
  if (msg.type === "thinking") {
12440
12845
  const lines = msg.content.split("\n");
12441
12846
  const charCount = msg.content.length;
12442
- const estTokens = Math.round(charCount / 4);
12847
+ const estTokens = Math.round(charCount / LLM_LIMITS.charsPerTokenEstimate);
12443
12848
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 0, marginBottom: 1, children: [
12444
12849
  /* @__PURE__ */ jsxs2(Box2, { children: [
12445
12850
  /* @__PURE__ */ jsx2(Text2, { color: THEME.cyan, bold: true, children: "Reasoning" }),
@@ -12701,9 +13106,9 @@ import { memo as memo5 } from "react";
12701
13106
  import { Box as Box5, Text as Text6 } from "ink";
12702
13107
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
12703
13108
  var formatElapsed = (totalSeconds) => {
12704
- const hours = Math.floor(totalSeconds / 3600);
12705
- const minutes = Math.floor(totalSeconds % 3600 / 60);
12706
- const seconds = Math.floor(totalSeconds % 60);
13109
+ const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
13110
+ const minutes = Math.floor(totalSeconds % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
13111
+ const seconds = Math.floor(totalSeconds % SECONDS_PER_MINUTE);
12707
13112
  const pad = (n) => String(n).padStart(2, "0");
12708
13113
  if (hours > 0) {
12709
13114
  return `${hours}:${pad(minutes)}:${pad(seconds)}`;
@@ -12857,11 +13262,11 @@ var App = ({ autoApprove = false, target }) => {
12857
13262
  }
12858
13263
  if (f.evidence.length > 0) {
12859
13264
  findingLines.push(` Evidence:`);
12860
- f.evidence.slice(0, 3).forEach((e) => {
12861
- const preview = e.length > 120 ? e.slice(0, 120) + "..." : e;
13265
+ f.evidence.slice(0, DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW).forEach((e) => {
13266
+ const preview = e.length > DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH ? e.slice(0, DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH) + "..." : e;
12862
13267
  findingLines.push(` \u25B8 ${preview}`);
12863
13268
  });
12864
- if (f.evidence.length > 3) findingLines.push(` ... +${f.evidence.length - 3} more`);
13269
+ if (f.evidence.length > DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW) findingLines.push(` ... +${f.evidence.length - DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW} more`);
12865
13270
  }
12866
13271
  findingLines.push("");
12867
13272
  });