pentesting 0.47.2 → 0.47.3

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.3";
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,68 @@ 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 WORKSPACE = {
832
+ /** Root directory */
833
+ get ROOT() {
834
+ return path.resolve(PENTESTING_ROOT);
835
+ },
836
+ /** Temporary files */
837
+ get TMP() {
838
+ return path.resolve(WORK_DIR);
839
+ },
840
+ /** Persistent memory */
841
+ get MEMORY() {
842
+ return path.resolve(MEMORY_DIR);
843
+ },
844
+ /** Generated reports */
845
+ get REPORTS() {
846
+ return path.resolve(REPORTS_DIR);
847
+ },
848
+ /** Session snapshots */
849
+ get SESSIONS() {
850
+ return path.resolve(SESSIONS_DIR);
851
+ },
852
+ /** Captured loot */
853
+ get LOOT() {
854
+ return path.resolve(LOOT_DIR);
855
+ },
856
+ /** Full tool outputs */
857
+ get OUTPUTS() {
858
+ return path.resolve(OUTPUTS_DIR);
859
+ },
860
+ /** Debug logs */
861
+ get DEBUG() {
862
+ return path.resolve(DEBUG_DIR);
863
+ }
864
+ };
865
+
866
+ // src/shared/constants/orchestrator.ts
867
+ var GRACEFUL_SHUTDOWN_WAIT_MS = 200;
868
+ var PROCESS_OUTPUT_TRUNCATION_LIMIT = 1e4;
869
+ var LONG_RUNNING_THRESHOLD_MS = 5 * MS_PER_MINUTE;
870
+ var VERY_LONG_RUNNING_THRESHOLD_MS = 15 * MS_PER_MINUTE;
871
+ var MAX_RECOMMENDATIONS_FOR_HEALTHY = 2;
872
+ var DEFAULT_DIRECTIVE_FOCUS = PHASES.RECON;
873
+
760
874
  // src/shared/utils/command-security-lists.ts
761
875
  var ALLOWED_BINARIES = /* @__PURE__ */ new Set([
762
876
  // Network scanning
@@ -964,7 +1078,7 @@ var SAFE_PIPE_TARGETS = /* @__PURE__ */ new Set([
964
1078
  "python3",
965
1079
  "python"
966
1080
  ]);
967
- var SAFE_REDIRECT_PATHS = ["/tmp/", "/root/", "/var/log/", "/opt/"];
1081
+ var SAFE_REDIRECT_PATHS = [`${WORK_DIR}/`];
968
1082
  var BLOCKED_BINARIES = /* @__PURE__ */ new Set([
969
1083
  "rm",
970
1084
  "shred",
@@ -989,50 +1103,6 @@ var BLOCKED_BINARIES = /* @__PURE__ */ new Set([
989
1103
  // src/shared/utils/debug-logger.ts
990
1104
  import { appendFileSync, writeFileSync } from "fs";
991
1105
  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
1106
  var DebugLogger = class _DebugLogger {
1037
1107
  static instance;
1038
1108
  logPath;
@@ -1123,16 +1193,16 @@ var SHELL_CHARS = {
1123
1193
  };
1124
1194
  function validateCommand(command) {
1125
1195
  if (!command || typeof command !== "string") {
1126
- return { safe: false, error: "Empty or invalid command" };
1196
+ return { isSafe: false, error: "Empty or invalid command" };
1127
1197
  }
1128
1198
  const normalizedCommand = command.trim();
1129
1199
  if (normalizedCommand.length === 0) {
1130
- return { safe: false, error: "Empty command" };
1200
+ return { isSafe: false, error: "Empty command" };
1131
1201
  }
1132
1202
  for (const pattern of INJECTION_PATTERNS) {
1133
1203
  if (pattern.test(normalizedCommand)) {
1134
1204
  return {
1135
- safe: false,
1205
+ isSafe: false,
1136
1206
  error: `Injection pattern detected: ${pattern.source}`,
1137
1207
  suggestion: "Avoid command substitution ($(), ``), variable expansion (${}) and control characters."
1138
1208
  };
@@ -1141,13 +1211,13 @@ function validateCommand(command) {
1141
1211
  const subCommands = splitChainedCommands(normalizedCommand);
1142
1212
  for (const subCmd of subCommands) {
1143
1213
  const result2 = validateSingleCommand(subCmd.trim());
1144
- if (!result2.safe) {
1214
+ if (!result2.isSafe) {
1145
1215
  return result2;
1146
1216
  }
1147
1217
  }
1148
1218
  const primaryBinary = extractBinary(subCommands[0].trim());
1149
1219
  return {
1150
- safe: true,
1220
+ isSafe: true,
1151
1221
  binary: primaryBinary || void 0,
1152
1222
  args: normalizedCommand.split(/\s+/).slice(1)
1153
1223
  };
@@ -1202,14 +1272,14 @@ function validateSingleCommand(command) {
1202
1272
  if (!binary) continue;
1203
1273
  if (BLOCKED_BINARIES.has(binary)) {
1204
1274
  return {
1205
- safe: false,
1275
+ isSafe: false,
1206
1276
  error: `Binary '${binary}' is blocked for security reasons`,
1207
1277
  suggestion: `'${binary}' is not allowed. Use a different approach.`
1208
1278
  };
1209
1279
  }
1210
1280
  if (idx > 0 && !SAFE_PIPE_TARGETS.has(binary) && !ALLOWED_BINARIES.has(binary)) {
1211
1281
  return {
1212
- safe: false,
1282
+ isSafe: false,
1213
1283
  error: `Pipe target '${binary}' is not in the allowed list`,
1214
1284
  suggestion: `Piping to '${binary}' is not allowed. Safe pipe targets: head, tail, grep, awk, sed, cut, sort, uniq, wc, jq, tee.`
1215
1285
  };
@@ -1219,10 +1289,10 @@ function validateSingleCommand(command) {
1219
1289
  }
1220
1290
  }
1221
1291
  const redirectResult = validateRedirects(command);
1222
- if (!redirectResult.safe) {
1292
+ if (!redirectResult.isSafe) {
1223
1293
  return redirectResult;
1224
1294
  }
1225
- return { safe: true };
1295
+ return { isSafe: true };
1226
1296
  }
1227
1297
  function splitByPipe(command) {
1228
1298
  const parts = [];
@@ -1265,33 +1335,91 @@ function stripRedirects(segment) {
1265
1335
  return segment.replace(/\d*>\s*&\d+/g, "").replace(/\d*>>\s*\S+/g, "").replace(/\d*>\s*\S+/g, "").replace(/<\s*\S+/g, "").trim();
1266
1336
  }
1267
1337
  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
- };
1338
+ const redirects = extractRedirectTargets(command);
1339
+ for (const { type, fd, target } of redirects) {
1340
+ if (type === ">" || type === ">>") {
1341
+ if (target.startsWith("&") || target === "/dev/null") continue;
1342
+ const isAllowed = SAFE_REDIRECT_PATHS.some((p) => target.startsWith(p));
1343
+ if (!isAllowed) {
1344
+ return {
1345
+ isSafe: false,
1346
+ error: `Redirect to '${target}' is not in allowed paths`,
1347
+ suggestion: `Redirect output to ${WORK_DIR}/ paths. Example: > ${WORK_DIR}/output.txt`
1348
+ };
1349
+ }
1350
+ } else if (type === "<") {
1351
+ const isAllowed = SAFE_REDIRECT_PATHS.some((p) => target.startsWith(p));
1352
+ if (!isAllowed) {
1353
+ return {
1354
+ isSafe: false,
1355
+ error: `Input redirect from '${target}' is not in allowed paths`,
1356
+ suggestion: `Input redirect only from ${WORK_DIR}/ paths.`
1357
+ };
1358
+ }
1280
1359
  }
1281
1360
  }
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
- };
1361
+ return { isSafe: true };
1362
+ }
1363
+ function extractRedirectTargets(command) {
1364
+ const redirects = [];
1365
+ let inSingle = false;
1366
+ let inDouble = false;
1367
+ let i = 0;
1368
+ while (i < command.length) {
1369
+ const ch = command[i];
1370
+ if (ch === "'" && !inDouble) {
1371
+ inSingle = !inSingle;
1372
+ i++;
1373
+ continue;
1374
+ }
1375
+ if (ch === '"' && !inSingle) {
1376
+ inDouble = !inDouble;
1377
+ i++;
1378
+ continue;
1379
+ }
1380
+ if (!inSingle && !inDouble) {
1381
+ if (ch === ">" || ch === "<") {
1382
+ const isAppend = ch === ">" && command[i + 1] === ">";
1383
+ const type = isAppend ? ">>" : ch;
1384
+ const jump = isAppend ? 2 : 1;
1385
+ let fd = "";
1386
+ if (ch === ">") {
1387
+ let b = i - 1;
1388
+ while (b >= 0 && /\d/.test(command[b])) {
1389
+ fd = command[b] + fd;
1390
+ b--;
1391
+ }
1392
+ }
1393
+ i += jump;
1394
+ while (i < command.length && /\s/.test(command[i])) i++;
1395
+ let target = "";
1396
+ let targetInSingle = false;
1397
+ let targetInDouble = false;
1398
+ while (i < command.length) {
1399
+ const tc = command[i];
1400
+ if (tc === "'" && !targetInDouble) {
1401
+ targetInSingle = !targetInSingle;
1402
+ i++;
1403
+ continue;
1404
+ }
1405
+ if (tc === '"' && !targetInSingle) {
1406
+ targetInDouble = !targetInDouble;
1407
+ i++;
1408
+ continue;
1409
+ }
1410
+ if (!targetInSingle && !targetInDouble && /[\s|;&<>]/.test(tc)) break;
1411
+ target += tc;
1412
+ i++;
1413
+ }
1414
+ if (target) {
1415
+ redirects.push({ type, fd, target });
1416
+ }
1417
+ continue;
1418
+ }
1292
1419
  }
1420
+ i++;
1293
1421
  }
1294
- return { safe: true };
1422
+ return redirects;
1295
1423
  }
1296
1424
  function extractBinary(command) {
1297
1425
  const parts = command.trim().split(/\s+/);
@@ -1514,7 +1642,7 @@ function clearCommandEventEmitter() {
1514
1642
  async function runCommand(command, args = [], options = {}) {
1515
1643
  const fullCommand = args.length > 0 ? `${command} ${args.join(" ")}` : command;
1516
1644
  const validation = validateCommand(fullCommand);
1517
- if (!validation.safe) {
1645
+ if (!validation.isSafe) {
1518
1646
  return {
1519
1647
  success: false,
1520
1648
  output: "",
@@ -1688,6 +1816,34 @@ function createTempFile(suffix = "") {
1688
1816
  return join2(tmpdir(), generateTempFilename(suffix));
1689
1817
  }
1690
1818
 
1819
+ // src/shared/constants/files.ts
1820
+ var FILE_EXTENSIONS = {
1821
+ // Data formats
1822
+ JSON: ".json",
1823
+ MARKDOWN: ".md",
1824
+ TXT: ".txt",
1825
+ XML: ".xml",
1826
+ // Network capture
1827
+ PCAP: ".pcap",
1828
+ HOSTS: ".hosts",
1829
+ MITM: ".mitm",
1830
+ // Process I/O
1831
+ STDOUT: ".stdout",
1832
+ STDERR: ".stderr",
1833
+ STDIN: ".stdin",
1834
+ // Scripts
1835
+ SH: ".sh",
1836
+ PY: ".py",
1837
+ CJS: ".cjs",
1838
+ // Config
1839
+ ENV: ".env",
1840
+ BAK: ".bak"
1841
+ };
1842
+ var SPECIAL_FILES = {
1843
+ LATEST_STATE: "latest.json",
1844
+ README: "README.md"
1845
+ };
1846
+
1691
1847
  // src/engine/process-cleanup.ts
1692
1848
  import { unlinkSync } from "fs";
1693
1849
 
@@ -1877,9 +2033,9 @@ function logEvent(processId, event, detail) {
1877
2033
  }
1878
2034
  function startBackgroundProcess(command, options = {}) {
1879
2035
  const processId = generatePrefixedId("bg");
1880
- const stdoutFile = createTempFile(".stdout");
1881
- const stderrFile = createTempFile(".stderr");
1882
- const stdinFile = createTempFile(".stdin");
2036
+ const stdoutFile = createTempFile(FILE_EXTENSIONS.STDOUT);
2037
+ const stderrFile = createTempFile(FILE_EXTENSIONS.STDERR);
2038
+ const stdinFile = createTempFile(FILE_EXTENSIONS.STDIN);
1883
2039
  const { tags, port, role, isInteractive } = detectProcessRole(command);
1884
2040
  let wrappedCmd;
1885
2041
  if (isInteractive) {
@@ -2979,7 +3135,7 @@ var WorkingMemory = class {
2979
3135
  const lines = ["<working-memory>"];
2980
3136
  if (failures.length > 0) {
2981
3137
  lines.push(`\u26A0\uFE0F FAILED ATTEMPTS (${failures.length} \u2014 DO NOT REPEAT):`);
2982
- for (const f of failures.slice(-5)) {
3138
+ for (const f of failures.slice(-DISPLAY_LIMITS.RECENT_FAILURES)) {
2983
3139
  lines.push(` \u2717 ${f.content}`);
2984
3140
  }
2985
3141
  }
@@ -2989,13 +3145,13 @@ var WorkingMemory = class {
2989
3145
  }
2990
3146
  if (successes.length > 0) {
2991
3147
  lines.push(`\u2705 RECENT SUCCESSES (${successes.length}):`);
2992
- for (const s of successes.slice(-3)) {
3148
+ for (const s of successes.slice(-DISPLAY_LIMITS.RECENT_SUCCESSES)) {
2993
3149
  lines.push(` \u2713 ${s.content}`);
2994
3150
  }
2995
3151
  }
2996
3152
  if (insights.length > 0) {
2997
3153
  lines.push(`\u{1F4A1} INSIGHTS:`);
2998
- for (const i of insights.slice(-3)) {
3154
+ for (const i of insights.slice(-DISPLAY_LIMITS.RECENT_INSIGHTS)) {
2999
3155
  lines.push(` \u2192 ${i.content}`);
3000
3156
  }
3001
3157
  }
@@ -3048,9 +3204,9 @@ var EpisodicMemory = class {
3048
3204
  toPrompt() {
3049
3205
  if (this.events.length === 0) return "";
3050
3206
  const lines = ["<session-timeline>"];
3051
- const recent = this.events.slice(-10);
3207
+ const recent = this.events.slice(-DISPLAY_LIMITS.RECENT_MEMORY_EVENTS);
3052
3208
  for (const e of recent) {
3053
- const mins = Math.floor((Date.now() - e.timestamp) / 6e4);
3209
+ const mins = Math.floor((Date.now() - e.timestamp) / MS_PER_MINUTE);
3054
3210
  const icon = {
3055
3211
  tool_success: "\u2705",
3056
3212
  tool_failure: "\u274C",
@@ -3069,7 +3225,6 @@ var EpisodicMemory = class {
3069
3225
  this.events = [];
3070
3226
  }
3071
3227
  };
3072
- var MEMORY_DIR = "/tmp/pentesting-memory";
3073
3228
  var MEMORY_FILE = join3(MEMORY_DIR, "persistent-knowledge.json");
3074
3229
  var PersistentMemory = class {
3075
3230
  knowledge;
@@ -3186,7 +3341,7 @@ var DynamicTechniqueLibrary = class {
3186
3341
  });
3187
3342
  if (this.techniques.length > this.maxTechniques) {
3188
3343
  this.techniques.sort((a, b) => {
3189
- if (a.verified !== b.verified) return a.verified ? -1 : 1;
3344
+ if (a.isVerified !== b.isVerified) return a.isVerified ? -1 : 1;
3190
3345
  return b.learnedAt - a.learnedAt;
3191
3346
  });
3192
3347
  this.techniques = this.techniques.slice(0, this.maxTechniques);
@@ -3222,7 +3377,7 @@ var DynamicTechniqueLibrary = class {
3222
3377
  source: `Web search: "${query}"`,
3223
3378
  technique: tech,
3224
3379
  applicableTo,
3225
- verified: false,
3380
+ isVerified: false,
3226
3381
  fromQuery: query
3227
3382
  });
3228
3383
  }
@@ -3233,7 +3388,7 @@ var DynamicTechniqueLibrary = class {
3233
3388
  verify(techniqueSubstring) {
3234
3389
  for (const t of this.techniques) {
3235
3390
  if (t.technique.toLowerCase().includes(techniqueSubstring.toLowerCase())) {
3236
- t.verified = true;
3391
+ t.isVerified = true;
3237
3392
  }
3238
3393
  }
3239
3394
  }
@@ -3261,8 +3416,8 @@ var DynamicTechniqueLibrary = class {
3261
3416
  */
3262
3417
  toPrompt() {
3263
3418
  if (this.techniques.length === 0) return "";
3264
- const verified = this.techniques.filter((t) => t.verified);
3265
- const unverified = this.techniques.filter((t) => !t.verified);
3419
+ const verified = this.techniques.filter((t) => t.isVerified);
3420
+ const unverified = this.techniques.filter((t) => !t.isVerified);
3266
3421
  const lines = ["<learned-techniques>"];
3267
3422
  if (verified.length > 0) {
3268
3423
  lines.push("VERIFIED (worked in this session):");
@@ -3539,8 +3694,8 @@ var SharedState = class {
3539
3694
  return this.data.currentPhase;
3540
3695
  }
3541
3696
  // --- CTF Mode ---
3542
- setCtfMode(enabled) {
3543
- this.data.ctfMode = enabled;
3697
+ setCtfMode(shouldEnable) {
3698
+ this.data.ctfMode = shouldEnable;
3544
3699
  }
3545
3700
  isCtfMode() {
3546
3701
  return this.data.ctfMode;
@@ -3847,8 +4002,8 @@ var ApprovalGate = class {
3847
4002
  /**
3848
4003
  * Set auto-approve mode
3849
4004
  */
3850
- setAutoApprove(enabled) {
3851
- this.shouldAutoApprove = enabled;
4005
+ setAutoApprove(shouldEnable) {
4006
+ this.shouldAutoApprove = shouldEnable;
3852
4007
  }
3853
4008
  /**
3854
4009
  * Get current auto-approve mode
@@ -4989,6 +5144,36 @@ function formatValidation(result2) {
4989
5144
  }
4990
5145
 
4991
5146
  // src/engine/tools/pentest-target-tools.ts
5147
+ function isPortArray(value) {
5148
+ if (!Array.isArray(value)) return false;
5149
+ return value.every(
5150
+ (item) => typeof item === "object" && item !== null && typeof item.port === "number"
5151
+ );
5152
+ }
5153
+ function parsePorts(value) {
5154
+ return isPortArray(value) ? value : [];
5155
+ }
5156
+ function isValidSeverity(value) {
5157
+ return typeof value === "string" && Object.values(SEVERITIES).includes(value);
5158
+ }
5159
+ function parseSeverity(value) {
5160
+ return isValidSeverity(value) ? value : "medium";
5161
+ }
5162
+ function isValidLootType(value) {
5163
+ return typeof value === "string" && Object.values(LOOT_TYPES).includes(value);
5164
+ }
5165
+ function parseLootType(value) {
5166
+ return isValidLootType(value) ? value : "file";
5167
+ }
5168
+ function isValidAttackTactic(value) {
5169
+ return typeof value === "string" && Object.values(ATTACK_TACTICS).includes(value);
5170
+ }
5171
+ function parseStringArray(value) {
5172
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : [];
5173
+ }
5174
+ function parseString(value, defaultValue = "") {
5175
+ return typeof value === "string" ? value : defaultValue;
5176
+ }
4992
5177
  var createTargetTools = (state) => [
4993
5178
  {
4994
5179
  name: TOOL_NAMES.ADD_TARGET,
@@ -5020,10 +5205,10 @@ The target will be tracked in SharedState and available for all agents.`,
5020
5205
  },
5021
5206
  required: ["ip"],
5022
5207
  execute: async (p) => {
5023
- const ip = p.ip;
5208
+ const ip = parseString(p.ip);
5024
5209
  const existing = state.getTarget(ip);
5025
5210
  if (existing) {
5026
- const newPorts = p.ports || [];
5211
+ const newPorts = parsePorts(p.ports);
5027
5212
  for (const np of newPorts) {
5028
5213
  const exists = existing.ports.some((ep) => ep.port === np.port);
5029
5214
  if (!exists) {
@@ -5037,11 +5222,11 @@ The target will be tracked in SharedState and available for all agents.`,
5037
5222
  state.attackGraph.addService(ip, np.port, np.service || "unknown", np.version);
5038
5223
  }
5039
5224
  }
5040
- if (p.hostname) existing.hostname = p.hostname;
5041
- if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...p.tags])];
5225
+ if (p.hostname) existing.hostname = parseString(p.hostname);
5226
+ if (p.tags) existing.tags = [.../* @__PURE__ */ new Set([...existing.tags, ...parseStringArray(p.tags)])];
5042
5227
  return { success: true, output: `Target ${ip} updated. Total ports: ${existing.ports.length}` };
5043
5228
  }
5044
- const ports = (p.ports || []).map((port) => ({
5229
+ const ports = parsePorts(p.ports).map((port) => ({
5045
5230
  port: port.port,
5046
5231
  service: port.service || "unknown",
5047
5232
  version: port.version,
@@ -5050,9 +5235,9 @@ The target will be tracked in SharedState and available for all agents.`,
5050
5235
  }));
5051
5236
  state.addTarget({
5052
5237
  ip,
5053
- hostname: p.hostname,
5238
+ hostname: parseString(p.hostname) || void 0,
5054
5239
  ports,
5055
- tags: p.tags || [],
5240
+ tags: parseStringArray(p.tags),
5056
5241
  firstSeen: Date.now()
5057
5242
  });
5058
5243
  return { success: true, output: `Target ${ip} added.${p.hostname ? ` Hostname: ${p.hostname}` : ""} Ports: ${ports.length}` };
@@ -5071,30 +5256,30 @@ Types: credential, hash, token, ssh_key, api_key, file, session, ticket, certifi
5071
5256
  },
5072
5257
  required: ["type", "host", "detail"],
5073
5258
  execute: async (p) => {
5074
- const lootType = p.type;
5259
+ const lootTypeStr = parseString(p.type);
5075
5260
  const crackableTypes = ["hash"];
5076
- const detail = p.detail;
5077
- const host = p.host;
5261
+ const detail = parseString(p.detail);
5262
+ const host = parseString(p.host);
5078
5263
  state.addLoot({
5079
- type: lootType,
5264
+ type: parseLootType(lootTypeStr),
5080
5265
  host,
5081
5266
  detail,
5082
5267
  obtainedAt: Date.now(),
5083
- isCrackable: crackableTypes.includes(lootType),
5268
+ isCrackable: crackableTypes.includes(lootTypeStr),
5084
5269
  isCracked: false
5085
5270
  });
5086
- if (["credential", "token", "ssh_key", "api_key"].includes(lootType)) {
5271
+ if (["credential", "token", "ssh_key", "api_key"].includes(lootTypeStr)) {
5087
5272
  const parts = detail.split(":");
5088
5273
  if (parts.length >= 2) {
5089
5274
  state.attackGraph.addCredential(parts[0], parts.slice(1).join(":"), host);
5090
5275
  }
5091
5276
  }
5092
- state.episodicMemory.record("access_gained", `Loot [${lootType}] from ${host}: ${detail.slice(0, DISPLAY_LIMITS.MEMORY_EVENT_PREVIEW)}`);
5277
+ state.episodicMemory.record("access_gained", `Loot [${lootTypeStr}] from ${host}: ${detail.slice(0, DISPLAY_LIMITS.MEMORY_EVENT_PREVIEW)}`);
5093
5278
  return {
5094
5279
  success: true,
5095
- output: `Loot recorded: [${lootType}] from ${host}
5280
+ output: `Loot recorded: [${lootTypeStr}] from ${host}
5096
5281
  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.`)
5282
+ ` + (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
5283
  };
5099
5284
  }
5100
5285
  },
@@ -5113,12 +5298,13 @@ Findings without evidence are marked as UNVERIFIED and have low credibility.`,
5113
5298
  },
5114
5299
  required: ["title", "severity", "description", "evidence"],
5115
5300
  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 || "";
5301
+ const evidence = parseStringArray(p.evidence);
5302
+ const title = parseString(p.title);
5303
+ const severity = parseSeverity(p.severity);
5304
+ const affected = parseStringArray(p.affected);
5305
+ const description = parseString(p.description);
5121
5306
  const validation = validateFinding(evidence, severity);
5307
+ const attackPattern = parseString(p.attackPattern);
5122
5308
  state.addFinding({
5123
5309
  id: generateId(AGENT_LIMITS.ID_RADIX, AGENT_LIMITS.ID_LENGTH),
5124
5310
  title,
@@ -5129,7 +5315,7 @@ Findings without evidence are marked as UNVERIFIED and have low credibility.`,
5129
5315
  isVerified: validation.isVerified,
5130
5316
  remediation: "",
5131
5317
  foundAt: Date.now(),
5132
- ...p.attackPattern ? { attackPattern: p.attackPattern } : {}
5318
+ ...attackPattern && isValidAttackTactic(attackPattern) ? { attackPattern } : {}
5133
5319
  });
5134
5320
  const hasExploit = validation.isVerified;
5135
5321
  const target = affected[0] || "unknown";
@@ -5147,7 +5333,7 @@ ${formatValidation(validation)}`
5147
5333
  }
5148
5334
  ];
5149
5335
 
5150
- // src/engine/tools-mid.ts
5336
+ // src/engine/tools/intel-utils.ts
5151
5337
  import { execFileSync } from "child_process";
5152
5338
 
5153
5339
  // src/shared/utils/config.ts
@@ -5160,7 +5346,7 @@ var ENV_KEYS = {
5160
5346
  THINKING: "PENTEST_THINKING",
5161
5347
  THINKING_BUDGET: "PENTEST_THINKING_BUDGET"
5162
5348
  };
5163
- var DEFAULT_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search";
5349
+ var DEFAULT_SEARCH_API_URL = "https://open.bigmodel.cn/api/paas/v4/tools/web-search-pro";
5164
5350
  function getApiKey() {
5165
5351
  return process.env[ENV_KEYS.API_KEY] || "";
5166
5352
  }
@@ -5171,7 +5357,10 @@ function getModel() {
5171
5357
  return process.env[ENV_KEYS.MODEL] || "";
5172
5358
  }
5173
5359
  function getSearchApiKey() {
5174
- return process.env[ENV_KEYS.SEARCH_API_KEY];
5360
+ if (process.env[ENV_KEYS.SEARCH_API_KEY]) {
5361
+ return process.env[ENV_KEYS.SEARCH_API_KEY];
5362
+ }
5363
+ return process.env[ENV_KEYS.API_KEY];
5175
5364
  }
5176
5365
  function getSearchApiUrl() {
5177
5366
  return process.env[ENV_KEYS.SEARCH_API_URL] || DEFAULT_SEARCH_API_URL;
@@ -5663,14 +5852,11 @@ async function webSearchWithBrowser(query, engine = "google") {
5663
5852
  }
5664
5853
 
5665
5854
  // src/engine/web-search-providers.ts
5666
- function getErrorMessage(error) {
5667
- return error instanceof Error ? error.message : String(error);
5668
- }
5669
5855
  async function searchWithGLM(query, apiKey, apiUrl) {
5670
5856
  debugLog("search", "GLM request START", { apiUrl, query });
5671
5857
  const requestBody = {
5672
5858
  model: "web-search-pro",
5673
- messages: [{ role: "user", content: query }],
5859
+ messages: [{ role: LLM_ROLES.USER, content: query }],
5674
5860
  stream: false
5675
5861
  };
5676
5862
  debugLog("search", "GLM request body", requestBody);
@@ -5789,38 +5975,14 @@ async function searchWithGenericApi(query, apiKey, apiUrl) {
5789
5975
  const data = await response.json();
5790
5976
  return { success: true, output: JSON.stringify(data, null, 2) };
5791
5977
  }
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
5978
 
5817
- // src/engine/tools-mid.ts
5979
+ // src/engine/tools/intel-utils.ts
5818
5980
  var PORT_STATE2 = {
5819
5981
  OPEN: "open",
5820
5982
  CLOSED: "closed",
5821
5983
  FILTERED: "filtered"
5822
5984
  };
5823
- function getErrorMessage2(error) {
5985
+ function getErrorMessage(error) {
5824
5986
  return error instanceof Error ? error.message : String(error);
5825
5987
  }
5826
5988
  async function parseNmap(xmlPath) {
@@ -5871,7 +6033,7 @@ async function parseNmap(xmlPath) {
5871
6033
  return {
5872
6034
  success: false,
5873
6035
  output: "",
5874
- error: getErrorMessage2(error)
6036
+ error: getErrorMessage(error)
5875
6037
  };
5876
6038
  }
5877
6039
  }
@@ -5882,7 +6044,7 @@ async function searchCVE(service, version) {
5882
6044
  return {
5883
6045
  success: false,
5884
6046
  output: "",
5885
- error: getErrorMessage2(error)
6047
+ error: getErrorMessage(error)
5886
6048
  };
5887
6049
  }
5888
6050
  }
@@ -5919,43 +6081,58 @@ async function searchExploitDB(service, version) {
5919
6081
  return {
5920
6082
  success: false,
5921
6083
  output: "",
5922
- error: getErrorMessage2(error)
6084
+ error: getErrorMessage(error)
5923
6085
  };
5924
6086
  }
5925
6087
  }
5926
6088
  async function webSearch(query, _engine) {
5927
6089
  debugLog("search", "webSearch START", { query });
6090
+ const apiKey = getSearchApiKey();
6091
+ const apiUrl = getSearchApiUrl();
6092
+ debugLog("search", "Search API config", {
6093
+ hasApiKey: !!apiKey,
6094
+ apiUrl,
6095
+ apiKeyPrefix: apiKey ? apiKey.slice(0, DISPLAY_LIMITS.API_KEY_PREFIX) + "..." : null
6096
+ });
6097
+ if (!apiKey) {
6098
+ return {
6099
+ success: false,
6100
+ output: "",
6101
+ error: `SEARCH_API_KEY is required for web search. Set it in your environment:
6102
+
6103
+ # Brave Search (recommended - free tier available)
6104
+ export SEARCH_API_KEY=your_brave_api_key
6105
+ # Get key at: https://brave.com/search/api/
6106
+
6107
+ # Or GLM Web Search (\u667A\u8C31 AI)
6108
+ export SEARCH_API_KEY=your_glm_api_key
6109
+ export SEARCH_API_URL=https://open.bigmodel.cn/api/paas/v4/tools/web-search-pro
6110
+
6111
+ # Or Serper (Google)
6112
+ export SEARCH_API_KEY=your_serper_key
6113
+ export SEARCH_API_URL=https://google.serper.dev/search`
6114
+ };
6115
+ }
5928
6116
  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
- }
6117
+ if (apiUrl.includes(SEARCH_URL_PATTERN.GLM) || apiUrl.includes(SEARCH_URL_PATTERN.ZHIPU)) {
6118
+ debugLog("search", "Using GLM search");
6119
+ return await searchWithGLM(query, apiKey, apiUrl);
6120
+ } else if (apiUrl.includes(SEARCH_URL_PATTERN.BRAVE)) {
6121
+ debugLog("search", "Using Brave search");
6122
+ return await searchWithBrave(query, apiKey, apiUrl);
6123
+ } else if (apiUrl.includes(SEARCH_URL_PATTERN.SERPER)) {
6124
+ debugLog("search", "Using Serper search");
6125
+ return await searchWithSerper(query, apiKey, apiUrl);
6126
+ } else {
6127
+ debugLog("search", "Using generic search API");
6128
+ return await searchWithGenericApi(query, apiKey, apiUrl);
5950
6129
  }
5951
- debugLog("search", "SEARCH_API_KEY not set \u2014 falling back to Playwright Google search");
5952
- return await searchWithPlaywright(query);
5953
6130
  } catch (error) {
5954
- debugLog("search", "webSearch ERROR", { error: getErrorMessage2(error) });
6131
+ debugLog("search", "webSearch ERROR", { error: getErrorMessage(error) });
5955
6132
  return {
5956
6133
  success: false,
5957
6134
  output: "",
5958
- error: getErrorMessage2(error)
6135
+ error: getErrorMessage(error)
5959
6136
  };
5960
6137
  }
5961
6138
  }
@@ -7235,7 +7412,7 @@ Requires root/sudo privileges.`,
7235
7412
  const iface = p.interface || "any";
7236
7413
  const filter = p.filter || "";
7237
7414
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SNIFF_DURATION;
7238
- const outputFile = p.output_file || createTempFile(".pcap");
7415
+ const outputFile = p.output_file || createTempFile(FILE_EXTENSIONS.PCAP);
7239
7416
  const count = p.count || NETWORK_CONFIG.DEFAULT_PACKET_COUNT;
7240
7417
  const extractCreds = p.extract_creds !== false;
7241
7418
  const filterFlag = filter ? `"${filter}"` : "";
@@ -7320,7 +7497,7 @@ Requires root/sudo privileges.`,
7320
7497
  const spoofIp = p.spoof_ip;
7321
7498
  const iface = p.interface || "";
7322
7499
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
7323
- const hostsFile = createTempFile(".hosts");
7500
+ const hostsFile = createTempFile(FILE_EXTENSIONS.HOSTS);
7324
7501
  const { writeFileSync: writeFileSync8 } = await import("fs");
7325
7502
  writeFileSync8(hostsFile, `${spoofIp} ${domain}
7326
7503
  ${spoofIp} *.${domain}
@@ -7372,7 +7549,7 @@ Combine with arp_spoof for transparent proxying.`,
7372
7549
  const port = p.port || NETWORK_CONFIG.DEFAULT_PROXY_PORT;
7373
7550
  const targetHost = p.target_host;
7374
7551
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
7375
- const outputFile = p.output_file || createTempFile(".mitm");
7552
+ const outputFile = p.output_file || createTempFile(FILE_EXTENSIONS.MITM);
7376
7553
  const sslInsecure = p.ssl_insecure !== false;
7377
7554
  let cmd;
7378
7555
  const sslFlag = sslInsecure ? "--ssl-insecure" : "";
@@ -7436,7 +7613,7 @@ This is a high-level tool that combines tcpdump capture with protocol analysis.`
7436
7613
  const iface = p.interface || "any";
7437
7614
  const protocols = p.protocols || "all";
7438
7615
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SNIFF_DURATION;
7439
- const outputFile = createTempFile(".pcap");
7616
+ const outputFile = createTempFile(FILE_EXTENSIONS.PCAP);
7440
7617
  let bpfFilter = `host ${target}`;
7441
7618
  if (protocols !== "all") {
7442
7619
  const protoFilters = [];
@@ -7587,15 +7764,6 @@ var ZombieHunter = class {
7587
7764
  }
7588
7765
  };
7589
7766
 
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
7767
  // src/engine/resource/health-monitor.ts
7600
7768
  var HEALTH_STATUS = {
7601
7769
  HEALTHY: "healthy",
@@ -8696,6 +8864,10 @@ var DOMAINS = {
8696
8864
  };
8697
8865
 
8698
8866
  // src/engine/tools-registry.ts
8867
+ var VALID_CATEGORIES = Object.values(SERVICE_CATEGORIES);
8868
+ function isValidCategory(id) {
8869
+ return VALID_CATEGORIES.includes(id);
8870
+ }
8699
8871
  var CategorizedToolRegistry = class extends ToolRegistry {
8700
8872
  categories = /* @__PURE__ */ new Map();
8701
8873
  constructor(state, scopeGuard, approvalGate, events) {
@@ -8727,13 +8899,13 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8727
8899
  ];
8728
8900
  const coreTools = coreToolNames.map((name) => this.getTool(name)).filter((t) => !!t);
8729
8901
  Object.keys(DOMAINS).forEach((id) => {
8730
- const cat = id;
8731
- this.categories.set(cat, {
8732
- name: cat,
8733
- description: DOMAINS[cat]?.description || "",
8902
+ if (!isValidCategory(id)) return;
8903
+ this.categories.set(id, {
8904
+ name: id,
8905
+ description: DOMAINS[id]?.description || "",
8734
8906
  tools: [...coreTools],
8735
- dangerLevel: this.calculateDanger(cat),
8736
- defaultApproval: CATEGORY_APPROVAL2[cat] || "confirm"
8907
+ dangerLevel: this.calculateDanger(id),
8908
+ defaultApproval: CATEGORY_APPROVAL2[id] || "confirm"
8737
8909
  });
8738
8910
  });
8739
8911
  }
@@ -8750,19 +8922,17 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8750
8922
  const cat = ServiceParser.detectCategory(p.port, p.service);
8751
8923
  if (cat && !seen.has(cat)) {
8752
8924
  seen.add(cat);
8753
- const suggestion = {
8925
+ results.push({
8754
8926
  category: cat,
8755
8927
  tools: this.getByCategory(cat).map((t) => t.name)
8756
- };
8757
- results.push(suggestion);
8928
+ });
8758
8929
  }
8759
8930
  });
8760
8931
  if (target.hostname && ServiceParser.isCloudHostname(target.hostname) && !seen.has(SERVICE_CATEGORIES.CLOUD)) {
8761
- const cloudSuggestion = {
8932
+ results.push({
8762
8933
  category: SERVICE_CATEGORIES.CLOUD,
8763
8934
  tools: this.getByCategory(SERVICE_CATEGORIES.CLOUD).map((t) => t.name)
8764
- };
8765
- results.push(cloudSuggestion);
8935
+ });
8766
8936
  }
8767
8937
  return results;
8768
8938
  }
@@ -9340,7 +9510,7 @@ function saveState(state) {
9340
9510
  }
9341
9511
  function pruneOldSessions(sessionsDir) {
9342
9512
  try {
9343
- const sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(".json") && f !== "latest.json").map((f) => ({
9513
+ const sessionFiles = readdirSync(sessionsDir).filter((f) => f.endsWith(FILE_EXTENSIONS.JSON) && f !== SPECIAL_FILES.LATEST_STATE).map((f) => ({
9344
9514
  name: f,
9345
9515
  path: join9(sessionsDir, f),
9346
9516
  mtime: statSync(join9(sessionsDir, f)).mtimeMs
@@ -9481,13 +9651,13 @@ function appendBlockedCommandHints(lines, errorLower) {
9481
9651
  if (errorLower.includes("pipe target") || errorLower.includes("injection")) {
9482
9652
  lines.push(`Fix: Use the tool's built-in output options instead of shell pipes.`);
9483
9653
  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`);
9654
+ lines.push(` 1. Save output to file: command > ${WORK_DIR}/output.txt, then use read_file("${WORK_DIR}/output.txt")`);
9655
+ lines.push(` 2. Use tool-specific flags: nmap -oN ${WORK_DIR}/scan.txt, curl -o ${WORK_DIR}/page.html`);
9486
9656
  lines.push(` 3. Use browse_url for web content instead of curl | grep`);
9487
9657
  lines.push(` 4. If filtering output: run the command first, then read_file + parse results`);
9488
9658
  } 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`);
9659
+ lines.push(`Fix: Redirect to ${WORK_DIR}/ path.`);
9660
+ lines.push(`Example: command > ${WORK_DIR}/output.txt or command 2>&1 > ${WORK_DIR}/errors.txt`);
9491
9661
  } else {
9492
9662
  lines.push(`Fix: Simplify the command. Avoid chaining complex operations.`);
9493
9663
  lines.push(`Break into multiple steps: run_cmd \u2192 read_file \u2192 run_cmd.`);
@@ -10006,7 +10176,7 @@ function createLLMDigestFn(llmClient) {
10006
10176
  return async (text, context) => {
10007
10177
  const truncatedText = text.length > LAYER3_MAX_INPUT_CHARS ? text.slice(0, LAYER3_MAX_INPUT_CHARS) + `
10008
10178
  ... [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.
10179
+ const messages = [{ role: LLM_ROLES.USER, content: `Analyze this pentesting tool output and extract actionable intelligence.
10010
10180
 
10011
10181
  Context: ${context}
10012
10182
 
@@ -11287,7 +11457,7 @@ var Strategist = class {
11287
11457
  // ─── LLM Call ───────────────────────────────────────────────
11288
11458
  async callLLM(input) {
11289
11459
  const messages = [{
11290
- role: "user",
11460
+ role: LLM_ROLES.USER,
11291
11461
  content: `Analyze this penetration test situation and write a tactical directive for the attack agent.
11292
11462
 
11293
11463
  ${input}`
@@ -11316,7 +11486,7 @@ ${input}`
11316
11486
  */
11317
11487
  formatForPrompt(directive, isStale = false) {
11318
11488
  if (!directive.content) return "";
11319
- const age = Math.floor((Date.now() - directive.generatedAt) / 6e4);
11489
+ const age = Math.floor((Date.now() - directive.generatedAt) / MS_PER_MINUTE);
11320
11490
  const staleWarning = isStale ? `
11321
11491
  NOTE: This directive is from ${age}min ago (Strategist call failed this turn). Verify assumptions are still valid.` : "";
11322
11492
  return [
@@ -11465,8 +11635,8 @@ var MainAgent = class extends CoreAgent {
11465
11635
  });
11466
11636
  }
11467
11637
  // ─── Public API Surface ─────────────────────────────────────
11468
- setAutoApprove(enabled) {
11469
- this.approvalGate.setAutoApprove(enabled);
11638
+ setAutoApprove(shouldEnable) {
11639
+ this.approvalGate.setAutoApprove(shouldEnable);
11470
11640
  }
11471
11641
  getState() {
11472
11642
  return this.state;
@@ -11978,9 +12148,10 @@ var useAgentEvents = (agent, eventsRef, state) => {
11978
12148
  const onReasoningDelta = (e) => {
11979
12149
  reasoningBufferRef.current += e.data.content;
11980
12150
  const chars = reasoningBufferRef.current.length;
11981
- const estTokens = Math.round(chars / 4);
12151
+ const estTokens = Math.round(chars / LLM_LIMITS.charsPerTokenEstimate);
12152
+ const firstLine = reasoningBufferRef.current.split("\n")[0]?.slice(0, TUI_DISPLAY_LIMITS.reasoningPreviewChars) || "";
11982
12153
  setCurrentStatus(`Reasoning (~${estTokens} tokens)
11983
- ${reasoningBufferRef.current}`);
12154
+ ${firstLine}`);
11984
12155
  };
11985
12156
  const onReasoningEnd = () => {
11986
12157
  const text = reasoningBufferRef.current.trim();
@@ -12439,7 +12610,7 @@ var MessageList = memo(({ messages }) => {
12439
12610
  if (msg.type === "thinking") {
12440
12611
  const lines = msg.content.split("\n");
12441
12612
  const charCount = msg.content.length;
12442
- const estTokens = Math.round(charCount / 4);
12613
+ const estTokens = Math.round(charCount / LLM_LIMITS.charsPerTokenEstimate);
12443
12614
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 0, marginBottom: 1, children: [
12444
12615
  /* @__PURE__ */ jsxs2(Box2, { children: [
12445
12616
  /* @__PURE__ */ jsx2(Text2, { color: THEME.cyan, bold: true, children: "Reasoning" }),
@@ -12701,9 +12872,9 @@ import { memo as memo5 } from "react";
12701
12872
  import { Box as Box5, Text as Text6 } from "ink";
12702
12873
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
12703
12874
  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);
12875
+ const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
12876
+ const minutes = Math.floor(totalSeconds % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
12877
+ const seconds = Math.floor(totalSeconds % SECONDS_PER_MINUTE);
12707
12878
  const pad = (n) => String(n).padStart(2, "0");
12708
12879
  if (hours > 0) {
12709
12880
  return `${hours}:${pad(minutes)}:${pad(seconds)}`;
@@ -12857,11 +13028,11 @@ var App = ({ autoApprove = false, target }) => {
12857
13028
  }
12858
13029
  if (f.evidence.length > 0) {
12859
13030
  findingLines.push(` Evidence:`);
12860
- f.evidence.slice(0, 3).forEach((e) => {
12861
- const preview = e.length > 120 ? e.slice(0, 120) + "..." : e;
13031
+ f.evidence.slice(0, DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW).forEach((e) => {
13032
+ const preview = e.length > DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH ? e.slice(0, DISPLAY_LIMITS.EVIDENCE_PREVIEW_LENGTH) + "..." : e;
12862
13033
  findingLines.push(` \u25B8 ${preview}`);
12863
13034
  });
12864
- if (f.evidence.length > 3) findingLines.push(` ... +${f.evidence.length - 3} more`);
13035
+ if (f.evidence.length > DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW) findingLines.push(` ... +${f.evidence.length - DISPLAY_LIMITS.EVIDENCE_ITEMS_PREVIEW} more`);
12865
13036
  }
12866
13037
  findingLines.push("");
12867
13038
  });