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.
- package/README.md +23 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/content/assetTransferBroker.js +169 -0
- package/extension/src/content/contentRuntime.js +27 -1
- package/extension/src/content/generated/content.bundle.js +551 -65
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +3 -0
- package/extension/src/content/modelPicker.js +2 -2
- package/extension/src/content/modelPickerSupport.js +3 -3
- package/extension/src/content/pageBridgeClient.js +1 -0
- package/extension/src/content/runResultActions.js +114 -0
- package/extension/src/content/runTimelineView.js +17 -0
- package/extension/src/content/sessionForkController.js +120 -0
- package/extension/src/content/writebackController.js +3 -0
- package/extension/src/content/writebackOrchestrator.js +3 -1
- package/extension/src/page/binaryAssetUploader.js +343 -0
- package/extension/src/page/trackedChangesLifecycle.js +2 -0
- package/extension/src/pageBridge.js +30 -1
- package/extension/src/shared/compatibility.js +4 -2
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/models.js +13 -2
- package/extension/src/shared/pageRpcContract.js +5 -1
- package/extension/src/shared/sessionState.js +12 -4
- package/extension/src/shared/storageDb.js +1 -1
- package/extension/src/shared/storageRunActions.js +11 -1
- package/extension/styles/panel.css +61 -0
- package/native-host/src/codexModels.js +6 -2
- package/native-host/src/codexSessionRunner.js +5 -1
- package/native-host/src/codexThreadFork.js +103 -0
- package/native-host/src/mirrorWorkspace.js +5 -19
- package/native-host/src/nativeAssetTransfer.js +183 -0
- package/native-host/src/taskRunnerRuntime.js +22 -0
- 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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|