@pensar/apex 0.0.101 → 0.0.103-canary.25377b86

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/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.101",
11
+ version: "0.0.103-canary.25377b86",
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.101",
31974
+ version: "0.0.103-canary.25377b86",
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
 
@@ -194814,7 +194809,8 @@ var init_operator = __esm(() => {
194814
194809
 
194815
194810
  // src/core/agents/offSecAgent/offensiveSecurityAgent.ts
194816
194811
  import { join as join22 } from "path";
194817
- 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";
194818
194814
  function wrapToolsWithApprovalGate(tools, gate) {
194819
194815
  const wrapped = {};
194820
194816
  for (const [name26, coreTool] of Object.entries(tools)) {
@@ -194968,13 +194964,11 @@ var init_offensiveSecurityAgent = __esm(() => {
194968
194964
  stopWhen,
194969
194965
  toolChoice: "auto",
194970
194966
  onStepFinish: (event) => {
194971
- try {
194972
- const allMessages = [
194973
- ...initialMessagesRef.current,
194974
- ...event.response.messages
194975
- ];
194976
- writeFileSync15(messagesPath, JSON.stringify(allMessages, null, 2));
194977
- } catch {}
194967
+ const allMessages = [
194968
+ ...initialMessagesRef.current,
194969
+ ...event.response.messages
194970
+ ];
194971
+ writeFile3(messagesPath, JSON.stringify(allMessages, null, 2)).catch(() => {});
194978
194972
  input.onStepFinish?.(event);
194979
194973
  },
194980
194974
  onSummarized: () => {
@@ -195498,7 +195492,7 @@ var init_agent4 = __esm(() => {
195498
195492
  });
195499
195493
 
195500
195494
  // src/core/session/execution-metrics.ts
195501
- 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";
195502
195496
  import { join as join25 } from "path";
195503
195497
  function toNonNegativeInteger(value) {
195504
195498
  const n = Number(value);
@@ -195544,7 +195538,7 @@ function writeSessionJsonTokenTotals(sessionRootPath, tokenUsage) {
195544
195538
  const parsed = JSON.parse(readFileSync11(path6, "utf-8"));
195545
195539
  parsed.tokensIn = tokenUsage.inputTokens;
195546
195540
  parsed.tokensOut = tokenUsage.outputTokens;
195547
- writeFileSync16(path6, JSON.stringify(parsed, null, 2));
195541
+ writeFileSync15(path6, JSON.stringify(parsed, null, 2));
195548
195542
  } catch {}
195549
195543
  }
195550
195544
  function writeExecutionMetrics(input) {
@@ -195559,7 +195553,7 @@ function writeExecutionMetrics(input) {
195559
195553
  runtime: input.runtime ?? existing?.runtime,
195560
195554
  updatedAt: new Date().toISOString()
195561
195555
  };
195562
- writeFileSync16(metricsPath(input.sessionRootPath), JSON.stringify(next, null, 2), "utf-8");
195556
+ writeFileSync15(metricsPath(input.sessionRootPath), JSON.stringify(next, null, 2), "utf-8");
195563
195557
  writeSessionJsonTokenTotals(input.sessionRootPath, next.tokenUsage);
195564
195558
  return next;
195565
195559
  }
@@ -196154,7 +196148,7 @@ __export(exports_pentest, {
196154
196148
  runPentestSwarm: () => runPentestSwarm,
196155
196149
  DEFAULT_CONCURRENCY: () => DEFAULT_CONCURRENCY4
196156
196150
  });
196157
- 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";
196158
196152
  import { join as join26 } from "path";
196159
196153
  function addUsageTotals(totals, usage) {
196160
196154
  if (!usage)
@@ -196324,8 +196318,8 @@ async function runPentestWorkflow(input) {
196324
196318
  });
196325
196319
  const mdPath2 = join26(session.rootPath, REPORT_FILENAME_MD);
196326
196320
  const jsonPath2 = join26(session.rootPath, REPORT_FILENAME_JSON);
196327
- writeFileSync17(mdPath2, renderMarkdown(report2));
196328
- writeFileSync17(jsonPath2, renderJson(report2));
196321
+ writeFileSync16(mdPath2, renderMarkdown(report2));
196322
+ writeFileSync16(jsonPath2, renderJson(report2));
196329
196323
  return {
196330
196324
  findings: [],
196331
196325
  findingsPath: session.findingsPath,
@@ -196364,8 +196358,8 @@ async function runPentestWorkflow(input) {
196364
196358
  });
196365
196359
  const mdPath = join26(session.rootPath, REPORT_FILENAME_MD);
196366
196360
  const jsonPath = join26(session.rootPath, REPORT_FILENAME_JSON);
196367
- writeFileSync17(mdPath, renderMarkdown(report));
196368
- writeFileSync17(jsonPath, renderJson(report));
196361
+ writeFileSync16(mdPath, renderMarkdown(report));
196362
+ writeFileSync16(jsonPath, renderJson(report));
196369
196363
  return {
196370
196364
  findings,
196371
196365
  findingsPath: session.findingsPath,
@@ -274517,7 +274511,27 @@ function CommandProvider({
274517
274511
  description: skill.description || "Skill"
274518
274512
  });
274519
274513
  }
274520
- 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
+ });
274521
274535
  }, [router, skills]);
274522
274536
  const executeCommand = import_react17.useCallback(async (input) => {
274523
274537
  return await router.execute(input, ctx3);
@@ -275734,6 +275748,40 @@ function computeTab(suggestions, selectedIndex) {
275734
275748
  function shouldResetHistory(historyIndex, isNavigatingHistory) {
275735
275749
  return historyIndex !== -1 && !isNavigatingHistory;
275736
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
+ }
275737
275785
 
275738
275786
  // src/tui/components/shared/use-paste-extmarks.ts
275739
275787
  var import_react30 = __toESM(require_react(), 1);
@@ -275827,6 +275875,7 @@ var PromptInput = import_react31.forwardRef(function PromptInput2({
275827
275875
  enableAutocomplete = false,
275828
275876
  autocompleteOptions = [],
275829
275877
  maxSuggestions = 10,
275878
+ maxVisibleSuggestions = 6,
275830
275879
  enableCommands = false,
275831
275880
  onCommandExecute,
275832
275881
  commandHistory = [],
@@ -275992,34 +276041,72 @@ var PromptInput = import_react31.forwardRef(function PromptInput2({
275992
276041
  setHistoryIndex(-1);
275993
276042
  }
275994
276043
  };
276044
+ const windowedView = import_react31.useMemo(() => computeVisibleWindow(suggestions, selectedSuggestionIndex, maxVisibleSuggestions), [suggestions, selectedSuggestionIndex, maxVisibleSuggestions]);
275995
276045
  const suggestionsBox = suggestions.length > 0 && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
275996
276046
  flexDirection: "column",
275997
276047
  ...autocompletePlacement === "above" ? { marginBottom: 1 } : { marginTop: 1 },
275998
- children: suggestions.map((suggestion, index) => {
275999
- const isSelected = index === selectedSuggestionIndex;
276000
- return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
276048
+ children: [
276049
+ windowedView.hasMore && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
276001
276050
  flexDirection: "row",
276002
276051
  gap: 1,
276003
276052
  children: [
276004
276053
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276005
- fg: isSelected ? colors2.primary : colors2.textMuted,
276006
- children: isSelected ? " ▸" : " "
276054
+ fg: colors2.textMuted,
276055
+ children: " "
276007
276056
  }, undefined, false, undefined, this),
276008
276057
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276009
- fg: isSelected ? colors2.text : colors2.textMuted,
276010
- 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: " ↓"
276011
276098
  }, undefined, false, undefined, this),
276012
- suggestion.description && /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276099
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
276013
276100
  fg: colors2.textMuted,
276014
276101
  children: [
276015
- " ",
276016
- suggestion.description
276102
+ suggestions.length - windowedView.end,
276103
+ " more below..."
276017
276104
  ]
276018
276105
  }, undefined, true, undefined, this)
276019
276106
  ]
276020
- }, suggestion.value, true, undefined, this);
276021
- })
276022
- }, undefined, false, undefined, this);
276107
+ }, undefined, true, undefined, this)
276108
+ ]
276109
+ }, undefined, true, undefined, this);
276023
276110
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
276024
276111
  flexDirection: "column",
276025
276112
  children: [
@@ -279763,7 +279850,7 @@ function ResponsibleUseDisclosure({
279763
279850
  }, undefined, false, undefined, this),
279764
279851
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
279765
279852
  fg: colors2.text,
279766
- children: "This penetration testing tool is designedfor AUTHORIZED security testing only."
279853
+ children: "This penetration testing tool is designed for AUTHORIZED security testing only."
279767
279854
  }, undefined, false, undefined, this),
279768
279855
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
279769
279856
  flexDirection: "column",
@@ -279806,7 +279893,7 @@ function ResponsibleUseDisclosure({
279806
279893
  flexDirection: "column",
279807
279894
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
279808
279895
  fg: colors2.error,
279809
- 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."
279810
279897
  }, undefined, false, undefined, this)
279811
279898
  }, undefined, false, undefined, this),
279812
279899
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
@@ -280113,18 +280200,23 @@ function HelpDialog() {
280113
280200
  });
280114
280201
  };
280115
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
+ }
280116
280212
  if (showDetail) {
280117
- if (evt.name === "escape" || evt.name === "return") {
280213
+ if (evt.name === "return") {
280118
280214
  evt.preventDefault();
280119
280215
  setShowDetail(false);
280120
280216
  }
280121
280217
  return;
280122
280218
  }
280123
280219
  switch (evt.name) {
280124
- case "escape":
280125
- evt.preventDefault();
280126
- handleClose();
280127
- break;
280128
280220
  case "up":
280129
280221
  case "k":
280130
280222
  evt.preventDefault();
@@ -281179,9 +281271,10 @@ function createKeybindings(deps) {
281179
281271
  return;
281180
281272
  }
281181
281273
  const isHome = route.data.type === "base" && route.data.path === "home";
281274
+ const isHelp = route.data.type === "base" && route.data.path === "help";
281182
281275
  const isOperator = route.data.type === "base" && route.data.path === "operator";
281183
281276
  const isSession = route.data.type === "pentest" || route.data.type === "operator";
281184
- if (!isHome && !isOperator && !isSession) {
281277
+ if (!isHome && !isHelp && !isOperator && !isSession) {
281185
281278
  route.navigate({
281186
281279
  type: "base",
281187
281280
  path: "home"
@@ -282925,6 +283018,15 @@ function getToolSummary(toolName, args) {
282925
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);
282926
283019
  return firstArg ? `${toolName} ${String(firstArg).slice(0, 50)}` : toolName;
282927
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
+ }
282928
283030
  function getArgsPreview(toolName, args, maxLength = 60) {
282929
283031
  const filteredArgs = Object.entries(args).filter(([k3]) => !k3.toLowerCase().includes("description"));
282930
283032
  if (filteredArgs.length === 0)
@@ -283758,7 +283860,9 @@ var ToolRenderer = import_react68.memo(function ToolRenderer2({
283758
283860
  const isCompleted = message.status === "completed";
283759
283861
  const isError = message.status === "error";
283760
283862
  const { toolName, args, result, logs, subagentLogs } = message;
283761
- const summary = getToolSummary(toolName, args);
283863
+ const summary = getToolDisplayLabel(toolName, args, {
283864
+ preferDescription: isPending
283865
+ });
283762
283866
  const resultDisplay = isCompleted || isError ? getResultSummary(result, toolName, args) : null;
283763
283867
  const borderColor = isError ? colors2.error : isPending ? colors2.warning : colors2.info;
283764
283868
  const hasSubagentLogs = subagentLogs && Object.keys(subagentLogs).length > 0;
@@ -286146,7 +286250,7 @@ function MessageList({
286146
286250
  }, undefined, false, undefined, this),
286147
286251
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
286148
286252
  fg: colors2.textMuted,
286149
- children: " - Cycle approval on/off"
286253
+ children: " - Switch between Plan or Default mode"
286150
286254
  }, undefined, false, undefined, this)
286151
286255
  ]
286152
286256
  }, undefined, true, undefined, this)
@@ -286749,7 +286853,7 @@ function navigateDown(selectedIndex, queueLength) {
286749
286853
  }
286750
286854
 
286751
286855
  // src/tui/components/operator-dashboard/index.tsx
286752
- import { existsSync as existsSync27, readFileSync as readFileSync14 } from "fs";
286856
+ import { existsSync as existsSync27, readFileSync as readFileSync14, writeFileSync as writeFileSync17 } from "fs";
286753
286857
  import { join as join28 } from "path";
286754
286858
  function OperatorDashboard({
286755
286859
  sessionId,
@@ -286789,6 +286893,8 @@ function OperatorDashboard({
286789
286893
  return filterOperatorAutocomplete(allAutocompleteOptions, skillSlugs);
286790
286894
  }, [allAutocompleteOptions, skills]);
286791
286895
  const [session, setSession] = import_react79.useState(null);
286896
+ const sessionRef = import_react79.useRef(null);
286897
+ sessionRef.current = session;
286792
286898
  const [loading, setLoading] = import_react79.useState(true);
286793
286899
  const [error40, setError] = import_react79.useState(null);
286794
286900
  const pendingNameRef = import_react79.useRef(null);
@@ -286800,6 +286906,8 @@ function OperatorDashboard({
286800
286906
  });
286801
286907
  const commandCancelledRef = import_react79.useRef(false);
286802
286908
  const [messages, setMessages] = import_react79.useState([]);
286909
+ const displayMessagesRef = import_react79.useRef([]);
286910
+ displayMessagesRef.current = messages;
286803
286911
  const textRef = import_react79.useRef("");
286804
286912
  const conversationRef = import_react79.useRef([]);
286805
286913
  const [inputValue, setInputValue] = import_react79.useState("");
@@ -287183,6 +287291,12 @@ function OperatorDashboard({
287183
287291
  { role: "user", content: prompt }
287184
287292
  ];
287185
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
+ }
287186
287300
  const onStepFinish = (event) => {
287187
287301
  const nextUsage = accumulateTokenUsage(tokenUsageRef.current, event.usage?.inputTokens ?? 0, event.usage?.outputTokens ?? 0);
287188
287302
  if (!nextUsage)
@@ -287200,22 +287314,32 @@ function OperatorDashboard({
287200
287314
  };
287201
287315
  const callbacks = {
287202
287316
  onTextDelta: (d3) => {
287317
+ if (gen !== generationRef.current)
287318
+ return;
287203
287319
  setThinking(false);
287204
287320
  appendText(d3.text);
287205
287321
  },
287206
287322
  onToolCallStreaming: (d3) => {
287323
+ if (gen !== generationRef.current)
287324
+ return;
287207
287325
  setThinking(false);
287208
287326
  addStreamingToolCall(d3.toolCallId, d3.toolName);
287209
287327
  },
287210
287328
  onToolCallDelta: (d3) => {
287329
+ if (gen !== generationRef.current)
287330
+ return;
287211
287331
  appendToolCallDelta(d3.toolCallId, d3.argsTextDelta);
287212
287332
  },
287213
287333
  onToolCall: (d3) => {
287334
+ if (gen !== generationRef.current)
287335
+ return;
287214
287336
  setThinking(false);
287215
287337
  commandCancelledRef.current = false;
287216
287338
  addToolCall(d3.toolCallId, d3.toolName, d3.input);
287217
287339
  },
287218
287340
  onToolResult: (d3) => {
287341
+ if (gen !== generationRef.current)
287342
+ return;
287219
287343
  flushCommandOutput();
287220
287344
  if (cmdFlushTimerRef.current) {
287221
287345
  clearInterval(cmdFlushTimerRef.current);
@@ -287275,6 +287399,8 @@ function OperatorDashboard({
287275
287399
  callbacks,
287276
287400
  onSessionReady: (s2) => {
287277
287401
  setSessionCwd(s2.rootPath);
287402
+ sessionRef.current = s2;
287403
+ setSession((prev) => prev ?? s2);
287278
287404
  }
287279
287405
  };
287280
287406
  try {
@@ -287520,19 +287646,61 @@ function OperatorDashboard({
287520
287646
  setQueuedMessages([]);
287521
287647
  queuedMessagesRef.current = [];
287522
287648
  setSelectedQueueIndex(-1);
287649
+ approvalGateRef.current.denyAll();
287523
287650
  setStatus("idle");
287524
287651
  setThinking(false);
287525
287652
  setIsExecuting(false);
287526
- approvalGateRef.current.denyAll();
287527
- if (session) {
287653
+ const activeSession = sessionRef.current;
287654
+ if (activeSession) {
287528
287655
  try {
287529
- const messagesPath = join28(session.rootPath, "messages.json");
287656
+ const messagesPath = join28(activeSession.rootPath, "messages.json");
287530
287657
  if (existsSync27(messagesPath)) {
287531
287658
  const raw = JSON.parse(readFileSync14(messagesPath, "utf-8"));
287532
287659
  if (Array.isArray(raw) && raw.length > 0) {
287533
287660
  conversationRef.current = sessions.getResumeMessages(raw);
287534
287661
  }
287535
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
+ }
287536
287704
  } catch {}
287537
287705
  }
287538
287706
  setMessages((prev) => {
@@ -287546,7 +287714,7 @@ function OperatorDashboard({
287546
287714
  }
287547
287715
  ];
287548
287716
  });
287549
- }, [session, setThinking, setIsExecuting]);
287717
+ }, [setThinking, setIsExecuting]);
287550
287718
  const toggleApproval = import_react79.useCallback(() => {
287551
287719
  setOperatorState((prev) => {
287552
287720
  const newVal = !prev.requireApproval;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pensar/apex",
3
- "version": "0.0.101",
3
+ "version": "0.0.103-canary.25377b86",
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
  // ---------------------------------------------------------------------------