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,411 @@
1
+ 'use strict';
2
+
3
+ const { truncateText } = require('./debugLog');
4
+
5
+ const PROJECT_CUSTOM_INSTRUCTIONS_MAX_CHARS = 12000;
6
+ const NATIVE_MESSAGE_PROMPT_MAX_CHARS = 1024 * 1024;
7
+
8
+ /**
9
+ * Build the full Codex turn prompt from run parameters.
10
+ * @param {Object} options
11
+ * @param {string} options.task - User task text.
12
+ * @param {string} options.customInstructions - Project custom instructions (may be empty).
13
+ * @param {Array} options.skills - Array of { id, name, content } loaded skill objects.
14
+ * @param {Object|null} options.skillInvocation - { skillId, forced } if slash-selected.
15
+ * @param {Array} options.attachments - Array of { name, relativePath } staged attachment refs.
16
+ * @param {string} options.activePath - Currently focused file path.
17
+ * @param {Array<string>} options.contextFiles - Selected context file paths.
18
+ * @param {Object} options.compileContext - { enabled, logSummary } compile context.
19
+ * @returns {{ systemPrompt: string, userPrompt: string }}
20
+ */
21
+ function buildCodexTurnPrompt(options = {}) {
22
+ const context = normalizePromptOptions(options);
23
+
24
+ const systemPrompt = [
25
+ 'Same Codex Overleaf session context:',
26
+ `Session id: ${context.session.id || 'none'}`,
27
+ '',
28
+ 'Recent turns in this UI session:',
29
+ formatSessionHistory(context.session.history),
30
+ '',
31
+ 'Current Overleaf workspace:',
32
+ `- Project: ${context.projectKey || context.projectId || 'unknown'}`,
33
+ `- Local workspace: ${context.workspacePath || 'current cwd'}`,
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.',
36
+ '',
37
+ 'Focus files:',
38
+ formatFocusFiles(context.focusFiles),
39
+ '',
40
+ 'Compilation context (@compile-log):',
41
+ formatCompileLogContext(context),
42
+ '',
43
+ 'Project custom instructions:',
44
+ formatCustomInstructionsContext(context.customInstructions),
45
+ '',
46
+ 'Project local skills:',
47
+ formatProjectLocalSkillsContext(context),
48
+ '',
49
+ 'Codex skill loading:',
50
+ formatCodexSkillLoadingContext(context),
51
+ '',
52
+ 'Selected Codex skill:',
53
+ formatSkillInvocationContext(context),
54
+ '',
55
+ 'Attachments for this turn:',
56
+ formatTurnAttachmentsContext(context.attachments),
57
+ '',
58
+ 'Mode for this turn:',
59
+ `- ${context.mode}`,
60
+ '- ask: inspect and explain only; do not edit files.',
61
+ '- confirm/auto: edit the local workspace directly when the request calls for changes. The browser bridge handles review, confirmation, deletion approval, and syncing back to Overleaf.',
62
+ '',
63
+ 'Write expectation for this turn:',
64
+ formatWriteExpectation({
65
+ mode: context.mode,
66
+ task: context.userTask,
67
+ skillInvocation: context.skillInvocation
68
+ })
69
+ ].join('\n');
70
+
71
+ const userPrompt = [
72
+ 'Current user request:',
73
+ context.userTask || '(empty request)'
74
+ ].join('\n');
75
+
76
+ return { systemPrompt, userPrompt };
77
+ }
78
+
79
+ /**
80
+ * Validate prompt size against native messaging limits.
81
+ * @param {string} systemPrompt
82
+ * @param {string} userPrompt
83
+ * @returns {{ ok: boolean, totalChars: number, exceedsLimit: boolean }}
84
+ */
85
+ function validatePromptSize(systemPrompt, userPrompt) {
86
+ const totalChars = String(systemPrompt || '').length + String(userPrompt || '').length;
87
+ const exceedsLimit = totalChars > NATIVE_MESSAGE_PROMPT_MAX_CHARS;
88
+ return {
89
+ ok: !exceedsLimit,
90
+ totalChars,
91
+ exceedsLimit
92
+ };
93
+ }
94
+
95
+ function normalizePromptOptions(options = {}) {
96
+ const params = options.params && typeof options.params === 'object' ? options.params : options;
97
+ const mirror = options.mirror || {};
98
+ const session = options.session || params.session || {};
99
+ const skills = normalizeLoadedSkills(
100
+ options.skills || options.projectLocalSkills?.skills || options.localSkills?.skills || []
101
+ );
102
+ const selectedSkillIds = Array.isArray(options.selectedSkillIds)
103
+ ? options.selectedSkillIds
104
+ : Array.isArray(params.selectedSkillIds)
105
+ ? params.selectedSkillIds
106
+ : skills.map(skill => skill.id).filter(Boolean);
107
+ const projectLocalSkills = options.projectLocalSkills || options.localSkills || {
108
+ skills,
109
+ missing: [],
110
+ selected: selectedSkillIds
111
+ };
112
+ const compileContext = options.compileContext || {};
113
+ const activePath = normalizeProjectPath(options.activePath || params.activePath || params.project?.activePath);
114
+ const contextFiles = options.contextFiles || params.contextFiles || params.focusFiles || session.focusFiles;
115
+ const focusFiles = normalizeFocusFiles(contextFiles);
116
+ if (activePath && !focusFiles.includes(activePath)) {
117
+ focusFiles.unshift(activePath);
118
+ }
119
+
120
+ return {
121
+ params,
122
+ session,
123
+ projectKey: mirror.projectKey || options.projectKey || params.projectId || params.project?.id || params.project?.projectId,
124
+ projectId: options.projectId || params.projectId || params.project?.id || params.project?.projectId,
125
+ workspacePath: mirror.workspacePath || options.workspacePath || '',
126
+ userTask: String(options.task !== undefined ? options.task : params.task || '').trim(),
127
+ mode: options.mode || params.mode || 'auto',
128
+ customInstructions: options.customInstructions !== undefined ? options.customInstructions : params.customInstructions,
129
+ selectedSkillIds,
130
+ projectLocalSkills,
131
+ loadCodexLocalSkills: options.loadCodexLocalSkills !== undefined
132
+ ? options.loadCodexLocalSkills
133
+ : params.loadCodexLocalSkills !== false,
134
+ loadCodexOverleafSkills: options.loadCodexOverleafSkills !== undefined
135
+ ? options.loadCodexOverleafSkills
136
+ : params.loadCodexOverleafSkills !== false,
137
+ skillInvocation: options.skillInvocation || params.skillInvocation || null,
138
+ codexSkillInvocationContext: options.codexSkillInvocationContext || null,
139
+ attachments: options.turnAttachments || options.attachments || [],
140
+ focusFiles: focusFiles.slice(0, 8),
141
+ compileLog: compileContext.logSummary !== undefined ? compileContext.logSummary : params.compileLog,
142
+ compileErrors: compileContext.errors !== undefined ? compileContext.errors : params.compileErrors,
143
+ compileWarnings: compileContext.warnings !== undefined ? compileContext.warnings : params.compileWarnings,
144
+ compileLogFresh: compileContext.fresh !== undefined ? compileContext.fresh : params.compileLogFresh,
145
+ compileLogCompiledAt: compileContext.compiledAt !== undefined ? compileContext.compiledAt : params.compileLogCompiledAt
146
+ };
147
+ }
148
+
149
+ function normalizeLoadedSkills(skills = []) {
150
+ return (Array.isArray(skills) ? skills : [])
151
+ .map(skill => ({
152
+ id: String(skill?.id || '').trim(),
153
+ title: String(skill?.title || skill?.name || skill?.id || '').trim(),
154
+ content: String(skill?.content || '')
155
+ }))
156
+ .filter(skill => skill.id && skill.content);
157
+ }
158
+
159
+ function formatTurnAttachmentsContext(attachments = []) {
160
+ if (!attachments.length) {
161
+ return '- none provided.';
162
+ }
163
+ return [
164
+ '- These files are user-provided context for this turn only.',
165
+ '- Read them if relevant. Do not edit them, do not include them in writeback, and do not write them to Overleaf.',
166
+ ...attachments.map(attachment => {
167
+ const attachmentPath = attachment.path || attachment.relativePath || attachment.name || 'attachment';
168
+ const size = Number.isFinite(Number(attachment.size)) ? Number(attachment.size) : 0;
169
+ const details = [
170
+ attachment.mimeType || 'application/octet-stream',
171
+ `${size} bytes`
172
+ ].join(', ');
173
+ return `- ${attachmentPath} (${details})`;
174
+ })
175
+ ].join('\n');
176
+ }
177
+
178
+ function formatWriteExpectation({ mode = 'auto', task = '', skillInvocation = null } = {}) {
179
+ if (isSkillInstallerInvocation(skillInvocation)) {
180
+ return [
181
+ '- This is a skill-install turn.',
182
+ '- You may use network and local commands needed to install or list Codex skills.',
183
+ '- Only install into $CODEX_HOME/skills, which is mapped to Codex Overleaf plugin skills for this turn.',
184
+ '- Do not edit Overleaf project files.'
185
+ ].join('\n');
186
+ }
187
+ if (mode === 'ask') {
188
+ return '- This is read-only. Inspect and explain; do not edit files.';
189
+ }
190
+ if (requestImpliesFileChanges(task)) {
191
+ return [
192
+ '- The request asks for file changes. You must edit the local workspace when you find concrete fixes.',
193
+ '- Do not stop at a suggestion list or say you will not modify files unless no safe concrete edit exists.',
194
+ '- If you intentionally leave files unchanged, explain the specific blocker in the final answer.'
195
+ ].join('\n');
196
+ }
197
+ return [
198
+ '- This mode can write. If the request asks for corrections, revisions, fixes, polishing, updates, or implementation, edit the local workspace directly.',
199
+ '- If the request is purely an inspection question, report findings without inventing unnecessary edits.'
200
+ ].join('\n');
201
+ }
202
+
203
+ function requestImpliesFileChanges(task = '') {
204
+ return /修正|修复|修改|改[一-龥]*|完善|补全|补充|润色|重写|改写|整理|调整|应用|写入|fix|correct|repair|revise|edit|update|rewrite|polish|improve|apply/i.test(String(task || ''));
205
+ }
206
+
207
+ function normalizeFocusFiles(value) {
208
+ const seen = new Set();
209
+ const files = [];
210
+ for (const item of Array.isArray(value) ? value : []) {
211
+ const filePath = normalizeProjectPath(item);
212
+ if (!filePath || seen.has(filePath)) {
213
+ continue;
214
+ }
215
+ seen.add(filePath);
216
+ files.push(filePath);
217
+ if (files.length >= 8) {
218
+ break;
219
+ }
220
+ }
221
+ return files;
222
+ }
223
+
224
+ function normalizeProjectPath(value) {
225
+ return String(value || '')
226
+ .replace(/^@file:/i, '')
227
+ .replace(/\\/g, '/')
228
+ .trim()
229
+ .replace(/^\/+/, '');
230
+ }
231
+
232
+ function formatFocusFiles(files) {
233
+ if (!files.length) {
234
+ return '- none; use the whole project when needed.';
235
+ }
236
+ return files.map(filePath => `- ${filePath}`).join('\n');
237
+ }
238
+
239
+ function formatCompileLogContext(context = {}) {
240
+ const log = String(context.compileLog || '').trim();
241
+ if (!log) {
242
+ return '- none provided.';
243
+ }
244
+
245
+ const errors = normalizeCompileMessages(context.compileErrors);
246
+ const warnings = normalizeCompileMessages(context.compileWarnings);
247
+ const fresh = context.compileLogFresh === false
248
+ ? 'possibly stale'
249
+ : 'fresh';
250
+ const compiledAt = context.compileLogCompiledAt
251
+ ? new Date(context.compileLogCompiledAt).toISOString()
252
+ : 'unknown';
253
+
254
+ return [
255
+ `- status: ${fresh}`,
256
+ `- compiledAt: ${compiledAt}`,
257
+ `- errors: ${errors.length}`,
258
+ `- warnings: ${warnings.length}`,
259
+ errors.length ? `- error summary:\n${errors.slice(0, 8).map(message => ` - ${message}`).join('\n')}` : '',
260
+ warnings.length ? `- warning summary:\n${warnings.slice(0, 8).map(message => ` - ${message}`).join('\n')}` : '',
261
+ '- log:',
262
+ fencedBlock(log)
263
+ ].filter(Boolean).join('\n');
264
+ }
265
+
266
+ function formatCustomInstructionsContext(customInstructions = '') {
267
+ const instructions = truncateText(
268
+ String(customInstructions || '').trim(),
269
+ PROJECT_CUSTOM_INSTRUCTIONS_MAX_CHARS
270
+ );
271
+ if (!instructions) {
272
+ return '- none provided.';
273
+ }
274
+ return fencedBlock(instructions);
275
+ }
276
+
277
+ function formatProjectLocalSkillsContext(context = {}) {
278
+ const selectedSkillIds = Array.isArray(context.selectedSkillIds) ? context.selectedSkillIds : [];
279
+ if (!selectedSkillIds.length) {
280
+ return '- none selected.';
281
+ }
282
+ const loaded = context.projectLocalSkills || { skills: [], missing: [] };
283
+ const sections = [];
284
+ for (const skill of loaded.skills || []) {
285
+ sections.push([
286
+ `## ${skill.id}: ${skill.title || skill.name || skill.id}`,
287
+ fencedBlock(skill.content)
288
+ ].join('\n'));
289
+ }
290
+ if (Array.isArray(loaded.missing) && loaded.missing.length) {
291
+ sections.push([
292
+ 'Missing selected local skills:',
293
+ loaded.missing.map(id => `- ${id}`).join('\n')
294
+ ].join('\n'));
295
+ }
296
+ return sections.length ? sections.join('\n\n') : '- none loaded.';
297
+ }
298
+
299
+ function formatCodexSkillLoadingContext(context = {}) {
300
+ return [
301
+ `- Codex local skills: ${context.loadCodexLocalSkills === false ? 'disabled' : 'enabled'}`,
302
+ `- Codex Overleaf skills: ${context.loadCodexOverleafSkills === false ? 'disabled' : 'enabled'}`
303
+ ].join('\n');
304
+ }
305
+
306
+ function formatSkillInvocationContext(context = {}) {
307
+ const invocation = normalizeSkillInvocation(context.skillInvocation);
308
+ const codexSkillInvocationContext = context.codexSkillInvocationContext || {};
309
+ const contextInvocation = normalizeSkillInvocation(codexSkillInvocationContext.invocation);
310
+ const effectiveInvocation = contextInvocation || invocation;
311
+ if (Array.isArray(codexSkillInvocationContext.ignored) && codexSkillInvocationContext.ignored.length) {
312
+ return [
313
+ 'Ignored selected Codex Overleaf skill:',
314
+ ...codexSkillInvocationContext.ignored.map(item => `- ${item.id}`),
315
+ '- Reason: Codex Overleaf skills are disabled for this turn.',
316
+ '- The stale slash selection was ignored; no selected skill instructions were forced into this prompt.'
317
+ ].join('\n');
318
+ }
319
+ if (Array.isArray(codexSkillInvocationContext.missing) && codexSkillInvocationContext.missing.length) {
320
+ return [
321
+ 'Missing selected Codex Overleaf skill:',
322
+ ...codexSkillInvocationContext.missing.map(id => `- ${id}`),
323
+ '- The stale slash selection was ignored; no selected skill instructions were forced into this prompt.'
324
+ ].join('\n');
325
+ }
326
+ if (!effectiveInvocation) {
327
+ return '- none.';
328
+ }
329
+ if (isSkillInstallerInvocation(effectiveInvocation)) {
330
+ return [
331
+ `- ${effectiveInvocation.id} (${effectiveInvocation.title})`,
332
+ '- Use the Codex skill-installer behavior for this turn.',
333
+ '- Install skills into the Codex Overleaf plugin skill home; this bridge maps $CODEX_HOME/skills to that persistent plugin skills directory.',
334
+ '- Accept natural-language requests, curated skill names, GitHub repo paths, or GitHub URLs.',
335
+ '- If the request does not name a skill or location, list installable curated skills and ask which one to install.',
336
+ '- Do not edit the Overleaf project workspace or write installed skill files into the project mirror.'
337
+ ].join('\n');
338
+ }
339
+ const loadedSkill = codexSkillInvocationContext.skill;
340
+ if (loadedSkill) {
341
+ return [
342
+ `- ${effectiveInvocation.id} (${effectiveInvocation.title})`,
343
+ '- REQUIRED for this turn: read and use this selected Codex Overleaf skill before answering.',
344
+ '- Follow the embedded SKILL.md instructions when they apply to the current user request.',
345
+ '',
346
+ 'Selected Codex Overleaf SKILL.md:',
347
+ `## ${loadedSkill.id}: ${loadedSkill.title || effectiveInvocation.title || loadedSkill.id}`,
348
+ fencedBlock(loadedSkill.content)
349
+ ].join('\n');
350
+ }
351
+ return [
352
+ `- ${effectiveInvocation.id} (${effectiveInvocation.title})`,
353
+ '- Use this selected Codex Overleaf skill for the current turn.',
354
+ '- Follow the skill instructions and workflow when they apply to the user request.'
355
+ ].join('\n');
356
+ }
357
+
358
+ function normalizeSkillInvocation(value) {
359
+ const id = String(value?.id || value?.skillId || '').trim();
360
+ if (!isSafeSkillId(id)) {
361
+ return null;
362
+ }
363
+ const title = String(value?.title || value?.name || (id === 'skill-installer' ? 'Skill Installer' : id))
364
+ .trim()
365
+ .slice(0, 80) || 'Skill Installer';
366
+ if (id === 'skill-installer') {
367
+ return { id, title };
368
+ }
369
+ if (value?.scope !== 'codex-overleaf' && !Object.prototype.hasOwnProperty.call(value || {}, 'skillId')) {
370
+ return null;
371
+ }
372
+ return { id, title, scope: 'codex-overleaf' };
373
+ }
374
+
375
+ function isSkillInstallerInvocation(value) {
376
+ return normalizeSkillInvocation(value)?.id === 'skill-installer';
377
+ }
378
+
379
+ function isSafeSkillId(id) {
380
+ return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(String(id || ''))
381
+ && !String(id || '').includes('..');
382
+ }
383
+
384
+ function normalizeCompileMessages(value) {
385
+ const messages = Array.isArray(value) ? value : [];
386
+ return messages
387
+ .map(message => String(message || '').replace(/\s+/g, ' ').trim())
388
+ .filter(Boolean);
389
+ }
390
+
391
+ function fencedBlock(value) {
392
+ return [
393
+ '```text',
394
+ String(value || '').replace(/```/g, '` ` `'),
395
+ '```'
396
+ ].join('\n');
397
+ }
398
+
399
+ function formatSessionHistory(history) {
400
+ const turns = Array.isArray(history) ? history.slice(-8) : [];
401
+ if (!turns.length) {
402
+ return '- none';
403
+ }
404
+ return turns.map((turn, index) => {
405
+ const task = truncateText(String(turn?.task || 'untitled task'), 600);
406
+ const result = truncateText(String(turn?.result || turn?.status || 'no result recorded'), 1200);
407
+ return `${index + 1}. User asked: ${task}\n Previous outcome: ${result}`;
408
+ }).join('\n');
409
+ }
410
+
411
+ module.exports = { buildCodexTurnPrompt, validatePromptSize };