pentesting 0.47.4 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,15 +33,8 @@ Pentesting support tool
33
33
 
34
34
  ## Quick Start with Docker (Recommended)
35
35
 
36
- ```bash
37
- # One-time use (data deleted after exit)
38
- docker run -it --rm \
39
- -e PENTEST_API_KEY="your_glm_api_key" \
40
- -e PENTEST_BASE_URL="https://open.bigmodel.cn/api/paas/v4" \
41
- -e PENTEST_MODEL="glm-5" \
42
- agnusdei1207/pentesting
43
36
 
44
- # Persistent data (saved to ./pentest-data/)
37
+ ```bash
45
38
  docker run -it --rm \
46
39
  -e PENTEST_API_KEY="your_glm_api_key" \
47
40
  -e PENTEST_BASE_URL="https://open.bigmodel.cn/api/paas/v4" \
@@ -50,8 +43,6 @@ docker run -it --rm \
50
43
  agnusdei1207/pentesting
51
44
  ```
52
45
 
53
- Web search is automatically configured to use GLM Web Search with your `PENTEST_API_KEY`.
54
-
55
46
  ### Using Brave Search
56
47
 
57
48
  ```bash
@@ -65,38 +56,6 @@ docker run -it --rm \
65
56
  agnusdei1207/pentesting
66
57
  ```
67
58
 
68
- Get Brave Search API key at: https://brave.com/search/api/
69
-
70
- ### Using Serper (Google Search)
71
-
72
- ```bash
73
- docker run -it --rm \
74
- -e PENTEST_API_KEY="your_glm_api_key" \
75
- -e PENTEST_BASE_URL="https://open.bigmodel.cn/api/paas/v4" \
76
- -e PENTEST_MODEL="glm-5" \
77
- -e SEARCH_API_KEY="your_serper_api_key" \
78
- -e SEARCH_API_URL="https://google.serper.dev/search" \
79
- -v ./pentest-data:/root/.pentest \
80
- agnusdei1207/pentesting
81
- ```
82
-
83
- Get Serper API key at: https://serper.dev/
84
-
85
- ## Environment Variables
86
-
87
- | Variable | Required | Default | Description |
88
- |----------|----------|---------|-------------|
89
- | `PENTEST_API_KEY` | ✅ Yes | - | LLM API key (also used for web search if `SEARCH_API_KEY` not set) |
90
- | `PENTEST_BASE_URL` | No | - | Custom API endpoint URL |
91
- | `PENTEST_MODEL` | No | - | Model name (e.g., `glm-5`) |
92
- | `SEARCH_API_KEY` | No | Uses `PENTEST_API_KEY` | Web search API key (optional, falls back to main key) |
93
- | `SEARCH_API_URL` | No | GLM Web Search | Web search API URL |
94
-
95
- ### Web Search Defaults
96
-
97
- - **Default**: GLM Web Search (`https://open.bigmodel.cn/api/paas/v4/tools/web-search-pro`)
98
- - **API Key**: Falls back to `PENTEST_API_KEY` if `SEARCH_API_KEY` not set
99
-
100
59
  ## Issue
101
60
 
102
61
  email: agnusdei1207@gmail.com
package/dist/main.js CHANGED
@@ -331,7 +331,7 @@ var ORPHAN_PROCESS_NAMES = [
331
331
 
332
332
  // src/shared/constants/agent.ts
333
333
  var APP_NAME = "Pentest AI";
334
- var APP_VERSION = "0.47.4";
334
+ var APP_VERSION = "0.48.1";
335
335
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
336
336
  var LLM_ROLES = {
337
337
  SYSTEM: "system",
@@ -698,6 +698,12 @@ var ATTACK_TACTICS = {
698
698
  C2: "command_and_control",
699
699
  IMPACT: "impact"
700
700
  };
701
+ var ATTACK_VALUE_RANK = {
702
+ HIGH: 3,
703
+ MED: 2,
704
+ LOW: 1,
705
+ NONE: 0
706
+ };
701
707
  var APPROVAL_STATUSES = {
702
708
  AUTO: "auto",
703
709
  USER_CONFIRMED: "user_confirmed",
@@ -816,10 +822,6 @@ var SECONDS_PER_HOUR = 3600;
816
822
 
817
823
  // src/shared/constants/paths.ts
818
824
  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
825
  var PENTESTING_ROOT = ".pentesting";
824
826
  var WORK_DIR = `${PENTESTING_ROOT}/tmp`;
825
827
  var MEMORY_DIR = `${PENTESTING_ROOT}/memory`;
@@ -829,6 +831,7 @@ var LOOT_DIR = `${PENTESTING_ROOT}/loot`;
829
831
  var OUTPUTS_DIR = `${PENTESTING_ROOT}/outputs`;
830
832
  var DEBUG_DIR = `${PENTESTING_ROOT}/debug`;
831
833
  var JOURNAL_DIR = `${PENTESTING_ROOT}/journal`;
834
+ var TURNS_DIR = `${PENTESTING_ROOT}/memory/turns`;
832
835
  var WORKSPACE = {
833
836
  /** Root directory */
834
837
  get ROOT() {
@@ -865,6 +868,10 @@ var WORKSPACE = {
865
868
  /** Persistent per-turn journal (§13 memo system) */
866
869
  get JOURNAL() {
867
870
  return path.resolve(JOURNAL_DIR);
871
+ },
872
+ /** Turn record files */
873
+ get TURNS() {
874
+ return path.resolve(TURNS_DIR);
868
875
  }
869
876
  };
870
877
 
@@ -7151,8 +7158,8 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7151
7158
  }
7152
7159
  },
7153
7160
  execute: async (p) => {
7154
- const { existsSync: existsSync11, statSync: statSync3, readdirSync: readdirSync4 } = await import("fs");
7155
- const { join: join13 } = await import("path");
7161
+ const { existsSync: existsSync12, statSync: statSync3, readdirSync: readdirSync4 } = await import("fs");
7162
+ const { join: join14 } = await import("path");
7156
7163
  const category = p.category || "";
7157
7164
  const search = p.search || "";
7158
7165
  const minSize = p.min_size || 0;
@@ -7198,7 +7205,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7198
7205
  results.push("");
7199
7206
  };
7200
7207
  const scanDir = (dirPath, maxDepth = 3, depth = 0) => {
7201
- if (depth > maxDepth || !existsSync11(dirPath)) return;
7208
+ if (depth > maxDepth || !existsSync12(dirPath)) return;
7202
7209
  let entries;
7203
7210
  try {
7204
7211
  entries = readdirSync4(dirPath, { withFileTypes: true });
@@ -7207,7 +7214,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7207
7214
  }
7208
7215
  for (const entry of entries) {
7209
7216
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
7210
- const fullPath = join13(dirPath, entry.name);
7217
+ const fullPath = join14(dirPath, entry.name);
7211
7218
  if (entry.isDirectory()) {
7212
7219
  scanDir(fullPath, maxDepth, depth + 1);
7213
7220
  continue;
@@ -7584,8 +7591,8 @@ Requires root/sudo privileges.`,
7584
7591
  const iface = p.interface || "";
7585
7592
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
7586
7593
  const hostsFile = createTempFile(FILE_EXTENSIONS.HOSTS);
7587
- const { writeFileSync: writeFileSync9 } = await import("fs");
7588
- writeFileSync9(hostsFile, `${spoofIp} ${domain}
7594
+ const { writeFileSync: writeFileSync10 } = await import("fs");
7595
+ writeFileSync10(hostsFile, `${spoofIp} ${domain}
7589
7596
  ${spoofIp} *.${domain}
7590
7597
  `);
7591
7598
  const ifaceFlag = iface ? `-i ${iface}` : "";
@@ -8962,80 +8969,80 @@ var ServiceParser = class {
8962
8969
 
8963
8970
  // src/domains/registry.ts
8964
8971
  import { join as join7, dirname as dirname3 } from "path";
8965
- import { fileURLToPath as fileURLToPath2 } from "url";
8966
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
8972
+ import { fileURLToPath } from "url";
8973
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
8967
8974
  var DOMAINS = {
8968
8975
  [SERVICE_CATEGORIES.NETWORK]: {
8969
8976
  id: SERVICE_CATEGORIES.NETWORK,
8970
8977
  name: "Network Infrastructure",
8971
8978
  description: "Vulnerability scanning, port mapping, and network service exploitation.",
8972
- promptPath: join7(__dirname2, "network/prompt.md")
8979
+ promptPath: join7(__dirname, "network/prompt.md")
8973
8980
  },
8974
8981
  [SERVICE_CATEGORIES.WEB]: {
8975
8982
  id: SERVICE_CATEGORIES.WEB,
8976
8983
  name: "Web Application",
8977
8984
  description: "Web app security testing, injection attacks, and auth bypass.",
8978
- promptPath: join7(__dirname2, "web/prompt.md")
8985
+ promptPath: join7(__dirname, "web/prompt.md")
8979
8986
  },
8980
8987
  [SERVICE_CATEGORIES.DATABASE]: {
8981
8988
  id: SERVICE_CATEGORIES.DATABASE,
8982
8989
  name: "Database Security",
8983
8990
  description: "SQL injection, database enumeration, and data extraction.",
8984
- promptPath: join7(__dirname2, "database/prompt.md")
8991
+ promptPath: join7(__dirname, "database/prompt.md")
8985
8992
  },
8986
8993
  [SERVICE_CATEGORIES.AD]: {
8987
8994
  id: SERVICE_CATEGORIES.AD,
8988
8995
  name: "Active Directory",
8989
8996
  description: "Kerberos, LDAP, and Windows domain privilege escalation.",
8990
- promptPath: join7(__dirname2, "ad/prompt.md")
8997
+ promptPath: join7(__dirname, "ad/prompt.md")
8991
8998
  },
8992
8999
  [SERVICE_CATEGORIES.EMAIL]: {
8993
9000
  id: SERVICE_CATEGORIES.EMAIL,
8994
9001
  name: "Email Services",
8995
9002
  description: "SMTP, IMAP, POP3 security and user enumeration.",
8996
- promptPath: join7(__dirname2, "email/prompt.md")
9003
+ promptPath: join7(__dirname, "email/prompt.md")
8997
9004
  },
8998
9005
  [SERVICE_CATEGORIES.REMOTE_ACCESS]: {
8999
9006
  id: SERVICE_CATEGORIES.REMOTE_ACCESS,
9000
9007
  name: "Remote Access",
9001
9008
  description: "SSH, RDP, VNC and other remote control protocols.",
9002
- promptPath: join7(__dirname2, "remote-access/prompt.md")
9009
+ promptPath: join7(__dirname, "remote-access/prompt.md")
9003
9010
  },
9004
9011
  [SERVICE_CATEGORIES.FILE_SHARING]: {
9005
9012
  id: SERVICE_CATEGORIES.FILE_SHARING,
9006
9013
  name: "File Sharing",
9007
9014
  description: "SMB, NFS, FTP and shared resource security.",
9008
- promptPath: join7(__dirname2, "file-sharing/prompt.md")
9015
+ promptPath: join7(__dirname, "file-sharing/prompt.md")
9009
9016
  },
9010
9017
  [SERVICE_CATEGORIES.CLOUD]: {
9011
9018
  id: SERVICE_CATEGORIES.CLOUD,
9012
9019
  name: "Cloud Infrastructure",
9013
9020
  description: "AWS, Azure, and GCP security and misconfiguration.",
9014
- promptPath: join7(__dirname2, "cloud/prompt.md")
9021
+ promptPath: join7(__dirname, "cloud/prompt.md")
9015
9022
  },
9016
9023
  [SERVICE_CATEGORIES.CONTAINER]: {
9017
9024
  id: SERVICE_CATEGORIES.CONTAINER,
9018
9025
  name: "Container Systems",
9019
9026
  description: "Docker and Kubernetes security testing.",
9020
- promptPath: join7(__dirname2, "container/prompt.md")
9027
+ promptPath: join7(__dirname, "container/prompt.md")
9021
9028
  },
9022
9029
  [SERVICE_CATEGORIES.API]: {
9023
9030
  id: SERVICE_CATEGORIES.API,
9024
9031
  name: "API Security",
9025
9032
  description: "REST, GraphQL, and SOAP API security testing.",
9026
- promptPath: join7(__dirname2, "api/prompt.md")
9033
+ promptPath: join7(__dirname, "api/prompt.md")
9027
9034
  },
9028
9035
  [SERVICE_CATEGORIES.WIRELESS]: {
9029
9036
  id: SERVICE_CATEGORIES.WIRELESS,
9030
9037
  name: "Wireless Networks",
9031
9038
  description: "WiFi and Bluetooth security testing.",
9032
- promptPath: join7(__dirname2, "wireless/prompt.md")
9039
+ promptPath: join7(__dirname, "wireless/prompt.md")
9033
9040
  },
9034
9041
  [SERVICE_CATEGORIES.ICS]: {
9035
9042
  id: SERVICE_CATEGORIES.ICS,
9036
9043
  name: "Industrial Systems",
9037
9044
  description: "Critical infrastructure - Modbus, DNP3, ENIP.",
9038
- promptPath: join7(__dirname2, "ics/prompt.md")
9045
+ promptPath: join7(__dirname, "ics/prompt.md")
9039
9046
  }
9040
9047
  };
9041
9048
 
@@ -9675,10 +9682,10 @@ function logLLM(message, data) {
9675
9682
  }
9676
9683
 
9677
9684
  // src/engine/orchestrator/orchestrator.ts
9678
- import { fileURLToPath as fileURLToPath3 } from "url";
9685
+ import { fileURLToPath as fileURLToPath2 } from "url";
9679
9686
  import { dirname as dirname4, join as join8 } from "path";
9680
- var __filename2 = fileURLToPath3(import.meta.url);
9681
- var __dirname3 = dirname4(__filename2);
9687
+ var __filename = fileURLToPath2(import.meta.url);
9688
+ var __dirname2 = dirname4(__filename);
9682
9689
 
9683
9690
  // src/engine/state-persistence.ts
9684
9691
  import { writeFileSync as writeFileSync6, readFileSync as readFileSync4, existsSync as existsSync6, readdirSync, statSync, unlinkSync as unlinkSync4, rmSync } from "fs";
@@ -9957,7 +9964,30 @@ var NOISE_PATTERNS = [
9957
9964
  ];
9958
9965
  function structuralPreprocess(output) {
9959
9966
  let cleaned = stripAnsi(output);
9960
- const lines = cleaned.split("\n");
9967
+ const filteredLines = filterAndDedup(cleaned.split("\n"));
9968
+ if (filteredLines.length > MAX_PREPROCESSED_LINES) {
9969
+ const headSize = Math.floor(MAX_PREPROCESSED_LINES * 0.5);
9970
+ const tailSize = Math.floor(MAX_PREPROCESSED_LINES * 0.3);
9971
+ const head = filteredLines.slice(0, headSize);
9972
+ const tail = filteredLines.slice(-tailSize);
9973
+ const skipped = filteredLines.length - headSize - tailSize;
9974
+ cleaned = [
9975
+ ...head,
9976
+ "",
9977
+ `... [${skipped} lines skipped for Analyst LLM context \u2014 full output saved to file] ...`,
9978
+ "",
9979
+ ...tail
9980
+ ].join("\n");
9981
+ } else {
9982
+ cleaned = filteredLines.join("\n");
9983
+ }
9984
+ if (cleaned.length > ANALYST_MAX_INPUT_CHARS) {
9985
+ cleaned = cleaned.slice(0, ANALYST_MAX_INPUT_CHARS) + `
9986
+ ... [truncated at ${ANALYST_MAX_INPUT_CHARS} chars for Analyst LLM \u2014 full output saved to file]`;
9987
+ }
9988
+ return cleaned;
9989
+ }
9990
+ function filterAndDedup(lines) {
9961
9991
  const result2 = [];
9962
9992
  let lastLine = "";
9963
9993
  let consecutiveDupes = 0;
@@ -9991,27 +10021,7 @@ function structuralPreprocess(output) {
9991
10021
  result2.push(lastLine);
9992
10022
  }
9993
10023
  }
9994
- if (result2.length > MAX_PREPROCESSED_LINES) {
9995
- const headSize = Math.floor(MAX_PREPROCESSED_LINES * 0.5);
9996
- const tailSize = Math.floor(MAX_PREPROCESSED_LINES * 0.3);
9997
- const head = result2.slice(0, headSize);
9998
- const tail = result2.slice(-tailSize);
9999
- const skipped = result2.length - headSize - tailSize;
10000
- cleaned = [
10001
- ...head,
10002
- "",
10003
- `... [${skipped} lines skipped for Analyst LLM context \u2014 full output saved to file] ...`,
10004
- "",
10005
- ...tail
10006
- ].join("\n");
10007
- } else {
10008
- cleaned = result2.join("\n");
10009
- }
10010
- if (cleaned.length > ANALYST_MAX_INPUT_CHARS) {
10011
- cleaned = cleaned.slice(0, ANALYST_MAX_INPUT_CHARS) + `
10012
- ... [truncated at ${ANALYST_MAX_INPUT_CHARS} chars for Analyst LLM \u2014 full output saved to file]`;
10013
- }
10014
- return cleaned;
10024
+ return result2;
10015
10025
  }
10016
10026
  var ANALYST_SYSTEM_PROMPT = `You are an independent pentesting output analyst. You receive raw tool output and must extract ONLY actionable intelligence for the main attack agent.
10017
10027
 
@@ -10260,38 +10270,7 @@ var CoreAgent = class _CoreAgent {
10260
10270
  }
10261
10271
  if (progress.consecutiveIdleIterations >= AGENT_LIMITS.MAX_CONSECUTIVE_IDLE) {
10262
10272
  progress.consecutiveIdleIterations = 0;
10263
- const phase = this.state.getPhase();
10264
- const targets = this.state.getTargets().size;
10265
- const findings = this.state.getFindings().length;
10266
- const phaseDirection = {
10267
- [PHASES.RECON]: `RECON: Scan targets. Enumerate services and versions.`,
10268
- [PHASES.VULN_ANALYSIS]: `VULN ANALYSIS: ${targets} target(s) discovered. Search for CVEs and known exploits.`,
10269
- [PHASES.EXPLOIT]: `EXPLOIT: ${findings} finding(s) available. Attack the highest-severity one.`,
10270
- [PHASES.POST_EXPLOIT]: `POST-EXPLOIT: Escalate privileges. Search for escalation paths.`,
10271
- [PHASES.PRIV_ESC]: `PRIVESC: Find and exploit privilege escalation vectors.`,
10272
- [PHASES.LATERAL]: `LATERAL: Reuse discovered credentials on other hosts.`,
10273
- [PHASES.WEB]: `WEB: Enumerate the attack surface. Test every input for injection.`
10274
- };
10275
- const direction = phaseDirection[phase] || phaseDirection[PHASES.RECON];
10276
- messages.push({
10277
- role: LLM_ROLES.USER,
10278
- content: `\u26A1 DEADLOCK: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} turns with ZERO tool calls.
10279
- Phase: ${phase} | Targets: ${targets} | Findings: ${findings} | Tools executed: ${progress.totalToolsExecuted} (${progress.toolSuccesses}\u2713 ${progress.toolErrors}\u2717)
10280
-
10281
- ${direction}
10282
-
10283
- ESCALATION CHAIN \u2014 follow this order:
10284
- 1. web_search: Search for techniques, bypasses, default creds, CVEs, HackTricks
10285
- 2. BYPASS: Try alternative approaches \u2014 different protocols, ports, encodings, methods
10286
- 3. ZERO-DAY EXPLORATION: Probe for unknown vulns \u2014 fuzz parameters, test edge cases, analyze error responses for leaks
10287
- 4. BRUTE-FORCE: Wordlists, credential stuffing, common passwords, custom password lists from context
10288
- 5. ask_user: ONLY as last resort \u2014 ask the user for hints, wordlists, or guidance
10289
-
10290
- RULES:
10291
- - Every turn MUST have tool calls
10292
- - NEVER silently give up \u2014 exhaust ALL 5 steps above first
10293
- - ACT NOW \u2014 do not plan, do not explain, do not summarize. EXECUTE.`
10294
- });
10273
+ messages.push({ role: LLM_ROLES.USER, content: this.buildDeadlockNudge(progress) });
10295
10274
  }
10296
10275
  } catch (error) {
10297
10276
  if (this.isAbortError(error)) {
@@ -10476,6 +10455,48 @@ ${firstLine}`, phase }
10476
10455
  return callbacks;
10477
10456
  }
10478
10457
  // ─────────────────────────────────────────────────────────────────
10458
+ // SUBSECTION: Deadlock Nudge Builder
10459
+ // ─────────────────────────────────────────────────────────────────
10460
+ /**
10461
+ * Build a deadlock nudge message for the agent.
10462
+ *
10463
+ * WHY separated: The nudge template is ~30 lines of prompt engineering.
10464
+ * Keeping it in run() obscures the iteration control logic.
10465
+ * Philosophy §12: Nudge is a safety net, not a driver —
10466
+ * it reminds the agent to ACT, but never prescribes HOW.
10467
+ */
10468
+ buildDeadlockNudge(progress) {
10469
+ const phase = this.state.getPhase();
10470
+ const targets = this.state.getTargets().size;
10471
+ const findings = this.state.getFindings().length;
10472
+ const phaseDirection = {
10473
+ [PHASES.RECON]: `RECON: Scan targets. Enumerate services and versions.`,
10474
+ [PHASES.VULN_ANALYSIS]: `VULN ANALYSIS: ${targets} target(s) discovered. Search for CVEs and known exploits.`,
10475
+ [PHASES.EXPLOIT]: `EXPLOIT: ${findings} finding(s) available. Attack the highest-severity one.`,
10476
+ [PHASES.POST_EXPLOIT]: `POST-EXPLOIT: Escalate privileges. Search for escalation paths.`,
10477
+ [PHASES.PRIV_ESC]: `PRIVESC: Find and exploit privilege escalation vectors.`,
10478
+ [PHASES.LATERAL]: `LATERAL: Reuse discovered credentials on other hosts.`,
10479
+ [PHASES.WEB]: `WEB: Enumerate the attack surface. Test every input for injection.`
10480
+ };
10481
+ const direction = phaseDirection[phase] || phaseDirection[PHASES.RECON];
10482
+ return `\u26A1 DEADLOCK: ${AGENT_LIMITS.MAX_CONSECUTIVE_IDLE} turns with ZERO tool calls.
10483
+ Phase: ${phase} | Targets: ${targets} | Findings: ${findings} | Tools executed: ${progress.totalToolsExecuted} (${progress.toolSuccesses}\u2713 ${progress.toolErrors}\u2717)
10484
+
10485
+ ${direction}
10486
+
10487
+ ESCALATION CHAIN \u2014 follow this order:
10488
+ 1. web_search: Search for techniques, bypasses, default creds, CVEs, HackTricks
10489
+ 2. BYPASS: Try alternative approaches \u2014 different protocols, ports, encodings, methods
10490
+ 3. ZERO-DAY EXPLORATION: Probe for unknown vulns \u2014 fuzz parameters, test edge cases, analyze error responses for leaks
10491
+ 4. BRUTE-FORCE: Wordlists, credential stuffing, common passwords, custom password lists from context
10492
+ 5. ask_user: ONLY as last resort \u2014 ask the user for hints, wordlists, or guidance
10493
+
10494
+ RULES:
10495
+ - Every turn MUST have tool calls
10496
+ - NEVER silently give up \u2014 exhaust ALL 5 steps above first
10497
+ - ACT NOW \u2014 do not plan, do not explain, do not summarize. EXECUTE.`;
10498
+ }
10499
+ // ─────────────────────────────────────────────────────────────────
10479
10500
  // SUBSECTION: Event Emitters
10480
10501
  // ─────────────────════════════════════════════════════════════
10481
10502
  emitThink(iteration, progress) {
@@ -10640,83 +10661,20 @@ ${firstLine}`, phase }
10640
10661
  const toolStartTime = Date.now();
10641
10662
  logLLM("CoreAgent executing tool", { id: call.id, name: call.name, input: call.input });
10642
10663
  if (!this.toolRegistry) {
10643
- return {
10644
- toolCallId: call.id,
10645
- output: "",
10646
- error: "Tool registry not initialized. Call setToolRegistry() first."
10647
- };
10664
+ return { toolCallId: call.id, output: "", error: "Tool registry not initialized. Call setToolRegistry() first." };
10648
10665
  }
10649
10666
  try {
10650
- const result2 = await this.toolRegistry.execute({
10651
- name: call.name,
10652
- input: call.input
10653
- });
10667
+ const result2 = await this.toolRegistry.execute({ name: call.name, input: call.input });
10654
10668
  let outputText = result2.output ?? "";
10655
10669
  this.scanForFlags(outputText);
10656
- if (result2.error) {
10657
- outputText = this.enrichToolError({ toolName: call.name, input: call.input, error: result2.error, originalOutput: outputText, progress });
10658
- if (progress) progress.toolErrors++;
10659
- } else {
10660
- if (progress) {
10661
- progress.toolSuccesses++;
10662
- progress.blockedCommandPatterns.clear();
10663
- }
10664
- }
10665
- const rawOutputForTUI = outputText;
10666
- let digestedOutputForLLM = outputText;
10667
- let digestResult = null;
10668
- try {
10669
- const llmDigestFn = createLLMDigestFn(this.llm);
10670
- digestResult = await digestToolOutput(
10671
- outputText,
10672
- call.name,
10673
- JSON.stringify(call.input).slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
10674
- llmDigestFn
10675
- );
10676
- digestedOutputForLLM = digestResult.digestedOutput;
10677
- } catch {
10678
- if (digestedOutputForLLM.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
10679
- const truncated = digestedOutputForLLM.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
10680
- const remaining = digestedOutputForLLM.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
10681
- digestedOutputForLLM = `${truncated}
10682
-
10683
- ... [TRUNCATED ${remaining} characters for context hygiene] ...
10684
- \u{1F4A1} TIP: If you need to see the full output, use a tool to read the file directly or run the command with | head, | tail, or | grep.`;
10685
- }
10686
- }
10687
- this.emitToolResult(call.name, result2.success, rawOutputForTUI, result2.error, Date.now() - toolStartTime);
10688
- const inputSummary = JSON.stringify(call.input);
10689
- this.turnToolJournal.push({
10690
- name: call.name,
10691
- inputSummary,
10692
- success: result2.success,
10693
- analystSummary: digestResult?.memo ? digestResult.memo.keyFindings.join("; ") || "No key findings" : digestedOutputForLLM,
10694
- outputFile: digestResult?.fullOutputPath ?? null
10695
- });
10696
- if (digestResult?.memo) {
10697
- const m = digestResult.memo;
10698
- this.turnMemo.keyFindings.push(...m.keyFindings);
10699
- this.turnMemo.credentials.push(...m.credentials);
10700
- this.turnMemo.attackVectors.push(...m.attackVectors);
10701
- this.turnMemo.failures.push(...m.failures);
10702
- this.turnMemo.suspicions.push(...m.suspicions);
10703
- const VALUE_RANK = { HIGH: 3, MED: 2, LOW: 1, NONE: 0 };
10704
- if ((VALUE_RANK[m.attackValue] ?? 0) > (VALUE_RANK[this.turnMemo.attackValue] ?? 0)) {
10705
- this.turnMemo.attackValue = m.attackValue;
10706
- }
10707
- this.turnMemo.nextSteps.push(...m.nextSteps);
10708
- if (m.reflection) this.turnReflections.push(m.reflection);
10709
- }
10710
- if (digestResult?.memo?.credentials.length) {
10711
- for (const cred of digestResult.memo.credentials) {
10712
- this.state.addLoot({
10713
- type: "credential",
10714
- host: "auto-extracted",
10715
- detail: cred,
10716
- obtainedAt: Date.now()
10717
- });
10718
- }
10719
- }
10670
+ outputText = this.handleToolResult(result2, call, outputText, progress);
10671
+ const { digestedOutputForLLM, digestResult } = await this.digestAndEmit(
10672
+ call,
10673
+ outputText,
10674
+ result2,
10675
+ toolStartTime
10676
+ );
10677
+ this.recordJournalMemo(call, result2, digestedOutputForLLM, digestResult);
10720
10678
  return { toolCallId: call.id, output: digestedOutputForLLM, error: result2.error };
10721
10679
  } catch (error) {
10722
10680
  const errorMsg = String(error);
@@ -10726,6 +10684,90 @@ ${firstLine}`, phase }
10726
10684
  return { toolCallId: call.id, output: enrichedError, error: errorMsg };
10727
10685
  }
10728
10686
  }
10687
+ /**
10688
+ * Handle tool result: enrich errors or track success.
10689
+ * @returns Possibly enriched output text.
10690
+ */
10691
+ handleToolResult(result2, call, outputText, progress) {
10692
+ if (result2.error) {
10693
+ if (progress) progress.toolErrors++;
10694
+ return this.enrichToolError({ toolName: call.name, input: call.input, error: result2.error, originalOutput: outputText, progress });
10695
+ }
10696
+ if (progress) {
10697
+ progress.toolSuccesses++;
10698
+ progress.blockedCommandPatterns.clear();
10699
+ }
10700
+ return outputText;
10701
+ }
10702
+ /**
10703
+ * Digest tool output via Analyst LLM (§13 ③) and emit TUI event.
10704
+ *
10705
+ * WHY separated: Digest + emit is a self-contained pipeline:
10706
+ * raw output → Analyst → digest + file → TUI event.
10707
+ * Isolating it makes the pipeline testable without running actual tools.
10708
+ */
10709
+ async digestAndEmit(call, outputText, result2, toolStartTime) {
10710
+ const digestFallbackOutput = outputText;
10711
+ let digestedOutputForLLM = outputText;
10712
+ let digestResult = null;
10713
+ try {
10714
+ const llmDigestFn = createLLMDigestFn(this.llm);
10715
+ digestResult = await digestToolOutput(
10716
+ outputText,
10717
+ call.name,
10718
+ JSON.stringify(call.input).slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
10719
+ llmDigestFn
10720
+ );
10721
+ digestedOutputForLLM = digestResult.digestedOutput;
10722
+ } catch {
10723
+ if (digestedOutputForLLM.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
10724
+ const truncated = digestedOutputForLLM.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
10725
+ const remaining = digestedOutputForLLM.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
10726
+ digestedOutputForLLM = `${truncated}
10727
+
10728
+ ... [TRUNCATED ${remaining} characters for context hygiene] ...
10729
+ \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.`;
10730
+ }
10731
+ }
10732
+ const outputFilePath = digestResult?.fullOutputPath ?? null;
10733
+ const tuiOutput = digestResult?.digestedOutput ? `${digestResult.digestedOutput}${outputFilePath ? `
10734
+ \u{1F4C4} Full output: ${outputFilePath}` : ""}` : digestFallbackOutput.slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY);
10735
+ this.emitToolResult(call.name, result2.success, tuiOutput, result2.error, Date.now() - toolStartTime);
10736
+ return { digestedOutputForLLM, digestResult };
10737
+ }
10738
+ /**
10739
+ * Record tool execution results to Journal and aggregate memos.
10740
+ *
10741
+ * WHY no truncation on inputSummary: Strategist needs full context —
10742
+ * "hydra -l admin -P rockyou.txt ssh://10.0.0.1" must survive intact.
10743
+ */
10744
+ recordJournalMemo(call, result2, digestedOutputForLLM, digestResult) {
10745
+ this.turnToolJournal.push({
10746
+ name: call.name,
10747
+ inputSummary: JSON.stringify(call.input),
10748
+ success: result2.success,
10749
+ analystSummary: digestResult?.memo ? digestResult.memo.keyFindings.join("; ") || "No key findings" : digestedOutputForLLM,
10750
+ outputFile: digestResult?.fullOutputPath ?? null
10751
+ });
10752
+ if (digestResult?.memo) {
10753
+ const m = digestResult.memo;
10754
+ this.turnMemo.keyFindings.push(...m.keyFindings);
10755
+ this.turnMemo.credentials.push(...m.credentials);
10756
+ this.turnMemo.attackVectors.push(...m.attackVectors);
10757
+ this.turnMemo.failures.push(...m.failures);
10758
+ this.turnMemo.suspicions.push(...m.suspicions);
10759
+ if ((ATTACK_VALUE_RANK[m.attackValue] ?? 0) > (ATTACK_VALUE_RANK[this.turnMemo.attackValue] ?? 0)) {
10760
+ this.turnMemo.attackValue = m.attackValue;
10761
+ }
10762
+ this.turnMemo.nextSteps.push(...m.nextSteps);
10763
+ if (m.reflection) this.turnReflections.push(m.reflection);
10764
+ }
10765
+ if (digestResult?.memo?.credentials.length) {
10766
+ for (const cred of digestResult.memo.credentials) {
10767
+ this.state.addLoot({ type: LOOT_TYPES.CREDENTIAL, host: "auto-extracted", detail: cred, obtainedAt: Date.now() });
10768
+ }
10769
+ }
10770
+ }
10729
10771
  /**
10730
10772
  * Enrich tool error — delegates to extracted module (§3-1)
10731
10773
  */
@@ -10794,7 +10836,7 @@ ${firstLine}`, phase }
10794
10836
  // src/agents/prompt-builder.ts
10795
10837
  import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
10796
10838
  import { join as join11, dirname as dirname5 } from "path";
10797
- import { fileURLToPath as fileURLToPath4 } from "url";
10839
+ import { fileURLToPath as fileURLToPath3 } from "url";
10798
10840
 
10799
10841
  // src/shared/constants/prompts.ts
10800
10842
  var PROMPT_PATHS = {
@@ -10853,6 +10895,44 @@ var PROMPT_CONFIG = {
10853
10895
  var INITIAL_TASKS = {
10854
10896
  RECON: "Initial reconnaissance and target discovery"
10855
10897
  };
10898
+ var CONTEXT_EXTRACTOR_PROMPT = `You are extracting actionable intelligence from a penetration testing session.
10899
+ DO NOT simply summarize or shorten. EXTRACT critical facts:
10900
+
10901
+ 1. DISCOVERED: Services, versions, paths, parameters (exact IPs, ports, versions)
10902
+ 2. CONFIRMED: Vulnerabilities or access confirmed
10903
+ 3. CREDENTIALS: Usernames, passwords, tokens, keys
10904
+ 4. DEAD ENDS: What failed \u2014 include EXACT command, tool, arguments, wordlist/file used.
10905
+ Distinguish between:
10906
+ - "This approach itself is impossible" (e.g., SSH key-only \u2192 no password brute force works)
10907
+ - "This specific attempt failed" (e.g., sqlmap with default tamper \u2192 try different tamper)
10908
+ 5. OPEN LEADS: Unexplored paths worth pursuing
10909
+
10910
+ Every line must include exact commands/tools/files used.
10911
+ The reader must be able to judge whether a retry with different parameters is worthwhile.`;
10912
+ var REFLECTION_PROMPT = `You are a tactical reviewer for a penetration testing agent.
10913
+ Review ALL actions from this turn \u2014 successes AND failures.
10914
+
10915
+ 1. ASSESSMENT: What did this turn accomplish? Rate: HIGH / MED / LOW / NONE.
10916
+ 2. SUCCESSES: What worked? Can this pattern be replicated elsewhere?
10917
+ 3. FAILURES: What failed? Is this a repeated pattern? If so \u2192 STOP this approach.
10918
+ 4. BLIND SPOTS: What was missed or overlooked?
10919
+ 5. NEXT PRIORITY: Single most valuable next action.
10920
+
10921
+ 3-5 lines. Every word must be actionable.`;
10922
+ var SUMMARY_REGENERATOR_PROMPT = `Update this penetration testing session summary with the new turn data.
10923
+
10924
+ Must include:
10925
+ - All discovered hosts, services, versions (exact IPs, ports, software versions)
10926
+ - All confirmed vulnerabilities
10927
+ - All obtained credentials
10928
+ - Failed attempts with EXACT commands/tools/arguments/files used.
10929
+ For each failure, state:
10930
+ - The root cause (auth method? WAF? patched? wrong params?)
10931
+ - Whether retrying with different parameters could work
10932
+ - Top unexplored leads
10933
+
10934
+ Remove outdated/superseded info. Keep concise but COMPLETE.
10935
+ The reader must be able to decide what to retry and what to never attempt again.`;
10856
10936
 
10857
10937
  // src/shared/constants/scoring.ts
10858
10938
  var ATTACK_SCORING = {
@@ -11018,7 +11098,6 @@ function getAttacksForService(service, port) {
11018
11098
  import { writeFileSync as writeFileSync8, readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync5 } from "fs";
11019
11099
  import { join as join10 } from "path";
11020
11100
  var MAX_JOURNAL_ENTRIES = 50;
11021
- var SUMMARY_REGEN_INTERVAL = 10;
11022
11101
  var MAX_OUTPUT_FILES = 30;
11023
11102
  var TURN_PREFIX = "turn-";
11024
11103
  var SUMMARY_FILE = "summary.md";
@@ -11075,9 +11154,6 @@ function getNextTurnNumber() {
11075
11154
  return 1;
11076
11155
  }
11077
11156
  }
11078
- function shouldRegenerateSummary(currentTurn) {
11079
- return currentTurn > 0 && currentTurn % SUMMARY_REGEN_INTERVAL === 0;
11080
- }
11081
11157
  function regenerateJournalSummary() {
11082
11158
  try {
11083
11159
  const entries = getRecentEntries();
@@ -11096,6 +11172,10 @@ function regenerateJournalSummary() {
11096
11172
  }
11097
11173
  }
11098
11174
  function buildSummaryFromEntries(entries) {
11175
+ const buckets = collectSummaryBuckets(entries);
11176
+ return formatSummaryMarkdown(buckets, entries);
11177
+ }
11178
+ function collectSummaryBuckets(entries) {
11099
11179
  const attempts = [];
11100
11180
  const findings = [];
11101
11181
  const credentials = [];
@@ -11104,19 +11184,11 @@ function buildSummaryFromEntries(entries) {
11104
11184
  const suspicions = [];
11105
11185
  const nextSteps = [];
11106
11186
  const reflections = [];
11107
- const VALUE_ORDER = { HIGH: 0, MED: 1, LOW: 2, NONE: 3 };
11108
11187
  const reversed = [...entries].reverse();
11109
11188
  for (const entry of reversed) {
11110
11189
  const value = entry.memo.attackValue || "LOW";
11111
11190
  for (const tool of entry.tools) {
11112
- attempts.push({
11113
- turn: entry.turn,
11114
- phase: entry.phase,
11115
- ok: tool.success,
11116
- name: tool.name,
11117
- input: tool.inputSummary,
11118
- value
11119
- });
11191
+ attempts.push({ turn: entry.turn, phase: entry.phase, ok: tool.success, name: tool.name, input: tool.inputSummary, value });
11120
11192
  }
11121
11193
  for (const finding of entry.memo.keyFindings) {
11122
11194
  const line = `- [T${entry.turn}|\u26A1${value}] ${finding}`;
@@ -11155,9 +11227,13 @@ function buildSummaryFromEntries(entries) {
11155
11227
  }
11156
11228
  }
11157
11229
  attempts.sort((a, b) => {
11158
- const vd = (VALUE_ORDER[a.value] ?? 3) - (VALUE_ORDER[b.value] ?? 3);
11230
+ const vd = (ATTACK_VALUE_RANK[b.value] ?? 0) - (ATTACK_VALUE_RANK[a.value] ?? 0);
11159
11231
  return vd !== 0 ? vd : b.turn - a.turn;
11160
11232
  });
11233
+ return { attempts, findings, credentials, successes, failures, suspicions, nextSteps, reflections };
11234
+ }
11235
+ function formatSummaryMarkdown(buckets, entries) {
11236
+ const { attempts, findings, credentials, successes, failures, suspicions, nextSteps, reflections } = buckets;
11161
11237
  const attemptLines = attempts.map(
11162
11238
  (a) => `- [T${a.turn}|${a.phase}|\u26A1${a.value}] ${a.ok ? "\u2705" : "\u274C"} ${a.name}: ${a.input}`
11163
11239
  );
@@ -11234,8 +11310,8 @@ function rotateOutputFiles() {
11234
11310
  }
11235
11311
 
11236
11312
  // src/agents/prompt-builder.ts
11237
- var __dirname4 = dirname5(fileURLToPath4(import.meta.url));
11238
- var PROMPTS_DIR = join11(__dirname4, "prompts");
11313
+ var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
11314
+ var PROMPTS_DIR = join11(__dirname3, "prompts");
11239
11315
  var TECHNIQUES_DIR = join11(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
11240
11316
  var { AGENT_FILES } = PROMPT_PATHS;
11241
11317
  var PHASE_PROMPT_MAP = {
@@ -11537,12 +11613,20 @@ ${lines.join("\n")}
11537
11613
  }
11538
11614
  // --- §13: Session Journal Summary ---
11539
11615
  /**
11540
- * Load journal summary from .pentesting/journal/summary.md
11541
- * Provides compressed history of past turns — what worked, what failed,
11542
- * what was discovered. Main LLM uses this for continuity across many turns.
11616
+ * Load journal summary prefers Summary Regenerator (⑥) output,
11617
+ * falls back to deterministic journal summary.
11543
11618
  */
11544
11619
  getJournalFragment() {
11545
11620
  try {
11621
+ const summaryPath = join11(WORKSPACE.TURNS, "summary.md");
11622
+ if (existsSync9(summaryPath)) {
11623
+ const summary2 = readFileSync6(summaryPath, "utf-8");
11624
+ if (summary2.trim()) {
11625
+ return `<session-journal>
11626
+ ${summary2}
11627
+ </session-journal>`;
11628
+ }
11629
+ }
11546
11630
  const summary = readJournalSummary();
11547
11631
  if (!summary) return "";
11548
11632
  return `<session-journal>
@@ -11566,9 +11650,9 @@ ${summary}
11566
11650
  // src/agents/strategist.ts
11567
11651
  import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
11568
11652
  import { join as join12, dirname as dirname6 } from "path";
11569
- import { fileURLToPath as fileURLToPath5 } from "url";
11570
- var __dirname5 = dirname6(fileURLToPath5(import.meta.url));
11571
- var STRATEGIST_PROMPT_PATH = join12(__dirname5, "prompts", "strategist-system.md");
11653
+ import { fileURLToPath as fileURLToPath4 } from "url";
11654
+ var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
11655
+ var STRATEGIST_PROMPT_PATH = join12(__dirname4, "prompts", "strategist-system.md");
11572
11656
  var Strategist = class {
11573
11657
  llm;
11574
11658
  state;
@@ -11626,7 +11710,14 @@ var Strategist = class {
11626
11710
  sections.push(failures);
11627
11711
  }
11628
11712
  try {
11629
- const journalSummary = readJournalSummary();
11713
+ let journalSummary = "";
11714
+ const summaryPath = join12(WORKSPACE.TURNS, "summary.md");
11715
+ if (existsSync10(summaryPath)) {
11716
+ journalSummary = readFileSync7(summaryPath, "utf-8").trim();
11717
+ }
11718
+ if (!journalSummary) {
11719
+ journalSummary = readJournalSummary();
11720
+ }
11630
11721
  if (journalSummary) {
11631
11722
  sections.push("");
11632
11723
  sections.push("## Session Journal (past turns summary)");
@@ -11743,7 +11834,94 @@ Detect stalls (repeated failures, no progress) and force completely different at
11743
11834
  Chain every finding: "If X works \u2192 immediately do Y \u2192 which enables Z."
11744
11835
  Maximum 50 lines. Zero preamble. Direct imperatives only. Never repeat failed approaches.`;
11745
11836
 
11837
+ // src/shared/utils/turn-record.ts
11838
+ function formatTurnRecord(input) {
11839
+ const { turn, timestamp, phase, tools, memo: memo6, reflection } = input;
11840
+ const time = timestamp.slice(0, 19).replace("T", " ");
11841
+ const sections = [];
11842
+ sections.push(`# Turn ${turn} | ${time} | Phase: ${phase}`);
11843
+ sections.push("");
11844
+ sections.push("## \uC2E4\uD589 \uB3C4\uAD6C");
11845
+ if (tools.length === 0) {
11846
+ sections.push("- (\uB3C4\uAD6C \uC2E4\uD589 \uC5C6\uC74C)");
11847
+ } else {
11848
+ for (const tool of tools) {
11849
+ const status = tool.success ? "\u2705" : "\u274C";
11850
+ const line = `- ${tool.name}(${tool.inputSummary}) \u2192 ${status} ${tool.analystSummary}`;
11851
+ sections.push(line);
11852
+ }
11853
+ }
11854
+ sections.push("");
11855
+ sections.push("## \uD575\uC2EC \uC778\uC0AC\uC774\uD2B8");
11856
+ if (memo6.keyFindings.length > 0) {
11857
+ for (const f of memo6.keyFindings) sections.push(`- DISCOVERED: ${f}`);
11858
+ }
11859
+ if (memo6.credentials.length > 0) {
11860
+ for (const c of memo6.credentials) sections.push(`- CREDENTIAL: ${c}`);
11861
+ }
11862
+ if (memo6.attackVectors.length > 0) {
11863
+ for (const v of memo6.attackVectors) sections.push(`- CONFIRMED: ${v}`);
11864
+ }
11865
+ if (memo6.failures.length > 0) {
11866
+ for (const f of memo6.failures) sections.push(`- DEAD END: ${f}`);
11867
+ }
11868
+ if (memo6.suspicions.length > 0) {
11869
+ for (const s of memo6.suspicions) sections.push(`- SUSPICIOUS: ${s}`);
11870
+ }
11871
+ if (memo6.nextSteps.length > 0) {
11872
+ for (const n of memo6.nextSteps) sections.push(`- NEXT: ${n}`);
11873
+ }
11874
+ if (memo6.keyFindings.length === 0 && memo6.failures.length === 0 && memo6.credentials.length === 0) {
11875
+ sections.push("- (\uD2B9\uC774\uC0AC\uD56D \uC5C6\uC74C)");
11876
+ }
11877
+ sections.push("");
11878
+ sections.push("## \uC790\uAE30\uBC18\uC131");
11879
+ sections.push(reflection || "- (\uBC18\uC131 \uC5C6\uC74C)");
11880
+ sections.push("");
11881
+ return sections.join("\n");
11882
+ }
11883
+ function formatForExtraction(messages) {
11884
+ const parts = ["\uB2E4\uC74C\uC740 \uD39C\uD14C\uC2A4\uD305 \uC138\uC158\uC758 \uB300\uD654 \uAE30\uB85D\uC785\uB2C8\uB2E4. \uD575\uC2EC \uC778\uC0AC\uC774\uD2B8\uB97C \uCD94\uCD9C\uD558\uC138\uC694:\n"];
11885
+ for (const msg of messages) {
11886
+ const role = msg.role === "assistant" ? "AGENT" : msg.role === "user" ? "RESULT" : msg.role.toUpperCase();
11887
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
11888
+ const truncated = content.length > 3e3 ? content.slice(0, 1500) + "\n...(truncated)...\n" + content.slice(-1500) : content;
11889
+ parts.push(`[${role}]
11890
+ ${truncated}
11891
+ `);
11892
+ }
11893
+ return parts.join("\n");
11894
+ }
11895
+ function formatReflectionInput(input) {
11896
+ const { tools, memo: memo6, phase } = input;
11897
+ const parts = [
11898
+ `\uD604\uC7AC Phase: ${phase}`,
11899
+ "",
11900
+ "\uC774\uBC88 \uD134 \uC2E4\uD589 \uACB0\uACFC:"
11901
+ ];
11902
+ for (const tool of tools) {
11903
+ const status = tool.success ? "\u2705 \uC131\uACF5" : "\u274C \uC2E4\uD328";
11904
+ parts.push(`- ${tool.name}(${tool.inputSummary}) \u2192 ${status}`);
11905
+ if (tool.analystSummary) {
11906
+ parts.push(` \uC694\uC57D: ${tool.analystSummary}`);
11907
+ }
11908
+ }
11909
+ if (tools.length === 0) {
11910
+ parts.push("- (\uB3C4\uAD6C \uC2E4\uD589 \uC5C6\uC74C)");
11911
+ }
11912
+ parts.push("");
11913
+ parts.push("Analyst \uCD94\uCD9C \uBA54\uBAA8:");
11914
+ if (memo6.keyFindings.length > 0) parts.push(` \uBC1C\uACAC: ${memo6.keyFindings.join(", ")}`);
11915
+ if (memo6.credentials.length > 0) parts.push(` \uD06C\uB808\uB374\uC15C: ${memo6.credentials.join(", ")}`);
11916
+ if (memo6.failures.length > 0) parts.push(` \uC2E4\uD328: ${memo6.failures.join(", ")}`);
11917
+ if (memo6.suspicions.length > 0) parts.push(` \uC758\uC2EC: ${memo6.suspicions.join(", ")}`);
11918
+ parts.push(` \uACF5\uACA9 \uAC00\uCE58: ${memo6.attackValue}`);
11919
+ return parts.join("\n");
11920
+ }
11921
+
11746
11922
  // src/agents/main-agent.ts
11923
+ import { writeFileSync as writeFileSync9, existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
11924
+ import { join as join13 } from "path";
11747
11925
  var MainAgent = class extends CoreAgent {
11748
11926
  promptBuilder;
11749
11927
  strategist;
@@ -11797,6 +11975,45 @@ var MainAgent = class extends CoreAgent {
11797
11975
  this.turnReflections = [];
11798
11976
  const dynamicPrompt = await this.getCurrentPrompt();
11799
11977
  const result2 = await super.step(iteration, messages, dynamicPrompt, progress);
11978
+ try {
11979
+ if (messages.length > 2) {
11980
+ const extraction = await this.llm.generateResponse(
11981
+ [{ role: "user", content: formatForExtraction(messages) }],
11982
+ void 0,
11983
+ CONTEXT_EXTRACTOR_PROMPT
11984
+ );
11985
+ if (extraction.content?.trim()) {
11986
+ messages.length = 0;
11987
+ messages.push({
11988
+ role: "user",
11989
+ content: `<session-context>
11990
+ ${extraction.content.trim()}
11991
+ </session-context>`
11992
+ });
11993
+ }
11994
+ }
11995
+ } catch {
11996
+ }
11997
+ try {
11998
+ if (this.turnToolJournal.length > 0) {
11999
+ const reflection = await this.llm.generateResponse(
12000
+ [{
12001
+ role: "user",
12002
+ content: formatReflectionInput({
12003
+ tools: this.turnToolJournal,
12004
+ memo: this.turnMemo,
12005
+ phase: this.state.getPhase()
12006
+ })
12007
+ }],
12008
+ void 0,
12009
+ REFLECTION_PROMPT
12010
+ );
12011
+ if (reflection.content?.trim()) {
12012
+ this.turnReflections.push(reflection.content.trim());
12013
+ }
12014
+ }
12015
+ } catch {
12016
+ }
11800
12017
  if (this.turnToolJournal.length > 0) {
11801
12018
  try {
11802
12019
  const entry = {
@@ -11808,7 +12025,50 @@ var MainAgent = class extends CoreAgent {
11808
12025
  reflection: this.turnReflections.length > 0 ? this.turnReflections.join(" | ") : this.turnMemo.nextSteps.join("; ")
11809
12026
  };
11810
12027
  writeJournalEntry(entry);
11811
- if (shouldRegenerateSummary(this.turnCounter)) {
12028
+ try {
12029
+ ensureDirExists(WORKSPACE.TURNS);
12030
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
12031
+ const turnFileName = `turn-${String(this.turnCounter).padStart(3, "0")}_${ts}.md`;
12032
+ const turnPath = join13(WORKSPACE.TURNS, turnFileName);
12033
+ const turnContent = formatTurnRecord({
12034
+ turn: this.turnCounter,
12035
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12036
+ phase: this.state.getPhase(),
12037
+ tools: this.turnToolJournal,
12038
+ memo: this.turnMemo,
12039
+ reflection: entry.reflection
12040
+ });
12041
+ writeFileSync9(turnPath, turnContent, "utf-8");
12042
+ } catch {
12043
+ }
12044
+ try {
12045
+ const summaryPath = join13(WORKSPACE.TURNS, "summary.md");
12046
+ const existingSummary = existsSync11(summaryPath) ? readFileSync8(summaryPath, "utf-8") : "";
12047
+ const turnData = formatTurnRecord({
12048
+ turn: this.turnCounter,
12049
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12050
+ phase: this.state.getPhase(),
12051
+ tools: this.turnToolJournal,
12052
+ memo: this.turnMemo,
12053
+ reflection: entry.reflection
12054
+ });
12055
+ const summaryResponse = await this.llm.generateResponse(
12056
+ [{
12057
+ role: "user",
12058
+ content: existingSummary ? `\uAE30\uC874 \uC694\uC57D:
12059
+ ${existingSummary}
12060
+
12061
+ \uC774\uBC88 \uD134:
12062
+ ${turnData}` : `\uCCAB \uD134 \uB370\uC774\uD130:
12063
+ ${turnData}`
12064
+ }],
12065
+ void 0,
12066
+ SUMMARY_REGENERATOR_PROMPT
12067
+ );
12068
+ if (summaryResponse.content?.trim()) {
12069
+ writeFileSync9(summaryPath, summaryResponse.content.trim(), "utf-8");
12070
+ }
12071
+ } catch {
11812
12072
  regenerateJournalSummary();
11813
12073
  }
11814
12074
  rotateJournalEntries();
@@ -596,4 +596,24 @@ Ask yourself at every Reflect step:
596
596
  8. **Search when stuck** — `web_search` and `browse_url` are the most powerful weapons
597
597
  9. **Write code directly if needed** — write scripts with `write_file` → execute with `run_cmd`
598
598
 
599
+ ## 📂 Session Memory — Past Turn Records
599
600
 
601
+ Your past actions and insights are saved as files. Use them freely:
602
+
603
+ ```
604
+ .pentesting/memory/turns/
605
+ ├── summary.md ← Full session summary (updated every turn)
606
+ ├── turn-001_2026-02-21T08-30-15.md ← Turn 1 details
607
+ ├── turn-002_2026-02-21T08-31-22.md ← Turn 2 details
608
+ └── ...
609
+ ```
610
+
611
+ **Each turn file has 3 sections:**
612
+ - `## 실행 도구` — Exact commands/tools/arguments used
613
+ - `## 핵심 인사이트` — What was discovered, confirmed, or failed
614
+ - `## 자기반성` — Turn assessment and next priority
615
+
616
+ **How to use:**
617
+ - `summary.md` gives you the full picture — read it to understand where you stand
618
+ - Need details of a specific past turn? → `read_file(".pentesting/memory/turns/turn-005_...")`
619
+ - All past findings, credentials, dead ends are preserved — never lost
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pentesting",
3
- "version": "0.47.4",
3
+ "version": "0.48.1",
4
4
  "description": "Autonomous Penetration Testing AI Agent",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",