@pensar/apex 0.0.100 → 0.0.101-canary.5bc4cfc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,14 +18,6 @@
18
18
 
19
19
  ## Installation
20
20
 
21
- ### Prerequisites
22
-
23
- - **API Key** for your chosen AI provider
24
-
25
- After installing, run `pensar doctor` to check for optional dependencies (like nmap) and install them.
26
-
27
- ### Install Apex
28
-
29
21
  #### macOS / Linux (Quick Install)
30
22
 
31
23
  ```bash
@@ -42,7 +34,7 @@ brew install apex
42
34
  #### Windows (PowerShell)
43
35
 
44
36
  ```powershell
45
- irm https://pensarai.com/apex.ps1 | iex
37
+ irm https://www.pensarai.com/apex.ps1 | iex
46
38
  ```
47
39
 
48
40
  #### npm
@@ -51,27 +43,6 @@ irm https://pensarai.com/apex.ps1 | iex
51
43
  npm install -g @pensar/apex
52
44
  ```
53
45
 
54
- ### Configuration
55
-
56
- Set your AI provider API key as an environment variable:
57
-
58
- ```bash
59
- export ANTHROPIC_API_KEY="your-api-key-here"
60
- # or for other providers:
61
- export OPENAI_API_KEY="your-api-key-here"
62
- export OPENROUTER_API_KEY="your-api-key-here"
63
-
64
- # AWS Bedrock (bearer token auth):
65
- export BEDROCK_API_KEY="your-bearer-token"
66
- export AWS_REGION="us-east-1"
67
-
68
- # AWS Bedrock (IAM credentials):
69
- export AWS_ACCESS_KEY_ID="..."
70
- export AWS_SECRET_ACCESS_KEY="..."
71
- export AWS_SESSION_TOKEN="..." # optional, for temporary credentials
72
- export AWS_REGION="us-east-1"
73
- ```
74
-
75
46
  ## Usage
76
47
 
77
48
  Run Apex:
package/bin/pensar.js CHANGED
@@ -66,6 +66,13 @@ if (command === "benchmark") {
66
66
 
67
67
  // Import and run auth
68
68
  await import(authPath);
69
+ } else if (command === "uninstall") {
70
+ // Run uninstall CLI
71
+ const uninstallPath = join(__dirname, "..", "build", "uninstall.js");
72
+
73
+ process.argv = [process.argv[0], uninstallPath, ...args.slice(1)];
74
+
75
+ await import(uninstallPath);
69
76
  } else if (command === "upgrade" || command === "update") {
70
77
  const currentVersion = getCurrentVersion();
71
78
  console.log(`Current version: v${currentVersion}`);
@@ -89,6 +96,7 @@ if (command === "benchmark") {
89
96
  console.log();
90
97
  console.log("Usage:");
91
98
  console.log(" pensar Launch the TUI (Terminal User Interface)");
99
+ console.log(" pensar uninstall Uninstall Pensar (keeps sessions, memories, skills)");
92
100
  console.log(" pensar upgrade Update pensar to the latest version");
93
101
  console.log(" pensar help Show this help message");
94
102
  console.log(" pensar version Show version number");
@@ -181,6 +189,14 @@ if (command === "benchmark") {
181
189
  console.log(" pensar auth logout Disconnect from Pensar Console");
182
190
  console.log(" pensar auth status Show connection status");
183
191
  console.log();
192
+ console.log("Uninstall Usage:");
193
+ console.log(
194
+ " pensar uninstall Fully uninstall Pensar"
195
+ );
196
+ console.log(
197
+ " pensar uninstall --force Skip confirmation prompt"
198
+ );
199
+ console.log();
184
200
  console.log("Header Modes (for quicktest, pentest, swarm):");
185
201
  console.log(" none No custom headers added to requests");
186
202
  console.log(
package/build/auth.js CHANGED
@@ -8,7 +8,7 @@ import fs from "fs/promises";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@pensar/apex",
11
- version: "0.0.100",
11
+ version: "0.0.101-canary.5bc4cfc4",
12
12
  description: "AI-powered penetration testing CLI tool with terminal UI",
13
13
  module: "src/tui/index.tsx",
14
14
  main: "build/index.js",
package/build/index.js CHANGED
@@ -31880,12 +31880,6 @@ var init_openrouter = __esm(() => {
31880
31880
  var PENSAR_MODELS;
31881
31881
  var init_pensar = __esm(() => {
31882
31882
  PENSAR_MODELS = [
31883
- {
31884
- id: "pensar:anthropic.claude-opus-4-6-v1",
31885
- name: "Claude Opus 4.6 (Pensar)",
31886
- provider: "pensar",
31887
- contextLength: 200000
31888
- },
31889
31883
  {
31890
31884
  id: "pensar:anthropic.claude-sonnet-4-5-20250929-v1:0",
31891
31885
  name: "Claude Sonnet 4.5 (Pensar)",
@@ -31977,7 +31971,7 @@ var package_default2;
31977
31971
  var init_package = __esm(() => {
31978
31972
  package_default2 = {
31979
31973
  name: "@pensar/apex",
31980
- version: "0.0.100",
31974
+ version: "0.0.101-canary.5bc4cfc4",
31981
31975
  description: "AI-powered penetration testing CLI tool with terminal UI",
31982
31976
  module: "src/tui/index.tsx",
31983
31977
  main: "build/index.js",
@@ -89913,6 +89907,7 @@ function convertMessagesToUI(messages, baseTime) {
89913
89907
  const input = part.input;
89914
89908
  const toolDescription = typeof input?.toolCallDescription === "string" ? input.toolCallDescription : part.toolName || "tool";
89915
89909
  const result = part.toolCallId ? toolResults.get(part.toolCallId) : undefined;
89910
+ const cancelled = typeof result === "string" && result.toLowerCase().includes("cancelled");
89916
89911
  uiMessages.push({
89917
89912
  role: "tool",
89918
89913
  content: toolDescription,
@@ -89921,7 +89916,7 @@ function convertMessagesToUI(messages, baseTime) {
89921
89916
  toolName: part.toolName,
89922
89917
  args: input,
89923
89918
  result,
89924
- status: "completed"
89919
+ status: cancelled ? "error" : "completed"
89925
89920
  });
89926
89921
  }
89927
89922
  }
@@ -107298,9 +107293,9 @@ var init_executeCommand = __esm(() => {
107298
107293
  init_dist5();
107299
107294
  init_zod();
107300
107295
  executeCommandInputSchema = exports_external.object({
107296
+ toolCallDescription: exports_external.string().describe("A concise, human-readable description of what this tool call is doing (e.g., 'Scanning for open ports on target')"),
107301
107297
  command: exports_external.string().describe("The shell command to execute"),
107302
- timeout: exports_external.number().optional().describe("Timeout in seconds. If omitted, the command runs until completion or abort."),
107303
- toolCallDescription: exports_external.string().describe("A concise, human-readable description of what this tool call is doing (e.g., 'Scanning for open ports on target')")
107298
+ timeout: exports_external.number().optional().describe("Timeout in seconds. If omitted, the command runs until completion or abort.")
107304
107299
  });
107305
107300
  });
107306
107301
 
@@ -193797,6 +193792,19 @@ COMMON SEARCH PATTERNS:
193797
193792
  execute: async ({ query }) => {
193798
193793
  try {
193799
193794
  const cfg = await config2.get();
193795
+ const apiUrl = getPensarApiUrl();
193796
+ const body = JSON.stringify({ query });
193797
+ if (cfg.pensarAPIKey && !cfg.accessToken) {
193798
+ const response2 = await fetch(`${apiUrl}/agents/web_search`, {
193799
+ method: "POST",
193800
+ headers: {
193801
+ "Content-Type": "application/json",
193802
+ "x-api-key": cfg.pensarAPIKey
193803
+ },
193804
+ body
193805
+ });
193806
+ return handleSearchResponse(response2);
193807
+ }
193800
193808
  const tokenResult = await ensureValidToken({
193801
193809
  accessToken: cfg.accessToken,
193802
193810
  refreshToken: cfg.refreshToken,
@@ -193823,8 +193831,6 @@ COMMON SEARCH PATTERNS:
193823
193831
  error: "Web search requires authentication. Please sign in again to your Pensar account."
193824
193832
  };
193825
193833
  }
193826
- const apiUrl = getPensarApiUrl();
193827
- const body = JSON.stringify({ query });
193828
193834
  const { signature, timestamp, nonce } = signGatewayRequest(cfg.gatewaySigningKey, "web_search", body);
193829
193835
  const response = await fetch(`${apiUrl}/agents/web_search`, {
193830
193836
  method: "POST",
@@ -193838,40 +193844,7 @@ COMMON SEARCH PATTERNS:
193838
193844
  },
193839
193845
  body
193840
193846
  });
193841
- if (!response.ok) {
193842
- if (response.status === 401) {
193843
- return {
193844
- success: false,
193845
- results: [],
193846
- error: "Authentication failed. Please sign in again to your Pensar account."
193847
- };
193848
- }
193849
- if (response.status === 429) {
193850
- return {
193851
- success: false,
193852
- results: [],
193853
- error: "Rate limit exceeded. Please wait a moment before searching again."
193854
- };
193855
- }
193856
- const errorText = await response.text().catch(() => "Unknown error");
193857
- return {
193858
- success: false,
193859
- results: [],
193860
- error: `Web search failed: ${response.status} ${response.statusText}. ${errorText}`
193861
- };
193862
- }
193863
- const data = await response.json();
193864
- if (data.error) {
193865
- return {
193866
- success: false,
193867
- results: [],
193868
- error: data.error
193869
- };
193870
- }
193871
- return {
193872
- success: true,
193873
- results: data.results || []
193874
- };
193847
+ return handleSearchResponse(response);
193875
193848
  } catch (error40) {
193876
193849
  const errorMsg = error40 instanceof Error ? error40.message : String(error40);
193877
193850
  return {
@@ -193883,6 +193856,42 @@ COMMON SEARCH PATTERNS:
193883
193856
  }
193884
193857
  });
193885
193858
  }
193859
+ async function handleSearchResponse(response) {
193860
+ if (!response.ok) {
193861
+ if (response.status === 401) {
193862
+ return {
193863
+ success: false,
193864
+ results: [],
193865
+ error: "Authentication failed. Please sign in again to your Pensar account."
193866
+ };
193867
+ }
193868
+ if (response.status === 429) {
193869
+ return {
193870
+ success: false,
193871
+ results: [],
193872
+ error: "Rate limit exceeded. Please wait a moment before searching again."
193873
+ };
193874
+ }
193875
+ const errorText = await response.text().catch(() => "Unknown error");
193876
+ return {
193877
+ success: false,
193878
+ results: [],
193879
+ error: `Web search failed: ${response.status} ${response.statusText}. ${errorText}`
193880
+ };
193881
+ }
193882
+ const data = await response.json();
193883
+ if (data.error) {
193884
+ return {
193885
+ success: false,
193886
+ results: [],
193887
+ error: data.error
193888
+ };
193889
+ }
193890
+ return {
193891
+ success: true,
193892
+ results: data.results || []
193893
+ };
193894
+ }
193886
193895
  var webSearchInputSchema2;
193887
193896
  var init_webSearch = __esm(() => {
193888
193897
  init_dist5();
@@ -194800,7 +194809,8 @@ var init_operator = __esm(() => {
194800
194809
 
194801
194810
  // src/core/agents/offSecAgent/offensiveSecurityAgent.ts
194802
194811
  import { join as join22 } from "path";
194803
- import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync10, existsSync as existsSync21 } from "fs";
194812
+ import { mkdirSync as mkdirSync10, existsSync as existsSync21 } from "fs";
194813
+ import { writeFile as writeFile3 } from "fs/promises";
194804
194814
  function wrapToolsWithApprovalGate(tools, gate) {
194805
194815
  const wrapped = {};
194806
194816
  for (const [name26, coreTool] of Object.entries(tools)) {
@@ -194954,13 +194964,11 @@ var init_offensiveSecurityAgent = __esm(() => {
194954
194964
  stopWhen,
194955
194965
  toolChoice: "auto",
194956
194966
  onStepFinish: (event) => {
194957
- try {
194958
- const allMessages = [
194959
- ...initialMessagesRef.current,
194960
- ...event.response.messages
194961
- ];
194962
- writeFileSync15(messagesPath, JSON.stringify(allMessages, null, 2));
194963
- } catch {}
194967
+ const allMessages = [
194968
+ ...initialMessagesRef.current,
194969
+ ...event.response.messages
194970
+ ];
194971
+ writeFile3(messagesPath, JSON.stringify(allMessages, null, 2)).catch(() => {});
194964
194972
  input.onStepFinish?.(event);
194965
194973
  },
194966
194974
  onSummarized: () => {
@@ -195484,7 +195492,7 @@ var init_agent4 = __esm(() => {
195484
195492
  });
195485
195493
 
195486
195494
  // src/core/session/execution-metrics.ts
195487
- import { existsSync as existsSync24, readFileSync as readFileSync11, writeFileSync as writeFileSync16 } from "fs";
195495
+ import { existsSync as existsSync24, readFileSync as readFileSync11, writeFileSync as writeFileSync15 } from "fs";
195488
195496
  import { join as join25 } from "path";
195489
195497
  function toNonNegativeInteger(value) {
195490
195498
  const n = Number(value);
@@ -195530,7 +195538,7 @@ function writeSessionJsonTokenTotals(sessionRootPath, tokenUsage) {
195530
195538
  const parsed = JSON.parse(readFileSync11(path6, "utf-8"));
195531
195539
  parsed.tokensIn = tokenUsage.inputTokens;
195532
195540
  parsed.tokensOut = tokenUsage.outputTokens;
195533
- writeFileSync16(path6, JSON.stringify(parsed, null, 2));
195541
+ writeFileSync15(path6, JSON.stringify(parsed, null, 2));
195534
195542
  } catch {}
195535
195543
  }
195536
195544
  function writeExecutionMetrics(input) {
@@ -195545,7 +195553,7 @@ function writeExecutionMetrics(input) {
195545
195553
  runtime: input.runtime ?? existing?.runtime,
195546
195554
  updatedAt: new Date().toISOString()
195547
195555
  };
195548
- writeFileSync16(metricsPath(input.sessionRootPath), JSON.stringify(next, null, 2), "utf-8");
195556
+ writeFileSync15(metricsPath(input.sessionRootPath), JSON.stringify(next, null, 2), "utf-8");
195549
195557
  writeSessionJsonTokenTotals(input.sessionRootPath, next.tokenUsage);
195550
195558
  return next;
195551
195559
  }
@@ -196140,7 +196148,7 @@ __export(exports_pentest, {
196140
196148
  runPentestSwarm: () => runPentestSwarm,
196141
196149
  DEFAULT_CONCURRENCY: () => DEFAULT_CONCURRENCY4
196142
196150
  });
196143
- import { existsSync as existsSync25, readdirSync as readdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync17 } from "fs";
196151
+ import { existsSync as existsSync25, readdirSync as readdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync16 } from "fs";
196144
196152
  import { join as join26 } from "path";
196145
196153
  function addUsageTotals(totals, usage) {
196146
196154
  if (!usage)
@@ -196310,8 +196318,8 @@ async function runPentestWorkflow(input) {
196310
196318
  });
196311
196319
  const mdPath2 = join26(session.rootPath, REPORT_FILENAME_MD);
196312
196320
  const jsonPath2 = join26(session.rootPath, REPORT_FILENAME_JSON);
196313
- writeFileSync17(mdPath2, renderMarkdown(report2));
196314
- writeFileSync17(jsonPath2, renderJson(report2));
196321
+ writeFileSync16(mdPath2, renderMarkdown(report2));
196322
+ writeFileSync16(jsonPath2, renderJson(report2));
196315
196323
  return {
196316
196324
  findings: [],
196317
196325
  findingsPath: session.findingsPath,
@@ -196350,8 +196358,8 @@ async function runPentestWorkflow(input) {
196350
196358
  });
196351
196359
  const mdPath = join26(session.rootPath, REPORT_FILENAME_MD);
196352
196360
  const jsonPath = join26(session.rootPath, REPORT_FILENAME_JSON);
196353
- writeFileSync17(mdPath, renderMarkdown(report));
196354
- writeFileSync17(jsonPath, renderJson(report));
196361
+ writeFileSync16(mdPath, renderMarkdown(report));
196362
+ writeFileSync16(jsonPath, renderJson(report));
196355
196363
  return {
196356
196364
  findings,
196357
196365
  findingsPath: session.findingsPath,
@@ -274503,7 +274511,27 @@ function CommandProvider({
274503
274511
  description: skill.description || "Skill"
274504
274512
  });
274505
274513
  }
274506
- return options;
274514
+ const priorityOrder = [
274515
+ "/pentest",
274516
+ "/operator",
274517
+ "/auth",
274518
+ "/models",
274519
+ "/sessions",
274520
+ "/themes",
274521
+ "/help"
274522
+ ];
274523
+ return options.sort((a, b2) => {
274524
+ const aIndex = priorityOrder.indexOf(a.value);
274525
+ const bIndex = priorityOrder.indexOf(b2.value);
274526
+ if (aIndex !== -1 && bIndex !== -1) {
274527
+ return aIndex - bIndex;
274528
+ }
274529
+ if (aIndex !== -1)
274530
+ return -1;
274531
+ if (bIndex !== -1)
274532
+ return 1;
274533
+ return a.value.localeCompare(b2.value);
274534
+ });
274507
274535
  }, [router, skills]);
274508
274536
  const executeCommand = import_react17.useCallback(async (input) => {
274509
274537
  return await router.execute(input, ctx3);
@@ -275720,6 +275748,40 @@ function computeTab(suggestions, selectedIndex) {
275720
275748
  function shouldResetHistory(historyIndex, isNavigatingHistory) {
275721
275749
  return historyIndex !== -1 && !isNavigatingHistory;
275722
275750
  }
275751
+ function computeVisibleWindow(suggestions, selectedIndex, maxVisible) {
275752
+ if (suggestions.length === 0) {
275753
+ return {
275754
+ start: 0,
275755
+ end: 0,
275756
+ visibleSuggestions: [],
275757
+ hasMore: false,
275758
+ hasMoreBelow: false
275759
+ };
275760
+ }
275761
+ if (suggestions.length <= maxVisible) {
275762
+ return {
275763
+ start: 0,
275764
+ end: suggestions.length,
275765
+ visibleSuggestions: suggestions,
275766
+ hasMore: false,
275767
+ hasMoreBelow: false
275768
+ };
275769
+ }
275770
+ const safeSelectedIndex = Math.max(0, selectedIndex);
275771
+ let start = Math.max(0, safeSelectedIndex - Math.floor(maxVisible / 2));
275772
+ let end = start + maxVisible;
275773
+ if (end > suggestions.length) {
275774
+ end = suggestions.length;
275775
+ start = Math.max(0, end - maxVisible);
275776
+ }
275777
+ return {
275778
+ start,
275779
+ end,
275780
+ visibleSuggestions: suggestions.slice(start, end),
275781
+ hasMore: start > 0,
275782
+ hasMoreBelow: end < suggestions.length
275783
+ };
275784
+ }
275723
275785
 
275724
275786
  // src/tui/components/shared/use-paste-extmarks.ts
275725
275787
  var import_react30 = __toESM(require_react(), 1);
@@ -275813,6 +275875,7 @@ var PromptInput = import_react31.forwardRef(function PromptInput2({
275813
275875
  enableAutocomplete = false,
275814
275876
  autocompleteOptions = [],
275815
275877
  maxSuggestions = 10,
275878
+ maxVisibleSuggestions = 6,
275816
275879
  enableCommands = false,
275817
275880
  onCommandExecute,
275818
275881
  commandHistory = [],
@@ -275978,34 +276041,72 @@ var PromptInput = import_react31.forwardRef(function PromptInput2({
275978
276041
  setHistoryIndex(-1);
275979
276042
  }
275980
276043
  };
276044
+ const windowedView = import_react31.useMemo(() => computeVisibleWindow(suggestions, selectedSuggestionIndex, maxVisibleSuggestions), [suggestions, selectedSuggestionIndex, maxVisibleSuggestions]);
275981
276045
  const suggestionsBox = suggestions.length > 0 && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
275982
276046
  flexDirection: "column",
275983
276047
  ...autocompletePlacement === "above" ? { marginBottom: 1 } : { marginTop: 1 },
275984
- children: suggestions.map((suggestion, index) => {
275985
- const isSelected = index === selectedSuggestionIndex;
275986
- return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
276048
+ children: [
276049
+ windowedView.hasMore && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
275987
276050
  flexDirection: "row",
275988
276051
  gap: 1,
275989
276052
  children: [
275990
276053
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
275991
- fg: isSelected ? colors2.primary : colors2.textMuted,
275992
- children: isSelected ? " ▸" : " "
276054
+ fg: colors2.textMuted,
276055
+ children: " "
275993
276056
  }, undefined, false, undefined, this),
275994
276057
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
275995
- fg: isSelected ? colors2.text : colors2.textMuted,
275996
- children: suggestion.label
276058
+ fg: colors2.textMuted,
276059
+ children: [
276060
+ windowedView.start,
276061
+ " more above..."
276062
+ ]
276063
+ }, undefined, true, undefined, this)
276064
+ ]
276065
+ }, undefined, true, undefined, this),
276066
+ windowedView.visibleSuggestions.map((suggestion, windowIndex) => {
276067
+ const actualIndex = windowedView.start + windowIndex;
276068
+ const isSelected = actualIndex === selectedSuggestionIndex;
276069
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
276070
+ flexDirection: "row",
276071
+ gap: 1,
276072
+ children: [
276073
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276074
+ fg: isSelected ? colors2.primary : colors2.textMuted,
276075
+ children: isSelected ? " ▸" : " "
276076
+ }, undefined, false, undefined, this),
276077
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276078
+ fg: isSelected ? colors2.text : colors2.textMuted,
276079
+ children: suggestion.label
276080
+ }, undefined, false, undefined, this),
276081
+ suggestion.description && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276082
+ fg: colors2.textMuted,
276083
+ children: [
276084
+ " ",
276085
+ suggestion.description
276086
+ ]
276087
+ }, undefined, true, undefined, this)
276088
+ ]
276089
+ }, suggestion.value, true, undefined, this);
276090
+ }),
276091
+ windowedView.hasMoreBelow && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
276092
+ flexDirection: "row",
276093
+ gap: 1,
276094
+ children: [
276095
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276096
+ fg: colors2.textMuted,
276097
+ children: " ↓"
275997
276098
  }, undefined, false, undefined, this),
275998
- suggestion.description && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276099
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
275999
276100
  fg: colors2.textMuted,
276000
276101
  children: [
276001
- " ",
276002
- suggestion.description
276102
+ suggestions.length - windowedView.end,
276103
+ " more below..."
276003
276104
  ]
276004
276105
  }, undefined, true, undefined, this)
276005
276106
  ]
276006
- }, suggestion.value, true, undefined, this);
276007
- })
276008
- }, undefined, false, undefined, this);
276107
+ }, undefined, true, undefined, this)
276108
+ ]
276109
+ }, undefined, true, undefined, this);
276009
276110
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
276010
276111
  flexDirection: "column",
276011
276112
  children: [
@@ -279749,7 +279850,7 @@ function ResponsibleUseDisclosure({
279749
279850
  }, undefined, false, undefined, this),
279750
279851
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
279751
279852
  fg: colors2.text,
279752
- children: "This penetration testing tool is designedfor AUTHORIZED security testing only."
279853
+ children: "This penetration testing tool is designed for AUTHORIZED security testing only."
279753
279854
  }, undefined, false, undefined, this),
279754
279855
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
279755
279856
  flexDirection: "column",
@@ -279792,7 +279893,7 @@ function ResponsibleUseDisclosure({
279792
279893
  flexDirection: "column",
279793
279894
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
279794
279895
  fg: colors2.error,
279795
- children: "Unauthorized access to computer systems is illegaland may result in criminal prosecution."
279896
+ children: "Unauthorized access to computer systems is illegal and may result in criminal prosecution."
279796
279897
  }, undefined, false, undefined, this)
279797
279898
  }, undefined, false, undefined, this),
279798
279899
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -280099,18 +280200,23 @@ function HelpDialog() {
280099
280200
  });
280100
280201
  };
280101
280202
  useKeyboard((evt) => {
280203
+ if (evt.name === "escape") {
280204
+ evt.preventDefault();
280205
+ if (showDetail) {
280206
+ setShowDetail(false);
280207
+ } else {
280208
+ handleClose();
280209
+ }
280210
+ return;
280211
+ }
280102
280212
  if (showDetail) {
280103
- if (evt.name === "escape" || evt.name === "return") {
280213
+ if (evt.name === "return") {
280104
280214
  evt.preventDefault();
280105
280215
  setShowDetail(false);
280106
280216
  }
280107
280217
  return;
280108
280218
  }
280109
280219
  switch (evt.name) {
280110
- case "escape":
280111
- evt.preventDefault();
280112
- handleClose();
280113
- break;
280114
280220
  case "up":
280115
280221
  case "k":
280116
280222
  evt.preventDefault();
@@ -281165,9 +281271,10 @@ function createKeybindings(deps) {
281165
281271
  return;
281166
281272
  }
281167
281273
  const isHome = route.data.type === "base" && route.data.path === "home";
281274
+ const isHelp = route.data.type === "base" && route.data.path === "help";
281168
281275
  const isOperator = route.data.type === "base" && route.data.path === "operator";
281169
281276
  const isSession = route.data.type === "pentest" || route.data.type === "operator";
281170
- if (!isHome && !isOperator && !isSession) {
281277
+ if (!isHome && !isHelp && !isOperator && !isSession) {
281171
281278
  route.navigate({
281172
281279
  type: "base",
281173
281280
  path: "home"
@@ -282911,6 +283018,15 @@ function getToolSummary(toolName, args) {
282911
283018
  const firstArg = Object.entries(args).filter(([k3]) => k3 !== "toolCallDescription").map(([, v3]) => typeof v3 === "string" ? v3 : JSON.stringify(v3)).find((v3) => v3 && v3.length > 0);
282912
283019
  return firstArg ? `${toolName} ${String(firstArg).slice(0, 50)}` : toolName;
282913
283020
  }
283021
+ function getToolDisplayLabel(toolName, args, options = {}) {
283022
+ if (options.preferDescription && toolName === "execute_command") {
283023
+ const description = args.toolCallDescription;
283024
+ if (typeof description === "string" && description.trim().length > 0) {
283025
+ return description.trim();
283026
+ }
283027
+ }
283028
+ return getToolSummary(toolName, args);
283029
+ }
282914
283030
  function getArgsPreview(toolName, args, maxLength = 60) {
282915
283031
  const filteredArgs = Object.entries(args).filter(([k3]) => !k3.toLowerCase().includes("description"));
282916
283032
  if (filteredArgs.length === 0)
@@ -283744,7 +283860,9 @@ var ToolRenderer = import_react68.memo(function ToolRenderer2({
283744
283860
  const isCompleted = message.status === "completed";
283745
283861
  const isError = message.status === "error";
283746
283862
  const { toolName, args, result, logs, subagentLogs } = message;
283747
- const summary = getToolSummary(toolName, args);
283863
+ const summary = getToolDisplayLabel(toolName, args, {
283864
+ preferDescription: isPending
283865
+ });
283748
283866
  const resultDisplay = isCompleted || isError ? getResultSummary(result, toolName, args) : null;
283749
283867
  const borderColor = isError ? colors2.error : isPending ? colors2.warning : colors2.info;
283750
283868
  const hasSubagentLogs = subagentLogs && Object.keys(subagentLogs).length > 0;
@@ -286132,7 +286250,7 @@ function MessageList({
286132
286250
  }, undefined, false, undefined, this),
286133
286251
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
286134
286252
  fg: colors2.textMuted,
286135
- children: " - Cycle approval on/off"
286253
+ children: " - Switch between Plan or Default mode"
286136
286254
  }, undefined, false, undefined, this)
286137
286255
  ]
286138
286256
  }, undefined, true, undefined, this)
@@ -286735,7 +286853,7 @@ function navigateDown(selectedIndex, queueLength) {
286735
286853
  }
286736
286854
 
286737
286855
  // src/tui/components/operator-dashboard/index.tsx
286738
- import { existsSync as existsSync27, readFileSync as readFileSync14 } from "fs";
286856
+ import { existsSync as existsSync27, readFileSync as readFileSync14, writeFileSync as writeFileSync17 } from "fs";
286739
286857
  import { join as join28 } from "path";
286740
286858
  function OperatorDashboard({
286741
286859
  sessionId,
@@ -286775,6 +286893,8 @@ function OperatorDashboard({
286775
286893
  return filterOperatorAutocomplete(allAutocompleteOptions, skillSlugs);
286776
286894
  }, [allAutocompleteOptions, skills]);
286777
286895
  const [session, setSession] = import_react79.useState(null);
286896
+ const sessionRef = import_react79.useRef(null);
286897
+ sessionRef.current = session;
286778
286898
  const [loading, setLoading] = import_react79.useState(true);
286779
286899
  const [error40, setError] = import_react79.useState(null);
286780
286900
  const pendingNameRef = import_react79.useRef(null);
@@ -286786,6 +286906,8 @@ function OperatorDashboard({
286786
286906
  });
286787
286907
  const commandCancelledRef = import_react79.useRef(false);
286788
286908
  const [messages, setMessages] = import_react79.useState([]);
286909
+ const displayMessagesRef = import_react79.useRef([]);
286910
+ displayMessagesRef.current = messages;
286789
286911
  const textRef = import_react79.useRef("");
286790
286912
  const conversationRef = import_react79.useRef([]);
286791
286913
  const [inputValue, setInputValue] = import_react79.useState("");
@@ -287169,6 +287291,12 @@ function OperatorDashboard({
287169
287291
  { role: "user", content: prompt }
287170
287292
  ];
287171
287293
  conversationRef.current = nextMessages;
287294
+ if (sessionRef.current) {
287295
+ try {
287296
+ const mp = join28(sessionRef.current.rootPath, "messages.json");
287297
+ writeFileSync17(mp, JSON.stringify(nextMessages, null, 2));
287298
+ } catch {}
287299
+ }
287172
287300
  const onStepFinish = (event) => {
287173
287301
  const nextUsage = accumulateTokenUsage(tokenUsageRef.current, event.usage?.inputTokens ?? 0, event.usage?.outputTokens ?? 0);
287174
287302
  if (!nextUsage)
@@ -287186,22 +287314,32 @@ function OperatorDashboard({
287186
287314
  };
287187
287315
  const callbacks = {
287188
287316
  onTextDelta: (d3) => {
287317
+ if (gen !== generationRef.current)
287318
+ return;
287189
287319
  setThinking(false);
287190
287320
  appendText(d3.text);
287191
287321
  },
287192
287322
  onToolCallStreaming: (d3) => {
287323
+ if (gen !== generationRef.current)
287324
+ return;
287193
287325
  setThinking(false);
287194
287326
  addStreamingToolCall(d3.toolCallId, d3.toolName);
287195
287327
  },
287196
287328
  onToolCallDelta: (d3) => {
287329
+ if (gen !== generationRef.current)
287330
+ return;
287197
287331
  appendToolCallDelta(d3.toolCallId, d3.argsTextDelta);
287198
287332
  },
287199
287333
  onToolCall: (d3) => {
287334
+ if (gen !== generationRef.current)
287335
+ return;
287200
287336
  setThinking(false);
287201
287337
  commandCancelledRef.current = false;
287202
287338
  addToolCall(d3.toolCallId, d3.toolName, d3.input);
287203
287339
  },
287204
287340
  onToolResult: (d3) => {
287341
+ if (gen !== generationRef.current)
287342
+ return;
287205
287343
  flushCommandOutput();
287206
287344
  if (cmdFlushTimerRef.current) {
287207
287345
  clearInterval(cmdFlushTimerRef.current);
@@ -287261,6 +287399,8 @@ function OperatorDashboard({
287261
287399
  callbacks,
287262
287400
  onSessionReady: (s2) => {
287263
287401
  setSessionCwd(s2.rootPath);
287402
+ sessionRef.current = s2;
287403
+ setSession((prev) => prev ?? s2);
287264
287404
  }
287265
287405
  };
287266
287406
  try {
@@ -287506,19 +287646,61 @@ function OperatorDashboard({
287506
287646
  setQueuedMessages([]);
287507
287647
  queuedMessagesRef.current = [];
287508
287648
  setSelectedQueueIndex(-1);
287649
+ approvalGateRef.current.denyAll();
287509
287650
  setStatus("idle");
287510
287651
  setThinking(false);
287511
287652
  setIsExecuting(false);
287512
- approvalGateRef.current.denyAll();
287513
- if (session) {
287653
+ const activeSession = sessionRef.current;
287654
+ if (activeSession) {
287514
287655
  try {
287515
- const messagesPath = join28(session.rootPath, "messages.json");
287656
+ const messagesPath = join28(activeSession.rootPath, "messages.json");
287516
287657
  if (existsSync27(messagesPath)) {
287517
287658
  const raw = JSON.parse(readFileSync14(messagesPath, "utf-8"));
287518
287659
  if (Array.isArray(raw) && raw.length > 0) {
287519
287660
  conversationRef.current = sessions.getResumeMessages(raw);
287520
287661
  }
287521
287662
  }
287663
+ const last = conversationRef.current[conversationRef.current.length - 1];
287664
+ if (last?.role === "user") {
287665
+ const partial2 = textRef.current.trim();
287666
+ const pendingTools = displayMessagesRef.current.filter((m4) => isToolMessage(m4) && (m4.status === "pending" || m4.status === "streaming"));
287667
+ const assistantContent = [
287668
+ {
287669
+ type: "text",
287670
+ text: partial2 || "[Response interrupted by user.]"
287671
+ }
287672
+ ];
287673
+ for (const t3 of pendingTools) {
287674
+ assistantContent.push({
287675
+ type: "tool-call",
287676
+ toolCallId: t3.toolCallId,
287677
+ toolName: t3.toolName,
287678
+ input: t3.args ?? {}
287679
+ });
287680
+ }
287681
+ conversationRef.current = [
287682
+ ...conversationRef.current,
287683
+ {
287684
+ role: "assistant",
287685
+ content: assistantContent
287686
+ }
287687
+ ];
287688
+ if (pendingTools.length > 0) {
287689
+ conversationRef.current = [
287690
+ ...conversationRef.current,
287691
+ {
287692
+ role: "tool",
287693
+ content: pendingTools.map((t3) => ({
287694
+ type: "tool-result",
287695
+ toolCallId: t3.toolCallId,
287696
+ toolName: t3.toolName ?? "unknown",
287697
+ output: "Cancelled by user."
287698
+ }))
287699
+ }
287700
+ ];
287701
+ }
287702
+ writeFileSync17(join28(activeSession.rootPath, "messages.json"), JSON.stringify(conversationRef.current, null, 2));
287703
+ }
287522
287704
  } catch {}
287523
287705
  }
287524
287706
  setMessages((prev) => {
@@ -287532,7 +287714,7 @@ function OperatorDashboard({
287532
287714
  }
287533
287715
  ];
287534
287716
  });
287535
- }, [session, setThinking, setIsExecuting]);
287717
+ }, [setThinking, setIsExecuting]);
287536
287718
  const toggleApproval = import_react79.useCallback(() => {
287537
287719
  setOperatorState((prev) => {
287538
287720
  const newVal = !prev.requireApproval;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pensar/apex",
3
- "version": "0.0.100",
3
+ "version": "0.0.101-canary.5bc4cfc4",
4
4
  "description": "AI-powered penetration testing CLI tool with terminal UI",
5
5
  "module": "src/tui/index.tsx",
6
6
  "main": "build/index.js",
@@ -66,6 +66,18 @@ export function detectInstallMethod(): InstallMethod {
66
66
  return "npm";
67
67
  }
68
68
 
69
+ // Determine if we're running as a compiled binary vs via an interpreter.
70
+ // Compiled Bun binaries have process.execPath pointing to the binary itself
71
+ // (e.g. ~/.local/bin/pensar), while interpreted scripts have it pointing to
72
+ // the runtime (e.g. ~/.bun/bin/bun or /usr/local/bin/node).
73
+ const execName = execPath.split("/").pop()?.replace(/\.exe$/, "") ?? "";
74
+ const isInterpreter =
75
+ execName === "bun" || execName === "node" || execName === "bun-debug";
76
+
77
+ if (!isInterpreter) {
78
+ return "binary";
79
+ }
80
+
69
81
  const npmCheck = spawnSync(
70
82
  "npm",
71
83
  ["list", "-g", "@pensar/apex", "--depth=0"],
@@ -177,7 +177,7 @@ describe("detectInstallMethod", () => {
177
177
  expect(detectInstallMethod()).toBe("npm");
178
178
  });
179
179
 
180
- it("detects npm via spawnSync fallback when path heuristics miss", async () => {
180
+ it("detects npm via spawnSync fallback when running under interpreter", async () => {
181
181
  Object.defineProperty(process, "execPath", {
182
182
  value: "/usr/local/bin/node",
183
183
  writable: true,
@@ -203,7 +203,7 @@ describe("detectInstallMethod", () => {
203
203
  );
204
204
  });
205
205
 
206
- it("returns binary when all heuristics fail", async () => {
206
+ it("returns binary when all heuristics fail under interpreter", async () => {
207
207
  Object.defineProperty(process, "execPath", {
208
208
  value: "/usr/local/bin/node",
209
209
  writable: true,
@@ -223,6 +223,38 @@ describe("detectInstallMethod", () => {
223
223
 
224
224
  expect(detectInstallMethod()).toBe("binary");
225
225
  });
226
+
227
+ it("returns binary for compiled binary even when npm global package exists", async () => {
228
+ Object.defineProperty(process, "execPath", {
229
+ value: "/home/user/.local/bin/pensar",
230
+ writable: true,
231
+ });
232
+ process.argv[1] = "upgrade";
233
+
234
+ const { spawnSync } = await import("child_process");
235
+ const mockedSpawnSync = vi.mocked(spawnSync);
236
+ mockedSpawnSync.mockReturnValue({
237
+ status: 0,
238
+ stdout: "└── @pensar/apex@0.1.0",
239
+ stderr: "",
240
+ pid: 0,
241
+ output: [],
242
+ signal: null,
243
+ });
244
+
245
+ expect(detectInstallMethod()).toBe("binary");
246
+ expect(mockedSpawnSync).not.toHaveBeenCalled();
247
+ });
248
+
249
+ it("returns binary for compiled binary without npm fallback", () => {
250
+ Object.defineProperty(process, "execPath", {
251
+ value: "/usr/local/bin/pensar",
252
+ writable: true,
253
+ });
254
+ process.argv[1] = "uninstall";
255
+
256
+ expect(detectInstallMethod()).toBe("binary");
257
+ });
226
258
  });
227
259
 
228
260
  // ---------------------------------------------------------------------------