pi-agent-browser-native 0.2.48 → 0.2.50

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.
Files changed (189) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +21 -11
  3. package/dist/extensions/agent-browser/index.js +808 -0
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
  6. package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
  9. package/dist/extensions/agent-browser/lib/config-policy.js +669 -0
  10. package/dist/extensions/agent-browser/lib/config.js +122 -0
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
  15. package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
  16. package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
  17. package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
  18. package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
  19. package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
  21. package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
  22. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
  23. package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
  24. package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
  25. package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
  26. package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
  27. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
  28. package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
  29. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
  49. package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
  50. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
  51. package/dist/extensions/agent-browser/lib/playbook.js +121 -0
  52. package/dist/extensions/agent-browser/lib/process.js +363 -0
  53. package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
  54. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
  55. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
  56. package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
  57. package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
  58. package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
  59. package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
  60. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
  61. package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
  62. package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
  63. package/dist/extensions/agent-browser/lib/results/network.js +73 -0
  64. package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
  65. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
  66. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
  67. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
  68. package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
  69. package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
  70. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +956 -0
  71. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
  72. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
  73. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
  74. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
  75. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
  76. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
  77. package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
  78. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
  79. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
  80. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
  81. package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
  82. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
  83. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
  84. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
  85. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
  86. package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
  87. package/dist/extensions/agent-browser/lib/results/text.js +40 -0
  88. package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
  89. package/dist/extensions/agent-browser/lib/runtime.js +855 -0
  90. package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
  91. package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
  92. package/dist/extensions/agent-browser/lib/temp.js +498 -0
  93. package/dist/extensions/agent-browser/lib/web-search.js +562 -0
  94. package/docs/ARCHITECTURE.md +5 -5
  95. package/docs/COMMAND_REFERENCE.md +4 -4
  96. package/docs/RELEASE.md +22 -11
  97. package/docs/REQUIREMENTS.md +1 -1
  98. package/docs/SUPPORT_MATRIX.md +5 -4
  99. package/docs/TOOL_CONTRACT.md +1 -1
  100. package/package.json +9 -5
  101. package/scripts/config.mjs +14 -20
  102. package/scripts/doctor.mjs +8 -7
  103. package/extensions/agent-browser/index.ts +0 -961
  104. package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
  105. package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
  106. package/extensions/agent-browser/lib/bash-guard.ts +0 -205
  107. package/extensions/agent-browser/lib/command-policy.ts +0 -71
  108. package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
  109. package/extensions/agent-browser/lib/config-policy.js +0 -690
  110. package/extensions/agent-browser/lib/config.ts +0 -211
  111. package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
  112. package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
  113. package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
  114. package/extensions/agent-browser/lib/electron/launch.ts +0 -499
  115. package/extensions/agent-browser/lib/executable-path.ts +0 -19
  116. package/extensions/agent-browser/lib/fs-utils.ts +0 -18
  117. package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
  118. package/extensions/agent-browser/lib/input-modes/job.ts +0 -527
  119. package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
  120. package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
  121. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
  122. package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
  123. package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
  124. package/extensions/agent-browser/lib/input-modes.ts +0 -45
  125. package/extensions/agent-browser/lib/json-schema.ts +0 -73
  126. package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
  127. package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
  128. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
  129. package/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.ts +0 -44
  130. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -280
  131. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -914
  132. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -521
  133. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
  134. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.ts +0 -158
  135. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.ts +0 -116
  136. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.ts +0 -147
  137. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.ts +0 -183
  138. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.ts +0 -58
  139. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -847
  140. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -559
  141. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
  142. package/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.ts +0 -8
  143. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
  144. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -565
  145. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
  146. package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
  147. package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
  148. package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -267
  149. package/extensions/agent-browser/lib/playbook.ts +0 -142
  150. package/extensions/agent-browser/lib/process.ts +0 -516
  151. package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
  152. package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
  153. package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
  154. package/extensions/agent-browser/lib/results/categories.ts +0 -106
  155. package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
  156. package/extensions/agent-browser/lib/results/contracts.ts +0 -241
  157. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
  158. package/extensions/agent-browser/lib/results/envelope.ts +0 -195
  159. package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
  160. package/extensions/agent-browser/lib/results/network.ts +0 -78
  161. package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
  162. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
  163. package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
  164. package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
  165. package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
  166. package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
  167. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
  168. package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
  169. package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
  170. package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
  171. package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
  172. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
  173. package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
  174. package/extensions/agent-browser/lib/results/presentation.ts +0 -257
  175. package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
  176. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
  177. package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
  178. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
  179. package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
  180. package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
  181. package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
  182. package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
  183. package/extensions/agent-browser/lib/results/text.ts +0 -40
  184. package/extensions/agent-browser/lib/runtime.ts +0 -988
  185. package/extensions/agent-browser/lib/session-page-state.ts +0 -512
  186. package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
  187. package/extensions/agent-browser/lib/temp.ts +0 -577
  188. package/extensions/agent-browser/lib/web-search.ts +0 -728
  189. /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
@@ -1,710 +0,0 @@
1
- /**
2
- * Purpose: Discover installed Electron desktop applications without invoking upstream agent-browser.
3
- * Responsibilities: Scan bounded macOS app bundles and Linux .desktop launchers, apply Electron framework evidence gates, and return small platform-tagged app metadata.
4
- * Scope: Discovery only; launch, cleanup, status, and CDP attachment live in later Electron lifecycle work items.
5
- * Usage: Called by the agent_browser top-level electron.list shorthand and directly by tests through parameterized scan locations.
6
- * Invariants/Assumptions: Discovery is best-effort, missing scan roots are ignored, malformed .desktop files are skipped, and results are capped before they reach model-visible output.
7
- */
8
-
9
- import { constants as fsConstants } from "node:fs";
10
- import { access, readdir, readFile, realpath, stat } from "node:fs/promises";
11
- import { homedir } from "node:os";
12
- import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
13
-
14
- import { pathExists } from "../fs-utils.js";
15
-
16
- export const ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS = 50;
17
- export const ELECTRON_DISCOVERY_MAX_RESULTS = 200;
18
-
19
- const LINUX_ELECTRON_CANDIDATE_MAX_DEPTH = 7;
20
- const LINUX_ELECTRON_CANDIDATE_MAX_ENTRIES = 5_000;
21
- const LINUX_NON_EXECUTABLE_CANDIDATE_EXTENSIONS = new Set([".asar", ".dat", ".desktop", ".json", ".md", ".pak", ".png", ".so", ".txt"]);
22
-
23
- export type ElectronDiscoveryPlatform = "darwin" | "linux" | "win32";
24
-
25
- export interface ElectronAppSensitivity {
26
- categories: string[];
27
- level: "likely-sensitive";
28
- reason: string;
29
- }
30
-
31
- export interface ElectronAppDiscovery {
32
- appPath?: string;
33
- bundleId?: string;
34
- comment?: string;
35
- desktopId?: string;
36
- executablePath: string;
37
- icon?: string;
38
- name: string;
39
- packageSource?: "desktop" | "flatpak" | "snap";
40
- platform: ElectronDiscoveryPlatform;
41
- sensitivity?: ElectronAppSensitivity;
42
- }
43
-
44
- export interface ElectronDiscoveryScanLocations {
45
- darwinApplicationDirectories?: string[];
46
- flatpakSystemAppDirectory?: string;
47
- flatpakUserAppDirectory?: string;
48
- homeDir?: string;
49
- linuxDesktopDirectories?: string[];
50
- pathEnv?: string;
51
- snapBinDirectory?: string;
52
- snapMountDirectory?: string;
53
- }
54
-
55
- export interface DiscoverElectronAppsOptions {
56
- locations?: ElectronDiscoveryScanLocations;
57
- maxResults?: number;
58
- platform?: NodeJS.Platform | ElectronDiscoveryPlatform;
59
- query?: string;
60
- }
61
-
62
- export interface ElectronDiscoveryResult {
63
- apps: ElectronAppDiscovery[];
64
- maxResults: number;
65
- omittedCount: number;
66
- platform: ElectronDiscoveryPlatform | "unsupported";
67
- query?: string;
68
- skippedCount?: number;
69
- }
70
-
71
- interface ResolvedElectronDiscoveryLocations {
72
- darwinApplicationDirectories: string[];
73
- flatpakSystemAppDirectory: string;
74
- flatpakUserAppDirectory: string;
75
- homeDir: string;
76
- linuxDesktopDirectories: string[];
77
- pathEnv: string;
78
- snapBinDirectory: string;
79
- snapMountDirectory: string;
80
- }
81
-
82
- interface LinuxDesktopEntry {
83
- comment?: string;
84
- desktopId: string;
85
- exec: string;
86
- filePath: string;
87
- icon?: string;
88
- name: string;
89
- }
90
-
91
- interface LinuxExecutableResolution {
92
- executablePath: string;
93
- packageSource: "desktop" | "flatpak" | "snap";
94
- }
95
-
96
- const ELECTRON_SENSITIVE_APP_CATEGORY_PATTERNS: Array<{ category: string; patterns: RegExp[] }> = [
97
- { category: "notes", patterns: [/\bobsidian\b/i, /\bnotion\b/i, /\blogseq\b/i] },
98
- { category: "chat", patterns: [/\bslack\b/i, /\bdiscord\b/i, /\bteams\b/i, /\bsignal\b/i, /\btelegram\b/i, /\bwhatsapp\b/i] },
99
- { category: "mail", patterns: [/\bmail\b/i, /\boutlook\b/i, /\bthunderbird\b/i, /\bspark\b/i, /\bproton[- ]?mail\b/i] },
100
- { category: "developer-workspace", patterns: [/\bvisual studio code\b/i, /\bvs ?code\b/i, /\bcode - insiders\b/i, /^code$/i, /\bcursor\b/i, /\bwindsurf\b/i] },
101
- { category: "passwords-auth", patterns: [/\b1password\b/i, /\bbitwarden\b/i, /\blastpass\b/i, /\bdashlane\b/i, /\bauthy\b/i, /\bauthenticator\b/i, /\bkeepass\b/i] },
102
- ];
103
-
104
- function normalizeSensitivityValue(value: string | undefined): string | undefined {
105
- return value?.trim().replace(/[_./\\-]+/g, " ").replace(/\s+/g, " ") || undefined;
106
- }
107
-
108
- export function getElectronAppSensitivity(app: ElectronAppDiscovery): ElectronAppSensitivity | undefined {
109
- const metadataValues = [app.name, app.bundleId, app.desktopId, app.appPath, app.executablePath]
110
- .map(normalizeSensitivityValue)
111
- .filter((value): value is string => value !== undefined);
112
- const categories = ELECTRON_SENSITIVE_APP_CATEGORY_PATTERNS
113
- .filter(({ patterns }) => metadataValues.some((value) => patterns.some((pattern) => pattern.test(value))))
114
- .map(({ category }) => category);
115
- if (categories.length === 0) return undefined;
116
- return {
117
- categories: [...new Set(categories)].sort(),
118
- level: "likely-sensitive",
119
- reason: "App name, bundle id, desktop id, or path matched common private-data app patterns; discovery still does not enforce policy.",
120
- };
121
- }
122
-
123
- function annotateElectronAppSensitivity(app: ElectronAppDiscovery): ElectronAppDiscovery {
124
- const sensitivity = getElectronAppSensitivity(app);
125
- return sensitivity ? { ...app, sensitivity } : app;
126
- }
127
-
128
- function normalizeMaxResults(maxResults: number | undefined): number {
129
- if (typeof maxResults !== "number" || !Number.isInteger(maxResults) || maxResults <= 0) {
130
- return ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS;
131
- }
132
- return Math.min(maxResults, ELECTRON_DISCOVERY_MAX_RESULTS);
133
- }
134
-
135
- function resolveLocations(locations: ElectronDiscoveryScanLocations | undefined): ResolvedElectronDiscoveryLocations {
136
- const homeDir = locations?.homeDir ?? homedir();
137
- return {
138
- darwinApplicationDirectories: locations?.darwinApplicationDirectories ?? ["/Applications", join(homeDir, "Applications")],
139
- flatpakSystemAppDirectory: locations?.flatpakSystemAppDirectory ?? "/var/lib/flatpak/app",
140
- flatpakUserAppDirectory: locations?.flatpakUserAppDirectory ?? join(homeDir, ".local", "share", "flatpak", "app"),
141
- homeDir,
142
- linuxDesktopDirectories: locations?.linuxDesktopDirectories ?? [
143
- join(homeDir, ".local", "share", "applications"),
144
- "/usr/share/applications",
145
- "/var/lib/snapd/desktop/applications",
146
- join(homeDir, ".local", "share", "flatpak", "exports", "share", "applications"),
147
- "/var/lib/flatpak/exports/share/applications",
148
- ],
149
- pathEnv: locations?.pathEnv ?? process.env.PATH ?? "",
150
- snapBinDirectory: locations?.snapBinDirectory ?? "/snap/bin",
151
- snapMountDirectory: locations?.snapMountDirectory ?? "/snap",
152
- };
153
- }
154
-
155
- async function isDirectory(path: string): Promise<boolean> {
156
- try {
157
- return (await stat(path)).isDirectory();
158
- } catch {
159
- return false;
160
- }
161
- }
162
-
163
- async function isFile(path: string): Promise<boolean> {
164
- try {
165
- return (await stat(path)).isFile();
166
- } catch {
167
- return false;
168
- }
169
- }
170
-
171
- async function isExecutableFile(path: string): Promise<boolean> {
172
- try {
173
- const metadata = await stat(path);
174
- if (!metadata.isFile()) return false;
175
- await access(path, fsConstants.X_OK);
176
- return true;
177
- } catch {
178
- return false;
179
- }
180
- }
181
-
182
- async function resolveRealPath(path: string): Promise<string> {
183
- try {
184
- return await realpath(path);
185
- } catch {
186
- return path;
187
- }
188
- }
189
-
190
- function escapeRegExp(value: string): string {
191
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
192
- }
193
-
194
- function decodeXmlEntities(value: string): string {
195
- return value
196
- .replaceAll("&amp;", "&")
197
- .replaceAll("&lt;", "<")
198
- .replaceAll("&gt;", ">")
199
- .replaceAll("&quot;", '"')
200
- .replaceAll("&apos;", "'");
201
- }
202
-
203
- function readPlistString(plist: string, key: string): string | undefined {
204
- const pattern = new RegExp(`<key>\\s*${escapeRegExp(key)}\\s*</key>\\s*<string>([\\s\\S]*?)</string>`, "i");
205
- const match = pattern.exec(plist);
206
- return match ? decodeXmlEntities(match[1]?.trim() ?? "") : undefined;
207
- }
208
-
209
- async function readMacInfoPlist(appPath: string): Promise<Record<string, string>> {
210
- try {
211
- const plist = await readFile(join(appPath, "Contents", "Info.plist"), "utf8");
212
- return {
213
- CFBundleDisplayName: readPlistString(plist, "CFBundleDisplayName") ?? "",
214
- CFBundleExecutable: readPlistString(plist, "CFBundleExecutable") ?? "",
215
- CFBundleIdentifier: readPlistString(plist, "CFBundleIdentifier") ?? "",
216
- CFBundleName: readPlistString(plist, "CFBundleName") ?? "",
217
- };
218
- } catch {
219
- return {};
220
- }
221
- }
222
-
223
- async function resolveMacExecutablePath(appPath: string, executableName: string | undefined, fallbackName: string): Promise<string | undefined> {
224
- const macOsDirectory = join(appPath, "Contents", "MacOS");
225
- if (executableName && executableName.trim().length > 0) {
226
- const executablePath = join(macOsDirectory, executableName);
227
- return await pathExists(executablePath) ? executablePath : undefined;
228
- }
229
- try {
230
- const entries = await readdir(macOsDirectory, { withFileTypes: true });
231
- const exact = entries.find((entry) => entry.isFile() && entry.name === fallbackName);
232
- const candidate = exact ?? entries.find((entry) => entry.isFile());
233
- return candidate ? join(macOsDirectory, candidate.name) : undefined;
234
- } catch {
235
- return undefined;
236
- }
237
- }
238
-
239
- export async function inspectDarwinApp(appPath: string): Promise<ElectronAppDiscovery | undefined> {
240
- const frameworkPath = join(appPath, "Contents", "Frameworks", "Electron Framework.framework");
241
- const resourcesPath = join(appPath, "Contents", "Resources");
242
- const hasElectronFramework = await isDirectory(frameworkPath);
243
- const hasAppPayload = await pathExists(join(resourcesPath, "app.asar")) || await isDirectory(join(resourcesPath, "app"));
244
- if (!hasElectronFramework || !hasAppPayload) return undefined;
245
-
246
- const info = await readMacInfoPlist(appPath);
247
- const appDirectoryName = basename(appPath, ".app");
248
- const executablePath = await resolveMacExecutablePath(appPath, info.CFBundleExecutable, appDirectoryName);
249
- if (!executablePath) return undefined;
250
- const name = info.CFBundleDisplayName || info.CFBundleName || appDirectoryName;
251
- return {
252
- appPath,
253
- bundleId: info.CFBundleIdentifier || undefined,
254
- executablePath,
255
- name,
256
- platform: "darwin",
257
- };
258
- }
259
-
260
- async function discoverDarwinApps(locations: ResolvedElectronDiscoveryLocations): Promise<{ apps: ElectronAppDiscovery[]; skippedCount: number }> {
261
- const apps: ElectronAppDiscovery[] = [];
262
- let skippedCount = 0;
263
- for (const directory of locations.darwinApplicationDirectories) {
264
- let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
265
- try {
266
- entries = await readdir(directory, { withFileTypes: true });
267
- } catch {
268
- continue;
269
- }
270
- for (const entry of entries) {
271
- if (!entry.name.endsWith(".app")) continue;
272
- const appPath = join(directory, entry.name);
273
- try {
274
- const app = await inspectDarwinApp(appPath);
275
- if (app) apps.push(app);
276
- } catch {
277
- skippedCount += 1;
278
- }
279
- }
280
- }
281
- return { apps, skippedCount };
282
- }
283
-
284
- function unescapeDesktopValue(value: string): string {
285
- return value
286
- .replace(/\\s/g, " ")
287
- .replace(/\\n/g, "\n")
288
- .replace(/\\r/g, "\r")
289
- .replace(/\\t/g, "\t")
290
- .replace(/\\\\/g, "\\");
291
- }
292
-
293
- function parseDesktopBoolean(value: string | undefined): boolean {
294
- return value?.trim().toLowerCase() === "true";
295
- }
296
-
297
- function parseDesktopFile(text: string, filePath: string): LinuxDesktopEntry | undefined {
298
- const fields = new Map<string, string>();
299
- let inDesktopEntry = false;
300
- for (const rawLine of text.split(/\r?\n/)) {
301
- const line = rawLine.trim();
302
- if (line.length === 0 || line.startsWith("#")) continue;
303
- if (line.startsWith("[") && line.endsWith("]")) {
304
- inDesktopEntry = line === "[Desktop Entry]";
305
- continue;
306
- }
307
- if (!inDesktopEntry) continue;
308
- const separatorIndex = line.indexOf("=");
309
- if (separatorIndex <= 0) continue;
310
- const key = line.slice(0, separatorIndex);
311
- const value = unescapeDesktopValue(line.slice(separatorIndex + 1));
312
- fields.set(key, value);
313
- }
314
-
315
- if (fields.get("Type") !== "Application") return undefined;
316
- if (parseDesktopBoolean(fields.get("NoDisplay")) || parseDesktopBoolean(fields.get("Hidden"))) return undefined;
317
- const exec = fields.get("Exec")?.trim();
318
- if (!exec) return undefined;
319
- const desktopId = basename(filePath, ".desktop");
320
- return {
321
- comment: fields.get("Comment") || undefined,
322
- desktopId,
323
- exec,
324
- filePath,
325
- icon: fields.get("Icon") || undefined,
326
- name: fields.get("Name") || desktopId,
327
- };
328
- }
329
-
330
- function stripDesktopExecFieldCodes(exec: string): string {
331
- const placeholder = "\u0000PERCENT\u0000";
332
- return exec
333
- .replaceAll("%%", placeholder)
334
- .replace(/%[A-Za-z]/g, "")
335
- .replaceAll(placeholder, "%");
336
- }
337
-
338
- function tokenizeDesktopExec(exec: string): string[] {
339
- const tokens: string[] = [];
340
- let current = "";
341
- let quote: '"' | "'" | undefined;
342
- let escaped = false;
343
- for (const char of stripDesktopExecFieldCodes(exec)) {
344
- if (escaped) {
345
- current += char;
346
- escaped = false;
347
- continue;
348
- }
349
- if (char === "\\") {
350
- escaped = true;
351
- continue;
352
- }
353
- if (quote) {
354
- if (char === quote) {
355
- quote = undefined;
356
- } else {
357
- current += char;
358
- }
359
- continue;
360
- }
361
- if (char === '"' || char === "'") {
362
- quote = char;
363
- continue;
364
- }
365
- if (/\s/.test(char)) {
366
- if (current.length > 0) {
367
- tokens.push(current);
368
- current = "";
369
- }
370
- continue;
371
- }
372
- current += char;
373
- }
374
- if (escaped) current += "\\";
375
- if (current.length > 0) tokens.push(current);
376
- return tokens;
377
- }
378
-
379
- function stripEnvLauncher(tokens: string[]): string[] {
380
- if (tokens.length === 0 || basename(tokens[0] ?? "") !== "env") return tokens;
381
- let index = 1;
382
- while (index < tokens.length) {
383
- const token = tokens[index] ?? "";
384
- if (token.startsWith("-")) {
385
- index += token === "-u" || token === "--unset" || token === "--chdir" ? 2 : 1;
386
- continue;
387
- }
388
- if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token)) {
389
- index += 1;
390
- continue;
391
- }
392
- break;
393
- }
394
- return tokens.slice(index);
395
- }
396
-
397
- async function directoryContainsChromePak(directory: string): Promise<boolean> {
398
- try {
399
- const entries = await readdir(directory, { withFileTypes: true });
400
- return entries.some((entry) => entry.isFile() && /^chrome_.*\.pak$/i.test(entry.name));
401
- } catch {
402
- return false;
403
- }
404
- }
405
-
406
- export async function hasLinuxElectronEvidence(executablePath: string): Promise<boolean> {
407
- const resolvedExecutablePath = await resolveRealPath(executablePath);
408
- if (!await isExecutableFile(resolvedExecutablePath)) return false;
409
- const executableDirectory = dirname(resolvedExecutablePath);
410
- if (!await directoryContainsChromePak(executableDirectory)) return false;
411
- const resourceBases = [executableDirectory, dirname(executableDirectory)];
412
- for (const base of resourceBases) {
413
- const resourcesDirectory = join(base, "resources");
414
- if (await pathExists(join(resourcesDirectory, "app.asar")) || await isDirectory(join(resourcesDirectory, "app"))) {
415
- return true;
416
- }
417
- }
418
- return false;
419
- }
420
-
421
- async function findExecutableInPath(command: string, pathEnv: string): Promise<string | undefined> {
422
- if (command.includes("/")) return undefined;
423
- for (const directory of pathEnv.split(":")) {
424
- if (directory.length === 0) continue;
425
- const candidate = join(directory, command);
426
- if (await isFile(candidate)) return candidate;
427
- }
428
- return undefined;
429
- }
430
-
431
- function pathIsWithin(path: string, parent: string): boolean {
432
- const relativePath = relative(resolve(parent), resolve(path));
433
- return relativePath.length === 0 || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
434
- }
435
-
436
- function isLikelyExecutableCandidate(path: string): boolean {
437
- return !LINUX_NON_EXECUTABLE_CANDIDATE_EXTENSIONS.has(extname(path).toLowerCase());
438
- }
439
-
440
- async function findElectronBinaryUnder(root: string, preferredNames: string[]): Promise<string | undefined> {
441
- if (!await isDirectory(root)) return undefined;
442
- const preferredNameSet = new Set(preferredNames.filter((name) => name.length > 0));
443
- const allFileCandidates: string[] = [];
444
- let visitedEntries = 0;
445
-
446
- async function visit(directory: string, depth: number): Promise<string | undefined> {
447
- if (depth > LINUX_ELECTRON_CANDIDATE_MAX_DEPTH || visitedEntries >= LINUX_ELECTRON_CANDIDATE_MAX_ENTRIES) return undefined;
448
- let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
449
- try {
450
- entries = await readdir(directory, { withFileTypes: true });
451
- } catch {
452
- return undefined;
453
- }
454
- for (const entry of entries) {
455
- visitedEntries += 1;
456
- if (visitedEntries > LINUX_ELECTRON_CANDIDATE_MAX_ENTRIES) return undefined;
457
- const path = join(directory, entry.name);
458
- if (entry.isDirectory()) {
459
- const found = await visit(path, depth + 1);
460
- if (found) return found;
461
- } else if (entry.isFile() && isLikelyExecutableCandidate(path)) {
462
- if (preferredNameSet.has(entry.name) && await hasLinuxElectronEvidence(path)) return await resolveRealPath(path);
463
- allFileCandidates.push(path);
464
- }
465
- }
466
- return undefined;
467
- }
468
-
469
- const preferredMatch = await visit(root, 0);
470
- if (preferredMatch) return preferredMatch;
471
- for (const candidate of allFileCandidates) {
472
- if (await hasLinuxElectronEvidence(candidate)) return await resolveRealPath(candidate);
473
- }
474
- return undefined;
475
- }
476
-
477
- function getSnapCandidateNames(commandName: string, desktopId: string): string[] {
478
- const names = [commandName, commandName.split(".")[0] ?? "", desktopId.split(".")[0] ?? ""];
479
- return [...new Set(names.filter((name) => name.length > 0))];
480
- }
481
-
482
- async function resolveSnapExecutable(commandPath: string, entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): Promise<string | undefined> {
483
- const commandName = basename(commandPath);
484
- for (const snapName of getSnapCandidateNames(commandName, entry.desktopId)) {
485
- const root = join(locations.snapMountDirectory, snapName, "current");
486
- const candidate = await findElectronBinaryUnder(root, [commandName, commandName.split(".").at(-1) ?? commandName]);
487
- if (candidate) return candidate;
488
- }
489
- return undefined;
490
- }
491
-
492
- function getFlatpakAppId(tokens: string[], desktopId: string): string {
493
- for (let index = tokens.length - 1; index >= 0; index -= 1) {
494
- const token = tokens[index] ?? "";
495
- if (!token.startsWith("-") && token.includes(".") && token !== "flatpak") return token;
496
- }
497
- return desktopId;
498
- }
499
-
500
- function getFlatpakCommandName(tokens: string[]): string | undefined {
501
- for (let index = 0; index < tokens.length; index += 1) {
502
- const token = tokens[index] ?? "";
503
- if (token.startsWith("--command=")) return token.slice("--command=".length);
504
- if (token === "--command") return tokens[index + 1];
505
- }
506
- return undefined;
507
- }
508
-
509
- function getFlatpakRoots(entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): string[] {
510
- const userExportDirectory = join(locations.homeDir, ".local", "share", "flatpak", "exports", "share", "applications");
511
- const systemExportDirectory = "/var/lib/flatpak/exports/share/applications";
512
- if (pathIsWithin(entry.filePath, userExportDirectory)) {
513
- return [locations.flatpakUserAppDirectory, locations.flatpakSystemAppDirectory];
514
- }
515
- if (pathIsWithin(entry.filePath, systemExportDirectory)) {
516
- return [locations.flatpakSystemAppDirectory, locations.flatpakUserAppDirectory];
517
- }
518
- return [locations.flatpakUserAppDirectory, locations.flatpakSystemAppDirectory];
519
- }
520
-
521
- async function resolveFlatpakExecutable(tokens: string[], entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): Promise<string | undefined> {
522
- const appId = getFlatpakAppId(tokens, entry.desktopId);
523
- const commandName = getFlatpakCommandName(tokens);
524
- const preferredNames = commandName ? [commandName] : [];
525
- for (const root of getFlatpakRoots(entry, locations)) {
526
- const filesRoot = join(root, appId, "current", "active", "files");
527
- const candidate = await findElectronBinaryUnder(filesRoot, preferredNames);
528
- if (candidate) return candidate;
529
- }
530
- return undefined;
531
- }
532
-
533
- async function resolveLinuxExecutable(entry: LinuxDesktopEntry, locations: ResolvedElectronDiscoveryLocations): Promise<LinuxExecutableResolution | undefined> {
534
- const tokens = stripEnvLauncher(tokenizeDesktopExec(entry.exec));
535
- const executableToken = tokens[0];
536
- if (!executableToken) return undefined;
537
-
538
- if (basename(executableToken) === "flatpak") {
539
- const executablePath = await resolveFlatpakExecutable(tokens, entry, locations);
540
- return executablePath ? { executablePath, packageSource: "flatpak" } : undefined;
541
- }
542
-
543
- let executablePath = executableToken;
544
- if (!isAbsolute(executablePath)) {
545
- const pathExecutable = await findExecutableInPath(executablePath, locations.pathEnv);
546
- if (!pathExecutable) return undefined;
547
- executablePath = pathExecutable;
548
- }
549
-
550
- if (pathIsWithin(executablePath, locations.snapBinDirectory)) {
551
- const snapExecutable = await resolveSnapExecutable(executablePath, entry, locations);
552
- return snapExecutable ? { executablePath: snapExecutable, packageSource: "snap" } : undefined;
553
- }
554
-
555
- return { executablePath: await resolveRealPath(executablePath), packageSource: "desktop" };
556
- }
557
-
558
- async function inspectLinuxDesktopFile(filePath: string, locations: ResolvedElectronDiscoveryLocations): Promise<ElectronAppDiscovery | undefined> {
559
- let text: string;
560
- try {
561
- text = await readFile(filePath, "utf8");
562
- } catch {
563
- return undefined;
564
- }
565
- const entry = parseDesktopFile(text, filePath);
566
- if (!entry) return undefined;
567
- const resolution = await resolveLinuxExecutable(entry, locations);
568
- if (!resolution) return undefined;
569
- if (!await hasLinuxElectronEvidence(resolution.executablePath)) return undefined;
570
- return {
571
- comment: entry.comment,
572
- desktopId: entry.desktopId,
573
- executablePath: resolution.executablePath,
574
- icon: entry.icon,
575
- name: entry.name,
576
- packageSource: resolution.packageSource,
577
- platform: "linux",
578
- };
579
- }
580
-
581
- async function discoverLinuxApps(locations: ResolvedElectronDiscoveryLocations): Promise<{ apps: ElectronAppDiscovery[]; skippedCount: number }> {
582
- const apps: ElectronAppDiscovery[] = [];
583
- let skippedCount = 0;
584
- for (const directory of locations.linuxDesktopDirectories) {
585
- let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
586
- try {
587
- entries = await readdir(directory, { withFileTypes: true });
588
- } catch {
589
- continue;
590
- }
591
- for (const entry of entries) {
592
- if (!entry.isFile() || extname(entry.name) !== ".desktop") continue;
593
- try {
594
- const app = await inspectLinuxDesktopFile(join(directory, entry.name), locations);
595
- if (app) apps.push(app);
596
- } catch {
597
- skippedCount += 1;
598
- }
599
- }
600
- }
601
- return { apps, skippedCount };
602
- }
603
-
604
- function appMatchesQuery(app: ElectronAppDiscovery, query: string | undefined): boolean {
605
- if (!query) return true;
606
- const normalizedQuery = query.toLowerCase();
607
- const searchableValues = [app.name, app.bundleId, app.appPath, app.executablePath, app.comment, app.desktopId, app.icon, app.packageSource]
608
- .filter((value): value is string => typeof value === "string" && value.length > 0)
609
- .map((value) => value.toLowerCase());
610
- return searchableValues.some((value) => value.includes(normalizedQuery));
611
- }
612
-
613
- function dedupeApps(apps: ElectronAppDiscovery[]): ElectronAppDiscovery[] {
614
- const seen = new Set<string>();
615
- const deduped: ElectronAppDiscovery[] = [];
616
- for (const app of apps) {
617
- const key = app.appPath ?? app.executablePath;
618
- if (seen.has(key)) continue;
619
- seen.add(key);
620
- deduped.push(app);
621
- }
622
- return deduped;
623
- }
624
-
625
- function sortApps(apps: ElectronAppDiscovery[]): ElectronAppDiscovery[] {
626
- return [...apps].sort((left, right) => {
627
- const nameComparison = left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
628
- return nameComparison === 0 ? left.executablePath.localeCompare(right.executablePath) : nameComparison;
629
- });
630
- }
631
-
632
- function findMacAppBundleAncestor(path: string): string | undefined {
633
- const parts = resolve(path).split(/[\\/]+/);
634
- const appIndex = parts.findIndex((part) => part.endsWith(".app"));
635
- return appIndex >= 0 ? parts.slice(0, appIndex + 1).join("/") || "/" : undefined;
636
- }
637
-
638
- async function inspectWin32Executable(executablePath: string): Promise<ElectronAppDiscovery | undefined> {
639
- const resolvedExecutablePath = await resolveRealPath(executablePath);
640
- if (!await isExecutableFile(resolvedExecutablePath)) return undefined;
641
- const executableDirectory = dirname(resolvedExecutablePath);
642
- const resourcesDirectory = join(executableDirectory, "resources");
643
- const hasAppPayload = await pathExists(join(resourcesDirectory, "app.asar")) || await isDirectory(join(resourcesDirectory, "app"));
644
- const hasPak = await directoryContainsChromePak(executableDirectory) || await pathExists(join(executableDirectory, "resources.pak"));
645
- if (!hasAppPayload || !hasPak) return undefined;
646
- return {
647
- executablePath: resolvedExecutablePath,
648
- name: basename(resolvedExecutablePath, extname(resolvedExecutablePath)),
649
- platform: "win32",
650
- };
651
- }
652
-
653
- export async function inspectElectronExecutablePath(
654
- executablePath: string,
655
- platform: NodeJS.Platform | ElectronDiscoveryPlatform = process.platform,
656
- ): Promise<ElectronAppDiscovery | undefined> {
657
- const resolvedExecutablePath = await resolveRealPath(executablePath);
658
- if (platform === "darwin") {
659
- const appPath = findMacAppBundleAncestor(resolvedExecutablePath);
660
- return appPath ? inspectDarwinApp(appPath) : undefined;
661
- }
662
- if (platform === "linux") {
663
- if (!await hasLinuxElectronEvidence(resolvedExecutablePath)) return undefined;
664
- return {
665
- executablePath: resolvedExecutablePath,
666
- name: basename(resolvedExecutablePath),
667
- platform: "linux",
668
- };
669
- }
670
- if (platform === "win32") {
671
- return inspectWin32Executable(resolvedExecutablePath);
672
- }
673
- return undefined;
674
- }
675
-
676
- export async function inspectElectronAppPath(
677
- appPath: string,
678
- platform: NodeJS.Platform | ElectronDiscoveryPlatform = process.platform,
679
- ): Promise<ElectronAppDiscovery | undefined> {
680
- if (platform === "darwin" || appPath.endsWith(".app")) {
681
- return inspectDarwinApp(appPath);
682
- }
683
- return inspectElectronExecutablePath(appPath, platform);
684
- }
685
-
686
- export async function discoverElectronApps(options: DiscoverElectronAppsOptions = {}): Promise<ElectronDiscoveryResult> {
687
- const platform = options.platform ?? process.platform;
688
- const query = options.query?.trim() || undefined;
689
- const maxResults = normalizeMaxResults(options.maxResults);
690
- const locations = resolveLocations(options.locations);
691
- let discovered: { apps: ElectronAppDiscovery[]; skippedCount: number };
692
- if (platform === "darwin") {
693
- discovered = await discoverDarwinApps(locations);
694
- } else if (platform === "linux") {
695
- discovered = await discoverLinuxApps(locations);
696
- } else {
697
- return { apps: [], maxResults, omittedCount: 0, platform: "unsupported", query };
698
- }
699
-
700
- const filteredApps = sortApps(dedupeApps(discovered.apps.map(annotateElectronAppSensitivity).filter((app) => appMatchesQuery(app, query))));
701
- const apps = filteredApps.slice(0, maxResults);
702
- return {
703
- apps,
704
- maxResults,
705
- omittedCount: Math.max(0, filteredApps.length - apps.length),
706
- platform,
707
- query,
708
- skippedCount: discovered.skippedCount || undefined,
709
- };
710
- }