pentesting 0.47.3 → 0.48.0

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
@@ -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.3";
334
+ var APP_VERSION = "0.48.0";
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`;
@@ -828,6 +830,8 @@ var SESSIONS_DIR = `${PENTESTING_ROOT}/sessions`;
828
830
  var LOOT_DIR = `${PENTESTING_ROOT}/loot`;
829
831
  var OUTPUTS_DIR = `${PENTESTING_ROOT}/outputs`;
830
832
  var DEBUG_DIR = `${PENTESTING_ROOT}/debug`;
833
+ var JOURNAL_DIR = `${PENTESTING_ROOT}/journal`;
834
+ var TURNS_DIR = `${PENTESTING_ROOT}/memory/turns`;
831
835
  var WORKSPACE = {
832
836
  /** Root directory */
833
837
  get ROOT() {
@@ -860,6 +864,14 @@ var WORKSPACE = {
860
864
  /** Debug logs */
861
865
  get DEBUG() {
862
866
  return path.resolve(DEBUG_DIR);
867
+ },
868
+ /** Persistent per-turn journal (§13 memo system) */
869
+ get JOURNAL() {
870
+ return path.resolve(JOURNAL_DIR);
871
+ },
872
+ /** Turn record files */
873
+ get TURNS() {
874
+ return path.resolve(TURNS_DIR);
863
875
  }
864
876
  };
865
877
 
@@ -2687,13 +2699,13 @@ var AttackGraph = class {
2687
2699
  * Record a credential discovery and create spray edges.
2688
2700
  */
2689
2701
  addCredential(username, password, source) {
2690
- const credId = this.addNode("credential", `${username}:***`, {
2702
+ const credId = this.addNode(NODE_TYPE.CREDENTIAL, `${username}:***`, {
2691
2703
  username,
2692
2704
  password,
2693
2705
  source
2694
2706
  });
2695
2707
  for (const [id, node] of this.nodes) {
2696
- if (node.type === "service") {
2708
+ if (node.type === NODE_TYPE.SERVICE) {
2697
2709
  const svc = String(node.data.service || "");
2698
2710
  if (["ssh", "ftp", "rdp", "smb", "http", "mysql", "postgresql", "mssql", "winrm", "vnc", "telnet"].some((s) => svc.includes(s))) {
2699
2711
  this.addEdge(credId, id, "can_try_on", 0.6);
@@ -2706,7 +2718,7 @@ var AttackGraph = class {
2706
2718
  * Record a vulnerability finding.
2707
2719
  */
2708
2720
  addVulnerability(title, target, severity, hasExploit = false) {
2709
- const vulnId = this.addNode("vulnerability", title, {
2721
+ const vulnId = this.addNode(NODE_TYPE.VULNERABILITY, title, {
2710
2722
  target,
2711
2723
  severity,
2712
2724
  hasExploit
@@ -2717,7 +2729,7 @@ var AttackGraph = class {
2717
2729
  }
2718
2730
  }
2719
2731
  if (hasExploit) {
2720
- const accessId = this.addNode("access", `shell via ${title}`, {
2732
+ const accessId = this.addNode(NODE_TYPE.ACCESS, `shell via ${title}`, {
2721
2733
  via: title,
2722
2734
  status: GRAPH_STATUS.POTENTIAL
2723
2735
  });
@@ -2729,14 +2741,14 @@ var AttackGraph = class {
2729
2741
  * Record gained access.
2730
2742
  */
2731
2743
  addAccess(host, level, via) {
2732
- const accessId = this.addNode("access", `${level}@${host}`, {
2744
+ const accessId = this.addNode(NODE_TYPE.ACCESS, `${level}@${host}`, {
2733
2745
  host,
2734
2746
  level,
2735
2747
  via
2736
2748
  });
2737
2749
  this.markSucceeded(accessId);
2738
2750
  if (["root", "admin", "SYSTEM", "Administrator"].includes(level)) {
2739
- const lootId = this.addNode("loot", `flags on ${host}`, {
2751
+ const lootId = this.addNode(NODE_TYPE.LOOT, `flags on ${host}`, {
2740
2752
  host,
2741
2753
  status: GRAPH_STATUS.NEEDS_SEARCH
2742
2754
  });
@@ -2748,7 +2760,7 @@ var AttackGraph = class {
2748
2760
  * Record OSINT discovery (Docker image, GitHub repo, company info, etc.)
2749
2761
  */
2750
2762
  addOSINT(category, detail, data = {}) {
2751
- const osintId = this.addNode("osint", `${category}: ${detail}`, {
2763
+ const osintId = this.addNode(NODE_TYPE.OSINT, `${category}: ${detail}`, {
2752
2764
  category,
2753
2765
  detail,
2754
2766
  ...data
@@ -3981,20 +3993,6 @@ var ScopeGuard = class {
3981
3993
  };
3982
3994
 
3983
3995
  // src/engine/approval.ts
3984
- var CATEGORY_APPROVAL = {
3985
- [SERVICE_CATEGORIES.NETWORK]: APPROVAL_LEVELS.CONFIRM,
3986
- [SERVICE_CATEGORIES.WEB]: APPROVAL_LEVELS.CONFIRM,
3987
- [SERVICE_CATEGORIES.DATABASE]: APPROVAL_LEVELS.REVIEW,
3988
- [SERVICE_CATEGORIES.AD]: APPROVAL_LEVELS.REVIEW,
3989
- [SERVICE_CATEGORIES.EMAIL]: APPROVAL_LEVELS.CONFIRM,
3990
- [SERVICE_CATEGORIES.REMOTE_ACCESS]: APPROVAL_LEVELS.REVIEW,
3991
- [SERVICE_CATEGORIES.FILE_SHARING]: APPROVAL_LEVELS.CONFIRM,
3992
- [SERVICE_CATEGORIES.CLOUD]: APPROVAL_LEVELS.REVIEW,
3993
- [SERVICE_CATEGORIES.CONTAINER]: APPROVAL_LEVELS.REVIEW,
3994
- [SERVICE_CATEGORIES.API]: APPROVAL_LEVELS.CONFIRM,
3995
- [SERVICE_CATEGORIES.WIRELESS]: APPROVAL_LEVELS.REVIEW,
3996
- [SERVICE_CATEGORIES.ICS]: APPROVAL_LEVELS.BLOCK
3997
- };
3998
3996
  var ApprovalGate = class {
3999
3997
  constructor(shouldAutoApprove = false) {
4000
3998
  this.shouldAutoApprove = shouldAutoApprove;
@@ -4298,7 +4296,7 @@ function autoExtractStructured(toolName, output) {
4298
4296
  data.vulnerabilities = vulns;
4299
4297
  hasData = true;
4300
4298
  }
4301
- if (toolName === "parse_nmap" || /nmap scan report/i.test(output)) {
4299
+ if (toolName === TOOL_NAMES.PARSE_NMAP || /nmap scan report/i.test(output)) {
4302
4300
  const nmap = extractNmapStructured(output);
4303
4301
  if (nmap.structured.openPorts && nmap.structured.openPorts.length > 0) {
4304
4302
  data.openPorts = nmap.structured.openPorts;
@@ -4703,7 +4701,8 @@ Used ports: ${usedPorts.join(", ")}
4703
4701
  [!] STRATEGY ADAPTATION REQUIRED:
4704
4702
  1. Try the next available port (e.g., ${nextPort} or 4445, 9001)
4705
4703
  2. If this is for a listener, update your Mission/Checklist with the NEW port so other agents know.
4706
- 3. Check bg_process({ action: "list" }) to see if you can stop the conflicting process.`
4704
+ 3. Check bg_process({ action: "list" }) to see if you can stop the conflicting process.`,
4705
+ error: `Port ${requestedPort} already in use`
4707
4706
  };
4708
4707
  }
4709
4708
  }
@@ -4850,7 +4849,7 @@ ${output.stderr.slice(-SYSTEM_LIMITS.MAX_STDERR_SLICE) || "(empty)"}` + connecti
4850
4849
  if (!cmd) return { success: false, output: "", error: "Missing command for interact. Provide the command to execute on the target." };
4851
4850
  const waitMs = Math.min(params.wait_ms || SYSTEM_LIMITS.DEFAULT_WAIT_MS_INTERACT, SYSTEM_LIMITS.MAX_WAIT_MS_INTERACT);
4852
4851
  const result2 = await sendToProcess(processId, cmd, waitMs);
4853
- if (!result2.success) return { success: false, output: result2.output };
4852
+ if (!result2.success) return { success: false, output: result2.output, error: result2.output };
4854
4853
  return {
4855
4854
  success: true,
4856
4855
  output: `Command sent: ${cmd}
@@ -4866,7 +4865,7 @@ ${result2.output}`
4866
4865
  if (!processId) return { success: false, output: "", error: "Missing process_id for promote" };
4867
4866
  const desc = params.description;
4868
4867
  const success = promoteToShell(processId, desc);
4869
- if (!success) return { success: false, output: `Process ${processId} not found` };
4868
+ if (!success) return { success: false, output: `Process ${processId} not found`, error: `Process ${processId} not found` };
4870
4869
  return {
4871
4870
  success: true,
4872
4871
  output: `[OK] Process ${processId} promoted to ACTIVE SHELL.
@@ -5016,7 +5015,8 @@ Examples:
5016
5015
  if (!validPhases.includes(newPhase)) {
5017
5016
  return {
5018
5017
  success: false,
5019
- output: `Invalid phase. Valid phases: ${validPhases.join(", ")}`
5018
+ output: `Invalid phase. Valid phases: ${validPhases.join(", ")}`,
5019
+ error: `Invalid phase: ${newPhase}`
5020
5020
  };
5021
5021
  }
5022
5022
  state.setPhase(newPhase);
@@ -5732,50 +5732,60 @@ var DEFAULT_BROWSER_OPTIONS = {
5732
5732
 
5733
5733
  // src/engine/tools/web-browser.ts
5734
5734
  async function browseUrl(url, options = {}) {
5735
- const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5736
- const { installed, browserInstalled } = await checkPlaywright();
5737
- if (!installed || !browserInstalled) {
5738
- const installResult = await installPlaywright();
5739
- if (!installResult.success) {
5735
+ try {
5736
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5737
+ const { installed, browserInstalled } = await checkPlaywright();
5738
+ if (!installed || !browserInstalled) {
5739
+ const installResult = await installPlaywright();
5740
+ if (!installResult.success) {
5741
+ return {
5742
+ success: false,
5743
+ output: "",
5744
+ error: `Playwright not available and auto-install failed: ${installResult.output}`
5745
+ };
5746
+ }
5747
+ }
5748
+ const screenshotPath = browserOptions.screenshot ? join6(join6(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
5749
+ const script = buildBrowseScript(url, browserOptions, screenshotPath);
5750
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "browse");
5751
+ if (!result2.success) {
5740
5752
  return {
5741
5753
  success: false,
5742
- output: "",
5743
- error: `Playwright not available and auto-install failed: ${installResult.output}`
5754
+ output: result2.output,
5755
+ error: result2.error
5756
+ };
5757
+ }
5758
+ if (result2.parsedData) {
5759
+ return {
5760
+ success: true,
5761
+ output: formatBrowserOutput(result2.parsedData, browserOptions),
5762
+ screenshots: screenshotPath ? [screenshotPath] : void 0,
5763
+ extractedData: result2.parsedData
5744
5764
  };
5745
5765
  }
5746
- }
5747
- const screenshotPath = browserOptions.screenshot ? join6(join6(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
5748
- const script = buildBrowseScript(url, browserOptions, screenshotPath);
5749
- const result2 = await runPlaywrightScript(script, browserOptions.timeout, "browse");
5750
- if (!result2.success) {
5751
5766
  return {
5752
- success: false,
5753
- output: result2.output,
5754
- error: result2.error
5767
+ success: true,
5768
+ output: result2.output || "Navigation completed",
5769
+ screenshots: screenshotPath ? [screenshotPath] : void 0
5755
5770
  };
5756
- }
5757
- if (result2.parsedData) {
5771
+ } catch (error) {
5772
+ const msg = error instanceof Error ? error.message : String(error);
5758
5773
  return {
5759
- success: true,
5760
- output: formatBrowserOutput(result2.parsedData, browserOptions),
5761
- screenshots: screenshotPath ? [screenshotPath] : void 0,
5762
- extractedData: result2.parsedData
5774
+ success: false,
5775
+ output: "",
5776
+ error: `Browser error: ${msg}`
5763
5777
  };
5764
5778
  }
5765
- return {
5766
- success: true,
5767
- output: result2.output || "Navigation completed",
5768
- screenshots: screenshotPath ? [screenshotPath] : void 0
5769
- };
5770
5779
  }
5771
5780
  async function fillAndSubmitForm(url, formData, options = {}) {
5772
- const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5773
- const safeUrl = safeJsString(url);
5774
- const safeFormData = JSON.stringify(formData);
5775
- const playwrightPath = getPlaywrightPath();
5776
- const safePlaywrightPath = safeJsString(playwrightPath);
5777
- const headlessMode = process.env.HEADLESS !== "false";
5778
- const script = `
5781
+ try {
5782
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5783
+ const safeUrl = safeJsString(url);
5784
+ const safeFormData = JSON.stringify(formData);
5785
+ const playwrightPath = getPlaywrightPath();
5786
+ const safePlaywrightPath = safeJsString(playwrightPath);
5787
+ const headlessMode = process.env.HEADLESS !== "false";
5788
+ const script = `
5779
5789
  const { chromium } = require(${safePlaywrightPath});
5780
5790
 
5781
5791
  (async () => {
@@ -5815,27 +5825,35 @@ const { chromium } = require(${safePlaywrightPath});
5815
5825
  }
5816
5826
  })();
5817
5827
  `;
5818
- const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
5819
- if (!result2.success) {
5828
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
5829
+ if (!result2.success) {
5830
+ return {
5831
+ success: false,
5832
+ output: result2.output,
5833
+ error: result2.error
5834
+ };
5835
+ }
5836
+ if (result2.parsedData) {
5837
+ const data = result2.parsedData;
5838
+ return {
5839
+ success: true,
5840
+ output: `Form submitted. Current URL: ${data.url}
5841
+ Title: ${data.title}`,
5842
+ extractedData: result2.parsedData
5843
+ };
5844
+ }
5820
5845
  return {
5821
- success: false,
5822
- output: result2.output,
5823
- error: result2.error
5846
+ success: true,
5847
+ output: result2.output || "Form submitted"
5824
5848
  };
5825
- }
5826
- if (result2.parsedData) {
5827
- const data = result2.parsedData;
5849
+ } catch (error) {
5850
+ const msg = error instanceof Error ? error.message : String(error);
5828
5851
  return {
5829
- success: true,
5830
- output: `Form submitted. Current URL: ${data.url}
5831
- Title: ${data.title}`,
5832
- extractedData: result2.parsedData
5852
+ success: false,
5853
+ output: "",
5854
+ error: `Form submission error: ${msg}`
5833
5855
  };
5834
5856
  }
5835
- return {
5836
- success: true,
5837
- output: result2.output || "Form submitted"
5838
- };
5839
5857
  }
5840
5858
  async function webSearchWithBrowser(query, engine = "google") {
5841
5859
  const searchUrls = {
@@ -5852,6 +5870,10 @@ async function webSearchWithBrowser(query, engine = "google") {
5852
5870
  }
5853
5871
 
5854
5872
  // src/engine/web-search-providers.ts
5873
+ var SEARCH_TIMEOUT_MS = 15e3;
5874
+ function getErrorMessage(error) {
5875
+ return error instanceof Error ? error.message : String(error);
5876
+ }
5855
5877
  async function searchWithGLM(query, apiKey, apiUrl) {
5856
5878
  debugLog("search", "GLM request START", { apiUrl, query });
5857
5879
  const requestBody = {
@@ -5860,21 +5882,39 @@ async function searchWithGLM(query, apiKey, apiUrl) {
5860
5882
  stream: false
5861
5883
  };
5862
5884
  debugLog("search", "GLM request body", requestBody);
5863
- const response = await fetch(apiUrl, {
5864
- method: "POST",
5865
- headers: {
5866
- [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5867
- [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`
5868
- },
5869
- body: JSON.stringify(requestBody)
5870
- });
5885
+ let response;
5886
+ try {
5887
+ response = await fetch(apiUrl, {
5888
+ method: "POST",
5889
+ headers: {
5890
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5891
+ [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`
5892
+ },
5893
+ body: JSON.stringify(requestBody),
5894
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5895
+ });
5896
+ } catch (err) {
5897
+ const msg = getErrorMessage(err);
5898
+ debugLog("search", "GLM fetch FAILED (network)", { error: msg });
5899
+ return { success: false, output: "", error: `GLM search network error: ${msg}. Check internet connection or API endpoint.` };
5900
+ }
5871
5901
  debugLog("search", "GLM response status", { status: response.status, ok: response.ok });
5872
5902
  if (!response.ok) {
5873
- const errorText = await response.text();
5903
+ let errorText = "";
5904
+ try {
5905
+ errorText = await response.text();
5906
+ } catch {
5907
+ }
5874
5908
  debugLog("search", "GLM response ERROR", { status: response.status, error: errorText });
5875
- throw new Error(`GLM Search API error: ${response.status} - ${errorText}`);
5909
+ return { success: false, output: "", error: `GLM Search API error ${response.status}: ${errorText.slice(0, 500)}` };
5910
+ }
5911
+ let data;
5912
+ try {
5913
+ data = await response.json();
5914
+ } catch (err) {
5915
+ debugLog("search", "GLM JSON parse FAILED", { error: getErrorMessage(err) });
5916
+ return { success: false, output: "", error: `GLM search returned invalid JSON: ${getErrorMessage(err)}` };
5876
5917
  }
5877
- const data = await response.json();
5878
5918
  debugLog("search", "GLM response data", { hasChoices: !!data.choices, choicesCount: data.choices?.length });
5879
5919
  let results = "";
5880
5920
  if (data.choices?.[0]?.message?.content) {
@@ -5902,19 +5942,37 @@ async function searchWithBrave(query, apiKey, apiUrl) {
5902
5942
  debugLog("search", "Brave request START", { apiUrl, query });
5903
5943
  const url = `${apiUrl}?q=${encodeURIComponent(query)}&count=${SEARCH_LIMIT.DEFAULT_RESULT_COUNT}`;
5904
5944
  debugLog("search", "Brave request URL", { url });
5905
- const response = await fetch(url, {
5906
- headers: {
5907
- [SEARCH_HEADER.ACCEPT]: "application/json",
5908
- [SEARCH_HEADER.X_SUBSCRIPTION_TOKEN]: apiKey
5909
- }
5910
- });
5945
+ let response;
5946
+ try {
5947
+ response = await fetch(url, {
5948
+ headers: {
5949
+ [SEARCH_HEADER.ACCEPT]: "application/json",
5950
+ [SEARCH_HEADER.X_SUBSCRIPTION_TOKEN]: apiKey
5951
+ },
5952
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5953
+ });
5954
+ } catch (err) {
5955
+ const msg = getErrorMessage(err);
5956
+ debugLog("search", "Brave fetch FAILED (network)", { error: msg });
5957
+ return { success: false, output: "", error: `Brave search network error: ${msg}. Check internet connection.` };
5958
+ }
5911
5959
  debugLog("search", "Brave response status", { status: response.status, ok: response.ok });
5912
5960
  if (!response.ok) {
5913
- const errorText = await response.text();
5961
+ let errorText = "";
5962
+ try {
5963
+ errorText = await response.text();
5964
+ } catch {
5965
+ }
5914
5966
  debugLog("search", "Brave response ERROR", { status: response.status, error: errorText });
5915
- throw new Error(`Brave API error: ${response.status}`);
5967
+ return { success: false, output: "", error: `Brave API error ${response.status}: ${errorText.slice(0, 500)}` };
5968
+ }
5969
+ let data;
5970
+ try {
5971
+ data = await response.json();
5972
+ } catch (err) {
5973
+ debugLog("search", "Brave JSON parse FAILED", { error: getErrorMessage(err) });
5974
+ return { success: false, output: "", error: `Brave search returned invalid JSON: ${getErrorMessage(err)}` };
5916
5975
  }
5917
- const data = await response.json();
5918
5976
  const results = data.web?.results || [];
5919
5977
  debugLog("search", "Brave results count", { count: results.length });
5920
5978
  if (results.length === 0) {
@@ -5930,21 +5988,39 @@ async function searchWithBrave(query, apiKey, apiUrl) {
5930
5988
  }
5931
5989
  async function searchWithSerper(query, apiKey, apiUrl) {
5932
5990
  debugLog("search", "Serper request START", { apiUrl, query });
5933
- const response = await fetch(apiUrl, {
5934
- method: "POST",
5935
- headers: {
5936
- [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5937
- [SEARCH_HEADER.X_API_KEY]: apiKey
5938
- },
5939
- body: JSON.stringify({ q: query })
5940
- });
5991
+ let response;
5992
+ try {
5993
+ response = await fetch(apiUrl, {
5994
+ method: "POST",
5995
+ headers: {
5996
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5997
+ [SEARCH_HEADER.X_API_KEY]: apiKey
5998
+ },
5999
+ body: JSON.stringify({ q: query }),
6000
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
6001
+ });
6002
+ } catch (err) {
6003
+ const msg = getErrorMessage(err);
6004
+ debugLog("search", "Serper fetch FAILED (network)", { error: msg });
6005
+ return { success: false, output: "", error: `Serper search network error: ${msg}. Check internet connection.` };
6006
+ }
5941
6007
  debugLog("search", "Serper response status", { status: response.status, ok: response.ok });
5942
6008
  if (!response.ok) {
5943
- const errorText = await response.text();
6009
+ let errorText = "";
6010
+ try {
6011
+ errorText = await response.text();
6012
+ } catch {
6013
+ }
5944
6014
  debugLog("search", "Serper response ERROR", { status: response.status, error: errorText });
5945
- throw new Error(`Serper API error: ${response.status}`);
6015
+ return { success: false, output: "", error: `Serper API error ${response.status}: ${errorText.slice(0, 500)}` };
6016
+ }
6017
+ let data;
6018
+ try {
6019
+ data = await response.json();
6020
+ } catch (err) {
6021
+ debugLog("search", "Serper JSON parse FAILED", { error: getErrorMessage(err) });
6022
+ return { success: false, output: "", error: `Serper search returned invalid JSON: ${getErrorMessage(err)}` };
5946
6023
  }
5947
- const data = await response.json();
5948
6024
  const results = data.organic || [];
5949
6025
  debugLog("search", "Serper results count", { count: results.length });
5950
6026
  if (results.length === 0) {
@@ -5959,20 +6035,36 @@ async function searchWithSerper(query, apiKey, apiUrl) {
5959
6035
  return { success: true, output: formatted };
5960
6036
  }
5961
6037
  async function searchWithGenericApi(query, apiKey, apiUrl) {
5962
- const response = await fetch(apiUrl, {
5963
- method: "POST",
5964
- headers: {
5965
- [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5966
- [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`,
5967
- [SEARCH_HEADER.X_API_KEY]: apiKey
5968
- },
5969
- body: JSON.stringify({ query, q: query })
5970
- });
6038
+ let response;
6039
+ try {
6040
+ response = await fetch(apiUrl, {
6041
+ method: "POST",
6042
+ headers: {
6043
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
6044
+ [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`,
6045
+ [SEARCH_HEADER.X_API_KEY]: apiKey
6046
+ },
6047
+ body: JSON.stringify({ query, q: query }),
6048
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
6049
+ });
6050
+ } catch (err) {
6051
+ const msg = getErrorMessage(err);
6052
+ return { success: false, output: "", error: `Search API network error: ${msg}. Check internet connection or API endpoint.` };
6053
+ }
5971
6054
  if (!response.ok) {
5972
- const errorText = await response.text();
5973
- throw new Error(`Search API error: ${response.status} - ${errorText}`);
6055
+ let errorText = "";
6056
+ try {
6057
+ errorText = await response.text();
6058
+ } catch {
6059
+ }
6060
+ return { success: false, output: "", error: `Search API error ${response.status}: ${errorText.slice(0, 500)}` };
6061
+ }
6062
+ let data;
6063
+ try {
6064
+ data = await response.json();
6065
+ } catch (err) {
6066
+ return { success: false, output: "", error: `Search API returned invalid JSON: ${getErrorMessage(err)}` };
5974
6067
  }
5975
- const data = await response.json();
5976
6068
  return { success: true, output: JSON.stringify(data, null, 2) };
5977
6069
  }
5978
6070
 
@@ -5982,7 +6074,7 @@ var PORT_STATE2 = {
5982
6074
  CLOSED: "closed",
5983
6075
  FILTERED: "filtered"
5984
6076
  };
5985
- function getErrorMessage(error) {
6077
+ function getErrorMessage2(error) {
5986
6078
  return error instanceof Error ? error.message : String(error);
5987
6079
  }
5988
6080
  async function parseNmap(xmlPath) {
@@ -6033,7 +6125,7 @@ async function parseNmap(xmlPath) {
6033
6125
  return {
6034
6126
  success: false,
6035
6127
  output: "",
6036
- error: getErrorMessage(error)
6128
+ error: getErrorMessage2(error)
6037
6129
  };
6038
6130
  }
6039
6131
  }
@@ -6044,7 +6136,7 @@ async function searchCVE(service, version) {
6044
6136
  return {
6045
6137
  success: false,
6046
6138
  output: "",
6047
- error: getErrorMessage(error)
6139
+ error: getErrorMessage2(error)
6048
6140
  };
6049
6141
  }
6050
6142
  }
@@ -6081,7 +6173,7 @@ async function searchExploitDB(service, version) {
6081
6173
  return {
6082
6174
  success: false,
6083
6175
  output: "",
6084
- error: getErrorMessage(error)
6176
+ error: getErrorMessage2(error)
6085
6177
  };
6086
6178
  }
6087
6179
  }
@@ -6128,11 +6220,11 @@ async function webSearch(query, _engine) {
6128
6220
  return await searchWithGenericApi(query, apiKey, apiUrl);
6129
6221
  }
6130
6222
  } catch (error) {
6131
- debugLog("search", "webSearch ERROR", { error: getErrorMessage(error) });
6223
+ debugLog("search", "webSearch ERROR", { error: getErrorMessage2(error) });
6132
6224
  return {
6133
6225
  success: false,
6134
6226
  output: "",
6135
- error: getErrorMessage(error)
6227
+ error: getErrorMessage2(error)
6136
6228
  };
6137
6229
  }
6138
6230
  }
@@ -6509,7 +6601,8 @@ ${results.join("\n\n")}`
6509
6601
  }
6510
6602
  return {
6511
6603
  success: false,
6512
- output: `Category ${category} not found in any edition.`
6604
+ output: `Category ${category} not found in any edition.`,
6605
+ error: `Category ${category} not found`
6513
6606
  };
6514
6607
  }
6515
6608
  if (edition === "all") {
@@ -6520,7 +6613,7 @@ ${results.join("\n\n")}`
6520
6613
  }
6521
6614
  const data = OWASP_FULL_HISTORY[edition];
6522
6615
  if (!data) {
6523
- return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.` };
6616
+ return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.`, error: `Edition ${edition} not found` };
6524
6617
  }
6525
6618
  return {
6526
6619
  success: true,
@@ -7065,8 +7158,8 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7065
7158
  }
7066
7159
  },
7067
7160
  execute: async (p) => {
7068
- const { existsSync: existsSync10, statSync: statSync2, readdirSync: readdirSync3 } = await import("fs");
7069
- const { join: join12 } = await import("path");
7161
+ const { existsSync: existsSync12, statSync: statSync3, readdirSync: readdirSync4 } = await import("fs");
7162
+ const { join: join14 } = await import("path");
7070
7163
  const category = p.category || "";
7071
7164
  const search = p.search || "";
7072
7165
  const minSize = p.min_size || 0;
@@ -7102,7 +7195,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7102
7195
  const processFile = (fullPath, fileName) => {
7103
7196
  const ext = fileName.split(".").pop()?.toLowerCase();
7104
7197
  if (!WORDLIST_EXTENSIONS.has(ext || "")) return;
7105
- const stats = statSync2(fullPath);
7198
+ const stats = statSync3(fullPath);
7106
7199
  if (stats.size < minSize) return;
7107
7200
  if (!matchesCategory(fullPath)) return;
7108
7201
  if (!matchesSearch(fullPath, fileName)) return;
@@ -7112,16 +7205,16 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7112
7205
  results.push("");
7113
7206
  };
7114
7207
  const scanDir = (dirPath, maxDepth = 3, depth = 0) => {
7115
- if (depth > maxDepth || !existsSync10(dirPath)) return;
7208
+ if (depth > maxDepth || !existsSync12(dirPath)) return;
7116
7209
  let entries;
7117
7210
  try {
7118
- entries = readdirSync3(dirPath, { withFileTypes: true });
7211
+ entries = readdirSync4(dirPath, { withFileTypes: true });
7119
7212
  } catch {
7120
7213
  return;
7121
7214
  }
7122
7215
  for (const entry of entries) {
7123
7216
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
7124
- const fullPath = join12(dirPath, entry.name);
7217
+ const fullPath = join14(dirPath, entry.name);
7125
7218
  if (entry.isDirectory()) {
7126
7219
  scanDir(fullPath, maxDepth, depth + 1);
7127
7220
  continue;
@@ -7498,8 +7591,8 @@ Requires root/sudo privileges.`,
7498
7591
  const iface = p.interface || "";
7499
7592
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
7500
7593
  const hostsFile = createTempFile(FILE_EXTENSIONS.HOSTS);
7501
- const { writeFileSync: writeFileSync8 } = await import("fs");
7502
- writeFileSync8(hostsFile, `${spoofIp} ${domain}
7594
+ const { writeFileSync: writeFileSync10 } = await import("fs");
7595
+ writeFileSync10(hostsFile, `${spoofIp} ${domain}
7503
7596
  ${spoofIp} *.${domain}
7504
7597
  `);
7505
7598
  const ifaceFlag = iface ? `-i ${iface}` : "";
@@ -8006,6 +8099,86 @@ Returns recommendations on process status, port conflicts, long-running tasks, e
8006
8099
  }
8007
8100
  ];
8008
8101
 
8102
+ // src/shared/constants/service-ports.ts
8103
+ var SERVICE_PORTS = {
8104
+ SSH: 22,
8105
+ FTP: 21,
8106
+ TELNET: 23,
8107
+ SMTP: 25,
8108
+ DNS: 53,
8109
+ HTTP: 80,
8110
+ POP3: 110,
8111
+ IMAP: 143,
8112
+ SMB_NETBIOS: 139,
8113
+ KERBEROS: 88,
8114
+ LDAP: 389,
8115
+ SMB: 445,
8116
+ HTTPS: 443,
8117
+ SMTPS: 465,
8118
+ SMTP_TLS: 587,
8119
+ MODBUS: 502,
8120
+ IMAPS: 993,
8121
+ POP3S: 995,
8122
+ MSSQL: 1433,
8123
+ MYSQL: 3306,
8124
+ RDP: 3389,
8125
+ POSTGRESQL: 5432,
8126
+ VNC: 5900,
8127
+ REDIS: 6379,
8128
+ DOCKER_HTTP: 2375,
8129
+ DOCKER_HTTPS: 2376,
8130
+ KUBERNETES_API: 6443,
8131
+ HTTP_ALT: 8080,
8132
+ HTTPS_ALT: 8443,
8133
+ NFS: 2049,
8134
+ DNP3: 2e4,
8135
+ MONGODB: 27017,
8136
+ ELASTICSEARCH: 9200,
8137
+ MEMCACHED: 11211,
8138
+ NODE_DEFAULT: 3e3,
8139
+ FLASK_DEFAULT: 5e3,
8140
+ DJANGO_DEFAULT: 8e3
8141
+ };
8142
+ var CRITICAL_SERVICE_PORTS = [
8143
+ SERVICE_PORTS.SSH,
8144
+ SERVICE_PORTS.RDP,
8145
+ SERVICE_PORTS.MYSQL,
8146
+ SERVICE_PORTS.POSTGRESQL,
8147
+ SERVICE_PORTS.REDIS,
8148
+ SERVICE_PORTS.MONGODB
8149
+ ];
8150
+ var NO_AUTH_CRITICAL_PORTS = [
8151
+ SERVICE_PORTS.REDIS,
8152
+ SERVICE_PORTS.MONGODB,
8153
+ SERVICE_PORTS.ELASTICSEARCH,
8154
+ SERVICE_PORTS.MEMCACHED
8155
+ ];
8156
+ var WEB_SERVICE_PORTS = [
8157
+ SERVICE_PORTS.HTTP,
8158
+ SERVICE_PORTS.HTTPS,
8159
+ SERVICE_PORTS.HTTP_ALT,
8160
+ SERVICE_PORTS.HTTPS_ALT,
8161
+ SERVICE_PORTS.NODE_DEFAULT,
8162
+ SERVICE_PORTS.FLASK_DEFAULT,
8163
+ SERVICE_PORTS.DJANGO_DEFAULT
8164
+ ];
8165
+ var PLAINTEXT_HTTP_PORTS = [
8166
+ SERVICE_PORTS.HTTP,
8167
+ SERVICE_PORTS.HTTP_ALT,
8168
+ SERVICE_PORTS.NODE_DEFAULT
8169
+ ];
8170
+ var DATABASE_PORTS = [
8171
+ SERVICE_PORTS.MYSQL,
8172
+ SERVICE_PORTS.POSTGRESQL,
8173
+ SERVICE_PORTS.MSSQL,
8174
+ SERVICE_PORTS.MONGODB,
8175
+ SERVICE_PORTS.REDIS
8176
+ ];
8177
+ var SMB_PORTS = [
8178
+ SERVICE_PORTS.SMB,
8179
+ SERVICE_PORTS.SMB_NETBIOS
8180
+ ];
8181
+
8009
8182
  // src/domains/network/tools.ts
8010
8183
  var NETWORK_TOOLS = [
8011
8184
  {
@@ -8051,7 +8224,7 @@ var NETWORK_CONFIG2 = {
8051
8224
  tools: NETWORK_TOOLS,
8052
8225
  dangerLevel: DANGER_LEVELS.ACTIVE,
8053
8226
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8054
- commonPorts: [21, 22, 80, 443, 445, 3389, 8080],
8227
+ commonPorts: [SERVICE_PORTS.FTP, SERVICE_PORTS.SSH, SERVICE_PORTS.HTTP, SERVICE_PORTS.HTTPS, SERVICE_PORTS.SMB, SERVICE_PORTS.RDP, SERVICE_PORTS.HTTP_ALT],
8055
8228
  commonServices: [SERVICES.FTP, SERVICES.SSH, SERVICES.HTTP, SERVICES.HTTPS, SERVICES.SMB]
8056
8229
  };
8057
8230
 
@@ -8114,7 +8287,7 @@ var WEB_CONFIG = {
8114
8287
  tools: WEB_TOOLS,
8115
8288
  dangerLevel: DANGER_LEVELS.ACTIVE,
8116
8289
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8117
- commonPorts: [80, 443, 8080],
8290
+ commonPorts: [SERVICE_PORTS.HTTP, SERVICE_PORTS.HTTPS, SERVICE_PORTS.HTTP_ALT],
8118
8291
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8119
8292
  };
8120
8293
 
@@ -8149,12 +8322,12 @@ var DATABASE_TOOLS = [
8149
8322
  description: "MySQL enumeration - version, users, databases",
8150
8323
  parameters: {
8151
8324
  target: { type: "string", description: "Target IP/hostname" },
8152
- port: { type: "string", description: "Port (default 3306)" }
8325
+ port: { type: "string", description: `Port (default ${SERVICE_PORTS.MYSQL})` }
8153
8326
  },
8154
8327
  required: ["target"],
8155
8328
  execute: async (params) => {
8156
8329
  const target = params.target;
8157
- const port = params.port || "3306";
8330
+ const port = params.port || String(SERVICE_PORTS.MYSQL);
8158
8331
  return await runCommand("mysql", ["-h", target, "-P", port, "-e", "SELECT VERSION(), USER(), DATABASE();"]);
8159
8332
  }
8160
8333
  },
@@ -8175,12 +8348,12 @@ var DATABASE_TOOLS = [
8175
8348
  description: "Redis enumeration",
8176
8349
  parameters: {
8177
8350
  target: { type: "string", description: "Target IP" },
8178
- port: { type: "string", description: "Port (default 6379)" }
8351
+ port: { type: "string", description: `Port (default ${SERVICE_PORTS.REDIS})` }
8179
8352
  },
8180
8353
  required: ["target"],
8181
8354
  execute: async (params) => {
8182
8355
  const target = params.target;
8183
- const port = params.port || "6379";
8356
+ const port = params.port || String(SERVICE_PORTS.REDIS);
8184
8357
  return await runCommand("redis-cli", ["-h", target, "-p", port, "INFO"]);
8185
8358
  }
8186
8359
  },
@@ -8212,7 +8385,7 @@ var DATABASE_CONFIG = {
8212
8385
  tools: DATABASE_TOOLS,
8213
8386
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8214
8387
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8215
- commonPorts: [1433, 3306, 5432, 6379, 27017],
8388
+ commonPorts: [SERVICE_PORTS.MSSQL, SERVICE_PORTS.MYSQL, SERVICE_PORTS.POSTGRESQL, SERVICE_PORTS.REDIS, SERVICE_PORTS.MONGODB],
8216
8389
  commonServices: [SERVICES.MYSQL, SERVICES.POSTGRES, SERVICES.REDIS, SERVICES.MONGODB]
8217
8390
  };
8218
8391
 
@@ -8265,7 +8438,7 @@ var AD_CONFIG = {
8265
8438
  tools: AD_TOOLS,
8266
8439
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8267
8440
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8268
- commonPorts: [88, 389, 445],
8441
+ commonPorts: [SERVICE_PORTS.KERBEROS, SERVICE_PORTS.LDAP, SERVICE_PORTS.SMB],
8269
8442
  commonServices: [SERVICES.AD, SERVICES.SMB]
8270
8443
  };
8271
8444
 
@@ -8289,7 +8462,7 @@ var EMAIL_CONFIG = {
8289
8462
  tools: EMAIL_TOOLS,
8290
8463
  dangerLevel: DANGER_LEVELS.ACTIVE,
8291
8464
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8292
- commonPorts: [25, 110, 143, 465, 587, 993, 995],
8465
+ commonPorts: [SERVICE_PORTS.SMTP, SERVICE_PORTS.POP3, SERVICE_PORTS.IMAP, SERVICE_PORTS.SMTPS, SERVICE_PORTS.SMTP_TLS, SERVICE_PORTS.IMAPS, SERVICE_PORTS.POP3S],
8293
8466
  commonServices: [SERVICES.SMTP, SERVICES.POP3, SERVICES.IMAP]
8294
8467
  };
8295
8468
 
@@ -8314,7 +8487,7 @@ var REMOTE_ACCESS_TOOLS = [
8314
8487
  },
8315
8488
  required: ["target"],
8316
8489
  execute: async (params) => {
8317
- return await runCommand("nmap", ["-p", "3389", "--script", "rdp-enum-encryption,rdp-ntlm-info", params.target]);
8490
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.RDP), "--script", "rdp-enum-encryption,rdp-ntlm-info", params.target]);
8318
8491
  }
8319
8492
  }
8320
8493
  ];
@@ -8324,7 +8497,7 @@ var REMOTE_ACCESS_CONFIG = {
8324
8497
  tools: REMOTE_ACCESS_TOOLS,
8325
8498
  dangerLevel: DANGER_LEVELS.ACTIVE,
8326
8499
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8327
- commonPorts: [22, 3389, 5900],
8500
+ commonPorts: [SERVICE_PORTS.SSH, SERVICE_PORTS.RDP, SERVICE_PORTS.VNC],
8328
8501
  commonServices: [SERVICES.SSH, SERVICES.RDP, SERVICES.VNC]
8329
8502
  };
8330
8503
 
@@ -8338,7 +8511,7 @@ var FILE_SHARING_TOOLS = [
8338
8511
  },
8339
8512
  required: ["target"],
8340
8513
  execute: async (params) => {
8341
- return await runCommand("nmap", ["-p", "21", "--script", "ftp-anon,ftp-syst", params.target]);
8514
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.FTP), "--script", "ftp-anon,ftp-syst", params.target]);
8342
8515
  }
8343
8516
  },
8344
8517
  {
@@ -8359,7 +8532,7 @@ var FILE_SHARING_CONFIG = {
8359
8532
  tools: FILE_SHARING_TOOLS,
8360
8533
  dangerLevel: DANGER_LEVELS.ACTIVE,
8361
8534
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8362
- commonPorts: [21, 139, 445, 2049],
8535
+ commonPorts: [SERVICE_PORTS.FTP, SERVICE_PORTS.SMB_NETBIOS, SERVICE_PORTS.SMB, SERVICE_PORTS.NFS],
8363
8536
  commonServices: [SERVICES.FTP, SERVICES.SMB, SERVICES.NFS]
8364
8537
  };
8365
8538
 
@@ -8403,7 +8576,7 @@ var CLOUD_CONFIG = {
8403
8576
  tools: CLOUD_TOOLS,
8404
8577
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8405
8578
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8406
- commonPorts: [443],
8579
+ commonPorts: [SERVICE_PORTS.HTTPS],
8407
8580
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8408
8581
  };
8409
8582
 
@@ -8438,7 +8611,7 @@ var CONTAINER_CONFIG = {
8438
8611
  tools: CONTAINER_TOOLS,
8439
8612
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8440
8613
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8441
- commonPorts: [2375, 2376, 5e3, 6443],
8614
+ commonPorts: [SERVICE_PORTS.DOCKER_HTTP, SERVICE_PORTS.DOCKER_HTTPS, SERVICE_PORTS.FLASK_DEFAULT, SERVICE_PORTS.KUBERNETES_API],
8442
8615
  commonServices: [SERVICES.DOCKER, SERVICES.KUBERNETES]
8443
8616
  };
8444
8617
 
@@ -8501,7 +8674,7 @@ var API_CONFIG = {
8501
8674
  tools: API_TOOLS,
8502
8675
  dangerLevel: DANGER_LEVELS.ACTIVE,
8503
8676
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8504
- commonPorts: [3e3, 5e3, 8e3, 8080],
8677
+ commonPorts: [SERVICE_PORTS.NODE_DEFAULT, SERVICE_PORTS.FLASK_DEFAULT, SERVICE_PORTS.DJANGO_DEFAULT, SERVICE_PORTS.HTTP_ALT],
8505
8678
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8506
8679
  };
8507
8680
 
@@ -8539,7 +8712,7 @@ var ICS_TOOLS = [
8539
8712
  },
8540
8713
  required: ["target"],
8541
8714
  execute: async (params) => {
8542
- return await runCommand("nmap", ["-p", "502", "--script", "modbus-discover", params.target]);
8715
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.MODBUS), "--script", "modbus-discover", params.target]);
8543
8716
  }
8544
8717
  }
8545
8718
  ];
@@ -8549,7 +8722,7 @@ var ICS_CONFIG = {
8549
8722
  tools: ICS_TOOLS,
8550
8723
  dangerLevel: DANGER_LEVELS.ACTIVE,
8551
8724
  defaultApproval: APPROVAL_LEVELS.BLOCK,
8552
- commonPorts: [502, 2e4],
8725
+ commonPorts: [SERVICE_PORTS.MODBUS, SERVICE_PORTS.DNP3],
8553
8726
  commonServices: [SERVICES.MODBUS, SERVICES.DNP3]
8554
8727
  };
8555
8728
 
@@ -8611,8 +8784,18 @@ var ToolRegistry = class {
8611
8784
  this.logDeniedAction(toolCall, approval.reason || "Execution denied");
8612
8785
  return { success: false, output: "", error: approval.reason || "Denied by policy" };
8613
8786
  }
8614
- const result2 = await tool.execute(toolCall.input);
8615
- const command = String(toolCall.input.command || toolCall.input.url || toolCall.input.query || "");
8787
+ let result2;
8788
+ try {
8789
+ result2 = await tool.execute(toolCall.input);
8790
+ } catch (execError) {
8791
+ const errMsg = execError instanceof Error ? execError.message : String(execError);
8792
+ result2 = {
8793
+ success: false,
8794
+ output: "",
8795
+ error: `Tool execution error: ${errMsg}`
8796
+ };
8797
+ }
8798
+ const command = String(toolCall.input.command || toolCall.input.url || toolCall.input.query || JSON.stringify(toolCall.input));
8616
8799
  if (result2.success) {
8617
8800
  this.state.workingMemory.recordSuccess(toolCall.name, command, result2.output || "");
8618
8801
  } else {
@@ -8715,7 +8898,7 @@ var SERVICE_CATEGORY_MAP = {
8715
8898
  "docker": SERVICE_CATEGORIES.CONTAINER,
8716
8899
  "modbus": SERVICE_CATEGORIES.ICS
8717
8900
  };
8718
- var CATEGORY_APPROVAL2 = {
8901
+ var CATEGORY_APPROVAL = {
8719
8902
  [SERVICE_CATEGORIES.NETWORK]: APPROVAL_LEVELS.CONFIRM,
8720
8903
  [SERVICE_CATEGORIES.WEB]: APPROVAL_LEVELS.CONFIRM,
8721
8904
  [SERVICE_CATEGORIES.DATABASE]: APPROVAL_LEVELS.REVIEW,
@@ -8786,80 +8969,80 @@ var ServiceParser = class {
8786
8969
 
8787
8970
  // src/domains/registry.ts
8788
8971
  import { join as join7, dirname as dirname3 } from "path";
8789
- import { fileURLToPath as fileURLToPath2 } from "url";
8790
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
8972
+ import { fileURLToPath } from "url";
8973
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
8791
8974
  var DOMAINS = {
8792
8975
  [SERVICE_CATEGORIES.NETWORK]: {
8793
8976
  id: SERVICE_CATEGORIES.NETWORK,
8794
8977
  name: "Network Infrastructure",
8795
8978
  description: "Vulnerability scanning, port mapping, and network service exploitation.",
8796
- promptPath: join7(__dirname2, "network/prompt.md")
8979
+ promptPath: join7(__dirname, "network/prompt.md")
8797
8980
  },
8798
8981
  [SERVICE_CATEGORIES.WEB]: {
8799
8982
  id: SERVICE_CATEGORIES.WEB,
8800
8983
  name: "Web Application",
8801
8984
  description: "Web app security testing, injection attacks, and auth bypass.",
8802
- promptPath: join7(__dirname2, "web/prompt.md")
8985
+ promptPath: join7(__dirname, "web/prompt.md")
8803
8986
  },
8804
8987
  [SERVICE_CATEGORIES.DATABASE]: {
8805
8988
  id: SERVICE_CATEGORIES.DATABASE,
8806
8989
  name: "Database Security",
8807
8990
  description: "SQL injection, database enumeration, and data extraction.",
8808
- promptPath: join7(__dirname2, "database/prompt.md")
8991
+ promptPath: join7(__dirname, "database/prompt.md")
8809
8992
  },
8810
8993
  [SERVICE_CATEGORIES.AD]: {
8811
8994
  id: SERVICE_CATEGORIES.AD,
8812
8995
  name: "Active Directory",
8813
8996
  description: "Kerberos, LDAP, and Windows domain privilege escalation.",
8814
- promptPath: join7(__dirname2, "ad/prompt.md")
8997
+ promptPath: join7(__dirname, "ad/prompt.md")
8815
8998
  },
8816
8999
  [SERVICE_CATEGORIES.EMAIL]: {
8817
9000
  id: SERVICE_CATEGORIES.EMAIL,
8818
9001
  name: "Email Services",
8819
9002
  description: "SMTP, IMAP, POP3 security and user enumeration.",
8820
- promptPath: join7(__dirname2, "email/prompt.md")
9003
+ promptPath: join7(__dirname, "email/prompt.md")
8821
9004
  },
8822
9005
  [SERVICE_CATEGORIES.REMOTE_ACCESS]: {
8823
9006
  id: SERVICE_CATEGORIES.REMOTE_ACCESS,
8824
9007
  name: "Remote Access",
8825
9008
  description: "SSH, RDP, VNC and other remote control protocols.",
8826
- promptPath: join7(__dirname2, "remote-access/prompt.md")
9009
+ promptPath: join7(__dirname, "remote-access/prompt.md")
8827
9010
  },
8828
9011
  [SERVICE_CATEGORIES.FILE_SHARING]: {
8829
9012
  id: SERVICE_CATEGORIES.FILE_SHARING,
8830
9013
  name: "File Sharing",
8831
9014
  description: "SMB, NFS, FTP and shared resource security.",
8832
- promptPath: join7(__dirname2, "file-sharing/prompt.md")
9015
+ promptPath: join7(__dirname, "file-sharing/prompt.md")
8833
9016
  },
8834
9017
  [SERVICE_CATEGORIES.CLOUD]: {
8835
9018
  id: SERVICE_CATEGORIES.CLOUD,
8836
9019
  name: "Cloud Infrastructure",
8837
9020
  description: "AWS, Azure, and GCP security and misconfiguration.",
8838
- promptPath: join7(__dirname2, "cloud/prompt.md")
9021
+ promptPath: join7(__dirname, "cloud/prompt.md")
8839
9022
  },
8840
9023
  [SERVICE_CATEGORIES.CONTAINER]: {
8841
9024
  id: SERVICE_CATEGORIES.CONTAINER,
8842
9025
  name: "Container Systems",
8843
9026
  description: "Docker and Kubernetes security testing.",
8844
- promptPath: join7(__dirname2, "container/prompt.md")
9027
+ promptPath: join7(__dirname, "container/prompt.md")
8845
9028
  },
8846
9029
  [SERVICE_CATEGORIES.API]: {
8847
9030
  id: SERVICE_CATEGORIES.API,
8848
9031
  name: "API Security",
8849
9032
  description: "REST, GraphQL, and SOAP API security testing.",
8850
- promptPath: join7(__dirname2, "api/prompt.md")
9033
+ promptPath: join7(__dirname, "api/prompt.md")
8851
9034
  },
8852
9035
  [SERVICE_CATEGORIES.WIRELESS]: {
8853
9036
  id: SERVICE_CATEGORIES.WIRELESS,
8854
9037
  name: "Wireless Networks",
8855
9038
  description: "WiFi and Bluetooth security testing.",
8856
- promptPath: join7(__dirname2, "wireless/prompt.md")
9039
+ promptPath: join7(__dirname, "wireless/prompt.md")
8857
9040
  },
8858
9041
  [SERVICE_CATEGORIES.ICS]: {
8859
9042
  id: SERVICE_CATEGORIES.ICS,
8860
9043
  name: "Industrial Systems",
8861
9044
  description: "Critical infrastructure - Modbus, DNP3, ENIP.",
8862
- promptPath: join7(__dirname2, "ics/prompt.md")
9045
+ promptPath: join7(__dirname, "ics/prompt.md")
8863
9046
  }
8864
9047
  };
8865
9048
 
@@ -8905,7 +9088,7 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8905
9088
  description: DOMAINS[id]?.description || "",
8906
9089
  tools: [...coreTools],
8907
9090
  dangerLevel: this.calculateDanger(id),
8908
- defaultApproval: CATEGORY_APPROVAL2[id] || "confirm"
9091
+ defaultApproval: CATEGORY_APPROVAL[id] || "confirm"
8909
9092
  });
8910
9093
  });
8911
9094
  }
@@ -8971,7 +9154,7 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8971
9154
  return Array.from(this.categories.values());
8972
9155
  }
8973
9156
  getApprovalForCategory(cat) {
8974
- return CATEGORY_APPROVAL2[cat] || "confirm";
9157
+ return CATEGORY_APPROVAL[cat] || "confirm";
8975
9158
  }
8976
9159
  };
8977
9160
 
@@ -9056,14 +9239,28 @@ var LLM_ERROR_TYPES = {
9056
9239
  var HTTP_STATUS = { BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, RATE_LIMIT: 429 };
9057
9240
  var NETWORK_ERROR_CODES = {
9058
9241
  ECONNRESET: "ECONNRESET",
9242
+ ECONNREFUSED: "ECONNREFUSED",
9059
9243
  ETIMEDOUT: "ETIMEDOUT",
9060
9244
  ENOTFOUND: "ENOTFOUND",
9061
- CONNECT_TIMEOUT: "UND_ERR_CONNECT_TIMEOUT"
9245
+ CONNECT_TIMEOUT: "UND_ERR_CONNECT_TIMEOUT",
9246
+ SOCKET_TIMEOUT: "UND_ERR_SOCKET"
9062
9247
  };
9063
9248
  var TRANSIENT_NETWORK_ERRORS = [
9064
9249
  NETWORK_ERROR_CODES.ECONNRESET,
9250
+ NETWORK_ERROR_CODES.ECONNREFUSED,
9065
9251
  NETWORK_ERROR_CODES.ETIMEDOUT,
9066
- NETWORK_ERROR_CODES.ENOTFOUND
9252
+ NETWORK_ERROR_CODES.ENOTFOUND,
9253
+ NETWORK_ERROR_CODES.SOCKET_TIMEOUT
9254
+ ];
9255
+ var NETWORK_ERROR_PATTERNS = [
9256
+ "fetch failed",
9257
+ "network error",
9258
+ "econnrefused",
9259
+ "econnreset",
9260
+ "enotfound",
9261
+ "etimedout",
9262
+ "socket hang up",
9263
+ "dns lookup failed"
9067
9264
  ];
9068
9265
  var LLMError = class extends Error {
9069
9266
  /** Structured error information */
@@ -9090,12 +9287,17 @@ function classifyError(error) {
9090
9287
  if (statusCode === HTTP_STATUS.BAD_REQUEST) {
9091
9288
  return { type: LLM_ERROR_TYPES.INVALID_REQUEST, message: errorMessage, statusCode, isRetryable: false, suggestedAction: "Modify request" };
9092
9289
  }
9093
- if (e.code && TRANSIENT_NETWORK_ERRORS.includes(e.code)) {
9290
+ const errorCode = e.code || e.cause?.code;
9291
+ if (errorCode && TRANSIENT_NETWORK_ERRORS.includes(errorCode)) {
9094
9292
  return { type: LLM_ERROR_TYPES.NETWORK_ERROR, message: errorMessage, isRetryable: true, suggestedAction: "Check network" };
9095
9293
  }
9096
- if (errorMessage.toLowerCase().includes("timeout") || e.code === NETWORK_ERROR_CODES.CONNECT_TIMEOUT) {
9294
+ if (errorMessage.toLowerCase().includes("timeout") || errorCode === NETWORK_ERROR_CODES.CONNECT_TIMEOUT) {
9097
9295
  return { type: LLM_ERROR_TYPES.TIMEOUT, message: errorMessage, isRetryable: true, suggestedAction: "Retry" };
9098
9296
  }
9297
+ const lowerMsg = errorMessage.toLowerCase();
9298
+ if (NETWORK_ERROR_PATTERNS.some((pattern) => lowerMsg.includes(pattern))) {
9299
+ return { type: LLM_ERROR_TYPES.NETWORK_ERROR, message: errorMessage, isRetryable: true, suggestedAction: "Check network" };
9300
+ }
9099
9301
  return { type: LLM_ERROR_TYPES.UNKNOWN, message: errorMessage, statusCode, isRetryable: false, suggestedAction: "Analyze error" };
9100
9302
  }
9101
9303
 
@@ -9174,7 +9376,11 @@ var LLMClient = class {
9174
9376
  signal
9175
9377
  });
9176
9378
  if (!response.ok) {
9177
- const errorBody = await response.text();
9379
+ let errorBody = `HTTP ${response.status}`;
9380
+ try {
9381
+ errorBody = await response.text();
9382
+ } catch {
9383
+ }
9178
9384
  const error = new Error(errorBody);
9179
9385
  error.status = response.status;
9180
9386
  throw error;
@@ -9476,10 +9682,10 @@ function logLLM(message, data) {
9476
9682
  }
9477
9683
 
9478
9684
  // src/engine/orchestrator/orchestrator.ts
9479
- import { fileURLToPath as fileURLToPath3 } from "url";
9685
+ import { fileURLToPath as fileURLToPath2 } from "url";
9480
9686
  import { dirname as dirname4, join as join8 } from "path";
9481
- var __filename2 = fileURLToPath3(import.meta.url);
9482
- var __dirname3 = dirname4(__filename2);
9687
+ var __filename = fileURLToPath2(import.meta.url);
9688
+ var __dirname2 = dirname4(__filename);
9483
9689
 
9484
9690
  // src/engine/state-persistence.ts
9485
9691
  import { writeFileSync as writeFileSync6, readFileSync as readFileSync4, existsSync as existsSync6, readdirSync, statSync, unlinkSync as unlinkSync4, rmSync } from "fs";
@@ -9547,7 +9753,10 @@ function loadState(state) {
9547
9753
  state.addLoot(loot);
9548
9754
  }
9549
9755
  for (const item of snapshot.todo) {
9550
- state.addTodo(item.content, item.priority);
9756
+ const id = state.addTodo(item.content, item.priority);
9757
+ if (item.status && item.status !== "pending") {
9758
+ state.updateTodo(id, { status: item.status });
9759
+ }
9551
9760
  }
9552
9761
  const validPhases = new Set(Object.values(PHASES));
9553
9762
  const restoredPhase = validPhases.has(snapshot.currentPhase) ? snapshot.currentPhase : PHASES.RECON;
@@ -9557,6 +9766,20 @@ function loadState(state) {
9557
9766
  }
9558
9767
  if (snapshot.missionChecklist?.length > 0) {
9559
9768
  state.addMissionChecklistItems(snapshot.missionChecklist.map((c) => c.text));
9769
+ const restoredChecklist = state.getMissionChecklist();
9770
+ const baseIndex = restoredChecklist.length - snapshot.missionChecklist.length;
9771
+ const completedUpdates = [];
9772
+ for (let i = 0; i < snapshot.missionChecklist.length; i++) {
9773
+ if (snapshot.missionChecklist[i].isCompleted && restoredChecklist[baseIndex + i]) {
9774
+ completedUpdates.push({
9775
+ id: restoredChecklist[baseIndex + i].id,
9776
+ isCompleted: true
9777
+ });
9778
+ }
9779
+ }
9780
+ if (completedUpdates.length > 0) {
9781
+ state.updateMissionChecklist(completedUpdates);
9782
+ }
9560
9783
  }
9561
9784
  return true;
9562
9785
  } catch (err) {
@@ -9570,8 +9793,9 @@ function clearWorkspace() {
9570
9793
  const dirsToClean = [
9571
9794
  { path: WORKSPACE.SESSIONS, label: "sessions" },
9572
9795
  { path: WORKSPACE.DEBUG, label: "debug logs" },
9573
- { path: WORKSPACE.TEMP, label: "temp files" },
9574
- { path: WORKSPACE.OUTPUTS, label: "outputs" }
9796
+ { path: WORKSPACE.TMP, label: "temp files" },
9797
+ { path: WORKSPACE.OUTPUTS, label: "outputs" },
9798
+ { path: WORKSPACE.JOURNAL, label: "journal" }
9575
9799
  ];
9576
9800
  for (const dir of dirsToClean) {
9577
9801
  try {
@@ -9664,306 +9888,66 @@ function appendBlockedCommandHints(lines, errorLower) {
9664
9888
  }
9665
9889
  }
9666
9890
 
9667
- // src/shared/utils/output-compressor.ts
9668
- var MIN_COMPRESS_LENGTH = 3e3;
9669
- var SUMMARY_HEADER = "\u2550\u2550\u2550 INTELLIGENCE SUMMARY (auto-extracted) \u2550\u2550\u2550";
9670
- var SUMMARY_FOOTER = "\u2550\u2550\u2550 END SUMMARY \u2014 Full output follows \u2550\u2550\u2550";
9671
- var EXTRACT_LIMITS = {
9672
- NMAP_PORTS: 30,
9673
- NMAP_VULNS: 10,
9674
- LINPEAS_SUDO: 500,
9675
- LINPEAS_WRITABLE: 300,
9676
- LINPEAS_CRON: 5,
9677
- LINPEAS_PASSWORDS: 5,
9678
- ENUM4LINUX_SHARES: 10,
9679
- DIRBUST_PATHS: 20,
9680
- SQLMAP_INJECTIONS: 5,
9681
- HASH_NTLM: 5,
9682
- HASH_PREVIEW_LEN: 100,
9683
- GENERIC_CREDS: 5,
9684
- GENERIC_PATHS: 10
9685
- };
9686
- var TOOL_SIGNATURES = [
9687
- {
9688
- name: "nmap",
9689
- signatures: [/Nmap scan report/i, /PORT\s+STATE\s+SERVICE/i, /Nmap done:/i],
9690
- extract: extractNmapIntel
9691
- },
9692
- {
9693
- name: "linpeas",
9694
- signatures: [/linpeas/i, /╔══.*╗/, /Linux Privilege Escalation/i],
9695
- extract: extractLinpeasIntel
9696
- },
9697
- {
9698
- name: "enum4linux",
9699
- signatures: [/enum4linux/i, /Starting enum4linux/i, /\|\s+Target\s+Information/i],
9700
- extract: extractEnum4linuxIntel
9701
- },
9702
- {
9703
- name: "gobuster/ffuf/feroxbuster",
9704
- signatures: [/Gobuster/i, /FFUF/i, /feroxbuster/i, /Status:\s*\d{3}/],
9705
- extract: extractDirBustIntel
9706
- },
9707
- {
9708
- name: "sqlmap",
9709
- signatures: [/sqlmap/i, /\[INFO\]\s+testing/i, /Parameter:\s+/i],
9710
- extract: extractSqlmapIntel
9711
- },
9712
- {
9713
- name: "hash_dump",
9714
- signatures: [/\$[0-9]\$/, /\$2[aby]\$/, /:[0-9]+:[a-f0-9]{32}:/i],
9715
- extract: extractHashIntel
9716
- }
9717
- ];
9718
- function compressToolOutput(output, toolName) {
9719
- if (!output || output.length < MIN_COMPRESS_LENGTH) {
9720
- return output;
9721
- }
9722
- let intel = [];
9723
- let detectedTool = "";
9724
- for (const sig of TOOL_SIGNATURES) {
9725
- const matched = sig.signatures.some((s) => s.test(output));
9726
- if (matched) {
9727
- detectedTool = sig.name;
9728
- intel = sig.extract(output);
9729
- break;
9730
- }
9891
+ // src/shared/utils/context-digest.ts
9892
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "fs";
9893
+ var PASSTHROUGH_THRESHOLD = 500;
9894
+ var PREPROCESS_THRESHOLD = 3e3;
9895
+ var MAX_PREPROCESSED_LINES = 800;
9896
+ var getOutputDir = () => WORKSPACE.OUTPUTS;
9897
+ var MAX_DUPLICATE_DISPLAY = 3;
9898
+ var ANALYST_MAX_INPUT_CHARS = 8e4;
9899
+ var FALLBACK_MAX_CHARS = 3e4;
9900
+ async function digestToolOutput(output, toolName, toolInput, analystFn) {
9901
+ const originalLength = output.length;
9902
+ if (originalLength < PASSTHROUGH_THRESHOLD) {
9903
+ return {
9904
+ digestedOutput: output,
9905
+ fullOutputPath: null,
9906
+ analystUsed: false,
9907
+ memo: null,
9908
+ originalLength,
9909
+ digestedLength: originalLength,
9910
+ compressionRatio: 1
9911
+ };
9731
9912
  }
9732
- if (intel.length === 0) {
9733
- intel = extractGenericIntel(output);
9734
- detectedTool = toolName || "unknown";
9913
+ const savedOutputPath = saveFullOutput(output, toolName);
9914
+ let preprocessed = output;
9915
+ if (originalLength > PREPROCESS_THRESHOLD) {
9916
+ preprocessed = structuralPreprocess(output);
9735
9917
  }
9736
- if (intel.length === 0) {
9737
- return output;
9918
+ if (analystFn) {
9919
+ try {
9920
+ const context = `Tool: ${toolName}${toolInput ? ` | Input: ${toolInput}` : ""}`;
9921
+ const rawAnalystResponse = await analystFn(preprocessed, context);
9922
+ const memo6 = parseAnalystMemo(rawAnalystResponse);
9923
+ const formatted = formatAnalystDigest(rawAnalystResponse, savedOutputPath, originalLength);
9924
+ return {
9925
+ digestedOutput: formatted,
9926
+ fullOutputPath: savedOutputPath,
9927
+ analystUsed: true,
9928
+ memo: memo6,
9929
+ originalLength,
9930
+ digestedLength: formatted.length,
9931
+ compressionRatio: formatted.length / originalLength
9932
+ };
9933
+ } catch (err) {
9934
+ debugLog("general", "Analyst LLM failed, falling back to truncation", {
9935
+ toolName,
9936
+ error: String(err)
9937
+ });
9938
+ }
9738
9939
  }
9739
- const summary = [
9740
- SUMMARY_HEADER,
9741
- `Tool: ${detectedTool} | Output length: ${output.length} chars`,
9742
- "",
9743
- ...intel,
9744
- "",
9745
- SUMMARY_FOOTER,
9746
- ""
9747
- ].join("\n");
9748
- return summary + output;
9749
- }
9750
- function extractNmapIntel(output) {
9751
- const intel = [];
9752
- const lines = output.split("\n");
9753
- const hostMatches = output.match(/Nmap scan report for\s+(\S+)/gi);
9754
- const openPorts = output.match(/^\d+\/\w+\s+open\s+/gm);
9755
- if (hostMatches) intel.push(`Hosts scanned: ${hostMatches.length}`);
9756
- if (openPorts) intel.push(`Open ports found: ${openPorts.length}`);
9757
- const portLines = lines.filter((l) => /^\d+\/\w+\s+open\s+/.test(l.trim()));
9758
- if (portLines.length > 0) {
9759
- intel.push("Open ports:");
9760
- for (const pl of portLines.slice(0, EXTRACT_LIMITS.NMAP_PORTS)) {
9761
- intel.push(` ${pl.trim()}`);
9762
- }
9763
- }
9764
- const vulnLines = lines.filter(
9765
- (l) => /VULNERABLE|CVE-\d{4}-\d+|exploit|CRITICAL/i.test(l)
9766
- );
9767
- if (vulnLines.length > 0) {
9768
- intel.push("\u26A0\uFE0F Vulnerability indicators:");
9769
- for (const vl of vulnLines.slice(0, EXTRACT_LIMITS.NMAP_VULNS)) {
9770
- intel.push(` ${vl.trim()}`);
9771
- }
9772
- }
9773
- const osMatch = output.match(/OS details:\s*(.+)/i) || output.match(/Running:\s*(.+)/i);
9774
- if (osMatch) intel.push(`OS: ${osMatch[1].trim()}`);
9775
- return intel;
9776
- }
9777
- function extractLinpeasIntel(output) {
9778
- const intel = [];
9779
- const suidSection = output.match(/SUID[\s\S]*?(?=\n[═╔╗━]+|\n\n\n)/i);
9780
- if (suidSection) {
9781
- const suidBins = suidSection[0].match(/\/\S+/g);
9782
- if (suidBins) {
9783
- const interestingSuid = suidBins.filter(
9784
- (b) => /python|perl|ruby|node|bash|vim|less|more|nano|find|nmap|awk|env|php|gcc|gdb|docker|strace|ltrace/i.test(b)
9785
- );
9786
- if (interestingSuid.length > 0) {
9787
- intel.push(`\u{1F534} Exploitable SUID binaries: ${interestingSuid.join(", ")}`);
9788
- }
9789
- }
9790
- }
9791
- const sudoMatch = output.match(/User \S+ may run[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
9792
- if (sudoMatch) {
9793
- intel.push(`\u{1F534} sudo -l: ${sudoMatch[0].trim().slice(0, EXTRACT_LIMITS.LINPEAS_SUDO)}`);
9794
- }
9795
- const writableMatch = output.match(/Interesting writable[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
9796
- if (writableMatch) {
9797
- intel.push(`\u{1F4DD} Writable: ${writableMatch[0].trim().slice(0, EXTRACT_LIMITS.LINPEAS_WRITABLE)}`);
9798
- }
9799
- const cronMatch = output.match(/Cron[\s\S]*?(?=\n\n|\n[═╔╗━])/i);
9800
- if (cronMatch) {
9801
- const cronLines = cronMatch[0].split("\n").filter((l) => l.includes("*") || /\/(root|cron)/i.test(l));
9802
- if (cronLines.length > 0) {
9803
- intel.push("\u23F0 Cron entries:");
9804
- cronLines.slice(0, EXTRACT_LIMITS.LINPEAS_CRON).forEach((c) => intel.push(` ${c.trim()}`));
9805
- }
9806
- }
9807
- const kernelMatch = output.match(/Linux version\s+(\S+)/i) || output.match(/Kernel:\s*(\S+)/i);
9808
- if (kernelMatch) intel.push(`\u{1F427} Kernel: ${kernelMatch[1]}`);
9809
- const passLines = output.split("\n").filter(
9810
- (l) => /password\s*[=:]\s*\S+/i.test(l) && !/\*\*\*|example|sample/i.test(l)
9811
- );
9812
- if (passLines.length > 0) {
9813
- intel.push("\u{1F511} Potential credentials found:");
9814
- passLines.slice(0, EXTRACT_LIMITS.LINPEAS_PASSWORDS).forEach((p) => intel.push(` ${p.trim()}`));
9815
- }
9816
- const cveMatches = output.match(/CVE-\d{4}-\d+/gi);
9817
- if (cveMatches) {
9818
- const uniqueCves = [...new Set(cveMatches)];
9819
- intel.push(`\u26A0\uFE0F CVEs mentioned: ${uniqueCves.join(", ")}`);
9820
- }
9821
- return intel;
9822
- }
9823
- function extractEnum4linuxIntel(output) {
9824
- const intel = [];
9825
- const userMatches = output.match(/user:\[(\S+?)\]/gi);
9826
- if (userMatches) {
9827
- const users = userMatches.map((u) => u.replace(/user:\[|\]/gi, ""));
9828
- intel.push(`\u{1F464} Users found: ${users.join(", ")}`);
9829
- }
9830
- const shareMatches = output.match(/Mapping: (\S+),\s*Listing: (\S+)/gi) || output.match(/\\\\\S+\\\\\S+/g);
9831
- if (shareMatches) {
9832
- intel.push(`\u{1F4C2} Shares: ${shareMatches.slice(0, EXTRACT_LIMITS.ENUM4LINUX_SHARES).join(", ")}`);
9833
- }
9834
- const domainMatch = output.match(/Domain:\s*\[(\S+?)\]/i) || output.match(/Workgroup:\s*\[(\S+?)\]/i);
9835
- if (domainMatch) intel.push(`\u{1F3E2} Domain: ${domainMatch[1]}`);
9836
- const policyMatch = output.match(/Password Complexity|Account Lockout/i);
9837
- if (policyMatch) intel.push("\u{1F510} Password policy information found");
9838
- return intel;
9839
- }
9840
- function extractDirBustIntel(output) {
9841
- const intel = [];
9842
- const lines = output.split("\n");
9843
- const interestingPaths = [];
9844
- for (const line of lines) {
9845
- const match = line.match(/(?:Status:\s*(\d{3})|(\d{3})\s+\d+\s+\d+).*?(\/\S+)/);
9846
- if (match) {
9847
- const status = match[1] || match[2];
9848
- const path2 = match[3];
9849
- if (["200", "301", "302", "403"].includes(status)) {
9850
- interestingPaths.push(`[${status}] ${path2}`);
9851
- }
9852
- }
9853
- const ffufMatch = line.match(/(\S+)\s+\[Status:\s*(\d+),?\s*Size:\s*(\d+)/);
9854
- if (ffufMatch && ["200", "301", "302", "403"].includes(ffufMatch[2])) {
9855
- interestingPaths.push(`[${ffufMatch[2]}] ${ffufMatch[1]} (${ffufMatch[3]}b)`);
9856
- }
9857
- }
9858
- if (interestingPaths.length > 0) {
9859
- intel.push(`\u{1F4C1} Discovered paths (${interestingPaths.length}):`);
9860
- interestingPaths.slice(0, EXTRACT_LIMITS.DIRBUST_PATHS).forEach((p) => intel.push(` ${p}`));
9861
- if (interestingPaths.length > EXTRACT_LIMITS.DIRBUST_PATHS) {
9862
- intel.push(` ... and ${interestingPaths.length - EXTRACT_LIMITS.DIRBUST_PATHS} more`);
9863
- }
9864
- }
9865
- return intel;
9866
- }
9867
- function extractSqlmapIntel(output) {
9868
- const intel = [];
9869
- const injectionTypes = output.match(/Type:\s*(\S.*?)(?:\n|$)/gi);
9870
- if (injectionTypes) {
9871
- intel.push("\u{1F489} SQL injection found:");
9872
- injectionTypes.slice(0, EXTRACT_LIMITS.SQLMAP_INJECTIONS).forEach((t) => intel.push(` ${t.trim()}`));
9873
- }
9874
- const dbMatch = output.match(/back-end DBMS:\s*(.+)/i);
9875
- if (dbMatch) intel.push(`\u{1F5C4}\uFE0F DBMS: ${dbMatch[1].trim()}`);
9876
- const dbListMatch = output.match(/available databases.*?:\s*([\s\S]*?)(?=\n\[|\n$)/i);
9877
- if (dbListMatch) {
9878
- intel.push(`\u{1F4CA} Databases: ${dbListMatch[1].trim().replace(/\n/g, ", ")}`);
9879
- }
9880
- const tableMatches = output.match(/Database:\s*\S+\s+Table:\s*\S+/gi);
9881
- if (tableMatches) {
9882
- intel.push(`\u{1F4CB} Tables dumped: ${tableMatches.length}`);
9883
- }
9884
- return intel;
9885
- }
9886
- function extractHashIntel(output) {
9887
- const intel = [];
9888
- const lines = output.split("\n");
9889
- const md5Hashes = lines.filter((l) => /\b[a-f0-9]{32}\b/i.test(l) && !l.includes("{"));
9890
- const sha256Hashes = lines.filter((l) => /\b[a-f0-9]{64}\b/i.test(l));
9891
- const unixHashes = lines.filter((l) => /\$[0-9]\$|\$2[aby]\$|\$6\$|\$5\$/i.test(l));
9892
- const ntlmHashes = lines.filter((l) => /:[0-9]+:[a-f0-9]{32}:[a-f0-9]{32}:::/i.test(l));
9893
- if (md5Hashes.length > 0) intel.push(`#\uFE0F\u20E3 MD5 hashes: ${md5Hashes.length}`);
9894
- if (sha256Hashes.length > 0) intel.push(`#\uFE0F\u20E3 SHA256 hashes: ${sha256Hashes.length}`);
9895
- if (unixHashes.length > 0) intel.push(`#\uFE0F\u20E3 Unix crypt hashes: ${unixHashes.length}`);
9896
- if (ntlmHashes.length > 0) {
9897
- intel.push(`#\uFE0F\u20E3 NTLM hashes: ${ntlmHashes.length}`);
9898
- ntlmHashes.slice(0, EXTRACT_LIMITS.HASH_NTLM).forEach((h) => intel.push(` ${h.trim().slice(0, EXTRACT_LIMITS.HASH_PREVIEW_LEN)}`));
9899
- }
9900
- return intel;
9901
- }
9902
- function extractGenericIntel(output) {
9903
- const intel = [];
9904
- const credPatterns = output.match(/(?:password|passwd|pwd|credentials?)\s*[=:]\s*\S+/gi);
9905
- if (credPatterns) {
9906
- intel.push("\u{1F511} Potential credentials detected:");
9907
- credPatterns.slice(0, EXTRACT_LIMITS.GENERIC_CREDS).forEach((c) => intel.push(` ${c.trim()}`));
9908
- }
9909
- const cves = output.match(/CVE-\d{4}-\d+/gi);
9910
- if (cves) {
9911
- const unique = [...new Set(cves)];
9912
- intel.push(`\u26A0\uFE0F CVEs mentioned: ${unique.join(", ")}`);
9913
- }
9914
- 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);
9915
- if (ips) {
9916
- const uniqueIps = [...new Set(ips)].filter(
9917
- (ip) => !ip.startsWith("0.") && !ip.startsWith("255.") && ip !== "127.0.0.1"
9918
- );
9919
- if (uniqueIps.length > 0 && uniqueIps.length <= 20) {
9920
- intel.push(`\u{1F310} IP addresses found: ${uniqueIps.join(", ")}`);
9921
- }
9922
- }
9923
- const paths = output.match(/\/(?:etc\/(?:shadow|passwd|sudoers)|root\/|home\/\S+|var\/www\/\S+|opt\/\S+)\S*/g);
9924
- if (paths) {
9925
- const uniquePaths = [...new Set(paths)].slice(0, EXTRACT_LIMITS.GENERIC_PATHS);
9926
- intel.push(`\u{1F4C2} Interesting paths: ${uniquePaths.join(", ")}`);
9927
- }
9928
- const flagPatterns = output.match(/(?:flag|secret|key|token)\{[^}]+\}/gi);
9929
- if (flagPatterns) {
9930
- intel.push("\u{1F3F4} FLAG/SECRET patterns detected:");
9931
- flagPatterns.forEach((f) => intel.push(` ${f}`));
9932
- }
9933
- return intel;
9940
+ const fallback = formatFallbackDigest(preprocessed, savedOutputPath, originalLength);
9941
+ return {
9942
+ digestedOutput: fallback,
9943
+ fullOutputPath: savedOutputPath,
9944
+ analystUsed: false,
9945
+ memo: null,
9946
+ originalLength,
9947
+ digestedLength: fallback.length,
9948
+ compressionRatio: fallback.length / originalLength
9949
+ };
9934
9950
  }
9935
-
9936
- // src/shared/utils/context-digest.ts
9937
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "fs";
9938
- var PASSTHROUGH_THRESHOLD = 3e3;
9939
- var LAYER2_THRESHOLD = 8e3;
9940
- var LAYER3_THRESHOLD = 5e4;
9941
- var MAX_REDUCED_LINES = 500;
9942
- var getOutputDir = () => WORKSPACE.OUTPUTS;
9943
- var MAX_DUPLICATE_DISPLAY = 3;
9944
- var LAYER3_MAX_INPUT_CHARS = 8e4;
9945
- var FALLBACK_MAX_CHARS = 3e4;
9946
- var SIGNAL_PATTERNS = [
9947
- /error|fail|denied|refused|timeout|exception/i,
9948
- /warning|warn|deprecated|insecure/i,
9949
- /success|found|detected|discovered|vulnerable|VULNERABLE/i,
9950
- /password|passwd|credential|secret|key|token|hash/i,
9951
- /CVE-\d{4}-\d+/i,
9952
- /\d+\/\w+\s+open\s+/,
9953
- // nmap open port
9954
- /flag\{|ctf\{|HTB\{|THM\{/i,
9955
- // CTF flags
9956
- /root:|admin:|www-data:|nobody:/,
9957
- // /etc/passwd entries
9958
- /\$[0-9]\$|\$2[aby]\$/,
9959
- // password hashes
9960
- /\b(?:192\.168|10\.|172\.(?:1[6-9]|2\d|3[01]))\.\d+\.\d+\b/,
9961
- // internal IPs
9962
- /BEGIN\s+(?:RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY/i,
9963
- // private keys
9964
- /Authorization:|Bearer\s+|Basic\s+/i
9965
- // auth tokens
9966
- ];
9967
9951
  var NOISE_PATTERNS = [
9968
9952
  /^\s*$/,
9969
9953
  // blank lines
@@ -9971,67 +9955,40 @@ var NOISE_PATTERNS = [
9971
9955
  // timestamp-only lines
9972
9956
  /^#+\s*$/,
9973
9957
  // separator lines
9974
- /^\s*Progress:\s*\[?[#=\-\s>]+\]?\s*\d+/i,
9958
+ /^\s*Progress:\s*\[?[#=\-\s>]+\]?\s*\d/i,
9975
9959
  // progress bars
9976
9960
  /\d+\s+requests?\s+(?:sent|made)/i,
9977
9961
  // request counters
9978
9962
  /^\s*(?:\.{3,}|={5,}|-{5,})\s*$/
9979
9963
  // decoration lines
9980
9964
  ];
9981
- async function digestToolOutput(output, toolName, toolInput, llmDigestFn) {
9982
- const originalLength = output.length;
9983
- if (originalLength < PASSTHROUGH_THRESHOLD) {
9984
- return {
9985
- digestedOutput: output,
9986
- fullOutputPath: null,
9987
- layersApplied: [],
9988
- originalLength,
9989
- digestedLength: originalLength,
9990
- compressionRatio: 1
9991
- };
9965
+ function structuralPreprocess(output) {
9966
+ let cleaned = stripAnsi(output);
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");
9992
9983
  }
9993
- const layersApplied = [];
9994
- let processed = output;
9995
- let savedOutputPath = null;
9996
- processed = compressToolOutput(processed, toolName);
9997
- layersApplied.push(1);
9998
- if (processed.length > LAYER2_THRESHOLD) {
9999
- processed = structuralReduce(processed);
10000
- layersApplied.push(2);
10001
- }
10002
- if (processed.length > LAYER3_THRESHOLD) {
10003
- savedOutputPath = saveFullOutput(output, toolName);
10004
- if (llmDigestFn) {
10005
- try {
10006
- const context = `Tool: ${toolName}${toolInput ? ` | Input: ${toolInput}` : ""}`;
10007
- const digest = await llmDigestFn(processed, context);
10008
- processed = formatLLMDigest(digest, savedOutputPath, originalLength);
10009
- layersApplied.push(3);
10010
- } catch (err) {
10011
- debugLog("general", "Context Digest Layer 3 failed, falling back", { toolName, error: String(err) });
10012
- processed = formatFallbackDigest(processed, savedOutputPath, originalLength);
10013
- layersApplied.push(3);
10014
- }
10015
- } else {
10016
- processed = formatFallbackDigest(processed, savedOutputPath, originalLength);
10017
- }
10018
- } else if (layersApplied.includes(2)) {
10019
- savedOutputPath = saveFullOutput(output, toolName);
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]`;
10020
9987
  }
10021
- return {
10022
- digestedOutput: processed,
10023
- fullOutputPath: savedOutputPath,
10024
- layersApplied,
10025
- originalLength,
10026
- digestedLength: processed.length,
10027
- compressionRatio: processed.length / originalLength
10028
- };
9988
+ return cleaned;
10029
9989
  }
10030
- function structuralReduce(output) {
10031
- let cleaned = stripAnsi(output);
10032
- const lines = cleaned.split("\n");
9990
+ function filterAndDedup(lines) {
10033
9991
  const result2 = [];
10034
- const duplicateCounts = /* @__PURE__ */ new Map();
10035
9992
  let lastLine = "";
10036
9993
  let consecutiveDupes = 0;
10037
9994
  for (const line of lines) {
@@ -10039,11 +9996,9 @@ function structuralReduce(output) {
10039
9996
  if (NOISE_PATTERNS.some((p) => p.test(trimmed))) {
10040
9997
  continue;
10041
9998
  }
10042
- const isSignal = SIGNAL_PATTERNS.some((p) => p.test(trimmed));
10043
9999
  const normalized = normalizeLine(trimmed);
10044
- if (normalized === normalizeLine(lastLine) && !isSignal) {
10000
+ if (normalized === normalizeLine(lastLine)) {
10045
10001
  consecutiveDupes++;
10046
- duplicateCounts.set(normalized, (duplicateCounts.get(normalized) || 1) + 1);
10047
10002
  continue;
10048
10003
  }
10049
10004
  if (consecutiveDupes > 0) {
@@ -10066,57 +10021,104 @@ function structuralReduce(output) {
10066
10021
  result2.push(lastLine);
10067
10022
  }
10068
10023
  }
10069
- if (result2.length > MAX_REDUCED_LINES) {
10070
- const headSize = Math.floor(MAX_REDUCED_LINES * 0.4);
10071
- const tailSize = Math.floor(MAX_REDUCED_LINES * 0.3);
10072
- const signalBudget = MAX_REDUCED_LINES - headSize - tailSize;
10073
- const head = result2.slice(0, headSize);
10074
- const tail = result2.slice(-tailSize);
10075
- const middle = result2.slice(headSize, -tailSize);
10076
- const middleSignals = middle.filter((line) => SIGNAL_PATTERNS.some((p) => p.test(line))).slice(0, signalBudget);
10077
- const skipped = middle.length - middleSignals.length;
10078
- cleaned = [
10079
- ...head,
10080
- "",
10081
- `... [${skipped} routine lines skipped \u2014 ${middleSignals.length} important lines preserved] ...`,
10082
- "",
10083
- ...middleSignals,
10084
- "",
10085
- `... [resuming last ${tailSize} lines] ...`,
10086
- "",
10087
- ...tail
10088
- ].join("\n");
10089
- } else {
10090
- cleaned = result2.join("\n");
10091
- }
10092
- return cleaned;
10024
+ return result2;
10093
10025
  }
10094
- var DIGEST_SYSTEM_PROMPT = `You are a pentesting output analyst. Given raw tool output, extract ONLY actionable intelligence. Be terse and structured.
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.
10095
10027
 
10096
10028
  FORMAT YOUR RESPONSE EXACTLY LIKE THIS:
10029
+
10097
10030
  ## Key Findings
10098
- - [finding 1]
10031
+ - [finding 1 with exact values: ports, versions, paths]
10099
10032
  - [finding 2]
10100
10033
 
10101
10034
  ## Credentials/Secrets
10102
- - [any discovered credentials, hashes, tokens, keys]
10035
+ - [any discovered credentials, hashes, tokens, keys, certificates]
10036
+ - (write "None found" if none)
10103
10037
 
10104
10038
  ## Attack Vectors
10105
- - [exploitable services, vulnerabilities, misconfigurations]
10039
+ - [exploitable services, vulnerabilities, misconfigurations, CVEs]
10040
+ - (write "None identified" if none)
10041
+
10042
+ ## Failures/Errors
10043
+ - [what was attempted and FAILED \u2014 include the FULL command, wordlist, target, and the reason WHY it failed]
10044
+ - [e.g.: "SSH brute force: hydra -l admin -P /usr/share/wordlists/rockyou.txt ssh://10.0.0.1 \u2014 connection refused (port filtered)"]
10045
+ - [e.g.: "SQLi on /login with sqlmap --tamper=space2comment \u2014 input sanitized, WAF detected (ModSecurity)"]
10046
+ - (write "No failures" if everything succeeded)
10047
+
10048
+ ## Suspicious Signals
10049
+ - [anomalies that are NOT confirmed vulnerabilities but suggest exploitable surface]
10050
+ - [e.g.: "Response time 3x slower on /admin path \u2014 possible auth check or backend processing"]
10051
+ - [e.g.: "X-Debug-Token header present \u2014 debug mode may be enabled"]
10052
+ - [e.g.: "Verbose error message reveals stack trace / internal path / DB schema"]
10053
+ - [e.g.: "Unexpected 302 redirect with session param leaked in URL"]
10054
+ - (write "No suspicious signals" if nothing anomalous)
10055
+
10056
+ ## Attack Value
10057
+ - [ONE word: HIGH / MED / LOW / NONE]
10058
+ - Reasoning: [1 sentence why \u2014 what makes this worth pursuing or abandoning]
10106
10059
 
10107
10060
  ## Next Steps
10108
10061
  - [recommended immediate actions based on findings]
10109
10062
 
10110
10063
  RULES:
10111
- - Be EXTREMELY concise \u2014 max 30 lines total
10112
- - Only include ACTIONABLE findings \u2014 skip routine/expected results
10064
+ - Include EXACT values: port numbers, versions, usernames, file paths, IPs, full commands used
10065
+ - For failures: include the COMPLETE command with all flags, wordlists, and targets \u2014 "brute force failed" alone is USELESS
10066
+ - Look for the UNEXPECTED \u2014 non-standard ports, unusual banners, timing anomalies, error leaks
10067
+ - Credentials include: passwords, hashes, API keys, tokens, private keys, cookies, session IDs
10068
+ - Flag any information disclosure: server versions, internal paths, stack traces, debug output
10113
10069
  - If nothing interesting found, say "No actionable findings in this output"
10114
- - Include exact values: port numbers, versions, usernames, file paths
10115
- - Never include decorative output, banners, or progress information`;
10116
- function formatLLMDigest(digest, filePath, originalChars) {
10070
+ - Never include decorative output, banners, or progress information
10071
+ - Do NOT miss subtle signals: unusual HTTP headers, non-standard responses, timing differences
10072
+ - Write as much detail as needed \u2014 do NOT artificially shorten. Every detail matters for strategy.
10073
+
10074
+ ## Reflection
10075
+ - What this output tells us: [1-line assessment]
10076
+ - Recommended next action: [1-2 specific follow-up actions]`;
10077
+ function parseAnalystMemo(response) {
10078
+ const sections = {};
10079
+ let currentSection = "";
10080
+ let reflectionLines = [];
10081
+ let attackValueLine = "NONE";
10082
+ let attackValueReasoning = "";
10083
+ for (const line of response.split("\n")) {
10084
+ if (line.startsWith("## ")) {
10085
+ currentSection = line.replace("## ", "").trim().toLowerCase();
10086
+ sections[currentSection] = [];
10087
+ } else if (currentSection === "reflection") {
10088
+ if (line.trim()) reflectionLines.push(line.trim());
10089
+ } else if (currentSection === "attack value") {
10090
+ const match = line.match(/\b(HIGH|MED|LOW|NONE)\b/);
10091
+ if (match) attackValueLine = match[1];
10092
+ const reasonMatch = line.match(/[Rr]easoning:\s*(.+)/);
10093
+ if (reasonMatch) attackValueReasoning = reasonMatch[1].trim();
10094
+ } else if (currentSection) {
10095
+ const trimmed = line.trim();
10096
+ if (!trimmed) continue;
10097
+ const content = trimmed.replace(/^(?:-|\*|\d+[.)]\s*)\s*/, "").trim();
10098
+ if (content) sections[currentSection].push(content);
10099
+ }
10100
+ }
10101
+ const filterNone = (items) => items.filter((i) => !/(^none|^no )/i.test(i.trim()));
10102
+ const rawValue = attackValueLine.toUpperCase();
10103
+ const attackValue = ["HIGH", "MED", "LOW", "NONE"].includes(rawValue) ? rawValue : "LOW";
10104
+ return {
10105
+ keyFindings: filterNone(sections["key findings"] || []),
10106
+ credentials: filterNone(sections["credentials/secrets"] || []),
10107
+ attackVectors: filterNone(sections["attack vectors"] || []),
10108
+ failures: filterNone(sections["failures/errors"] || []),
10109
+ suspicions: filterNone(sections["suspicious signals"] || []),
10110
+ attackValue,
10111
+ nextSteps: filterNone(sections["next steps"] || []),
10112
+ reflection: [
10113
+ attackValueReasoning ? `[${attackValue}] ${attackValueReasoning}` : "",
10114
+ ...reflectionLines
10115
+ ].filter(Boolean).join(" | ")
10116
+ };
10117
+ }
10118
+ function formatAnalystDigest(digest, filePath, originalChars) {
10117
10119
  return [
10118
10120
  "\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",
10119
- "\u2551 CONTEXT DIGEST (LLM-summarized) \u2551",
10121
+ "\u2551 ANALYST DIGEST (Independent LLM analysis) \u2551",
10120
10122
  "\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",
10121
10123
  "",
10122
10124
  digest,
@@ -10126,29 +10128,26 @@ function formatLLMDigest(digest, filePath, originalChars) {
10126
10128
  ].join("\n");
10127
10129
  }
10128
10130
  function formatFallbackDigest(processed, filePath, originalChars) {
10129
- const lines = processed.split("\n");
10130
- const summaryEndIdx = lines.findIndex((l) => l.includes("END SUMMARY"));
10131
- const summaryBlock = summaryEndIdx > 0 ? lines.slice(0, summaryEndIdx + 1).join("\n") : "";
10132
- const remaining = summaryEndIdx > 0 ? lines.slice(summaryEndIdx + 1).join("\n") : processed;
10133
10131
  const maxChars = FALLBACK_MAX_CHARS;
10134
- let truncatedRemaining = remaining;
10135
- if (remaining.length > maxChars) {
10132
+ let truncated = processed;
10133
+ if (processed.length > maxChars) {
10136
10134
  const headChars = Math.floor(maxChars * 0.6);
10137
10135
  const tailChars = Math.floor(maxChars * 0.4);
10138
- const skipped = remaining.length - headChars - tailChars;
10139
- truncatedRemaining = remaining.slice(0, headChars) + `
10136
+ const skipped = processed.length - headChars - tailChars;
10137
+ truncated = processed.slice(0, headChars) + `
10140
10138
 
10141
- ... [${skipped} chars omitted \u2014 read full output from file] ...
10139
+ ... [${skipped} chars omitted \u2014 Analyst LLM unavailable, read full output from file] ...
10142
10140
 
10143
- ` + remaining.slice(-tailChars);
10141
+ ` + processed.slice(-tailChars);
10144
10142
  }
10145
10143
  return [
10146
- summaryBlock,
10147
- truncatedRemaining,
10144
+ "\u26A0\uFE0F ANALYST UNAVAILABLE \u2014 showing truncated raw output:",
10145
+ "",
10146
+ truncated,
10148
10147
  "",
10149
10148
  `\u{1F4C2} Full output saved: ${filePath} (${originalChars} chars)`,
10150
10149
  `\u{1F4A1} Use read_file("${filePath}") to see the complete raw output.`
10151
- ].filter(Boolean).join("\n");
10150
+ ].join("\n");
10152
10151
  }
10153
10152
  function stripAnsi(text) {
10154
10153
  return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1B\[[\d;]*m/g, "");
@@ -10174,20 +10173,21 @@ function saveFullOutput(output, toolName) {
10174
10173
  }
10175
10174
  function createLLMDigestFn(llmClient) {
10176
10175
  return async (text, context) => {
10177
- const truncatedText = text.length > LAYER3_MAX_INPUT_CHARS ? text.slice(0, LAYER3_MAX_INPUT_CHARS) + `
10178
- ... [truncated for summarization, ${text.length - LAYER3_MAX_INPUT_CHARS} chars omitted]` : text;
10179
- const messages = [{ role: LLM_ROLES.USER, content: `Analyze this pentesting tool output and extract actionable intelligence.
10176
+ const messages = [{
10177
+ role: LLM_ROLES.USER,
10178
+ content: `Analyze this pentesting tool output and extract actionable intelligence.
10180
10179
 
10181
10180
  Context: ${context}
10182
10181
 
10183
10182
  --- OUTPUT START ---
10184
- ${truncatedText}
10185
- --- OUTPUT END ---` }];
10183
+ ${text}
10184
+ --- OUTPUT END ---`
10185
+ }];
10186
10186
  const response = await llmClient.generateResponse(
10187
10187
  messages,
10188
10188
  void 0,
10189
- // no tools — summarization only
10190
- DIGEST_SYSTEM_PROMPT
10189
+ // no tools — analysis only
10190
+ ANALYST_SYSTEM_PROMPT
10191
10191
  );
10192
10192
  return response.content || "No actionable findings extracted.";
10193
10193
  };
@@ -10202,6 +10202,16 @@ var CoreAgent = class _CoreAgent {
10202
10202
  agentType;
10203
10203
  maxIterations;
10204
10204
  abortController = null;
10205
+ /**
10206
+ * Collected tool execution records for the current turn.
10207
+ * MainAgent reads this after each step to write journal entries.
10208
+ * Cleared at the start of each step.
10209
+ */
10210
+ turnToolJournal = [];
10211
+ /** Aggregated memo from all tools in the current turn */
10212
+ turnMemo = { keyFindings: [], credentials: [], attackVectors: [], failures: [], suspicions: [], attackValue: "LOW", nextSteps: [] };
10213
+ /** Analyst reflections collected during this turn (1-line assessments) */
10214
+ turnReflections = [];
10205
10215
  constructor(agentType, state, events, toolRegistry, maxIterations) {
10206
10216
  this.agentType = agentType;
10207
10217
  this.state = state;
@@ -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)) {
@@ -10329,7 +10308,22 @@ RULES:
10329
10308
  });
10330
10309
  continue;
10331
10310
  }
10332
- throw error;
10311
+ const unexpectedMsg = error instanceof Error ? error.message : String(error);
10312
+ this.events.emit({
10313
+ type: EVENT_TYPES.ERROR,
10314
+ timestamp: Date.now(),
10315
+ data: {
10316
+ message: `Unexpected error: ${unexpectedMsg}`,
10317
+ phase: this.state.getPhase(),
10318
+ isRecoverable: true
10319
+ }
10320
+ });
10321
+ messages.push({
10322
+ role: LLM_ROLES.USER,
10323
+ content: `\u26A0\uFE0F UNEXPECTED ERROR: ${unexpectedMsg}
10324
+ This may be a transient issue. Continue your task \u2014 retry the last action or try an alternative approach.`
10325
+ });
10326
+ continue;
10333
10327
  }
10334
10328
  }
10335
10329
  const summary = `Max iterations (${this.maxIterations}) reached. Progress: ${progress.totalToolsExecuted} tools executed (${progress.toolSuccesses} succeeded, ${progress.toolErrors} failed). Current phase: ${this.state.getPhase()}. Findings: ${this.state.getFindings?.()?.length ?? "unknown"}.`;
@@ -10461,6 +10455,48 @@ ${firstLine}`, phase }
10461
10455
  return callbacks;
10462
10456
  }
10463
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
+ // ─────────────────────────────────────────────────────────────────
10464
10500
  // SUBSECTION: Event Emitters
10465
10501
  // ─────────────────════════════════════════════════════════════
10466
10502
  emitThink(iteration, progress) {
@@ -10625,49 +10661,21 @@ ${firstLine}`, phase }
10625
10661
  const toolStartTime = Date.now();
10626
10662
  logLLM("CoreAgent executing tool", { id: call.id, name: call.name, input: call.input });
10627
10663
  if (!this.toolRegistry) {
10628
- return {
10629
- toolCallId: call.id,
10630
- output: "",
10631
- error: "Tool registry not initialized. Call setToolRegistry() first."
10632
- };
10664
+ return { toolCallId: call.id, output: "", error: "Tool registry not initialized. Call setToolRegistry() first." };
10633
10665
  }
10634
10666
  try {
10635
- const result2 = await this.toolRegistry.execute({
10636
- name: call.name,
10637
- input: call.input
10638
- });
10667
+ const result2 = await this.toolRegistry.execute({ name: call.name, input: call.input });
10639
10668
  let outputText = result2.output ?? "";
10640
10669
  this.scanForFlags(outputText);
10641
- if (result2.error) {
10642
- outputText = this.enrichToolError({ toolName: call.name, input: call.input, error: result2.error, originalOutput: outputText, progress });
10643
- if (progress) progress.toolErrors++;
10644
- } else {
10645
- if (progress) {
10646
- progress.toolSuccesses++;
10647
- progress.blockedCommandPatterns.clear();
10648
- }
10649
- }
10650
- try {
10651
- const llmDigestFn = createLLMDigestFn(this.llm);
10652
- const digestResult = await digestToolOutput(
10653
- outputText,
10654
- call.name,
10655
- JSON.stringify(call.input).slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
10656
- llmDigestFn
10657
- );
10658
- outputText = digestResult.digestedOutput;
10659
- } catch {
10660
- if (outputText.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
10661
- const truncated = outputText.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
10662
- const remaining = outputText.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
10663
- outputText = `${truncated}
10664
-
10665
- ... [TRUNCATED ${remaining} characters for context hygiene] ...
10666
- \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.`;
10667
- }
10668
- }
10669
- this.emitToolResult(call.name, result2.success, outputText, result2.error, Date.now() - toolStartTime);
10670
- return { toolCallId: call.id, output: outputText, error: result2.error };
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);
10678
+ return { toolCallId: call.id, output: digestedOutputForLLM, error: result2.error };
10671
10679
  } catch (error) {
10672
10680
  const errorMsg = String(error);
10673
10681
  const enrichedError = this.enrichToolError({ toolName: call.name, input: call.input, error: errorMsg, originalOutput: "", progress });
@@ -10676,6 +10684,90 @@ ${firstLine}`, phase }
10676
10684
  return { toolCallId: call.id, output: enrichedError, error: errorMsg };
10677
10685
  }
10678
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
+ }
10679
10771
  /**
10680
10772
  * Enrich tool error — delegates to extracted module (§3-1)
10681
10773
  */
@@ -10742,9 +10834,9 @@ ${firstLine}`, phase }
10742
10834
  };
10743
10835
 
10744
10836
  // src/agents/prompt-builder.ts
10745
- import { readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
10746
- import { join as join10, dirname as dirname5 } from "path";
10747
- import { fileURLToPath as fileURLToPath4 } from "url";
10837
+ import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
10838
+ import { join as join11, dirname as dirname5 } from "path";
10839
+ import { fileURLToPath as fileURLToPath3 } from "url";
10748
10840
 
10749
10841
  // src/shared/constants/prompts.ts
10750
10842
  var PROMPT_PATHS = {
@@ -10803,73 +10895,44 @@ var PROMPT_CONFIG = {
10803
10895
  var INITIAL_TASKS = {
10804
10896
  RECON: "Initial reconnaissance and target discovery"
10805
10897
  };
10806
-
10807
- // src/shared/constants/service-ports.ts
10808
- var SERVICE_PORTS = {
10809
- SSH: 22,
10810
- FTP: 21,
10811
- TELNET: 23,
10812
- SMTP: 25,
10813
- DNS: 53,
10814
- HTTP: 80,
10815
- POP3: 110,
10816
- IMAP: 143,
10817
- SMB_NETBIOS: 139,
10818
- SMB: 445,
10819
- HTTPS: 443,
10820
- MSSQL: 1433,
10821
- MYSQL: 3306,
10822
- RDP: 3389,
10823
- POSTGRESQL: 5432,
10824
- REDIS: 6379,
10825
- HTTP_ALT: 8080,
10826
- HTTPS_ALT: 8443,
10827
- MONGODB: 27017,
10828
- ELASTICSEARCH: 9200,
10829
- MEMCACHED: 11211,
10830
- NODE_DEFAULT: 3e3,
10831
- FLASK_DEFAULT: 5e3,
10832
- DJANGO_DEFAULT: 8e3
10833
- };
10834
- var CRITICAL_SERVICE_PORTS = [
10835
- SERVICE_PORTS.SSH,
10836
- SERVICE_PORTS.RDP,
10837
- SERVICE_PORTS.MYSQL,
10838
- SERVICE_PORTS.POSTGRESQL,
10839
- SERVICE_PORTS.REDIS,
10840
- SERVICE_PORTS.MONGODB
10841
- ];
10842
- var NO_AUTH_CRITICAL_PORTS = [
10843
- SERVICE_PORTS.REDIS,
10844
- SERVICE_PORTS.MONGODB,
10845
- SERVICE_PORTS.ELASTICSEARCH,
10846
- SERVICE_PORTS.MEMCACHED
10847
- ];
10848
- var WEB_SERVICE_PORTS = [
10849
- SERVICE_PORTS.HTTP,
10850
- SERVICE_PORTS.HTTPS,
10851
- SERVICE_PORTS.HTTP_ALT,
10852
- SERVICE_PORTS.HTTPS_ALT,
10853
- SERVICE_PORTS.NODE_DEFAULT,
10854
- SERVICE_PORTS.FLASK_DEFAULT,
10855
- SERVICE_PORTS.DJANGO_DEFAULT
10856
- ];
10857
- var PLAINTEXT_HTTP_PORTS = [
10858
- SERVICE_PORTS.HTTP,
10859
- SERVICE_PORTS.HTTP_ALT,
10860
- SERVICE_PORTS.NODE_DEFAULT
10861
- ];
10862
- var DATABASE_PORTS = [
10863
- SERVICE_PORTS.MYSQL,
10864
- SERVICE_PORTS.POSTGRESQL,
10865
- SERVICE_PORTS.MSSQL,
10866
- SERVICE_PORTS.MONGODB,
10867
- SERVICE_PORTS.REDIS
10868
- ];
10869
- var SMB_PORTS = [
10870
- SERVICE_PORTS.SMB,
10871
- SERVICE_PORTS.SMB_NETBIOS
10872
- ];
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.`;
10873
10936
 
10874
10937
  // src/shared/constants/scoring.ts
10875
10938
  var ATTACK_SCORING = {
@@ -11031,10 +11094,225 @@ function getAttacksForService(service, port) {
11031
11094
  return attacks;
11032
11095
  }
11033
11096
 
11097
+ // src/shared/utils/journal.ts
11098
+ import { writeFileSync as writeFileSync8, readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync5 } from "fs";
11099
+ import { join as join10 } from "path";
11100
+ var MAX_JOURNAL_ENTRIES = 50;
11101
+ var MAX_OUTPUT_FILES = 30;
11102
+ var TURN_PREFIX = "turn-";
11103
+ var SUMMARY_FILE = "summary.md";
11104
+ function writeJournalEntry(entry) {
11105
+ try {
11106
+ const journalDir = WORKSPACE.JOURNAL;
11107
+ ensureDirExists(journalDir);
11108
+ const padded = String(entry.turn).padStart(4, "0");
11109
+ const filePath = join10(journalDir, `${TURN_PREFIX}${padded}.json`);
11110
+ writeFileSync8(filePath, JSON.stringify(entry, null, 2), "utf-8");
11111
+ return filePath;
11112
+ } catch (err) {
11113
+ debugLog("general", "Failed to write journal entry", { turn: entry.turn, error: String(err) });
11114
+ return null;
11115
+ }
11116
+ }
11117
+ function readJournalSummary() {
11118
+ try {
11119
+ const summaryPath = join10(WORKSPACE.JOURNAL, SUMMARY_FILE);
11120
+ if (!existsSync8(summaryPath)) return "";
11121
+ return readFileSync5(summaryPath, "utf-8");
11122
+ } catch {
11123
+ return "";
11124
+ }
11125
+ }
11126
+ function getRecentEntries(count = MAX_JOURNAL_ENTRIES) {
11127
+ try {
11128
+ const journalDir = WORKSPACE.JOURNAL;
11129
+ if (!existsSync8(journalDir)) return [];
11130
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort().slice(-count);
11131
+ const entries = [];
11132
+ for (const file of files) {
11133
+ try {
11134
+ const raw = readFileSync5(join10(journalDir, file), "utf-8");
11135
+ entries.push(JSON.parse(raw));
11136
+ } catch {
11137
+ }
11138
+ }
11139
+ return entries;
11140
+ } catch {
11141
+ return [];
11142
+ }
11143
+ }
11144
+ function getNextTurnNumber() {
11145
+ try {
11146
+ const journalDir = WORKSPACE.JOURNAL;
11147
+ if (!existsSync8(journalDir)) return 1;
11148
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort();
11149
+ if (files.length === 0) return 1;
11150
+ const lastFile = files[files.length - 1];
11151
+ const match = lastFile.match(/turn-(\d+)\.json/);
11152
+ return match ? parseInt(match[1], 10) + 1 : 1;
11153
+ } catch {
11154
+ return 1;
11155
+ }
11156
+ }
11157
+ function regenerateJournalSummary() {
11158
+ try {
11159
+ const entries = getRecentEntries();
11160
+ if (entries.length === 0) return;
11161
+ const journalDir = WORKSPACE.JOURNAL;
11162
+ ensureDirExists(journalDir);
11163
+ const summary = buildSummaryFromEntries(entries);
11164
+ const summaryPath = join10(journalDir, SUMMARY_FILE);
11165
+ writeFileSync8(summaryPath, summary, "utf-8");
11166
+ debugLog("general", "Journal summary regenerated", {
11167
+ entries: entries.length,
11168
+ summaryLength: summary.length
11169
+ });
11170
+ } catch (err) {
11171
+ debugLog("general", "Failed to regenerate journal summary", { error: String(err) });
11172
+ }
11173
+ }
11174
+ function buildSummaryFromEntries(entries) {
11175
+ const buckets = collectSummaryBuckets(entries);
11176
+ return formatSummaryMarkdown(buckets, entries);
11177
+ }
11178
+ function collectSummaryBuckets(entries) {
11179
+ const attempts = [];
11180
+ const findings = [];
11181
+ const credentials = [];
11182
+ const successes = [];
11183
+ const failures = [];
11184
+ const suspicions = [];
11185
+ const nextSteps = [];
11186
+ const reflections = [];
11187
+ const reversed = [...entries].reverse();
11188
+ for (const entry of reversed) {
11189
+ const value = entry.memo.attackValue || "LOW";
11190
+ for (const tool of entry.tools) {
11191
+ attempts.push({ turn: entry.turn, phase: entry.phase, ok: tool.success, name: tool.name, input: tool.inputSummary, value });
11192
+ }
11193
+ for (const finding of entry.memo.keyFindings) {
11194
+ const line = `- [T${entry.turn}|\u26A1${value}] ${finding}`;
11195
+ if (!findings.includes(line)) findings.push(line);
11196
+ }
11197
+ for (const cred of entry.memo.credentials) {
11198
+ const line = `- [T${entry.turn}] ${cred}`;
11199
+ if (!credentials.includes(line)) credentials.push(line);
11200
+ }
11201
+ for (const vector of entry.memo.attackVectors) {
11202
+ const line = `- [T${entry.turn}] ${vector}`;
11203
+ if (!successes.includes(line)) successes.push(line);
11204
+ }
11205
+ for (const fail of entry.memo.failures) {
11206
+ const line = `- [T${entry.turn}] ${fail}`;
11207
+ if (!failures.includes(line)) failures.push(line);
11208
+ }
11209
+ for (const tool of entry.tools) {
11210
+ if (!tool.success) {
11211
+ const detail = `${tool.name}(${tool.inputSummary}): ${tool.analystSummary}`;
11212
+ const line = `- [T${entry.turn}] ${detail}`;
11213
+ if (!failures.includes(line)) failures.push(line);
11214
+ }
11215
+ }
11216
+ for (const s of entry.memo.suspicions || []) {
11217
+ const line = `- [T${entry.turn}] ${s}`;
11218
+ if (!suspicions.includes(line)) suspicions.push(line);
11219
+ }
11220
+ if (nextSteps.length < 5) {
11221
+ for (const step of entry.memo.nextSteps) {
11222
+ if (!nextSteps.includes(`- ${step}`)) nextSteps.push(`- ${step}`);
11223
+ }
11224
+ }
11225
+ if (entry.reflection) {
11226
+ reflections.push(`- [T${entry.turn}|\u26A1${value}] ${entry.reflection}`);
11227
+ }
11228
+ }
11229
+ attempts.sort((a, b) => {
11230
+ const vd = (ATTACK_VALUE_RANK[b.value] ?? 0) - (ATTACK_VALUE_RANK[a.value] ?? 0);
11231
+ return vd !== 0 ? vd : b.turn - a.turn;
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;
11237
+ const attemptLines = attempts.map(
11238
+ (a) => `- [T${a.turn}|${a.phase}|\u26A1${a.value}] ${a.ok ? "\u2705" : "\u274C"} ${a.name}: ${a.input}`
11239
+ );
11240
+ const lastTurn = entries[entries.length - 1]?.turn || 0;
11241
+ const sections = [
11242
+ `# Session Journal Summary`,
11243
+ `> Turn ${lastTurn} / ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19)}`,
11244
+ ""
11245
+ ];
11246
+ const addSection = (title, items) => {
11247
+ if (items.length === 0) return;
11248
+ sections.push(`## ${title}`);
11249
+ sections.push(...items);
11250
+ sections.push("");
11251
+ };
11252
+ if (attemptLines.length > 0) {
11253
+ sections.push("## Techniques Tried (by attack value)");
11254
+ sections.push("> \u26A1HIGH=keep drilling \u26A1MED=worth exploring \u26A1LOW=low priority \u26A1NONE=abandon");
11255
+ sections.push(...attemptLines);
11256
+ sections.push("");
11257
+ }
11258
+ addSection("\u{1F9E0} Analyst Analysis (attack value rationale)", reflections);
11259
+ addSection("\u{1F50D} Suspicious Signals (unconfirmed, needs investigation)", suspicions);
11260
+ addSection("\u{1F4CB} Key Findings", findings);
11261
+ addSection("\u{1F511} Credentials Obtained", credentials);
11262
+ addSection("\u2705 Successful Attack Vectors", successes);
11263
+ addSection("\u274C Failure Causes (do not repeat)", failures);
11264
+ addSection("\u27A1\uFE0F Next Recommendations", nextSteps);
11265
+ return sections.join("\n");
11266
+ }
11267
+ function rotateJournalEntries() {
11268
+ try {
11269
+ const journalDir = WORKSPACE.JOURNAL;
11270
+ if (!existsSync8(journalDir)) return;
11271
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort();
11272
+ if (files.length <= MAX_JOURNAL_ENTRIES) return;
11273
+ const toDelete = files.slice(0, files.length - MAX_JOURNAL_ENTRIES);
11274
+ for (const file of toDelete) {
11275
+ try {
11276
+ unlinkSync5(join10(journalDir, file));
11277
+ } catch {
11278
+ }
11279
+ }
11280
+ debugLog("general", "Journal entries rotated", {
11281
+ deleted: toDelete.length,
11282
+ remaining: MAX_JOURNAL_ENTRIES
11283
+ });
11284
+ } catch {
11285
+ }
11286
+ }
11287
+ function rotateOutputFiles() {
11288
+ try {
11289
+ const outputDir = WORKSPACE.OUTPUTS;
11290
+ if (!existsSync8(outputDir)) return;
11291
+ const files = readdirSync2(outputDir).filter((f) => f.endsWith(".txt")).map((f) => ({
11292
+ name: f,
11293
+ path: join10(outputDir, f),
11294
+ mtime: statSync2(join10(outputDir, f)).mtimeMs
11295
+ })).sort((a, b) => b.mtime - a.mtime);
11296
+ if (files.length <= MAX_OUTPUT_FILES) return;
11297
+ const toDelete = files.slice(MAX_OUTPUT_FILES);
11298
+ for (const file of toDelete) {
11299
+ try {
11300
+ unlinkSync5(file.path);
11301
+ } catch {
11302
+ }
11303
+ }
11304
+ debugLog("general", "Output files rotated", {
11305
+ deleted: toDelete.length,
11306
+ remaining: MAX_OUTPUT_FILES
11307
+ });
11308
+ } catch {
11309
+ }
11310
+ }
11311
+
11034
11312
  // src/agents/prompt-builder.ts
11035
- var __dirname4 = dirname5(fileURLToPath4(import.meta.url));
11036
- var PROMPTS_DIR = join10(__dirname4, "prompts");
11037
- var TECHNIQUES_DIR = join10(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
11313
+ var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
11314
+ var PROMPTS_DIR = join11(__dirname3, "prompts");
11315
+ var TECHNIQUES_DIR = join11(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
11038
11316
  var { AGENT_FILES } = PROMPT_PATHS;
11039
11317
  var PHASE_PROMPT_MAP = {
11040
11318
  // Direct mappings — phase has its own prompt file
@@ -11108,6 +11386,7 @@ var PromptBuilder = class {
11108
11386
  * 13. Learned techniques (#7: dynamic technique library)
11109
11387
  * 14. Persistent memory (#12: cross-session knowledge)
11110
11388
  * ★ 15. STRATEGIC DIRECTIVE — LLM-generated tactical instructions (D-CIPHER)
11389
+ * ★ 15b. SESSION JOURNAL — compressed history of past turns (§13 memo system)
11111
11390
  * 16. User context
11112
11391
  */
11113
11392
  async build(userInput, phase) {
@@ -11133,8 +11412,10 @@ var PromptBuilder = class {
11133
11412
  // #12
11134
11413
  this.getDynamicTechniquesFragment(),
11135
11414
  // #7
11136
- this.getPersistentMemoryFragment()
11415
+ this.getPersistentMemoryFragment(),
11137
11416
  // #12
11417
+ this.getJournalFragment()
11418
+ // §13 session journal
11138
11419
  ];
11139
11420
  const strategistDirective = await this.getStrategistFragment();
11140
11421
  if (strategistDirective) {
@@ -11158,8 +11439,8 @@ ${content}
11158
11439
  * Load a prompt file from src/agents/prompts/
11159
11440
  */
11160
11441
  loadPromptFile(filename) {
11161
- const path2 = join10(PROMPTS_DIR, filename);
11162
- return existsSync8(path2) ? readFileSync5(path2, PROMPT_CONFIG.ENCODING) : "";
11442
+ const path2 = join11(PROMPTS_DIR, filename);
11443
+ return existsSync9(path2) ? readFileSync6(path2, PROMPT_CONFIG.ENCODING) : "";
11163
11444
  }
11164
11445
  /**
11165
11446
  * Load phase-specific prompt.
@@ -11202,18 +11483,18 @@ ${content}
11202
11483
  * as general reference — NO code change needed to add new techniques.
11203
11484
  *
11204
11485
  * The map is an optimization (priority ordering), not a gate.
11205
- * "마크다운 파일 하나를 폴더에 넣으면, PromptBuilder 자동으로 발견하고 로드한다."
11486
+ * "Drop a markdown file in the folder, PromptBuilder auto-discovers and loads it."
11206
11487
  */
11207
11488
  loadPhaseRelevantTechniques(phase) {
11208
- if (!existsSync8(TECHNIQUES_DIR)) return "";
11489
+ if (!existsSync9(TECHNIQUES_DIR)) return "";
11209
11490
  const priorityTechniques = PHASE_TECHNIQUE_MAP[phase] || [];
11210
11491
  const loadedSet = /* @__PURE__ */ new Set();
11211
11492
  const fragments = [];
11212
11493
  for (const technique of priorityTechniques) {
11213
- const filePath = join10(TECHNIQUES_DIR, `${technique}.md`);
11494
+ const filePath = join11(TECHNIQUES_DIR, `${technique}.md`);
11214
11495
  try {
11215
- if (!existsSync8(filePath)) continue;
11216
- const content = readFileSync5(filePath, PROMPT_CONFIG.ENCODING);
11496
+ if (!existsSync9(filePath)) continue;
11497
+ const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
11217
11498
  if (content) {
11218
11499
  fragments.push(`<technique-reference category="${technique}">
11219
11500
  ${content}
@@ -11224,10 +11505,10 @@ ${content}
11224
11505
  }
11225
11506
  }
11226
11507
  try {
11227
- const allFiles = readdirSync2(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
11508
+ const allFiles = readdirSync3(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
11228
11509
  for (const file of allFiles) {
11229
- const filePath = join10(TECHNIQUES_DIR, file);
11230
- const content = readFileSync5(filePath, PROMPT_CONFIG.ENCODING);
11510
+ const filePath = join11(TECHNIQUES_DIR, file);
11511
+ const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
11231
11512
  if (content) {
11232
11513
  const category = file.replace(".md", "");
11233
11514
  fragments.push(`<technique-reference category="${category}">
@@ -11330,6 +11611,31 @@ ${lines.join("\n")}
11330
11611
  }
11331
11612
  return this.state.persistentMemory.toPrompt(services);
11332
11613
  }
11614
+ // --- §13: Session Journal Summary ---
11615
+ /**
11616
+ * Load journal summary — prefers Summary Regenerator (⑥) output,
11617
+ * falls back to deterministic journal summary.
11618
+ */
11619
+ getJournalFragment() {
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
+ }
11630
+ const summary = readJournalSummary();
11631
+ if (!summary) return "";
11632
+ return `<session-journal>
11633
+ ${summary}
11634
+ </session-journal>`;
11635
+ } catch {
11636
+ return "";
11637
+ }
11638
+ }
11333
11639
  // --- D-CIPHER: Strategist Meta-Prompting ---
11334
11640
  /**
11335
11641
  * Generate strategic directive via Strategist LLM.
@@ -11342,29 +11648,11 @@ ${lines.join("\n")}
11342
11648
  };
11343
11649
 
11344
11650
  // src/agents/strategist.ts
11345
- import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
11346
- import { join as join11, dirname as dirname6 } from "path";
11347
- import { fileURLToPath as fileURLToPath5 } from "url";
11348
-
11349
- // src/shared/constants/strategist.ts
11350
- var STRATEGIST_LIMITS = {
11351
- /** Maximum characters of state context sent to Strategist LLM.
11352
- * WHY: Keeps Strategist input focused and affordable (~3-5K tokens).
11353
- * Full state can be 20K+; Strategist needs summary, not everything. */
11354
- MAX_INPUT_CHARS: 15e3,
11355
- /** Maximum characters for the Strategist's response.
11356
- * WHY: Directives should be terse and actionable (~800-1500 tokens).
11357
- * Enhanced format includes SITUATION, priorities, EXHAUSTED, and SEARCH ORDERS. */
11358
- MAX_OUTPUT_CHARS: 5e3,
11359
- /** Maximum lines in the directive output.
11360
- * WHY: Forces concise, prioritized directives while allowing
11361
- * structured format (priorities + exhausted + search orders). */
11362
- MAX_DIRECTIVE_LINES: 60
11363
- };
11364
-
11365
- // src/agents/strategist.ts
11366
- var __dirname5 = dirname6(fileURLToPath5(import.meta.url));
11367
- var STRATEGIST_PROMPT_PATH = join11(__dirname5, "prompts", "strategist-system.md");
11651
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
11652
+ import { join as join12, dirname as dirname6 } from "path";
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");
11368
11656
  var Strategist = class {
11369
11657
  llm;
11370
11658
  state;
@@ -11415,24 +11703,40 @@ var Strategist = class {
11415
11703
  const sections = [];
11416
11704
  sections.push("## Engagement State");
11417
11705
  sections.push(this.state.toPrompt());
11418
- const timeline = this.state.episodicMemory.toPrompt();
11419
- if (timeline) {
11420
- sections.push("");
11421
- sections.push("## Recent Actions");
11422
- sections.push(timeline);
11423
- }
11424
11706
  const failures = this.state.workingMemory.toPrompt();
11425
11707
  if (failures) {
11426
11708
  sections.push("");
11427
11709
  sections.push("## Failed Attempts (DO NOT REPEAT THESE)");
11428
11710
  sections.push(failures);
11429
11711
  }
11712
+ try {
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
+ }
11721
+ if (journalSummary) {
11722
+ sections.push("");
11723
+ sections.push("## Session Journal (past turns summary)");
11724
+ sections.push(journalSummary);
11725
+ }
11726
+ } catch {
11727
+ }
11430
11728
  const graph = this.state.attackGraph.toPrompt();
11431
11729
  if (graph) {
11432
11730
  sections.push("");
11433
11731
  sections.push("## Attack Graph");
11434
11732
  sections.push(graph);
11435
11733
  }
11734
+ const timeline = this.state.episodicMemory.toPrompt();
11735
+ if (timeline) {
11736
+ sections.push("");
11737
+ sections.push("## Recent Actions");
11738
+ sections.push(timeline);
11739
+ }
11436
11740
  const techniques = this.state.dynamicTechniques.toPrompt();
11437
11741
  if (techniques) {
11438
11742
  sections.push("");
@@ -11448,11 +11752,7 @@ var Strategist = class {
11448
11752
  sections.push(`## Challenge Type: ${analysis.primaryType.toUpperCase()} (${(analysis.confidence * 100).toFixed(0)}%)`);
11449
11753
  sections.push(analysis.strategySuggestion);
11450
11754
  }
11451
- let input = sections.join("\n");
11452
- if (input.length > STRATEGIST_LIMITS.MAX_INPUT_CHARS) {
11453
- input = input.slice(0, STRATEGIST_LIMITS.MAX_INPUT_CHARS) + "\n\n... [state truncated for Strategist context]";
11454
- }
11455
- return input;
11755
+ return sections.join("\n");
11456
11756
  }
11457
11757
  // ─── LLM Call ───────────────────────────────────────────────
11458
11758
  async callLLM(input) {
@@ -11469,9 +11769,6 @@ ${input}`
11469
11769
  this.systemPrompt
11470
11770
  );
11471
11771
  let content = response.content || "";
11472
- if (content.length > STRATEGIST_LIMITS.MAX_OUTPUT_CHARS) {
11473
- content = content.slice(0, STRATEGIST_LIMITS.MAX_OUTPUT_CHARS) + "\n... [directive truncated]";
11474
- }
11475
11772
  const cost = response.usage ? response.usage.input_tokens + response.usage.output_tokens : 0;
11476
11773
  this.totalTokenCost += cost;
11477
11774
  return {
@@ -11501,8 +11798,8 @@ NOTE: This directive is from ${age}min ago (Strategist call failed this turn). V
11501
11798
  // ─── System Prompt Loading ──────────────────────────────────
11502
11799
  loadSystemPrompt() {
11503
11800
  try {
11504
- if (existsSync9(STRATEGIST_PROMPT_PATH)) {
11505
- return readFileSync6(STRATEGIST_PROMPT_PATH, "utf-8");
11801
+ if (existsSync10(STRATEGIST_PROMPT_PATH)) {
11802
+ return readFileSync7(STRATEGIST_PROMPT_PATH, "utf-8");
11506
11803
  }
11507
11804
  } catch {
11508
11805
  }
@@ -11537,13 +11834,102 @@ Detect stalls (repeated failures, no progress) and force completely different at
11537
11834
  Chain every finding: "If X works \u2192 immediately do Y \u2192 which enables Z."
11538
11835
  Maximum 50 lines. Zero preamble. Direct imperatives only. Never repeat failed approaches.`;
11539
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
+
11540
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";
11541
11925
  var MainAgent = class extends CoreAgent {
11542
11926
  promptBuilder;
11543
11927
  strategist;
11544
11928
  approvalGate;
11545
11929
  scopeGuard;
11546
11930
  userInput = "";
11931
+ /** Monotonic turn counter for journal entries */
11932
+ turnCounter = 0;
11547
11933
  constructor(state, events, toolRegistry, approvalGate, scopeGuard) {
11548
11934
  super(AGENT_ROLES.ORCHESTRATOR, state, events, toolRegistry);
11549
11935
  this.approvalGate = approvalGate;
@@ -11581,8 +11967,116 @@ var MainAgent = class extends CoreAgent {
11581
11967
  * The Strategist LLM generates a fresh tactical directive every turn.
11582
11968
  */
11583
11969
  async step(iteration, messages, _unusedPrompt, progress) {
11970
+ if (this.turnCounter === 0) {
11971
+ this.turnCounter = getNextTurnNumber();
11972
+ }
11973
+ this.turnToolJournal = [];
11974
+ this.turnMemo = { keyFindings: [], credentials: [], attackVectors: [], failures: [], suspicions: [], attackValue: "LOW", nextSteps: [] };
11975
+ this.turnReflections = [];
11584
11976
  const dynamicPrompt = await this.getCurrentPrompt();
11585
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
+ }
12017
+ if (this.turnToolJournal.length > 0) {
12018
+ try {
12019
+ const entry = {
12020
+ turn: this.turnCounter,
12021
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12022
+ phase: this.state.getPhase(),
12023
+ tools: this.turnToolJournal,
12024
+ memo: this.turnMemo,
12025
+ reflection: this.turnReflections.length > 0 ? this.turnReflections.join(" | ") : this.turnMemo.nextSteps.join("; ")
12026
+ };
12027
+ writeJournalEntry(entry);
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 {
12072
+ regenerateJournalSummary();
12073
+ }
12074
+ rotateJournalEntries();
12075
+ rotateOutputFiles();
12076
+ } catch {
12077
+ }
12078
+ this.turnCounter++;
12079
+ }
11586
12080
  this.emitStateChange();
11587
12081
  return result2;
11588
12082
  }