codex-overleaf-link 1.1.2 → 1.2.0

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1>Codex Overleaf Link</h1>
4
4
  <p><strong>Empower Overleaf with Codex.</strong></p>
5
5
  <p>
6
- <img src="https://img.shields.io/badge/version-1.1.2-blue" alt="version">
6
+ <img src="https://img.shields.io/badge/version-1.2.0-blue" alt="version">
7
7
  <img src="https://img.shields.io/badge/platform-macOS%20%2F%20Windows%20%2F%20Linux-lightgrey" alt="platform">
8
8
  <img src="https://img.shields.io/badge/chrome-MV3-green" alt="chrome manifest v3">
9
9
  <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node version">
@@ -29,43 +29,36 @@ Codex Overleaf Link bridges the two: it adds a Codex panel directly inside Overl
29
29
 
30
30
  ## Install
31
31
 
32
- The official release path is still a one-command native-host install plus Chrome's required manual approval for an unpacked extension. The bundled extension key gives the official unpacked build a stable id, so you do not need to pass an extension id for normal release installs.
32
+ The recommended release path is npm-first for the native host, plus Chrome's required manual approval for the unpacked extension. The bundled extension key gives the official unpacked build a stable id, so normal release installs do not need `--extension-id`.
33
33
 
34
- macOS / Linux latest source install:
34
+ 1. Install or update the native host with the pinned npm package:
35
35
 
36
36
  ```bash
37
- curl -fsSL "https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/main/install.sh?$(date +%s)" | bash
37
+ npm exec --yes codex-overleaf-link@1.2.0 -- install-native
38
38
  ```
39
39
 
40
- Recommended version-pinned native-host install or update for v1.1.2:
40
+ 2. Download `codex-overleaf-link-extension-v1.2.0.zip` from the [v1.2.0 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.2.0), then unzip it to a stable local folder.
41
41
 
42
- ```bash
43
- npm exec --yes codex-overleaf-link@1.1.2 -- install-native
44
- ```
42
+ 3. Open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the unzipped extension folder.
43
+
44
+ 4. Open any Overleaf project. The Codex panel appears on the right.
45
+
46
+ If you modify the extension key or load a custom build that gets a different id, rerun the native install with `--extension-id <chrome-extension-id>`.
45
47
 
46
- GitHub Release script fallback for macOS / Linux:
48
+ Source installer fallback for macOS / Linux, mainly for development or source checkout installs:
47
49
 
48
50
  ```bash
49
- CODEX_OVERLEAF_REF=v1.1.2 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.2/install.sh)"
51
+ CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)"
50
52
  ```
51
53
 
52
- GitHub Release script fallback for Windows from PowerShell:
54
+ Source installer fallback for Windows from PowerShell:
53
55
 
54
56
  ```powershell
55
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.2/install.ps1 -OutFile install.ps1
56
- $env:CODEX_OVERLEAF_REF='v1.1.2'
57
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.ps1 -OutFile install.ps1
58
+ $env:CODEX_OVERLEAF_REF='v1.2.0'
57
59
  powershell -ExecutionPolicy Bypass -File install.ps1
58
60
  ```
59
61
 
60
- The macOS / Linux installer creates a visible `~/Codex Overleaf Link Extension` shortcut to the extension folder. On macOS it also opens Chrome's extension page, opens Finder to the shortcut, and copies the shortcut path. The Windows installer prints the extension folder path after registering the native host.
61
-
62
- Chrome still requires one manual approval step for unpacked extensions:
63
-
64
- 1. Enable **Developer mode** in `chrome://extensions`.
65
- 2. Click **Load unpacked** and select `~/Codex Overleaf Link Extension` on macOS/Linux, or the printed extension folder on Windows.
66
-
67
- If you modify the extension key or load a custom build that gets a different id, rerun the native install with `--extension-id <chrome-extension-id>`.
68
-
69
62
  ## npm Native Host CLI
70
63
 
71
64
  npm installs, updates, uninstalls, and diagnoses the native host only. npm does not install the Chrome extension; install the Chrome extension separately from the release source checkout or extension zip.
@@ -73,19 +66,19 @@ npm installs, updates, uninstalls, and diagnoses the native host only. npm does
73
66
  Install or update the native host for the official release extension id:
74
67
 
75
68
  ```bash
76
- npm exec --yes codex-overleaf-link@1.1.2 -- install-native
69
+ npm exec --yes codex-overleaf-link@1.2.0 -- install-native
77
70
  ```
78
71
 
79
72
  Diagnose the registered native host:
80
73
 
81
74
  ```bash
82
- npm exec --yes codex-overleaf-link@1.1.2 -- doctor
75
+ npm exec --yes codex-overleaf-link@1.2.0 -- doctor
83
76
  ```
84
77
 
85
78
  Uninstall the native host:
86
79
 
87
80
  ```bash
88
- npm exec --yes codex-overleaf-link@1.1.2 -- uninstall-native
81
+ npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native
89
82
  ```
90
83
 
91
84
  Use `--extension-id <chrome-extension-id>` only for a custom/dev unpacked extension id that differs from the official bundled id.
@@ -123,10 +116,10 @@ If Chrome assigns a different extension id, rerun `npm run install:native -- --e
123
116
  <details>
124
117
  <summary><strong>Update</strong></summary>
125
118
 
126
- For a deterministic v1.1.2 update, run the pinned npm command. This is also the native mismatch recovery command shown by the popup and panel when they report **Native host update required**.
119
+ For a deterministic v1.2.0 update, run the pinned npm command. This is also the native mismatch recovery command shown by the popup and panel when they report **Native host update required**.
127
120
 
128
121
  ```bash
129
- npm exec --yes codex-overleaf-link@1.1.2 -- install-native
122
+ npm exec --yes codex-overleaf-link@1.2.0 -- install-native
130
123
  ```
131
124
 
132
125
  If npm is unavailable, use the GitHub Release script fallback for your platform.
@@ -134,14 +127,14 @@ If npm is unavailable, use the GitHub Release script fallback for your platform.
134
127
  macOS / Linux:
135
128
 
136
129
  ```bash
137
- CODEX_OVERLEAF_REF=v1.1.2 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.2/install.sh)"
130
+ CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)"
138
131
  ```
139
132
 
140
133
  Windows PowerShell:
141
134
 
142
135
  ```powershell
143
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.2/install.ps1 -OutFile install.ps1
144
- $env:CODEX_OVERLEAF_REF='v1.1.2'
136
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.ps1 -OutFile install.ps1
137
+ $env:CODEX_OVERLEAF_REF='v1.2.0'
145
138
  powershell -ExecutionPolicy Bypass -File install.ps1
146
139
  ```
147
140
 
@@ -151,14 +144,15 @@ Then reload the extension in `chrome://extensions` and refresh the Overleaf page
151
144
 
152
145
  ## GitHub Release Artifacts
153
146
 
154
- The v1.1.2 GitHub Release contains:
147
+ The v1.2.0 GitHub Release contains:
155
148
 
156
- - `codex-overleaf-link-extension-v1.1.2.zip`: loadable Chrome extension package for manual unpacked installation.
157
- - `codex-overleaf-native-host-v1.1.2.tar.gz`: native host runtime files used by the installer and release verification.
158
- - `codex-overleaf-link-1.1.2.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
159
- - `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.1.2` when run directly from the release artifact.
160
- - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.1.2` when run directly from the release artifact.
149
+ - `codex-overleaf-link-extension-v1.2.0.zip`: loadable Chrome extension package for manual unpacked installation.
150
+ - `codex-overleaf-native-host-v1.2.0.tar.gz`: native host runtime files used by the installer and release verification.
151
+ - `codex-overleaf-link-1.2.0.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
152
+ - `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.2.0` when run directly from the release artifact.
153
+ - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.2.0` when run directly from the release artifact.
161
154
  - `uninstall-native-host.mjs`: native host uninstaller that removes the Chrome Native Messaging manifest, bridge executable, and runtime copy.
155
+ - `nativeHostPlatform.js`, `manifest.js`, `runtimeInstaller.js`: helper files required by the loose uninstaller asset.
162
156
  - `SHA256SUMS` and `release-manifest.json`: checksum and artifact metadata for release verification.
163
157
 
164
158
  <details>
@@ -208,7 +202,7 @@ The uninstaller removes the Native Messaging registration, bridge executable, an
208
202
  Linux Chromium install or update:
209
203
 
210
204
  ```bash
211
- CODEX_OVERLEAF_REF=v1.1.2 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.2/install.sh)" -- --browser chromium
205
+ CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)" -- --browser chromium
212
206
  ```
213
207
 
214
208
  Linux Chromium uninstall:
@@ -350,7 +344,7 @@ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per
350
344
  Run the pinned npm native-host installer, reload the extension in `chrome://extensions`, then refresh the Overleaf tab. This also fixes extension/native version mismatch and native protocol mismatch.
351
345
 
352
346
  ```bash
353
- npm exec --yes codex-overleaf-link@1.1.2 -- install-native
347
+ npm exec --yes codex-overleaf-link@1.2.0 -- install-native
354
348
  ```
355
349
 
356
350
  If npm is unavailable, use the GitHub Release script fallback for your platform.
@@ -358,14 +352,14 @@ If npm is unavailable, use the GitHub Release script fallback for your platform.
358
352
  macOS/Linux:
359
353
 
360
354
  ```bash
361
- CODEX_OVERLEAF_REF=v1.1.2 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.2/install.sh)"
355
+ CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)"
362
356
  ```
363
357
 
364
358
  Windows PowerShell:
365
359
 
366
360
  ```powershell
367
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.2/install.ps1 -OutFile install.ps1
368
- $env:CODEX_OVERLEAF_REF='v1.1.2'
361
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.ps1 -OutFile install.ps1
362
+ $env:CODEX_OVERLEAF_REF='v1.2.0'
369
363
  powershell -ExecutionPolicy Bypass -File install.ps1
370
364
  ```
371
365
 
@@ -424,8 +418,8 @@ Use this matrix for release-candidate signoff and compatibility reports. Record
424
418
  | Browser/channel/version | Google Chrome channel and version. | Google Chrome channel and version. | Google Chrome channel and version. | Chromium channel/package and version. |
425
419
  | Install mode | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout; native host installed with `--browser chromium`. |
426
420
  | Extension id | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. |
427
- | Installer/update command | `npm exec --yes codex-overleaf-link@1.1.2 -- install-native` | `npm exec --yes codex-overleaf-link@1.1.2 -- install-native` | `npm exec --yes codex-overleaf-link@1.1.2 -- install-native` | `npm exec --yes codex-overleaf-link@1.1.2 -- install-native --browser chromium` |
428
- | Uninstall command | `npm exec --yes codex-overleaf-link@1.1.2 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.1.2 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.1.2 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.1.2 -- uninstall-native --browser chromium` |
421
+ | Installer/update command | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native --browser chromium` |
422
+ | Uninstall command | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native --browser chromium` |
429
423
  | Manifest/registry path | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.codex.overleaf.json` | `HKCU\Software\Google\Chrome\NativeMessagingHosts\com.codex.overleaf` -> `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime\com.codex.overleaf.json` | `~/.config/google-chrome/NativeMessagingHosts/com.codex.overleaf.json` | `~/.config/chromium/NativeMessagingHosts/com.codex.overleaf.json` |
430
424
  | Bridge/runtime/source path | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `%LOCALAPPDATA%\CodexOverleaf\codex-overleaf-bridge.cmd`; runtime `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime`; source `%LOCALAPPDATA%\CodexOverleaf\source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. |
431
425
  | Node/Git/Codex/TeX | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. |
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '1.1.2';
15
+ const BUILD_TARGET_VERSION = '1.2.0';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -0,0 +1,404 @@
1
+ (function initLineReferences(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafLineReferences = factory();
6
+ }
7
+ })(typeof globalThis !== 'undefined' ? globalThis : window, function lineReferencesFactory() {
8
+ 'use strict';
9
+
10
+ const VALID_PARSE_MODES = new Set([
11
+ 'plain-text-token',
12
+ 'markdown-link-label',
13
+ 'markdown-link-target'
14
+ ]);
15
+ const TEXT_EXTENSION_PATTERN = '(?:tex|bib|sty|cls|bst|bbx|cbx|lbx|cfg|def|clo|ist|txt|md|latex)';
16
+ const REFERENCE_PREFIX_PATTERN = '(^|[\\s\\[({"\'])';
17
+ const PATH_PATTERN = `([^\\s\\[\\](){}<>"'\`,;]+?\\.${TEXT_EXTENSION_PATTERN})`;
18
+ const MARKDOWN_LINK_PATTERN = /\[([^\]]*)\]\(([^)]*)\)/g;
19
+ const BARE_LOCAL_PATH_PATTERN = /(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/]projects[\\/][^\s)\]]+)/gi;
20
+
21
+ function parseLineReferencesFromText({ text, mode }) {
22
+ return collectLineReferences(text, mode).map(toPublicReference);
23
+ }
24
+
25
+ function resolveProjectReference({ rawPath, projectFiles }) {
26
+ const normalizedRawPath = normalizeReferencePath(rawPath);
27
+ if (!normalizedRawPath || hasUnsafePathSegments(normalizedRawPath)) {
28
+ return null;
29
+ }
30
+
31
+ const textFiles = collectTextProjectFiles(projectFiles);
32
+ const exactMatch = textFiles.find(file => file.normalizedPath === normalizedRawPath);
33
+ if (exactMatch) {
34
+ return buildResolvedReference(exactMatch, 'exact');
35
+ }
36
+
37
+ const suffixMatches = textFiles.filter(file => normalizedRawPath.endsWith(`/${file.normalizedPath}`));
38
+ if (suffixMatches.length === 1) {
39
+ return buildResolvedReference(suffixMatches[0], 'suffix');
40
+ }
41
+
42
+ const rawBasename = getBasename(normalizedRawPath);
43
+ const basenameMatches = textFiles.filter(file => getBasename(file.normalizedPath) === rawBasename);
44
+ if (basenameMatches.length === 1) {
45
+ return buildResolvedReference(basenameMatches[0], 'basename');
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ function sanitizeLocalReferences(text, { projectFiles, context } = {}) {
52
+ if (typeof text !== 'string' || !text) {
53
+ return '';
54
+ }
55
+
56
+ let sanitized = text.replace(MARKDOWN_LINK_PATTERN, (rawMarkdown, label, target) => {
57
+ const sanitizedLabel = sanitizeLocalReferenceText(label, {
58
+ projectFiles,
59
+ placeholderBrackets: false
60
+ });
61
+ const trimmedTarget = String(target || '').trim();
62
+ if (/^https?:\/\//i.test(trimmedTarget)) {
63
+ return `[${sanitizedLabel}](${target})`;
64
+ }
65
+ if (containsLocalTarget(trimmedTarget)) {
66
+ return `[${sanitizedLabel}]`;
67
+ }
68
+ const sanitizedTarget = sanitizeLocalReferenceText(target, {
69
+ projectFiles,
70
+ placeholderBrackets: false
71
+ });
72
+ return `[${sanitizedLabel}](${sanitizedTarget})`;
73
+ });
74
+
75
+ sanitized = sanitizeLocalReferenceText(sanitized, {
76
+ projectFiles,
77
+ placeholderBrackets: true
78
+ });
79
+
80
+ if (context === 'render' || context === 'persist') {
81
+ return sanitized;
82
+ }
83
+ return sanitized;
84
+ }
85
+
86
+ function isLocalPathLike(value) {
87
+ if (typeof value !== 'string') {
88
+ return false;
89
+ }
90
+
91
+ const rawValue = value.trim();
92
+ if (!rawValue || /^https?:\/\//i.test(rawValue)) {
93
+ return false;
94
+ }
95
+
96
+ const normalizedPath = normalizeReferencePath(rawValue);
97
+ return /^file:\/\//i.test(rawValue)
98
+ || /^[A-Za-z]:[\\/]/.test(rawValue)
99
+ || rawValue.startsWith('/')
100
+ || rawValue.includes('.codex-overleaf/projects')
101
+ || rawValue.includes('.codex-overleaf\\projects')
102
+ || normalizedPath.includes('/.codex-overleaf/projects/')
103
+ || /^Users\/[^/]+\//.test(normalizedPath)
104
+ || /^home\/[^/]+\//.test(normalizedPath);
105
+ }
106
+
107
+ function normalizeReferencePath(value) {
108
+ if (typeof value !== 'string') {
109
+ return '';
110
+ }
111
+
112
+ let normalized = value.trim();
113
+ if (!normalized) {
114
+ return '';
115
+ }
116
+
117
+ if (/^file:\/\//i.test(normalized)) {
118
+ normalized = normalized.replace(/^file:\/\/\/?/i, '/');
119
+ }
120
+
121
+ try {
122
+ normalized = decodeURI(normalized);
123
+ } catch (_error) {
124
+ return '';
125
+ }
126
+
127
+ normalized = normalized
128
+ .replace(/\\/g, '/')
129
+ .replace(/\/+/g, '/')
130
+ .trim();
131
+
132
+ let previous;
133
+ do {
134
+ previous = normalized;
135
+ normalized = normalized
136
+ .replace(/^@+/, '')
137
+ .replace(/^\/+/, '')
138
+ .replace(/^\.\//, '');
139
+ } while (normalized !== previous);
140
+
141
+ return normalized;
142
+ }
143
+
144
+ function collectLineReferences(text, mode) {
145
+ if (!VALID_PARSE_MODES.has(mode) || typeof text !== 'string' || !text) {
146
+ return [];
147
+ }
148
+
149
+ const refs = [];
150
+ collectColonReferences(text, mode, refs);
151
+ collectLineWordReferences(text, mode, refs);
152
+ collectChineseLineReferences(text, mode, refs);
153
+
154
+ return refs
155
+ .filter(ref => !shouldSkipReference(ref))
156
+ .sort((left, right) => left.index - right.index || right.rawText.length - left.rawText.length)
157
+ .reduce((deduped, ref) => {
158
+ if (deduped.some(existing => rangesOverlap(existing, ref))) {
159
+ return deduped;
160
+ }
161
+ deduped.push(ref);
162
+ return deduped;
163
+ }, []);
164
+ }
165
+
166
+ function collectColonReferences(text, mode, refs) {
167
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}:(\\d+)(?::(\\d+))?(?![-:\\d])`, 'gi');
168
+ let match;
169
+ while ((match = pattern.exec(text)) !== null) {
170
+ const line = parsePositiveInteger(match[3]);
171
+ const column = match[4] ? parsePositiveInteger(match[4]) : null;
172
+ if (!line || (match[4] && !column)) {
173
+ continue;
174
+ }
175
+ const prefixLength = match[1].length;
176
+ const rawText = match[0].slice(prefixLength);
177
+ refs.push({
178
+ rawText,
179
+ displayText: rawText,
180
+ rawPath: match[2],
181
+ line,
182
+ column,
183
+ source: mode,
184
+ index: match.index + prefixLength
185
+ });
186
+ }
187
+ }
188
+
189
+ function collectLineWordReferences(text, mode, refs) {
190
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+line\\s+(\\d+)(?![-:\\d])`, 'gi');
191
+ let match;
192
+ while ((match = pattern.exec(text)) !== null) {
193
+ const line = parsePositiveInteger(match[3]);
194
+ if (!line) {
195
+ continue;
196
+ }
197
+ const prefixLength = match[1].length;
198
+ const rawText = match[0].slice(prefixLength);
199
+ refs.push({
200
+ rawText,
201
+ displayText: `${match[2]}:${line}`,
202
+ rawPath: match[2],
203
+ line,
204
+ column: null,
205
+ source: mode,
206
+ index: match.index + prefixLength
207
+ });
208
+ }
209
+ }
210
+
211
+ function collectChineseLineReferences(text, mode, refs) {
212
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+第\\s*(\\d+)\\s*行`, 'gi');
213
+ let match;
214
+ while ((match = pattern.exec(text)) !== null) {
215
+ const line = parsePositiveInteger(match[3]);
216
+ if (!line) {
217
+ continue;
218
+ }
219
+ const prefixLength = match[1].length;
220
+ const rawText = match[0].slice(prefixLength);
221
+ refs.push({
222
+ rawText,
223
+ displayText: `${match[2]}:${line}`,
224
+ rawPath: match[2],
225
+ line,
226
+ column: null,
227
+ source: mode,
228
+ index: match.index + prefixLength
229
+ });
230
+ }
231
+ }
232
+
233
+ function sanitizeLocalReferenceText(text, { projectFiles, placeholderBrackets }) {
234
+ let sanitized = replaceLineReferences(String(text || ''), {
235
+ projectFiles,
236
+ placeholderBrackets
237
+ });
238
+ sanitized = replaceBareLocalPaths(sanitized, {
239
+ projectFiles,
240
+ placeholderBrackets
241
+ });
242
+ return sanitized;
243
+ }
244
+
245
+ function replaceLineReferences(text, { projectFiles, placeholderBrackets }) {
246
+ const refs = collectLineReferences(text, 'plain-text-token')
247
+ .filter(ref => isLocalPathLike(ref.rawPath))
248
+ .sort((left, right) => right.index - left.index);
249
+ let sanitized = text;
250
+ for (const ref of refs) {
251
+ const replacement = formatReferenceReplacement(ref, {
252
+ projectFiles,
253
+ placeholderBrackets
254
+ });
255
+ sanitized = `${sanitized.slice(0, ref.index)}${replacement}${sanitized.slice(ref.index + ref.rawText.length)}`;
256
+ }
257
+ return sanitized;
258
+ }
259
+
260
+ function replaceBareLocalPaths(text, { projectFiles, placeholderBrackets }) {
261
+ return text.replace(BARE_LOCAL_PATH_PATTERN, (rawPath, offset, fullText) => {
262
+ if (/^[A-Za-z]:[\\/]/.test(rawPath) && offset > 0 && /[A-Za-z]/.test(fullText[offset - 1])) {
263
+ return rawPath;
264
+ }
265
+ const { value, trailing } = splitTrailingPunctuation(rawPath);
266
+ const resolved = resolveProjectReference({ rawPath: value, projectFiles });
267
+ if (resolved) {
268
+ return `${resolved.path}${trailing}`;
269
+ }
270
+ return `${formatLocalPathPlaceholder(null, placeholderBrackets)}${trailing}`;
271
+ });
272
+ }
273
+
274
+ function formatReferenceReplacement(ref, { projectFiles, placeholderBrackets }) {
275
+ const resolved = resolveProjectReference({
276
+ rawPath: ref.rawPath,
277
+ projectFiles
278
+ });
279
+ if (resolved) {
280
+ return `${resolved.path}:${ref.line}${ref.column ? `:${ref.column}` : ''}`;
281
+ }
282
+ return formatLocalPathPlaceholder(ref.line, placeholderBrackets);
283
+ }
284
+
285
+ function formatLocalPathPlaceholder(line, includeBrackets) {
286
+ const value = line ? `local path:${line}` : 'local path';
287
+ return includeBrackets ? `[${value}]` : value;
288
+ }
289
+
290
+ function containsLocalTarget(target) {
291
+ if (!target) {
292
+ return false;
293
+ }
294
+ if (isLocalPathLike(target)) {
295
+ return true;
296
+ }
297
+ return collectLineReferences(target, 'markdown-link-target')
298
+ .some(ref => isLocalPathLike(ref.rawPath));
299
+ }
300
+
301
+ function collectTextProjectFiles(projectFiles) {
302
+ if (!Array.isArray(projectFiles)) {
303
+ return [];
304
+ }
305
+
306
+ const result = [];
307
+ for (const file of projectFiles) {
308
+ if (!file || file.kind !== 'text' || typeof file.path !== 'string') {
309
+ continue;
310
+ }
311
+
312
+ const normalizedPath = normalizeReferencePath(file.path);
313
+ if (!normalizedPath || hasUnsafePathSegments(normalizedPath)) {
314
+ continue;
315
+ }
316
+
317
+ result.push({
318
+ entry: file,
319
+ path: normalizedPath,
320
+ normalizedPath
321
+ });
322
+ }
323
+ return result;
324
+ }
325
+
326
+ function buildResolvedReference(file, resolution) {
327
+ return {
328
+ path: file.path,
329
+ file: file.entry,
330
+ normalizedPath: file.normalizedPath,
331
+ resolution
332
+ };
333
+ }
334
+
335
+ function shouldSkipReference(ref) {
336
+ const rawPath = String(ref.rawPath || '');
337
+ return /^https?:\/\//i.test(rawPath)
338
+ || isEmailLikePath(rawPath)
339
+ || hasUnsafeLineRange(ref.rawText);
340
+ }
341
+
342
+ function isEmailLikePath(rawPath) {
343
+ return /^[^@\s/\\]+@[^@\s/\\]+$/.test(rawPath);
344
+ }
345
+
346
+ function hasUnsafeLineRange(rawText) {
347
+ return /:\d+-\d+(?=$|[^\d])/.test(rawText);
348
+ }
349
+
350
+ function hasUnsafePathSegments(path) {
351
+ return normalizeReferencePath(path)
352
+ .split('/')
353
+ .some(segment => segment === '.' || segment === '..');
354
+ }
355
+
356
+ function rangesOverlap(left, right) {
357
+ const leftEnd = left.index + left.rawText.length;
358
+ const rightEnd = right.index + right.rawText.length;
359
+ return left.index < rightEnd && right.index < leftEnd;
360
+ }
361
+
362
+ function parsePositiveInteger(value) {
363
+ const parsed = Number(value);
364
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
365
+ return null;
366
+ }
367
+ return parsed;
368
+ }
369
+
370
+ function getBasename(path) {
371
+ const parts = String(path || '').split('/');
372
+ return parts[parts.length - 1] || '';
373
+ }
374
+
375
+ function splitTrailingPunctuation(value) {
376
+ const match = String(value || '').match(/^(.*?)([.,;!?]+)$/);
377
+ if (!match) {
378
+ return { value, trailing: '' };
379
+ }
380
+ return {
381
+ value: match[1],
382
+ trailing: match[2]
383
+ };
384
+ }
385
+
386
+ function toPublicReference(ref) {
387
+ return {
388
+ rawText: ref.rawText,
389
+ displayText: ref.displayText,
390
+ rawPath: ref.rawPath,
391
+ line: ref.line,
392
+ column: ref.column,
393
+ source: ref.source
394
+ };
395
+ }
396
+
397
+ return {
398
+ isLocalPathLike,
399
+ normalizeReferencePath,
400
+ parseLineReferencesFromText,
401
+ resolveProjectReference,
402
+ sanitizeLocalReferences
403
+ };
404
+ });
@@ -81,6 +81,7 @@
81
81
  /\b(?:sk|pk)-[A-Za-z0-9][A-Za-z0-9_-]{7,}\b/g,
82
82
  /\b(?:api[_-]?key|token|password|passwd|secret)\b\s*[:=]\s*["']?[^"'\s,;]+["']?/gi
83
83
  ];
84
+ const LineReferences = loadLineReferences();
84
85
 
85
86
  function normalizePanelState(input = {}, options = {}) {
86
87
  const state = {
@@ -143,7 +144,7 @@
143
144
  }
144
145
 
145
146
  function normalizeSession(session, fallbackState = DEFAULT_PANEL_STATE, options = {}) {
146
- const history = Array.isArray(session.history) ? session.history.slice(-10) : [];
147
+ const history = normalizeHistoryEntries(session.history);
147
148
  const normalizedRuns = normalizeRuns(session.runs, options);
148
149
  const runs = normalizedRuns.length ? normalizedRuns : recoverRunsFromHistory(session, history);
149
150
  const rawTitle = typeof session.title === 'string' ? session.title.trim() : '';
@@ -159,7 +160,7 @@
159
160
  updatedAt: typeof session.updatedAt === 'string' ? session.updatedAt : new Date().toISOString(),
160
161
  history,
161
162
  runs,
162
- task: typeof session.task === 'string' ? session.task : '',
163
+ task: sanitizeAssistantVisibleText(session.task),
163
164
  mode: VALID_MODES.has(session.mode) ? session.mode : fallbackState.mode,
164
165
  model: typeof session.model === 'string' && session.model ? session.model : fallbackState.model,
165
166
  reasoningEffort: VALID_REASONING.has(session.reasoningEffort)
@@ -267,9 +268,9 @@
267
268
  titleSource,
268
269
  createdAt: overrides.createdAt || new Date().toISOString(),
269
270
  updatedAt: overrides.updatedAt || new Date().toISOString(),
270
- history: Array.isArray(overrides.history) ? overrides.history.slice(-10) : [],
271
+ history: normalizeHistoryEntries(overrides.history),
271
272
  runs,
272
- task: typeof overrides.task === 'string' ? overrides.task : '',
273
+ task: sanitizeAssistantVisibleText(overrides.task),
273
274
  mode: VALID_MODES.has(overrides.mode) ? overrides.mode : DEFAULT_PANEL_STATE.mode,
274
275
  model: typeof overrides.model === 'string' && overrides.model ? overrides.model : DEFAULT_PANEL_STATE.model,
275
276
  reasoningEffort: VALID_REASONING.has(overrides.reasoningEffort)
@@ -286,6 +287,8 @@
286
287
 
287
288
  function recordSessionResult(session, entry) {
288
289
  const base = session?.id ? session : createSession();
290
+ const task = sanitizeAssistantVisibleText(entry?.task) || 'untitled task';
291
+ const result = sanitizeAssistantVisibleText(entry?.result || entry?.status) || 'completed';
289
292
  return {
290
293
  ...base,
291
294
  id: base.id,
@@ -293,12 +296,12 @@
293
296
  history: [
294
297
  ...(Array.isArray(base.history) ? base.history : []),
295
298
  {
296
- task: entry.task || 'untitled task',
297
- result: entry.result || entry.status || 'completed',
298
- at: entry.at || new Date().toISOString()
299
+ task,
300
+ result,
301
+ at: entry?.at || new Date().toISOString()
299
302
  }
300
303
  ].slice(-10),
301
- updatedAt: entry.at || new Date().toISOString()
304
+ updatedAt: entry?.at || new Date().toISOString()
302
305
  };
303
306
  }
304
307
 
@@ -419,7 +422,7 @@
419
422
  }
420
423
 
421
424
  function sanitizeAutoTitle(value) {
422
- const title = String(value || '')
425
+ const title = sanitizeAssistantVisibleText(value)
423
426
  .replace(/@file:[^\s]+/g, ' ')
424
427
  .replace(/@(context|compile-log)\b/g, ' ')
425
428
  .replace(/\s+/g, ' ')
@@ -506,13 +509,13 @@
506
509
 
507
510
  return {
508
511
  id: run.id,
509
- task: typeof run.task === 'string' && run.task ? run.task : 'untitled task',
512
+ task: sanitizeAssistantVisibleText(run.task) || 'untitled task',
510
513
  mode: typeof run.mode === 'string' ? run.mode : '',
511
514
  model: typeof run.model === 'string' ? run.model : '',
512
515
  reasoningEffort: typeof run.reasoningEffort === 'string' ? run.reasoningEffort : '',
513
516
  speedTier: typeof run.speedTier === 'string' ? run.speedTier : '',
514
517
  status: shouldStopRestoredRun ? 'failed' : normalizeRunStatus(run.status),
515
- statusText: shouldStopRestoredRun ? '页面刷新后已停止跟踪' : typeof run.statusText === 'string' ? run.statusText : '',
518
+ statusText: shouldStopRestoredRun ? '页面刷新后已停止跟踪' : sanitizeAssistantVisibleText(run.statusText),
516
519
  startedAt: typeof run.startedAt === 'string' ? run.startedAt : '',
517
520
  finishedAt: shouldStopRestoredRun ? new Date().toISOString() : typeof run.finishedAt === 'string' ? run.finishedAt : '',
518
521
  events: events.slice(-MAX_RUN_EVENTS),
@@ -522,7 +525,7 @@
522
525
  undoBaseFiles: normalizeRunFiles(run.undoBaseFiles),
523
526
  undoTrackedChanges: normalizeRunTrackedChanges(run.undoTrackedChanges),
524
527
  undoExpectedFiles: normalizeRunFiles(run.undoExpectedFiles),
525
- undoStatus: typeof run.undoStatus === 'string' ? redactSecretLikeText(run.undoStatus) : ''
528
+ undoStatus: sanitizeAssistantVisibleText(run.undoStatus)
526
529
  };
527
530
  }
528
531
 
@@ -538,14 +541,14 @@
538
541
  return (Array.isArray(events) ? events : [])
539
542
  .filter(event => event && typeof event.title === 'string')
540
543
  .map(event => ({
541
- title: event.title,
544
+ title: sanitizeAssistantVisibleText(event.title) || 'Event',
542
545
  status: typeof event.status === 'string' ? event.status : 'info',
543
- detail: event.detail,
546
+ detail: sanitizeAssistantVisibleValue(event.detail),
544
547
  timestamp: typeof event.timestamp === 'string' ? event.timestamp : '',
545
548
  kind: typeof event.kind === 'string' ? event.kind : 'activity',
546
- technicalDetail: event.technicalDetail,
547
- streamKey: typeof event.streamKey === 'string' ? event.streamKey : '',
548
- streamRole: typeof event.streamRole === 'string' ? event.streamRole : ''
549
+ technicalDetail: sanitizeAssistantVisibleValue(event.technicalDetail),
550
+ streamKey: sanitizeAssistantVisibleText(event.streamKey),
551
+ streamRole: sanitizeAssistantVisibleText(event.streamRole)
549
552
  }))
550
553
  .slice(-MAX_RUN_EVENTS);
551
554
  }
@@ -624,8 +627,8 @@
624
627
  normalized.push({
625
628
  key,
626
629
  id: typeof change.id === 'string' ? change.id : '',
627
- path: typeof change.path === 'string' ? change.path : '',
628
- label: typeof change.label === 'string' ? change.label : ''
630
+ path: sanitizeAssistantVisibleText(change.path),
631
+ label: sanitizeAssistantVisibleText(change.label)
629
632
  });
630
633
  }
631
634
  return normalized;
@@ -638,7 +641,7 @@
638
641
  if (typeof item !== 'string') {
639
642
  continue;
640
643
  }
641
- const path = redactSecretLikeText(item.trim());
644
+ const path = sanitizeAssistantVisibleText(item.trim());
642
645
  if (!path || seen.has(path)) {
643
646
  continue;
644
647
  }
@@ -914,7 +917,7 @@
914
917
  if (typeof value !== 'string' || !value.trim()) {
915
918
  return '';
916
919
  }
917
- const text = redactSecretLikeText(value);
920
+ const text = sanitizeAssistantVisibleText(value);
918
921
  if (isOmittedStorageSummary(text)) {
919
922
  return text;
920
923
  }
@@ -938,7 +941,7 @@
938
941
  for (const key of Object.keys(value)) {
939
942
  const item = value[key];
940
943
  if (/(^|[A-Z_])path$/i.test(key) && typeof item === 'string') {
941
- const path = redactSecretLikeText(item).replace(/\\/g, '/').replace(/^\/+/, '').trim();
944
+ const path = sanitizeAssistantVisibleText(item).replace(/\\/g, '/').replace(/^\/+/, '').trim();
942
945
  if (path) {
943
946
  state.count += 1;
944
947
  if (!state.seen.has(path) && state.items.length < 5) {
@@ -991,13 +994,107 @@
991
994
  }
992
995
 
993
996
  function normalizeTextField(value, maxChars) {
994
- const text = typeof value === 'string' ? redactSecretLikeText(value) : '';
997
+ const text = sanitizeAssistantVisibleText(value);
995
998
  if (!Number.isFinite(maxChars) || maxChars <= 0 || text.length <= maxChars) {
996
999
  return text;
997
1000
  }
998
1001
  return `${text.slice(0, Math.max(0, maxChars - 1))}…`;
999
1002
  }
1000
1003
 
1004
+ function normalizeHistoryEntries(history) {
1005
+ return (Array.isArray(history) ? history : [])
1006
+ .slice(-10)
1007
+ .map(entry => ({
1008
+ task: sanitizeAssistantVisibleText(entry?.task) || 'untitled task',
1009
+ result: sanitizeAssistantVisibleText(entry?.result || entry?.status) || 'completed',
1010
+ at: typeof entry?.at === 'string' ? entry.at : ''
1011
+ }));
1012
+ }
1013
+
1014
+ function sanitizeAssistantVisibleValue(value, depth = 0) {
1015
+ if (typeof value === 'string') {
1016
+ return sanitizeAssistantVisibleText(value);
1017
+ }
1018
+ if (value === undefined || value === null || typeof value === 'number' || typeof value === 'boolean') {
1019
+ return value;
1020
+ }
1021
+ if (depth > 12) {
1022
+ return '[redacted nested value]';
1023
+ }
1024
+ if (Array.isArray(value)) {
1025
+ return value.map(item => sanitizeAssistantVisibleValue(item, depth + 1));
1026
+ }
1027
+ if (typeof value === 'object') {
1028
+ const result = {};
1029
+ for (const key of Object.keys(value)) {
1030
+ const safeKey = sanitizeAssistantVisibleText(key) || key;
1031
+ result[safeKey] = sanitizeAssistantVisibleValue(value[key], depth + 1);
1032
+ }
1033
+ return result;
1034
+ }
1035
+ return value;
1036
+ }
1037
+
1038
+ function sanitizeAssistantVisibleText(value) {
1039
+ if (typeof value !== 'string' || !value) {
1040
+ return '';
1041
+ }
1042
+ const secretRedacted = redactSecretLikeText(value);
1043
+ if (!mightContainLocalReferenceText(secretRedacted)) {
1044
+ return secretRedacted;
1045
+ }
1046
+ if (LineReferences?.sanitizeLocalReferences) {
1047
+ try {
1048
+ return LineReferences.sanitizeLocalReferences(secretRedacted, {
1049
+ projectFiles: [],
1050
+ context: 'persist'
1051
+ });
1052
+ } catch (_error) {
1053
+ return fallbackSanitizeLocalReferences(secretRedacted);
1054
+ }
1055
+ }
1056
+ return fallbackSanitizeLocalReferences(secretRedacted);
1057
+ }
1058
+
1059
+ function mightContainLocalReferenceText(value) {
1060
+ return /(?:file:\/\/\/?|[A-Za-z]:[\\/]|\/(?:Users|home|private|var|tmp)\/|[\\/]\.codex-overleaf[\\/]projects[\\/]|\.codex-overleaf[\\/]projects[\\/])/i.test(String(value || ''));
1061
+ }
1062
+
1063
+ function loadLineReferences() {
1064
+ if (typeof globalThis !== 'undefined' && globalThis.CodexOverleafLineReferences) {
1065
+ return globalThis.CodexOverleafLineReferences;
1066
+ }
1067
+ if (typeof require === 'function') {
1068
+ try {
1069
+ return require('./lineReferences');
1070
+ } catch (_error) {
1071
+ return null;
1072
+ }
1073
+ }
1074
+ return null;
1075
+ }
1076
+
1077
+ function fallbackSanitizeLocalReferences(value) {
1078
+ return String(value || '')
1079
+ .replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_raw, label, target) => {
1080
+ const safeLabel = fallbackSanitizeBareLocalPaths(label);
1081
+ return /^https?:\/\//i.test(String(target || '').trim())
1082
+ ? `[${safeLabel}](${target})`
1083
+ : `[${safeLabel}]`;
1084
+ })
1085
+ .replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/][^\s)\]]+)/gi, rawPath => {
1086
+ const line = String(rawPath || '').match(/:(\d+)(?::\d+)?(?:[.,;!?])?$/)?.[1];
1087
+ return line ? `[local path:${line}]` : '[local path]';
1088
+ });
1089
+ }
1090
+
1091
+ function fallbackSanitizeBareLocalPaths(value) {
1092
+ return String(value || '').replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/][^\s)\]]+)/gi, rawPath => {
1093
+ const line = String(rawPath || '').match(/:(\d+)(?::\d+)?(?:[.,;!?])?$/)?.[1];
1094
+ return line ? `[local path:${line}]` : '[local path]';
1095
+ });
1096
+ }
1097
+
1001
1098
  function redactSecretLikeText(value) {
1002
1099
  if (typeof value !== 'string') {
1003
1100
  return '';
@@ -812,13 +812,71 @@
812
812
  }
813
813
 
814
814
  function normalizeTextField(value, maxChars) {
815
- var text = typeof value === 'string' ? redactSecretLikeText(value) : '';
815
+ var text = sanitizeAssistantVisibleText(value);
816
816
  if (!Number.isFinite(maxChars) || maxChars <= 0 || text.length <= maxChars) {
817
817
  return text;
818
818
  }
819
819
  return text.slice(0, Math.max(0, maxChars - 1)) + '…';
820
820
  }
821
821
 
822
+ function sanitizeAssistantVisibleText(value) {
823
+ if (typeof value !== 'string' || !value) {
824
+ return '';
825
+ }
826
+ var text = redactSecretLikeText(value);
827
+ if (!mightContainLocalReferenceText(text)) {
828
+ return text;
829
+ }
830
+ return sanitizeLocalReferencesForStorage(text);
831
+ }
832
+
833
+ function mightContainLocalReferenceText(value) {
834
+ return /(?:file:\/\/\/?|[A-Za-z]:[\\/]|\/(?:Users|home|private|var|tmp)\/|[\\/]\.codex-overleaf[\\/]projects[\\/]|\.codex-overleaf[\\/]projects[\\/])/i.test(String(value || ''));
835
+ }
836
+
837
+ function sanitizeLocalReferencesForStorage(value) {
838
+ return String(value || '')
839
+ .replace(/\[([^\]]*)\]\(([^)]*)\)/g, function (_raw, label, target) {
840
+ var safeLabel = sanitizeBareLocalPaths(label, false);
841
+ var trimmedTarget = String(target || '').trim();
842
+ if (/^https?:\/\//i.test(trimmedTarget)) {
843
+ return '[' + safeLabel + '](' + sanitizeHttpTarget(trimmedTarget) + ')';
844
+ }
845
+ if (mightContainLocalReferenceText(trimmedTarget)) {
846
+ return '[' + safeLabel + ']';
847
+ }
848
+ return '[' + safeLabel + '](' + sanitizeBareLocalPaths(trimmedTarget, false) + ')';
849
+ })
850
+ .replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/]projects[\\/][^\s)\]]+)/gi, function (rawPath) {
851
+ return formatLocalPathPlaceholder(rawPath, true);
852
+ });
853
+ }
854
+
855
+ function sanitizeBareLocalPaths(value, includeBrackets) {
856
+ return String(value || '').replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/]projects[\\/][^\s)\]]+)/gi, function (rawPath) {
857
+ return formatLocalPathPlaceholder(rawPath, includeBrackets);
858
+ });
859
+ }
860
+
861
+ function sanitizeHttpTarget(value) {
862
+ try {
863
+ var parsed = new URL(value);
864
+ if (mightContainLocalReferenceText(parsed.href)) {
865
+ parsed.search = '';
866
+ parsed.hash = '';
867
+ }
868
+ return parsed.href;
869
+ } catch (_error) {
870
+ return '';
871
+ }
872
+ }
873
+
874
+ function formatLocalPathPlaceholder(rawPath, includeBrackets) {
875
+ var lineMatch = String(rawPath || '').match(/:(\d+)(?::\d+)?(?:[.,;!?])?$/);
876
+ var value = lineMatch ? 'local path:' + lineMatch[1] : 'local path';
877
+ return includeBrackets ? '[' + value + ']' : value;
878
+ }
879
+
822
880
  function nonNegativeInteger(value) {
823
881
  var number = Number(value);
824
882
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
@@ -4,6 +4,7 @@ function buildCodexPrompt(request) {
4
4
  const project = request.project || {};
5
5
  const files = Array.isArray(project.files) ? project.files : [];
6
6
  const focusFiles = normalizeFocusFiles(request.focusFiles || request.session?.focusFiles);
7
+ const activePath = normalizeProjectPath(project.activePath);
7
8
  const orderedFiles = orderFilesByFocus(files, focusFiles);
8
9
  const inventory = orderedFiles.map(file => `- ${file.path} (${String(file.content || '').length} chars)`).join('\n') || '- no files supplied';
9
10
 
@@ -15,7 +16,7 @@ function buildCodexPrompt(request) {
15
16
  `Reasoning effort: ${request.reasoningEffort || 'default'}`,
16
17
  `Session: ${request.session?.id || 'none'}`,
17
18
  `Task: ${request.task}`,
18
- `Active file: ${project.activePath || 'unknown'}`,
19
+ `Active file: ${activePath || 'unknown'}`,
19
20
  '',
20
21
  'Recent session history:',
21
22
  formatSessionHistory(request.session?.history),
@@ -23,6 +24,12 @@ function buildCodexPrompt(request) {
23
24
  'Focus files:',
24
25
  formatFocusFiles(focusFiles, files),
25
26
  '',
27
+ 'Project location citation rules:',
28
+ formatProjectLocationCitationRules(),
29
+ '',
30
+ 'Focused project file inventory:',
31
+ formatFocusedProjectFileInventory([activePath, ...focusFiles]),
32
+ '',
26
33
  'Files:',
27
34
  inventory,
28
35
  '',
@@ -62,10 +69,7 @@ function normalizeFocusFiles(value) {
62
69
  const seen = new Set();
63
70
  const files = [];
64
71
  for (const item of Array.isArray(value) ? value : []) {
65
- if (typeof item !== 'string') {
66
- continue;
67
- }
68
- const path = item.trim();
72
+ const path = normalizeProjectPath(item);
69
73
  if (!path || seen.has(path)) {
70
74
  continue;
71
75
  }
@@ -75,6 +79,42 @@ function normalizeFocusFiles(value) {
75
79
  return files.slice(0, 5);
76
80
  }
77
81
 
82
+ function normalizeProjectPath(value) {
83
+ const path = String(value || '')
84
+ .replace(/^@file:/i, '')
85
+ .replace(/\\/g, '/')
86
+ .trim();
87
+ if (!path || /^file:\/\//i.test(path) || /^[A-Za-z]:\//.test(path)) {
88
+ return '';
89
+ }
90
+ const projectPath = path.replace(/^\/+/, '');
91
+ const wasAbsolutePath = path.startsWith('/');
92
+ if (wasAbsolutePath && /^(Users|home|tmp|var|private|Volumes)\//i.test(projectPath)) {
93
+ return '';
94
+ }
95
+ if (/(^|\/)\.codex-overleaf\/projects(\/|$)/.test(projectPath)) {
96
+ return '';
97
+ }
98
+ return projectPath;
99
+ }
100
+
101
+ function formatProjectLocationCitationRules() {
102
+ return [
103
+ '- In any user-visible output, cite project locations using only Overleaf project-relative paths from the project file inventory below.',
104
+ '- Use path:LINE[:COLUMN] (for example, path/to/file.tex:12:3).',
105
+ '- Do not cite local absolute paths, temporary workspace paths, file:// URLs, or markdown links to local files.',
106
+ '- If a line number is uncertain, cite only the project-relative file path.'
107
+ ].join('\n');
108
+ }
109
+
110
+ function formatFocusedProjectFileInventory(files) {
111
+ const focusFiles = normalizeFocusFiles(files);
112
+ if (!focusFiles.length) {
113
+ return '- none selected.';
114
+ }
115
+ return focusFiles.map(filePath => `- ${filePath}`).join('\n');
116
+ }
117
+
78
118
  function formatFocusFiles(focusFiles, files) {
79
119
  if (!focusFiles.length) {
80
120
  return '- none (default to whole project)';
@@ -30,10 +30,15 @@ function buildCodexTurnPrompt(options = {}) {
30
30
  '',
31
31
  'Current Overleaf workspace:',
32
32
  `- Project: ${context.projectKey || context.projectId || 'unknown'}`,
33
- `- Local workspace: ${context.workspacePath || 'current cwd'}`,
34
33
  '- The local workspace was synced from Overleaf immediately before this turn.',
35
34
  '- If the recent session history conflicts with the files in the workspace, trust the files.',
36
35
  '',
36
+ 'Project location citation rules:',
37
+ formatProjectLocationCitationRules(),
38
+ '',
39
+ 'Focused project file inventory:',
40
+ formatFocusedProjectFileInventory(context.focusFiles),
41
+ '',
37
42
  'Focus files:',
38
43
  formatFocusFiles(context.focusFiles),
39
44
  '',
@@ -222,11 +227,40 @@ function normalizeFocusFiles(value) {
222
227
  }
223
228
 
224
229
  function normalizeProjectPath(value) {
225
- return String(value || '')
230
+ const path = String(value || '')
226
231
  .replace(/^@file:/i, '')
227
232
  .replace(/\\/g, '/')
228
- .trim()
229
- .replace(/^\/+/, '');
233
+ .trim();
234
+ if (!path || /^file:\/\//i.test(path) || /^[A-Za-z]:\//.test(path)) {
235
+ return '';
236
+ }
237
+ const projectPath = path.replace(/^\/+/, '');
238
+ const wasAbsolutePath = path.startsWith('/');
239
+ if (wasAbsolutePath && /^(Users|home|tmp|var|private|Volumes)\//i.test(projectPath)) {
240
+ return '';
241
+ }
242
+ if (/(^|\/)\.codex-overleaf\/projects(\/|$)/.test(projectPath)) {
243
+ return '';
244
+ }
245
+ return projectPath;
246
+ }
247
+
248
+ function formatProjectLocationCitationRules() {
249
+ return [
250
+ '- In any user-visible output, cite project locations using only Overleaf project-relative paths from the project file inventory below.',
251
+ '- Use path:LINE[:COLUMN] (for example, path/to/file.tex:12:3).',
252
+ '- Do not wrap project location citations in backticks or inline code; write main.tex:28 as normal text so the browser can turn it into a jump button.',
253
+ '- Do not cite local absolute paths, temporary workspace paths, file:// URLs, or markdown links to local files.',
254
+ '- If a line number is uncertain, cite only the project-relative file path.'
255
+ ].join('\n');
256
+ }
257
+
258
+ function formatFocusedProjectFileInventory(files) {
259
+ const focusFiles = normalizeFocusFiles(files);
260
+ if (!focusFiles.length) {
261
+ return '- none selected.';
262
+ }
263
+ return focusFiles.map(filePath => `- ${filePath}`).join('\n');
230
264
  }
231
265
 
232
266
  function formatFocusFiles(files) {
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "codex-overleaf-link",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Ghqqqq/codex-overleaf-link.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Ghqqqq/codex-overleaf-link/issues"
13
+ },
14
+ "homepage": "https://github.com/Ghqqqq/codex-overleaf-link#readme",
7
15
  "scripts": {
8
16
  "agent:codex": "node scripts/codex-json-agent.mjs",
9
17
  "test": "node scripts/run-tests.mjs",