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.
- package/LICENSE +21 -0
- package/README.md +457 -0
- package/bin/codex-overleaf-link.mjs +223 -0
- package/extension/src/shared/agentTranscript.js +1175 -0
- package/extension/src/shared/auditRecords.js +568 -0
- package/extension/src/shared/compatibility.js +372 -0
- package/extension/src/shared/compileAdapter.js +176 -0
- package/extension/src/shared/governanceRules.js +252 -0
- package/extension/src/shared/i18n.js +565 -0
- package/extension/src/shared/models.js +106 -0
- package/extension/src/shared/otText.js +505 -0
- package/extension/src/shared/projectFiles.js +180 -0
- package/extension/src/shared/reviewing.js +99 -0
- package/extension/src/shared/sensitiveScan.js +116 -0
- package/extension/src/shared/sessionState.js +1084 -0
- package/extension/src/shared/staleGuard.js +150 -0
- package/extension/src/shared/storageDb.js +986 -0
- package/extension/src/shared/storageKeys.js +29 -0
- package/extension/src/shared/storageMigration.js +168 -0
- package/extension/src/shared/summary.js +248 -0
- package/extension/src/shared/undoOperations.js +369 -0
- package/native-host/src/codexArgs.js +43 -0
- package/native-host/src/codexHome.js +538 -0
- package/native-host/src/codexModels.js +247 -0
- package/native-host/src/codexPrompt.js +192 -0
- package/native-host/src/codexPromptAssembly.js +411 -0
- package/native-host/src/codexSessionRunner.js +1247 -0
- package/native-host/src/commandApproval.js +914 -0
- package/native-host/src/debugLog.js +78 -0
- package/native-host/src/diffEngine.js +247 -0
- package/native-host/src/index.js +132 -0
- package/native-host/src/launcher.js +81 -0
- package/native-host/src/localSkills.js +476 -0
- package/native-host/src/manifest.js +226 -0
- package/native-host/src/mirrorSensitiveScan.js +119 -0
- package/native-host/src/mirrorWorkspace.js +1019 -0
- package/native-host/src/nativeDoctor.js +826 -0
- package/native-host/src/nativeEnvironment.js +315 -0
- package/native-host/src/nativeHostPlatform.js +112 -0
- package/native-host/src/nativeMessaging.js +60 -0
- package/native-host/src/nativeQuotas.js +294 -0
- package/native-host/src/nativeResponseBudget.js +194 -0
- package/native-host/src/runtimeInstaller.js +357 -0
- package/native-host/src/taskRunner.js +3 -0
- package/native-host/src/taskRunnerRuntime.js +1083 -0
- package/native-host/src/textPatch.js +287 -0
- package/package.json +40 -0
- package/scripts/codex-json-agent.mjs +269 -0
- package/scripts/install-native-host.mjs +255 -0
- package/scripts/npm-package-files-v1.1.1.txt +52 -0
- package/scripts/uninstall-native-host.mjs +298 -0
- package/scripts/verify-npm-package.mjs +296 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { getProjectMirror } = require('./mirrorWorkspace');
|
|
6
|
+
const { getHomeDir } = require('./nativeHostPlatform');
|
|
7
|
+
const { truncateText } = require('./debugLog');
|
|
8
|
+
|
|
9
|
+
const SKILLS_DIR = path.join('.codex-overleaf', 'skills');
|
|
10
|
+
const CODEX_OVERLEAF_SKILLS_DIR = path.join('.codex-overleaf', 'skills');
|
|
11
|
+
const MAX_SKILL_CONTENT_BYTES = 64 * 1024;
|
|
12
|
+
const MAX_SKILL_CONTENT_CHARS = MAX_SKILL_CONTENT_BYTES;
|
|
13
|
+
const MAX_SKILL_PREVIEW_CHARS = 240;
|
|
14
|
+
const PROJECT_SKILL_SCOPE = 'project';
|
|
15
|
+
const CODEX_OVERLEAF_SKILL_SCOPE = 'codex-overleaf';
|
|
16
|
+
|
|
17
|
+
function listProjectSkills({ projectId, rootDir } = {}) {
|
|
18
|
+
const skillsDir = getProjectSkillsDir(projectId, { rootDir });
|
|
19
|
+
if (!fs.existsSync(skillsDir)) {
|
|
20
|
+
return { skills: [] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const skills = [];
|
|
24
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
25
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const id = entry.name.slice(0, -3);
|
|
29
|
+
if (!isSafeSkillId(id)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const filePath = resolveSkillPath(projectId, id, { rootDir });
|
|
33
|
+
const stat = fs.statSync(filePath);
|
|
34
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
35
|
+
const title = inferSkillTitle(content) || id;
|
|
36
|
+
skills.push({
|
|
37
|
+
id,
|
|
38
|
+
title,
|
|
39
|
+
name: title,
|
|
40
|
+
scope: PROJECT_SKILL_SCOPE,
|
|
41
|
+
size: stat.size,
|
|
42
|
+
updatedAt: stat.mtime.toISOString(),
|
|
43
|
+
preview: buildSkillPreview(content)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { skills: skills.sort((left, right) => left.id.localeCompare(right.id)) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function installProjectSkill({ projectId, skillId, content, rootDir } = {}) {
|
|
51
|
+
const id = validateSkillId(skillId);
|
|
52
|
+
if (typeof content !== 'string') {
|
|
53
|
+
throw new Error('Project-local skills require markdown/text content');
|
|
54
|
+
}
|
|
55
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_SKILL_CONTENT_BYTES) {
|
|
56
|
+
throw new Error(`Project-local skill content exceeds ${MAX_SKILL_CONTENT_BYTES} bytes`);
|
|
57
|
+
}
|
|
58
|
+
if (content.includes('\u0000')) {
|
|
59
|
+
throw new Error('Project-local skill content must be markdown/text');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const filePath = resolveSkillPath(projectId, id, { rootDir });
|
|
63
|
+
const skillsDir = getProjectSkillsDir(projectId, { rootDir });
|
|
64
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
65
|
+
assertNoSymlinkEscape(path.dirname(filePath), skillsDir, 'Unsafe project skill path');
|
|
66
|
+
assertNoSymlinkEscape(filePath, skillsDir, 'Unsafe project skill path');
|
|
67
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
68
|
+
|
|
69
|
+
const stat = fs.statSync(filePath);
|
|
70
|
+
const title = inferSkillTitle(content) || id;
|
|
71
|
+
return {
|
|
72
|
+
id,
|
|
73
|
+
title,
|
|
74
|
+
name: title,
|
|
75
|
+
scope: PROJECT_SKILL_SCOPE,
|
|
76
|
+
size: stat.size,
|
|
77
|
+
updatedAt: stat.mtime.toISOString(),
|
|
78
|
+
preview: buildSkillPreview(content)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeProjectSkill({ projectId, skillId, rootDir } = {}) {
|
|
83
|
+
const id = validateSkillId(skillId);
|
|
84
|
+
const filePath = resolveSkillPath(projectId, id, { rootDir });
|
|
85
|
+
assertNoSymlinkEscape(filePath, getProjectSkillsDir(projectId, { rootDir }), 'Unsafe project skill path');
|
|
86
|
+
const removed = fs.existsSync(filePath);
|
|
87
|
+
if (removed) {
|
|
88
|
+
fs.rmSync(filePath, { force: true });
|
|
89
|
+
}
|
|
90
|
+
return { id, removed };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function listCodexOverleafSkills({ env = process.env, skillsRoot } = {}) {
|
|
94
|
+
const root = getCodexOverleafSkillsRoot({ env, skillsRoot });
|
|
95
|
+
if (!fs.existsSync(root)) {
|
|
96
|
+
return { skills: [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const skills = [];
|
|
100
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
101
|
+
if (!entry.isDirectory() || !isSafeSkillId(entry.name)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const filePath = resolveCodexOverleafSkillPath(entry.name, { env, skillsRoot });
|
|
105
|
+
if (!fs.existsSync(filePath)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const stat = fs.statSync(filePath);
|
|
109
|
+
if (!stat.isFile()) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
113
|
+
skills.push(buildSkillMetadata({
|
|
114
|
+
id: entry.name,
|
|
115
|
+
content,
|
|
116
|
+
size: stat.size,
|
|
117
|
+
updatedAt: stat.mtime.toISOString(),
|
|
118
|
+
scope: CODEX_OVERLEAF_SKILL_SCOPE
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { skills: skills.sort((left, right) => left.id.localeCompare(right.id)) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function installCodexOverleafSkill({ skillId, content, env = process.env, skillsRoot } = {}) {
|
|
126
|
+
const id = validateSkillId(skillId);
|
|
127
|
+
validateSkillContent(content, 'Codex Overleaf skills');
|
|
128
|
+
|
|
129
|
+
const root = getCodexOverleafSkillsRoot({ env, skillsRoot });
|
|
130
|
+
const filePath = resolveCodexOverleafSkillPath(id, { env, skillsRoot });
|
|
131
|
+
const skillDir = path.dirname(filePath);
|
|
132
|
+
assertNoSymlinkEscape(skillDir, root, 'Unsafe Codex Overleaf skill path');
|
|
133
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
134
|
+
assertNoSymlinkEscape(skillDir, root, 'Unsafe Codex Overleaf skill path');
|
|
135
|
+
assertNoSymlinkEscape(filePath, root, 'Unsafe Codex Overleaf skill path');
|
|
136
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
137
|
+
|
|
138
|
+
const stat = fs.statSync(filePath);
|
|
139
|
+
return buildSkillMetadata({
|
|
140
|
+
id,
|
|
141
|
+
content,
|
|
142
|
+
size: stat.size,
|
|
143
|
+
updatedAt: stat.mtime.toISOString(),
|
|
144
|
+
scope: CODEX_OVERLEAF_SKILL_SCOPE
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function removeCodexOverleafSkill({ skillId, env = process.env, skillsRoot } = {}) {
|
|
149
|
+
const id = validateSkillId(skillId);
|
|
150
|
+
const root = getCodexOverleafSkillsRoot({ env, skillsRoot });
|
|
151
|
+
const skillDir = resolveInside(root, id, 'Unsafe Codex Overleaf skill path');
|
|
152
|
+
const removed = fs.existsSync(skillDir);
|
|
153
|
+
if (removed) {
|
|
154
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
return { id, scope: CODEX_OVERLEAF_SKILL_SCOPE, removed };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function loadSelectedProjectSkills({ projectId, selectedSkillIds, rootDir, projectRoot } = {}) {
|
|
160
|
+
const ids = normalizeSelectedSkillIds(selectedSkillIds);
|
|
161
|
+
const skills = [];
|
|
162
|
+
const missing = [];
|
|
163
|
+
|
|
164
|
+
for (const id of ids) {
|
|
165
|
+
const filePath = resolveSkillPath(projectId, id, { rootDir, projectRoot });
|
|
166
|
+
assertNoSymlinkEscape(filePath, getProjectSkillsDir(projectId, { rootDir, projectRoot }), 'Unsafe project skill path');
|
|
167
|
+
if (!fs.existsSync(filePath)) {
|
|
168
|
+
missing.push(id);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const stat = fs.statSync(filePath);
|
|
172
|
+
if (stat.size > MAX_SKILL_CONTENT_BYTES) {
|
|
173
|
+
missing.push(id);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
177
|
+
skills.push({
|
|
178
|
+
id,
|
|
179
|
+
title: inferSkillTitle(content) || id,
|
|
180
|
+
content: truncateText(content, MAX_SKILL_CONTENT_CHARS)
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { skills, missing };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function materializeProjectSkillsAsCodexSkills({
|
|
188
|
+
projectId,
|
|
189
|
+
rootDir,
|
|
190
|
+
projectRoot,
|
|
191
|
+
targetRoot
|
|
192
|
+
} = {}) {
|
|
193
|
+
const target = path.resolve(String(targetRoot || ''));
|
|
194
|
+
if (!targetRoot) {
|
|
195
|
+
throw new Error('Codex skill target root is required');
|
|
196
|
+
}
|
|
197
|
+
const skillsDir = getProjectSkillsDir(projectId, { rootDir, projectRoot });
|
|
198
|
+
const result = { installed: [], skipped: [] };
|
|
199
|
+
if (!fs.existsSync(skillsDir)) {
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fs.mkdirSync(target, { recursive: true });
|
|
204
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
205
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const id = entry.name.slice(0, -3);
|
|
209
|
+
if (!isSafeSkillId(id)) {
|
|
210
|
+
result.skipped.push({ id, reason: 'unsafe_id' });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const sourcePath = resolveSkillPath(projectId, id, { rootDir, projectRoot });
|
|
214
|
+
assertNoSymlinkEscape(sourcePath, skillsDir, 'Unsafe project skill path');
|
|
215
|
+
const stat = fs.statSync(sourcePath);
|
|
216
|
+
if (!stat.isFile() || stat.size > MAX_SKILL_CONTENT_BYTES) {
|
|
217
|
+
result.skipped.push({ id, reason: 'invalid_content' });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const skillDir = resolveInside(target, id, 'Unsafe materialized project skill path');
|
|
221
|
+
const skillPath = resolveInside(skillDir, 'SKILL.md', 'Unsafe materialized project skill path');
|
|
222
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
223
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
224
|
+
fs.writeFileSync(skillPath, fs.readFileSync(sourcePath, 'utf8'), 'utf8');
|
|
225
|
+
result.installed.push(id);
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function loadSelectedCodexOverleafSkill({
|
|
231
|
+
skillId,
|
|
232
|
+
loadCodexOverleafSkills = true,
|
|
233
|
+
env = process.env,
|
|
234
|
+
skillsRoot
|
|
235
|
+
} = {}) {
|
|
236
|
+
const rawId = String(skillId || '').trim();
|
|
237
|
+
if (!rawId) {
|
|
238
|
+
return { skill: null, missing: [], ignored: [] };
|
|
239
|
+
}
|
|
240
|
+
const id = validateSkillId(rawId);
|
|
241
|
+
if (loadCodexOverleafSkills === false) {
|
|
242
|
+
return {
|
|
243
|
+
skill: null,
|
|
244
|
+
missing: [],
|
|
245
|
+
ignored: [{
|
|
246
|
+
id,
|
|
247
|
+
reason: 'codex_overleaf_skills_disabled'
|
|
248
|
+
}]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const filePath = resolveCodexOverleafSkillPath(id, { env, skillsRoot });
|
|
253
|
+
if (!fs.existsSync(filePath)) {
|
|
254
|
+
return { skill: null, missing: [id], ignored: [] };
|
|
255
|
+
}
|
|
256
|
+
const stat = fs.statSync(filePath);
|
|
257
|
+
if (!stat.isFile() || stat.size > MAX_SKILL_CONTENT_BYTES) {
|
|
258
|
+
return { skill: null, missing: [id], ignored: [] };
|
|
259
|
+
}
|
|
260
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
261
|
+
return {
|
|
262
|
+
skill: {
|
|
263
|
+
id,
|
|
264
|
+
title: inferSkillTitle(content) || id,
|
|
265
|
+
scope: CODEX_OVERLEAF_SKILL_SCOPE,
|
|
266
|
+
path: filePath,
|
|
267
|
+
content: truncateText(content, MAX_SKILL_CONTENT_CHARS)
|
|
268
|
+
},
|
|
269
|
+
missing: [],
|
|
270
|
+
ignored: []
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeSelectedSkillIds(selectedSkillIds) {
|
|
275
|
+
const seen = new Set();
|
|
276
|
+
const ids = [];
|
|
277
|
+
for (const value of Array.isArray(selectedSkillIds) ? selectedSkillIds : []) {
|
|
278
|
+
const id = validateSkillId(value);
|
|
279
|
+
if (seen.has(id)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
seen.add(id);
|
|
283
|
+
ids.push(id);
|
|
284
|
+
}
|
|
285
|
+
return ids;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getProjectSkillsDir(projectId, options = {}) {
|
|
289
|
+
validateProjectId(projectId);
|
|
290
|
+
const mirror = options.projectRoot
|
|
291
|
+
? { projectRoot: options.projectRoot }
|
|
292
|
+
: getProjectMirror(projectId, options);
|
|
293
|
+
const root = path.resolve(mirror.projectRoot);
|
|
294
|
+
const skillsDir = path.resolve(mirror.projectRoot, SKILLS_DIR);
|
|
295
|
+
if (skillsDir !== root && !skillsDir.startsWith(root + path.sep)) {
|
|
296
|
+
throw new Error('Unsafe project skills path');
|
|
297
|
+
}
|
|
298
|
+
return skillsDir;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getCodexOverleafSkillsRoot({ env = process.env, skillsRoot } = {}) {
|
|
302
|
+
const defaultRoot = getDefaultCodexOverleafSkillsRoot({ env });
|
|
303
|
+
if (skillsRoot) {
|
|
304
|
+
return validateCodexOverleafSkillsRoot(path.resolve(skillsRoot), defaultRoot);
|
|
305
|
+
}
|
|
306
|
+
if (env.CODEX_OVERLEAF_SKILLS_ROOT) {
|
|
307
|
+
return validateCodexOverleafSkillsRoot(path.resolve(env.CODEX_OVERLEAF_SKILLS_ROOT), defaultRoot);
|
|
308
|
+
}
|
|
309
|
+
return validateCodexOverleafSkillsRoot(defaultRoot, defaultRoot);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function getDefaultCodexOverleafSkillsRoot({ env = process.env } = {}) {
|
|
313
|
+
return path.resolve(getHomeDir({ env }), CODEX_OVERLEAF_SKILLS_DIR);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function validateCodexOverleafSkillsRoot(root, defaultRoot) {
|
|
317
|
+
const resolvedRoot = path.resolve(root);
|
|
318
|
+
const resolvedDefault = path.resolve(defaultRoot);
|
|
319
|
+
if (!isInsideOrSamePath(resolvedRoot, resolvedDefault)) {
|
|
320
|
+
throw new Error(`Codex Overleaf skill root must stay inside ${resolvedDefault}`);
|
|
321
|
+
}
|
|
322
|
+
assertNoSymlinkEscape(resolvedRoot, path.dirname(resolvedDefault), 'Codex Overleaf skill root escapes the plugin data root');
|
|
323
|
+
return resolvedRoot;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function resolveSkillPath(projectId, skillId, options = {}) {
|
|
327
|
+
const id = validateSkillId(skillId);
|
|
328
|
+
const skillsDir = getProjectSkillsDir(projectId, options);
|
|
329
|
+
const target = path.resolve(skillsDir, `${id}.md`);
|
|
330
|
+
if (target !== skillsDir && !target.startsWith(skillsDir + path.sep)) {
|
|
331
|
+
throw new Error('Unsafe project skill path');
|
|
332
|
+
}
|
|
333
|
+
assertNoSymlinkEscape(target, skillsDir, 'Unsafe project skill path');
|
|
334
|
+
return target;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function resolveCodexOverleafSkillPath(skillId, options = {}) {
|
|
338
|
+
const id = validateSkillId(skillId);
|
|
339
|
+
const root = getCodexOverleafSkillsRoot(options);
|
|
340
|
+
const skillDir = resolveInside(root, id, 'Unsafe Codex Overleaf skill path');
|
|
341
|
+
return resolveInside(skillDir, 'SKILL.md', 'Unsafe Codex Overleaf skill path');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function resolveInside(root, child, message) {
|
|
345
|
+
const base = path.resolve(root);
|
|
346
|
+
const target = path.resolve(base, child);
|
|
347
|
+
if (!isInsideOrSamePath(target, base)) {
|
|
348
|
+
throw new Error(message);
|
|
349
|
+
}
|
|
350
|
+
return target;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function assertNoSymlinkEscape(target, containmentRoot, message) {
|
|
354
|
+
const root = path.resolve(containmentRoot);
|
|
355
|
+
const resolvedTarget = path.resolve(target);
|
|
356
|
+
if (!isInsideOrSamePath(resolvedTarget, root)) {
|
|
357
|
+
throw new Error(message);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const relativeParts = path.relative(root, resolvedTarget).split(path.sep).filter(Boolean);
|
|
361
|
+
let current = root;
|
|
362
|
+
assertNotSymlink(current, message);
|
|
363
|
+
for (const part of relativeParts) {
|
|
364
|
+
current = path.join(current, part);
|
|
365
|
+
assertNotSymlink(current, message);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function assertNotSymlink(target, message) {
|
|
370
|
+
try {
|
|
371
|
+
if (fs.lstatSync(target).isSymbolicLink()) {
|
|
372
|
+
throw new Error(message);
|
|
373
|
+
}
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error.code === 'ENOENT') {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isInsideOrSamePath(target, root) {
|
|
383
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
384
|
+
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function validateProjectId(projectId) {
|
|
388
|
+
if (typeof projectId !== 'string' || !projectId.trim()) {
|
|
389
|
+
throw new Error('Project id is required');
|
|
390
|
+
}
|
|
391
|
+
if (projectId.length > 512) {
|
|
392
|
+
throw new Error('Project id is too long');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function validateSkillId(skillId) {
|
|
397
|
+
const id = String(skillId || '').trim();
|
|
398
|
+
if (!isSafeSkillId(id)) {
|
|
399
|
+
throw new Error('Invalid skill id');
|
|
400
|
+
}
|
|
401
|
+
return id;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function validateSkillContent(content, label) {
|
|
405
|
+
if (typeof content !== 'string') {
|
|
406
|
+
throw new Error(`${label} require markdown/text content`);
|
|
407
|
+
}
|
|
408
|
+
if (Buffer.byteLength(content, 'utf8') > MAX_SKILL_CONTENT_BYTES) {
|
|
409
|
+
throw new Error(`${label} content exceeds ${MAX_SKILL_CONTENT_BYTES} bytes`);
|
|
410
|
+
}
|
|
411
|
+
if (content.includes('\u0000')) {
|
|
412
|
+
throw new Error(`${label} content must be markdown/text`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isSafeSkillId(id) {
|
|
417
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(String(id || ''))
|
|
418
|
+
&& !String(id).includes('..');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function inferSkillTitle(content) {
|
|
422
|
+
for (const line of String(content || '').split(/\r?\n/)) {
|
|
423
|
+
const heading = /^#\s+(.+?)\s*$/.exec(line);
|
|
424
|
+
if (heading) {
|
|
425
|
+
return truncateText(heading[1].trim(), 120);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
for (const line of String(content || '').split(/\r?\n/)) {
|
|
429
|
+
const clean = line.trim();
|
|
430
|
+
if (clean) {
|
|
431
|
+
return truncateText(clean.replace(/^#+\s*/, ''), 120);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildSkillPreview(content) {
|
|
438
|
+
return truncateText(
|
|
439
|
+
String(content || '')
|
|
440
|
+
.replace(/^#\s+.*(?:\r?\n)+/, '')
|
|
441
|
+
.replace(/\s+/g, ' ')
|
|
442
|
+
.trim(),
|
|
443
|
+
MAX_SKILL_PREVIEW_CHARS
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function buildSkillMetadata({ id, content, size, updatedAt, scope }) {
|
|
448
|
+
const title = inferSkillTitle(content) || id;
|
|
449
|
+
return {
|
|
450
|
+
id,
|
|
451
|
+
title,
|
|
452
|
+
name: title,
|
|
453
|
+
scope,
|
|
454
|
+
size,
|
|
455
|
+
updatedAt,
|
|
456
|
+
preview: buildSkillPreview(content)
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
module.exports = {
|
|
461
|
+
CODEX_OVERLEAF_SKILL_SCOPE,
|
|
462
|
+
MAX_SKILL_CONTENT_BYTES,
|
|
463
|
+
MAX_SKILL_CONTENT_CHARS,
|
|
464
|
+
PROJECT_SKILL_SCOPE,
|
|
465
|
+
getDefaultCodexOverleafSkillsRoot,
|
|
466
|
+
getCodexOverleafSkillsRoot,
|
|
467
|
+
installCodexOverleafSkill,
|
|
468
|
+
installProjectSkill,
|
|
469
|
+
listCodexOverleafSkills,
|
|
470
|
+
listProjectSkills,
|
|
471
|
+
loadSelectedCodexOverleafSkill,
|
|
472
|
+
loadSelectedProjectSkills,
|
|
473
|
+
materializeProjectSkillsAsCodexSkills,
|
|
474
|
+
removeCodexOverleafSkill,
|
|
475
|
+
removeProjectSkill
|
|
476
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const HOST_NAME = 'com.codex.overleaf';
|
|
7
|
+
const HOST_DESCRIPTION = 'Codex Overleaf local bridge';
|
|
8
|
+
const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
|
|
9
|
+
|
|
10
|
+
const MACOS_MANIFEST_DIRS = {
|
|
11
|
+
chrome: 'Library/Application Support/Google/Chrome/NativeMessagingHosts',
|
|
12
|
+
chromium: 'Library/Application Support/Chromium/NativeMessagingHosts'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const LINUX_MANIFEST_DIRS = {
|
|
16
|
+
chrome: '.config/google-chrome/NativeMessagingHosts',
|
|
17
|
+
chromium: '.config/chromium/NativeMessagingHosts'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const WINDOWS_REGISTRY_PATHS = {
|
|
21
|
+
chrome: 'Software\\Google\\Chrome\\NativeMessagingHosts',
|
|
22
|
+
chromium: 'Software\\Chromium\\NativeMessagingHosts'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function validateExtensionId(extensionId) {
|
|
26
|
+
return typeof extensionId === 'string' && /^[a-p]{32}$/.test(extensionId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const validateChromeExtensionId = validateExtensionId;
|
|
30
|
+
|
|
31
|
+
function buildAllowedOrigin(extensionId) {
|
|
32
|
+
if (!validateExtensionId(extensionId)) {
|
|
33
|
+
throw new Error(`Invalid Chrome extension id: ${extensionId}`);
|
|
34
|
+
}
|
|
35
|
+
return `chrome-extension://${extensionId}/`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildHostManifest({ extensionId, extensionIds, bridgePath, platform = process.platform }) {
|
|
39
|
+
const ids = normalizeExtensionIds(extensionIds || [extensionId]);
|
|
40
|
+
if (!ids.length) {
|
|
41
|
+
throw new Error(`Invalid Chrome extension id: ${extensionId}`);
|
|
42
|
+
}
|
|
43
|
+
if (!bridgePath || !isAbsolutePathForPlatform(bridgePath, platform)) {
|
|
44
|
+
throw new Error('Native bridge path must be absolute');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
name: HOST_NAME,
|
|
49
|
+
description: HOST_DESCRIPTION,
|
|
50
|
+
path: bridgePath,
|
|
51
|
+
type: 'stdio',
|
|
52
|
+
allowed_origins: ids.map(buildAllowedOrigin)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeExtensionIds(extensionIds) {
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const ids = [];
|
|
59
|
+
for (const value of Array.isArray(extensionIds) ? extensionIds : [extensionIds]) {
|
|
60
|
+
const id = typeof value === 'string' ? value : '';
|
|
61
|
+
if (!id || seen.has(id)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!validateExtensionId(id)) {
|
|
65
|
+
throw new Error(`Invalid Chrome extension id: ${id}`);
|
|
66
|
+
}
|
|
67
|
+
seen.add(id);
|
|
68
|
+
ids.push(id);
|
|
69
|
+
}
|
|
70
|
+
return ids;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isAbsolutePathForPlatform(targetPath, platform = process.platform) {
|
|
74
|
+
if (platform === 'win32') {
|
|
75
|
+
return path.win32.isAbsolute(targetPath);
|
|
76
|
+
}
|
|
77
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
78
|
+
return path.posix.isAbsolute(targetPath);
|
|
79
|
+
}
|
|
80
|
+
return path.isAbsolute(targetPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getNativeHostManifestPath(options = {}) {
|
|
84
|
+
const platform = options.platform || process.platform;
|
|
85
|
+
const browser = normalizeBrowser(options.browser || 'chrome');
|
|
86
|
+
|
|
87
|
+
if (platform === 'darwin') {
|
|
88
|
+
const manifestDir = MACOS_MANIFEST_DIRS[browser];
|
|
89
|
+
if (!manifestDir) {
|
|
90
|
+
throwUnsupportedBrowser(platform, browser);
|
|
91
|
+
}
|
|
92
|
+
return requireAbsolutePath(
|
|
93
|
+
path.posix.join(getHomeDir(options), manifestDir, `${HOST_NAME}.json`),
|
|
94
|
+
platform
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (platform === 'linux') {
|
|
99
|
+
const manifestDir = LINUX_MANIFEST_DIRS[browser];
|
|
100
|
+
if (!manifestDir) {
|
|
101
|
+
throwUnsupportedBrowser(platform, browser);
|
|
102
|
+
}
|
|
103
|
+
return requireAbsolutePath(
|
|
104
|
+
path.posix.join(getHomeDir(options), manifestDir, `${HOST_NAME}.json`),
|
|
105
|
+
platform
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (platform === 'win32') {
|
|
110
|
+
return requireAbsolutePath(
|
|
111
|
+
path.win32.join(
|
|
112
|
+
getWindowsLocalAppData(options),
|
|
113
|
+
'codex-overleaf',
|
|
114
|
+
'native-messaging-hosts',
|
|
115
|
+
`${HOST_NAME}.json`
|
|
116
|
+
),
|
|
117
|
+
platform
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throwUnsupportedPlatform(platform);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getChromeNativeHostManifestPath(options = {}) {
|
|
125
|
+
return getNativeHostManifestPath({
|
|
126
|
+
...options,
|
|
127
|
+
platform: 'darwin',
|
|
128
|
+
browser: 'chrome'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getWindowsRegistryMetadata(options = {}) {
|
|
133
|
+
const browser = normalizeBrowser(options.browser || 'chrome');
|
|
134
|
+
const registryPath = WINDOWS_REGISTRY_PATHS[browser];
|
|
135
|
+
if (!registryPath) {
|
|
136
|
+
throwUnsupportedBrowser('win32', browser);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const registryKey = `HKCU\\${registryPath}\\${HOST_NAME}`;
|
|
140
|
+
const manifestPath = getNativeHostManifestPath({
|
|
141
|
+
...options,
|
|
142
|
+
platform: 'win32',
|
|
143
|
+
browser
|
|
144
|
+
});
|
|
145
|
+
const quotedRegistryKey = quoteWindowsCommandArg(registryKey);
|
|
146
|
+
const quotedManifestPath = quoteWindowsCommandArg(manifestPath);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
kind: 'registry',
|
|
150
|
+
browser,
|
|
151
|
+
root: 'HKCU',
|
|
152
|
+
registryPath,
|
|
153
|
+
registryKey,
|
|
154
|
+
manifestPath,
|
|
155
|
+
quotedRegistryKey,
|
|
156
|
+
quotedManifestPath,
|
|
157
|
+
addArgs: ['add', registryKey, '/ve', '/t', 'REG_SZ', '/d', manifestPath, '/f'],
|
|
158
|
+
deleteArgs: ['delete', registryKey, '/f'],
|
|
159
|
+
addCommand: `reg.exe add ${quotedRegistryKey} /ve /t REG_SZ /d ${quotedManifestPath} /f`,
|
|
160
|
+
deleteCommand: `reg.exe delete ${quotedRegistryKey} /f`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeBrowser(browser) {
|
|
165
|
+
if (!browser || browser === 'auto') {
|
|
166
|
+
return 'chrome';
|
|
167
|
+
}
|
|
168
|
+
if (browser === 'chrome' || browser === 'chromium') {
|
|
169
|
+
return browser;
|
|
170
|
+
}
|
|
171
|
+
return browser;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getHomeDir(options = {}) {
|
|
175
|
+
const env = options.env || process.env;
|
|
176
|
+
if (options.homeDir) {
|
|
177
|
+
return options.homeDir;
|
|
178
|
+
}
|
|
179
|
+
return env.HOME || env.USERPROFILE || os.homedir();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getWindowsLocalAppData(options = {}) {
|
|
183
|
+
const env = options.env || process.env;
|
|
184
|
+
if (env.LOCALAPPDATA) {
|
|
185
|
+
return env.LOCALAPPDATA;
|
|
186
|
+
}
|
|
187
|
+
const homeDir = options.homeDir || env.USERPROFILE || env.HOME || os.homedir();
|
|
188
|
+
return path.win32.join(homeDir, 'AppData', 'Local');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function requireAbsolutePath(targetPath, platform) {
|
|
192
|
+
if (!isAbsolutePathForPlatform(targetPath, platform)) {
|
|
193
|
+
throw new Error(`Native host manifest path must be absolute: ${targetPath}`);
|
|
194
|
+
}
|
|
195
|
+
return targetPath;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function quoteWindowsCommandArg(value) {
|
|
199
|
+
const text = String(value);
|
|
200
|
+
if (/["\r\n]/.test(text)) {
|
|
201
|
+
throw new Error('Windows registry command argument contains an unsafe character');
|
|
202
|
+
}
|
|
203
|
+
return `"${text}"`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function throwUnsupportedPlatform(platform) {
|
|
207
|
+
throw new Error(`Unsupported native host platform: ${platform}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function throwUnsupportedBrowser(platform, browser) {
|
|
211
|
+
throw new Error(`Unsupported native host browser for ${platform}: ${browser}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
DEFAULT_CHROME_EXTENSION_ID,
|
|
216
|
+
HOST_DESCRIPTION,
|
|
217
|
+
HOST_NAME,
|
|
218
|
+
buildAllowedOrigin,
|
|
219
|
+
buildHostManifest,
|
|
220
|
+
getChromeNativeHostManifestPath,
|
|
221
|
+
getNativeHostManifestPath,
|
|
222
|
+
getWindowsRegistryMetadata,
|
|
223
|
+
normalizeExtensionIds,
|
|
224
|
+
validateChromeExtensionId,
|
|
225
|
+
validateExtensionId
|
|
226
|
+
};
|