pi-agent-browser-native 0.2.47 → 0.2.49

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 (185) hide show
  1. package/CHANGELOG.md +63 -19
  2. package/README.md +52 -19
  3. package/dist/extensions/agent-browser/index.js +785 -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 +686 -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 +448 -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 +960 -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 +816 -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 +10 -10
  95. package/docs/COMMAND_REFERENCE.md +35 -21
  96. package/docs/ELECTRON.md +3 -3
  97. package/docs/RELEASE.md +46 -26
  98. package/docs/REQUIREMENTS.md +1 -1
  99. package/docs/SUPPORT_MATRIX.md +35 -106
  100. package/docs/TOOL_CONTRACT.md +23 -21
  101. package/package.json +12 -8
  102. package/scripts/agent-browser-capability-baseline.mjs +6 -3
  103. package/scripts/config.mjs +8 -2
  104. package/scripts/doctor.mjs +19 -17
  105. package/scripts/platform-smoke.mjs +1 -1
  106. package/extensions/agent-browser/index.ts +0 -952
  107. package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
  108. package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
  109. package/extensions/agent-browser/lib/bash-guard.ts +0 -205
  110. package/extensions/agent-browser/lib/command-policy.ts +0 -71
  111. package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
  112. package/extensions/agent-browser/lib/config-policy.js +0 -690
  113. package/extensions/agent-browser/lib/config.ts +0 -209
  114. package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
  115. package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
  116. package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
  117. package/extensions/agent-browser/lib/electron/launch.ts +0 -499
  118. package/extensions/agent-browser/lib/executable-path.ts +0 -19
  119. package/extensions/agent-browser/lib/fs-utils.ts +0 -18
  120. package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
  121. package/extensions/agent-browser/lib/input-modes/job.ts +0 -451
  122. package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
  123. package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
  124. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
  125. package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
  126. package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
  127. package/extensions/agent-browser/lib/input-modes.ts +0 -45
  128. package/extensions/agent-browser/lib/json-schema.ts +0 -73
  129. package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
  130. package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
  131. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
  132. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -257
  133. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -912
  134. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -512
  135. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
  136. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -1481
  137. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -564
  138. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
  139. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
  140. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -564
  141. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
  142. package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
  143. package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
  144. package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -252
  145. package/extensions/agent-browser/lib/playbook.ts +0 -142
  146. package/extensions/agent-browser/lib/process.ts +0 -516
  147. package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
  148. package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
  149. package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
  150. package/extensions/agent-browser/lib/results/categories.ts +0 -106
  151. package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
  152. package/extensions/agent-browser/lib/results/contracts.ts +0 -241
  153. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
  154. package/extensions/agent-browser/lib/results/envelope.ts +0 -195
  155. package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
  156. package/extensions/agent-browser/lib/results/network.ts +0 -78
  157. package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
  158. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
  159. package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
  160. package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
  161. package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
  162. package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
  163. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
  164. package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
  165. package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
  166. package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
  167. package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
  168. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
  169. package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
  170. package/extensions/agent-browser/lib/results/presentation.ts +0 -257
  171. package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
  172. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
  173. package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
  174. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
  175. package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
  176. package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
  177. package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
  178. package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
  179. package/extensions/agent-browser/lib/results/text.ts +0 -40
  180. package/extensions/agent-browser/lib/runtime.ts +0 -988
  181. package/extensions/agent-browser/lib/session-page-state.ts +0 -512
  182. package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
  183. package/extensions/agent-browser/lib/temp.ts +0 -577
  184. package/extensions/agent-browser/lib/web-search.ts +0 -721
  185. /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
- }