codex-overleaf-link 2.5.0 → 2.5.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 (168) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +39 -27
  3. package/extension/bootstrap/background.js +52 -9
  4. package/extension/bootstrap/manifest.template.json +1 -1
  5. package/extension/bootstrap/popup.html +1 -0
  6. package/extension/bootstrap/popupFallback.js +55 -0
  7. package/extension/bootstrap/updateStatus.js +1 -0
  8. package/extension/src/background.js +206 -53
  9. package/extension/src/backgroundBootstrapMigration.js +181 -0
  10. package/extension/src/backgroundSharedSessions.js +74 -14
  11. package/extension/src/backgroundUpdateCoordinator.js +118 -21
  12. package/extension/src/backgroundUpdateSafety.js +173 -0
  13. package/extension/src/content/activeTurnControl.js +8 -2
  14. package/extension/src/content/applyResultFormatters.js +46 -73
  15. package/extension/src/content/assetTransferBroker.js +72 -14
  16. package/extension/src/content/composerAttachments.js +93 -45
  17. package/extension/src/content/composerPanel.js +18 -0
  18. package/extension/src/content/contentRuntime.js +661 -676
  19. package/extension/src/content/diagnosticsController.js +122 -96
  20. package/extension/src/content/diagnosticsExport.js +19 -5
  21. package/extension/src/content/diagnosticsPanel.js +30 -0
  22. package/extension/src/content/directoryCleanupRecovery.js +54 -0
  23. package/extension/src/content/generated/content.bundle.js +6879 -2786
  24. package/extension/src/content/generated/content.bundle.meta.json +11 -3
  25. package/extension/src/content/globalPreferencesController.js +1 -1
  26. package/extension/src/content/legacyGlobalRegistry.js +6 -0
  27. package/extension/src/content/localSkillsPanel.js +102 -12
  28. package/extension/src/content/markdownText.js +5 -6
  29. package/extension/src/content/modelPicker.js +7 -4
  30. package/extension/src/content/nativeCompatibilityController.js +45 -20
  31. package/extension/src/content/otWarmMirror.js +4 -5
  32. package/extension/src/content/otWarmMirrorController.js +17 -20
  33. package/extension/src/content/pageBridgeClient.js +15 -2
  34. package/extension/src/content/panelMaintenance.js +40 -12
  35. package/extension/src/content/panelRenderer.js +6 -0
  36. package/extension/src/content/projectProviderSelection.js +13 -4
  37. package/extension/src/content/projectSessionCleanup.js +109 -62
  38. package/extension/src/content/projectSettingsCoordinator.js +39 -2
  39. package/extension/src/content/providerSettingsCoordinator.js +46 -8
  40. package/extension/src/content/providerSettingsDialog.js +71 -13
  41. package/extension/src/content/recentProjects.js +11 -5
  42. package/extension/src/content/runActivitySummary.js +40 -6
  43. package/extension/src/content/runCompletionMeta.js +344 -0
  44. package/extension/src/content/runController.js +37 -14
  45. package/extension/src/content/runFailureNotice.js +21 -12
  46. package/extension/src/content/runGuidanceController.js +72 -27
  47. package/extension/src/content/runGuidanceView.js +59 -10
  48. package/extension/src/content/runQueueScheduler.js +148 -42
  49. package/extension/src/content/runRecoveryActions.js +146 -0
  50. package/extension/src/content/runResultActions.js +8 -125
  51. package/extension/src/content/runReviewController.js +574 -0
  52. package/extension/src/content/runScrollLayout.js +21 -2
  53. package/extension/src/content/runTimelineView.js +146 -153
  54. package/extension/src/content/scopedPersistencePanelState.js +51 -6
  55. package/extension/src/content/selectionContextView.js +1 -1
  56. package/extension/src/content/sensitivePreflight.js +140 -0
  57. package/extension/src/content/sessionForkController.js +80 -17
  58. package/extension/src/content/sessionManager.js +1 -1
  59. package/extension/src/content/settingsPanel.js +7 -3
  60. package/extension/src/content/settingsWorkbench.js +5 -0
  61. package/extension/src/content/sharedSessionViewSync.js +45 -8
  62. package/extension/src/content/updateIdle.js +4 -1
  63. package/extension/src/content/updateNotice.js +60 -8
  64. package/extension/src/content/updateSafetyReceiver.js +58 -0
  65. package/extension/src/content/writebackController.js +53 -4
  66. package/extension/src/content/writebackOrchestrator.js +82 -73
  67. package/extension/src/content/writingStyleSettings.js +35 -11
  68. package/extension/src/page/binaryAssetUploader.js +136 -14
  69. package/extension/src/page/fileMover.js +256 -0
  70. package/extension/src/page/ownedDirectoryCleanup.js +94 -0
  71. package/extension/src/page/referenceProjects.js +1 -0
  72. package/extension/src/page/reviewContext.js +113 -0
  73. package/extension/src/page/saveState.js +6 -4
  74. package/extension/src/page/snapshotRouter.js +84 -39
  75. package/extension/src/page/textFileCreator.js +105 -25
  76. package/extension/src/page/trackedChangeCapture.js +12 -2
  77. package/extension/src/page/trackedChangeOwnership.js +19 -1
  78. package/extension/src/page/trackedChangeReplay.js +41 -21
  79. package/extension/src/page/trackedChangesLifecycle.js +217 -199
  80. package/extension/src/page/treeOperations.js +7 -10
  81. package/extension/src/page/updateSafetyProbe.js +41 -0
  82. package/extension/src/page/writebackReceiptJournal.js +2 -0
  83. package/extension/src/page/writebackRouter.js +80 -79
  84. package/extension/src/pageBridge.js +19 -3
  85. package/extension/src/popup.js +62 -10
  86. package/extension/src/shared/agentTranscript.js +23 -0
  87. package/extension/src/shared/auditRecords.js +2 -0
  88. package/extension/src/shared/compatibility.js +22 -6
  89. package/extension/src/shared/compileAdapter.js +79 -57
  90. package/extension/src/shared/composerAttachmentDraft.js +151 -0
  91. package/extension/src/shared/directoryCleanupProof.js +43 -0
  92. package/extension/src/shared/failureReasons.js +309 -3
  93. package/extension/src/shared/globalPreferences.js +30 -13
  94. package/extension/src/shared/governanceRules.js +4 -37
  95. package/extension/src/shared/i18n.js +298 -2
  96. package/extension/src/shared/managedUpdateProjection.js +2 -2
  97. package/extension/src/shared/nativeRequestIdentity.js +1 -0
  98. package/extension/src/shared/pageRpcContract.js +5 -4
  99. package/extension/src/shared/projectFiles.js +21 -44
  100. package/extension/src/shared/providerProfiles.js +5 -3
  101. package/extension/src/shared/runInputQueue.js +48 -5
  102. package/extension/src/shared/selectionContext.js +12 -3
  103. package/extension/src/shared/sessionState.js +72 -27
  104. package/extension/src/shared/settlementFacts.js +10 -2
  105. package/extension/src/shared/sharedSessionBridge.js +37 -14
  106. package/extension/src/shared/sharedSessionTransfer.js +140 -0
  107. package/extension/src/shared/staleGuard.js +118 -13
  108. package/extension/src/shared/storageDb.js +58 -26
  109. package/extension/src/shared/storageMigration.js +71 -111
  110. package/extension/src/shared/storageRunActions.js +209 -26
  111. package/extension/src/shared/subagentActivityModel.js +1 -1
  112. package/extension/src/shared/undoOperations.js +136 -13
  113. package/extension/src/shared/updateConsent.js +6 -4
  114. package/extension/src/shared/updateRuntimeIdentity.js +1 -1
  115. package/extension/src/shared/writebackIntent.js +16 -0
  116. package/extension/src/shared/writebackOperationEvidence.js +4 -15
  117. package/extension/src/shared/writebackSettlement.js +38 -38
  118. package/extension/styles/panel.css +19 -5
  119. package/native-host/src/anthropicBridgeRequest.js +4 -2
  120. package/native-host/src/anthropicBridgeResponse.js +6 -2
  121. package/native-host/src/anthropicMessagesBridge.js +24 -16
  122. package/native-host/src/bootstrapMigration.js +296 -0
  123. package/native-host/src/bootstrapMigrationContract.js +101 -0
  124. package/native-host/src/bootstrapUpdate.js +233 -0
  125. package/native-host/src/bootstrapUpdateWatchdog.js +44 -0
  126. package/native-host/src/chatBridgeResponse.js +10 -0
  127. package/native-host/src/chatCompletionsBridge.js +21 -15
  128. package/native-host/src/codexConfigPaths.js +106 -0
  129. package/native-host/src/codexHome.js +66 -73
  130. package/native-host/src/codexHomeScope.js +124 -0
  131. package/native-host/src/codexInheritedAuth.js +90 -0
  132. package/native-host/src/codexLocalSkills.js +301 -0
  133. package/native-host/src/codexPromptAssembly.js +31 -12
  134. package/native-host/src/codexProviderTest.js +24 -26
  135. package/native-host/src/codexSessionRunner.js +110 -183
  136. package/native-host/src/codexSessionTiming.js +18 -8
  137. package/native-host/src/codexSkillIsolation.js +170 -0
  138. package/native-host/src/codexThreadFork.js +19 -3
  139. package/native-host/src/index.js +19 -7
  140. package/native-host/src/managedInstall.js +110 -35
  141. package/native-host/src/managedInstallRecovery.js +140 -0
  142. package/native-host/src/managedRuntimeEvidence.js +57 -0
  143. package/native-host/src/mirrorWorkspace.js +92 -22
  144. package/native-host/src/nativeDoctor.js +15 -0
  145. package/native-host/src/nativeHelperLifecycle.js +21 -0
  146. package/native-host/src/nativeHostRegistration.js +33 -0
  147. package/native-host/src/nativeRunLifecycle.js +71 -0
  148. package/native-host/src/processStopBarrier.js +42 -7
  149. package/native-host/src/projectPathPlatform.js +30 -0
  150. package/native-host/src/providerHttp.js +80 -0
  151. package/native-host/src/providerProfile.js +4 -2
  152. package/native-host/src/providerReasoning.js +2 -7
  153. package/native-host/src/providerRuntime.js +1 -0
  154. package/native-host/src/providerStore.js +72 -14
  155. package/native-host/src/responsesPassthroughBridge.js +266 -52
  156. package/native-host/src/runtimeInstaller.js +21 -1
  157. package/native-host/src/selectionScope.js +36 -8
  158. package/native-host/src/subagentAdmission.js +26 -0
  159. package/native-host/src/subagentBroker.js +79 -66
  160. package/native-host/src/subagentTelemetry.js +19 -10
  161. package/native-host/src/subagentWorkspacePath.js +5 -2
  162. package/native-host/src/taskRunnerRuntime.js +77 -54
  163. package/native-host/src/tomlStatements.js +140 -0
  164. package/native-host/src/updateManager.js +297 -39
  165. package/native-host/src/writingStyleRuntime.js +57 -29
  166. package/native-host/src/writingStyleStore.js +73 -0
  167. package/package.json +1 -1
  168. package/scripts/install-managed.mjs +1 -0
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawn } = require('node:child_process');
7
+ const { version: PACKAGE_VERSION } = require('../../package.json');
8
+ const { getCodexOverleafSkillsRoot } = require('./localSkills');
9
+ const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
10
+ const { runOwnedNativeHelper } = require('./nativeHelperLifecycle');
11
+ const { stopCodexAppServer, throwIfAborted, getAbortReason } = require('./codexSessionTiming');
12
+
13
+ const CODEX_LOCAL_SKILL_SCOPE = 'codex-local';
14
+ const MAX_SKILL_BYTES = 64 * 1024;
15
+ const MAX_DISCOVERY_BYTES = 4 * 1024 * 1024;
16
+ const DISCOVERY_TIMEOUT_MS = 30000;
17
+
18
+ // System skills are regenerated inside each scoped home. Ordinary skills keep
19
+ // their physical source identity, including aliases and same-name skills.
20
+ function createCodexLocalSkillIdentity(skill = {}, env = process.env) {
21
+ const name = String(skill.name || '').trim();
22
+ let source;
23
+ if (skill.scope === 'system' && name) source = `system:${name}`;
24
+ else {
25
+ const location = canonicalSkillPath(skill.path);
26
+ if (!location) return '';
27
+ source = `path:${location}`;
28
+ }
29
+ return `local-${crypto.createHash('sha256').update(source).digest('hex')}`;
30
+ }
31
+
32
+ function isCodexOverleafSkillSource(skill = {}, env = process.env) {
33
+ if (!path.isAbsolute(String(skill.path || ''))) return false;
34
+ const root = getCodexOverleafSkillsRoot({ env });
35
+ const roots = comparablePaths(root);
36
+ return comparablePaths(skill.path).some(candidate => roots.some(base => isInside(candidate, base)));
37
+ }
38
+
39
+ async function listCodexLocalSkills(options = {}) {
40
+ const { entries, discoveryWarnings } = await discoverLocalSkills(options);
41
+ return { skills: entries.map(entry => entry.metadata), discoveryWarnings };
42
+ }
43
+
44
+ async function loadSelectedCodexLocalSkill({ skillId, loadCodexLocalSkills = true,
45
+ enabledCodexLocalSkillIds, env = process.env, signal, timeoutMs } = {}) {
46
+ const id = String(skillId || '').trim();
47
+ const empty = { skill: null, missing: [], ignored: [] };
48
+ if (!id) return empty;
49
+ if (loadCodexLocalSkills === false) return { ...empty, ignored: [{ id, reason: 'codex_local_skills_disabled' }] };
50
+ if (Array.isArray(enabledCodexLocalSkillIds) && !enabledCodexLocalSkillIds.includes(id)) {
51
+ return { ...empty, ignored: [{ id, reason: 'codex_local_skill_disabled' }] };
52
+ }
53
+ if (!/^local-[a-f0-9]{64}$/.test(id)) return { ...empty, missing: [id] };
54
+ const { entries } = await discoverLocalSkills({ env, signal, timeoutMs });
55
+ throwIfAborted(signal);
56
+ const entry = entries.find(item => item.metadata.id === id);
57
+ if (!entry) return { ...empty, missing: [id] };
58
+ if (!Array.isArray(enabledCodexLocalSkillIds) && entry.metadata.enabled === false) {
59
+ return { ...empty, ignored: [{ id, reason: 'codex_local_skill_disabled' }] };
60
+ }
61
+ const content = readSelectedContent(entry);
62
+ if (content === null) return { ...empty, missing: [id] };
63
+ return { ...empty, skill: { ...entry.metadata, path: entry.canonicalPath, content } };
64
+ }
65
+
66
+ async function discoverLocalSkills({ env = process.env, signal, timeoutMs = DISCOVERY_TIMEOUT_MS } = {}) {
67
+ return runOwnedNativeHelper(async ownedSignal => {
68
+ throwIfAborted(ownedSignal);
69
+ // Lazy import avoids the home -> isolation -> identity helper cycle.
70
+ const { buildCodexHomeEnv } = require('./codexHome');
71
+ const childEnv = buildCodexHomeEnv(env, { isolateRuntimeHome: true, forceIsolatedSkillSettings: true,
72
+ loadCodexLocalSkills: true, loadCodexOverleafSkills: false });
73
+ throwIfAborted(ownedSignal);
74
+ const result = await runSkillsMetadataProcess(childEnv, ownedSignal, timeoutMs);
75
+ throwIfAborted(ownedSignal);
76
+ return normalizeCatalog(result, childEnv);
77
+ }, signal);
78
+ }
79
+
80
+ function normalizeCatalog(result, env) {
81
+ if (!Array.isArray(result?.data)) throw codedError('codex_local_skills_invalid_response', 'Codex returned an invalid local skill catalog.');
82
+ const entries = new Map();
83
+ let skippedCount = 0;
84
+ for (const group of result.data) {
85
+ if (!Array.isArray(group?.skills)) throw codedError('codex_local_skills_invalid_response', 'Codex returned an invalid local skill catalog.');
86
+ if (group.errors !== undefined && !Array.isArray(group.errors)) {
87
+ throw codedError('codex_local_skills_invalid_response', 'Codex returned an invalid local skill catalog.');
88
+ }
89
+ // Invalid source files are omitted by Codex. Keep valid skills available
90
+ // while exposing only a count, never filesystem paths or error contents.
91
+ skippedCount += group.errors?.length || 0;
92
+ for (const skill of group.skills) {
93
+ if (!skill || typeof skill !== 'object' || isCodexOverleafSkillSource(skill, env)) continue;
94
+ const name = cleanText(skill.name, 160);
95
+ const canonicalPath = canonicalSkillPath(skill.path);
96
+ const id = createCodexLocalSkillIdentity(skill, env);
97
+ if (!id || !name || !canonicalPath) continue;
98
+ const sourceKind = getSkillSourceKind(skill, canonicalPath, env);
99
+ const description = cleanText(skill.description, 2048);
100
+ const metadata = { id, name, title: cleanText(skill.interface?.displayName, 160) || name,
101
+ description, preview: cleanText(skill.interface?.shortDescription || skill.shortDescription || description, 240),
102
+ scope: CODEX_LOCAL_SKILL_SCOPE, sourceKind,
103
+ sourceLabel: sourceKind === 'plugin' ? cleanText(skill.pluginId, 160) || pluginNamePrefix(name) : '',
104
+ enabled: skill.enabled !== false, removable: false };
105
+ const existing = entries.get(id);
106
+ if (existing) {
107
+ // An enabled alias makes this same physical skill discoverable.
108
+ existing.metadata.enabled ||= metadata.enabled;
109
+ continue;
110
+ }
111
+ entries.set(id, { metadata, canonicalPath, discoveredPath: String(skill.path) });
112
+ }
113
+ }
114
+ const resolvedEntries = Array.from(entries.values());
115
+ const names = new Map(), titles = new Map();
116
+ for (const { metadata } of resolvedEntries) {
117
+ names.set(metadata.name, (names.get(metadata.name) || 0) + 1);
118
+ titles.set(metadata.title, (titles.get(metadata.title) || 0) + 1);
119
+ }
120
+ for (const entry of resolvedEntries) {
121
+ if (names.get(entry.metadata.name) > 1 || titles.get(entry.metadata.title) > 1) {
122
+ entry.metadata.sourceHint = buildSourceHint(entry, env, resolvedEntries);
123
+ }
124
+ }
125
+ return {
126
+ entries: resolvedEntries.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name) || a.metadata.id.localeCompare(b.metadata.id)),
127
+ discoveryWarnings: skippedCount ? [{ code: 'codex_local_skills_skipped', count: skippedCount }] : []
128
+ };
129
+ }
130
+
131
+ function buildSourceHint(entry, env, entries) {
132
+ if (entry.metadata.sourceKind === 'system') return `.system/${entry.metadata.name}`;
133
+ const { getUserCodexHome } = require('./codexHome');
134
+ const userHome = getUserCodexHome(env), directory = path.dirname(entry.canonicalPath);
135
+ const relativeDirectory = root => {
136
+ for (const base of comparablePaths(root)) {
137
+ if (directory === base || isInside(directory, base)) return path.relative(base, directory).split(path.sep).join('/');
138
+ }
139
+ return null;
140
+ };
141
+ if (entry.metadata.sourceKind === 'plugin') {
142
+ const relative = relativeDirectory(path.join(userHome, 'plugins'));
143
+ if (relative !== null) return `plugins/${relative.replace(/^cache\//, '')}`;
144
+ }
145
+ const relative = relativeDirectory(userHome);
146
+ if (relative !== null) return relative || path.basename(directory);
147
+ // A user skill symlink may point outside the home. Recover its original
148
+ // relative entry from the composed discovery path without exposing its target.
149
+ const discovered = path.resolve(entry.discoveredPath), runtimeSkills = path.join(String(env.CODEX_HOME || ''), 'skills');
150
+ if (isInside(discovered, runtimeSkills)) {
151
+ const inSkills = path.relative(runtimeSkills, discovered);
152
+ if (canonicalSkillPath(path.join(userHome, 'skills', inSkills)) === entry.canonicalPath) {
153
+ return `skills/${path.dirname(inSkills).split(path.sep).join('/')}`;
154
+ }
155
+ }
156
+ const parts = directory.split(path.sep).filter(Boolean);
157
+ const peers = entries.filter(other => other !== entry && (other.metadata.name === entry.metadata.name
158
+ || other.metadata.title === entry.metadata.title)).map(other => path.dirname(other.canonicalPath).split(path.sep).filter(Boolean));
159
+ let count = Math.min(3, parts.length);
160
+ while (count < parts.length && peers.some(other => other.slice(-count).join('/') === parts.slice(-count).join('/'))) count++;
161
+ return `…/${parts.slice(-count).join('/')}`;
162
+ }
163
+
164
+ function getSkillSourceKind(skill, canonicalPath, env) {
165
+ if (skill.scope === 'system') return 'system';
166
+ if (skill.pluginId) return 'plugin';
167
+ // Current app-server metadata reports plugin skills as scope "user" and
168
+ // does not carry a pluginId. Classify only returned files under the active
169
+ // user plugin root; discovering old cache directories is never necessary.
170
+ const { getUserCodexHome } = require('./codexHome');
171
+ const pluginRoots = comparablePaths(path.join(getUserCodexHome(env), 'plugins'));
172
+ if (pluginRoots.some(root => isInside(canonicalPath, root))) return 'plugin';
173
+ return skill.scope === 'user' ? 'user' : 'local';
174
+ }
175
+
176
+ function pluginNamePrefix(name) {
177
+ const prefix = name.includes(':') ? name.slice(0, name.indexOf(':')).trim() : '';
178
+ return /[/\\]/.test(prefix) ? '' : prefix;
179
+ }
180
+
181
+ function runSkillsMetadataProcess(env, signal, timeoutMs) {
182
+ const command = resolveCodexCommand(env);
183
+ if (!command) throw codedError('codex_not_found', 'Codex CLI was not found.');
184
+ return new Promise((resolve, reject) => {
185
+ throwIfAborted(signal);
186
+ const child = spawn(command, ['app-server', '--listen', 'stdio://'], {
187
+ cwd: env.CODEX_HOME, env, shell: shouldUseShellForCommand(command, env),
188
+ detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe']
189
+ });
190
+ const pending = new Map();
191
+ let sequence = 0, buffer = '', outputBytes = 0, settled = false;
192
+ const onAbort = () => finish(getAbortReason(signal));
193
+ const budget = Math.min(DISCOVERY_TIMEOUT_MS, Math.max(1, Number(timeoutMs) || DISCOVERY_TIMEOUT_MS));
194
+ const timer = setTimeout(() => finish(codedError('codex_local_skills_timeout', 'Local Codex skill discovery timed out.')), budget);
195
+ signal.addEventListener('abort', onAbort, { once: true });
196
+ child.stdout.setEncoding('utf8');
197
+ child.stderr.setEncoding('utf8');
198
+ child.stdout.on('data', chunk => {
199
+ if (settled || !countOutput(chunk)) return;
200
+ buffer += chunk;
201
+ const lines = buffer.split(/\r?\n/);
202
+ buffer = lines.pop() || '';
203
+ for (const line of lines) {
204
+ if (settled) break;
205
+ if (line.trim()) handleMessage(line);
206
+ }
207
+ });
208
+ // Stderr may contain user configuration paths or server diagnostics. Count
209
+ // it for bounded lifetime, but never return it as catalog/UI content.
210
+ child.stderr.on('data', chunk => { if (!settled) countOutput(chunk); });
211
+ child.stdin.on('error', () => finish(codedError('codex_local_skills_failed', 'Local Codex skill discovery failed.')));
212
+ child.on('error', error => finish(codedError(error.code === 'ENOENT' ? 'codex_not_found' : 'codex_local_skills_failed', 'Codex could not start local skill discovery.')));
213
+ child.on('close', () => { if (!settled) finish(codedError('codex_local_skills_failed', 'Codex exited before local skill discovery completed.')); });
214
+ (async () => {
215
+ throwIfAborted(signal);
216
+ await request('initialize', { clientInfo: { name: 'codex-overleaf-link', version: PACKAGE_VERSION }, capabilities: null });
217
+ child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
218
+ const result = await request('skills/list', { cwd: env.CODEX_HOME, cwds: [env.CODEX_HOME], includeDisabled: true, forceReload: true });
219
+ finish(null, result);
220
+ })().catch(finish);
221
+
222
+ function countOutput(chunk) {
223
+ outputBytes += Buffer.byteLength(chunk);
224
+ if (outputBytes <= MAX_DISCOVERY_BYTES) return true;
225
+ finish(codedError('codex_local_skills_output_limit', 'Local Codex skill discovery exceeded its output limit.'));
226
+ return false;
227
+ }
228
+ function request(method, params) {
229
+ return new Promise((resolveRequest, rejectRequest) => {
230
+ if (settled) return rejectRequest(codedError('codex_local_skills_closed', 'Local Codex skill discovery has closed.'));
231
+ const id = ++sequence;
232
+ pending.set(id, { resolve: resolveRequest, reject: rejectRequest });
233
+ child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
234
+ });
235
+ }
236
+ function handleMessage(line) {
237
+ let message;
238
+ try { message = JSON.parse(line); } catch { return; }
239
+ const waiter = pending.get(message?.id);
240
+ if (waiter && (Object.hasOwn(message, 'result') || message.error)) {
241
+ pending.delete(message.id);
242
+ if (message.error) waiter.reject(codedError('codex_local_skills_failed', 'Codex could not list local skills.'));
243
+ else waiter.resolve(message.result);
244
+ } else if (message && Object.hasOwn(message, 'id') && message.method) {
245
+ child.stdin.write(`${JSON.stringify({ id: message.id, error: { code: -32601, message: 'Unsupported during skill discovery' } })}\n`);
246
+ }
247
+ }
248
+ function finish(error, result) {
249
+ if (settled) return;
250
+ settled = true;
251
+ clearTimeout(timer);
252
+ signal.removeEventListener('abort', onAbort);
253
+ for (const waiter of pending.values()) waiter.reject(error || codedError('codex_local_skills_closed', 'Local Codex skill discovery has closed.'));
254
+ pending.clear();
255
+ stopCodexAppServer(child, { processGroup: process.platform !== 'win32', reason: error }).then(() => {
256
+ if (error) reject(error); else resolve(result);
257
+ }, reject);
258
+ }
259
+ });
260
+ }
261
+
262
+ function readSelectedContent(entry) {
263
+ let fd;
264
+ try {
265
+ if (canonicalSkillPath(entry.discoveredPath) !== entry.canonicalPath) return null;
266
+ fd = fs.openSync(entry.canonicalPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
267
+ const stat = fs.fstatSync(fd);
268
+ if (!stat.isFile() || stat.size > MAX_SKILL_BYTES) return null;
269
+ const bytes = Buffer.alloc(MAX_SKILL_BYTES + 1);
270
+ let length = 0, count;
271
+ while ((count = fs.readSync(fd, bytes, length, bytes.length - length, length)) > 0) {
272
+ length += count;
273
+ if (length > MAX_SKILL_BYTES) return null;
274
+ }
275
+ if (canonicalSkillPath(entry.discoveredPath) !== entry.canonicalPath || bytes.subarray(0, length).includes(0)) return null;
276
+ const current = fs.statSync(entry.canonicalPath);
277
+ if (current.dev !== stat.dev || current.ino !== stat.ino) return null;
278
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, length));
279
+ } catch (error) {
280
+ if (['ENOENT', 'ENOTDIR', 'EACCES', 'ELOOP'].includes(error.code) || error instanceof TypeError) return null;
281
+ throw error;
282
+ } finally { if (fd !== undefined) fs.closeSync(fd); }
283
+ }
284
+
285
+ function canonicalSkillPath(value) {
286
+ if (!path.isAbsolute(String(value || '')) || path.basename(String(value)) !== 'SKILL.md') return '';
287
+ try {
288
+ const resolved = fs.realpathSync(String(value));
289
+ return fs.statSync(resolved).isFile() ? resolved : '';
290
+ } catch { return ''; }
291
+ }
292
+ function comparablePaths(value) {
293
+ const resolved = path.resolve(value);
294
+ try { return [resolved, fs.realpathSync(resolved)]; } catch { return [resolved]; }
295
+ }
296
+ function isInside(candidate, root) { const relative = path.relative(root, candidate); return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); }
297
+ function cleanText(value, limit) { return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim().slice(0, limit) : ''; }
298
+ function codedError(code, message) { return Object.assign(new Error(message), { code }); }
299
+
300
+ module.exports = { CODEX_LOCAL_SKILL_SCOPE, createCodexLocalSkillIdentity, isCodexOverleafSkillSource,
301
+ listCodexLocalSkills, loadSelectedCodexLocalSkill };
@@ -31,8 +31,7 @@ function buildCodexTurnPrompt(options = {}) {
31
31
  '',
32
32
  'Current Overleaf workspace:',
33
33
  `- Project: ${context.projectKey || context.projectId || 'unknown'}`,
34
- '- The local workspace was synced from Overleaf immediately before this turn.',
35
- '- If the recent session history conflicts with the files in the workspace, trust the files.',
34
+ ...formatWorkspaceSnapshotContext(context.unreadFiles),
36
35
  ...(isSkillInstallerInvocation(context.skillInvocation) ? [] : ['', formatOverleafWorkspaceRules()]),
37
36
  '',
38
37
  'Project location citation rules:',
@@ -131,6 +130,7 @@ function normalizePromptOptions(options = {}) {
131
130
  projectKey: mirror.projectKey || options.projectKey || params.projectId || params.project?.id || params.project?.projectId,
132
131
  projectId: options.projectId || params.projectId || params.project?.id || params.project?.projectId,
133
132
  workspacePath: mirror.workspacePath || options.workspacePath || '',
133
+ unreadFiles: Array.isArray(mirror.skippedFiles) ? mirror.skippedFiles : [],
134
134
  userTask: String(options.task !== undefined ? options.task : params.task || '').trim(),
135
135
  mode: options.mode || params.mode || 'auto',
136
136
  customInstructions: options.customInstructions !== undefined ? options.customInstructions : params.customInstructions,
@@ -154,6 +154,19 @@ function normalizePromptOptions(options = {}) {
154
154
  };
155
155
  }
156
156
 
157
+ function formatWorkspaceSnapshotContext(unreadFiles) {
158
+ if (!unreadFiles.length) return [
159
+ '- The local workspace was synced from Overleaf immediately before this turn.',
160
+ '- If the recent session history conflicts with the files in the workspace, trust the files.'
161
+ ];
162
+ return [
163
+ '- The readable project files were synced from Overleaf before this turn.',
164
+ '- The following assets were not read because of the binary size limits. A retained local copy may be stale; a missing local copy does not mean the asset was deleted from Overleaf.',
165
+ '- Do not rely on these assets as current evidence or edit them. Their local changes cannot be written back until their current Overleaf content is read. Other readable files remain available for this task.',
166
+ ...unreadFiles.map(file => `- Unavailable current content: ${JSON.stringify(file.path)} (${file.reason}).`)
167
+ ];
168
+ }
169
+
157
170
  function normalizeLoadedSkills(skills = []) {
158
171
  return (Array.isArray(skills) ? skills : [])
159
172
  .map(skill => ({
@@ -395,17 +408,20 @@ function formatSkillInvocationContext(context = {}) {
395
408
  const codexSkillInvocationContext = context.codexSkillInvocationContext || {};
396
409
  const contextInvocation = normalizeSkillInvocation(codexSkillInvocationContext.invocation);
397
410
  const effectiveInvocation = contextInvocation || invocation;
411
+ const skillLabel = effectiveInvocation?.scope === 'codex-local' ? 'local Codex' : 'Codex Overleaf';
398
412
  if (Array.isArray(codexSkillInvocationContext.ignored) && codexSkillInvocationContext.ignored.length) {
399
413
  return [
400
- 'Ignored selected Codex Overleaf skill:',
414
+ `Ignored selected ${skillLabel} skill:`,
401
415
  ...codexSkillInvocationContext.ignored.map(item => `- ${item.id}`),
402
- '- Reason: Codex Overleaf skills are disabled for this turn.',
416
+ effectiveInvocation?.scope === 'codex-local'
417
+ ? '- Reason: The selected local Codex skill is disabled for this turn.'
418
+ : '- Reason: Codex Overleaf skills are disabled for this turn.',
403
419
  '- The stale slash selection was ignored; no selected skill instructions were forced into this prompt.'
404
420
  ].join('\n');
405
421
  }
406
422
  if (Array.isArray(codexSkillInvocationContext.missing) && codexSkillInvocationContext.missing.length) {
407
423
  return [
408
- 'Missing selected Codex Overleaf skill:',
424
+ `Missing selected ${skillLabel} skill:`,
409
425
  ...codexSkillInvocationContext.missing.map(id => `- ${id}`),
410
426
  '- The stale slash selection was ignored; no selected skill instructions were forced into this prompt.'
411
427
  ].join('\n');
@@ -427,17 +443,19 @@ function formatSkillInvocationContext(context = {}) {
427
443
  if (loadedSkill) {
428
444
  return [
429
445
  `- ${effectiveInvocation.id} (${effectiveInvocation.title})`,
430
- '- REQUIRED for this turn: read and use this selected Codex Overleaf skill before answering.',
446
+ `- REQUIRED for this turn: read and use this selected ${skillLabel} skill before answering.`,
431
447
  '- Follow the embedded SKILL.md instructions when they apply to the current user request.',
448
+ ...(effectiveInvocation.scope === 'codex-local' && loadedSkill.path
449
+ ? [`- Skill source: ${loadedSkill.path}`, '- Resolve relative skill resources against this SKILL.md directory.'] : []),
432
450
  '',
433
- 'Selected Codex Overleaf SKILL.md:',
451
+ `Selected ${skillLabel} SKILL.md:`,
434
452
  `## ${loadedSkill.id}: ${loadedSkill.title || effectiveInvocation.title || loadedSkill.id}`,
435
453
  fencedBlock(loadedSkill.content)
436
454
  ].join('\n');
437
455
  }
438
456
  return [
439
457
  `- ${effectiveInvocation.id} (${effectiveInvocation.title})`,
440
- '- Use this selected Codex Overleaf skill for the current turn.',
458
+ `- Use this selected ${skillLabel} skill for the current turn.`,
441
459
  '- Follow the skill instructions and workflow when they apply to the user request.'
442
460
  ].join('\n');
443
461
  }
@@ -450,17 +468,18 @@ function normalizeSkillInvocation(value) {
450
468
  const title = String(value?.title || value?.name || (id === 'skill-installer' ? 'Skill Installer' : id))
451
469
  .trim()
452
470
  .slice(0, 80) || 'Skill Installer';
453
- if (id === 'skill-installer') {
471
+ if (id === 'skill-installer' && (!value?.scope || value.scope === 'builtin')) {
454
472
  return { id, title };
455
473
  }
456
- if (value?.scope !== 'codex-overleaf' && !Object.prototype.hasOwnProperty.call(value || {}, 'skillId')) {
474
+ if (!['codex-overleaf', 'codex-local'].includes(value?.scope) && !Object.prototype.hasOwnProperty.call(value || {}, 'skillId')) {
457
475
  return null;
458
476
  }
459
- return { id, title, scope: 'codex-overleaf' };
477
+ return { id, title, scope: value?.scope === 'codex-local' ? 'codex-local' : 'codex-overleaf' };
460
478
  }
461
479
 
462
480
  function isSkillInstallerInvocation(value) {
463
- return normalizeSkillInvocation(value)?.id === 'skill-installer';
481
+ const invocation = normalizeSkillInvocation(value);
482
+ return invocation?.id === 'skill-installer' && !invocation.scope;
464
483
  }
465
484
 
466
485
  function isSafeSkillId(id) {
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const { spawn, spawnSync } = require('node:child_process');
3
+ const { runOwnedNativeHelper } = require('./nativeHelperLifecycle');
4
+ const { stopCodexAppServer } = require('./codexSessionTiming');
5
+ const { spawn } = require('node:child_process');
4
6
  const crypto = require('node:crypto');
5
7
  const fs = require('node:fs');
6
8
  const os = require('node:os');
@@ -12,16 +14,22 @@ const { providerError } = require('./providerProfile');
12
14
  const MAX_OUTPUT_BYTES = 256 * 1024;
13
15
 
14
16
  async function runProviderConnectionTest({ launch, env = process.env, signal } = {}) {
15
- const prepared = await prepareProviderLaunch(launch, { signal });
16
- try {
17
- return await runProviderConnectionTestProcess({ launch: prepared.launch, env, signal });
18
- } finally {
19
- await prepared.close();
20
- }
17
+ throwIfTestCancelled(signal);
18
+ return runOwnedNativeHelper(async ownedSignal => {
19
+ throwIfTestCancelled(ownedSignal);
20
+ const prepared = await prepareProviderLaunch(launch, { signal: ownedSignal });
21
+ try {
22
+ throwIfTestCancelled(ownedSignal);
23
+ return await runProviderConnectionTestProcess({ launch: prepared.launch, env, signal: ownedSignal });
24
+ } finally {
25
+ await prepared.close();
26
+ }
27
+ }, signal);
21
28
  }
22
29
 
23
30
  function runProviderConnectionTestProcess({ launch, env = process.env, signal } = {}) {
24
31
  return new Promise((resolve, reject) => {
32
+ throwIfTestCancelled(signal);
25
33
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-overleaf-provider-test-'));
26
34
  const codexHome = path.join(root, 'codex-home');
27
35
  const workspace = path.join(root, 'workspace');
@@ -52,6 +60,7 @@ function runProviderConnectionTestProcess({ launch, env = process.env, signal }
52
60
  const startedAt = Date.now();
53
61
  let child;
54
62
  try {
63
+ throwIfTestCancelled(signal);
55
64
  child = spawn(command, args, {
56
65
  cwd: workspace,
57
66
  env: childEnv,
@@ -61,7 +70,8 @@ function runProviderConnectionTestProcess({ launch, env = process.env, signal }
61
70
  });
62
71
  } catch (error) {
63
72
  cleanup(root);
64
- reject(providerError('provider_test_spawn_failed', 'Codex could not start the provider connection test.', { cause: error }));
73
+ reject(error?.code === 'provider_test_cancelled' ? error
74
+ : providerError('provider_test_spawn_failed', 'Codex could not start the provider connection test.', { cause: error }));
65
75
  return;
66
76
  }
67
77
  let stdout = '';
@@ -121,10 +131,10 @@ function runProviderConnectionTestProcess({ launch, env = process.env, signal }
121
131
  settled = true;
122
132
  clearTimeout(timer);
123
133
  signal?.removeEventListener('abort', onAbort);
124
- if (error) terminateProcessTree(child);
125
- cleanup(root);
126
- if (error) reject(error);
127
- else resolve(result);
134
+ stopCodexAppServer(child, { processGroup: process.platform !== 'win32', reason: error }).then(() => {
135
+ cleanup(root);
136
+ if (error) reject(error); else resolve(result);
137
+ }, stopError => { cleanup(root); reject(stopError); });
128
138
  }
129
139
  });
130
140
  }
@@ -168,20 +178,8 @@ function hasLeakedToolControlTokens(value) {
168
178
  return /<\|tool_call_(?:begin|argument_begin|end)\|>/i.test(String(value || ''));
169
179
  }
170
180
 
171
- function terminateProcessTree(child) {
172
- if (!child?.pid) return;
173
- try {
174
- if (process.platform === 'win32') {
175
- spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
176
- } else {
177
- process.kill(-child.pid, 'SIGTERM');
178
- setTimeout(() => {
179
- try { process.kill(-child.pid, 'SIGKILL'); } catch (_error) {}
180
- }, 500).unref?.();
181
- }
182
- } catch (_error) {
183
- try { child.kill('SIGKILL'); } catch (_killError) {}
184
- }
181
+ function throwIfTestCancelled(signal) {
182
+ if (signal?.aborted) throw providerError('provider_test_cancelled', 'Provider connection test was cancelled.');
185
183
  }
186
184
 
187
185
  function cleanup(root) {