codex-overleaf-link 1.1.1

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +457 -0
  3. package/bin/codex-overleaf-link.mjs +223 -0
  4. package/extension/src/shared/agentTranscript.js +1175 -0
  5. package/extension/src/shared/auditRecords.js +568 -0
  6. package/extension/src/shared/compatibility.js +372 -0
  7. package/extension/src/shared/compileAdapter.js +176 -0
  8. package/extension/src/shared/governanceRules.js +252 -0
  9. package/extension/src/shared/i18n.js +565 -0
  10. package/extension/src/shared/models.js +106 -0
  11. package/extension/src/shared/otText.js +505 -0
  12. package/extension/src/shared/projectFiles.js +180 -0
  13. package/extension/src/shared/reviewing.js +99 -0
  14. package/extension/src/shared/sensitiveScan.js +116 -0
  15. package/extension/src/shared/sessionState.js +1084 -0
  16. package/extension/src/shared/staleGuard.js +150 -0
  17. package/extension/src/shared/storageDb.js +986 -0
  18. package/extension/src/shared/storageKeys.js +29 -0
  19. package/extension/src/shared/storageMigration.js +168 -0
  20. package/extension/src/shared/summary.js +248 -0
  21. package/extension/src/shared/undoOperations.js +369 -0
  22. package/native-host/src/codexArgs.js +43 -0
  23. package/native-host/src/codexHome.js +538 -0
  24. package/native-host/src/codexModels.js +247 -0
  25. package/native-host/src/codexPrompt.js +192 -0
  26. package/native-host/src/codexPromptAssembly.js +411 -0
  27. package/native-host/src/codexSessionRunner.js +1247 -0
  28. package/native-host/src/commandApproval.js +914 -0
  29. package/native-host/src/debugLog.js +78 -0
  30. package/native-host/src/diffEngine.js +247 -0
  31. package/native-host/src/index.js +132 -0
  32. package/native-host/src/launcher.js +81 -0
  33. package/native-host/src/localSkills.js +476 -0
  34. package/native-host/src/manifest.js +226 -0
  35. package/native-host/src/mirrorSensitiveScan.js +119 -0
  36. package/native-host/src/mirrorWorkspace.js +1019 -0
  37. package/native-host/src/nativeDoctor.js +826 -0
  38. package/native-host/src/nativeEnvironment.js +315 -0
  39. package/native-host/src/nativeHostPlatform.js +112 -0
  40. package/native-host/src/nativeMessaging.js +60 -0
  41. package/native-host/src/nativeQuotas.js +294 -0
  42. package/native-host/src/nativeResponseBudget.js +194 -0
  43. package/native-host/src/runtimeInstaller.js +357 -0
  44. package/native-host/src/taskRunner.js +3 -0
  45. package/native-host/src/taskRunnerRuntime.js +1083 -0
  46. package/native-host/src/textPatch.js +287 -0
  47. package/package.json +40 -0
  48. package/scripts/codex-json-agent.mjs +269 -0
  49. package/scripts/install-native-host.mjs +255 -0
  50. package/scripts/npm-package-files-v1.1.1.txt +52 -0
  51. package/scripts/uninstall-native-host.mjs +298 -0
  52. package/scripts/verify-npm-package.mjs +296 -0
@@ -0,0 +1,826 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawn } = require('node:child_process');
7
+ const { HOST_NAME, validateChromeExtensionId } = require('./manifest');
8
+ const {
9
+ getHomeDir,
10
+ getNativeHostPlatform,
11
+ getNativeHostRegistrationTarget
12
+ } = require('./nativeHostPlatform');
13
+ const { decodeFrames, encodeMessage } = require('./nativeMessaging');
14
+ const {
15
+ buildBridgePingParams,
16
+ evaluateNativeCompatibility
17
+ } = require('../../extension/src/shared/compatibility');
18
+ const { version: PACKAGE_VERSION } = require('../../package.json');
19
+
20
+ const DEFAULT_DOCTOR_TIMEOUT_MS = 3000;
21
+ const DOCTOR_PING_ID = 'doctor-ping';
22
+ const MAX_DOCTOR_STDERR_BYTES = 64 * 1024;
23
+ const MAX_DOCTOR_STDOUT_BYTES = 2 * 1024 * 1024;
24
+
25
+ async function runDoctor(options = {}) {
26
+ const platform = getNativeHostPlatform(options);
27
+ const browser = normalizeBrowser(options.browser || 'auto');
28
+ const env = options.env || process.env;
29
+ const homeDir = options.homeDir || getHomeDir({ ...options, env, platform });
30
+ const pathOptions = {
31
+ revealPaths: Boolean(options.revealPaths),
32
+ homeDir,
33
+ platform
34
+ };
35
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
36
+ const registrationTarget = getNativeHostRegistrationTarget({
37
+ ...options,
38
+ browser,
39
+ env,
40
+ homeDir,
41
+ platform
42
+ });
43
+ const runtimeRoot = options.runtimeRoot
44
+ ? normalizeDiagnosticPath(resolveRuntimeRoot(options.runtimeRoot), pathOptions)
45
+ : undefined;
46
+ const body = {
47
+ ok: false,
48
+ status: 'unknown',
49
+ browser: registrationTarget.browser,
50
+ platform,
51
+ registration: sanitizeRegistrationTarget(registrationTarget, pathOptions),
52
+ manifest: {
53
+ path: normalizeDiagnosticPath(registrationTarget.manifestPath, pathOptions),
54
+ exists: false,
55
+ valid: false,
56
+ errors: []
57
+ },
58
+ bridge: null,
59
+ ping: {
60
+ attempted: false
61
+ },
62
+ compatibility: null,
63
+ diagnostics: {
64
+ browser: registrationTarget.browser,
65
+ registrationKind: registrationTarget.kind,
66
+ manifestPath: normalizeDiagnosticPath(registrationTarget.manifestPath, pathOptions)
67
+ }
68
+ };
69
+
70
+ if (runtimeRoot) {
71
+ body.runtimeRoot = runtimeRoot;
72
+ body.diagnostics.runtimeRoot = runtimeRoot;
73
+ }
74
+
75
+ const manifestResult = readAndValidateManifest(registrationTarget.manifestPath, {
76
+ platform,
77
+ pathOptions
78
+ });
79
+ Object.assign(body.manifest, manifestResult.manifest);
80
+
81
+ if (!manifestResult.manifest.exists || !manifestResult.manifest.valid) {
82
+ return finalizeDoctorResult(body);
83
+ }
84
+
85
+ const bridgePath = manifestResult.bridgePath;
86
+ body.bridge = validateBridgePath(bridgePath, { platform, pathOptions });
87
+ body.diagnostics.bridgePath = body.bridge.path;
88
+ body.diagnostics.allowedOrigins = body.manifest.allowedOrigins || [];
89
+
90
+ if (!body.bridge.valid || !body.bridge.exists) {
91
+ return finalizeDoctorResult(body);
92
+ }
93
+
94
+ const pingResult = await pingNativeBridge(bridgePath, {
95
+ id: DOCTOR_PING_ID,
96
+ method: 'bridge.ping',
97
+ params: buildBridgePingParams({ version: PACKAGE_VERSION })
98
+ }, {
99
+ env,
100
+ pathOptions,
101
+ platform,
102
+ timeoutMs
103
+ });
104
+ const rawPingResponse = pingResult.rawResponse;
105
+ delete pingResult.rawResponse;
106
+ body.ping = pingResult;
107
+
108
+ if (body.ping.ok) {
109
+ body.compatibility = sanitizeCompatibility(
110
+ evaluateNativeCompatibility(rawPingResponse, { version: PACKAGE_VERSION })
111
+ );
112
+ }
113
+
114
+ return finalizeDoctorResult(body);
115
+ }
116
+
117
+ function classifyDoctorResult(body = {}) {
118
+ if (!body.manifest || body.manifest.exists === false) {
119
+ return {
120
+ exitCode: 2,
121
+ ok: false,
122
+ status: 'missing_install'
123
+ };
124
+ }
125
+
126
+ if (body.manifest.valid === false) {
127
+ return {
128
+ exitCode: 2,
129
+ ok: false,
130
+ status: 'invalid_manifest'
131
+ };
132
+ }
133
+
134
+ if (!body.bridge || body.bridge.valid === false) {
135
+ return {
136
+ exitCode: 2,
137
+ ok: false,
138
+ status: 'invalid_bridge'
139
+ };
140
+ }
141
+
142
+ if (body.bridge.exists === false) {
143
+ return {
144
+ exitCode: 2,
145
+ ok: false,
146
+ status: 'missing_bridge'
147
+ };
148
+ }
149
+
150
+ if (!body.ping || body.ping.ok !== true) {
151
+ return {
152
+ exitCode: 3,
153
+ ok: false,
154
+ status: 'execution_failure'
155
+ };
156
+ }
157
+
158
+ if (body.compatibility?.classification === 'compatible') {
159
+ return {
160
+ exitCode: 0,
161
+ ok: true,
162
+ status: 'ok'
163
+ };
164
+ }
165
+
166
+ if (body.compatibility?.classification === 'update-available') {
167
+ return {
168
+ exitCode: 3,
169
+ ok: false,
170
+ status: 'native_stale'
171
+ };
172
+ }
173
+
174
+ return {
175
+ exitCode: 3,
176
+ ok: false,
177
+ status: 'native_incompatible'
178
+ };
179
+ }
180
+
181
+ function formatDoctorHuman(body = {}) {
182
+ const lines = [
183
+ 'Native host doctor',
184
+ `Status: ${body.status || 'unknown'}`,
185
+ `OK: ${body.ok === true ? 'yes' : 'no'}`,
186
+ `Browser: ${body.browser || 'unknown'}`,
187
+ `Manifest: ${body.manifest?.path || 'unknown'}`
188
+ ];
189
+
190
+ if (body.registration?.kind) {
191
+ lines.push(`Registration: ${body.registration.kind}`);
192
+ }
193
+ if (body.registration?.registryKey) {
194
+ lines.push(`Registry key: ${body.registration.registryKey}`);
195
+ }
196
+ if (body.bridge?.path) {
197
+ lines.push(`Bridge: ${body.bridge.path}`);
198
+ }
199
+ if (body.runtimeRoot) {
200
+ lines.push(`Runtime root: ${body.runtimeRoot}`);
201
+ }
202
+ if (Array.isArray(body.manifest?.allowedOrigins) && body.manifest.allowedOrigins.length) {
203
+ lines.push(`Allowed origins: ${body.manifest.allowedOrigins.join(', ')}`);
204
+ }
205
+ if (body.compatibility) {
206
+ lines.push(`Native compatibility: ${body.compatibility.classification || 'unknown'}`);
207
+ lines.push(`Native version: ${body.compatibility.nativeVersion || 'unknown'}`);
208
+ }
209
+ if (body.ping?.ok === false) {
210
+ lines.push(`Ping: failed (${body.ping.error || 'unknown error'})`);
211
+ } else if (body.ping?.ok === true) {
212
+ lines.push('Ping: ok');
213
+ } else {
214
+ lines.push('Ping: not run');
215
+ }
216
+ if (Array.isArray(body.manifest?.errors) && body.manifest.errors.length) {
217
+ lines.push(`Manifest errors: ${body.manifest.errors.join('; ')}`);
218
+ }
219
+ if (Array.isArray(body.bridge?.errors) && body.bridge.errors.length) {
220
+ lines.push(`Bridge errors: ${body.bridge.errors.join('; ')}`);
221
+ }
222
+
223
+ return `${lines.join('\n')}\n`;
224
+ }
225
+
226
+ function normalizeDiagnosticPath(targetPath, options = {}) {
227
+ if (!targetPath) {
228
+ return '';
229
+ }
230
+ const value = String(targetPath);
231
+ if (options.revealPaths) {
232
+ return value;
233
+ }
234
+
235
+ const platform = options.platform || process.platform;
236
+ const platformPath = getPlatformPath(platform);
237
+ const homeDir = options.homeDir || getHomeFromEnv(options.env || process.env, platform);
238
+ const normalizedValue = platformPath.normalize(value);
239
+ const normalizedHome = homeDir ? platformPath.normalize(String(homeDir)) : '';
240
+ const comparisonValue = platform === 'win32' ? normalizedValue.toLowerCase() : normalizedValue;
241
+ const comparisonHome = platform === 'win32' ? normalizedHome.toLowerCase() : normalizedHome;
242
+
243
+ if (comparisonHome && comparisonValue === comparisonHome) {
244
+ return '~';
245
+ }
246
+
247
+ if (comparisonHome && comparisonValue.startsWith(`${comparisonHome}${platformPath.sep}`)) {
248
+ return `~${platformPath.sep}${normalizedValue.slice(normalizedHome.length + 1)}`;
249
+ }
250
+
251
+ if (isAbsolutePathForPlatform(value, platform)) {
252
+ return `${platformPath.sep}<absolute-path>${platformPath.sep}${platformPath.basename(value)}`;
253
+ }
254
+
255
+ return value;
256
+ }
257
+
258
+ function finalizeDoctorResult(body) {
259
+ const classification = classifyDoctorResult(body);
260
+ body.ok = classification.ok;
261
+ body.status = classification.status;
262
+ return {
263
+ exitCode: classification.exitCode,
264
+ body
265
+ };
266
+ }
267
+
268
+ function readAndValidateManifest(manifestPath, options) {
269
+ const manifest = {
270
+ path: normalizeDiagnosticPath(manifestPath, options.pathOptions),
271
+ exists: false,
272
+ valid: false,
273
+ errors: []
274
+ };
275
+
276
+ let raw;
277
+ try {
278
+ raw = fs.readFileSync(manifestPath, 'utf8');
279
+ manifest.exists = true;
280
+ } catch (error) {
281
+ if (error?.code !== 'ENOENT') {
282
+ manifest.errors.push(`Unable to read manifest: ${sanitizePathInMessage(error.message, manifestPath, options.pathOptions)}`);
283
+ }
284
+ return { manifest, bridgePath: '' };
285
+ }
286
+
287
+ let parsed;
288
+ try {
289
+ parsed = JSON.parse(raw);
290
+ } catch (error) {
291
+ manifest.errors.push(`Invalid manifest JSON: ${error.message}`);
292
+ return { manifest, bridgePath: '' };
293
+ }
294
+
295
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
296
+ manifest.errors.push('Manifest must be a JSON object');
297
+ return { manifest, bridgePath: '' };
298
+ }
299
+
300
+ if (parsed.name !== HOST_NAME) {
301
+ manifest.errors.push(`Manifest name must be ${HOST_NAME}`);
302
+ }
303
+ if (parsed.type !== 'stdio') {
304
+ manifest.errors.push('Manifest type must be stdio');
305
+ }
306
+ if (typeof parsed.path !== 'string' || !parsed.path) {
307
+ manifest.errors.push('Manifest path must be a non-empty string');
308
+ } else if (!isAbsolutePathForPlatform(parsed.path, options.platform)) {
309
+ manifest.errors.push('Manifest path must be absolute');
310
+ } else {
311
+ manifest.bridgePath = normalizeDiagnosticPath(parsed.path, options.pathOptions);
312
+ }
313
+
314
+ try {
315
+ manifest.allowedOrigins = normalizeAllowedOrigins(parsed.allowed_origins);
316
+ } catch (error) {
317
+ manifest.errors.push(error.message);
318
+ }
319
+
320
+ manifest.valid = manifest.errors.length === 0;
321
+ return {
322
+ manifest,
323
+ bridgePath: typeof parsed.path === 'string' ? parsed.path : ''
324
+ };
325
+ }
326
+
327
+ function normalizeAllowedOrigins(origins) {
328
+ if (!Array.isArray(origins) || origins.length === 0) {
329
+ throw new Error('Manifest allowed_origins must be a non-empty array');
330
+ }
331
+
332
+ const seen = new Set();
333
+ const normalized = [];
334
+ for (const origin of origins) {
335
+ const value = typeof origin === 'string' ? origin.trim() : '';
336
+ const match = /^chrome-extension:\/\/([a-p]{32})\/?$/.exec(value);
337
+ if (!match || !validateChromeExtensionId(match[1])) {
338
+ throw new Error(`Invalid allowed origin: ${String(origin)}`);
339
+ }
340
+ const clean = `chrome-extension://${match[1]}/`;
341
+ if (!seen.has(clean)) {
342
+ seen.add(clean);
343
+ normalized.push(clean);
344
+ }
345
+ }
346
+
347
+ if (!normalized.length) {
348
+ throw new Error('Manifest allowed_origins must contain at least one valid Chrome extension origin');
349
+ }
350
+
351
+ return normalized;
352
+ }
353
+
354
+ function validateBridgePath(bridgePath, options) {
355
+ const bridge = {
356
+ path: normalizeDiagnosticPath(bridgePath, options.pathOptions),
357
+ exists: false,
358
+ valid: true,
359
+ errors: []
360
+ };
361
+
362
+ if (!bridgePath || typeof bridgePath !== 'string') {
363
+ bridge.valid = false;
364
+ bridge.errors.push('Bridge path is missing');
365
+ return bridge;
366
+ }
367
+
368
+ if (!isAbsolutePathForPlatform(bridgePath, options.platform)) {
369
+ bridge.valid = false;
370
+ bridge.errors.push('Bridge path must be absolute');
371
+ return bridge;
372
+ }
373
+
374
+ let stat;
375
+ try {
376
+ stat = fs.statSync(bridgePath);
377
+ bridge.exists = true;
378
+ } catch (error) {
379
+ if (error?.code !== 'ENOENT') {
380
+ bridge.valid = false;
381
+ bridge.errors.push(`Unable to inspect bridge: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`);
382
+ }
383
+ return bridge;
384
+ }
385
+
386
+ if (!stat.isFile()) {
387
+ bridge.valid = false;
388
+ bridge.errors.push('Bridge path must point to a file');
389
+ }
390
+
391
+ return bridge;
392
+ }
393
+
394
+ function pingNativeBridge(bridgePath, message, options = {}) {
395
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
396
+ const env = options.env || process.env;
397
+ const spawnInvocation = buildDoctorBridgeSpawnInvocation(bridgePath, {
398
+ env,
399
+ platform: options.platform
400
+ });
401
+
402
+ return new Promise(resolve => {
403
+ let child;
404
+ let settled = false;
405
+ let timedOut = false;
406
+ let stdout = Buffer.alloc(0);
407
+ let stderr = Buffer.alloc(0);
408
+ let forceKillTimer = null;
409
+
410
+ function settle(result) {
411
+ if (settled) {
412
+ return;
413
+ }
414
+ settled = true;
415
+ clearTimeout(timeoutTimer);
416
+ clearTimeout(forceKillTimer);
417
+ resolve({
418
+ attempted: true,
419
+ ...result
420
+ });
421
+ }
422
+
423
+ function killChild(signal) {
424
+ try {
425
+ if (child && !child.killed) {
426
+ child.kill(signal);
427
+ }
428
+ } catch {
429
+ // Best effort; the close/error handlers will settle the ping.
430
+ }
431
+ }
432
+
433
+ const timeoutTimer = setTimeout(() => {
434
+ timedOut = true;
435
+ killChild('SIGTERM');
436
+ forceKillTimer = setTimeout(() => {
437
+ killChild('SIGKILL');
438
+ settle({
439
+ ok: false,
440
+ timedOut: true,
441
+ error: `Bridge ping timed out after ${timeoutMs}ms`
442
+ });
443
+ }, 500);
444
+ if (typeof forceKillTimer.unref === 'function') {
445
+ forceKillTimer.unref();
446
+ }
447
+ }, timeoutMs);
448
+ if (typeof timeoutTimer.unref === 'function') {
449
+ timeoutTimer.unref();
450
+ }
451
+
452
+ try {
453
+ child = spawn(spawnInvocation.command, spawnInvocation.args, {
454
+ env,
455
+ stdio: ['pipe', 'pipe', 'pipe'],
456
+ windowsVerbatimArguments: spawnInvocation.windowsVerbatimArguments === true,
457
+ windowsHide: true
458
+ });
459
+ } catch (error) {
460
+ settle({
461
+ ok: false,
462
+ error: `Unable to start bridge: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`,
463
+ errorCode: error.code || undefined
464
+ });
465
+ return;
466
+ }
467
+
468
+ child.on('error', error => {
469
+ settle({
470
+ ok: false,
471
+ error: `Unable to start bridge: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`,
472
+ errorCode: error.code || undefined
473
+ });
474
+ });
475
+
476
+ child.stdout.on('data', chunk => {
477
+ stdout = appendBoundedBuffer(stdout, chunk, MAX_DOCTOR_STDOUT_BYTES);
478
+ });
479
+
480
+ child.stderr.on('data', chunk => {
481
+ stderr = appendBoundedBuffer(stderr, chunk, MAX_DOCTOR_STDERR_BYTES);
482
+ });
483
+
484
+ child.stdin.on('error', () => {
485
+ // The close/error handlers produce the deterministic doctor result.
486
+ });
487
+
488
+ child.on('close', (exitCode, signal) => {
489
+ if (settled) {
490
+ return;
491
+ }
492
+ if (timedOut) {
493
+ settle({
494
+ ok: false,
495
+ timedOut: true,
496
+ exitCode,
497
+ signal: signal || undefined,
498
+ error: `Bridge ping timed out after ${timeoutMs}ms`
499
+ });
500
+ return;
501
+ }
502
+
503
+ if ((exitCode !== 0 && exitCode !== null) || signal) {
504
+ settle({
505
+ ok: false,
506
+ exitCode,
507
+ signal: signal || undefined,
508
+ error: summarizeBridgeFailure(exitCode, signal, stderr)
509
+ });
510
+ return;
511
+ }
512
+
513
+ let decoded;
514
+ try {
515
+ decoded = decodeFrames(stdout);
516
+ } catch (error) {
517
+ settle({
518
+ ok: false,
519
+ exitCode,
520
+ signal: signal || undefined,
521
+ error: `Bridge returned an invalid native message: ${error.message}`
522
+ });
523
+ return;
524
+ }
525
+
526
+ if (decoded.remainder.length) {
527
+ settle({
528
+ ok: false,
529
+ exitCode,
530
+ signal: signal || undefined,
531
+ error: 'Bridge returned trailing partial native output'
532
+ });
533
+ return;
534
+ }
535
+
536
+ const response = decoded.messages.find(item => item?.id === message.id);
537
+ if (response) {
538
+ settle({
539
+ ok: true,
540
+ exitCode,
541
+ signal: signal || undefined,
542
+ rawResponse: response,
543
+ response: sanitizePingResponse(response, options.pathOptions)
544
+ });
545
+ return;
546
+ }
547
+
548
+ settle({
549
+ ok: false,
550
+ exitCode,
551
+ signal: signal || undefined,
552
+ error: `Bridge did not return a response for ${message.id}`
553
+ });
554
+ });
555
+
556
+ try {
557
+ child.stdin.end(encodeMessage(message));
558
+ } catch (error) {
559
+ killChild('SIGTERM');
560
+ settle({
561
+ ok: false,
562
+ error: `Unable to write bridge ping: ${sanitizePathInMessage(error.message, bridgePath, options.pathOptions)}`
563
+ });
564
+ }
565
+ });
566
+ }
567
+
568
+ function buildDoctorBridgeSpawnInvocation(bridgePath, options = {}) {
569
+ const platform = options.platform || process.platform;
570
+ if (platform !== 'win32' || !/\.(?:cmd|bat)$/i.test(String(bridgePath))) {
571
+ return {
572
+ command: bridgePath,
573
+ args: []
574
+ };
575
+ }
576
+
577
+ return {
578
+ command: options.env?.ComSpec || options.env?.COMSPEC || 'cmd.exe',
579
+ args: ['/d', '/c', quoteWindowsCommandPath(bridgePath)],
580
+ windowsVerbatimArguments: true
581
+ };
582
+ }
583
+
584
+ function quoteWindowsCommandPath(targetPath) {
585
+ return `"${String(targetPath).replace(/"/g, '""')}"`;
586
+ }
587
+
588
+ function sanitizePingResponse(response, pathOptions) {
589
+ if (!response || typeof response !== 'object') {
590
+ return response;
591
+ }
592
+ if (response.ok !== true) {
593
+ return {
594
+ id: response.id,
595
+ ok: response.ok,
596
+ error: sanitizeNativeError(response.error, pathOptions)
597
+ };
598
+ }
599
+ const native = response.result && typeof response.result === 'object' ? response.result : {};
600
+ return {
601
+ id: response.id,
602
+ ok: true,
603
+ result: sanitizeNativePingResult(native)
604
+ };
605
+ }
606
+
607
+ function sanitizeNativePingResult(native) {
608
+ return {
609
+ host: native.host,
610
+ platform: native.platform,
611
+ protocolVersion: native.protocolVersion,
612
+ supportedProtocol: native.supportedProtocol,
613
+ capabilities: native.capabilities && typeof native.capabilities === 'object'
614
+ ? Object.keys(native.capabilities).filter(capability => native.capabilities[capability] === true).sort()
615
+ : [],
616
+ minExtensionVersion: native.minExtensionVersion,
617
+ version: native.version,
618
+ environment: {
619
+ codexOk: native.environment?.codex?.ok === true,
620
+ latexOk: native.environment?.latex?.ok === true
621
+ }
622
+ };
623
+ }
624
+
625
+ function sanitizeNativeError(error, pathOptions) {
626
+ if (!error || typeof error !== 'object') {
627
+ return undefined;
628
+ }
629
+ return {
630
+ code: typeof error.code === 'string' ? error.code : undefined,
631
+ message: typeof error.message === 'string'
632
+ ? sanitizeDiagnosticMessage(error.message, pathOptions)
633
+ : undefined
634
+ };
635
+ }
636
+
637
+ function sanitizeCompatibility(compatibility) {
638
+ if (!compatibility || typeof compatibility !== 'object') {
639
+ return null;
640
+ }
641
+ return {
642
+ status: compatibility.status,
643
+ classification: compatibility.classification,
644
+ nativeVersion: compatibility.nativeVersion,
645
+ currentNativeVersion: compatibility.currentNativeVersion,
646
+ extensionVersion: compatibility.extensionVersion,
647
+ requiredVersion: compatibility.requiredVersion,
648
+ platform: compatibility.platform,
649
+ missingCapabilities: Array.isArray(compatibility.missingCapabilities)
650
+ ? compatibility.missingCapabilities.slice()
651
+ : [],
652
+ missingUpdateCapabilities: Array.isArray(compatibility.missingUpdateCapabilities)
653
+ ? compatibility.missingUpdateCapabilities.slice()
654
+ : [],
655
+ releaseUrl: compatibility.releaseUrl,
656
+ updateCommand: compatibility.updateCommand
657
+ };
658
+ }
659
+
660
+ function sanitizeRegistrationTarget(target, pathOptions) {
661
+ const sanitized = {
662
+ kind: target.kind,
663
+ browser: target.browser,
664
+ manifestPath: normalizeDiagnosticPath(target.manifestPath, pathOptions)
665
+ };
666
+ if (target.registryKey) {
667
+ sanitized.registryKey = target.registryKey;
668
+ }
669
+ return sanitized;
670
+ }
671
+
672
+ function appendBoundedBuffer(current, chunk, maxBytes) {
673
+ const next = Buffer.concat([current, chunk]);
674
+ if (next.length <= maxBytes) {
675
+ return next;
676
+ }
677
+ return next.subarray(next.length - maxBytes);
678
+ }
679
+
680
+ function summarizeBridgeFailure(exitCode, signal, stderr) {
681
+ const stderrSummary = stderr && stderr.length
682
+ ? firstDiagnosticLine(stderr.toString('utf8'))
683
+ : '';
684
+ if (exitCode !== 0 && exitCode !== null) {
685
+ return stderrSummary
686
+ ? `Bridge exited with status ${exitCode}: ${stderrSummary}`
687
+ : `Bridge exited with status ${exitCode}`;
688
+ }
689
+ if (signal) {
690
+ return stderrSummary
691
+ ? `Bridge exited with signal ${signal}: ${stderrSummary}`
692
+ : `Bridge exited with signal ${signal}`;
693
+ }
694
+ if (stderrSummary) {
695
+ return `Bridge wrote stderr but no native response: ${stderrSummary}`;
696
+ }
697
+ return 'Bridge produced no native response';
698
+ }
699
+
700
+ function sanitizePathInMessage(message, targetPath, pathOptions) {
701
+ const firstLine = firstDiagnosticLine(message);
702
+ if (!targetPath || !firstLine.includes(targetPath)) {
703
+ return firstLine;
704
+ }
705
+ return firstLine.split(targetPath).join(normalizeDiagnosticPath(targetPath, pathOptions));
706
+ }
707
+
708
+ function sanitizeDiagnosticMessage(message, pathOptions = {}) {
709
+ const firstLine = firstDiagnosticLine(message);
710
+ if (pathOptions.revealPaths) {
711
+ return firstLine;
712
+ }
713
+
714
+ const platform = pathOptions.platform || process.platform;
715
+ const platformPath = getPlatformPath(platform);
716
+ const homeDir = pathOptions.homeDir || getHomeFromEnv(pathOptions.env || process.env, platform);
717
+ let redacted = firstLine;
718
+
719
+ if (homeDir) {
720
+ const normalizedHome = platformPath.normalize(String(homeDir));
721
+ redacted = platform === 'win32'
722
+ ? replaceCaseInsensitive(redacted, normalizedHome, '~')
723
+ : redacted.split(normalizedHome).join('~');
724
+ }
725
+
726
+ if (hasUnredactedAbsolutePath(redacted)) {
727
+ return 'Native error message redacted (absolute path omitted)';
728
+ }
729
+
730
+ return redacted;
731
+ }
732
+
733
+ function firstDiagnosticLine(message) {
734
+ return String(message || '')
735
+ .split(/\r?\n/)
736
+ .find(line => line.trim() && !/^\s*at\s+/.test(line)) ||
737
+ 'unknown error';
738
+ }
739
+
740
+ function replaceCaseInsensitive(value, needle, replacement) {
741
+ const lowerValue = String(value).toLowerCase();
742
+ const lowerNeedle = String(needle).toLowerCase();
743
+ if (!lowerNeedle) {
744
+ return value;
745
+ }
746
+
747
+ let result = '';
748
+ let offset = 0;
749
+ let index = lowerValue.indexOf(lowerNeedle, offset);
750
+ while (index !== -1) {
751
+ result += value.slice(offset, index) + replacement;
752
+ offset = index + needle.length;
753
+ index = lowerValue.indexOf(lowerNeedle, offset);
754
+ }
755
+ return result + value.slice(offset);
756
+ }
757
+
758
+ function hasUnredactedAbsolutePath(message) {
759
+ const candidate = String(message)
760
+ .replace(/~\/[^\s"'`<>|]+/g, '~')
761
+ .replace(/~\\[^\s"'`<>|]+/g, '~');
762
+ if (/[A-Za-z]:[\\/]/.test(candidate)) {
763
+ return true;
764
+ }
765
+ if (candidate.includes('\\')) {
766
+ return true;
767
+ }
768
+ return candidate.includes('/');
769
+ }
770
+
771
+ function normalizeTimeoutMs(timeoutMs) {
772
+ const value = Number(timeoutMs);
773
+ if (!Number.isFinite(value) || value <= 0) {
774
+ return DEFAULT_DOCTOR_TIMEOUT_MS;
775
+ }
776
+ return Math.max(1, Math.floor(value));
777
+ }
778
+
779
+ function normalizeBrowser(browser) {
780
+ if (!browser || browser === 'auto') {
781
+ return 'auto';
782
+ }
783
+ if (browser === 'chrome' || browser === 'chromium') {
784
+ return browser;
785
+ }
786
+ throw new Error(`Unsupported doctor browser: ${browser}`);
787
+ }
788
+
789
+ function resolveRuntimeRoot(runtimeRoot) {
790
+ if (!runtimeRoot) {
791
+ return '';
792
+ }
793
+ return path.isAbsolute(String(runtimeRoot))
794
+ ? String(runtimeRoot)
795
+ : path.resolve(String(runtimeRoot));
796
+ }
797
+
798
+ function getHomeFromEnv(env, platform) {
799
+ if (platform === 'win32') {
800
+ return env.USERPROFILE || env.HOME || os.homedir();
801
+ }
802
+ return env.HOME || env.USERPROFILE || os.homedir();
803
+ }
804
+
805
+ function isAbsolutePathForPlatform(targetPath, platform) {
806
+ if (platform === 'win32') {
807
+ return path.win32.isAbsolute(targetPath);
808
+ }
809
+ if (platform === 'darwin' || platform === 'linux') {
810
+ return path.posix.isAbsolute(targetPath);
811
+ }
812
+ return path.isAbsolute(targetPath);
813
+ }
814
+
815
+ function getPlatformPath(platform) {
816
+ return platform === 'win32' ? path.win32 : path.posix;
817
+ }
818
+
819
+ module.exports = {
820
+ buildDoctorBridgeSpawnInvocation,
821
+ classifyDoctorResult,
822
+ formatDoctorHuman,
823
+ normalizeDiagnosticPath,
824
+ normalizeBrowser,
825
+ runDoctor
826
+ };