codex-overleaf-link 2.3.1 → 2.3.2

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 (33) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/content/assetTransferBroker.js +169 -0
  4. package/extension/src/content/contentRuntime.js +27 -1
  5. package/extension/src/content/generated/content.bundle.js +551 -65
  6. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  7. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  8. package/extension/src/content/modelPicker.js +2 -2
  9. package/extension/src/content/modelPickerSupport.js +3 -3
  10. package/extension/src/content/pageBridgeClient.js +1 -0
  11. package/extension/src/content/runResultActions.js +114 -0
  12. package/extension/src/content/runTimelineView.js +17 -0
  13. package/extension/src/content/sessionForkController.js +120 -0
  14. package/extension/src/content/writebackController.js +3 -0
  15. package/extension/src/content/writebackOrchestrator.js +3 -1
  16. package/extension/src/page/binaryAssetUploader.js +343 -0
  17. package/extension/src/page/trackedChangesLifecycle.js +2 -0
  18. package/extension/src/pageBridge.js +30 -1
  19. package/extension/src/shared/compatibility.js +4 -2
  20. package/extension/src/shared/i18n.js +16 -0
  21. package/extension/src/shared/models.js +13 -2
  22. package/extension/src/shared/pageRpcContract.js +5 -1
  23. package/extension/src/shared/sessionState.js +12 -4
  24. package/extension/src/shared/storageDb.js +1 -1
  25. package/extension/src/shared/storageRunActions.js +11 -1
  26. package/extension/styles/panel.css +61 -0
  27. package/native-host/src/codexModels.js +6 -2
  28. package/native-host/src/codexSessionRunner.js +5 -1
  29. package/native-host/src/codexThreadFork.js +103 -0
  30. package/native-host/src/mirrorWorkspace.js +5 -19
  31. package/native-host/src/nativeAssetTransfer.js +183 -0
  32. package/native-host/src/taskRunnerRuntime.js +22 -0
  33. package/package.json +1 -1
@@ -2359,6 +2359,67 @@
2359
2359
  margin-left: auto;
2360
2360
  }
2361
2361
 
2362
+ #codex-overleaf-panel .run-result-actions {
2363
+ display: flex;
2364
+ justify-content: flex-start;
2365
+ align-items: center;
2366
+ gap: 2px;
2367
+ min-height: 30px;
2368
+ margin: 10px 0 0;
2369
+ }
2370
+
2371
+ #codex-overleaf-panel .run-result-actions[hidden] { display: none; }
2372
+
2373
+ #codex-overleaf-panel .run-copy-action,
2374
+ #codex-overleaf-panel .run-fork-action {
2375
+ display: inline-grid;
2376
+ place-items: center;
2377
+ width: 30px;
2378
+ height: 30px;
2379
+ border: 0;
2380
+ border-radius: 999px;
2381
+ background: transparent;
2382
+ color: var(--tl-fg-4);
2383
+ opacity: 1;
2384
+ transition: color 120ms ease, background 120ms ease, opacity 120ms ease;
2385
+ }
2386
+
2387
+ #codex-overleaf-panel .run-copy-action:hover:not(:disabled),
2388
+ #codex-overleaf-panel .run-fork-action:hover:not(:disabled) {
2389
+ background: var(--tl-hover);
2390
+ color: var(--tl-accent);
2391
+ }
2392
+
2393
+ #codex-overleaf-panel .run-copy-action:disabled,
2394
+ #codex-overleaf-panel .run-fork-action:disabled {
2395
+ cursor: not-allowed;
2396
+ opacity: 0.38;
2397
+ }
2398
+
2399
+ #codex-overleaf-panel .run-copy-action svg,
2400
+ #codex-overleaf-panel .run-fork-action svg {
2401
+ width: 16px;
2402
+ height: 16px;
2403
+ fill: none;
2404
+ stroke: currentColor;
2405
+ stroke-width: 1.7;
2406
+ stroke-linecap: round;
2407
+ stroke-linejoin: round;
2408
+ }
2409
+
2410
+ #codex-overleaf-panel .run-copy-action[hidden],
2411
+ #codex-overleaf-panel .run-fork-action[hidden] { display: none; }
2412
+
2413
+ #codex-overleaf-panel .run-completed-time {
2414
+ margin-left: 5px;
2415
+ color: var(--tl-fg-4);
2416
+ font-size: 12px;
2417
+ font-variant-numeric: tabular-nums;
2418
+ line-height: 30px;
2419
+ }
2420
+
2421
+ #codex-overleaf-panel .run-completed-time[hidden] { display: none; }
2422
+
2362
2423
  /* When Accept All is hidden, the Undo button takes the right-aligned slot. */
2363
2424
  #codex-overleaf-panel .run-turn-meta [data-run-accept][hidden] + [data-run-undo] {
2364
2425
  margin-left: auto;
@@ -181,7 +181,7 @@ function buildModelEntry({
181
181
  defaultSpeedTier = 'standard'
182
182
  }) {
183
183
  const normalizedEfforts = normalizeReasoningEfforts(reasoningEfforts);
184
- const normalizedDefault = getString(defaultReasoningEffort) || 'medium';
184
+ const normalizedDefault = normalizeReasoningEffort(defaultReasoningEffort) || 'medium';
185
185
  const normalizedSpeedTiers = normalizeSpeedTiers(speedTiers);
186
186
  const normalizedDefaultSpeedTier = getString(defaultSpeedTier) || 'standard';
187
187
  return {
@@ -202,7 +202,7 @@ function normalizeReasoningEfforts(rawEfforts) {
202
202
  const seen = new Set();
203
203
  const result = [];
204
204
  for (const rawEffort of rawEfforts) {
205
- const effort = getString(typeof rawEffort === 'string' ? rawEffort : rawEffort?.effort);
205
+ const effort = normalizeReasoningEffort(typeof rawEffort === 'string' ? rawEffort : rawEffort?.effort);
206
206
  if (!effort || seen.has(effort)) {
207
207
  continue;
208
208
  }
@@ -212,6 +212,10 @@ function normalizeReasoningEfforts(rawEfforts) {
212
212
  return result;
213
213
  }
214
214
 
215
+ function normalizeReasoningEffort(rawEffort) {
216
+ return getString(rawEffort);
217
+ }
218
+
215
219
  function normalizeSpeedTiers(rawTiers) {
216
220
  const tiers = ['standard'];
217
221
  if (!Array.isArray(rawTiers)) {
@@ -22,6 +22,7 @@ const {
22
22
  loadSelectedProjectSkills
23
23
  } = require('./localSkills');
24
24
  const { createSubagentBroker } = require('./subagentBroker');
25
+ const { prepareBinaryAssetChanges } = require('./nativeAssetTransfer');
25
26
  const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
26
27
  const { applyProviderEnvironment, buildProviderConfigArgs, prepareProviderLaunch } = require('./codexProviderLaunch');
27
28
  const { buildReadProgressRules, createReadProgressController } = require('./readProgressGuard');
@@ -259,6 +260,9 @@ async function runCodexSession({ params = {}, env = process.env, emit = () => {}
259
260
  });
260
261
  }
261
262
 
263
+ const preparedAssets = prepareBinaryAssetChanges({ projectId, rootDir, changes: rawSyncChanges });
264
+ rawSyncChanges = preparedAssets.changes;
265
+ unsupportedChanges.push(...preparedAssets.unsupportedChanges);
262
266
  const syncChanges = rawSyncChanges.map(change => {
263
267
  if (change.type === 'write' && typeof change.previousContent === 'string') {
264
268
  return {
@@ -1461,7 +1465,7 @@ function extractCodexAppServerErrorMessage(params = {}) {
1461
1465
  }
1462
1466
 
1463
1467
  function normalizeReasoningEffort(value) {
1464
- return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(value) ? value : null;
1468
+ return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(value) ? value : null;
1465
1469
  }
1466
1470
 
1467
1471
  function normalizeSpeedTier(value) {
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+ const { version: PACKAGE_VERSION } = require('../../package.json');
5
+ const { buildCodexHomeEnv } = require('./codexHome');
6
+ const { resolveCodexCommand, shouldUseShellForCommand } = require('./codexCommand');
7
+ const { buildCodexAppServerArgs } = require('./codexSessionRunner');
8
+
9
+ function forkCodexThread(params = {}, env = process.env) {
10
+ const threadId = cleanId(params.threadId, 'threadId');
11
+ const lastTurnId = cleanId(params.lastTurnId, 'lastTurnId');
12
+ const childEnv = buildCodexHomeEnv(env, {
13
+ loadCodexLocalSkills: params.loadCodexLocalSkills !== false,
14
+ loadCodexOverleafSkills: params.loadCodexOverleafSkills !== false
15
+ });
16
+ const command = resolveCodexCommand(childEnv);
17
+ if (!command) return Promise.reject(codedError('codex_not_found', 'Codex CLI was not found.'));
18
+
19
+ return new Promise((resolve, reject) => {
20
+ const child = spawn(command, buildCodexAppServerArgs({
21
+ speedTier: params.speedTier || 'standard',
22
+ loadCodexLocalSkills: params.loadCodexLocalSkills !== false
23
+ }), {
24
+ env: childEnv,
25
+ shell: shouldUseShellForCommand(command, childEnv),
26
+ stdio: ['pipe', 'pipe', 'pipe']
27
+ });
28
+ const pending = new Map();
29
+ let nextId = 1;
30
+ let buffer = '';
31
+ let stderr = '';
32
+ let settled = false;
33
+ const timeout = setTimeout(() => finish(codedError('codex_thread_fork_timeout', 'Codex thread fork timed out.')), 30000);
34
+
35
+ child.stdout.setEncoding('utf8');
36
+ child.stderr.setEncoding('utf8');
37
+ child.stdout.on('data', chunk => {
38
+ buffer += chunk;
39
+ const lines = buffer.split(/\r?\n/);
40
+ buffer = lines.pop() || '';
41
+ for (const line of lines) if (line.trim()) handleMessage(line);
42
+ });
43
+ child.stderr.on('data', chunk => { stderr += chunk; });
44
+ child.on('error', finish);
45
+ child.on('close', code => {
46
+ if (!settled) finish(codedError('codex_thread_fork_failed', stderr || `Codex app-server exited with code ${code}.`));
47
+ });
48
+
49
+ (async () => {
50
+ await request('initialize', {
51
+ clientInfo: { name: 'codex-overleaf-link', version: PACKAGE_VERSION },
52
+ capabilities: null
53
+ });
54
+ notify('initialized');
55
+ const result = await request('thread/fork', { threadId, lastTurnId });
56
+ const forkedThreadId = result?.thread?.id || result?.threadId || '';
57
+ if (!forkedThreadId || forkedThreadId === threadId) {
58
+ throw codedError('codex_thread_fork_invalid_response', 'Codex app-server did not return a new thread id.');
59
+ }
60
+ finish(null, { threadId: forkedThreadId, sourceThreadId: threadId, lastTurnId });
61
+ })().catch(finish);
62
+
63
+ function request(method, requestParams) {
64
+ const id = nextId++;
65
+ child.stdin.write(`${JSON.stringify({ id, method, params: requestParams })}\n`);
66
+ return new Promise((resolveRequest, rejectRequest) => pending.set(id, { resolve: resolveRequest, reject: rejectRequest }));
67
+ }
68
+ function notify(method, requestParams) {
69
+ child.stdin.write(`${JSON.stringify({ method, params: requestParams })}\n`);
70
+ }
71
+ function handleMessage(line) {
72
+ let message;
73
+ try { message = JSON.parse(line); } catch (_error) { return; }
74
+ if (Object.prototype.hasOwnProperty.call(message, 'id') && (message.result !== undefined || message.error)) {
75
+ const waiter = pending.get(message.id);
76
+ if (!waiter) return;
77
+ pending.delete(message.id);
78
+ if (message.error) waiter.reject(codedError('codex_thread_fork_failed', message.error.message || JSON.stringify(message.error)));
79
+ else waiter.resolve(message.result);
80
+ } else if (Object.prototype.hasOwnProperty.call(message, 'id') && message.method) {
81
+ child.stdin.write(`${JSON.stringify({ id: message.id, error: { code: -32601, message: 'Unsupported during thread fork' } })}\n`);
82
+ }
83
+ }
84
+ function finish(error, result) {
85
+ if (settled) return;
86
+ settled = true;
87
+ clearTimeout(timeout);
88
+ for (const waiter of pending.values()) waiter.reject(error || codedError('codex_thread_fork_closed', 'Fork process closed.'));
89
+ pending.clear();
90
+ try { child.stdin.end(); } catch (_error) { /* best effort */ }
91
+ try { child.kill(); } catch (_error) { /* best effort */ }
92
+ if (error) reject(error); else resolve(result);
93
+ }
94
+ });
95
+ }
96
+
97
+ function cleanId(value, field) {
98
+ const id = String(value || '').trim();
99
+ if (!id || id.length > 160) throw codedError('codex_thread_fork_invalid', `${field} is required.`);
100
+ return id;
101
+ }
102
+ function codedError(code, message) { const error = new Error(message); error.code = code; return error; }
103
+ module.exports = { forkCodexThread };
@@ -152,34 +152,19 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
152
152
  if (hashBytes(bytes) === previous.hash) {
153
153
  continue;
154
154
  }
155
- if (stat.size > SAFE_INLINE_BINARY_CHANGE_BYTES) {
156
- unsupportedChanges.push(buildOversizedBinaryPayloadChange({
157
- filePath,
158
- size: stat.size,
159
- previous
160
- }));
161
- continue;
162
- }
163
155
  currentByPath.set(filePath, {
164
156
  binary: true,
165
- bytes,
157
+ sha256: hashBytes(bytes),
166
158
  size: stat.size,
167
159
  previous
168
160
  });
169
161
  continue;
170
162
  }
171
163
  if (!previous) {
172
- if (stat.size > SAFE_INLINE_BINARY_CHANGE_BYTES) {
173
- unsupportedChanges.push(buildOversizedBinaryPayloadChange({
174
- filePath,
175
- size: stat.size,
176
- previous
177
- }));
178
- continue;
179
- }
164
+ const bytes = fs.readFileSync(target);
180
165
  currentByPath.set(filePath, {
181
166
  binary: true,
182
- bytes: fs.readFileSync(target),
167
+ sha256: hashBytes(bytes),
183
168
  size: stat.size,
184
169
  previous
185
170
  });
@@ -218,7 +203,8 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
218
203
  changes.push({
219
204
  type: previous ? 'overwrite-binary' : 'binary-create',
220
205
  path: filePath,
221
- contentBase64: contentOrBinary.bytes.toString('base64'),
206
+ assetSourcePath: filePath,
207
+ sha256: contentOrBinary.sha256,
222
208
  previousExists: Boolean(previous),
223
209
  previousKind: previous?.kind || '',
224
210
  previousSize: previous?.size,
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { getProjectMirror } = require('./mirrorWorkspace');
7
+
8
+ const MAX_ASSET_BYTES = 10 * 1024 * 1024;
9
+ const ASSET_CHUNK_BYTES = 192 * 1024;
10
+ const ASSET_TOKEN_TTL_MS = 15 * 60 * 1000;
11
+ const transfers = new Map();
12
+
13
+ function prepareBinaryAssetChanges({ projectId, rootDir, changes = [] } = {}) {
14
+ const prepared = [];
15
+ const unsupportedChanges = [];
16
+ for (const change of changes) {
17
+ if (change?.type !== 'binary-create' && change?.type !== 'overwrite-binary') {
18
+ prepared.push(change);
19
+ continue;
20
+ }
21
+ try {
22
+ prepared.push(registerAssetChange({ projectId, rootDir, change }));
23
+ } catch (error) {
24
+ unsupportedChanges.push({
25
+ type: 'unsupported-local-file',
26
+ path: change?.path || '',
27
+ reason: error.code || 'binary_asset_registration_failed',
28
+ size: change?.size,
29
+ attemptedChangeType: change?.type || 'binary-create'
30
+ });
31
+ }
32
+ }
33
+ return { changes: prepared, unsupportedChanges };
34
+ }
35
+
36
+ function registerAssetChange({ projectId, rootDir, change }) {
37
+ cleanupExpiredTransfers();
38
+ const mirror = getProjectMirror(projectId, { rootDir });
39
+ const relativePath = normalizeProjectPath(change.assetSourcePath || change.path);
40
+ const target = resolveSafeWorkspaceFile(mirror.workspacePath, relativePath);
41
+ const stat = fs.statSync(target);
42
+ if (!stat.isFile()) throw codedError('binary_asset_source_invalid', 'Asset source is not a file.');
43
+ if (stat.size > MAX_ASSET_BYTES) throw codedError('binary_file_too_large', 'Asset exceeds the 10 MiB writeback limit.');
44
+ const sha256 = hashFile(target);
45
+ if (change.sha256 && change.sha256 !== sha256) {
46
+ throw codedError('binary_asset_source_changed', 'Asset changed before transfer registration.');
47
+ }
48
+ const token = crypto.randomUUID();
49
+ const expiresAtMs = Date.now() + ASSET_TOKEN_TTL_MS;
50
+ transfers.set(token, {
51
+ token,
52
+ path: relativePath,
53
+ target,
54
+ size: stat.size,
55
+ sha256,
56
+ expiresAtMs
57
+ });
58
+ const { contentBase64, assetSourcePath, ...safeChange } = change;
59
+ return {
60
+ ...safeChange,
61
+ size: stat.size,
62
+ assetRef: {
63
+ token,
64
+ path: relativePath,
65
+ size: stat.size,
66
+ sha256,
67
+ mimeType: inferMimeType(relativePath),
68
+ chunkSize: ASSET_CHUNK_BYTES,
69
+ expiresAt: new Date(expiresAtMs).toISOString()
70
+ }
71
+ };
72
+ }
73
+
74
+ function readAssetChunk(params = {}) {
75
+ cleanupExpiredTransfers();
76
+ const transfer = getTransfer(params.token);
77
+ const offset = Number(params.offset || 0);
78
+ const requestedLength = Number(params.length || ASSET_CHUNK_BYTES);
79
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > transfer.size) {
80
+ throw codedError('asset_chunk_offset_invalid', 'Asset chunk offset is invalid.');
81
+ }
82
+ const length = Math.min(
83
+ ASSET_CHUNK_BYTES,
84
+ Number.isSafeInteger(requestedLength) && requestedLength > 0 ? requestedLength : ASSET_CHUNK_BYTES,
85
+ transfer.size - offset
86
+ );
87
+ assertSourceStable(transfer);
88
+ const handle = fs.openSync(transfer.target, 'r');
89
+ try {
90
+ const buffer = Buffer.alloc(length);
91
+ const bytesRead = length ? fs.readSync(handle, buffer, 0, length, offset) : 0;
92
+ const nextOffset = offset + bytesRead;
93
+ return {
94
+ token: transfer.token,
95
+ path: transfer.path,
96
+ offset,
97
+ nextOffset,
98
+ size: transfer.size,
99
+ sha256: transfer.sha256,
100
+ contentBase64: buffer.subarray(0, bytesRead).toString('base64'),
101
+ eof: nextOffset >= transfer.size
102
+ };
103
+ } finally {
104
+ fs.closeSync(handle);
105
+ }
106
+ }
107
+
108
+ function releaseAsset(params = {}) {
109
+ return { released: transfers.delete(String(params.token || '')) };
110
+ }
111
+
112
+ function getTransfer(tokenValue) {
113
+ const token = String(tokenValue || '');
114
+ const transfer = transfers.get(token);
115
+ if (!transfer) throw codedError('asset_token_invalid', 'Asset transfer token is invalid or expired.');
116
+ if (transfer.expiresAtMs <= Date.now()) {
117
+ transfers.delete(token);
118
+ throw codedError('asset_token_expired', 'Asset transfer token expired.');
119
+ }
120
+ return transfer;
121
+ }
122
+
123
+ function assertSourceStable(transfer) {
124
+ let stat;
125
+ try { stat = fs.statSync(transfer.target); } catch (_error) {
126
+ throw codedError('binary_asset_source_missing', 'Asset source disappeared during transfer.');
127
+ }
128
+ if (!stat.isFile() || stat.size !== transfer.size) {
129
+ throw codedError('binary_asset_source_changed', 'Asset source changed during transfer.');
130
+ }
131
+ }
132
+
133
+ function resolveSafeWorkspaceFile(workspacePath, relativePath) {
134
+ const root = path.resolve(workspacePath);
135
+ const target = path.resolve(root, relativePath);
136
+ if (target === root || !target.startsWith(`${root}${path.sep}`)) {
137
+ throw codedError('binary_asset_path_unsafe', 'Asset path escapes the project workspace.');
138
+ }
139
+ if (fs.lstatSync(target).isSymbolicLink()) {
140
+ throw codedError('binary_asset_path_unsafe', 'Symbolic-link assets cannot be transferred.');
141
+ }
142
+ return target;
143
+ }
144
+
145
+ function normalizeProjectPath(value) {
146
+ const normalized = String(value || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
147
+ if (!normalized || normalized.split('/').some(part => !part || part === '.' || part === '..')) {
148
+ throw codedError('binary_asset_path_invalid', 'Asset path is not a safe project-relative path.');
149
+ }
150
+ return normalized;
151
+ }
152
+
153
+ function hashFile(target) {
154
+ return crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex');
155
+ }
156
+
157
+ function inferMimeType(filePath) {
158
+ return {
159
+ '.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg',
160
+ '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml',
161
+ '.webp': 'image/webp', '.eps': 'application/postscript'
162
+ }[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
163
+ }
164
+
165
+ function cleanupExpiredTransfers() {
166
+ const now = Date.now();
167
+ for (const [token, transfer] of transfers) if (transfer.expiresAtMs <= now) transfers.delete(token);
168
+ }
169
+
170
+ function codedError(code, message) {
171
+ const error = new Error(message);
172
+ error.code = code;
173
+ return error;
174
+ }
175
+
176
+ module.exports = {
177
+ ASSET_CHUNK_BYTES,
178
+ ASSET_TOKEN_TTL_MS,
179
+ MAX_ASSET_BYTES,
180
+ prepareBinaryAssetChanges,
181
+ readAssetChunk,
182
+ releaseAsset
183
+ };
@@ -19,6 +19,8 @@ const {
19
19
  resolveRunProviderForRun
20
20
  } = require('./providerRuntime');
21
21
  const { clearPluginCodexHistory } = require('./codexHome');
22
+ const { forkCodexThread } = require('./codexThreadFork');
23
+ const { readAssetChunk, releaseAsset } = require('./nativeAssetTransfer');
22
24
  const { logDebug, truncateText } = require('./debugLog');
23
25
  const { HOST_NAME } = require('./manifest');
24
26
  const { getNativeRuntimePlatform, summarizeNativeEnvironment } = require('./nativeEnvironment');
@@ -116,6 +118,26 @@ async function handleRequest(request, env = process.env, emit = () => {}) {
116
118
  return handleCodexHistoryClear(request, env);
117
119
  }
118
120
 
121
+ if (request.method === 'codex.thread.fork') {
122
+ try {
123
+ return okResponse(request.id, await forkCodexThread(request.params || {}, env));
124
+ } catch (error) {
125
+ return errorResponse(request.id, error.code || 'codex_thread_fork_failed', error.message);
126
+ }
127
+ }
128
+
129
+ if (request.method === 'asset.readChunk') {
130
+ try {
131
+ return okResponse(request.id, readAssetChunk(request.params || {}));
132
+ } catch (error) {
133
+ return errorResponse(request.id, error.code || 'asset_chunk_read_failed', error.message);
134
+ }
135
+ }
136
+
137
+ if (request.method === 'asset.release') {
138
+ return okResponse(request.id, releaseAsset(request.params || {}));
139
+ }
140
+
119
141
  if (request.method === 'skills.list') {
120
142
  return handleSkillsList(request, env);
121
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-overleaf-link",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",