@vybestack/llxprt-code-core 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7

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 (168) hide show
  1. package/dist/src/config/config.d.ts +20 -0
  2. package/dist/src/config/config.js +143 -23
  3. package/dist/src/config/config.js.map +1 -1
  4. package/dist/src/config/configBaseCore.d.ts +6 -2
  5. package/dist/src/config/configBaseCore.js +8 -0
  6. package/dist/src/config/configBaseCore.js.map +1 -1
  7. package/dist/src/config/configConstructor.d.ts +2 -0
  8. package/dist/src/config/configConstructor.js +9 -1
  9. package/dist/src/config/configConstructor.js.map +1 -1
  10. package/dist/src/config/configTestHarness.d.ts +8 -6
  11. package/dist/src/config/configTestHarness.js +10 -4
  12. package/dist/src/config/configTestHarness.js.map +1 -1
  13. package/dist/src/config/configTypes.d.ts +7 -2
  14. package/dist/src/config/configTypes.js +2 -0
  15. package/dist/src/config/configTypes.js.map +1 -1
  16. package/dist/src/config/liveTrustTransitionLifecycle.d.ts +35 -0
  17. package/dist/src/config/liveTrustTransitionLifecycle.js +180 -0
  18. package/dist/src/config/liveTrustTransitionLifecycle.js.map +1 -0
  19. package/dist/src/config/schedulerSingleton.d.ts +2 -2
  20. package/dist/src/config/schedulerSingleton.js.map +1 -1
  21. package/dist/src/core/compression/types.d.ts +48 -1
  22. package/dist/src/core/compression/types.js +41 -0
  23. package/dist/src/core/compression/types.js.map +1 -1
  24. package/dist/src/core/model-limits.json +96 -0
  25. package/dist/src/core/model-limits.schema.d.ts +123 -0
  26. package/dist/src/core/model-limits.schema.js +75 -0
  27. package/dist/src/core/model-limits.schema.js.map +1 -0
  28. package/dist/src/core/tokenLimits.d.ts +6 -8
  29. package/dist/src/core/tokenLimits.js +56 -132
  30. package/dist/src/core/tokenLimits.js.map +1 -1
  31. package/dist/src/core/turn.d.ts +20 -3
  32. package/dist/src/core/turn.js +20 -3
  33. package/dist/src/core/turn.js.map +1 -1
  34. package/dist/src/hooks/hookRegistry.d.ts +1 -1
  35. package/dist/src/hooks/hookRegistry.js +3 -1
  36. package/dist/src/hooks/hookRegistry.js.map +1 -1
  37. package/dist/src/hooks/hookRunner.d.ts +6 -0
  38. package/dist/src/hooks/hookRunner.js +22 -6
  39. package/dist/src/hooks/hookRunner.js.map +1 -1
  40. package/dist/src/hooks/hookSystem.d.ts +10 -1
  41. package/dist/src/hooks/hookSystem.js +69 -12
  42. package/dist/src/hooks/hookSystem.js.map +1 -1
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.js +1 -0
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/llm-types/geminiContent.d.ts +3 -0
  47. package/dist/src/policy/config.d.ts +4 -2
  48. package/dist/src/policy/config.js +22 -18
  49. package/dist/src/policy/config.js.map +1 -1
  50. package/dist/src/recording/ReplayEngine.js +39 -2
  51. package/dist/src/recording/ReplayEngine.js.map +1 -1
  52. package/dist/src/recording/SessionDiscovery.d.ts +8 -0
  53. package/dist/src/recording/SessionDiscovery.js +88 -3
  54. package/dist/src/recording/SessionDiscovery.js.map +1 -1
  55. package/dist/src/recording/SessionLockManager.d.ts +3 -0
  56. package/dist/src/recording/SessionLockManager.js +9 -3
  57. package/dist/src/recording/SessionLockManager.js.map +1 -1
  58. package/dist/src/recording/SessionRecordingService.d.ts +14 -1
  59. package/dist/src/recording/SessionRecordingService.js +27 -3
  60. package/dist/src/recording/SessionRecordingService.js.map +1 -1
  61. package/dist/src/recording/index.d.ts +4 -3
  62. package/dist/src/recording/index.js +4 -3
  63. package/dist/src/recording/index.js.map +1 -1
  64. package/dist/src/recording/replay-test-helpers.d.ts +5 -0
  65. package/dist/src/recording/replay-test-helpers.js +13 -0
  66. package/dist/src/recording/replay-test-helpers.js.map +1 -1
  67. package/dist/src/recording/resumeNotFoundMessages.d.ts +56 -0
  68. package/dist/src/recording/resumeNotFoundMessages.js +64 -0
  69. package/dist/src/recording/resumeNotFoundMessages.js.map +1 -0
  70. package/dist/src/recording/resumeSession.js +3 -2
  71. package/dist/src/recording/resumeSession.js.map +1 -1
  72. package/dist/src/recording/sessionManagement.d.ts +3 -0
  73. package/dist/src/recording/sessionManagement.js +38 -19
  74. package/dist/src/recording/sessionManagement.js.map +1 -1
  75. package/dist/src/recording/types.d.ts +42 -3
  76. package/dist/src/recording/types.js +1 -1
  77. package/dist/src/recording/types.js.map +1 -1
  78. package/dist/src/runtime/AgentRuntimeLoader.js +2 -1
  79. package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -1
  80. package/dist/src/scheduler/liveOutput.d.ts +10 -19
  81. package/dist/src/scheduler/liveOutput.js +21 -21
  82. package/dist/src/scheduler/liveOutput.js.map +1 -1
  83. package/dist/src/scheduler/types.d.ts +2 -2
  84. package/dist/src/services/history/ContentConverters.d.ts +4 -0
  85. package/dist/src/services/history/ContentConverters.js +77 -59
  86. package/dist/src/services/history/ContentConverters.js.map +1 -1
  87. package/dist/src/services/history/HistoryService.d.ts +13 -1
  88. package/dist/src/services/history/HistoryService.js +71 -0
  89. package/dist/src/services/history/HistoryService.js.map +1 -1
  90. package/dist/src/services/history/IContent.d.ts +10 -0
  91. package/dist/src/services/history/IContent.js.map +1 -1
  92. package/dist/src/skills/skillManager.d.ts +11 -1
  93. package/dist/src/skills/skillManager.js +21 -1
  94. package/dist/src/skills/skillManager.js.map +1 -1
  95. package/dist/src/test-utils/mock-tool.d.ts +3 -3
  96. package/dist/src/test-utils/mock-tool.js.map +1 -1
  97. package/dist/src/test-utils/tools.d.ts +2 -2
  98. package/dist/src/test-utils/tools.js.map +1 -1
  99. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js +1 -6
  100. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js.map +1 -1
  101. package/dist/src/utils/browser-profile-discovery.d.ts +40 -0
  102. package/dist/src/utils/browser-profile-discovery.js +251 -0
  103. package/dist/src/utils/browser-profile-discovery.js.map +1 -0
  104. package/dist/src/utils/env.d.ts +12 -0
  105. package/dist/src/utils/env.js +15 -0
  106. package/dist/src/utils/env.js.map +1 -0
  107. package/dist/src/utils/events.d.ts +10 -1
  108. package/dist/src/utils/events.js +22 -0
  109. package/dist/src/utils/events.js.map +1 -1
  110. package/dist/src/utils/fileUtils.js +8 -1
  111. package/dist/src/utils/fileUtils.js.map +1 -1
  112. package/dist/src/utils/retry.js +3 -4
  113. package/dist/src/utils/retry.js.map +1 -1
  114. package/dist/src/utils/secure-browser-launcher.d.ts +35 -1
  115. package/dist/src/utils/secure-browser-launcher.js +341 -1
  116. package/dist/src/utils/secure-browser-launcher.js.map +1 -1
  117. package/dist/src/utils/terminalSerializer.d.ts +12 -0
  118. package/dist/src/utils/terminalSerializer.js.map +1 -1
  119. package/dist/src/utils/thoughtUtils.d.ts +2 -0
  120. package/dist/src/utils/thoughtUtils.js.map +1 -1
  121. package/package.json +13 -9
  122. package/src/config/config.ts +161 -22
  123. package/src/config/configBaseCore.ts +10 -2
  124. package/src/config/configConstructor.ts +11 -0
  125. package/src/config/configTestHarness.ts +10 -4
  126. package/src/config/configTypes.ts +6 -1
  127. package/src/config/liveTrustTransitionLifecycle.ts +250 -0
  128. package/src/config/schedulerSingleton.ts +2 -5
  129. package/src/core/compression/types.ts +81 -1
  130. package/src/core/legacy-model-limits.expected.txt +96 -0
  131. package/src/core/model-limits.json +96 -0
  132. package/src/core/model-limits.schema.ts +87 -0
  133. package/src/core/tokenLimits.ts +73 -147
  134. package/src/core/turn.ts +20 -3
  135. package/src/hooks/hookRegistry.ts +3 -1
  136. package/src/hooks/hookRunner.ts +24 -7
  137. package/src/hooks/hookSystem.ts +73 -13
  138. package/src/index.ts +1 -0
  139. package/src/llm-types/geminiContent.ts +3 -0
  140. package/src/policy/config.ts +32 -19
  141. package/src/recording/ReplayEngine.ts +51 -2
  142. package/src/recording/SessionDiscovery.ts +99 -4
  143. package/src/recording/SessionLockManager.ts +10 -3
  144. package/src/recording/SessionRecordingService.ts +36 -3
  145. package/src/recording/index.ts +19 -2
  146. package/src/recording/replay-test-helpers.ts +14 -0
  147. package/src/recording/resumeNotFoundMessages.ts +75 -0
  148. package/src/recording/resumeSession.ts +3 -1
  149. package/src/recording/sessionManagement.ts +51 -21
  150. package/src/recording/types.ts +44 -2
  151. package/src/runtime/AgentRuntimeLoader.ts +3 -1
  152. package/src/scheduler/liveOutput.ts +25 -22
  153. package/src/scheduler/types.ts +5 -2
  154. package/src/services/history/ContentConverters.ts +91 -68
  155. package/src/services/history/HistoryService.ts +92 -1
  156. package/src/services/history/IContent.ts +13 -0
  157. package/src/skills/skillManager.ts +26 -1
  158. package/src/test-utils/mock-tool.ts +4 -3
  159. package/src/test-utils/tools.ts +4 -3
  160. package/src/tools-adapters/CoreShellToolHostAdapter.ts +1 -7
  161. package/src/utils/browser-profile-discovery.ts +358 -0
  162. package/src/utils/env.ts +15 -0
  163. package/src/utils/events.ts +31 -0
  164. package/src/utils/fileUtils.ts +11 -1
  165. package/src/utils/retry.ts +3 -4
  166. package/src/utils/secure-browser-launcher.ts +500 -2
  167. package/src/utils/terminalSerializer.ts +9 -0
  168. package/src/utils/thoughtUtils.ts +2 -0
@@ -7,11 +7,146 @@
7
7
  import { execFile, type ExecFileOptions } from 'node:child_process';
8
8
  import { promisify } from 'node:util';
9
9
  import { platform } from 'node:os';
10
+ import { stat } from 'node:fs/promises';
11
+ import { env } from 'node:process';
10
12
  import { URL } from 'node:url';
13
+ import { envOr } from './env.js';
14
+ import * as nodePath from 'node:path';
11
15
 
12
16
  const execFileAsync = promisify(execFile);
13
17
  const WINDOWS_BROWSER_URL_ENV_VAR = 'LLXPRT_BROWSER_URL';
14
18
 
19
+ /**
20
+ * Type for the options object passed to execFileAsync for browser launches.
21
+ * Node's ExecFileOptions does not declare `detached` or `stdio` (they live on
22
+ * SpawnOptions), but execFile forwards them to spawn at runtime. Declaring
23
+ * them explicitly here keeps TypeScript checking of the known keys while
24
+ * permitting the spawn-only fields we rely on.
25
+ */
26
+ type BrowserExecOptions = ExecFileOptions & {
27
+ detached?: boolean;
28
+ stdio?: 'ignore' | 'pipe' | 'inherit';
29
+ };
30
+
31
+ /**
32
+ * Shared exec options for browser launches. Both the default-browser path
33
+ * and the specific-browser path use these so behavior stays consistent.
34
+ */
35
+ function getBrowserExecOptions(): BrowserExecOptions {
36
+ const browserEnv = { ...env };
37
+ delete browserEnv.SHELL;
38
+ return {
39
+ env: browserEnv,
40
+ detached: true,
41
+ stdio: 'ignore',
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Candidate Chrome/Chromium binary names on Linux/BSD, tried in order when
47
+ * the preferred name is not installed. Different distros ship the browser
48
+ * under different names (google-chrome, google-chrome-stable, chromium,
49
+ * chromium-browser).
50
+ */
51
+ const LINUX_CHROME_BINARIES = [
52
+ 'google-chrome',
53
+ 'google-chrome-stable',
54
+ 'chromium',
55
+ 'chromium-browser',
56
+ ];
57
+
58
+ /**
59
+ * Candidate Firefox binary names on Linux/BSD (firefox, firefox-esr on
60
+ * Debian/Ubuntu).
61
+ */
62
+ const LINUX_FIREFOX_BINARIES = ['firefox', 'firefox-esr'];
63
+
64
+ /**
65
+ * Typical Windows install locations for Chrome (relative to the system drive
66
+ * root, Program Files, and LOCALAPPDATA). Probed when a bare `chrome.exe` is
67
+ * not on PATH.
68
+ */
69
+ const WINDOWS_CHROME_PATHS = [
70
+ () =>
71
+ nodePath.join(
72
+ envOr(env.PROGRAMFILES, 'C:\\Program Files'),
73
+ 'Google',
74
+ 'Chrome',
75
+ 'Application',
76
+ 'chrome.exe',
77
+ ),
78
+ () =>
79
+ nodePath.join(
80
+ envOr(env['PROGRAMFILES(X86)'], 'C:\\Program Files (x86)'),
81
+ 'Google',
82
+ 'Chrome',
83
+ 'Application',
84
+ 'chrome.exe',
85
+ ),
86
+ () =>
87
+ nodePath.join(
88
+ envOr(
89
+ env.LOCALAPPDATA,
90
+ nodePath.join(
91
+ envOr(env.USERPROFILE, 'C:\\Users\\Public'),
92
+ 'AppData',
93
+ 'Local',
94
+ ),
95
+ ),
96
+ 'Google',
97
+ 'Chrome',
98
+ 'Application',
99
+ 'chrome.exe',
100
+ ),
101
+ ];
102
+
103
+ /**
104
+ * Typical Windows install locations for Firefox, probed when a bare
105
+ * `firefox.exe` is not on PATH.
106
+ */
107
+ const WINDOWS_FIREFOX_PATHS = [
108
+ () =>
109
+ nodePath.join(
110
+ envOr(env.PROGRAMFILES, 'C:\\Program Files'),
111
+ 'Mozilla Firefox',
112
+ 'firefox.exe',
113
+ ),
114
+ () =>
115
+ nodePath.join(
116
+ envOr(env['PROGRAMFILES(X86)'] ?? undefined, 'C:\\Program Files (x86)'),
117
+ 'Mozilla Firefox',
118
+ 'firefox.exe',
119
+ ),
120
+ ];
121
+
122
+ /**
123
+ * Supported browser kinds for targeted launching.
124
+ */
125
+ export type BrowserKind = 'chrome' | 'firefox' | 'safari';
126
+
127
+ /**
128
+ * Whether the platform is Linux or a BSD variant. Centralized so the
129
+ * default-browser path and the specific-browser fallback path agree on which
130
+ * platforms count as "Linux-like".
131
+ */
132
+ function isLinuxLike(platformName: NodeJS.Platform): boolean {
133
+ return (
134
+ platformName === 'linux' ||
135
+ platformName === 'freebsd' ||
136
+ platformName === 'openbsd'
137
+ );
138
+ }
139
+
140
+ /**
141
+ * Options for launching a specific browser binary with an optional profile.
142
+ * Chrome accepts a profile directory name. Firefox accepts either a profile
143
+ * name or an absolute profile path; names use `-P` and paths use `-profile`.
144
+ */
145
+ export interface BrowserLaunchOptions {
146
+ browser?: BrowserKind;
147
+ profileDirectory?: string;
148
+ }
149
+
15
150
  /**
16
151
  * Validates that a URL is safe to open in a browser.
17
152
  * Only allows HTTP and HTTPS URLs to prevent command injection.
@@ -95,6 +230,95 @@ function createBrowserLaunchPlan(
95
230
  }
96
231
  }
97
232
 
233
+ /**
234
+ * Characters that must never appear in a profile directory name because they
235
+ * enable path traversal, act as path separators, or are shell/argument
236
+ * metacharacters. Everything else (including Unicode display names with
237
+ * parentheses, ampersands, or CJK characters) is permitted, because the
238
+ * launcher passes the value as a distinct argv element to execFile (no
239
+ * shell), so only traversal/separator characters are a real injection
240
+ * vector.
241
+ */
242
+ const PROFILE_DIRECTORY_FORBIDDEN =
243
+ // \p{Cc} matches Unicode control characters (incl. NUL, newline, tab);
244
+ // backslash and forward slash are path separators; a bare or dotted ".."
245
+ // enables traversal. Using \p{Cc} (rather than a literal \x00-\x1f range)
246
+ // keeps the pattern free of embedded control characters.
247
+ /[\\/]|(?:^|[/\\])\.\.(?:[/\\]|$)|\p{Cc}/u;
248
+
249
+ /**
250
+ * Validates a profile directory name against a denylist of dangerous
251
+ * characters.
252
+ *
253
+ * Security: This guard prevents path traversal and command injection by
254
+ * rejecting path separators (/, \), NUL/control characters, and traversal
255
+ * sequences (..). Because the launcher uses execFile (no shell) and passes
256
+ * the profile name as a discrete argv element, shell metacharacters are not
257
+ * an injection vector, so legitimate display names containing parentheses,
258
+ * ampersands, or Unicode characters are accepted.
259
+ *
260
+ * @param dir The profile directory name to validate
261
+ * @throws Error if the name contains disallowed characters
262
+ */
263
+ export function validateProfileDirectory(dir: string): void {
264
+ if (
265
+ !dir ||
266
+ typeof dir !== 'string' ||
267
+ PROFILE_DIRECTORY_FORBIDDEN.test(dir)
268
+ ) {
269
+ throw new Error(
270
+ `Invalid profile directory: "${dir}". Path separators, control characters, and traversal sequences (..) are not allowed.`,
271
+ );
272
+ }
273
+ }
274
+
275
+ const FIREFOX_PROFILE_PATH_FORBIDDEN = /\p{Cc}/u;
276
+
277
+ function isFirefoxProfilePath(
278
+ browser: BrowserKind,
279
+ profileDirectory: unknown,
280
+ ): profileDirectory is string {
281
+ return (
282
+ browser === 'firefox' &&
283
+ typeof profileDirectory === 'string' &&
284
+ nodePath.isAbsolute(profileDirectory)
285
+ );
286
+ }
287
+
288
+ function validateBrowserProfileSelection(
289
+ browser: BrowserKind,
290
+ profileDirectory: unknown,
291
+ ): void {
292
+ if (isFirefoxProfilePath(browser, profileDirectory)) {
293
+ if (FIREFOX_PROFILE_PATH_FORBIDDEN.test(profileDirectory)) {
294
+ throw new Error(
295
+ `Invalid Firefox profile path: "${profileDirectory}". Control characters are not allowed.`,
296
+ );
297
+ }
298
+ return;
299
+ }
300
+ if (typeof profileDirectory !== 'string') {
301
+ throw new Error(
302
+ `Invalid profile directory: "${String(
303
+ profileDirectory,
304
+ )}". Profile directory must be a string.`,
305
+ );
306
+ }
307
+ validateProfileDirectory(profileDirectory);
308
+ }
309
+
310
+ function firefoxProfileArguments(
311
+ profileDirectory: string | undefined,
312
+ ): string[] {
313
+ if (!profileDirectory) {
314
+ return [];
315
+ }
316
+ return [
317
+ nodePath.isAbsolute(profileDirectory) ? '-profile' : '-P',
318
+ profileDirectory,
319
+ ];
320
+ }
321
+
98
322
  /**
99
323
  * Opens a URL in the default browser using platform-specific commands.
100
324
  * This implementation avoids shell injection vulnerabilities by:
@@ -102,12 +326,41 @@ function createBrowserLaunchPlan(
102
326
  * 2. Using execFile instead of exec to avoid shell interpretation
103
327
  * 3. Keeping URL data out of PowerShell command source
104
328
  *
329
+ * When `options.browser` is set, launches that specific browser binary
330
+ * directly (instead of the OS default browser), optionally with a specific
331
+ * profile directory. This ensures OAuth always uses the intended browser
332
+ * profile for the given bucket.
333
+ *
105
334
  * @param url The URL to open
335
+ * @param options Optional browser and profile directory selection
106
336
  * @throws Error if the URL is invalid or if opening the browser fails
107
337
  */
108
- export async function openBrowserSecurely(url: string): Promise<void> {
338
+ export async function openBrowserSecurely(
339
+ url: string,
340
+ options?: BrowserLaunchOptions,
341
+ ): Promise<void> {
109
342
  validateUrl(url);
110
343
 
344
+ if (options?.browser) {
345
+ if (options.profileDirectory !== undefined) {
346
+ validateBrowserProfileSelection(
347
+ options.browser,
348
+ options.profileDirectory,
349
+ );
350
+ }
351
+ await openSpecificBrowser(url, options.browser, options.profileDirectory);
352
+ return;
353
+ }
354
+
355
+ await openDefaultBrowser(url);
356
+ }
357
+
358
+ /**
359
+ * Open the OS-default browser for the given URL. This preserves the original
360
+ * platform-specific behaviour (including the Linux fallback chain) used when
361
+ * no particular browser/profile is requested.
362
+ */
363
+ async function openDefaultBrowser(url: string): Promise<void> {
111
364
  const platformName = platform();
112
365
  const launchPlan = createBrowserLaunchPlan(platformName, url);
113
366
 
@@ -131,7 +384,6 @@ export async function openBrowserSecurely(url: string): Promise<void> {
131
384
  'chromium',
132
385
  'google-chrome',
133
386
  ];
134
-
135
387
  const succeeded = await tryFallbackBrowserCommands(
136
388
  fallbackCommands,
137
389
  url,
@@ -216,3 +468,249 @@ async function tryFallbackBrowserCommands(
216
468
  }
217
469
  return false;
218
470
  }
471
+
472
+ /**
473
+ * Launch a specific browser binary with an optional profile directory.
474
+ *
475
+ * Uses execFile (no shell) throughout to prevent command injection.
476
+ * Profile directory names are validated by the caller via
477
+ * {@link validateProfileDirectory} before reaching this function.
478
+ */
479
+ async function openSpecificBrowser(
480
+ url: string,
481
+ browser: BrowserKind,
482
+ profileDirectory: string | undefined,
483
+ ): Promise<void> {
484
+ const platformName = platform();
485
+ let command: string;
486
+ let args: string[];
487
+
488
+ switch (browser) {
489
+ case 'chrome':
490
+ [command, args] = buildChromeLaunchArgs(
491
+ platformName,
492
+ profileDirectory,
493
+ url,
494
+ );
495
+ break;
496
+
497
+ case 'firefox':
498
+ [command, args] = buildFirefoxLaunchArgs(
499
+ platformName,
500
+ profileDirectory,
501
+ url,
502
+ );
503
+ break;
504
+
505
+ case 'safari':
506
+ if (platformName !== 'darwin') {
507
+ throw new Error('Safari is only available on macOS');
508
+ }
509
+ // Safari has no profile concept; launch the app with the URL
510
+ command = 'open';
511
+ args = ['-a', 'Safari', url];
512
+ break;
513
+
514
+ default:
515
+ throw new Error(`Unsupported browser: ${browser}`);
516
+ }
517
+
518
+ try {
519
+ await execFileAsync(command, args, getBrowserExecOptions());
520
+ } catch (error) {
521
+ const succeeded =
522
+ (await tryLinuxFallbackBinaries(platformName, browser, command, args)) ||
523
+ (await tryWindowsFallbackBinaries(platformName, browser, command, args));
524
+ if (succeeded) {
525
+ return;
526
+ }
527
+ throw new Error(
528
+ `Failed to open ${browser}: ${error instanceof Error ? error.message : 'Unknown error'}`,
529
+ { cause: error },
530
+ );
531
+ }
532
+ }
533
+
534
+ /**
535
+ * On Linux/BSD, the preferred Chrome/Firefox binary name varies by distro
536
+ * (google-chrome vs. chromium, firefox vs. firefox-esr). This helper tries
537
+ * the known fallback binaries for the given browser and resolves true if one
538
+ * succeeded (so the caller can suppress the original error).
539
+ *
540
+ * Resolves false when the platform is not Linux/BSD, the browser has no
541
+ * fallback list, or every fallback binary failed.
542
+ */
543
+ async function tryLinuxFallbackBinaries(
544
+ platformName: NodeJS.Platform,
545
+ browser: BrowserKind,
546
+ primaryCommand: string,
547
+ args: string[],
548
+ ): Promise<boolean> {
549
+ if (!isLinuxLike(platformName)) {
550
+ return false;
551
+ }
552
+ const fallbacks = linuxBrowserFallbacks(browser);
553
+ const tried = new Set<string>([primaryCommand]);
554
+ for (const fallback of fallbacks) {
555
+ if (tried.has(fallback)) {
556
+ continue;
557
+ }
558
+ tried.add(fallback);
559
+ try {
560
+ await execFileAsync(fallback, args, getBrowserExecOptions());
561
+ return true;
562
+ } catch {
563
+ // Try the next fallback binary.
564
+ }
565
+ }
566
+ return false;
567
+ }
568
+
569
+ /**
570
+ * Resolve the candidate fallback binary names for a browser on Linux/BSD.
571
+ * Returns an empty array for browsers without known alternates (e.g. Safari).
572
+ */
573
+ function linuxBrowserFallbacks(browser: BrowserKind): string[] {
574
+ if (browser === 'chrome') {
575
+ return LINUX_CHROME_BINARIES;
576
+ }
577
+ if (browser === 'firefox') {
578
+ return LINUX_FIREFOX_BINARIES;
579
+ }
580
+ return [];
581
+ }
582
+
583
+ /**
584
+ * Resolve the candidate fallback paths for a browser on Windows. Returns an
585
+ * empty array for browsers without known install locations (e.g. Safari).
586
+ */
587
+ function windowsBrowserFallbackPaths(
588
+ browser: BrowserKind,
589
+ ): Array<() => string> {
590
+ if (browser === 'chrome') {
591
+ return WINDOWS_CHROME_PATHS;
592
+ }
593
+ if (browser === 'firefox') {
594
+ return WINDOWS_FIREFOX_PATHS;
595
+ }
596
+ return [];
597
+ }
598
+
599
+ /**
600
+ * On Windows, a bare `chrome.exe`/`firefox.exe` is only on PATH if the user
601
+ * opted in during install. When it is missing (ENOENT), probe the typical
602
+ * install locations under Program Files and LOCALAPPDATA and launch the first
603
+ * one that exists. Resolves false when the platform is not Windows, the
604
+ * browser has no fallback list, or every candidate failed.
605
+ */
606
+ async function tryWindowsFallbackBinaries(
607
+ platformName: NodeJS.Platform,
608
+ browser: BrowserKind,
609
+ primaryCommand: string,
610
+ args: string[],
611
+ ): Promise<boolean> {
612
+ if (platformName !== 'win32') {
613
+ return false;
614
+ }
615
+ const candidates = windowsBrowserFallbackPaths(browser);
616
+ const tried = new Set<string>([primaryCommand]);
617
+ for (const candidate of candidates) {
618
+ const resolved = candidate();
619
+ // Only attempt to execute real files so a directory at a candidate path is
620
+ // not mistaken for a browser binary (execFileAsync would emit a confusing
621
+ // EACCES/UNKNOWN error).
622
+ const isExecutableFile = await stat(resolved)
623
+ .then((candidateStat) => candidateStat.isFile())
624
+ .catch(() => false);
625
+ if (tried.has(resolved) || !isExecutableFile) {
626
+ continue;
627
+ }
628
+ tried.add(resolved);
629
+ try {
630
+ await execFileAsync(resolved, args, getBrowserExecOptions());
631
+ return true;
632
+ } catch {
633
+ // Try the next candidate path.
634
+ }
635
+ }
636
+ return false;
637
+ }
638
+
639
+ /**
640
+ * Build the command and arguments to launch Chrome/Chromium with a profile.
641
+ */
642
+ function buildChromeLaunchArgs(
643
+ platformName: NodeJS.Platform,
644
+ profileDirectory: string | undefined,
645
+ url: string,
646
+ ): [string, string[]] {
647
+ switch (platformName) {
648
+ case 'darwin': {
649
+ const args = ['-a', 'Google Chrome'];
650
+ if (profileDirectory !== undefined) {
651
+ args.push('--args', `--profile-directory=${profileDirectory}`);
652
+ }
653
+ args.push(url);
654
+ return ['open', args];
655
+ }
656
+
657
+ case 'win32': {
658
+ // Invoke the Chrome executable directly via execFileAsync — no shell,
659
+ // no PowerShell -Command string. Each argument is passed as a distinct
660
+ // argv element so spaces in the profile directory are preserved and
661
+ // there is no command-string boundary to inject through.
662
+ const args: string[] = [];
663
+ if (profileDirectory !== undefined) {
664
+ args.push(`--profile-directory=${profileDirectory}`);
665
+ }
666
+ args.push(url);
667
+ return ['chrome.exe', args];
668
+ }
669
+
670
+ case 'linux':
671
+ case 'freebsd':
672
+ case 'openbsd': {
673
+ const args: string[] = [];
674
+ if (profileDirectory !== undefined) {
675
+ args.push(`--profile-directory=${profileDirectory}`);
676
+ }
677
+ args.push(url);
678
+ return ['google-chrome', args];
679
+ }
680
+
681
+ default:
682
+ throw new Error(`Unsupported platform for Chrome: ${platformName}`);
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Build the command and arguments to launch Firefox with a profile.
688
+ */
689
+ function buildFirefoxLaunchArgs(
690
+ platformName: NodeJS.Platform,
691
+ profileDirectory: string | undefined,
692
+ url: string,
693
+ ): [string, string[]] {
694
+ const profileArguments = firefoxProfileArguments(profileDirectory);
695
+ switch (platformName) {
696
+ case 'darwin': {
697
+ const args =
698
+ profileArguments.length > 0
699
+ ? ['-n', '-a', 'Firefox', '--args', ...profileArguments]
700
+ : ['-a', 'Firefox'];
701
+ args.push(url);
702
+ return ['open', args];
703
+ }
704
+
705
+ case 'win32':
706
+ return ['firefox.exe', [...profileArguments, url]];
707
+
708
+ case 'linux':
709
+ case 'freebsd':
710
+ case 'openbsd':
711
+ return ['firefox', [...profileArguments, url]];
712
+
713
+ default:
714
+ throw new Error(`Unsupported platform for Firefox: ${platformName}`);
715
+ }
716
+ }
@@ -19,6 +19,15 @@ export interface AnsiToken {
19
19
  export type AnsiLine = AnsiToken[];
20
20
  export type AnsiOutput = AnsiLine[];
21
21
 
22
+ /**
23
+ * Explicit tagged update protocol for live-output streaming. Discriminated
24
+ * by `mode`: text stream producers emit `append` (incremental deltas);
25
+ * terminal-buffer producers emit `replace` (full snapshots).
26
+ */
27
+ export type LiveOutputUpdate =
28
+ | { mode: 'append'; data: string }
29
+ | { mode: 'replace'; data: AnsiOutput };
30
+
22
31
  const enum Attribute {
23
32
  inverse = 1,
24
33
  bold = 2,
@@ -7,6 +7,8 @@
7
7
  export type ThoughtSummary = {
8
8
  subject: string;
9
9
  description: string;
10
+ streamId?: string;
11
+ streamStatus?: 'delta' | 'complete';
10
12
  };
11
13
 
12
14
  const START_DELIMITER = '**';