pentesting 0.24.1 → 0.24.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
@@ -13,7 +13,7 @@ import chalk from "chalk";
13
13
  import gradient from "gradient-string";
14
14
 
15
15
  // src/platform/tui/app.tsx
16
- import { useState as useState4, useCallback as useCallback3, useEffect as useEffect4 } from "react";
16
+ import { useState as useState4, useCallback as useCallback3, useEffect as useEffect4, useRef as useRef3 } from "react";
17
17
  import { Box as Box6, useInput, useApp } from "ink";
18
18
 
19
19
  // src/platform/tui/hooks/useAgent.ts
@@ -286,7 +286,7 @@ var ORPHAN_PROCESS_NAMES = [
286
286
  var ID_LENGTH = AGENT_LIMITS.ID_LENGTH;
287
287
  var ID_RADIX = AGENT_LIMITS.ID_RADIX;
288
288
  var APP_NAME = "Pentest AI";
289
- var APP_VERSION = "0.24.1";
289
+ var APP_VERSION = "0.24.3";
290
290
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
291
291
  var LLM_ROLES = {
292
292
  SYSTEM: "system",
@@ -560,11 +560,6 @@ var UI_COMMANDS = {
560
560
  };
561
561
  var PASSIVE_BINARIES = ["whois", "dig", "curl -i"];
562
562
  var EXPLOIT_BINARIES = ["impacket"];
563
- var MASK_PARTS = {
564
- C24: "24",
565
- C16: "16",
566
- C8: "8"
567
- };
568
563
  var COMMAND_EVENT_TYPES = {
569
564
  TOOL_MISSING: "tool_missing",
570
565
  TOOL_INSTALL: "tool_install",
@@ -870,6 +865,10 @@ var WORKSPACE = {
870
865
  /** Temporary files for active operations */
871
866
  get TEMP() {
872
867
  return path.join(getWorkspaceRoot(), "temp");
868
+ },
869
+ /** Full tool output files saved by Context Digest */
870
+ get OUTPUTS() {
871
+ return path.join(getWorkspaceRoot(), "outputs");
873
872
  }
874
873
  };
875
874
 
@@ -2164,7 +2163,9 @@ var SharedState = class {
2164
2163
  missionChecklist: [],
2165
2164
  ctfMode: true,
2166
2165
  // CTF mode ON by default
2167
- flags: []
2166
+ flags: [],
2167
+ startedAt: Date.now(),
2168
+ deadlineAt: 0
2168
2169
  };
2169
2170
  }
2170
2171
  // --- Mission & Persistent Context ---
@@ -2322,6 +2323,42 @@ var SharedState = class {
2322
2323
  getFlags() {
2323
2324
  return this.data.flags;
2324
2325
  }
2326
+ // --- Time Management ---
2327
+ /** Set a deadline for time-aware strategy (e.g., CTF competition end time) */
2328
+ setDeadline(deadlineMs) {
2329
+ this.data.deadlineAt = deadlineMs;
2330
+ }
2331
+ /** Get milliseconds since agent started */
2332
+ getElapsedMs() {
2333
+ return Date.now() - this.data.startedAt;
2334
+ }
2335
+ /** Get remaining milliseconds until deadline (0 if no deadline) */
2336
+ getRemainingMs() {
2337
+ if (this.data.deadlineAt === 0) return 0;
2338
+ return Math.max(0, this.data.deadlineAt - Date.now());
2339
+ }
2340
+ /** Get time status string for prompt injection */
2341
+ getTimeStatus() {
2342
+ const elapsed = this.getElapsedMs();
2343
+ const mins = Math.floor(elapsed / 6e4);
2344
+ let status = `\u23F1 Elapsed: ${mins}min`;
2345
+ if (this.data.deadlineAt > 0) {
2346
+ const remainingMs = this.getRemainingMs();
2347
+ const remainingMins = Math.floor(remainingMs / 6e4);
2348
+ if (remainingMins <= 0) {
2349
+ status += " | \u26A0\uFE0F TIME IS UP \u2014 submit any flags you have NOW";
2350
+ } else if (remainingMins <= 15) {
2351
+ status += ` | \u{1F534} ${remainingMins}min LEFT \u2014 FINAL PUSH: submit flags, check obvious locations, env vars, DBs`;
2352
+ } else if (remainingMins <= 30) {
2353
+ status += ` | \u{1F7E1} ${remainingMins}min LEFT \u2014 wrap up current vector, ensure all findings are captured`;
2354
+ } else if (remainingMins <= 60) {
2355
+ status += ` | \u{1F7E0} ${remainingMins}min LEFT \u2014 focus on highest-probability vectors only`;
2356
+ } else {
2357
+ status += ` | \u{1F7E2} ${remainingMins}min remaining`;
2358
+ }
2359
+ }
2360
+ return status;
2361
+ }
2325
2362
  /**
2326
2363
  * @remarks
2327
2364
  * WHY: Delegating complex string builder to specialized serializer.
@@ -2493,30 +2530,43 @@ var ScopeGuard = class {
2493
2530
  return false;
2494
2531
  }
2495
2532
  /**
2496
- * Simple CIDR matching (IP-only for now)
2533
+ * Accurate CIDR matching using bitwise arithmetic.
2534
+ * Supports any prefix length /0 through /32.
2535
+ * No external dependencies — pure math.
2497
2536
  */
2498
2537
  matchesCidr(target, cidr) {
2499
2538
  if (target === cidr) return true;
2500
2539
  if (cidr.includes("/")) {
2501
- const [network, mask] = cidr.split("/");
2502
- if (mask === MASK_PARTS.C24) {
2503
- const networkPrefix = network.split(".").slice(0, 3).join(".");
2504
- const targetPrefix = target.split(".").slice(0, 3).join(".");
2505
- return networkPrefix === targetPrefix;
2506
- }
2507
- if (mask === MASK_PARTS.C8) {
2508
- const networkPrefix = network.split(".")[0];
2509
- const targetPrefix = target.split(".")[0];
2510
- return networkPrefix === targetPrefix;
2540
+ const [network, maskStr] = cidr.split("/");
2541
+ const maskBits = parseInt(maskStr, 10);
2542
+ if (isNaN(maskBits) || maskBits < 0 || maskBits > 32) {
2543
+ return target === cidr;
2511
2544
  }
2512
- if (mask === MASK_PARTS.C16) {
2513
- const networkPrefix = network.split(".").slice(0, 2).join(".");
2514
- const targetPrefix = target.split(".").slice(0, 2).join(".");
2515
- return networkPrefix === targetPrefix;
2545
+ const targetInt = this.ipToUint32(target);
2546
+ const networkInt = this.ipToUint32(network);
2547
+ if (targetInt === null || networkInt === null) {
2548
+ return target === cidr;
2516
2549
  }
2550
+ const mask = maskBits === 0 ? 0 : ~0 << 32 - maskBits >>> 0;
2551
+ return (targetInt & mask) >>> 0 === (networkInt & mask) >>> 0;
2517
2552
  }
2518
2553
  return target === cidr;
2519
2554
  }
2555
+ /**
2556
+ * Convert IPv4 dotted-decimal string to unsigned 32-bit integer.
2557
+ * Returns null if the string is not a valid IPv4 address.
2558
+ */
2559
+ ipToUint32(ip) {
2560
+ const parts = ip.split(".");
2561
+ if (parts.length !== 4) return null;
2562
+ let result2 = 0;
2563
+ for (const part of parts) {
2564
+ const octet = parseInt(part, 10);
2565
+ if (isNaN(octet) || octet < 0 || octet > 255) return null;
2566
+ result2 = (result2 << 8 | octet) >>> 0;
2567
+ }
2568
+ return result2;
2569
+ }
2520
2570
  /**
2521
2571
  * Extract IPs and Domains from string
2522
2572
  */
@@ -4803,7 +4853,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4803
4853
  }
4804
4854
  },
4805
4855
  execute: async (p) => {
4806
- const { existsSync: existsSync7, statSync: statSync2, readdirSync: readdirSync3 } = await import("fs");
4856
+ const { existsSync: existsSync8, statSync: statSync2, readdirSync: readdirSync3 } = await import("fs");
4807
4857
  const { join: join10 } = await import("path");
4808
4858
  const category = p.category || "";
4809
4859
  const search = p.search || "";
@@ -4850,7 +4900,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
4850
4900
  results.push("");
4851
4901
  };
4852
4902
  const scanDir = (dirPath, maxDepth = 3, depth = 0) => {
4853
- if (depth > maxDepth || !existsSync7(dirPath)) return;
4903
+ if (depth > maxDepth || !existsSync8(dirPath)) return;
4854
4904
  let entries;
4855
4905
  try {
4856
4906
  entries = readdirSync3(dirPath, { withFileTypes: true });
@@ -5233,8 +5283,8 @@ Requires root/sudo privileges.`,
5233
5283
  const iface = p.interface || "";
5234
5284
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
5235
5285
  const hostsFile = createTempFile(".hosts");
5236
- const { writeFileSync: writeFileSync6 } = await import("fs");
5237
- writeFileSync6(hostsFile, `${spoofIp} ${domain}
5286
+ const { writeFileSync: writeFileSync7 } = await import("fs");
5287
+ writeFileSync7(hostsFile, `${spoofIp} ${domain}
5238
5288
  ${spoofIp} *.${domain}
5239
5289
  `);
5240
5290
  const ifaceFlag = iface ? `-i ${iface}` : "";
@@ -6651,7 +6701,7 @@ var FLAG_PATTERNS = {
6651
6701
  // Generic CTF flag formats
6652
6702
  generic: /flag\{[^}]+\}/gi,
6653
6703
  curly_upper: /FLAG\{[^}]+\}/g,
6654
- // Platform-specific (major competitions)
6704
+ // Platform-specific Major International Competitions
6655
6705
  htb: /HTB\{[^}]+\}/g,
6656
6706
  thm: /THM\{[^}]+\}/g,
6657
6707
  picoCTF: /picoCTF\{[^}]+\}/g,
@@ -6667,8 +6717,65 @@ var FLAG_PATTERNS = {
6667
6717
  // hxp CTF
6668
6718
  zer0pts: /zer0pts\{[^}]+\}/g,
6669
6719
  // zer0pts CTF
6720
+ // Asia-Pacific Major Competitions
6721
+ seccon: /SECCON\{[^}]+\}/g,
6722
+ // SECCON CTF (Japan)
6723
+ hitcon: /hitcon\{[^}]+\}/gi,
6724
+ // HITCON CTF (Taiwan)
6725
+ codegate: /codegate\{[^}]+\}/gi,
6726
+ // Codegate CTF (Korea)
6727
+ wacon: /WACON\{[^}]+\}/g,
6728
+ // WACON CTF (Korea)
6729
+ linectf: /LINECTF\{[^}]+\}/g,
6730
+ // LINE CTF (Korea/Japan)
6731
+ tsgctf: /TSGCTF\{[^}]+\}/g,
6732
+ // TSG CTF (Japan)
6733
+ kosenctf: /KosenCTF\{[^}]+\}/g,
6734
+ // Kosen CTF (Japan)
6735
+ asis: /ASIS\{[^}]+\}/g,
6736
+ // ASIS CTF (Iran)
6737
+ // Americas / Europe Major Competitions
6738
+ plaidctf: /PCTF\{[^}]+\}/g,
6739
+ // PlaidCTF (PPP)
6740
+ dicectf: /dice\{[^}]+\}/gi,
6741
+ // DiceCTF
6742
+ rwctf: /rwctf\{[^}]+\}/gi,
6743
+ // Real World CTF
6744
+ rctf: /RCTF\{[^}]+\}/g,
6745
+ // RCTF
6746
+ n1ctf: /N1CTF\{[^}]+\}/g,
6747
+ // N1CTF (Nu1L)
6748
+ bctf: /BCTF\{[^}]+\}/g,
6749
+ // BCTF
6750
+ _0ctf: /0CTF\{[^}]+\}/g,
6751
+ // 0CTF (Shanghai)
6752
+ defcamp: /CTF\{[^}]+\}/g,
6753
+ // DefCamp
6754
+ insomnihack: /INS\{[^}]+\}/g,
6755
+ // Insomni'hack
6756
+ justctf: /justCTF\{[^}]+\}/g,
6757
+ // justCTF
6758
+ lactf: /lactf\{[^}]+\}/gi,
6759
+ // LA CTF (UCLA)
6760
+ damctf: /dam\{[^}]+\}/gi,
6761
+ // DamCTF
6762
+ tjctf: /tjctf\{[^}]+\}/gi,
6763
+ // TJCTF
6764
+ buckeye: /buckeye\{[^}]+\}/gi,
6765
+ // BuckeyeCTF
6766
+ uiuctf: /uiuctf\{[^}]+\}/gi,
6767
+ // UIUCTF
6768
+ // Platform-specific
6769
+ cyber_apocalypse: /HTB\{[^}]+\}/g,
6770
+ // HTB Cyber Apocalypse (same as HTB)
6771
+ offshift: /oS\{[^}]+\}/g,
6772
+ // Offshift CTF
6773
+ imaginaryctf: /ictf\{[^}]+\}/gi,
6774
+ // ImaginaryCTF
6775
+ corctf: /corctf\{[^}]+\}/gi,
6776
+ // corCTF
6777
+ // Generic CTFd format — catches most custom competitions
6670
6778
  ctfd_generic: /[A-Z]{2,10}\{[^}]+\}/g,
6671
- // Generic CTFd format
6672
6779
  // Hash-style flags (HTB user.txt / root.txt — 32-char hex)
6673
6780
  hash_flag: /\b[a-f0-9]{32}\b/g,
6674
6781
  // Base64-encoded flag detection (common in steganography/forensics)
@@ -6765,6 +6872,520 @@ function appendBlockedCommandHints(lines, errorLower) {
6765
6872
  }
6766
6873
  }
6767
6874
 
6875
+ // src/shared/utils/output-compressor.ts
6876
+ var MIN_COMPRESS_LENGTH = 3e3;
6877
+ var SUMMARY_HEADER = "\u2550\u2550\u2550 INTELLIGENCE SUMMARY (auto-extracted) \u2550\u2550\u2550";
6878
+ var SUMMARY_FOOTER = "\u2550\u2550\u2550 END SUMMARY \u2014 Full output follows \u2550\u2550\u2550";
6879
+ var TOOL_SIGNATURES = [
6880
+ {
6881
+ name: "nmap",
6882
+ signatures: [/Nmap scan report/i, /PORT\s+STATE\s+SERVICE/i, /Nmap done:/i],
6883
+ extract: extractNmapIntel
6884
+ },
6885
+ {
6886
+ name: "linpeas",
6887
+ signatures: [/linpeas/i, /╔══.*╗/, /Linux Privilege Escalation/i],
6888
+ extract: extractLinpeasIntel
6889
+ },
6890
+ {
6891
+ name: "enum4linux",
6892
+ signatures: [/enum4linux/i, /Starting enum4linux/i, /\|\s+Target\s+Information/i],
6893
+ extract: extractEnum4linuxIntel
6894
+ },
6895
+ {
6896
+ name: "gobuster/ffuf/feroxbuster",
6897
+ signatures: [/Gobuster/i, /FFUF/i, /feroxbuster/i, /Status:\s*\d{3}/],
6898
+ extract: extractDirBustIntel
6899
+ },
6900
+ {
6901
+ name: "sqlmap",
6902
+ signatures: [/sqlmap/i, /\[INFO\]\s+testing/i, /Parameter:\s+/i],
6903
+ extract: extractSqlmapIntel
6904
+ },
6905
+ {
6906
+ name: "hash_dump",
6907
+ signatures: [/\$[0-9]\$/, /\$2[aby]\$/, /:[0-9]+:[a-f0-9]{32}:/i],
6908
+ extract: extractHashIntel
6909
+ }
6910
+ ];
6911
+ function compressToolOutput(output, toolName) {
6912
+ if (!output || output.length < MIN_COMPRESS_LENGTH) {
6913
+ return output;
6914
+ }
6915
+ let intel = [];
6916
+ let detectedTool = "";
6917
+ for (const sig of TOOL_SIGNATURES) {
6918
+ const matched = sig.signatures.some((s) => s.test(output));
6919
+ if (matched) {
6920
+ detectedTool = sig.name;
6921
+ intel = sig.extract(output);
6922
+ break;
6923
+ }
6924
+ }
6925
+ if (intel.length === 0) {
6926
+ intel = extractGenericIntel(output);
6927
+ detectedTool = toolName || "unknown";
6928
+ }
6929
+ if (intel.length === 0) {
6930
+ return output;
6931
+ }
6932
+ const summary = [
6933
+ SUMMARY_HEADER,
6934
+ `Tool: ${detectedTool} | Output length: ${output.length} chars`,
6935
+ "",
6936
+ ...intel,
6937
+ "",
6938
+ SUMMARY_FOOTER,
6939
+ ""
6940
+ ].join("\n");
6941
+ return summary + output;
6942
+ }
6943
+ function extractNmapIntel(output) {
6944
+ const intel = [];
6945
+ const lines = output.split("\n");
6946
+ const hostMatches = output.match(/Nmap scan report for\s+(\S+)/gi);
6947
+ const openPorts = output.match(/^\d+\/\w+\s+open\s+/gm);
6948
+ if (hostMatches) intel.push(`Hosts scanned: ${hostMatches.length}`);
6949
+ if (openPorts) intel.push(`Open ports found: ${openPorts.length}`);
6950
+ const portLines = lines.filter((l) => /^\d+\/\w+\s+open\s+/.test(l.trim()));
6951
+ if (portLines.length > 0) {
6952
+ intel.push("Open ports:");
6953
+ for (const pl of portLines.slice(0, 30)) {
6954
+ intel.push(` ${pl.trim()}`);
6955
+ }
6956
+ }
6957
+ const vulnLines = lines.filter(
6958
+ (l) => /VULNERABLE|CVE-\d{4}-\d+|exploit|CRITICAL/i.test(l)
6959
+ );
6960
+ if (vulnLines.length > 0) {
6961
+ intel.push("\u26A0\uFE0F Vulnerability indicators:");
6962
+ for (const vl of vulnLines.slice(0, 10)) {
6963
+ intel.push(` ${vl.trim()}`);
6964
+ }
6965
+ }
6966
+ const osMatch = output.match(/OS details:\s*(.+)/i) || output.match(/Running:\s*(.+)/i);
6967
+ if (osMatch) intel.push(`OS: ${osMatch[1].trim()}`);
6968
+ return intel;
6969
+ }
6970
+ function extractLinpeasIntel(output) {
6971
+ const intel = [];
6972
+ const suidSection = output.match(/SUID[\s\S]*?(?=\n[═╔╗━]+|\n\n\n)/i);
6973
+ if (suidSection) {
6974
+ const suidBins = suidSection[0].match(/\/\S+/g);
6975
+ if (suidBins) {
6976
+ const interestingSuid = suidBins.filter(
6977
+ (b) => /python|perl|ruby|node|bash|vim|less|more|nano|find|nmap|awk|env|php|gcc|gdb|docker|strace|ltrace/i.test(b)
6978
+ );
6979
+ if (interestingSuid.length > 0) {
6980
+ intel.push(`\u{1F534} Exploitable SUID binaries: ${interestingSuid.join(", ")}`);
6981
+ }
6982
+ }
6983
+ }
6984
+ const sudoMatch = output.match(/User \S+ may run[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
6985
+ if (sudoMatch) {
6986
+ intel.push(`\u{1F534} sudo -l: ${sudoMatch[0].trim().slice(0, 500)}`);
6987
+ }
6988
+ const writableMatch = output.match(/Interesting writable[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
6989
+ if (writableMatch) {
6990
+ intel.push(`\u{1F4DD} Writable: ${writableMatch[0].trim().slice(0, 300)}`);
6991
+ }
6992
+ const cronMatch = output.match(/Cron[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
6993
+ if (cronMatch) {
6994
+ const cronLines = cronMatch[0].split("\n").filter((l) => l.includes("*") || /\/(root|cron)/i.test(l));
6995
+ if (cronLines.length > 0) {
6996
+ intel.push("\u23F0 Cron entries:");
6997
+ cronLines.slice(0, 5).forEach((c) => intel.push(` ${c.trim()}`));
6998
+ }
6999
+ }
7000
+ const kernelMatch = output.match(/Linux version\s+(\S+)/i) || output.match(/Kernel:\s*(\S+)/i);
7001
+ if (kernelMatch) intel.push(`\u{1F427} Kernel: ${kernelMatch[1]}`);
7002
+ const passLines = output.split("\n").filter(
7003
+ (l) => /password\s*[=:]\s*\S+/i.test(l) && !/\*\*\*|example|sample/i.test(l)
7004
+ );
7005
+ if (passLines.length > 0) {
7006
+ intel.push("\u{1F511} Potential credentials found:");
7007
+ passLines.slice(0, 5).forEach((p) => intel.push(` ${p.trim()}`));
7008
+ }
7009
+ const cveMatches = output.match(/CVE-\d{4}-\d+/gi);
7010
+ if (cveMatches) {
7011
+ const uniqueCves = [...new Set(cveMatches)];
7012
+ intel.push(`\u26A0\uFE0F CVEs mentioned: ${uniqueCves.join(", ")}`);
7013
+ }
7014
+ return intel;
7015
+ }
7016
+ function extractEnum4linuxIntel(output) {
7017
+ const intel = [];
7018
+ const userMatches = output.match(/user:\[(\S+?)\]/gi);
7019
+ if (userMatches) {
7020
+ const users = userMatches.map((u) => u.replace(/user:\[|\]/gi, ""));
7021
+ intel.push(`\u{1F464} Users found: ${users.join(", ")}`);
7022
+ }
7023
+ const shareMatches = output.match(/Mapping: (\S+),\s*Listing: (\S+)/gi) || output.match(/\\\\\S+\\\\\S+/g);
7024
+ if (shareMatches) {
7025
+ intel.push(`\u{1F4C2} Shares: ${shareMatches.slice(0, 10).join(", ")}`);
7026
+ }
7027
+ const domainMatch = output.match(/Domain:\s*\[(\S+?)\]/i) || output.match(/Workgroup:\s*\[(\S+?)\]/i);
7028
+ if (domainMatch) intel.push(`\u{1F3E2} Domain: ${domainMatch[1]}`);
7029
+ const policyMatch = output.match(/Password Complexity|Account Lockout/i);
7030
+ if (policyMatch) intel.push("\u{1F510} Password policy information found");
7031
+ return intel;
7032
+ }
7033
+ function extractDirBustIntel(output) {
7034
+ const intel = [];
7035
+ const lines = output.split("\n");
7036
+ const interestingPaths = [];
7037
+ for (const line of lines) {
7038
+ const match = line.match(/(?:Status:\s*(\d{3})|(\d{3})\s+\d+\s+\d+).*?(\/\S+)/);
7039
+ if (match) {
7040
+ const status = match[1] || match[2];
7041
+ const path2 = match[3];
7042
+ if (["200", "301", "302", "403"].includes(status)) {
7043
+ interestingPaths.push(`[${status}] ${path2}`);
7044
+ }
7045
+ }
7046
+ const ffufMatch = line.match(/(\S+)\s+\[Status:\s*(\d+),?\s*Size:\s*(\d+)/);
7047
+ if (ffufMatch && ["200", "301", "302", "403"].includes(ffufMatch[2])) {
7048
+ interestingPaths.push(`[${ffufMatch[2]}] ${ffufMatch[1]} (${ffufMatch[3]}b)`);
7049
+ }
7050
+ }
7051
+ if (interestingPaths.length > 0) {
7052
+ intel.push(`\u{1F4C1} Discovered paths (${interestingPaths.length}):`);
7053
+ interestingPaths.slice(0, 20).forEach((p) => intel.push(` ${p}`));
7054
+ if (interestingPaths.length > 20) {
7055
+ intel.push(` ... and ${interestingPaths.length - 20} more`);
7056
+ }
7057
+ }
7058
+ return intel;
7059
+ }
7060
+ function extractSqlmapIntel(output) {
7061
+ const intel = [];
7062
+ const injectionTypes = output.match(/Type:\s*(\S.*?)(?:\n|$)/gi);
7063
+ if (injectionTypes) {
7064
+ intel.push("\u{1F489} SQL injection found:");
7065
+ injectionTypes.slice(0, 5).forEach((t) => intel.push(` ${t.trim()}`));
7066
+ }
7067
+ const dbMatch = output.match(/back-end DBMS:\s*(.+)/i);
7068
+ if (dbMatch) intel.push(`\u{1F5C4}\uFE0F DBMS: ${dbMatch[1].trim()}`);
7069
+ const dbListMatch = output.match(/available databases.*?:\s*([\s\S]*?)(?=\n\[|\n$)/i);
7070
+ if (dbListMatch) {
7071
+ intel.push(`\u{1F4CA} Databases: ${dbListMatch[1].trim().replace(/\n/g, ", ")}`);
7072
+ }
7073
+ const tableMatches = output.match(/Database:\s*\S+\s+Table:\s*\S+/gi);
7074
+ if (tableMatches) {
7075
+ intel.push(`\u{1F4CB} Tables dumped: ${tableMatches.length}`);
7076
+ }
7077
+ return intel;
7078
+ }
7079
+ function extractHashIntel(output) {
7080
+ const intel = [];
7081
+ const lines = output.split("\n");
7082
+ const md5Hashes = lines.filter((l) => /\b[a-f0-9]{32}\b/i.test(l) && !l.includes("{"));
7083
+ const sha256Hashes = lines.filter((l) => /\b[a-f0-9]{64}\b/i.test(l));
7084
+ const unixHashes = lines.filter((l) => /\$[0-9]\$|\$2[aby]\$|\$6\$|\$5\$/i.test(l));
7085
+ const ntlmHashes = lines.filter((l) => /:[0-9]+:[a-f0-9]{32}:[a-f0-9]{32}:::/i.test(l));
7086
+ if (md5Hashes.length > 0) intel.push(`#\uFE0F\u20E3 MD5 hashes: ${md5Hashes.length}`);
7087
+ if (sha256Hashes.length > 0) intel.push(`#\uFE0F\u20E3 SHA256 hashes: ${sha256Hashes.length}`);
7088
+ if (unixHashes.length > 0) intel.push(`#\uFE0F\u20E3 Unix crypt hashes: ${unixHashes.length}`);
7089
+ if (ntlmHashes.length > 0) {
7090
+ intel.push(`#\uFE0F\u20E3 NTLM hashes: ${ntlmHashes.length}`);
7091
+ ntlmHashes.slice(0, 5).forEach((h) => intel.push(` ${h.trim().slice(0, 100)}`));
7092
+ }
7093
+ return intel;
7094
+ }
7095
+ function extractGenericIntel(output) {
7096
+ const intel = [];
7097
+ const credPatterns = output.match(/(?:password|passwd|pwd|credentials?)\s*[=:]\s*\S+/gi);
7098
+ if (credPatterns) {
7099
+ intel.push("\u{1F511} Potential credentials detected:");
7100
+ credPatterns.slice(0, 5).forEach((c) => intel.push(` ${c.trim()}`));
7101
+ }
7102
+ const cves = output.match(/CVE-\d{4}-\d+/gi);
7103
+ if (cves) {
7104
+ const unique = [...new Set(cves)];
7105
+ intel.push(`\u26A0\uFE0F CVEs mentioned: ${unique.join(", ")}`);
7106
+ }
7107
+ 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);
7108
+ if (ips) {
7109
+ const uniqueIps = [...new Set(ips)].filter(
7110
+ (ip) => !ip.startsWith("0.") && !ip.startsWith("255.") && ip !== "127.0.0.1"
7111
+ );
7112
+ if (uniqueIps.length > 0 && uniqueIps.length <= 20) {
7113
+ intel.push(`\u{1F310} IP addresses found: ${uniqueIps.join(", ")}`);
7114
+ }
7115
+ }
7116
+ const paths = output.match(/\/(?:etc\/(?:shadow|passwd|sudoers)|root\/|home\/\S+|var\/www\/\S+|opt\/\S+)\S*/g);
7117
+ if (paths) {
7118
+ const uniquePaths = [...new Set(paths)].slice(0, 10);
7119
+ intel.push(`\u{1F4C2} Interesting paths: ${uniquePaths.join(", ")}`);
7120
+ }
7121
+ const flagPatterns = output.match(/(?:flag|secret|key|token)\{[^}]+\}/gi);
7122
+ if (flagPatterns) {
7123
+ intel.push("\u{1F3F4} FLAG/SECRET patterns detected:");
7124
+ flagPatterns.forEach((f) => intel.push(` ${f}`));
7125
+ }
7126
+ return intel;
7127
+ }
7128
+
7129
+ // src/shared/utils/context-digest.ts
7130
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync2, existsSync as existsSync6 } from "fs";
7131
+ var PASSTHROUGH_THRESHOLD = 3e3;
7132
+ var LAYER2_THRESHOLD = 8e3;
7133
+ var LAYER3_THRESHOLD = 5e4;
7134
+ var MAX_REDUCED_LINES = 500;
7135
+ var getOutputDir = () => WORKSPACE.OUTPUTS;
7136
+ var MAX_DUPLICATE_DISPLAY = 3;
7137
+ var LAYER3_MAX_INPUT_CHARS = 8e4;
7138
+ var FALLBACK_MAX_CHARS = 3e4;
7139
+ var SIGNAL_PATTERNS = [
7140
+ /error|fail|denied|refused|timeout|exception/i,
7141
+ /warning|warn|deprecated|insecure/i,
7142
+ /success|found|detected|discovered|vulnerable|VULNERABLE/i,
7143
+ /password|passwd|credential|secret|key|token|hash/i,
7144
+ /CVE-\d{4}-\d+/i,
7145
+ /\d+\/\w+\s+open\s+/,
7146
+ // nmap open port
7147
+ /flag\{|ctf\{|HTB\{|THM\{/i,
7148
+ // CTF flags
7149
+ /root:|admin:|www-data:|nobody:/,
7150
+ // /etc/passwd entries
7151
+ /\$[0-9]\$|\$2[aby]\$/,
7152
+ // password hashes
7153
+ /\b(?:192\.168|10\.|172\.(?:1[6-9]|2\d|3[01]))\.\d+\.\d+\b/,
7154
+ // internal IPs
7155
+ /BEGIN\s+(?:RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY/i,
7156
+ // private keys
7157
+ /Authorization:|Bearer\s+|Basic\s+/i
7158
+ // auth tokens
7159
+ ];
7160
+ var NOISE_PATTERNS = [
7161
+ /^\s*$/,
7162
+ // blank lines
7163
+ /^\[[\d:]+\]\s*$/,
7164
+ // timestamp-only lines
7165
+ /^#+\s*$/,
7166
+ // separator lines
7167
+ /^\s*Progress:\s*\[?[#=\-\s>]+\]?\s*\d+/i,
7168
+ // progress bars
7169
+ /\d+\s+requests?\s+(?:sent|made)/i,
7170
+ // request counters
7171
+ /^\s*(?:\.{3,}|={5,}|-{5,})\s*$/
7172
+ // decoration lines
7173
+ ];
7174
+ async function digestToolOutput(output, toolName, toolInput, llmDigestFn) {
7175
+ const originalLength = output.length;
7176
+ if (originalLength < PASSTHROUGH_THRESHOLD) {
7177
+ return {
7178
+ digestedOutput: output,
7179
+ fullOutputPath: null,
7180
+ layersApplied: [],
7181
+ originalLength,
7182
+ digestedLength: originalLength,
7183
+ compressionRatio: 1
7184
+ };
7185
+ }
7186
+ const layersApplied = [];
7187
+ let processed = output;
7188
+ let savedOutputPath = null;
7189
+ processed = compressToolOutput(processed, toolName);
7190
+ layersApplied.push(1);
7191
+ if (processed.length > LAYER2_THRESHOLD) {
7192
+ processed = structuralReduce(processed);
7193
+ layersApplied.push(2);
7194
+ }
7195
+ if (processed.length > LAYER3_THRESHOLD) {
7196
+ savedOutputPath = saveFullOutput(output, toolName);
7197
+ if (llmDigestFn) {
7198
+ try {
7199
+ const context = `Tool: ${toolName}${toolInput ? ` | Input: ${toolInput}` : ""}`;
7200
+ const digest = await llmDigestFn(processed, context);
7201
+ processed = formatLLMDigest(digest, savedOutputPath, originalLength);
7202
+ layersApplied.push(3);
7203
+ } catch (err) {
7204
+ debugLog("general", "Context Digest Layer 3 failed, falling back", { toolName, error: String(err) });
7205
+ processed = formatFallbackDigest(processed, savedOutputPath, originalLength);
7206
+ layersApplied.push(3);
7207
+ }
7208
+ } else {
7209
+ processed = formatFallbackDigest(processed, savedOutputPath, originalLength);
7210
+ }
7211
+ } else if (layersApplied.includes(2)) {
7212
+ savedOutputPath = saveFullOutput(output, toolName);
7213
+ }
7214
+ return {
7215
+ digestedOutput: processed,
7216
+ fullOutputPath: savedOutputPath,
7217
+ layersApplied,
7218
+ originalLength,
7219
+ digestedLength: processed.length,
7220
+ compressionRatio: processed.length / originalLength
7221
+ };
7222
+ }
7223
+ function structuralReduce(output) {
7224
+ let cleaned = stripAnsi(output);
7225
+ const lines = cleaned.split("\n");
7226
+ const result2 = [];
7227
+ const duplicateCounts = /* @__PURE__ */ new Map();
7228
+ let lastLine = "";
7229
+ let consecutiveDupes = 0;
7230
+ for (const line of lines) {
7231
+ const trimmed = line.trim();
7232
+ if (NOISE_PATTERNS.some((p) => p.test(trimmed))) {
7233
+ continue;
7234
+ }
7235
+ const isSignal = SIGNAL_PATTERNS.some((p) => p.test(trimmed));
7236
+ const normalized = normalizeLine(trimmed);
7237
+ if (normalized === normalizeLine(lastLine) && !isSignal) {
7238
+ consecutiveDupes++;
7239
+ duplicateCounts.set(normalized, (duplicateCounts.get(normalized) || 1) + 1);
7240
+ continue;
7241
+ }
7242
+ if (consecutiveDupes > 0) {
7243
+ if (consecutiveDupes <= MAX_DUPLICATE_DISPLAY) {
7244
+ for (let i = 0; i < consecutiveDupes; i++) {
7245
+ result2.push(lastLine);
7246
+ }
7247
+ } else {
7248
+ result2.push(` ... (${consecutiveDupes} similar lines collapsed)`);
7249
+ }
7250
+ consecutiveDupes = 0;
7251
+ }
7252
+ result2.push(line);
7253
+ lastLine = trimmed;
7254
+ }
7255
+ if (consecutiveDupes > MAX_DUPLICATE_DISPLAY) {
7256
+ result2.push(` ... (${consecutiveDupes} similar lines collapsed)`);
7257
+ } else {
7258
+ for (let i = 0; i < consecutiveDupes; i++) {
7259
+ result2.push(lastLine);
7260
+ }
7261
+ }
7262
+ if (result2.length > MAX_REDUCED_LINES) {
7263
+ const headSize = Math.floor(MAX_REDUCED_LINES * 0.4);
7264
+ const tailSize = Math.floor(MAX_REDUCED_LINES * 0.3);
7265
+ const signalBudget = MAX_REDUCED_LINES - headSize - tailSize;
7266
+ const head = result2.slice(0, headSize);
7267
+ const tail = result2.slice(-tailSize);
7268
+ const middle = result2.slice(headSize, -tailSize);
7269
+ const middleSignals = middle.filter((line) => SIGNAL_PATTERNS.some((p) => p.test(line))).slice(0, signalBudget);
7270
+ const skipped = middle.length - middleSignals.length;
7271
+ cleaned = [
7272
+ ...head,
7273
+ "",
7274
+ `... [${skipped} routine lines skipped \u2014 ${middleSignals.length} important lines preserved] ...`,
7275
+ "",
7276
+ ...middleSignals,
7277
+ "",
7278
+ `... [resuming last ${tailSize} lines] ...`,
7279
+ "",
7280
+ ...tail
7281
+ ].join("\n");
7282
+ } else {
7283
+ cleaned = result2.join("\n");
7284
+ }
7285
+ return cleaned;
7286
+ }
7287
+ var DIGEST_SYSTEM_PROMPT = `You are a pentesting output analyst. Given raw tool output, extract ONLY actionable intelligence. Be terse and structured.
7288
+
7289
+ FORMAT YOUR RESPONSE EXACTLY LIKE THIS:
7290
+ ## Key Findings
7291
+ - [finding 1]
7292
+ - [finding 2]
7293
+
7294
+ ## Credentials/Secrets
7295
+ - [any discovered credentials, hashes, tokens, keys]
7296
+
7297
+ ## Attack Vectors
7298
+ - [exploitable services, vulnerabilities, misconfigurations]
7299
+
7300
+ ## Next Steps
7301
+ - [recommended immediate actions based on findings]
7302
+
7303
+ RULES:
7304
+ - Be EXTREMELY concise \u2014 max 30 lines total
7305
+ - Only include ACTIONABLE findings \u2014 skip routine/expected results
7306
+ - If nothing interesting found, say "No actionable findings in this output"
7307
+ - Include exact values: port numbers, versions, usernames, file paths
7308
+ - Never include decorative output, banners, or progress information`;
7309
+ function formatLLMDigest(digest, filePath, originalChars) {
7310
+ return [
7311
+ "\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",
7312
+ "\u2551 CONTEXT DIGEST (LLM-summarized) \u2551",
7313
+ "\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",
7314
+ "",
7315
+ digest,
7316
+ "",
7317
+ `\u{1F4C2} Full output saved: ${filePath} (${originalChars} chars)`,
7318
+ `\u{1F4A1} Use read_file("${filePath}") to see the complete raw output if needed.`
7319
+ ].join("\n");
7320
+ }
7321
+ function formatFallbackDigest(processed, filePath, originalChars) {
7322
+ const lines = processed.split("\n");
7323
+ const summaryEndIdx = lines.findIndex((l) => l.includes("END SUMMARY"));
7324
+ const summaryBlock = summaryEndIdx > 0 ? lines.slice(0, summaryEndIdx + 1).join("\n") : "";
7325
+ const remaining = summaryEndIdx > 0 ? lines.slice(summaryEndIdx + 1).join("\n") : processed;
7326
+ const maxChars = FALLBACK_MAX_CHARS;
7327
+ let truncatedRemaining = remaining;
7328
+ if (remaining.length > maxChars) {
7329
+ const headChars = Math.floor(maxChars * 0.6);
7330
+ const tailChars = Math.floor(maxChars * 0.4);
7331
+ const skipped = remaining.length - headChars - tailChars;
7332
+ truncatedRemaining = remaining.slice(0, headChars) + `
7333
+
7334
+ ... [${skipped} chars omitted \u2014 read full output from file] ...
7335
+
7336
+ ` + remaining.slice(-tailChars);
7337
+ }
7338
+ return [
7339
+ summaryBlock,
7340
+ truncatedRemaining,
7341
+ "",
7342
+ `\u{1F4C2} Full output saved: ${filePath} (${originalChars} chars)`,
7343
+ `\u{1F4A1} Use read_file("${filePath}") to see the complete raw output.`
7344
+ ].filter(Boolean).join("\n");
7345
+ }
7346
+ function stripAnsi(text) {
7347
+ return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1B\[[\d;]*m/g, "");
7348
+ }
7349
+ function normalizeLine(line) {
7350
+ return line.replace(/\d+/g, "N").replace(/0x[a-f0-9]+/gi, "H").replace(/\s+/g, " ").trim().toLowerCase();
7351
+ }
7352
+ function saveFullOutput(output, toolName) {
7353
+ try {
7354
+ const outputDir = getOutputDir();
7355
+ if (!existsSync6(outputDir)) {
7356
+ mkdirSync2(outputDir, { recursive: true });
7357
+ }
7358
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
7359
+ const safeName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 30);
7360
+ const filePath = `${outputDir}/${timestamp}_${safeName}.txt`;
7361
+ writeFileSync6(filePath, output, "utf-8");
7362
+ return filePath;
7363
+ } catch (err) {
7364
+ debugLog("general", "Failed to save full output to file", { toolName, error: String(err) });
7365
+ return "(failed to save \u2014 output too large or disk full)";
7366
+ }
7367
+ }
7368
+ function createLLMDigestFn(llmClient) {
7369
+ return async (text, context) => {
7370
+ const truncatedText = text.length > LAYER3_MAX_INPUT_CHARS ? text.slice(0, LAYER3_MAX_INPUT_CHARS) + `
7371
+ ... [truncated for summarization, ${text.length - LAYER3_MAX_INPUT_CHARS} chars omitted]` : text;
7372
+ const messages = [{ role: "user", content: `Analyze this pentesting tool output and extract actionable intelligence.
7373
+
7374
+ Context: ${context}
7375
+
7376
+ --- OUTPUT START ---
7377
+ ${truncatedText}
7378
+ --- OUTPUT END ---` }];
7379
+ const response = await llmClient.generateResponse(
7380
+ messages,
7381
+ void 0,
7382
+ // no tools — summarization only
7383
+ DIGEST_SYSTEM_PROMPT
7384
+ );
7385
+ return response.content || "No actionable findings extracted.";
7386
+ };
7387
+ }
7388
+
6768
7389
  // src/agents/core-agent.ts
6769
7390
  var CoreAgent = class _CoreAgent {
6770
7391
  llm;
@@ -7154,14 +7775,7 @@ Please decide how to handle this error and continue.`;
7154
7775
  input: call.input
7155
7776
  });
7156
7777
  let outputText = result2.output ?? "";
7157
- if (outputText.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
7158
- const truncated = outputText.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
7159
- const remaining = outputText.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
7160
- outputText = `${truncated}
7161
-
7162
- ... [TRUNCATED ${remaining} characters for context hygiene] ...
7163
- \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.`;
7164
- }
7778
+ this.scanForFlags(outputText);
7165
7779
  if (result2.error) {
7166
7780
  outputText = this.enrichToolError({ toolName: call.name, input: call.input, error: result2.error, originalOutput: outputText, progress });
7167
7781
  if (progress) progress.toolErrors++;
@@ -7171,8 +7785,26 @@ Please decide how to handle this error and continue.`;
7171
7785
  progress.blockedCommandPatterns.clear();
7172
7786
  }
7173
7787
  }
7788
+ try {
7789
+ const llmDigestFn = createLLMDigestFn(this.llm);
7790
+ const digestResult = await digestToolOutput(
7791
+ outputText,
7792
+ call.name,
7793
+ JSON.stringify(call.input).slice(0, 200),
7794
+ llmDigestFn
7795
+ );
7796
+ outputText = digestResult.digestedOutput;
7797
+ } catch {
7798
+ if (outputText.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
7799
+ const truncated = outputText.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
7800
+ const remaining = outputText.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
7801
+ outputText = `${truncated}
7802
+
7803
+ ... [TRUNCATED ${remaining} characters for context hygiene] ...
7804
+ \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.`;
7805
+ }
7806
+ }
7174
7807
  this.emitToolResult(call.name, result2.success, outputText, result2.error, Date.now() - toolStartTime);
7175
- this.scanForFlags(outputText);
7176
7808
  return { toolCallId: call.id, output: outputText, error: result2.error };
7177
7809
  } catch (error) {
7178
7810
  const errorMsg = String(error);
@@ -7248,7 +7880,7 @@ Please decide how to handle this error and continue.`;
7248
7880
  };
7249
7881
 
7250
7882
  // src/agents/prompt-builder.ts
7251
- import { readFileSync as readFileSync4, existsSync as existsSync6, readdirSync as readdirSync2 } from "fs";
7883
+ import { readFileSync as readFileSync4, existsSync as existsSync7, readdirSync as readdirSync2 } from "fs";
7252
7884
  import { join as join9, dirname as dirname5 } from "path";
7253
7885
  import { fileURLToPath as fileURLToPath4 } from "url";
7254
7886
 
@@ -7363,7 +7995,8 @@ var PromptBuilder = class {
7363
7995
  * 5. Scope constraints
7364
7996
  * 6. Current state (targets, findings, loot, active processes)
7365
7997
  * 7. TODO list
7366
- * 8. User context
7998
+ * 8. Time awareness (elapsed + deadline)
7999
+ * 9. User context
7367
8000
  */
7368
8001
  build(userInput, phase) {
7369
8002
  const fragments = [
@@ -7375,6 +8008,7 @@ var PromptBuilder = class {
7375
8008
  this.getScopeFragment(),
7376
8009
  this.getStateFragment(),
7377
8010
  this.getTodoFragment(),
8011
+ this.getTimeFragment(),
7378
8012
  PROMPT_DEFAULTS.USER_CONTEXT(userInput)
7379
8013
  ];
7380
8014
  return fragments.filter((f) => !!f).join("\n\n");
@@ -7395,7 +8029,7 @@ ${content}
7395
8029
  */
7396
8030
  loadPromptFile(filename) {
7397
8031
  const path2 = join9(PROMPTS_DIR, filename);
7398
- return existsSync6(path2) ? readFileSync4(path2, PROMPT_CONFIG.ENCODING) : "";
8032
+ return existsSync7(path2) ? readFileSync4(path2, PROMPT_CONFIG.ENCODING) : "";
7399
8033
  }
7400
8034
  /**
7401
8035
  * Load phase-specific prompt.
@@ -7441,14 +8075,14 @@ ${content}
7441
8075
  * "마크다운 파일 하나를 폴더에 넣으면, PromptBuilder가 자동으로 발견하고 로드한다."
7442
8076
  */
7443
8077
  loadPhaseRelevantTechniques(phase) {
7444
- if (!existsSync6(TECHNIQUES_DIR)) return "";
8078
+ if (!existsSync7(TECHNIQUES_DIR)) return "";
7445
8079
  const priorityTechniques = PHASE_TECHNIQUE_MAP[phase] || [];
7446
8080
  const loadedSet = /* @__PURE__ */ new Set();
7447
8081
  const fragments = [];
7448
8082
  for (const technique of priorityTechniques) {
7449
8083
  const filePath = join9(TECHNIQUES_DIR, `${technique}.md`);
7450
8084
  try {
7451
- if (!existsSync6(filePath)) continue;
8085
+ if (!existsSync7(filePath)) continue;
7452
8086
  const content = readFileSync4(filePath, PROMPT_CONFIG.ENCODING);
7453
8087
  if (content) {
7454
8088
  fragments.push(`<technique-reference category="${technique}">
@@ -7494,6 +8128,11 @@ ${content}
7494
8128
  const list = todo.map((t) => `[${t.status === TODO_STATUSES.DONE ? "x" : " "}] ${t.content} (${t.priority})`).join("\n");
7495
8129
  return PROMPT_XML.TODO(list || PROMPT_DEFAULTS.EMPTY_TODO);
7496
8130
  }
8131
+ getTimeFragment() {
8132
+ return `<time-status>
8133
+ ${this.state.getTimeStatus()}
8134
+ </time-status>`;
8135
+ }
7497
8136
  };
7498
8137
 
7499
8138
  // src/agents/main-agent.ts
@@ -7795,7 +8434,7 @@ var THEME = {
7795
8434
  },
7796
8435
  // Gradients
7797
8436
  gradient: {
7798
- cyber: ["#00d4ff", "#00ff9f"],
8437
+ cyber: ["#38bdf8", "#818cf8", "#c084fc", "#e879f9", "#22d3ee"],
7799
8438
  danger: ["#ef4444", "#7f1d1d"],
7800
8439
  success: ["#22c55e", "#14532d"],
7801
8440
  gold: ["#f59e0b", "#78350f"],
@@ -7807,13 +8446,12 @@ var THEME = {
7807
8446
  identity: "#38bdf8"
7808
8447
  };
7809
8448
  var ASCII_BANNER = `
7810
- \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557
7811
- \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
8449
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557
8450
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
7812
8451
  \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557
7813
8452
  \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
7814
8453
  \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
7815
- \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
7816
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
8454
+ \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
7817
8455
  A U T O N O M O U S S E C U R I T Y A G E N T
7818
8456
  `;
7819
8457
  var ICONS = {
@@ -7980,6 +8618,7 @@ var useAgentState = () => {
7980
8618
  }, []);
7981
8619
  const manageTimer = useCallback((action) => {
7982
8620
  if (action === "start") {
8621
+ if (timerRef.current) clearInterval(timerRef.current);
7983
8622
  startTimeRef.current = Date.now();
7984
8623
  timerRef.current = setInterval(() => {
7985
8624
  setElapsedTime(Math.floor((Date.now() - startTimeRef.current) / 1e3));
@@ -8034,6 +8673,7 @@ var useAgentEvents = (agent, eventsRef, state) => {
8034
8673
  setCurrentStatus,
8035
8674
  setRetryState,
8036
8675
  setCurrentTokens,
8676
+ setInputRequest,
8037
8677
  setStats,
8038
8678
  lastResponseMetaRef,
8039
8679
  retryCountdownRef,
@@ -8086,7 +8726,7 @@ var useAgentEvents = (agent, eventsRef, state) => {
8086
8726
  return new Promise((resolve) => {
8087
8727
  const isPassword = /password|passphrase/i.test(p);
8088
8728
  const inputType = /sudo/i.test(p) ? "sudo_password" : isPassword ? "password" : "text";
8089
- state.setInputRequest({
8729
+ setInputRequest({
8090
8730
  isActive: true,
8091
8731
  prompt: p.trim(),
8092
8732
  isPassword,
@@ -8100,7 +8740,7 @@ var useAgentEvents = (agent, eventsRef, state) => {
8100
8740
  const hiddenTypes = ["password", "sudo_password", "ssh_password", "passphrase", "api_key", "credential"];
8101
8741
  const isPassword = hiddenTypes.includes(request.type);
8102
8742
  const displayPrompt = buildCredentialPrompt(request);
8103
- state.setInputRequest({
8743
+ setInputRequest({
8104
8744
  isActive: true,
8105
8745
  prompt: displayPrompt,
8106
8746
  isPassword,
@@ -8148,6 +8788,7 @@ var useAgentEvents = (agent, eventsRef, state) => {
8148
8788
  setCurrentStatus,
8149
8789
  setRetryState,
8150
8790
  setCurrentTokens,
8791
+ setInputRequest,
8151
8792
  setStats,
8152
8793
  lastResponseMetaRef,
8153
8794
  retryCountdownRef,
@@ -8155,7 +8796,7 @@ var useAgentEvents = (agent, eventsRef, state) => {
8155
8796
  tokenAccumRef,
8156
8797
  lastStepTokensRef,
8157
8798
  clearAllTimers,
8158
- state
8799
+ eventsRef
8159
8800
  ]);
8160
8801
  };
8161
8802
  function formatToolInput(toolName, input) {
@@ -8238,6 +8879,7 @@ var useAgent = (shouldAutoApprove, target) => {
8238
8879
  isProcessing,
8239
8880
  setIsProcessing,
8240
8881
  currentStatus,
8882
+ setCurrentStatus,
8241
8883
  elapsedTime,
8242
8884
  retryState,
8243
8885
  currentTokens,
@@ -8259,7 +8901,7 @@ var useAgent = (shouldAutoApprove, target) => {
8259
8901
  const executeTask = useCallback2(async (task) => {
8260
8902
  setIsProcessing(true);
8261
8903
  manageTimer("start");
8262
- state.setCurrentStatus("Thinking");
8904
+ setCurrentStatus("Thinking");
8263
8905
  resetCumulativeCounters();
8264
8906
  try {
8265
8907
  const response = await agent.execute(task);
@@ -8271,23 +8913,26 @@ var useAgent = (shouldAutoApprove, target) => {
8271
8913
  } finally {
8272
8914
  manageTimer("stop");
8273
8915
  setIsProcessing(false);
8274
- state.setCurrentStatus("");
8916
+ setCurrentStatus("");
8275
8917
  }
8276
- }, [agent, addMessage, manageTimer, resetCumulativeCounters, setIsProcessing, lastResponseMetaRef, state]);
8918
+ }, [agent, addMessage, manageTimer, resetCumulativeCounters, setIsProcessing, lastResponseMetaRef, setCurrentStatus]);
8277
8919
  const abort = useCallback2(() => {
8278
8920
  agent.abort();
8279
8921
  setIsProcessing(false);
8280
8922
  manageTimer("stop");
8281
- state.setCurrentStatus("");
8923
+ setCurrentStatus("");
8282
8924
  addMessage("system", "Interrupted");
8283
- }, [agent, addMessage, manageTimer, setIsProcessing, state]);
8925
+ }, [agent, addMessage, manageTimer, setIsProcessing, setCurrentStatus]);
8926
+ const inputRequestRef = useRef2(inputRequest);
8927
+ inputRequestRef.current = inputRequest;
8284
8928
  const cancelInputRequest = useCallback2(() => {
8285
- if (inputRequest.isActive && inputRequest.resolve) {
8286
- inputRequest.resolve(null);
8929
+ const ir = inputRequestRef.current;
8930
+ if (ir.isActive && ir.resolve) {
8931
+ ir.resolve(null);
8287
8932
  setInputRequest({ isActive: false, prompt: "", isPassword: false, resolve: null });
8288
8933
  addMessage("system", "Input cancelled");
8289
8934
  }
8290
- }, [inputRequest, setInputRequest, addMessage]);
8935
+ }, [setInputRequest, addMessage]);
8291
8936
  return {
8292
8937
  agent,
8293
8938
  messages,
@@ -8683,13 +9328,16 @@ var App = ({ autoApprove = false, target }) => {
8683
9328
  cancelInputRequest,
8684
9329
  addMessage
8685
9330
  } = useAgent(autoApproveMode, target);
9331
+ const inputRequestRef = useRef3(inputRequest);
9332
+ inputRequestRef.current = inputRequest;
8686
9333
  const handleExit = useCallback3(() => {
8687
- if (inputRequest.isActive && inputRequest.resolve) inputRequest.resolve(null);
9334
+ const ir = inputRequestRef.current;
9335
+ if (ir.isActive && ir.resolve) ir.resolve(null);
8688
9336
  cleanupAllProcesses().catch(() => {
8689
9337
  });
8690
9338
  exit();
8691
9339
  setTimeout(() => process.exit(0), DISPLAY_LIMITS.EXIT_DELAY);
8692
- }, [exit, inputRequest, cancelInputRequest]);
9340
+ }, [exit]);
8693
9341
  const handleCommand = useCallback3(async (cmd, args) => {
8694
9342
  switch (cmd) {
8695
9343
  case UI_COMMANDS.HELP:
@@ -8789,21 +9437,22 @@ ${procData.stdout || "(no output)"}
8789
9437
  }
8790
9438
  }, [addMessage, executeTask, handleCommand]);
8791
9439
  const handleSecretSubmit = useCallback3((value) => {
8792
- if (!inputRequest.isActive || !inputRequest.resolve) return;
8793
- const displayText = inputRequest.isPassword ? "\u2022".repeat(value.length) : value;
8794
- const promptLabel = inputRequest.prompt || "Input";
9440
+ const ir = inputRequestRef.current;
9441
+ if (!ir.isActive || !ir.resolve) return;
9442
+ const displayText = ir.isPassword ? "\u2022".repeat(value.length) : value;
9443
+ const promptLabel = ir.prompt || "Input";
8795
9444
  addMessage("system", `\u21B3 ${promptLabel} ${displayText}`);
8796
- inputRequest.resolve(value);
9445
+ ir.resolve(value);
8797
9446
  setInputRequest({ isActive: false, prompt: "", isPassword: false, resolve: null });
8798
9447
  setSecretInput("");
8799
- }, [inputRequest, addMessage, setInputRequest]);
8800
- useInput((ch, key) => {
9448
+ }, [addMessage, setInputRequest]);
9449
+ useInput(useCallback3((ch, key) => {
8801
9450
  if (key.escape) {
8802
- if (inputRequest.isActive) cancelInputRequest();
9451
+ if (inputRequestRef.current.isActive) cancelInputRequest();
8803
9452
  else if (isProcessing) abort();
8804
9453
  }
8805
9454
  if (key.ctrl && ch === "c") handleExit();
8806
- });
9455
+ }, [cancelInputRequest, isProcessing, abort, handleExit]));
8807
9456
  useEffect4(() => {
8808
9457
  const onSignal = () => handleExit();
8809
9458
  process.on("SIGINT", onSignal);