codex-overleaf-link 1.1.3 → 1.2.6

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.3-blue" alt="version">
6
+ <img src="https://img.shields.io/badge/version-1.2.6-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">
@@ -34,10 +34,10 @@ The recommended release path is npm-first for the native host, plus Chrome's req
34
34
  1. Install or update the native host with the pinned npm package:
35
35
 
36
36
  ```bash
37
- npm exec --yes codex-overleaf-link@1.1.3 -- install-native
37
+ npm exec --yes codex-overleaf-link@1.2.6 -- install-native
38
38
  ```
39
39
 
40
- 2. Download `codex-overleaf-link-extension-v1.1.3.zip` from the [v1.1.3 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.1.3), then unzip it to a stable local folder.
40
+ 2. Download `codex-overleaf-link-extension-v1.2.6.zip` from the [v1.2.6 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.2.6), then unzip it to a stable local folder.
41
41
 
42
42
  3. Open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the unzipped extension folder.
43
43
 
@@ -48,14 +48,14 @@ If you modify the extension key or load a custom build that gets a different id,
48
48
  Source installer fallback for macOS / Linux, mainly for development or source checkout installs:
49
49
 
50
50
  ```bash
51
- CODEX_OVERLEAF_REF=v1.1.3 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.3/install.sh)"
51
+ CODEX_OVERLEAF_REF=v1.2.6 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.6/install.sh)"
52
52
  ```
53
53
 
54
54
  Source installer fallback for Windows from PowerShell:
55
55
 
56
56
  ```powershell
57
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.3/install.ps1 -OutFile install.ps1
58
- $env:CODEX_OVERLEAF_REF='v1.1.3'
57
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.6/install.ps1 -OutFile install.ps1
58
+ $env:CODEX_OVERLEAF_REF='v1.2.6'
59
59
  powershell -ExecutionPolicy Bypass -File install.ps1
60
60
  ```
61
61
 
@@ -66,19 +66,19 @@ npm installs, updates, uninstalls, and diagnoses the native host only. npm does
66
66
  Install or update the native host for the official release extension id:
67
67
 
68
68
  ```bash
69
- npm exec --yes codex-overleaf-link@1.1.3 -- install-native
69
+ npm exec --yes codex-overleaf-link@1.2.6 -- install-native
70
70
  ```
71
71
 
72
72
  Diagnose the registered native host:
73
73
 
74
74
  ```bash
75
- npm exec --yes codex-overleaf-link@1.1.3 -- doctor
75
+ npm exec --yes codex-overleaf-link@1.2.6 -- doctor
76
76
  ```
77
77
 
78
78
  Uninstall the native host:
79
79
 
80
80
  ```bash
81
- npm exec --yes codex-overleaf-link@1.1.3 -- uninstall-native
81
+ npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native
82
82
  ```
83
83
 
84
84
  Use `--extension-id <chrome-extension-id>` only for a custom/dev unpacked extension id that differs from the official bundled id.
@@ -116,10 +116,10 @@ If Chrome assigns a different extension id, rerun `npm run install:native -- --e
116
116
  <details>
117
117
  <summary><strong>Update</strong></summary>
118
118
 
119
- For a deterministic v1.1.3 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.6 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**.
120
120
 
121
121
  ```bash
122
- npm exec --yes codex-overleaf-link@1.1.3 -- install-native
122
+ npm exec --yes codex-overleaf-link@1.2.6 -- install-native
123
123
  ```
124
124
 
125
125
  If npm is unavailable, use the GitHub Release script fallback for your platform.
@@ -127,14 +127,14 @@ If npm is unavailable, use the GitHub Release script fallback for your platform.
127
127
  macOS / Linux:
128
128
 
129
129
  ```bash
130
- CODEX_OVERLEAF_REF=v1.1.3 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.3/install.sh)"
130
+ CODEX_OVERLEAF_REF=v1.2.6 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.6/install.sh)"
131
131
  ```
132
132
 
133
133
  Windows PowerShell:
134
134
 
135
135
  ```powershell
136
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.3/install.ps1 -OutFile install.ps1
137
- $env:CODEX_OVERLEAF_REF='v1.1.3'
136
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.6/install.ps1 -OutFile install.ps1
137
+ $env:CODEX_OVERLEAF_REF='v1.2.6'
138
138
  powershell -ExecutionPolicy Bypass -File install.ps1
139
139
  ```
140
140
 
@@ -144,13 +144,13 @@ Then reload the extension in `chrome://extensions` and refresh the Overleaf page
144
144
 
145
145
  ## GitHub Release Artifacts
146
146
 
147
- The v1.1.3 GitHub Release contains:
147
+ The v1.2.6 GitHub Release contains:
148
148
 
149
- - `codex-overleaf-link-extension-v1.1.3.zip`: loadable Chrome extension package for manual unpacked installation.
150
- - `codex-overleaf-native-host-v1.1.3.tar.gz`: native host runtime files used by the installer and release verification.
151
- - `codex-overleaf-link-1.1.3.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.1.3` when run directly from the release artifact.
153
- - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.1.3` when run directly from the release artifact.
149
+ - `codex-overleaf-link-extension-v1.2.6.zip`: loadable Chrome extension package for manual unpacked installation.
150
+ - `codex-overleaf-native-host-v1.2.6.tar.gz`: native host runtime files used by the installer and release verification.
151
+ - `codex-overleaf-link-1.2.6.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.6` when run directly from the release artifact.
153
+ - `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.2.6` when run directly from the release artifact.
154
154
  - `uninstall-native-host.mjs`: native host uninstaller that removes the Chrome Native Messaging manifest, bridge executable, and runtime copy.
155
155
  - `nativeHostPlatform.js`, `manifest.js`, `runtimeInstaller.js`: helper files required by the loose uninstaller asset.
156
156
  - `SHA256SUMS` and `release-manifest.json`: checksum and artifact metadata for release verification.
@@ -161,16 +161,16 @@ The v1.1.3 GitHub Release contains:
161
161
  macOS / Linux:
162
162
 
163
163
  ```bash
164
- node ~/.codex-overleaf/source/scripts/uninstall-native-host.mjs
164
+ npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native
165
165
  ```
166
166
 
167
167
  Windows PowerShell:
168
168
 
169
169
  ```powershell
170
- node $env:LOCALAPPDATA\CodexOverleaf\source\scripts\uninstall-native-host.mjs
170
+ npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native
171
171
  ```
172
172
 
173
- If you installed from a manual checkout, you can also run `npm run uninstall:native` inside the repo.
173
+ If you installed from a manual checkout or source installer, you can also run `npm run uninstall:native` inside the repo, use `node ~/.codex-overleaf/source/scripts/uninstall-native-host.mjs` on macOS / Linux, or use `node $env:LOCALAPPDATA\CodexOverleaf\source\scripts\uninstall-native-host.mjs` on Windows PowerShell.
174
174
 
175
175
  Remove the extension from `chrome://extensions`. Optionally delete `~/.codex-overleaf` on macOS / Linux to remove local mirrors, native runtime files, and plugin history. On Windows, `%LOCALAPPDATA%\CodexOverleaf` contains the native source, runtime, bridge, and native log, while `%USERPROFILE%\.codex-overleaf` contains project mirrors, plugin Codex home/history, and Codex Overleaf skills. Full Windows cleanup requires deleting both roots, or following [Local Data And Cleanup](#local-data-and-cleanup).
176
176
 
@@ -202,13 +202,13 @@ The uninstaller removes the Native Messaging registration, bridge executable, an
202
202
  Linux Chromium install or update:
203
203
 
204
204
  ```bash
205
- CODEX_OVERLEAF_REF=v1.1.3 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.3/install.sh)" -- --browser chromium
205
+ CODEX_OVERLEAF_REF=v1.2.6 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.6/install.sh)" -- --browser chromium
206
206
  ```
207
207
 
208
208
  Linux Chromium uninstall:
209
209
 
210
210
  ```bash
211
- node ~/.codex-overleaf/source/scripts/uninstall-native-host.mjs --browser chromium
211
+ npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native --browser chromium
212
212
  ```
213
213
 
214
214
  ## Features
@@ -335,7 +335,7 @@ Full uninstall and data deletion:
335
335
  - macOS/Linux: `rm -rf ~/.codex-overleaf ~/Codex\ Overleaf\ Link\ Extension`
336
336
  - Windows PowerShell: `Remove-Item -Recurse -Force "$env:LOCALAPPDATA\CodexOverleaf", "$env:USERPROFILE\.codex-overleaf" -ErrorAction SilentlyContinue`
337
337
 
338
- Composer attachments are turn-scoped Codex context. Limits are 8 attachments per run and 12 MiB per attachment. Attachments are staged under `.codex-overleaf-attachments` inside the mirror workspace and are ignored during writeback.
338
+ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per run, 12 MiB per attachment, and 32 MiB total raw attachment size per run. Attachments are staged under `.codex-overleaf-attachments` inside the mirror workspace and are ignored during writeback.
339
339
 
340
340
  ## FAQ And Troubleshooting
341
341
 
@@ -344,7 +344,7 @@ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per
344
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.
345
345
 
346
346
  ```bash
347
- npm exec --yes codex-overleaf-link@1.1.3 -- install-native
347
+ npm exec --yes codex-overleaf-link@1.2.6 -- install-native
348
348
  ```
349
349
 
350
350
  If npm is unavailable, use the GitHub Release script fallback for your platform.
@@ -352,14 +352,14 @@ If npm is unavailable, use the GitHub Release script fallback for your platform.
352
352
  macOS/Linux:
353
353
 
354
354
  ```bash
355
- CODEX_OVERLEAF_REF=v1.1.3 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.3/install.sh)"
355
+ CODEX_OVERLEAF_REF=v1.2.6 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.6/install.sh)"
356
356
  ```
357
357
 
358
358
  Windows PowerShell:
359
359
 
360
360
  ```powershell
361
- iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.1.3/install.ps1 -OutFile install.ps1
362
- $env:CODEX_OVERLEAF_REF='v1.1.3'
361
+ iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.6/install.ps1 -OutFile install.ps1
362
+ $env:CODEX_OVERLEAF_REF='v1.2.6'
363
363
  powershell -ExecutionPolicy Bypass -File install.ps1
364
364
  ```
365
365
 
@@ -418,8 +418,8 @@ Use this matrix for release-candidate signoff and compatibility reports. Record
418
418
  | Browser/channel/version | Google Chrome channel and version. | Google Chrome channel and version. | Google Chrome channel and version. | Chromium channel/package and version. |
419
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`. |
420
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`. |
421
- | Installer/update command | `npm exec --yes codex-overleaf-link@1.1.3 -- install-native` | `npm exec --yes codex-overleaf-link@1.1.3 -- install-native` | `npm exec --yes codex-overleaf-link@1.1.3 -- install-native` | `npm exec --yes codex-overleaf-link@1.1.3 -- install-native --browser chromium` |
422
- | Uninstall command | `npm exec --yes codex-overleaf-link@1.1.3 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.1.3 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.1.3 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.1.3 -- uninstall-native --browser chromium` |
421
+ | Installer/update command | `npm exec --yes codex-overleaf-link@1.2.6 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.6 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.6 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.6 -- install-native --browser chromium` |
422
+ | Uninstall command | `npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.6 -- uninstall-native --browser chromium` |
423
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` |
424
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`. |
425
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.3';
15
+ const BUILD_TARGET_VERSION = '1.2.6';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -0,0 +1,491 @@
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 BARE_LOCAL_PATH_PATTERN = /(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/]projects[\\/][^\s)\]]+)/gi;
19
+
20
+ function parseLineReferencesFromText({ text, mode }) {
21
+ return collectLineReferences(text, mode).map(toPublicReference);
22
+ }
23
+
24
+ function resolveProjectReference({ rawPath, projectFiles }) {
25
+ const normalizedRawPath = normalizeReferencePath(rawPath);
26
+ if (!normalizedRawPath || hasUnsafePathSegments(normalizedRawPath)) {
27
+ return null;
28
+ }
29
+
30
+ const textFiles = collectTextProjectFiles(projectFiles);
31
+ const exactMatch = textFiles.find(file => file.normalizedPath === normalizedRawPath);
32
+ if (exactMatch) {
33
+ return buildResolvedReference(exactMatch, 'exact');
34
+ }
35
+
36
+ const suffixMatches = textFiles.filter(file => normalizedRawPath.endsWith(`/${file.normalizedPath}`));
37
+ if (suffixMatches.length === 1) {
38
+ return buildResolvedReference(suffixMatches[0], 'suffix');
39
+ }
40
+
41
+ const rawBasename = getBasename(normalizedRawPath);
42
+ const basenameMatches = textFiles.filter(file => getBasename(file.normalizedPath) === rawBasename);
43
+ if (basenameMatches.length === 1) {
44
+ return buildResolvedReference(basenameMatches[0], 'basename');
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+ function sanitizeLocalReferences(text, { projectFiles, context } = {}) {
51
+ if (typeof text !== 'string' || !text) {
52
+ return '';
53
+ }
54
+
55
+ let sanitized = replaceMarkdownLinks(text, (_rawMarkdown, label, target) => {
56
+ const sanitizedLabel = sanitizeLocalReferenceText(label, {
57
+ projectFiles,
58
+ placeholderBrackets: false
59
+ });
60
+ const trimmedTarget = String(target || '').trim();
61
+ if (/^https?:\/\//i.test(trimmedTarget)) {
62
+ return `[${sanitizedLabel}](${target})`;
63
+ }
64
+ if (containsLocalTarget(trimmedTarget)) {
65
+ return `[${sanitizedLabel}]`;
66
+ }
67
+ const sanitizedTarget = sanitizeLocalReferenceText(target, {
68
+ projectFiles,
69
+ placeholderBrackets: false
70
+ });
71
+ return `[${sanitizedLabel}](${sanitizedTarget})`;
72
+ });
73
+
74
+ sanitized = sanitizeLocalReferenceText(sanitized, {
75
+ projectFiles,
76
+ placeholderBrackets: true
77
+ });
78
+
79
+ if (context === 'render' || context === 'persist') {
80
+ return sanitized;
81
+ }
82
+ return sanitized;
83
+ }
84
+
85
+ function replaceMarkdownLinks(text, replacer) {
86
+ const source = String(text || '');
87
+ let output = '';
88
+ let cursor = 0;
89
+
90
+ for (let index = 0; index < source.length; index += 1) {
91
+ if (source[index] !== '[') {
92
+ continue;
93
+ }
94
+ const closeLabel = source.indexOf(']', index + 1);
95
+ if (closeLabel === -1 || source[closeLabel + 1] !== '(') {
96
+ continue;
97
+ }
98
+ const targetStart = closeLabel + 2;
99
+ const targetEnd = findMarkdownTargetEnd(source, targetStart);
100
+ if (targetEnd === -1) {
101
+ continue;
102
+ }
103
+ output += source.slice(cursor, index);
104
+ output += replacer(
105
+ source.slice(index, targetEnd + 1),
106
+ source.slice(index + 1, closeLabel),
107
+ unescapeMarkdownTarget(source.slice(targetStart, targetEnd))
108
+ );
109
+ cursor = targetEnd + 1;
110
+ index = targetEnd;
111
+ }
112
+
113
+ return output + source.slice(cursor);
114
+ }
115
+
116
+ function findMarkdownTargetEnd(text, start) {
117
+ let depth = 0;
118
+ for (let index = start; index < text.length; index += 1) {
119
+ const char = text[index];
120
+ if (char === '\\') {
121
+ index += 1;
122
+ continue;
123
+ }
124
+ if (char === '(') {
125
+ depth += 1;
126
+ continue;
127
+ }
128
+ if (char === ')') {
129
+ if (depth === 0) {
130
+ return index;
131
+ }
132
+ depth -= 1;
133
+ }
134
+ }
135
+ return -1;
136
+ }
137
+
138
+ function unescapeMarkdownTarget(value) {
139
+ return String(value || '').replace(/\\([()\\])/g, '$1');
140
+ }
141
+
142
+ function isLocalPathLike(value) {
143
+ if (typeof value !== 'string') {
144
+ return false;
145
+ }
146
+
147
+ const rawValue = value.trim();
148
+ if (!rawValue || /^https?:\/\//i.test(rawValue)) {
149
+ return false;
150
+ }
151
+
152
+ const normalizedPath = normalizeReferencePath(rawValue);
153
+ return /^file:\/\//i.test(rawValue)
154
+ || /^[A-Za-z]:[\\/]/.test(rawValue)
155
+ || rawValue.startsWith('/')
156
+ || rawValue.includes('.codex-overleaf/projects')
157
+ || rawValue.includes('.codex-overleaf\\projects')
158
+ || normalizedPath.includes('/.codex-overleaf/projects/')
159
+ || /^Users\/[^/]+\//.test(normalizedPath)
160
+ || /^home\/[^/]+\//.test(normalizedPath);
161
+ }
162
+
163
+ function normalizeReferencePath(value) {
164
+ if (typeof value !== 'string') {
165
+ return '';
166
+ }
167
+
168
+ let normalized = value.trim();
169
+ if (!normalized) {
170
+ return '';
171
+ }
172
+
173
+ normalized = stripReferenceQueryAndHash(normalized);
174
+
175
+ if (/^file:\/\//i.test(normalized)) {
176
+ normalized = normalized.replace(/^file:\/\/\/?/i, '/');
177
+ }
178
+
179
+ try {
180
+ normalized = decodeURI(normalized);
181
+ } catch (_error) {
182
+ return '';
183
+ }
184
+
185
+ normalized = normalized
186
+ .replace(/\\/g, '/')
187
+ .replace(/\/+/g, '/')
188
+ .trim();
189
+
190
+ let previous;
191
+ do {
192
+ previous = normalized;
193
+ normalized = normalized
194
+ .replace(/^@+/, '')
195
+ .replace(/^\/+/, '')
196
+ .replace(/^\.\//, '');
197
+ } while (normalized !== previous);
198
+
199
+ return normalized;
200
+ }
201
+
202
+ function collectLineReferences(text, mode) {
203
+ if (!VALID_PARSE_MODES.has(mode) || typeof text !== 'string' || !text) {
204
+ return [];
205
+ }
206
+
207
+ const refs = [];
208
+ if (mode === 'markdown-link-target') {
209
+ collectMarkdownTargetReferences(text, mode, refs);
210
+ }
211
+ collectColonReferences(text, mode, refs);
212
+ collectLineWordReferences(text, mode, refs);
213
+ collectChineseLineReferences(text, mode, refs);
214
+
215
+ return refs
216
+ .filter(ref => !shouldSkipReference(ref))
217
+ .sort((left, right) => left.index - right.index || right.rawText.length - left.rawText.length)
218
+ .reduce((deduped, ref) => {
219
+ if (deduped.some(existing => rangesOverlap(existing, ref))) {
220
+ return deduped;
221
+ }
222
+ deduped.push(ref);
223
+ return deduped;
224
+ }, []);
225
+ }
226
+
227
+ function collectMarkdownTargetReferences(text, mode, refs) {
228
+ const normalizedText = stripReferenceQueryAndHash(String(text || '').trim());
229
+ const match = normalizedText.match(new RegExp(`^(.+?\\.${TEXT_EXTENSION_PATTERN}):(\\d+)(?::(\\d+))?$`, 'i'));
230
+ if (!match) {
231
+ return;
232
+ }
233
+ const line = parsePositiveInteger(match[2]);
234
+ const column = match[3] ? parsePositiveInteger(match[3]) : null;
235
+ if (!line || (match[3] && !column)) {
236
+ return;
237
+ }
238
+ refs.push({
239
+ rawText: text,
240
+ displayText: text,
241
+ rawPath: match[1],
242
+ line,
243
+ column,
244
+ source: mode,
245
+ index: 0
246
+ });
247
+ }
248
+
249
+ function collectColonReferences(text, mode, refs) {
250
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}:(\\d+)(?::(\\d+))?(?![-:\\d])`, 'gi');
251
+ let match;
252
+ while ((match = pattern.exec(text)) !== null) {
253
+ const line = parsePositiveInteger(match[3]);
254
+ const column = match[4] ? parsePositiveInteger(match[4]) : null;
255
+ if (!line || (match[4] && !column)) {
256
+ continue;
257
+ }
258
+ const prefixLength = match[1].length;
259
+ const rawText = match[0].slice(prefixLength);
260
+ refs.push({
261
+ rawText,
262
+ displayText: rawText,
263
+ rawPath: match[2],
264
+ line,
265
+ column,
266
+ source: mode,
267
+ index: match.index + prefixLength
268
+ });
269
+ }
270
+ }
271
+
272
+ function collectLineWordReferences(text, mode, refs) {
273
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+line\\s+(\\d+)(?![-:\\d])`, 'gi');
274
+ let match;
275
+ while ((match = pattern.exec(text)) !== null) {
276
+ const line = parsePositiveInteger(match[3]);
277
+ if (!line) {
278
+ continue;
279
+ }
280
+ const prefixLength = match[1].length;
281
+ const rawText = match[0].slice(prefixLength);
282
+ refs.push({
283
+ rawText,
284
+ displayText: `${match[2]}:${line}`,
285
+ rawPath: match[2],
286
+ line,
287
+ column: null,
288
+ source: mode,
289
+ index: match.index + prefixLength
290
+ });
291
+ }
292
+ }
293
+
294
+ function collectChineseLineReferences(text, mode, refs) {
295
+ const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+第\\s*(\\d+)\\s*行`, 'gi');
296
+ let match;
297
+ while ((match = pattern.exec(text)) !== null) {
298
+ const line = parsePositiveInteger(match[3]);
299
+ if (!line) {
300
+ continue;
301
+ }
302
+ const prefixLength = match[1].length;
303
+ const rawText = match[0].slice(prefixLength);
304
+ refs.push({
305
+ rawText,
306
+ displayText: `${match[2]}:${line}`,
307
+ rawPath: match[2],
308
+ line,
309
+ column: null,
310
+ source: mode,
311
+ index: match.index + prefixLength
312
+ });
313
+ }
314
+ }
315
+
316
+ function sanitizeLocalReferenceText(text, { projectFiles, placeholderBrackets }) {
317
+ let sanitized = replaceLineReferences(String(text || ''), {
318
+ projectFiles,
319
+ placeholderBrackets
320
+ });
321
+ sanitized = replaceBareLocalPaths(sanitized, {
322
+ projectFiles,
323
+ placeholderBrackets
324
+ });
325
+ return sanitized;
326
+ }
327
+
328
+ function replaceLineReferences(text, { projectFiles, placeholderBrackets }) {
329
+ const refs = collectLineReferences(text, 'plain-text-token')
330
+ .filter(ref => isLocalPathLike(ref.rawPath))
331
+ .sort((left, right) => right.index - left.index);
332
+ let sanitized = text;
333
+ for (const ref of refs) {
334
+ const replacement = formatReferenceReplacement(ref, {
335
+ projectFiles,
336
+ placeholderBrackets
337
+ });
338
+ sanitized = `${sanitized.slice(0, ref.index)}${replacement}${sanitized.slice(ref.index + ref.rawText.length)}`;
339
+ }
340
+ return sanitized;
341
+ }
342
+
343
+ function replaceBareLocalPaths(text, { projectFiles, placeholderBrackets }) {
344
+ return text.replace(BARE_LOCAL_PATH_PATTERN, (rawPath, offset, fullText) => {
345
+ if (/^[A-Za-z]:[\\/]/.test(rawPath) && offset > 0 && /[A-Za-z]/.test(fullText[offset - 1])) {
346
+ return rawPath;
347
+ }
348
+ const { value, trailing } = splitTrailingPunctuation(rawPath);
349
+ const resolved = resolveProjectReference({ rawPath: value, projectFiles });
350
+ if (resolved) {
351
+ return `${resolved.path}${trailing}`;
352
+ }
353
+ return `${formatLocalPathPlaceholder(null, placeholderBrackets)}${trailing}`;
354
+ });
355
+ }
356
+
357
+ function formatReferenceReplacement(ref, { projectFiles, placeholderBrackets }) {
358
+ const resolved = resolveProjectReference({
359
+ rawPath: ref.rawPath,
360
+ projectFiles
361
+ });
362
+ if (resolved) {
363
+ return `${resolved.path}:${ref.line}${ref.column ? `:${ref.column}` : ''}`;
364
+ }
365
+ return formatLocalPathPlaceholder(ref.line, placeholderBrackets);
366
+ }
367
+
368
+ function formatLocalPathPlaceholder(line, includeBrackets) {
369
+ const value = line ? `local path:${line}` : 'local path';
370
+ return includeBrackets ? `[${value}]` : value;
371
+ }
372
+
373
+ function containsLocalTarget(target) {
374
+ if (!target) {
375
+ return false;
376
+ }
377
+ if (isLocalPathLike(target)) {
378
+ return true;
379
+ }
380
+ return collectLineReferences(target, 'markdown-link-target')
381
+ .some(ref => isLocalPathLike(ref.rawPath));
382
+ }
383
+
384
+ function stripReferenceQueryAndHash(value) {
385
+ return String(value || '').replace(/([:.]\d+(?::\d+)?)(?:[?#].*)$/, '$1');
386
+ }
387
+
388
+ function collectTextProjectFiles(projectFiles) {
389
+ if (!Array.isArray(projectFiles)) {
390
+ return [];
391
+ }
392
+
393
+ const result = [];
394
+ for (const file of projectFiles) {
395
+ if (!file || file.kind !== 'text' || typeof file.path !== 'string') {
396
+ continue;
397
+ }
398
+
399
+ const normalizedPath = normalizeReferencePath(file.path);
400
+ if (!normalizedPath || hasUnsafePathSegments(normalizedPath)) {
401
+ continue;
402
+ }
403
+
404
+ result.push({
405
+ entry: file,
406
+ path: normalizedPath,
407
+ normalizedPath
408
+ });
409
+ }
410
+ return result;
411
+ }
412
+
413
+ function buildResolvedReference(file, resolution) {
414
+ return {
415
+ path: file.path,
416
+ file: file.entry,
417
+ normalizedPath: file.normalizedPath,
418
+ resolution
419
+ };
420
+ }
421
+
422
+ function shouldSkipReference(ref) {
423
+ const rawPath = String(ref.rawPath || '');
424
+ return /^https?:\/\//i.test(rawPath)
425
+ || isEmailLikePath(rawPath)
426
+ || hasUnsafeLineRange(ref.rawText);
427
+ }
428
+
429
+ function isEmailLikePath(rawPath) {
430
+ return /^[^@\s/\\]+@[^@\s/\\]+$/.test(rawPath);
431
+ }
432
+
433
+ function hasUnsafeLineRange(rawText) {
434
+ return /:\d+-\d+(?=$|[^\d])/.test(rawText);
435
+ }
436
+
437
+ function hasUnsafePathSegments(path) {
438
+ return normalizeReferencePath(path)
439
+ .split('/')
440
+ .some(segment => segment === '.' || segment === '..');
441
+ }
442
+
443
+ function rangesOverlap(left, right) {
444
+ const leftEnd = left.index + left.rawText.length;
445
+ const rightEnd = right.index + right.rawText.length;
446
+ return left.index < rightEnd && right.index < leftEnd;
447
+ }
448
+
449
+ function parsePositiveInteger(value) {
450
+ const parsed = Number(value);
451
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
452
+ return null;
453
+ }
454
+ return parsed;
455
+ }
456
+
457
+ function getBasename(path) {
458
+ const parts = String(path || '').split('/');
459
+ return parts[parts.length - 1] || '';
460
+ }
461
+
462
+ function splitTrailingPunctuation(value) {
463
+ const match = String(value || '').match(/^(.*?)([.,;!?]+)$/);
464
+ if (!match) {
465
+ return { value, trailing: '' };
466
+ }
467
+ return {
468
+ value: match[1],
469
+ trailing: match[2]
470
+ };
471
+ }
472
+
473
+ function toPublicReference(ref) {
474
+ return {
475
+ rawText: ref.rawText,
476
+ displayText: ref.displayText,
477
+ rawPath: ref.rawPath,
478
+ line: ref.line,
479
+ column: ref.column,
480
+ source: ref.source
481
+ };
482
+ }
483
+
484
+ return {
485
+ isLocalPathLike,
486
+ normalizeReferencePath,
487
+ parseLineReferencesFromText,
488
+ resolveProjectReference,
489
+ sanitizeLocalReferences
490
+ };
491
+ });
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-overleaf-link",
3
- "version": "1.1.3",
3
+ "version": "1.2.6",
4
4
  "description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -19,6 +19,7 @@
19
19
  "benchmark:large": "node scripts/benchmark-large-project.mjs --gate",
20
20
  "smoke:extension": "node scripts/smoke-extension.mjs",
21
21
  "verify:release": "node scripts/verify-release.mjs",
22
+ "verify:release-artifacts": "node scripts/verify-release-artifacts.mjs",
22
23
  "build:release": "node scripts/build-release.mjs",
23
24
  "install:native": "node scripts/install-native-host.mjs",
24
25
  "uninstall:native": "node scripts/uninstall-native-host.mjs",