pentesting 0.47.3 → 0.47.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -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.47.4";
335
335
  var APP_DESCRIPTION = "Autonomous Penetration Testing AI Agent";
336
336
  var LLM_ROLES = {
337
337
  SYSTEM: "system",
@@ -828,6 +828,7 @@ var SESSIONS_DIR = `${PENTESTING_ROOT}/sessions`;
828
828
  var LOOT_DIR = `${PENTESTING_ROOT}/loot`;
829
829
  var OUTPUTS_DIR = `${PENTESTING_ROOT}/outputs`;
830
830
  var DEBUG_DIR = `${PENTESTING_ROOT}/debug`;
831
+ var JOURNAL_DIR = `${PENTESTING_ROOT}/journal`;
831
832
  var WORKSPACE = {
832
833
  /** Root directory */
833
834
  get ROOT() {
@@ -860,6 +861,10 @@ var WORKSPACE = {
860
861
  /** Debug logs */
861
862
  get DEBUG() {
862
863
  return path.resolve(DEBUG_DIR);
864
+ },
865
+ /** Persistent per-turn journal (§13 memo system) */
866
+ get JOURNAL() {
867
+ return path.resolve(JOURNAL_DIR);
863
868
  }
864
869
  };
865
870
 
@@ -2687,13 +2692,13 @@ var AttackGraph = class {
2687
2692
  * Record a credential discovery and create spray edges.
2688
2693
  */
2689
2694
  addCredential(username, password, source) {
2690
- const credId = this.addNode("credential", `${username}:***`, {
2695
+ const credId = this.addNode(NODE_TYPE.CREDENTIAL, `${username}:***`, {
2691
2696
  username,
2692
2697
  password,
2693
2698
  source
2694
2699
  });
2695
2700
  for (const [id, node] of this.nodes) {
2696
- if (node.type === "service") {
2701
+ if (node.type === NODE_TYPE.SERVICE) {
2697
2702
  const svc = String(node.data.service || "");
2698
2703
  if (["ssh", "ftp", "rdp", "smb", "http", "mysql", "postgresql", "mssql", "winrm", "vnc", "telnet"].some((s) => svc.includes(s))) {
2699
2704
  this.addEdge(credId, id, "can_try_on", 0.6);
@@ -2706,7 +2711,7 @@ var AttackGraph = class {
2706
2711
  * Record a vulnerability finding.
2707
2712
  */
2708
2713
  addVulnerability(title, target, severity, hasExploit = false) {
2709
- const vulnId = this.addNode("vulnerability", title, {
2714
+ const vulnId = this.addNode(NODE_TYPE.VULNERABILITY, title, {
2710
2715
  target,
2711
2716
  severity,
2712
2717
  hasExploit
@@ -2717,7 +2722,7 @@ var AttackGraph = class {
2717
2722
  }
2718
2723
  }
2719
2724
  if (hasExploit) {
2720
- const accessId = this.addNode("access", `shell via ${title}`, {
2725
+ const accessId = this.addNode(NODE_TYPE.ACCESS, `shell via ${title}`, {
2721
2726
  via: title,
2722
2727
  status: GRAPH_STATUS.POTENTIAL
2723
2728
  });
@@ -2729,14 +2734,14 @@ var AttackGraph = class {
2729
2734
  * Record gained access.
2730
2735
  */
2731
2736
  addAccess(host, level, via) {
2732
- const accessId = this.addNode("access", `${level}@${host}`, {
2737
+ const accessId = this.addNode(NODE_TYPE.ACCESS, `${level}@${host}`, {
2733
2738
  host,
2734
2739
  level,
2735
2740
  via
2736
2741
  });
2737
2742
  this.markSucceeded(accessId);
2738
2743
  if (["root", "admin", "SYSTEM", "Administrator"].includes(level)) {
2739
- const lootId = this.addNode("loot", `flags on ${host}`, {
2744
+ const lootId = this.addNode(NODE_TYPE.LOOT, `flags on ${host}`, {
2740
2745
  host,
2741
2746
  status: GRAPH_STATUS.NEEDS_SEARCH
2742
2747
  });
@@ -2748,7 +2753,7 @@ var AttackGraph = class {
2748
2753
  * Record OSINT discovery (Docker image, GitHub repo, company info, etc.)
2749
2754
  */
2750
2755
  addOSINT(category, detail, data = {}) {
2751
- const osintId = this.addNode("osint", `${category}: ${detail}`, {
2756
+ const osintId = this.addNode(NODE_TYPE.OSINT, `${category}: ${detail}`, {
2752
2757
  category,
2753
2758
  detail,
2754
2759
  ...data
@@ -3981,20 +3986,6 @@ var ScopeGuard = class {
3981
3986
  };
3982
3987
 
3983
3988
  // 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
3989
  var ApprovalGate = class {
3999
3990
  constructor(shouldAutoApprove = false) {
4000
3991
  this.shouldAutoApprove = shouldAutoApprove;
@@ -4298,7 +4289,7 @@ function autoExtractStructured(toolName, output) {
4298
4289
  data.vulnerabilities = vulns;
4299
4290
  hasData = true;
4300
4291
  }
4301
- if (toolName === "parse_nmap" || /nmap scan report/i.test(output)) {
4292
+ if (toolName === TOOL_NAMES.PARSE_NMAP || /nmap scan report/i.test(output)) {
4302
4293
  const nmap = extractNmapStructured(output);
4303
4294
  if (nmap.structured.openPorts && nmap.structured.openPorts.length > 0) {
4304
4295
  data.openPorts = nmap.structured.openPorts;
@@ -4703,7 +4694,8 @@ Used ports: ${usedPorts.join(", ")}
4703
4694
  [!] STRATEGY ADAPTATION REQUIRED:
4704
4695
  1. Try the next available port (e.g., ${nextPort} or 4445, 9001)
4705
4696
  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.`
4697
+ 3. Check bg_process({ action: "list" }) to see if you can stop the conflicting process.`,
4698
+ error: `Port ${requestedPort} already in use`
4707
4699
  };
4708
4700
  }
4709
4701
  }
@@ -4850,7 +4842,7 @@ ${output.stderr.slice(-SYSTEM_LIMITS.MAX_STDERR_SLICE) || "(empty)"}` + connecti
4850
4842
  if (!cmd) return { success: false, output: "", error: "Missing command for interact. Provide the command to execute on the target." };
4851
4843
  const waitMs = Math.min(params.wait_ms || SYSTEM_LIMITS.DEFAULT_WAIT_MS_INTERACT, SYSTEM_LIMITS.MAX_WAIT_MS_INTERACT);
4852
4844
  const result2 = await sendToProcess(processId, cmd, waitMs);
4853
- if (!result2.success) return { success: false, output: result2.output };
4845
+ if (!result2.success) return { success: false, output: result2.output, error: result2.output };
4854
4846
  return {
4855
4847
  success: true,
4856
4848
  output: `Command sent: ${cmd}
@@ -4866,7 +4858,7 @@ ${result2.output}`
4866
4858
  if (!processId) return { success: false, output: "", error: "Missing process_id for promote" };
4867
4859
  const desc = params.description;
4868
4860
  const success = promoteToShell(processId, desc);
4869
- if (!success) return { success: false, output: `Process ${processId} not found` };
4861
+ if (!success) return { success: false, output: `Process ${processId} not found`, error: `Process ${processId} not found` };
4870
4862
  return {
4871
4863
  success: true,
4872
4864
  output: `[OK] Process ${processId} promoted to ACTIVE SHELL.
@@ -5016,7 +5008,8 @@ Examples:
5016
5008
  if (!validPhases.includes(newPhase)) {
5017
5009
  return {
5018
5010
  success: false,
5019
- output: `Invalid phase. Valid phases: ${validPhases.join(", ")}`
5011
+ output: `Invalid phase. Valid phases: ${validPhases.join(", ")}`,
5012
+ error: `Invalid phase: ${newPhase}`
5020
5013
  };
5021
5014
  }
5022
5015
  state.setPhase(newPhase);
@@ -5732,50 +5725,60 @@ var DEFAULT_BROWSER_OPTIONS = {
5732
5725
 
5733
5726
  // src/engine/tools/web-browser.ts
5734
5727
  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) {
5728
+ try {
5729
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5730
+ const { installed, browserInstalled } = await checkPlaywright();
5731
+ if (!installed || !browserInstalled) {
5732
+ const installResult = await installPlaywright();
5733
+ if (!installResult.success) {
5734
+ return {
5735
+ success: false,
5736
+ output: "",
5737
+ error: `Playwright not available and auto-install failed: ${installResult.output}`
5738
+ };
5739
+ }
5740
+ }
5741
+ const screenshotPath = browserOptions.screenshot ? join6(join6(tmpdir3(), BROWSER_PATHS.TEMP_DIR_NAME), `screenshot-${Date.now()}.png`) : void 0;
5742
+ const script = buildBrowseScript(url, browserOptions, screenshotPath);
5743
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "browse");
5744
+ if (!result2.success) {
5740
5745
  return {
5741
5746
  success: false,
5742
- output: "",
5743
- error: `Playwright not available and auto-install failed: ${installResult.output}`
5747
+ output: result2.output,
5748
+ error: result2.error
5749
+ };
5750
+ }
5751
+ if (result2.parsedData) {
5752
+ return {
5753
+ success: true,
5754
+ output: formatBrowserOutput(result2.parsedData, browserOptions),
5755
+ screenshots: screenshotPath ? [screenshotPath] : void 0,
5756
+ extractedData: result2.parsedData
5744
5757
  };
5745
5758
  }
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
5759
  return {
5752
- success: false,
5753
- output: result2.output,
5754
- error: result2.error
5760
+ success: true,
5761
+ output: result2.output || "Navigation completed",
5762
+ screenshots: screenshotPath ? [screenshotPath] : void 0
5755
5763
  };
5756
- }
5757
- if (result2.parsedData) {
5764
+ } catch (error) {
5765
+ const msg = error instanceof Error ? error.message : String(error);
5758
5766
  return {
5759
- success: true,
5760
- output: formatBrowserOutput(result2.parsedData, browserOptions),
5761
- screenshots: screenshotPath ? [screenshotPath] : void 0,
5762
- extractedData: result2.parsedData
5767
+ success: false,
5768
+ output: "",
5769
+ error: `Browser error: ${msg}`
5763
5770
  };
5764
5771
  }
5765
- return {
5766
- success: true,
5767
- output: result2.output || "Navigation completed",
5768
- screenshots: screenshotPath ? [screenshotPath] : void 0
5769
- };
5770
5772
  }
5771
5773
  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 = `
5774
+ try {
5775
+ const browserOptions = { ...DEFAULT_BROWSER_OPTIONS, ...options };
5776
+ const safeUrl = safeJsString(url);
5777
+ const safeFormData = JSON.stringify(formData);
5778
+ const playwrightPath = getPlaywrightPath();
5779
+ const safePlaywrightPath = safeJsString(playwrightPath);
5780
+ const headlessMode = process.env.HEADLESS !== "false";
5781
+ const script = `
5779
5782
  const { chromium } = require(${safePlaywrightPath});
5780
5783
 
5781
5784
  (async () => {
@@ -5815,27 +5818,35 @@ const { chromium } = require(${safePlaywrightPath});
5815
5818
  }
5816
5819
  })();
5817
5820
  `;
5818
- const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
5819
- if (!result2.success) {
5821
+ const result2 = await runPlaywrightScript(script, browserOptions.timeout, "form");
5822
+ if (!result2.success) {
5823
+ return {
5824
+ success: false,
5825
+ output: result2.output,
5826
+ error: result2.error
5827
+ };
5828
+ }
5829
+ if (result2.parsedData) {
5830
+ const data = result2.parsedData;
5831
+ return {
5832
+ success: true,
5833
+ output: `Form submitted. Current URL: ${data.url}
5834
+ Title: ${data.title}`,
5835
+ extractedData: result2.parsedData
5836
+ };
5837
+ }
5820
5838
  return {
5821
- success: false,
5822
- output: result2.output,
5823
- error: result2.error
5839
+ success: true,
5840
+ output: result2.output || "Form submitted"
5824
5841
  };
5825
- }
5826
- if (result2.parsedData) {
5827
- const data = result2.parsedData;
5842
+ } catch (error) {
5843
+ const msg = error instanceof Error ? error.message : String(error);
5828
5844
  return {
5829
- success: true,
5830
- output: `Form submitted. Current URL: ${data.url}
5831
- Title: ${data.title}`,
5832
- extractedData: result2.parsedData
5845
+ success: false,
5846
+ output: "",
5847
+ error: `Form submission error: ${msg}`
5833
5848
  };
5834
5849
  }
5835
- return {
5836
- success: true,
5837
- output: result2.output || "Form submitted"
5838
- };
5839
5850
  }
5840
5851
  async function webSearchWithBrowser(query, engine = "google") {
5841
5852
  const searchUrls = {
@@ -5852,6 +5863,10 @@ async function webSearchWithBrowser(query, engine = "google") {
5852
5863
  }
5853
5864
 
5854
5865
  // src/engine/web-search-providers.ts
5866
+ var SEARCH_TIMEOUT_MS = 15e3;
5867
+ function getErrorMessage(error) {
5868
+ return error instanceof Error ? error.message : String(error);
5869
+ }
5855
5870
  async function searchWithGLM(query, apiKey, apiUrl) {
5856
5871
  debugLog("search", "GLM request START", { apiUrl, query });
5857
5872
  const requestBody = {
@@ -5860,21 +5875,39 @@ async function searchWithGLM(query, apiKey, apiUrl) {
5860
5875
  stream: false
5861
5876
  };
5862
5877
  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
- });
5878
+ let response;
5879
+ try {
5880
+ response = await fetch(apiUrl, {
5881
+ method: "POST",
5882
+ headers: {
5883
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5884
+ [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`
5885
+ },
5886
+ body: JSON.stringify(requestBody),
5887
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5888
+ });
5889
+ } catch (err) {
5890
+ const msg = getErrorMessage(err);
5891
+ debugLog("search", "GLM fetch FAILED (network)", { error: msg });
5892
+ return { success: false, output: "", error: `GLM search network error: ${msg}. Check internet connection or API endpoint.` };
5893
+ }
5871
5894
  debugLog("search", "GLM response status", { status: response.status, ok: response.ok });
5872
5895
  if (!response.ok) {
5873
- const errorText = await response.text();
5896
+ let errorText = "";
5897
+ try {
5898
+ errorText = await response.text();
5899
+ } catch {
5900
+ }
5874
5901
  debugLog("search", "GLM response ERROR", { status: response.status, error: errorText });
5875
- throw new Error(`GLM Search API error: ${response.status} - ${errorText}`);
5902
+ return { success: false, output: "", error: `GLM Search API error ${response.status}: ${errorText.slice(0, 500)}` };
5903
+ }
5904
+ let data;
5905
+ try {
5906
+ data = await response.json();
5907
+ } catch (err) {
5908
+ debugLog("search", "GLM JSON parse FAILED", { error: getErrorMessage(err) });
5909
+ return { success: false, output: "", error: `GLM search returned invalid JSON: ${getErrorMessage(err)}` };
5876
5910
  }
5877
- const data = await response.json();
5878
5911
  debugLog("search", "GLM response data", { hasChoices: !!data.choices, choicesCount: data.choices?.length });
5879
5912
  let results = "";
5880
5913
  if (data.choices?.[0]?.message?.content) {
@@ -5902,19 +5935,37 @@ async function searchWithBrave(query, apiKey, apiUrl) {
5902
5935
  debugLog("search", "Brave request START", { apiUrl, query });
5903
5936
  const url = `${apiUrl}?q=${encodeURIComponent(query)}&count=${SEARCH_LIMIT.DEFAULT_RESULT_COUNT}`;
5904
5937
  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
- });
5938
+ let response;
5939
+ try {
5940
+ response = await fetch(url, {
5941
+ headers: {
5942
+ [SEARCH_HEADER.ACCEPT]: "application/json",
5943
+ [SEARCH_HEADER.X_SUBSCRIPTION_TOKEN]: apiKey
5944
+ },
5945
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5946
+ });
5947
+ } catch (err) {
5948
+ const msg = getErrorMessage(err);
5949
+ debugLog("search", "Brave fetch FAILED (network)", { error: msg });
5950
+ return { success: false, output: "", error: `Brave search network error: ${msg}. Check internet connection.` };
5951
+ }
5911
5952
  debugLog("search", "Brave response status", { status: response.status, ok: response.ok });
5912
5953
  if (!response.ok) {
5913
- const errorText = await response.text();
5954
+ let errorText = "";
5955
+ try {
5956
+ errorText = await response.text();
5957
+ } catch {
5958
+ }
5914
5959
  debugLog("search", "Brave response ERROR", { status: response.status, error: errorText });
5915
- throw new Error(`Brave API error: ${response.status}`);
5960
+ return { success: false, output: "", error: `Brave API error ${response.status}: ${errorText.slice(0, 500)}` };
5961
+ }
5962
+ let data;
5963
+ try {
5964
+ data = await response.json();
5965
+ } catch (err) {
5966
+ debugLog("search", "Brave JSON parse FAILED", { error: getErrorMessage(err) });
5967
+ return { success: false, output: "", error: `Brave search returned invalid JSON: ${getErrorMessage(err)}` };
5916
5968
  }
5917
- const data = await response.json();
5918
5969
  const results = data.web?.results || [];
5919
5970
  debugLog("search", "Brave results count", { count: results.length });
5920
5971
  if (results.length === 0) {
@@ -5930,21 +5981,39 @@ async function searchWithBrave(query, apiKey, apiUrl) {
5930
5981
  }
5931
5982
  async function searchWithSerper(query, apiKey, apiUrl) {
5932
5983
  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
- });
5984
+ let response;
5985
+ try {
5986
+ response = await fetch(apiUrl, {
5987
+ method: "POST",
5988
+ headers: {
5989
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
5990
+ [SEARCH_HEADER.X_API_KEY]: apiKey
5991
+ },
5992
+ body: JSON.stringify({ q: query }),
5993
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
5994
+ });
5995
+ } catch (err) {
5996
+ const msg = getErrorMessage(err);
5997
+ debugLog("search", "Serper fetch FAILED (network)", { error: msg });
5998
+ return { success: false, output: "", error: `Serper search network error: ${msg}. Check internet connection.` };
5999
+ }
5941
6000
  debugLog("search", "Serper response status", { status: response.status, ok: response.ok });
5942
6001
  if (!response.ok) {
5943
- const errorText = await response.text();
6002
+ let errorText = "";
6003
+ try {
6004
+ errorText = await response.text();
6005
+ } catch {
6006
+ }
5944
6007
  debugLog("search", "Serper response ERROR", { status: response.status, error: errorText });
5945
- throw new Error(`Serper API error: ${response.status}`);
6008
+ return { success: false, output: "", error: `Serper API error ${response.status}: ${errorText.slice(0, 500)}` };
6009
+ }
6010
+ let data;
6011
+ try {
6012
+ data = await response.json();
6013
+ } catch (err) {
6014
+ debugLog("search", "Serper JSON parse FAILED", { error: getErrorMessage(err) });
6015
+ return { success: false, output: "", error: `Serper search returned invalid JSON: ${getErrorMessage(err)}` };
5946
6016
  }
5947
- const data = await response.json();
5948
6017
  const results = data.organic || [];
5949
6018
  debugLog("search", "Serper results count", { count: results.length });
5950
6019
  if (results.length === 0) {
@@ -5959,20 +6028,36 @@ async function searchWithSerper(query, apiKey, apiUrl) {
5959
6028
  return { success: true, output: formatted };
5960
6029
  }
5961
6030
  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
- });
6031
+ let response;
6032
+ try {
6033
+ response = await fetch(apiUrl, {
6034
+ method: "POST",
6035
+ headers: {
6036
+ [SEARCH_HEADER.CONTENT_TYPE]: "application/json",
6037
+ [SEARCH_HEADER.AUTHORIZATION]: `Bearer ${apiKey}`,
6038
+ [SEARCH_HEADER.X_API_KEY]: apiKey
6039
+ },
6040
+ body: JSON.stringify({ query, q: query }),
6041
+ signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS)
6042
+ });
6043
+ } catch (err) {
6044
+ const msg = getErrorMessage(err);
6045
+ return { success: false, output: "", error: `Search API network error: ${msg}. Check internet connection or API endpoint.` };
6046
+ }
5971
6047
  if (!response.ok) {
5972
- const errorText = await response.text();
5973
- throw new Error(`Search API error: ${response.status} - ${errorText}`);
6048
+ let errorText = "";
6049
+ try {
6050
+ errorText = await response.text();
6051
+ } catch {
6052
+ }
6053
+ return { success: false, output: "", error: `Search API error ${response.status}: ${errorText.slice(0, 500)}` };
6054
+ }
6055
+ let data;
6056
+ try {
6057
+ data = await response.json();
6058
+ } catch (err) {
6059
+ return { success: false, output: "", error: `Search API returned invalid JSON: ${getErrorMessage(err)}` };
5974
6060
  }
5975
- const data = await response.json();
5976
6061
  return { success: true, output: JSON.stringify(data, null, 2) };
5977
6062
  }
5978
6063
 
@@ -5982,7 +6067,7 @@ var PORT_STATE2 = {
5982
6067
  CLOSED: "closed",
5983
6068
  FILTERED: "filtered"
5984
6069
  };
5985
- function getErrorMessage(error) {
6070
+ function getErrorMessage2(error) {
5986
6071
  return error instanceof Error ? error.message : String(error);
5987
6072
  }
5988
6073
  async function parseNmap(xmlPath) {
@@ -6033,7 +6118,7 @@ async function parseNmap(xmlPath) {
6033
6118
  return {
6034
6119
  success: false,
6035
6120
  output: "",
6036
- error: getErrorMessage(error)
6121
+ error: getErrorMessage2(error)
6037
6122
  };
6038
6123
  }
6039
6124
  }
@@ -6044,7 +6129,7 @@ async function searchCVE(service, version) {
6044
6129
  return {
6045
6130
  success: false,
6046
6131
  output: "",
6047
- error: getErrorMessage(error)
6132
+ error: getErrorMessage2(error)
6048
6133
  };
6049
6134
  }
6050
6135
  }
@@ -6081,7 +6166,7 @@ async function searchExploitDB(service, version) {
6081
6166
  return {
6082
6167
  success: false,
6083
6168
  output: "",
6084
- error: getErrorMessage(error)
6169
+ error: getErrorMessage2(error)
6085
6170
  };
6086
6171
  }
6087
6172
  }
@@ -6128,11 +6213,11 @@ async function webSearch(query, _engine) {
6128
6213
  return await searchWithGenericApi(query, apiKey, apiUrl);
6129
6214
  }
6130
6215
  } catch (error) {
6131
- debugLog("search", "webSearch ERROR", { error: getErrorMessage(error) });
6216
+ debugLog("search", "webSearch ERROR", { error: getErrorMessage2(error) });
6132
6217
  return {
6133
6218
  success: false,
6134
6219
  output: "",
6135
- error: getErrorMessage(error)
6220
+ error: getErrorMessage2(error)
6136
6221
  };
6137
6222
  }
6138
6223
  }
@@ -6509,7 +6594,8 @@ ${results.join("\n\n")}`
6509
6594
  }
6510
6595
  return {
6511
6596
  success: false,
6512
- output: `Category ${category} not found in any edition.`
6597
+ output: `Category ${category} not found in any edition.`,
6598
+ error: `Category ${category} not found`
6513
6599
  };
6514
6600
  }
6515
6601
  if (edition === "all") {
@@ -6520,7 +6606,7 @@ ${results.join("\n\n")}`
6520
6606
  }
6521
6607
  const data = OWASP_FULL_HISTORY[edition];
6522
6608
  if (!data) {
6523
- return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.` };
6609
+ return { success: false, output: `Year ${edition} not found in database. Reference 2017-2025.`, error: `Edition ${edition} not found` };
6524
6610
  }
6525
6611
  return {
6526
6612
  success: true,
@@ -7065,8 +7151,8 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7065
7151
  }
7066
7152
  },
7067
7153
  execute: async (p) => {
7068
- const { existsSync: existsSync10, statSync: statSync2, readdirSync: readdirSync3 } = await import("fs");
7069
- const { join: join12 } = await import("path");
7154
+ const { existsSync: existsSync11, statSync: statSync3, readdirSync: readdirSync4 } = await import("fs");
7155
+ const { join: join13 } = await import("path");
7070
7156
  const category = p.category || "";
7071
7157
  const search = p.search || "";
7072
7158
  const minSize = p.min_size || 0;
@@ -7102,7 +7188,7 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7102
7188
  const processFile = (fullPath, fileName) => {
7103
7189
  const ext = fileName.split(".").pop()?.toLowerCase();
7104
7190
  if (!WORDLIST_EXTENSIONS.has(ext || "")) return;
7105
- const stats = statSync2(fullPath);
7191
+ const stats = statSync3(fullPath);
7106
7192
  if (stats.size < minSize) return;
7107
7193
  if (!matchesCategory(fullPath)) return;
7108
7194
  if (!matchesSearch(fullPath, fileName)) return;
@@ -7112,16 +7198,16 @@ Returns: All available wordlists with their paths, sizes, and categories.`,
7112
7198
  results.push("");
7113
7199
  };
7114
7200
  const scanDir = (dirPath, maxDepth = 3, depth = 0) => {
7115
- if (depth > maxDepth || !existsSync10(dirPath)) return;
7201
+ if (depth > maxDepth || !existsSync11(dirPath)) return;
7116
7202
  let entries;
7117
7203
  try {
7118
- entries = readdirSync3(dirPath, { withFileTypes: true });
7204
+ entries = readdirSync4(dirPath, { withFileTypes: true });
7119
7205
  } catch {
7120
7206
  return;
7121
7207
  }
7122
7208
  for (const entry of entries) {
7123
7209
  if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
7124
- const fullPath = join12(dirPath, entry.name);
7210
+ const fullPath = join13(dirPath, entry.name);
7125
7211
  if (entry.isDirectory()) {
7126
7212
  scanDir(fullPath, maxDepth, depth + 1);
7127
7213
  continue;
@@ -7498,8 +7584,8 @@ Requires root/sudo privileges.`,
7498
7584
  const iface = p.interface || "";
7499
7585
  const duration = p.duration || NETWORK_CONFIG.DEFAULT_SPOOF_DURATION;
7500
7586
  const hostsFile = createTempFile(FILE_EXTENSIONS.HOSTS);
7501
- const { writeFileSync: writeFileSync8 } = await import("fs");
7502
- writeFileSync8(hostsFile, `${spoofIp} ${domain}
7587
+ const { writeFileSync: writeFileSync9 } = await import("fs");
7588
+ writeFileSync9(hostsFile, `${spoofIp} ${domain}
7503
7589
  ${spoofIp} *.${domain}
7504
7590
  `);
7505
7591
  const ifaceFlag = iface ? `-i ${iface}` : "";
@@ -8006,6 +8092,86 @@ Returns recommendations on process status, port conflicts, long-running tasks, e
8006
8092
  }
8007
8093
  ];
8008
8094
 
8095
+ // src/shared/constants/service-ports.ts
8096
+ var SERVICE_PORTS = {
8097
+ SSH: 22,
8098
+ FTP: 21,
8099
+ TELNET: 23,
8100
+ SMTP: 25,
8101
+ DNS: 53,
8102
+ HTTP: 80,
8103
+ POP3: 110,
8104
+ IMAP: 143,
8105
+ SMB_NETBIOS: 139,
8106
+ KERBEROS: 88,
8107
+ LDAP: 389,
8108
+ SMB: 445,
8109
+ HTTPS: 443,
8110
+ SMTPS: 465,
8111
+ SMTP_TLS: 587,
8112
+ MODBUS: 502,
8113
+ IMAPS: 993,
8114
+ POP3S: 995,
8115
+ MSSQL: 1433,
8116
+ MYSQL: 3306,
8117
+ RDP: 3389,
8118
+ POSTGRESQL: 5432,
8119
+ VNC: 5900,
8120
+ REDIS: 6379,
8121
+ DOCKER_HTTP: 2375,
8122
+ DOCKER_HTTPS: 2376,
8123
+ KUBERNETES_API: 6443,
8124
+ HTTP_ALT: 8080,
8125
+ HTTPS_ALT: 8443,
8126
+ NFS: 2049,
8127
+ DNP3: 2e4,
8128
+ MONGODB: 27017,
8129
+ ELASTICSEARCH: 9200,
8130
+ MEMCACHED: 11211,
8131
+ NODE_DEFAULT: 3e3,
8132
+ FLASK_DEFAULT: 5e3,
8133
+ DJANGO_DEFAULT: 8e3
8134
+ };
8135
+ var CRITICAL_SERVICE_PORTS = [
8136
+ SERVICE_PORTS.SSH,
8137
+ SERVICE_PORTS.RDP,
8138
+ SERVICE_PORTS.MYSQL,
8139
+ SERVICE_PORTS.POSTGRESQL,
8140
+ SERVICE_PORTS.REDIS,
8141
+ SERVICE_PORTS.MONGODB
8142
+ ];
8143
+ var NO_AUTH_CRITICAL_PORTS = [
8144
+ SERVICE_PORTS.REDIS,
8145
+ SERVICE_PORTS.MONGODB,
8146
+ SERVICE_PORTS.ELASTICSEARCH,
8147
+ SERVICE_PORTS.MEMCACHED
8148
+ ];
8149
+ var WEB_SERVICE_PORTS = [
8150
+ SERVICE_PORTS.HTTP,
8151
+ SERVICE_PORTS.HTTPS,
8152
+ SERVICE_PORTS.HTTP_ALT,
8153
+ SERVICE_PORTS.HTTPS_ALT,
8154
+ SERVICE_PORTS.NODE_DEFAULT,
8155
+ SERVICE_PORTS.FLASK_DEFAULT,
8156
+ SERVICE_PORTS.DJANGO_DEFAULT
8157
+ ];
8158
+ var PLAINTEXT_HTTP_PORTS = [
8159
+ SERVICE_PORTS.HTTP,
8160
+ SERVICE_PORTS.HTTP_ALT,
8161
+ SERVICE_PORTS.NODE_DEFAULT
8162
+ ];
8163
+ var DATABASE_PORTS = [
8164
+ SERVICE_PORTS.MYSQL,
8165
+ SERVICE_PORTS.POSTGRESQL,
8166
+ SERVICE_PORTS.MSSQL,
8167
+ SERVICE_PORTS.MONGODB,
8168
+ SERVICE_PORTS.REDIS
8169
+ ];
8170
+ var SMB_PORTS = [
8171
+ SERVICE_PORTS.SMB,
8172
+ SERVICE_PORTS.SMB_NETBIOS
8173
+ ];
8174
+
8009
8175
  // src/domains/network/tools.ts
8010
8176
  var NETWORK_TOOLS = [
8011
8177
  {
@@ -8051,7 +8217,7 @@ var NETWORK_CONFIG2 = {
8051
8217
  tools: NETWORK_TOOLS,
8052
8218
  dangerLevel: DANGER_LEVELS.ACTIVE,
8053
8219
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8054
- commonPorts: [21, 22, 80, 443, 445, 3389, 8080],
8220
+ commonPorts: [SERVICE_PORTS.FTP, SERVICE_PORTS.SSH, SERVICE_PORTS.HTTP, SERVICE_PORTS.HTTPS, SERVICE_PORTS.SMB, SERVICE_PORTS.RDP, SERVICE_PORTS.HTTP_ALT],
8055
8221
  commonServices: [SERVICES.FTP, SERVICES.SSH, SERVICES.HTTP, SERVICES.HTTPS, SERVICES.SMB]
8056
8222
  };
8057
8223
 
@@ -8114,7 +8280,7 @@ var WEB_CONFIG = {
8114
8280
  tools: WEB_TOOLS,
8115
8281
  dangerLevel: DANGER_LEVELS.ACTIVE,
8116
8282
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8117
- commonPorts: [80, 443, 8080],
8283
+ commonPorts: [SERVICE_PORTS.HTTP, SERVICE_PORTS.HTTPS, SERVICE_PORTS.HTTP_ALT],
8118
8284
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8119
8285
  };
8120
8286
 
@@ -8149,12 +8315,12 @@ var DATABASE_TOOLS = [
8149
8315
  description: "MySQL enumeration - version, users, databases",
8150
8316
  parameters: {
8151
8317
  target: { type: "string", description: "Target IP/hostname" },
8152
- port: { type: "string", description: "Port (default 3306)" }
8318
+ port: { type: "string", description: `Port (default ${SERVICE_PORTS.MYSQL})` }
8153
8319
  },
8154
8320
  required: ["target"],
8155
8321
  execute: async (params) => {
8156
8322
  const target = params.target;
8157
- const port = params.port || "3306";
8323
+ const port = params.port || String(SERVICE_PORTS.MYSQL);
8158
8324
  return await runCommand("mysql", ["-h", target, "-P", port, "-e", "SELECT VERSION(), USER(), DATABASE();"]);
8159
8325
  }
8160
8326
  },
@@ -8175,12 +8341,12 @@ var DATABASE_TOOLS = [
8175
8341
  description: "Redis enumeration",
8176
8342
  parameters: {
8177
8343
  target: { type: "string", description: "Target IP" },
8178
- port: { type: "string", description: "Port (default 6379)" }
8344
+ port: { type: "string", description: `Port (default ${SERVICE_PORTS.REDIS})` }
8179
8345
  },
8180
8346
  required: ["target"],
8181
8347
  execute: async (params) => {
8182
8348
  const target = params.target;
8183
- const port = params.port || "6379";
8349
+ const port = params.port || String(SERVICE_PORTS.REDIS);
8184
8350
  return await runCommand("redis-cli", ["-h", target, "-p", port, "INFO"]);
8185
8351
  }
8186
8352
  },
@@ -8212,7 +8378,7 @@ var DATABASE_CONFIG = {
8212
8378
  tools: DATABASE_TOOLS,
8213
8379
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8214
8380
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8215
- commonPorts: [1433, 3306, 5432, 6379, 27017],
8381
+ commonPorts: [SERVICE_PORTS.MSSQL, SERVICE_PORTS.MYSQL, SERVICE_PORTS.POSTGRESQL, SERVICE_PORTS.REDIS, SERVICE_PORTS.MONGODB],
8216
8382
  commonServices: [SERVICES.MYSQL, SERVICES.POSTGRES, SERVICES.REDIS, SERVICES.MONGODB]
8217
8383
  };
8218
8384
 
@@ -8265,7 +8431,7 @@ var AD_CONFIG = {
8265
8431
  tools: AD_TOOLS,
8266
8432
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8267
8433
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8268
- commonPorts: [88, 389, 445],
8434
+ commonPorts: [SERVICE_PORTS.KERBEROS, SERVICE_PORTS.LDAP, SERVICE_PORTS.SMB],
8269
8435
  commonServices: [SERVICES.AD, SERVICES.SMB]
8270
8436
  };
8271
8437
 
@@ -8289,7 +8455,7 @@ var EMAIL_CONFIG = {
8289
8455
  tools: EMAIL_TOOLS,
8290
8456
  dangerLevel: DANGER_LEVELS.ACTIVE,
8291
8457
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8292
- commonPorts: [25, 110, 143, 465, 587, 993, 995],
8458
+ commonPorts: [SERVICE_PORTS.SMTP, SERVICE_PORTS.POP3, SERVICE_PORTS.IMAP, SERVICE_PORTS.SMTPS, SERVICE_PORTS.SMTP_TLS, SERVICE_PORTS.IMAPS, SERVICE_PORTS.POP3S],
8293
8459
  commonServices: [SERVICES.SMTP, SERVICES.POP3, SERVICES.IMAP]
8294
8460
  };
8295
8461
 
@@ -8314,7 +8480,7 @@ var REMOTE_ACCESS_TOOLS = [
8314
8480
  },
8315
8481
  required: ["target"],
8316
8482
  execute: async (params) => {
8317
- return await runCommand("nmap", ["-p", "3389", "--script", "rdp-enum-encryption,rdp-ntlm-info", params.target]);
8483
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.RDP), "--script", "rdp-enum-encryption,rdp-ntlm-info", params.target]);
8318
8484
  }
8319
8485
  }
8320
8486
  ];
@@ -8324,7 +8490,7 @@ var REMOTE_ACCESS_CONFIG = {
8324
8490
  tools: REMOTE_ACCESS_TOOLS,
8325
8491
  dangerLevel: DANGER_LEVELS.ACTIVE,
8326
8492
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8327
- commonPorts: [22, 3389, 5900],
8493
+ commonPorts: [SERVICE_PORTS.SSH, SERVICE_PORTS.RDP, SERVICE_PORTS.VNC],
8328
8494
  commonServices: [SERVICES.SSH, SERVICES.RDP, SERVICES.VNC]
8329
8495
  };
8330
8496
 
@@ -8338,7 +8504,7 @@ var FILE_SHARING_TOOLS = [
8338
8504
  },
8339
8505
  required: ["target"],
8340
8506
  execute: async (params) => {
8341
- return await runCommand("nmap", ["-p", "21", "--script", "ftp-anon,ftp-syst", params.target]);
8507
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.FTP), "--script", "ftp-anon,ftp-syst", params.target]);
8342
8508
  }
8343
8509
  },
8344
8510
  {
@@ -8359,7 +8525,7 @@ var FILE_SHARING_CONFIG = {
8359
8525
  tools: FILE_SHARING_TOOLS,
8360
8526
  dangerLevel: DANGER_LEVELS.ACTIVE,
8361
8527
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8362
- commonPorts: [21, 139, 445, 2049],
8528
+ commonPorts: [SERVICE_PORTS.FTP, SERVICE_PORTS.SMB_NETBIOS, SERVICE_PORTS.SMB, SERVICE_PORTS.NFS],
8363
8529
  commonServices: [SERVICES.FTP, SERVICES.SMB, SERVICES.NFS]
8364
8530
  };
8365
8531
 
@@ -8403,7 +8569,7 @@ var CLOUD_CONFIG = {
8403
8569
  tools: CLOUD_TOOLS,
8404
8570
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8405
8571
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8406
- commonPorts: [443],
8572
+ commonPorts: [SERVICE_PORTS.HTTPS],
8407
8573
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8408
8574
  };
8409
8575
 
@@ -8438,7 +8604,7 @@ var CONTAINER_CONFIG = {
8438
8604
  tools: CONTAINER_TOOLS,
8439
8605
  dangerLevel: DANGER_LEVELS.EXPLOIT,
8440
8606
  defaultApproval: APPROVAL_LEVELS.REVIEW,
8441
- commonPorts: [2375, 2376, 5e3, 6443],
8607
+ commonPorts: [SERVICE_PORTS.DOCKER_HTTP, SERVICE_PORTS.DOCKER_HTTPS, SERVICE_PORTS.FLASK_DEFAULT, SERVICE_PORTS.KUBERNETES_API],
8442
8608
  commonServices: [SERVICES.DOCKER, SERVICES.KUBERNETES]
8443
8609
  };
8444
8610
 
@@ -8501,7 +8667,7 @@ var API_CONFIG = {
8501
8667
  tools: API_TOOLS,
8502
8668
  dangerLevel: DANGER_LEVELS.ACTIVE,
8503
8669
  defaultApproval: APPROVAL_LEVELS.CONFIRM,
8504
- commonPorts: [3e3, 5e3, 8e3, 8080],
8670
+ commonPorts: [SERVICE_PORTS.NODE_DEFAULT, SERVICE_PORTS.FLASK_DEFAULT, SERVICE_PORTS.DJANGO_DEFAULT, SERVICE_PORTS.HTTP_ALT],
8505
8671
  commonServices: [SERVICES.HTTP, SERVICES.HTTPS]
8506
8672
  };
8507
8673
 
@@ -8539,7 +8705,7 @@ var ICS_TOOLS = [
8539
8705
  },
8540
8706
  required: ["target"],
8541
8707
  execute: async (params) => {
8542
- return await runCommand("nmap", ["-p", "502", "--script", "modbus-discover", params.target]);
8708
+ return await runCommand("nmap", ["-p", String(SERVICE_PORTS.MODBUS), "--script", "modbus-discover", params.target]);
8543
8709
  }
8544
8710
  }
8545
8711
  ];
@@ -8549,7 +8715,7 @@ var ICS_CONFIG = {
8549
8715
  tools: ICS_TOOLS,
8550
8716
  dangerLevel: DANGER_LEVELS.ACTIVE,
8551
8717
  defaultApproval: APPROVAL_LEVELS.BLOCK,
8552
- commonPorts: [502, 2e4],
8718
+ commonPorts: [SERVICE_PORTS.MODBUS, SERVICE_PORTS.DNP3],
8553
8719
  commonServices: [SERVICES.MODBUS, SERVICES.DNP3]
8554
8720
  };
8555
8721
 
@@ -8611,8 +8777,18 @@ var ToolRegistry = class {
8611
8777
  this.logDeniedAction(toolCall, approval.reason || "Execution denied");
8612
8778
  return { success: false, output: "", error: approval.reason || "Denied by policy" };
8613
8779
  }
8614
- const result2 = await tool.execute(toolCall.input);
8615
- const command = String(toolCall.input.command || toolCall.input.url || toolCall.input.query || "");
8780
+ let result2;
8781
+ try {
8782
+ result2 = await tool.execute(toolCall.input);
8783
+ } catch (execError) {
8784
+ const errMsg = execError instanceof Error ? execError.message : String(execError);
8785
+ result2 = {
8786
+ success: false,
8787
+ output: "",
8788
+ error: `Tool execution error: ${errMsg}`
8789
+ };
8790
+ }
8791
+ const command = String(toolCall.input.command || toolCall.input.url || toolCall.input.query || JSON.stringify(toolCall.input));
8616
8792
  if (result2.success) {
8617
8793
  this.state.workingMemory.recordSuccess(toolCall.name, command, result2.output || "");
8618
8794
  } else {
@@ -8715,7 +8891,7 @@ var SERVICE_CATEGORY_MAP = {
8715
8891
  "docker": SERVICE_CATEGORIES.CONTAINER,
8716
8892
  "modbus": SERVICE_CATEGORIES.ICS
8717
8893
  };
8718
- var CATEGORY_APPROVAL2 = {
8894
+ var CATEGORY_APPROVAL = {
8719
8895
  [SERVICE_CATEGORIES.NETWORK]: APPROVAL_LEVELS.CONFIRM,
8720
8896
  [SERVICE_CATEGORIES.WEB]: APPROVAL_LEVELS.CONFIRM,
8721
8897
  [SERVICE_CATEGORIES.DATABASE]: APPROVAL_LEVELS.REVIEW,
@@ -8905,7 +9081,7 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8905
9081
  description: DOMAINS[id]?.description || "",
8906
9082
  tools: [...coreTools],
8907
9083
  dangerLevel: this.calculateDanger(id),
8908
- defaultApproval: CATEGORY_APPROVAL2[id] || "confirm"
9084
+ defaultApproval: CATEGORY_APPROVAL[id] || "confirm"
8909
9085
  });
8910
9086
  });
8911
9087
  }
@@ -8971,7 +9147,7 @@ var CategorizedToolRegistry = class extends ToolRegistry {
8971
9147
  return Array.from(this.categories.values());
8972
9148
  }
8973
9149
  getApprovalForCategory(cat) {
8974
- return CATEGORY_APPROVAL2[cat] || "confirm";
9150
+ return CATEGORY_APPROVAL[cat] || "confirm";
8975
9151
  }
8976
9152
  };
8977
9153
 
@@ -9056,14 +9232,28 @@ var LLM_ERROR_TYPES = {
9056
9232
  var HTTP_STATUS = { BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403, RATE_LIMIT: 429 };
9057
9233
  var NETWORK_ERROR_CODES = {
9058
9234
  ECONNRESET: "ECONNRESET",
9235
+ ECONNREFUSED: "ECONNREFUSED",
9059
9236
  ETIMEDOUT: "ETIMEDOUT",
9060
9237
  ENOTFOUND: "ENOTFOUND",
9061
- CONNECT_TIMEOUT: "UND_ERR_CONNECT_TIMEOUT"
9238
+ CONNECT_TIMEOUT: "UND_ERR_CONNECT_TIMEOUT",
9239
+ SOCKET_TIMEOUT: "UND_ERR_SOCKET"
9062
9240
  };
9063
9241
  var TRANSIENT_NETWORK_ERRORS = [
9064
9242
  NETWORK_ERROR_CODES.ECONNRESET,
9243
+ NETWORK_ERROR_CODES.ECONNREFUSED,
9065
9244
  NETWORK_ERROR_CODES.ETIMEDOUT,
9066
- NETWORK_ERROR_CODES.ENOTFOUND
9245
+ NETWORK_ERROR_CODES.ENOTFOUND,
9246
+ NETWORK_ERROR_CODES.SOCKET_TIMEOUT
9247
+ ];
9248
+ var NETWORK_ERROR_PATTERNS = [
9249
+ "fetch failed",
9250
+ "network error",
9251
+ "econnrefused",
9252
+ "econnreset",
9253
+ "enotfound",
9254
+ "etimedout",
9255
+ "socket hang up",
9256
+ "dns lookup failed"
9067
9257
  ];
9068
9258
  var LLMError = class extends Error {
9069
9259
  /** Structured error information */
@@ -9090,12 +9280,17 @@ function classifyError(error) {
9090
9280
  if (statusCode === HTTP_STATUS.BAD_REQUEST) {
9091
9281
  return { type: LLM_ERROR_TYPES.INVALID_REQUEST, message: errorMessage, statusCode, isRetryable: false, suggestedAction: "Modify request" };
9092
9282
  }
9093
- if (e.code && TRANSIENT_NETWORK_ERRORS.includes(e.code)) {
9283
+ const errorCode = e.code || e.cause?.code;
9284
+ if (errorCode && TRANSIENT_NETWORK_ERRORS.includes(errorCode)) {
9094
9285
  return { type: LLM_ERROR_TYPES.NETWORK_ERROR, message: errorMessage, isRetryable: true, suggestedAction: "Check network" };
9095
9286
  }
9096
- if (errorMessage.toLowerCase().includes("timeout") || e.code === NETWORK_ERROR_CODES.CONNECT_TIMEOUT) {
9287
+ if (errorMessage.toLowerCase().includes("timeout") || errorCode === NETWORK_ERROR_CODES.CONNECT_TIMEOUT) {
9097
9288
  return { type: LLM_ERROR_TYPES.TIMEOUT, message: errorMessage, isRetryable: true, suggestedAction: "Retry" };
9098
9289
  }
9290
+ const lowerMsg = errorMessage.toLowerCase();
9291
+ if (NETWORK_ERROR_PATTERNS.some((pattern) => lowerMsg.includes(pattern))) {
9292
+ return { type: LLM_ERROR_TYPES.NETWORK_ERROR, message: errorMessage, isRetryable: true, suggestedAction: "Check network" };
9293
+ }
9099
9294
  return { type: LLM_ERROR_TYPES.UNKNOWN, message: errorMessage, statusCode, isRetryable: false, suggestedAction: "Analyze error" };
9100
9295
  }
9101
9296
 
@@ -9174,7 +9369,11 @@ var LLMClient = class {
9174
9369
  signal
9175
9370
  });
9176
9371
  if (!response.ok) {
9177
- const errorBody = await response.text();
9372
+ let errorBody = `HTTP ${response.status}`;
9373
+ try {
9374
+ errorBody = await response.text();
9375
+ } catch {
9376
+ }
9178
9377
  const error = new Error(errorBody);
9179
9378
  error.status = response.status;
9180
9379
  throw error;
@@ -9547,7 +9746,10 @@ function loadState(state) {
9547
9746
  state.addLoot(loot);
9548
9747
  }
9549
9748
  for (const item of snapshot.todo) {
9550
- state.addTodo(item.content, item.priority);
9749
+ const id = state.addTodo(item.content, item.priority);
9750
+ if (item.status && item.status !== "pending") {
9751
+ state.updateTodo(id, { status: item.status });
9752
+ }
9551
9753
  }
9552
9754
  const validPhases = new Set(Object.values(PHASES));
9553
9755
  const restoredPhase = validPhases.has(snapshot.currentPhase) ? snapshot.currentPhase : PHASES.RECON;
@@ -9557,6 +9759,20 @@ function loadState(state) {
9557
9759
  }
9558
9760
  if (snapshot.missionChecklist?.length > 0) {
9559
9761
  state.addMissionChecklistItems(snapshot.missionChecklist.map((c) => c.text));
9762
+ const restoredChecklist = state.getMissionChecklist();
9763
+ const baseIndex = restoredChecklist.length - snapshot.missionChecklist.length;
9764
+ const completedUpdates = [];
9765
+ for (let i = 0; i < snapshot.missionChecklist.length; i++) {
9766
+ if (snapshot.missionChecklist[i].isCompleted && restoredChecklist[baseIndex + i]) {
9767
+ completedUpdates.push({
9768
+ id: restoredChecklist[baseIndex + i].id,
9769
+ isCompleted: true
9770
+ });
9771
+ }
9772
+ }
9773
+ if (completedUpdates.length > 0) {
9774
+ state.updateMissionChecklist(completedUpdates);
9775
+ }
9560
9776
  }
9561
9777
  return true;
9562
9778
  } catch (err) {
@@ -9570,8 +9786,9 @@ function clearWorkspace() {
9570
9786
  const dirsToClean = [
9571
9787
  { path: WORKSPACE.SESSIONS, label: "sessions" },
9572
9788
  { path: WORKSPACE.DEBUG, label: "debug logs" },
9573
- { path: WORKSPACE.TEMP, label: "temp files" },
9574
- { path: WORKSPACE.OUTPUTS, label: "outputs" }
9789
+ { path: WORKSPACE.TMP, label: "temp files" },
9790
+ { path: WORKSPACE.OUTPUTS, label: "outputs" },
9791
+ { path: WORKSPACE.JOURNAL, label: "journal" }
9575
9792
  ];
9576
9793
  for (const dir of dirsToClean) {
9577
9794
  try {
@@ -9664,374 +9881,84 @@ function appendBlockedCommandHints(lines, errorLower) {
9664
9881
  }
9665
9882
  }
9666
9883
 
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
- }
9731
- }
9732
- if (intel.length === 0) {
9733
- intel = extractGenericIntel(output);
9734
- detectedTool = toolName || "unknown";
9735
- }
9736
- if (intel.length === 0) {
9737
- return output;
9738
- }
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;
9934
- }
9935
-
9936
9884
  // src/shared/utils/context-digest.ts
9937
9885
  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;
9886
+ var PASSTHROUGH_THRESHOLD = 500;
9887
+ var PREPROCESS_THRESHOLD = 3e3;
9888
+ var MAX_PREPROCESSED_LINES = 800;
9942
9889
  var getOutputDir = () => WORKSPACE.OUTPUTS;
9943
9890
  var MAX_DUPLICATE_DISPLAY = 3;
9944
- var LAYER3_MAX_INPUT_CHARS = 8e4;
9891
+ var ANALYST_MAX_INPUT_CHARS = 8e4;
9945
9892
  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
- var NOISE_PATTERNS = [
9968
- /^\s*$/,
9969
- // blank lines
9970
- /^\[[\d:]+\]\s*$/,
9971
- // timestamp-only lines
9972
- /^#+\s*$/,
9973
- // separator lines
9974
- /^\s*Progress:\s*\[?[#=\-\s>]+\]?\s*\d+/i,
9975
- // progress bars
9976
- /\d+\s+requests?\s+(?:sent|made)/i,
9977
- // request counters
9978
- /^\s*(?:\.{3,}|={5,}|-{5,})\s*$/
9979
- // decoration lines
9980
- ];
9981
- async function digestToolOutput(output, toolName, toolInput, llmDigestFn) {
9893
+ async function digestToolOutput(output, toolName, toolInput, analystFn) {
9982
9894
  const originalLength = output.length;
9983
9895
  if (originalLength < PASSTHROUGH_THRESHOLD) {
9984
9896
  return {
9985
9897
  digestedOutput: output,
9986
9898
  fullOutputPath: null,
9987
- layersApplied: [],
9899
+ analystUsed: false,
9900
+ memo: null,
9988
9901
  originalLength,
9989
9902
  digestedLength: originalLength,
9990
9903
  compressionRatio: 1
9991
9904
  };
9992
9905
  }
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);
9906
+ const savedOutputPath = saveFullOutput(output, toolName);
9907
+ let preprocessed = output;
9908
+ if (originalLength > PREPROCESS_THRESHOLD) {
9909
+ preprocessed = structuralPreprocess(output);
9910
+ }
9911
+ if (analystFn) {
9912
+ try {
9913
+ const context = `Tool: ${toolName}${toolInput ? ` | Input: ${toolInput}` : ""}`;
9914
+ const rawAnalystResponse = await analystFn(preprocessed, context);
9915
+ const memo6 = parseAnalystMemo(rawAnalystResponse);
9916
+ const formatted = formatAnalystDigest(rawAnalystResponse, savedOutputPath, originalLength);
9917
+ return {
9918
+ digestedOutput: formatted,
9919
+ fullOutputPath: savedOutputPath,
9920
+ analystUsed: true,
9921
+ memo: memo6,
9922
+ originalLength,
9923
+ digestedLength: formatted.length,
9924
+ compressionRatio: formatted.length / originalLength
9925
+ };
9926
+ } catch (err) {
9927
+ debugLog("general", "Analyst LLM failed, falling back to truncation", {
9928
+ toolName,
9929
+ error: String(err)
9930
+ });
10017
9931
  }
10018
- } else if (layersApplied.includes(2)) {
10019
- savedOutputPath = saveFullOutput(output, toolName);
10020
9932
  }
9933
+ const fallback = formatFallbackDigest(preprocessed, savedOutputPath, originalLength);
10021
9934
  return {
10022
- digestedOutput: processed,
9935
+ digestedOutput: fallback,
10023
9936
  fullOutputPath: savedOutputPath,
10024
- layersApplied,
9937
+ analystUsed: false,
9938
+ memo: null,
10025
9939
  originalLength,
10026
- digestedLength: processed.length,
10027
- compressionRatio: processed.length / originalLength
9940
+ digestedLength: fallback.length,
9941
+ compressionRatio: fallback.length / originalLength
10028
9942
  };
10029
9943
  }
10030
- function structuralReduce(output) {
9944
+ var NOISE_PATTERNS = [
9945
+ /^\s*$/,
9946
+ // blank lines
9947
+ /^\[[\d:]+\]\s*$/,
9948
+ // timestamp-only lines
9949
+ /^#+\s*$/,
9950
+ // separator lines
9951
+ /^\s*Progress:\s*\[?[#=\-\s>]+\]?\s*\d/i,
9952
+ // progress bars
9953
+ /\d+\s+requests?\s+(?:sent|made)/i,
9954
+ // request counters
9955
+ /^\s*(?:\.{3,}|={5,}|-{5,})\s*$/
9956
+ // decoration lines
9957
+ ];
9958
+ function structuralPreprocess(output) {
10031
9959
  let cleaned = stripAnsi(output);
10032
9960
  const lines = cleaned.split("\n");
10033
9961
  const result2 = [];
10034
- const duplicateCounts = /* @__PURE__ */ new Map();
10035
9962
  let lastLine = "";
10036
9963
  let consecutiveDupes = 0;
10037
9964
  for (const line of lines) {
@@ -10039,11 +9966,9 @@ function structuralReduce(output) {
10039
9966
  if (NOISE_PATTERNS.some((p) => p.test(trimmed))) {
10040
9967
  continue;
10041
9968
  }
10042
- const isSignal = SIGNAL_PATTERNS.some((p) => p.test(trimmed));
10043
9969
  const normalized = normalizeLine(trimmed);
10044
- if (normalized === normalizeLine(lastLine) && !isSignal) {
9970
+ if (normalized === normalizeLine(lastLine)) {
10045
9971
  consecutiveDupes++;
10046
- duplicateCounts.set(normalized, (duplicateCounts.get(normalized) || 1) + 1);
10047
9972
  continue;
10048
9973
  }
10049
9974
  if (consecutiveDupes > 0) {
@@ -10066,57 +9991,124 @@ function structuralReduce(output) {
10066
9991
  result2.push(lastLine);
10067
9992
  }
10068
9993
  }
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;
9994
+ if (result2.length > MAX_PREPROCESSED_LINES) {
9995
+ const headSize = Math.floor(MAX_PREPROCESSED_LINES * 0.5);
9996
+ const tailSize = Math.floor(MAX_PREPROCESSED_LINES * 0.3);
10073
9997
  const head = result2.slice(0, headSize);
10074
9998
  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;
9999
+ const skipped = result2.length - headSize - tailSize;
10078
10000
  cleaned = [
10079
10001
  ...head,
10080
10002
  "",
10081
- `... [${skipped} routine lines skipped \u2014 ${middleSignals.length} important lines preserved] ...`,
10082
- "",
10083
- ...middleSignals,
10084
- "",
10085
- `... [resuming last ${tailSize} lines] ...`,
10003
+ `... [${skipped} lines skipped for Analyst LLM context \u2014 full output saved to file] ...`,
10086
10004
  "",
10087
10005
  ...tail
10088
10006
  ].join("\n");
10089
10007
  } else {
10090
10008
  cleaned = result2.join("\n");
10091
10009
  }
10010
+ if (cleaned.length > ANALYST_MAX_INPUT_CHARS) {
10011
+ cleaned = cleaned.slice(0, ANALYST_MAX_INPUT_CHARS) + `
10012
+ ... [truncated at ${ANALYST_MAX_INPUT_CHARS} chars for Analyst LLM \u2014 full output saved to file]`;
10013
+ }
10092
10014
  return cleaned;
10093
10015
  }
10094
- var DIGEST_SYSTEM_PROMPT = `You are a pentesting output analyst. Given raw tool output, extract ONLY actionable intelligence. Be terse and structured.
10016
+ var ANALYST_SYSTEM_PROMPT = `You are an independent pentesting output analyst. You receive raw tool output and must extract ONLY actionable intelligence for the main attack agent.
10095
10017
 
10096
10018
  FORMAT YOUR RESPONSE EXACTLY LIKE THIS:
10019
+
10097
10020
  ## Key Findings
10098
- - [finding 1]
10021
+ - [finding 1 with exact values: ports, versions, paths]
10099
10022
  - [finding 2]
10100
10023
 
10101
10024
  ## Credentials/Secrets
10102
- - [any discovered credentials, hashes, tokens, keys]
10025
+ - [any discovered credentials, hashes, tokens, keys, certificates]
10026
+ - (write "None found" if none)
10103
10027
 
10104
10028
  ## Attack Vectors
10105
- - [exploitable services, vulnerabilities, misconfigurations]
10029
+ - [exploitable services, vulnerabilities, misconfigurations, CVEs]
10030
+ - (write "None identified" if none)
10031
+
10032
+ ## Failures/Errors
10033
+ - [what was attempted and FAILED \u2014 include the FULL command, wordlist, target, and the reason WHY it failed]
10034
+ - [e.g.: "SSH brute force: hydra -l admin -P /usr/share/wordlists/rockyou.txt ssh://10.0.0.1 \u2014 connection refused (port filtered)"]
10035
+ - [e.g.: "SQLi on /login with sqlmap --tamper=space2comment \u2014 input sanitized, WAF detected (ModSecurity)"]
10036
+ - (write "No failures" if everything succeeded)
10037
+
10038
+ ## Suspicious Signals
10039
+ - [anomalies that are NOT confirmed vulnerabilities but suggest exploitable surface]
10040
+ - [e.g.: "Response time 3x slower on /admin path \u2014 possible auth check or backend processing"]
10041
+ - [e.g.: "X-Debug-Token header present \u2014 debug mode may be enabled"]
10042
+ - [e.g.: "Verbose error message reveals stack trace / internal path / DB schema"]
10043
+ - [e.g.: "Unexpected 302 redirect with session param leaked in URL"]
10044
+ - (write "No suspicious signals" if nothing anomalous)
10045
+
10046
+ ## Attack Value
10047
+ - [ONE word: HIGH / MED / LOW / NONE]
10048
+ - Reasoning: [1 sentence why \u2014 what makes this worth pursuing or abandoning]
10106
10049
 
10107
10050
  ## Next Steps
10108
10051
  - [recommended immediate actions based on findings]
10109
10052
 
10110
10053
  RULES:
10111
- - Be EXTREMELY concise \u2014 max 30 lines total
10112
- - Only include ACTIONABLE findings \u2014 skip routine/expected results
10054
+ - Include EXACT values: port numbers, versions, usernames, file paths, IPs, full commands used
10055
+ - For failures: include the COMPLETE command with all flags, wordlists, and targets \u2014 "brute force failed" alone is USELESS
10056
+ - Look for the UNEXPECTED \u2014 non-standard ports, unusual banners, timing anomalies, error leaks
10057
+ - Credentials include: passwords, hashes, API keys, tokens, private keys, cookies, session IDs
10058
+ - Flag any information disclosure: server versions, internal paths, stack traces, debug output
10113
10059
  - 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) {
10060
+ - Never include decorative output, banners, or progress information
10061
+ - Do NOT miss subtle signals: unusual HTTP headers, non-standard responses, timing differences
10062
+ - Write as much detail as needed \u2014 do NOT artificially shorten. Every detail matters for strategy.
10063
+
10064
+ ## Reflection
10065
+ - What this output tells us: [1-line assessment]
10066
+ - Recommended next action: [1-2 specific follow-up actions]`;
10067
+ function parseAnalystMemo(response) {
10068
+ const sections = {};
10069
+ let currentSection = "";
10070
+ let reflectionLines = [];
10071
+ let attackValueLine = "NONE";
10072
+ let attackValueReasoning = "";
10073
+ for (const line of response.split("\n")) {
10074
+ if (line.startsWith("## ")) {
10075
+ currentSection = line.replace("## ", "").trim().toLowerCase();
10076
+ sections[currentSection] = [];
10077
+ } else if (currentSection === "reflection") {
10078
+ if (line.trim()) reflectionLines.push(line.trim());
10079
+ } else if (currentSection === "attack value") {
10080
+ const match = line.match(/\b(HIGH|MED|LOW|NONE)\b/);
10081
+ if (match) attackValueLine = match[1];
10082
+ const reasonMatch = line.match(/[Rr]easoning:\s*(.+)/);
10083
+ if (reasonMatch) attackValueReasoning = reasonMatch[1].trim();
10084
+ } else if (currentSection) {
10085
+ const trimmed = line.trim();
10086
+ if (!trimmed) continue;
10087
+ const content = trimmed.replace(/^(?:-|\*|\d+[.)]\s*)\s*/, "").trim();
10088
+ if (content) sections[currentSection].push(content);
10089
+ }
10090
+ }
10091
+ const filterNone = (items) => items.filter((i) => !/(^none|^no )/i.test(i.trim()));
10092
+ const rawValue = attackValueLine.toUpperCase();
10093
+ const attackValue = ["HIGH", "MED", "LOW", "NONE"].includes(rawValue) ? rawValue : "LOW";
10094
+ return {
10095
+ keyFindings: filterNone(sections["key findings"] || []),
10096
+ credentials: filterNone(sections["credentials/secrets"] || []),
10097
+ attackVectors: filterNone(sections["attack vectors"] || []),
10098
+ failures: filterNone(sections["failures/errors"] || []),
10099
+ suspicions: filterNone(sections["suspicious signals"] || []),
10100
+ attackValue,
10101
+ nextSteps: filterNone(sections["next steps"] || []),
10102
+ reflection: [
10103
+ attackValueReasoning ? `[${attackValue}] ${attackValueReasoning}` : "",
10104
+ ...reflectionLines
10105
+ ].filter(Boolean).join(" | ")
10106
+ };
10107
+ }
10108
+ function formatAnalystDigest(digest, filePath, originalChars) {
10117
10109
  return [
10118
10110
  "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
10119
- "\u2551 CONTEXT DIGEST (LLM-summarized) \u2551",
10111
+ "\u2551 ANALYST DIGEST (Independent LLM analysis) \u2551",
10120
10112
  "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
10121
10113
  "",
10122
10114
  digest,
@@ -10126,29 +10118,26 @@ function formatLLMDigest(digest, filePath, originalChars) {
10126
10118
  ].join("\n");
10127
10119
  }
10128
10120
  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
10121
  const maxChars = FALLBACK_MAX_CHARS;
10134
- let truncatedRemaining = remaining;
10135
- if (remaining.length > maxChars) {
10122
+ let truncated = processed;
10123
+ if (processed.length > maxChars) {
10136
10124
  const headChars = Math.floor(maxChars * 0.6);
10137
10125
  const tailChars = Math.floor(maxChars * 0.4);
10138
- const skipped = remaining.length - headChars - tailChars;
10139
- truncatedRemaining = remaining.slice(0, headChars) + `
10126
+ const skipped = processed.length - headChars - tailChars;
10127
+ truncated = processed.slice(0, headChars) + `
10140
10128
 
10141
- ... [${skipped} chars omitted \u2014 read full output from file] ...
10129
+ ... [${skipped} chars omitted \u2014 Analyst LLM unavailable, read full output from file] ...
10142
10130
 
10143
- ` + remaining.slice(-tailChars);
10131
+ ` + processed.slice(-tailChars);
10144
10132
  }
10145
10133
  return [
10146
- summaryBlock,
10147
- truncatedRemaining,
10134
+ "\u26A0\uFE0F ANALYST UNAVAILABLE \u2014 showing truncated raw output:",
10135
+ "",
10136
+ truncated,
10148
10137
  "",
10149
10138
  `\u{1F4C2} Full output saved: ${filePath} (${originalChars} chars)`,
10150
10139
  `\u{1F4A1} Use read_file("${filePath}") to see the complete raw output.`
10151
- ].filter(Boolean).join("\n");
10140
+ ].join("\n");
10152
10141
  }
10153
10142
  function stripAnsi(text) {
10154
10143
  return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1B\[[\d;]*m/g, "");
@@ -10174,20 +10163,21 @@ function saveFullOutput(output, toolName) {
10174
10163
  }
10175
10164
  function createLLMDigestFn(llmClient) {
10176
10165
  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.
10166
+ const messages = [{
10167
+ role: LLM_ROLES.USER,
10168
+ content: `Analyze this pentesting tool output and extract actionable intelligence.
10180
10169
 
10181
10170
  Context: ${context}
10182
10171
 
10183
10172
  --- OUTPUT START ---
10184
- ${truncatedText}
10185
- --- OUTPUT END ---` }];
10173
+ ${text}
10174
+ --- OUTPUT END ---`
10175
+ }];
10186
10176
  const response = await llmClient.generateResponse(
10187
10177
  messages,
10188
10178
  void 0,
10189
- // no tools — summarization only
10190
- DIGEST_SYSTEM_PROMPT
10179
+ // no tools — analysis only
10180
+ ANALYST_SYSTEM_PROMPT
10191
10181
  );
10192
10182
  return response.content || "No actionable findings extracted.";
10193
10183
  };
@@ -10202,6 +10192,16 @@ var CoreAgent = class _CoreAgent {
10202
10192
  agentType;
10203
10193
  maxIterations;
10204
10194
  abortController = null;
10195
+ /**
10196
+ * Collected tool execution records for the current turn.
10197
+ * MainAgent reads this after each step to write journal entries.
10198
+ * Cleared at the start of each step.
10199
+ */
10200
+ turnToolJournal = [];
10201
+ /** Aggregated memo from all tools in the current turn */
10202
+ turnMemo = { keyFindings: [], credentials: [], attackVectors: [], failures: [], suspicions: [], attackValue: "LOW", nextSteps: [] };
10203
+ /** Analyst reflections collected during this turn (1-line assessments) */
10204
+ turnReflections = [];
10205
10205
  constructor(agentType, state, events, toolRegistry, maxIterations) {
10206
10206
  this.agentType = agentType;
10207
10207
  this.state = state;
@@ -10329,7 +10329,22 @@ RULES:
10329
10329
  });
10330
10330
  continue;
10331
10331
  }
10332
- throw error;
10332
+ const unexpectedMsg = error instanceof Error ? error.message : String(error);
10333
+ this.events.emit({
10334
+ type: EVENT_TYPES.ERROR,
10335
+ timestamp: Date.now(),
10336
+ data: {
10337
+ message: `Unexpected error: ${unexpectedMsg}`,
10338
+ phase: this.state.getPhase(),
10339
+ isRecoverable: true
10340
+ }
10341
+ });
10342
+ messages.push({
10343
+ role: LLM_ROLES.USER,
10344
+ content: `\u26A0\uFE0F UNEXPECTED ERROR: ${unexpectedMsg}
10345
+ This may be a transient issue. Continue your task \u2014 retry the last action or try an alternative approach.`
10346
+ });
10347
+ continue;
10333
10348
  }
10334
10349
  }
10335
10350
  const summary = `Max iterations (${this.maxIterations}) reached. Progress: ${progress.totalToolsExecuted} tools executed (${progress.toolSuccesses} succeeded, ${progress.toolErrors} failed). Current phase: ${this.state.getPhase()}. Findings: ${this.state.getFindings?.()?.length ?? "unknown"}.`;
@@ -10647,27 +10662,62 @@ ${firstLine}`, phase }
10647
10662
  progress.blockedCommandPatterns.clear();
10648
10663
  }
10649
10664
  }
10665
+ const rawOutputForTUI = outputText;
10666
+ let digestedOutputForLLM = outputText;
10667
+ let digestResult = null;
10650
10668
  try {
10651
10669
  const llmDigestFn = createLLMDigestFn(this.llm);
10652
- const digestResult = await digestToolOutput(
10670
+ digestResult = await digestToolOutput(
10653
10671
  outputText,
10654
10672
  call.name,
10655
10673
  JSON.stringify(call.input).slice(0, DISPLAY_LIMITS.OUTPUT_SUMMARY),
10656
10674
  llmDigestFn
10657
10675
  );
10658
- outputText = digestResult.digestedOutput;
10676
+ digestedOutputForLLM = digestResult.digestedOutput;
10659
10677
  } 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}
10678
+ if (digestedOutputForLLM.length > AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH) {
10679
+ const truncated = digestedOutputForLLM.slice(0, AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH);
10680
+ const remaining = digestedOutputForLLM.length - AGENT_LIMITS.MAX_TOOL_OUTPUT_LENGTH;
10681
+ digestedOutputForLLM = `${truncated}
10664
10682
 
10665
10683
  ... [TRUNCATED ${remaining} characters for context hygiene] ...
10666
10684
  \u{1F4A1} TIP: If you need to see the full output, use a tool to read the file directly or run the command with | head, | tail, or | grep.`;
10667
10685
  }
10668
10686
  }
10669
- this.emitToolResult(call.name, result2.success, outputText, result2.error, Date.now() - toolStartTime);
10670
- return { toolCallId: call.id, output: outputText, error: result2.error };
10687
+ this.emitToolResult(call.name, result2.success, rawOutputForTUI, result2.error, Date.now() - toolStartTime);
10688
+ const inputSummary = JSON.stringify(call.input);
10689
+ this.turnToolJournal.push({
10690
+ name: call.name,
10691
+ inputSummary,
10692
+ success: result2.success,
10693
+ analystSummary: digestResult?.memo ? digestResult.memo.keyFindings.join("; ") || "No key findings" : digestedOutputForLLM,
10694
+ outputFile: digestResult?.fullOutputPath ?? null
10695
+ });
10696
+ if (digestResult?.memo) {
10697
+ const m = digestResult.memo;
10698
+ this.turnMemo.keyFindings.push(...m.keyFindings);
10699
+ this.turnMemo.credentials.push(...m.credentials);
10700
+ this.turnMemo.attackVectors.push(...m.attackVectors);
10701
+ this.turnMemo.failures.push(...m.failures);
10702
+ this.turnMemo.suspicions.push(...m.suspicions);
10703
+ const VALUE_RANK = { HIGH: 3, MED: 2, LOW: 1, NONE: 0 };
10704
+ if ((VALUE_RANK[m.attackValue] ?? 0) > (VALUE_RANK[this.turnMemo.attackValue] ?? 0)) {
10705
+ this.turnMemo.attackValue = m.attackValue;
10706
+ }
10707
+ this.turnMemo.nextSteps.push(...m.nextSteps);
10708
+ if (m.reflection) this.turnReflections.push(m.reflection);
10709
+ }
10710
+ if (digestResult?.memo?.credentials.length) {
10711
+ for (const cred of digestResult.memo.credentials) {
10712
+ this.state.addLoot({
10713
+ type: "credential",
10714
+ host: "auto-extracted",
10715
+ detail: cred,
10716
+ obtainedAt: Date.now()
10717
+ });
10718
+ }
10719
+ }
10720
+ return { toolCallId: call.id, output: digestedOutputForLLM, error: result2.error };
10671
10721
  } catch (error) {
10672
10722
  const errorMsg = String(error);
10673
10723
  const enrichedError = this.enrichToolError({ toolName: call.name, input: call.input, error: errorMsg, originalOutput: "", progress });
@@ -10742,8 +10792,8 @@ ${firstLine}`, phase }
10742
10792
  };
10743
10793
 
10744
10794
  // 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";
10795
+ import { readFileSync as readFileSync6, existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
10796
+ import { join as join11, dirname as dirname5 } from "path";
10747
10797
  import { fileURLToPath as fileURLToPath4 } from "url";
10748
10798
 
10749
10799
  // src/shared/constants/prompts.ts
@@ -10804,73 +10854,6 @@ var INITIAL_TASKS = {
10804
10854
  RECON: "Initial reconnaissance and target discovery"
10805
10855
  };
10806
10856
 
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
- ];
10873
-
10874
10857
  // src/shared/constants/scoring.ts
10875
10858
  var ATTACK_SCORING = {
10876
10859
  /** Base score for all attack prioritization */
@@ -11031,10 +11014,229 @@ function getAttacksForService(service, port) {
11031
11014
  return attacks;
11032
11015
  }
11033
11016
 
11017
+ // src/shared/utils/journal.ts
11018
+ import { writeFileSync as writeFileSync8, readFileSync as readFileSync5, existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync5 } from "fs";
11019
+ import { join as join10 } from "path";
11020
+ var MAX_JOURNAL_ENTRIES = 50;
11021
+ var SUMMARY_REGEN_INTERVAL = 10;
11022
+ var MAX_OUTPUT_FILES = 30;
11023
+ var TURN_PREFIX = "turn-";
11024
+ var SUMMARY_FILE = "summary.md";
11025
+ function writeJournalEntry(entry) {
11026
+ try {
11027
+ const journalDir = WORKSPACE.JOURNAL;
11028
+ ensureDirExists(journalDir);
11029
+ const padded = String(entry.turn).padStart(4, "0");
11030
+ const filePath = join10(journalDir, `${TURN_PREFIX}${padded}.json`);
11031
+ writeFileSync8(filePath, JSON.stringify(entry, null, 2), "utf-8");
11032
+ return filePath;
11033
+ } catch (err) {
11034
+ debugLog("general", "Failed to write journal entry", { turn: entry.turn, error: String(err) });
11035
+ return null;
11036
+ }
11037
+ }
11038
+ function readJournalSummary() {
11039
+ try {
11040
+ const summaryPath = join10(WORKSPACE.JOURNAL, SUMMARY_FILE);
11041
+ if (!existsSync8(summaryPath)) return "";
11042
+ return readFileSync5(summaryPath, "utf-8");
11043
+ } catch {
11044
+ return "";
11045
+ }
11046
+ }
11047
+ function getRecentEntries(count = MAX_JOURNAL_ENTRIES) {
11048
+ try {
11049
+ const journalDir = WORKSPACE.JOURNAL;
11050
+ if (!existsSync8(journalDir)) return [];
11051
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort().slice(-count);
11052
+ const entries = [];
11053
+ for (const file of files) {
11054
+ try {
11055
+ const raw = readFileSync5(join10(journalDir, file), "utf-8");
11056
+ entries.push(JSON.parse(raw));
11057
+ } catch {
11058
+ }
11059
+ }
11060
+ return entries;
11061
+ } catch {
11062
+ return [];
11063
+ }
11064
+ }
11065
+ function getNextTurnNumber() {
11066
+ try {
11067
+ const journalDir = WORKSPACE.JOURNAL;
11068
+ if (!existsSync8(journalDir)) return 1;
11069
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort();
11070
+ if (files.length === 0) return 1;
11071
+ const lastFile = files[files.length - 1];
11072
+ const match = lastFile.match(/turn-(\d+)\.json/);
11073
+ return match ? parseInt(match[1], 10) + 1 : 1;
11074
+ } catch {
11075
+ return 1;
11076
+ }
11077
+ }
11078
+ function shouldRegenerateSummary(currentTurn) {
11079
+ return currentTurn > 0 && currentTurn % SUMMARY_REGEN_INTERVAL === 0;
11080
+ }
11081
+ function regenerateJournalSummary() {
11082
+ try {
11083
+ const entries = getRecentEntries();
11084
+ if (entries.length === 0) return;
11085
+ const journalDir = WORKSPACE.JOURNAL;
11086
+ ensureDirExists(journalDir);
11087
+ const summary = buildSummaryFromEntries(entries);
11088
+ const summaryPath = join10(journalDir, SUMMARY_FILE);
11089
+ writeFileSync8(summaryPath, summary, "utf-8");
11090
+ debugLog("general", "Journal summary regenerated", {
11091
+ entries: entries.length,
11092
+ summaryLength: summary.length
11093
+ });
11094
+ } catch (err) {
11095
+ debugLog("general", "Failed to regenerate journal summary", { error: String(err) });
11096
+ }
11097
+ }
11098
+ function buildSummaryFromEntries(entries) {
11099
+ const attempts = [];
11100
+ const findings = [];
11101
+ const credentials = [];
11102
+ const successes = [];
11103
+ const failures = [];
11104
+ const suspicions = [];
11105
+ const nextSteps = [];
11106
+ const reflections = [];
11107
+ const VALUE_ORDER = { HIGH: 0, MED: 1, LOW: 2, NONE: 3 };
11108
+ const reversed = [...entries].reverse();
11109
+ for (const entry of reversed) {
11110
+ const value = entry.memo.attackValue || "LOW";
11111
+ for (const tool of entry.tools) {
11112
+ attempts.push({
11113
+ turn: entry.turn,
11114
+ phase: entry.phase,
11115
+ ok: tool.success,
11116
+ name: tool.name,
11117
+ input: tool.inputSummary,
11118
+ value
11119
+ });
11120
+ }
11121
+ for (const finding of entry.memo.keyFindings) {
11122
+ const line = `- [T${entry.turn}|\u26A1${value}] ${finding}`;
11123
+ if (!findings.includes(line)) findings.push(line);
11124
+ }
11125
+ for (const cred of entry.memo.credentials) {
11126
+ const line = `- [T${entry.turn}] ${cred}`;
11127
+ if (!credentials.includes(line)) credentials.push(line);
11128
+ }
11129
+ for (const vector of entry.memo.attackVectors) {
11130
+ const line = `- [T${entry.turn}] ${vector}`;
11131
+ if (!successes.includes(line)) successes.push(line);
11132
+ }
11133
+ for (const fail of entry.memo.failures) {
11134
+ const line = `- [T${entry.turn}] ${fail}`;
11135
+ if (!failures.includes(line)) failures.push(line);
11136
+ }
11137
+ for (const tool of entry.tools) {
11138
+ if (!tool.success) {
11139
+ const detail = `${tool.name}(${tool.inputSummary}): ${tool.analystSummary}`;
11140
+ const line = `- [T${entry.turn}] ${detail}`;
11141
+ if (!failures.includes(line)) failures.push(line);
11142
+ }
11143
+ }
11144
+ for (const s of entry.memo.suspicions || []) {
11145
+ const line = `- [T${entry.turn}] ${s}`;
11146
+ if (!suspicions.includes(line)) suspicions.push(line);
11147
+ }
11148
+ if (nextSteps.length < 5) {
11149
+ for (const step of entry.memo.nextSteps) {
11150
+ if (!nextSteps.includes(`- ${step}`)) nextSteps.push(`- ${step}`);
11151
+ }
11152
+ }
11153
+ if (entry.reflection) {
11154
+ reflections.push(`- [T${entry.turn}|\u26A1${value}] ${entry.reflection}`);
11155
+ }
11156
+ }
11157
+ attempts.sort((a, b) => {
11158
+ const vd = (VALUE_ORDER[a.value] ?? 3) - (VALUE_ORDER[b.value] ?? 3);
11159
+ return vd !== 0 ? vd : b.turn - a.turn;
11160
+ });
11161
+ const attemptLines = attempts.map(
11162
+ (a) => `- [T${a.turn}|${a.phase}|\u26A1${a.value}] ${a.ok ? "\u2705" : "\u274C"} ${a.name}: ${a.input}`
11163
+ );
11164
+ const lastTurn = entries[entries.length - 1]?.turn || 0;
11165
+ const sections = [
11166
+ `# Session Journal Summary`,
11167
+ `> Turn ${lastTurn} / ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19)}`,
11168
+ ""
11169
+ ];
11170
+ const addSection = (title, items) => {
11171
+ if (items.length === 0) return;
11172
+ sections.push(`## ${title}`);
11173
+ sections.push(...items);
11174
+ sections.push("");
11175
+ };
11176
+ if (attemptLines.length > 0) {
11177
+ sections.push("## Techniques Tried (by attack value)");
11178
+ sections.push("> \u26A1HIGH=keep drilling \u26A1MED=worth exploring \u26A1LOW=low priority \u26A1NONE=abandon");
11179
+ sections.push(...attemptLines);
11180
+ sections.push("");
11181
+ }
11182
+ addSection("\u{1F9E0} Analyst Analysis (attack value rationale)", reflections);
11183
+ addSection("\u{1F50D} Suspicious Signals (unconfirmed, needs investigation)", suspicions);
11184
+ addSection("\u{1F4CB} Key Findings", findings);
11185
+ addSection("\u{1F511} Credentials Obtained", credentials);
11186
+ addSection("\u2705 Successful Attack Vectors", successes);
11187
+ addSection("\u274C Failure Causes (do not repeat)", failures);
11188
+ addSection("\u27A1\uFE0F Next Recommendations", nextSteps);
11189
+ return sections.join("\n");
11190
+ }
11191
+ function rotateJournalEntries() {
11192
+ try {
11193
+ const journalDir = WORKSPACE.JOURNAL;
11194
+ if (!existsSync8(journalDir)) return;
11195
+ const files = readdirSync2(journalDir).filter((f) => f.startsWith(TURN_PREFIX) && f.endsWith(".json")).sort();
11196
+ if (files.length <= MAX_JOURNAL_ENTRIES) return;
11197
+ const toDelete = files.slice(0, files.length - MAX_JOURNAL_ENTRIES);
11198
+ for (const file of toDelete) {
11199
+ try {
11200
+ unlinkSync5(join10(journalDir, file));
11201
+ } catch {
11202
+ }
11203
+ }
11204
+ debugLog("general", "Journal entries rotated", {
11205
+ deleted: toDelete.length,
11206
+ remaining: MAX_JOURNAL_ENTRIES
11207
+ });
11208
+ } catch {
11209
+ }
11210
+ }
11211
+ function rotateOutputFiles() {
11212
+ try {
11213
+ const outputDir = WORKSPACE.OUTPUTS;
11214
+ if (!existsSync8(outputDir)) return;
11215
+ const files = readdirSync2(outputDir).filter((f) => f.endsWith(".txt")).map((f) => ({
11216
+ name: f,
11217
+ path: join10(outputDir, f),
11218
+ mtime: statSync2(join10(outputDir, f)).mtimeMs
11219
+ })).sort((a, b) => b.mtime - a.mtime);
11220
+ if (files.length <= MAX_OUTPUT_FILES) return;
11221
+ const toDelete = files.slice(MAX_OUTPUT_FILES);
11222
+ for (const file of toDelete) {
11223
+ try {
11224
+ unlinkSync5(file.path);
11225
+ } catch {
11226
+ }
11227
+ }
11228
+ debugLog("general", "Output files rotated", {
11229
+ deleted: toDelete.length,
11230
+ remaining: MAX_OUTPUT_FILES
11231
+ });
11232
+ } catch {
11233
+ }
11234
+ }
11235
+
11034
11236
  // src/agents/prompt-builder.ts
11035
11237
  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);
11238
+ var PROMPTS_DIR = join11(__dirname4, "prompts");
11239
+ var TECHNIQUES_DIR = join11(PROMPTS_DIR, PROMPT_PATHS.TECHNIQUES_DIR);
11038
11240
  var { AGENT_FILES } = PROMPT_PATHS;
11039
11241
  var PHASE_PROMPT_MAP = {
11040
11242
  // Direct mappings — phase has its own prompt file
@@ -11108,6 +11310,7 @@ var PromptBuilder = class {
11108
11310
  * 13. Learned techniques (#7: dynamic technique library)
11109
11311
  * 14. Persistent memory (#12: cross-session knowledge)
11110
11312
  * ★ 15. STRATEGIC DIRECTIVE — LLM-generated tactical instructions (D-CIPHER)
11313
+ * ★ 15b. SESSION JOURNAL — compressed history of past turns (§13 memo system)
11111
11314
  * 16. User context
11112
11315
  */
11113
11316
  async build(userInput, phase) {
@@ -11133,8 +11336,10 @@ var PromptBuilder = class {
11133
11336
  // #12
11134
11337
  this.getDynamicTechniquesFragment(),
11135
11338
  // #7
11136
- this.getPersistentMemoryFragment()
11339
+ this.getPersistentMemoryFragment(),
11137
11340
  // #12
11341
+ this.getJournalFragment()
11342
+ // §13 session journal
11138
11343
  ];
11139
11344
  const strategistDirective = await this.getStrategistFragment();
11140
11345
  if (strategistDirective) {
@@ -11158,8 +11363,8 @@ ${content}
11158
11363
  * Load a prompt file from src/agents/prompts/
11159
11364
  */
11160
11365
  loadPromptFile(filename) {
11161
- const path2 = join10(PROMPTS_DIR, filename);
11162
- return existsSync8(path2) ? readFileSync5(path2, PROMPT_CONFIG.ENCODING) : "";
11366
+ const path2 = join11(PROMPTS_DIR, filename);
11367
+ return existsSync9(path2) ? readFileSync6(path2, PROMPT_CONFIG.ENCODING) : "";
11163
11368
  }
11164
11369
  /**
11165
11370
  * Load phase-specific prompt.
@@ -11202,18 +11407,18 @@ ${content}
11202
11407
  * as general reference — NO code change needed to add new techniques.
11203
11408
  *
11204
11409
  * The map is an optimization (priority ordering), not a gate.
11205
- * "마크다운 파일 하나를 폴더에 넣으면, PromptBuilder 자동으로 발견하고 로드한다."
11410
+ * "Drop a markdown file in the folder, PromptBuilder auto-discovers and loads it."
11206
11411
  */
11207
11412
  loadPhaseRelevantTechniques(phase) {
11208
- if (!existsSync8(TECHNIQUES_DIR)) return "";
11413
+ if (!existsSync9(TECHNIQUES_DIR)) return "";
11209
11414
  const priorityTechniques = PHASE_TECHNIQUE_MAP[phase] || [];
11210
11415
  const loadedSet = /* @__PURE__ */ new Set();
11211
11416
  const fragments = [];
11212
11417
  for (const technique of priorityTechniques) {
11213
- const filePath = join10(TECHNIQUES_DIR, `${technique}.md`);
11418
+ const filePath = join11(TECHNIQUES_DIR, `${technique}.md`);
11214
11419
  try {
11215
- if (!existsSync8(filePath)) continue;
11216
- const content = readFileSync5(filePath, PROMPT_CONFIG.ENCODING);
11420
+ if (!existsSync9(filePath)) continue;
11421
+ const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
11217
11422
  if (content) {
11218
11423
  fragments.push(`<technique-reference category="${technique}">
11219
11424
  ${content}
@@ -11224,10 +11429,10 @@ ${content}
11224
11429
  }
11225
11430
  }
11226
11431
  try {
11227
- const allFiles = readdirSync2(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
11432
+ const allFiles = readdirSync3(TECHNIQUES_DIR).filter((f) => f.endsWith(".md") && f !== "README.md" && !loadedSet.has(f));
11228
11433
  for (const file of allFiles) {
11229
- const filePath = join10(TECHNIQUES_DIR, file);
11230
- const content = readFileSync5(filePath, PROMPT_CONFIG.ENCODING);
11434
+ const filePath = join11(TECHNIQUES_DIR, file);
11435
+ const content = readFileSync6(filePath, PROMPT_CONFIG.ENCODING);
11231
11436
  if (content) {
11232
11437
  const category = file.replace(".md", "");
11233
11438
  fragments.push(`<technique-reference category="${category}">
@@ -11330,6 +11535,23 @@ ${lines.join("\n")}
11330
11535
  }
11331
11536
  return this.state.persistentMemory.toPrompt(services);
11332
11537
  }
11538
+ // --- §13: Session Journal Summary ---
11539
+ /**
11540
+ * Load journal summary from .pentesting/journal/summary.md
11541
+ * Provides compressed history of past turns — what worked, what failed,
11542
+ * what was discovered. Main LLM uses this for continuity across many turns.
11543
+ */
11544
+ getJournalFragment() {
11545
+ try {
11546
+ const summary = readJournalSummary();
11547
+ if (!summary) return "";
11548
+ return `<session-journal>
11549
+ ${summary}
11550
+ </session-journal>`;
11551
+ } catch {
11552
+ return "";
11553
+ }
11554
+ }
11333
11555
  // --- D-CIPHER: Strategist Meta-Prompting ---
11334
11556
  /**
11335
11557
  * Generate strategic directive via Strategist LLM.
@@ -11342,29 +11564,11 @@ ${lines.join("\n")}
11342
11564
  };
11343
11565
 
11344
11566
  // src/agents/strategist.ts
11345
- import { readFileSync as readFileSync6, existsSync as existsSync9 } from "fs";
11346
- import { join as join11, dirname as dirname6 } from "path";
11567
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
11568
+ import { join as join12, dirname as dirname6 } from "path";
11347
11569
  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
11570
  var __dirname5 = dirname6(fileURLToPath5(import.meta.url));
11367
- var STRATEGIST_PROMPT_PATH = join11(__dirname5, "prompts", "strategist-system.md");
11571
+ var STRATEGIST_PROMPT_PATH = join12(__dirname5, "prompts", "strategist-system.md");
11368
11572
  var Strategist = class {
11369
11573
  llm;
11370
11574
  state;
@@ -11415,24 +11619,33 @@ var Strategist = class {
11415
11619
  const sections = [];
11416
11620
  sections.push("## Engagement State");
11417
11621
  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
11622
  const failures = this.state.workingMemory.toPrompt();
11425
11623
  if (failures) {
11426
11624
  sections.push("");
11427
11625
  sections.push("## Failed Attempts (DO NOT REPEAT THESE)");
11428
11626
  sections.push(failures);
11429
11627
  }
11628
+ try {
11629
+ const journalSummary = readJournalSummary();
11630
+ if (journalSummary) {
11631
+ sections.push("");
11632
+ sections.push("## Session Journal (past turns summary)");
11633
+ sections.push(journalSummary);
11634
+ }
11635
+ } catch {
11636
+ }
11430
11637
  const graph = this.state.attackGraph.toPrompt();
11431
11638
  if (graph) {
11432
11639
  sections.push("");
11433
11640
  sections.push("## Attack Graph");
11434
11641
  sections.push(graph);
11435
11642
  }
11643
+ const timeline = this.state.episodicMemory.toPrompt();
11644
+ if (timeline) {
11645
+ sections.push("");
11646
+ sections.push("## Recent Actions");
11647
+ sections.push(timeline);
11648
+ }
11436
11649
  const techniques = this.state.dynamicTechniques.toPrompt();
11437
11650
  if (techniques) {
11438
11651
  sections.push("");
@@ -11448,11 +11661,7 @@ var Strategist = class {
11448
11661
  sections.push(`## Challenge Type: ${analysis.primaryType.toUpperCase()} (${(analysis.confidence * 100).toFixed(0)}%)`);
11449
11662
  sections.push(analysis.strategySuggestion);
11450
11663
  }
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;
11664
+ return sections.join("\n");
11456
11665
  }
11457
11666
  // ─── LLM Call ───────────────────────────────────────────────
11458
11667
  async callLLM(input) {
@@ -11469,9 +11678,6 @@ ${input}`
11469
11678
  this.systemPrompt
11470
11679
  );
11471
11680
  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
11681
  const cost = response.usage ? response.usage.input_tokens + response.usage.output_tokens : 0;
11476
11682
  this.totalTokenCost += cost;
11477
11683
  return {
@@ -11501,8 +11707,8 @@ NOTE: This directive is from ${age}min ago (Strategist call failed this turn). V
11501
11707
  // ─── System Prompt Loading ──────────────────────────────────
11502
11708
  loadSystemPrompt() {
11503
11709
  try {
11504
- if (existsSync9(STRATEGIST_PROMPT_PATH)) {
11505
- return readFileSync6(STRATEGIST_PROMPT_PATH, "utf-8");
11710
+ if (existsSync10(STRATEGIST_PROMPT_PATH)) {
11711
+ return readFileSync7(STRATEGIST_PROMPT_PATH, "utf-8");
11506
11712
  }
11507
11713
  } catch {
11508
11714
  }
@@ -11544,6 +11750,8 @@ var MainAgent = class extends CoreAgent {
11544
11750
  approvalGate;
11545
11751
  scopeGuard;
11546
11752
  userInput = "";
11753
+ /** Monotonic turn counter for journal entries */
11754
+ turnCounter = 0;
11547
11755
  constructor(state, events, toolRegistry, approvalGate, scopeGuard) {
11548
11756
  super(AGENT_ROLES.ORCHESTRATOR, state, events, toolRegistry);
11549
11757
  this.approvalGate = approvalGate;
@@ -11581,8 +11789,34 @@ var MainAgent = class extends CoreAgent {
11581
11789
  * The Strategist LLM generates a fresh tactical directive every turn.
11582
11790
  */
11583
11791
  async step(iteration, messages, _unusedPrompt, progress) {
11792
+ if (this.turnCounter === 0) {
11793
+ this.turnCounter = getNextTurnNumber();
11794
+ }
11795
+ this.turnToolJournal = [];
11796
+ this.turnMemo = { keyFindings: [], credentials: [], attackVectors: [], failures: [], suspicions: [], attackValue: "LOW", nextSteps: [] };
11797
+ this.turnReflections = [];
11584
11798
  const dynamicPrompt = await this.getCurrentPrompt();
11585
11799
  const result2 = await super.step(iteration, messages, dynamicPrompt, progress);
11800
+ if (this.turnToolJournal.length > 0) {
11801
+ try {
11802
+ const entry = {
11803
+ turn: this.turnCounter,
11804
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
11805
+ phase: this.state.getPhase(),
11806
+ tools: this.turnToolJournal,
11807
+ memo: this.turnMemo,
11808
+ reflection: this.turnReflections.length > 0 ? this.turnReflections.join(" | ") : this.turnMemo.nextSteps.join("; ")
11809
+ };
11810
+ writeJournalEntry(entry);
11811
+ if (shouldRegenerateSummary(this.turnCounter)) {
11812
+ regenerateJournalSummary();
11813
+ }
11814
+ rotateJournalEntries();
11815
+ rotateOutputFiles();
11816
+ } catch {
11817
+ }
11818
+ this.turnCounter++;
11819
+ }
11586
11820
  this.emitStateChange();
11587
11821
  return result2;
11588
11822
  }