@pensar/apex 0.0.101 → 0.0.103-canary.29795a63

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
@@ -73,6 +73,26 @@ if (command === "benchmark") {
73
73
  process.argv = [process.argv[0], uninstallPath, ...args.slice(1)];
74
74
 
75
75
  await import(uninstallPath);
76
+ } else if (command === "projects") {
77
+ const p = join(__dirname, "..", "build", "projects.js");
78
+ process.argv = [process.argv[0], p, ...args.slice(1)];
79
+ await import(p);
80
+ } else if (command === "pentests") {
81
+ const p = join(__dirname, "..", "build", "pentests.js");
82
+ process.argv = [process.argv[0], p, ...args.slice(1)];
83
+ await import(p);
84
+ } else if (command === "issues") {
85
+ const p = join(__dirname, "..", "build", "issues.js");
86
+ process.argv = [process.argv[0], p, ...args.slice(1)];
87
+ await import(p);
88
+ } else if (command === "fixes") {
89
+ const p = join(__dirname, "..", "build", "fixes.js");
90
+ process.argv = [process.argv[0], p, ...args.slice(1)];
91
+ await import(p);
92
+ } else if (command === "logs") {
93
+ const p = join(__dirname, "..", "build", "logs.js");
94
+ process.argv = [process.argv[0], p, ...args.slice(1)];
95
+ await import(p);
76
96
  } else if (command === "upgrade" || command === "update") {
77
97
  const currentVersion = getCurrentVersion();
78
98
  console.log(`Current version: v${currentVersion}`);
@@ -109,6 +129,21 @@ if (command === "benchmark") {
109
129
  console.log(
110
130
  " pensar auth Connect to Pensar Console for managed inference"
111
131
  );
132
+ console.log(
133
+ " pensar projects List workspace projects"
134
+ );
135
+ console.log(
136
+ " pensar pentests List and manage pentests"
137
+ );
138
+ console.log(
139
+ " pensar issues List and manage security issues"
140
+ );
141
+ console.log(
142
+ " pensar fixes View security fixes"
143
+ );
144
+ console.log(
145
+ " pensar logs View agent execution logs"
146
+ );
112
147
  console.log();
113
148
  console.log("Options:");
114
149
  console.log(" -h, --help Show this help message");
@@ -226,6 +261,14 @@ if (command === "benchmark") {
226
261
  console.log(" pensar auth");
227
262
  console.log(" pensar auth status");
228
263
  console.log(" pensar auth logout");
264
+ console.log();
265
+ console.log("Console API:");
266
+ console.log(" pensar projects");
267
+ console.log(" pensar pentests <projectId>");
268
+ console.log(" pensar issues <projectId>");
269
+ console.log(" pensar issues get <issueId>");
270
+ console.log(" pensar fixes <issueId>");
271
+ console.log(" pensar logs <issueId>");
229
272
  } else if (args.length === 0) {
230
273
  // No command specified, run the TUI
231
274
  const appPath = join(__dirname, "..", "build", "index.js");
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.29795a63",
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.29795a63",
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,
@@ -273074,7 +273068,7 @@ var useTerminalDimensions = () => {
273074
273068
  };
273075
273069
 
273076
273070
  // src/tui/index.tsx
273077
- var import_react88 = __toESM(require_react(), 1);
273071
+ var import_react90 = __toESM(require_react(), 1);
273078
273072
 
273079
273073
  // src/tui/components/footer.tsx
273080
273074
  import os6 from "os";
@@ -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();
@@ -281127,6 +281219,98 @@ var actionsByKey = new Map(allActions.map((action) => [action.key, action]));
281127
281219
  var actionsById = new Map(allActions.map((action) => [action.id, action]));
281128
281220
  // src/tui/keybindings/keybind.tsx
281129
281221
  var LeaderKeyContext = import_react62.createContext(null);
281222
+ // src/tui/terminal-focus.ts
281223
+ var bracketedFocusModeEnabled = false;
281224
+ function setupTerminalFocusHandling(renderer, options = {}) {
281225
+ const { onTerminalFocus, debug = false } = options;
281226
+ const log2 = (...args) => {
281227
+ if (debug)
281228
+ console.error("[TerminalFocus]", ...args);
281229
+ };
281230
+ const enableBracketedFocus = () => {
281231
+ if (process.stdout.isTTY) {
281232
+ process.stdout.write("\x1B[?1004h");
281233
+ bracketedFocusModeEnabled = true;
281234
+ log2("Enabled bracketed focus mode");
281235
+ }
281236
+ };
281237
+ const disableBracketedFocus = () => {
281238
+ if (process.stdout.isTTY && bracketedFocusModeEnabled) {
281239
+ process.stdout.write("\x1B[?1004l");
281240
+ bracketedFocusModeEnabled = false;
281241
+ log2("Disabled bracketed focus mode");
281242
+ }
281243
+ };
281244
+ const showCursor = () => {
281245
+ if (process.stdout.isTTY) {
281246
+ process.stdout.write("\x1B[?25h");
281247
+ log2("Showed cursor");
281248
+ }
281249
+ };
281250
+ const handleFocusIn = () => {
281251
+ log2("Terminal gained focus");
281252
+ showCursor();
281253
+ renderer.requestRender();
281254
+ if (onTerminalFocus) {
281255
+ setTimeout(() => {
281256
+ onTerminalFocus();
281257
+ log2("Re-focused prompt input");
281258
+ }, 10);
281259
+ }
281260
+ };
281261
+ const handleFocusOut = () => {
281262
+ log2("Terminal lost focus");
281263
+ };
281264
+ const handleSigCont = () => {
281265
+ log2("Received SIGCONT (terminal resumed)");
281266
+ showCursor();
281267
+ renderer.requestRender();
281268
+ if (onTerminalFocus) {
281269
+ setTimeout(() => {
281270
+ onTerminalFocus();
281271
+ log2("Re-focused prompt input after SIGCONT");
281272
+ }, 10);
281273
+ }
281274
+ };
281275
+ let stdinListenerActive = false;
281276
+ const handleStdinData = (data) => {
281277
+ const str = data.toString();
281278
+ if (str.includes("\x1B[I")) {
281279
+ handleFocusIn();
281280
+ }
281281
+ if (str.includes("\x1B[O")) {
281282
+ handleFocusOut();
281283
+ }
281284
+ };
281285
+ const setupListeners = () => {
281286
+ enableBracketedFocus();
281287
+ showCursor();
281288
+ process.on("SIGCONT", handleSigCont);
281289
+ if (process.stdin.isTTY && !stdinListenerActive) {
281290
+ process.stdin.on("data", handleStdinData);
281291
+ stdinListenerActive = true;
281292
+ log2("Set up stdin listener for bracketed focus events");
281293
+ }
281294
+ };
281295
+ const cleanup = () => {
281296
+ log2("Cleaning up terminal focus handling");
281297
+ disableBracketedFocus();
281298
+ process.off("SIGCONT", handleSigCont);
281299
+ if (stdinListenerActive) {
281300
+ process.stdin.off("data", handleStdinData);
281301
+ stdinListenerActive = false;
281302
+ }
281303
+ };
281304
+ setupListeners();
281305
+ return cleanup;
281306
+ }
281307
+ function cleanupTerminalFocusMode() {
281308
+ if (process.stdout.isTTY && bracketedFocusModeEnabled) {
281309
+ process.stdout.write("\x1B[?1004l");
281310
+ bracketedFocusModeEnabled = false;
281311
+ }
281312
+ }
281313
+
281130
281314
  // src/tui/keybindings/registry.ts
281131
281315
  function createKeybindings(deps) {
281132
281316
  const {
@@ -281155,6 +281339,7 @@ function createKeybindings(deps) {
281155
281339
  const now2 = Date.now();
281156
281340
  const lastPress = ctrlCPressTime;
281157
281341
  if (lastPress && now2 - lastPress < 1000) {
281342
+ cleanupTerminalFocusMode();
281158
281343
  renderer.destroy();
281159
281344
  process.exit(0);
281160
281345
  } else {
@@ -281179,9 +281364,10 @@ function createKeybindings(deps) {
281179
281364
  return;
281180
281365
  }
281181
281366
  const isHome = route.data.type === "base" && route.data.path === "home";
281367
+ const isHelp = route.data.type === "base" && route.data.path === "help";
281182
281368
  const isOperator = route.data.type === "base" && route.data.path === "operator";
281183
281369
  const isSession = route.data.type === "pentest" || route.data.type === "operator";
281184
- if (!isHome && !isOperator && !isSession) {
281370
+ if (!isHome && !isHelp && !isOperator && !isSession) {
281185
281371
  route.navigate({
281186
281372
  type: "base",
281187
281373
  path: "home"
@@ -282925,6 +283111,15 @@ function getToolSummary(toolName, args) {
282925
283111
  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
283112
  return firstArg ? `${toolName} ${String(firstArg).slice(0, 50)}` : toolName;
282927
283113
  }
283114
+ function getToolDisplayLabel(toolName, args, options = {}) {
283115
+ if (options.preferDescription && toolName === "execute_command") {
283116
+ const description = args.toolCallDescription;
283117
+ if (typeof description === "string" && description.trim().length > 0) {
283118
+ return description.trim();
283119
+ }
283120
+ }
283121
+ return getToolSummary(toolName, args);
283122
+ }
282928
283123
  function getArgsPreview(toolName, args, maxLength = 60) {
282929
283124
  const filteredArgs = Object.entries(args).filter(([k3]) => !k3.toLowerCase().includes("description"));
282930
283125
  if (filteredArgs.length === 0)
@@ -283758,7 +283953,9 @@ var ToolRenderer = import_react68.memo(function ToolRenderer2({
283758
283953
  const isCompleted = message.status === "completed";
283759
283954
  const isError = message.status === "error";
283760
283955
  const { toolName, args, result, logs, subagentLogs } = message;
283761
- const summary = getToolSummary(toolName, args);
283956
+ const summary = getToolDisplayLabel(toolName, args, {
283957
+ preferDescription: isPending
283958
+ });
283762
283959
  const resultDisplay = isCompleted || isError ? getResultSummary(result, toolName, args) : null;
283763
283960
  const borderColor = isError ? colors2.error : isPending ? colors2.warning : colors2.info;
283764
283961
  const hasSubagentLogs = subagentLogs && Object.keys(subagentLogs).length > 0;
@@ -286146,7 +286343,7 @@ function MessageList({
286146
286343
  }, undefined, false, undefined, this),
286147
286344
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
286148
286345
  fg: colors2.textMuted,
286149
- children: " - Cycle approval on/off"
286346
+ children: " - Switch between Plan or Default mode"
286150
286347
  }, undefined, false, undefined, this)
286151
286348
  ]
286152
286349
  }, undefined, true, undefined, this)
@@ -286749,7 +286946,7 @@ function navigateDown(selectedIndex, queueLength) {
286749
286946
  }
286750
286947
 
286751
286948
  // src/tui/components/operator-dashboard/index.tsx
286752
- import { existsSync as existsSync27, readFileSync as readFileSync14 } from "fs";
286949
+ import { existsSync as existsSync27, readFileSync as readFileSync14, writeFileSync as writeFileSync17 } from "fs";
286753
286950
  import { join as join28 } from "path";
286754
286951
  function OperatorDashboard({
286755
286952
  sessionId,
@@ -286789,6 +286986,8 @@ function OperatorDashboard({
286789
286986
  return filterOperatorAutocomplete(allAutocompleteOptions, skillSlugs);
286790
286987
  }, [allAutocompleteOptions, skills]);
286791
286988
  const [session, setSession] = import_react79.useState(null);
286989
+ const sessionRef = import_react79.useRef(null);
286990
+ sessionRef.current = session;
286792
286991
  const [loading, setLoading] = import_react79.useState(true);
286793
286992
  const [error40, setError] = import_react79.useState(null);
286794
286993
  const pendingNameRef = import_react79.useRef(null);
@@ -286800,6 +286999,8 @@ function OperatorDashboard({
286800
286999
  });
286801
287000
  const commandCancelledRef = import_react79.useRef(false);
286802
287001
  const [messages, setMessages] = import_react79.useState([]);
287002
+ const displayMessagesRef = import_react79.useRef([]);
287003
+ displayMessagesRef.current = messages;
286803
287004
  const textRef = import_react79.useRef("");
286804
287005
  const conversationRef = import_react79.useRef([]);
286805
287006
  const [inputValue, setInputValue] = import_react79.useState("");
@@ -287183,6 +287384,12 @@ function OperatorDashboard({
287183
287384
  { role: "user", content: prompt }
287184
287385
  ];
287185
287386
  conversationRef.current = nextMessages;
287387
+ if (sessionRef.current) {
287388
+ try {
287389
+ const mp = join28(sessionRef.current.rootPath, "messages.json");
287390
+ writeFileSync17(mp, JSON.stringify(nextMessages, null, 2));
287391
+ } catch {}
287392
+ }
287186
287393
  const onStepFinish = (event) => {
287187
287394
  const nextUsage = accumulateTokenUsage(tokenUsageRef.current, event.usage?.inputTokens ?? 0, event.usage?.outputTokens ?? 0);
287188
287395
  if (!nextUsage)
@@ -287200,22 +287407,32 @@ function OperatorDashboard({
287200
287407
  };
287201
287408
  const callbacks = {
287202
287409
  onTextDelta: (d3) => {
287410
+ if (gen !== generationRef.current)
287411
+ return;
287203
287412
  setThinking(false);
287204
287413
  appendText(d3.text);
287205
287414
  },
287206
287415
  onToolCallStreaming: (d3) => {
287416
+ if (gen !== generationRef.current)
287417
+ return;
287207
287418
  setThinking(false);
287208
287419
  addStreamingToolCall(d3.toolCallId, d3.toolName);
287209
287420
  },
287210
287421
  onToolCallDelta: (d3) => {
287422
+ if (gen !== generationRef.current)
287423
+ return;
287211
287424
  appendToolCallDelta(d3.toolCallId, d3.argsTextDelta);
287212
287425
  },
287213
287426
  onToolCall: (d3) => {
287427
+ if (gen !== generationRef.current)
287428
+ return;
287214
287429
  setThinking(false);
287215
287430
  commandCancelledRef.current = false;
287216
287431
  addToolCall(d3.toolCallId, d3.toolName, d3.input);
287217
287432
  },
287218
287433
  onToolResult: (d3) => {
287434
+ if (gen !== generationRef.current)
287435
+ return;
287219
287436
  flushCommandOutput();
287220
287437
  if (cmdFlushTimerRef.current) {
287221
287438
  clearInterval(cmdFlushTimerRef.current);
@@ -287275,6 +287492,8 @@ function OperatorDashboard({
287275
287492
  callbacks,
287276
287493
  onSessionReady: (s2) => {
287277
287494
  setSessionCwd(s2.rootPath);
287495
+ sessionRef.current = s2;
287496
+ setSession((prev) => prev ?? s2);
287278
287497
  }
287279
287498
  };
287280
287499
  try {
@@ -287520,19 +287739,61 @@ function OperatorDashboard({
287520
287739
  setQueuedMessages([]);
287521
287740
  queuedMessagesRef.current = [];
287522
287741
  setSelectedQueueIndex(-1);
287742
+ approvalGateRef.current.denyAll();
287523
287743
  setStatus("idle");
287524
287744
  setThinking(false);
287525
287745
  setIsExecuting(false);
287526
- approvalGateRef.current.denyAll();
287527
- if (session) {
287746
+ const activeSession = sessionRef.current;
287747
+ if (activeSession) {
287528
287748
  try {
287529
- const messagesPath = join28(session.rootPath, "messages.json");
287749
+ const messagesPath = join28(activeSession.rootPath, "messages.json");
287530
287750
  if (existsSync27(messagesPath)) {
287531
287751
  const raw = JSON.parse(readFileSync14(messagesPath, "utf-8"));
287532
287752
  if (Array.isArray(raw) && raw.length > 0) {
287533
287753
  conversationRef.current = sessions.getResumeMessages(raw);
287534
287754
  }
287535
287755
  }
287756
+ const last = conversationRef.current[conversationRef.current.length - 1];
287757
+ if (last?.role === "user") {
287758
+ const partial2 = textRef.current.trim();
287759
+ const pendingTools = displayMessagesRef.current.filter((m4) => isToolMessage(m4) && (m4.status === "pending" || m4.status === "streaming"));
287760
+ const assistantContent = [
287761
+ {
287762
+ type: "text",
287763
+ text: partial2 || "[Response interrupted by user.]"
287764
+ }
287765
+ ];
287766
+ for (const t3 of pendingTools) {
287767
+ assistantContent.push({
287768
+ type: "tool-call",
287769
+ toolCallId: t3.toolCallId,
287770
+ toolName: t3.toolName,
287771
+ input: t3.args ?? {}
287772
+ });
287773
+ }
287774
+ conversationRef.current = [
287775
+ ...conversationRef.current,
287776
+ {
287777
+ role: "assistant",
287778
+ content: assistantContent
287779
+ }
287780
+ ];
287781
+ if (pendingTools.length > 0) {
287782
+ conversationRef.current = [
287783
+ ...conversationRef.current,
287784
+ {
287785
+ role: "tool",
287786
+ content: pendingTools.map((t3) => ({
287787
+ type: "tool-result",
287788
+ toolCallId: t3.toolCallId,
287789
+ toolName: t3.toolName ?? "unknown",
287790
+ output: "Cancelled by user."
287791
+ }))
287792
+ }
287793
+ ];
287794
+ }
287795
+ writeFileSync17(join28(activeSession.rootPath, "messages.json"), JSON.stringify(conversationRef.current, null, 2));
287796
+ }
287536
287797
  } catch {}
287537
287798
  }
287538
287799
  setMessages((prev) => {
@@ -287546,7 +287807,7 @@ function OperatorDashboard({
287546
287807
  }
287547
287808
  ];
287548
287809
  });
287549
- }, [session, setThinking, setIsExecuting]);
287810
+ }, [setThinking, setIsExecuting]);
287550
287811
  const toggleApproval = import_react79.useCallback(() => {
287551
287812
  setOperatorState((prev) => {
287552
287813
  const newVal = !prev.requireApproval;
@@ -290935,74 +291196,92 @@ function setupAutoCopy(renderer, copyToClipboard) {
290935
291196
  });
290936
291197
  }
290937
291198
 
291199
+ // src/tui/components/terminal-focus-handler.tsx
291200
+ var import_react87 = __toESM(require_react(), 1);
291201
+ function TerminalFocusHandler() {
291202
+ const { refocusPrompt } = useFocus();
291203
+ const renderer = useRenderer();
291204
+ import_react87.useEffect(() => {
291205
+ const cleanup = setupTerminalFocusHandling(renderer, {
291206
+ onTerminalFocus: refocusPrompt,
291207
+ debug: false
291208
+ });
291209
+ return cleanup;
291210
+ }, [renderer, refocusPrompt]);
291211
+ return null;
291212
+ }
291213
+
290938
291214
  // src/tui/index.tsx
290939
291215
  function App({ appConfig }) {
290940
- const [focusIndex, setFocusIndex] = import_react88.useState(0);
290941
- const [cwd, setCwd] = import_react88.useState(process.cwd());
290942
- const [ctrlCPressTime, setCtrlCPressTime] = import_react88.useState(null);
290943
- const [showExitWarning, setShowExitWarning] = import_react88.useState(false);
290944
- const [inputKey, setInputKey] = import_react88.useState(0);
290945
- const [showSessionsDialog, setShowSessionsDialog] = import_react88.useState(false);
290946
- const [showShortcutsDialog, setShowShortcutsDialog] = import_react88.useState(false);
290947
- const [showThemeDialog, setShowThemeDialog] = import_react88.useState(false);
290948
- const [showAuthDialog, setShowAuthDialog] = import_react88.useState(false);
290949
- const [showPentestDialog, setShowPentestDialog] = import_react88.useState(false);
290950
- const [pendingPentestFlags, setPendingPentestFlags] = import_react88.useState(undefined);
291216
+ const [focusIndex, setFocusIndex] = import_react90.useState(0);
291217
+ const [cwd, setCwd] = import_react90.useState(process.cwd());
291218
+ const [ctrlCPressTime, setCtrlCPressTime] = import_react90.useState(null);
291219
+ const [showExitWarning, setShowExitWarning] = import_react90.useState(false);
291220
+ const [inputKey, setInputKey] = import_react90.useState(0);
291221
+ const [showSessionsDialog, setShowSessionsDialog] = import_react90.useState(false);
291222
+ const [showShortcutsDialog, setShowShortcutsDialog] = import_react90.useState(false);
291223
+ const [showThemeDialog, setShowThemeDialog] = import_react90.useState(false);
291224
+ const [showAuthDialog, setShowAuthDialog] = import_react90.useState(false);
291225
+ const [showPentestDialog, setShowPentestDialog] = import_react90.useState(false);
291226
+ const [pendingPentestFlags, setPendingPentestFlags] = import_react90.useState(undefined);
290951
291227
  const navigableItems = ["command-input"];
290952
291228
  return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(ConfigProvider, {
290953
291229
  config: appConfig,
290954
291230
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(SessionProvider, {
290955
291231
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(RouteProvider, {
290956
291232
  children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(FocusProvider, {
290957
- children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(InputProvider, {
290958
- children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(DialogProvider, {
290959
- children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AgentProvider, {
290960
- children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(CommandProvider, {
290961
- onOpenSessionsDialog: () => setShowSessionsDialog(true),
290962
- onOpenThemeDialog: () => setShowThemeDialog(true),
290963
- onOpenAuthDialog: () => setShowAuthDialog(true),
290964
- onOpenPentestDialog: (flags) => {
290965
- setPendingPentestFlags(flags);
290966
- setShowPentestDialog(true);
290967
- },
290968
- children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(KeybindingProvider, {
290969
- deps: {
290970
- ctrlCPressTime,
290971
- setCtrlCPressTime,
290972
- setShowExitWarning,
290973
- setInputKey,
290974
- setShowSessionsDialog,
290975
- setShowShortcutsDialog,
290976
- setFocusIndex,
290977
- navigableItems
291233
+ children: [
291234
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(TerminalFocusHandler, {}, undefined, false, undefined, this),
291235
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(InputProvider, {
291236
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(DialogProvider, {
291237
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AgentProvider, {
291238
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(CommandProvider, {
291239
+ onOpenSessionsDialog: () => setShowSessionsDialog(true),
291240
+ onOpenThemeDialog: () => setShowThemeDialog(true),
291241
+ onOpenAuthDialog: () => setShowAuthDialog(true),
291242
+ onOpenPentestDialog: (flags) => {
291243
+ setPendingPentestFlags(flags);
291244
+ setShowPentestDialog(true);
290978
291245
  },
290979
- children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AppContent, {
290980
- focusIndex,
290981
- showSessionsDialog,
290982
- setShowSessionsDialog,
290983
- showShortcutsDialog,
290984
- setShowShortcutsDialog,
290985
- showThemeDialog,
290986
- setShowThemeDialog,
290987
- showAuthDialog,
290988
- setShowAuthDialog,
290989
- showPentestDialog,
290990
- setShowPentestDialog,
290991
- pendingPentestFlags,
290992
- setPendingPentestFlags,
290993
- cwd,
290994
- setCtrlCPressTime,
290995
- showExitWarning,
290996
- setShowExitWarning,
290997
- inputKey,
290998
- setInputKey
291246
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(KeybindingProvider, {
291247
+ deps: {
291248
+ ctrlCPressTime,
291249
+ setCtrlCPressTime,
291250
+ setShowExitWarning,
291251
+ setInputKey,
291252
+ setShowSessionsDialog,
291253
+ setShowShortcutsDialog,
291254
+ setFocusIndex,
291255
+ navigableItems
291256
+ },
291257
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(AppContent, {
291258
+ focusIndex,
291259
+ showSessionsDialog,
291260
+ setShowSessionsDialog,
291261
+ showShortcutsDialog,
291262
+ setShowShortcutsDialog,
291263
+ showThemeDialog,
291264
+ setShowThemeDialog,
291265
+ showAuthDialog,
291266
+ setShowAuthDialog,
291267
+ showPentestDialog,
291268
+ setShowPentestDialog,
291269
+ pendingPentestFlags,
291270
+ setPendingPentestFlags,
291271
+ cwd,
291272
+ setCtrlCPressTime,
291273
+ showExitWarning,
291274
+ setShowExitWarning,
291275
+ inputKey,
291276
+ setInputKey
291277
+ }, undefined, false, undefined, this)
290999
291278
  }, undefined, false, undefined, this)
291000
291279
  }, undefined, false, undefined, this)
291001
291280
  }, undefined, false, undefined, this)
291002
291281
  }, undefined, false, undefined, this)
291003
291282
  }, undefined, false, undefined, this)
291004
- }, undefined, false, undefined, this)
291005
- }, undefined, false, undefined, this)
291283
+ ]
291284
+ }, undefined, true, undefined, this)
291006
291285
  }, undefined, false, undefined, this)
291007
291286
  }, undefined, false, undefined, this)
291008
291287
  }, undefined, false, undefined, this);
@@ -291034,14 +291313,14 @@ function AppContent({
291034
291313
  const { toast } = useToast();
291035
291314
  const { refocusPrompt } = useFocus();
291036
291315
  const { setExternalDialogOpen } = useDialog();
291037
- import_react88.useEffect(() => {
291316
+ import_react90.useEffect(() => {
291038
291317
  checkForUpdate().then(({ updateAvailable, currentVersion, latestVersion }) => {
291039
291318
  if (!updateAvailable)
291040
291319
  return;
291041
291320
  toast(`Update available: v${currentVersion} → v${latestVersion}. Run: pensar upgrade`, "warn", 8000);
291042
291321
  });
291043
291322
  }, []);
291044
- import_react88.useEffect(() => {
291323
+ import_react90.useEffect(() => {
291045
291324
  if (route.data.type !== "base")
291046
291325
  return;
291047
291326
  if (!config3.data.responsibleUseAccepted && route.data.path !== "disclosure") {
@@ -291050,12 +291329,12 @@ function AppContent({
291050
291329
  route.navigate({ type: "base", path: "providers" });
291051
291330
  }
291052
291331
  }, [config3.data.responsibleUseAccepted, route.data]);
291053
- import_react88.useEffect(() => {
291332
+ import_react90.useEffect(() => {
291054
291333
  if (showThemeDialog || showAuthDialog || showPentestDialog) {
291055
291334
  setExternalDialogOpen(true);
291056
291335
  }
291057
291336
  }, [showThemeDialog, showAuthDialog, showPentestDialog]);
291058
- import_react88.useEffect(() => {
291337
+ import_react90.useEffect(() => {
291059
291338
  if (showExitWarning) {
291060
291339
  const timer = setTimeout(() => {
291061
291340
  setShowExitWarning(false);
@@ -291287,18 +291566,21 @@ async function main2() {
291287
291566
  const { copyToClipboard } = createClipboardManager(renderer);
291288
291567
  setupAutoCopy(renderer, copyToClipboard);
291289
291568
  const cleanup = () => {
291569
+ cleanupTerminalFocusMode();
291290
291570
  renderer.destroy();
291291
291571
  process.exit(0);
291292
291572
  };
291293
291573
  process.on("SIGINT", cleanup);
291294
291574
  process.on("SIGTERM", cleanup);
291295
291575
  process.on("uncaughtException", (err) => {
291576
+ cleanupTerminalFocusMode();
291296
291577
  renderer.destroy();
291297
291578
  console.error("Uncaught exception:", err);
291298
291579
  writeErrorLog(err, "UNCAUGHT");
291299
291580
  process.exit(1);
291300
291581
  });
291301
291582
  process.on("unhandledRejection", (reason) => {
291583
+ cleanupTerminalFocusMode();
291302
291584
  renderer.destroy();
291303
291585
  console.error("Unhandled rejection:", reason);
291304
291586
  writeErrorLog(reason, "UNHANDLED_REJECTION");
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.29795a63",
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
  // ---------------------------------------------------------------------------