@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +95 -89
  2. package/dist/bin/cleanup-accounts.mjs +63 -63
  3. package/dist/bin/codex-remote.mjs +374 -367
  4. package/dist/bin/manage-relay-admin.mjs +20 -20
  5. package/dist/bin/migrate-relay-db.mjs +109 -109
  6. package/dist/bin/publish-android-release.mjs +62 -58
  7. package/dist/bin/self-relay-admin.mjs +8 -8
  8. package/dist/bin/self-relay-demo.mjs +33 -33
  9. package/dist/bin/self-relay-server.mjs +66 -66
  10. package/dist/build-platform.json +1 -1
  11. package/dist/get-lan-networks.ps1 +40 -40
  12. package/dist/get-shared-service-url.ps1 +6 -6
  13. package/dist/shared/app-server-events.cjs +422 -412
  14. package/dist/shared/code-browser.cjs +159 -159
  15. package/dist/shared/file-changes.cjs +31 -31
  16. package/dist/shared/file-citations.cjs +117 -117
  17. package/dist/shared/file-mentions.cjs +92 -92
  18. package/dist/shared/message-delivery.cjs +54 -54
  19. package/dist/shared/p2p-data-channel.cjs +72 -72
  20. package/dist/shared/p2p-diagnostics.cjs +28 -28
  21. package/dist/shared/p2p-file-channel.cjs +74 -74
  22. package/dist/shared/plan-pricing.cjs +32 -32
  23. package/dist/shared/question-attention.cjs +40 -0
  24. package/dist/shared/server-requests.cjs +219 -219
  25. package/dist/shared/timeline-sync.cjs +173 -173
  26. package/dist/skills/code-relax/SKILL.md +22 -22
  27. package/dist/src/archived-threads.mjs +22 -0
  28. package/dist/src/background-process.mjs +18 -18
  29. package/dist/src/bounded-process-log.mjs +63 -63
  30. package/dist/src/celsius-client.mjs +107 -107
  31. package/dist/src/code-browser.mjs +100 -100
  32. package/dist/src/desktop-launcher.mjs +46 -46
  33. package/dist/src/dots-approval-timeline.mjs +99 -99
  34. package/dist/src/dots-avatar.mjs +42 -42
  35. package/dist/src/dots-chat-projection.mjs +50 -50
  36. package/dist/src/dots-chat-server.mjs +461 -461
  37. package/dist/src/dots-message-images.mjs +319 -319
  38. package/dist/src/dots-profile.mjs +77 -77
  39. package/dist/src/dots-room-activity.mjs +94 -94
  40. package/dist/src/dots-room-client.mjs +135 -135
  41. package/dist/src/dots-room-projection.mjs +94 -94
  42. package/dist/src/dots-schedules.mjs +66 -66
  43. package/dist/src/dots-summary-tasks.mjs +32 -32
  44. package/dist/src/dots-task-catalog.mjs +430 -430
  45. package/dist/src/dots-task-files.mjs +52 -52
  46. package/dist/src/execution-health.mjs +26 -26
  47. package/dist/src/file-transfer-range.mjs +22 -22
  48. package/dist/src/file-upload-session.mjs +130 -130
  49. package/dist/src/hosted-history-app-server.mjs +103 -103
  50. package/dist/src/hosted-history-cache.mjs +113 -113
  51. package/dist/src/lan-socket-server.mjs +265 -263
  52. package/dist/src/managed-app-server.mjs +244 -244
  53. package/dist/src/message-images.mjs +9 -9
  54. package/dist/src/message-upload-files.mjs +74 -74
  55. package/dist/src/onboarding.mjs +12 -12
  56. package/dist/src/plan-pricing.mjs +47 -47
  57. package/dist/src/platform/README.md +12 -12
  58. package/dist/src/platform/codex-protocol.mjs +46 -46
  59. package/dist/src/platform/codex-runtime.mjs +3 -3
  60. package/dist/src/platform/credential-store.mjs +3 -3
  61. package/dist/src/platform/desktop-host.mjs +3 -3
  62. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  63. package/dist/src/platform/desktop-project-store.mjs +208 -149
  64. package/dist/src/platform/file-opener.mjs +9 -0
  65. package/dist/src/platform/linux/README.md +3 -3
  66. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  67. package/dist/src/platform/linux/credential-store.mjs +28 -0
  68. package/dist/src/platform/linux/desktop-host.mjs +83 -0
  69. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  70. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  71. package/dist/src/platform/linux/network-info.mjs +11 -0
  72. package/dist/src/platform/linux/process.mjs +42 -0
  73. package/dist/src/platform/linux/project-locations.mjs +17 -0
  74. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  75. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  76. package/dist/src/platform/linux/service-host.mjs +18 -0
  77. package/dist/src/platform/linux/system.mjs +49 -0
  78. package/dist/src/platform/linux/user-environment.mjs +7 -0
  79. package/dist/src/platform/macos/README.md +3 -3
  80. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  81. package/dist/src/platform/macos/credential-store.mjs +10 -10
  82. package/dist/src/platform/macos/desktop-host.mjs +82 -82
  83. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
  84. package/dist/src/platform/macos/network-info.mjs +8 -28
  85. package/dist/src/platform/macos/project-locations.mjs +8 -8
  86. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  87. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  88. package/dist/src/platform/macos/service-host.mjs +18 -18
  89. package/dist/src/platform/macos/system.mjs +29 -29
  90. package/dist/src/platform/macos/user-environment.mjs +7 -7
  91. package/dist/src/platform/native-package.mjs +58 -46
  92. package/dist/src/platform/network-info.mjs +3 -3
  93. package/dist/src/platform/path-identity.mjs +5 -5
  94. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  95. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  96. package/dist/src/platform/project-locations.mjs +3 -3
  97. package/dist/src/platform/runtime-paths.mjs +8 -8
  98. package/dist/src/platform/select.mjs +5 -5
  99. package/dist/src/platform/service-host.mjs +3 -3
  100. package/dist/src/platform/user-environment.mjs +3 -3
  101. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  102. package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
  103. package/dist/src/platform/windows/background-process.mjs +30 -30
  104. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  105. package/dist/src/platform/windows/credential-store.mjs +27 -27
  106. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  107. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  108. package/dist/src/platform/windows/desktop-host.mjs +58 -58
  109. package/dist/src/platform/windows/desktop-info.ps1 +14 -14
  110. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  111. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  112. package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
  113. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  114. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  115. package/dist/src/platform/windows/network-info.mjs +34 -34
  116. package/dist/src/platform/windows/project-locations.mjs +16 -16
  117. package/dist/src/platform/windows/resident-start.js +20 -20
  118. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  119. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  120. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  121. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  122. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  123. package/dist/src/platform/windows/service-host.mjs +31 -31
  124. package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
  125. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  126. package/dist/src/platform/windows/user-environment.mjs +50 -50
  127. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  128. package/dist/src/project-archive.mjs +30 -0
  129. package/dist/src/project-create.mjs +121 -119
  130. package/dist/src/project-management.mjs +59 -69
  131. package/dist/src/project-remove.mjs +55 -0
  132. package/dist/src/project-update.mjs +56 -0
  133. package/dist/src/runtime-discovery.mjs +47 -47
  134. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  135. package/dist/src/self-relay/account-connections.mjs +52 -52
  136. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  137. package/dist/src/self-relay/admin-page.mjs +80 -80
  138. package/dist/src/self-relay/admin-server.mjs +74 -74
  139. package/dist/src/self-relay/admin-state.mjs +127 -127
  140. package/dist/src/self-relay/admin-style.mjs +14 -14
  141. package/dist/src/self-relay/android-releases.mjs +61 -61
  142. package/dist/src/self-relay/client.mjs +61 -61
  143. package/dist/src/self-relay/connector.mjs +439 -438
  144. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  145. package/dist/src/self-relay/demo.mjs +37 -37
  146. package/dist/src/self-relay/device-info.mjs +27 -0
  147. package/dist/src/self-relay/device-store.mjs +92 -84
  148. package/dist/src/self-relay/diagnostics.mjs +54 -54
  149. package/dist/src/self-relay/email-accounts.mjs +183 -183
  150. package/dist/src/self-relay/file-wire.mjs +13 -13
  151. package/dist/src/self-relay/lan-authorizations.mjs +71 -42
  152. package/dist/src/self-relay/lifecycle.mjs +264 -250
  153. package/dist/src/self-relay/long-pairing.mjs +40 -40
  154. package/dist/src/self-relay/mysql-state.mjs +118 -105
  155. package/dist/src/self-relay/p2p-probe.mjs +159 -159
  156. package/dist/src/self-relay/privacy-page.mjs +26 -26
  157. package/dist/src/self-relay/public-releases.mjs +34 -0
  158. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  159. package/dist/src/self-relay/server.mjs +1006 -932
  160. package/dist/src/self-relay/usage-daily.mjs +55 -55
  161. package/dist/src/self-relay/usage-meter.mjs +141 -141
  162. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  163. package/dist/src/self-relay/wire.mjs +36 -36
  164. package/dist/src/server.mjs +1861 -1778
  165. package/dist/src/service-control.mjs +82 -82
  166. package/dist/src/service-doctor.mjs +110 -110
  167. package/dist/src/service-health.mjs +33 -33
  168. package/dist/src/service-install.mjs +91 -91
  169. package/dist/src/service-lifecycle.mjs +138 -138
  170. package/dist/src/service-restart.mjs +81 -81
  171. package/dist/src/shared-app-server.mjs +615 -611
  172. package/dist/src/shared-catalog.mjs +132 -132
  173. package/dist/src/shared-recovery.mjs +146 -146
  174. package/dist/src/shared-thread-stream.mjs +164 -164
  175. package/dist/src/skill-install.mjs +51 -51
  176. package/dist/src/thread-title-generation.mjs +150 -150
  177. package/dist/src/turn-model-history.mjs +47 -47
  178. package/dist/src/usage-reset.mjs +17 -17
  179. package/dist/src/workstation-downloads.mjs +120 -120
  180. package/dist/tools/rtc-route-helper.ps1 +99 -99
  181. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  182. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  183. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  184. package/package.json +7 -6
  185. package/tools/postinstall.mjs +127 -127
@@ -1,49 +1,49 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { dotsProfile } from '../../dots-profile.mjs';
5
-
6
- // Desktop 26.928.2636.0 persists its current TBO sidebar profiles here.
7
- // Read display names, appearance colors and snapshot references; identity comes from the authenticated kernel.
8
- export function desktopDotsProfiles(codexDataRoot, stateRoot) {
9
- const snapshotPath = ({ accountId, userId }) => stateRoot && path.join(stateRoot, 'dots-profiles',
10
- createHash('sha256').update(JSON.stringify([accountId, userId])).digest('hex') + '.json');
11
- const read = async ({ accountId, userId }) => {
12
- let state;
13
- try {
14
- state = JSON.parse(await fs.readFile(path.join(codexDataRoot, '.codex-global-state.json'), 'utf8'));
15
- } catch (error) {
16
- if (error.code !== 'ENOENT') throw error;
17
- }
18
- const cache = state?.['electron-persisted-atom-state']?.['cloud-aeon-sidebar-cache-v1'];
19
- const names = new Map();
20
- if (!accountId || !userId) return names;
21
- const matching = cache?.hostId === 'durable' && cache.accountId === accountId && cache.userId === userId;
22
- for (const [threadId, profile] of Object.entries(matching ? cache.profilesByThreadId || {} : {})) {
23
- const value = dotsProfile(profile);
24
- for (const id of [threadId, profile.active_root_thread_id, profile.root_thread_id]) {
25
- if (typeof id === 'string' && id) names.set(id, value);
26
- }
27
- }
28
- const file = snapshotPath({ accountId, userId });
29
- if (file) {
30
- try {
31
- const saved = JSON.parse(await fs.readFile(file, 'utf8'));
32
- if (saved.accountId !== accountId || saved.userId !== userId || !Array.isArray(saved.profiles)) throw new Error('点点资料缓存身份无效。');
33
- const profiles = new Map(saved.profiles);
34
- const byId = new Map([...profiles.values()].map(profile => [profile.profileId, profile]));
35
- for (const [id, profile] of names) if (byId.has(profile.profileId)) names.set(id, byId.get(profile.profileId));
36
- for (const [id, profile] of profiles) names.set(id, profile);
37
- } catch (error) { if (error.code !== 'ENOENT') console.error('读取点点资料缓存失败:', error.message); }
38
- }
39
- return names;
40
- };
41
- read.save = async (identity, profiles) => {
42
- const file = snapshotPath(identity);
43
- if (!file) return;
44
- await fs.mkdir(path.dirname(file), { recursive: true });
45
- await fs.writeFile(file + '.tmp', JSON.stringify({ ...identity, profiles: [...profiles] }), 'utf8');
46
- await fs.rename(file + '.tmp', file);
47
- };
48
- return read;
49
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { dotsProfile } from '../../dots-profile.mjs';
5
+
6
+ // Desktop 26.928.2636.0 persists its current TBO sidebar profiles here.
7
+ // Read display names, appearance colors and snapshot references; identity comes from the authenticated kernel.
8
+ export function desktopDotsProfiles(codexDataRoot, stateRoot) {
9
+ const snapshotPath = ({ accountId, userId }) => stateRoot && path.join(stateRoot, 'dots-profiles',
10
+ createHash('sha256').update(JSON.stringify([accountId, userId])).digest('hex') + '.json');
11
+ const read = async ({ accountId, userId }) => {
12
+ let state;
13
+ try {
14
+ state = JSON.parse(await fs.readFile(path.join(codexDataRoot, '.codex-global-state.json'), 'utf8'));
15
+ } catch (error) {
16
+ if (error.code !== 'ENOENT') throw error;
17
+ }
18
+ const cache = state?.['electron-persisted-atom-state']?.['cloud-aeon-sidebar-cache-v1'];
19
+ const names = new Map();
20
+ if (!accountId || !userId) return names;
21
+ const matching = cache?.hostId === 'durable' && cache.accountId === accountId && cache.userId === userId;
22
+ for (const [threadId, profile] of Object.entries(matching ? cache.profilesByThreadId || {} : {})) {
23
+ const value = dotsProfile(profile);
24
+ for (const id of [threadId, profile.active_root_thread_id, profile.root_thread_id]) {
25
+ if (typeof id === 'string' && id) names.set(id, value);
26
+ }
27
+ }
28
+ const file = snapshotPath({ accountId, userId });
29
+ if (file) {
30
+ try {
31
+ const saved = JSON.parse(await fs.readFile(file, 'utf8'));
32
+ if (saved.accountId !== accountId || saved.userId !== userId || !Array.isArray(saved.profiles)) throw new Error('点点资料缓存身份无效。');
33
+ const profiles = new Map(saved.profiles);
34
+ const byId = new Map([...profiles.values()].map(profile => [profile.profileId, profile]));
35
+ for (const [id, profile] of names) if (byId.has(profile.profileId)) names.set(id, byId.get(profile.profileId));
36
+ for (const [id, profile] of profiles) names.set(id, profile);
37
+ } catch (error) { if (error.code !== 'ENOENT') console.error('读取点点资料缓存失败:', error.message); }
38
+ }
39
+ return names;
40
+ };
41
+ read.save = async (identity, profiles) => {
42
+ const file = snapshotPath(identity);
43
+ if (!file) return;
44
+ await fs.mkdir(path.dirname(file), { recursive: true });
45
+ await fs.writeFile(file + '.tmp', JSON.stringify({ ...identity, profiles: [...profiles] }), 'utf8');
46
+ await fs.rename(file + '.tmp', file);
47
+ };
48
+ return read;
49
+ }
@@ -1,41 +1,41 @@
1
- import fs from 'node:fs';
2
- import { spawn } from 'node:child_process';
3
- import { attachProcessLog } from '../../bounded-process-log.mjs';
4
-
5
- const [requestPath, readyPath, instance, diagnosticPath] = process.argv.slice(2);
6
- let result = { instance, brokerPid: process.pid };
7
-
8
- function diagnostic(message) {
9
- if (!diagnosticPath) return;
10
- fs.appendFileSync(diagnosticPath, `${new Date().toISOString()} ${message}\n`, 'utf8');
11
- }
12
-
13
- try {
14
- const spec = JSON.parse(fs.readFileSync(requestPath, 'utf8').replace(/^\uFEFF/u, ''));
15
- diagnostic('request loaded');
16
- if (spec.instance !== instance) throw new Error('Windows process broker request identity mismatch.');
17
- fs.rmSync(requestPath, { force: true });
18
- const logs = spec.logs || [];
19
- const child = spawn(spec.executable, spec.args || [], {
20
- windowsHide: true,
21
- env: spec.environment,
22
- stdio: ['ignore', logs[0] ? 'pipe' : 'ignore', logs[1] ? 'pipe' : 'ignore'],
23
- });
24
- if (child.stdout) attachProcessLog(child.stdout, logs[0]);
25
- if (child.stderr) attachProcessLog(child.stderr, logs[1]);
26
- await new Promise((resolve, reject) => {
27
- child.once('spawn', resolve);
28
- child.once('error', reject);
29
- });
30
- // Keep this WMI-owned broker alive with the target so Codex tools inherit
31
- // its hidden console instead of creating a visible console per command.
32
- result.pid = child.pid;
33
- diagnostic(`target started (PID ${child.pid})`);
34
- } catch (error) {
35
- result.error = error?.stack || error?.message || String(error);
36
- try { diagnostic(`failed: ${result.error}`); } catch {}
37
- }
38
-
39
- const temporary = `${readyPath}.tmp`;
40
- fs.writeFileSync(temporary, JSON.stringify(result), 'utf8');
41
- fs.renameSync(temporary, readyPath);
1
+ import fs from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { attachProcessLog } from '../../bounded-process-log.mjs';
4
+
5
+ const [requestPath, readyPath, instance, diagnosticPath] = process.argv.slice(2);
6
+ let result = { instance, brokerPid: process.pid };
7
+
8
+ function diagnostic(message) {
9
+ if (!diagnosticPath) return;
10
+ fs.appendFileSync(diagnosticPath, `${new Date().toISOString()} ${message}\n`, 'utf8');
11
+ }
12
+
13
+ try {
14
+ const spec = JSON.parse(fs.readFileSync(requestPath, 'utf8').replace(/^\uFEFF/u, ''));
15
+ diagnostic('request loaded');
16
+ if (spec.instance !== instance) throw new Error('Windows process broker request identity mismatch.');
17
+ fs.rmSync(requestPath, { force: true });
18
+ const logs = spec.logs || [];
19
+ const child = spawn(spec.executable, spec.args || [], {
20
+ windowsHide: true,
21
+ env: spec.environment,
22
+ stdio: ['ignore', logs[0] ? 'pipe' : 'ignore', logs[1] ? 'pipe' : 'ignore'],
23
+ });
24
+ if (child.stdout) attachProcessLog(child.stdout, logs[0]);
25
+ if (child.stderr) attachProcessLog(child.stderr, logs[1]);
26
+ await new Promise((resolve, reject) => {
27
+ child.once('spawn', resolve);
28
+ child.once('error', reject);
29
+ });
30
+ // Keep this WMI-owned broker alive with the target so Codex tools inherit
31
+ // its hidden console instead of creating a visible console per command.
32
+ result.pid = child.pid;
33
+ diagnostic(`target started (PID ${child.pid})`);
34
+ } catch (error) {
35
+ result.error = error?.stack || error?.message || String(error);
36
+ try { diagnostic(`failed: ${result.error}`); } catch {}
37
+ }
38
+
39
+ const temporary = `${readyPath}.tmp`;
40
+ fs.writeFileSync(temporary, JSON.stringify(result), 'utf8');
41
+ fs.renameSync(temporary, readyPath);
@@ -1,34 +1,34 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
-
6
- const exec = promisify(execFile);
7
- const bridgeRoot = fileURLToPath(new URL('../../../', import.meta.url));
8
-
9
- export async function lanNetworks() {
10
- const executable = path.join(process.env.SystemRoot, 'System32/WindowsPowerShell/v1.0/powershell.exe');
11
- const { stdout } = await exec(executable, ['-NoProfile', '-NonInteractive', '-File', path.join(bridgeRoot, 'get-lan-networks.ps1')],
12
- { windowsHide: true, encoding: 'utf8' });
13
- const records = JSON.parse(stdout.trim() || '[]');
14
- if (!Array.isArray(records) || records.length > 8) throw new Error('Windows 返回了无效的局域网信息');
15
- return records.map(record => {
16
- if (!privateIpv4(record.address) || !['wifi', 'ethernet'].includes(record.networkType) ||
17
- !plainLabel(record.networkName) || !plainLabel(record.interfaceName)) throw new Error('Windows 返回了无效的局域网信息');
18
- return { address: record.address, networkType: record.networkType,
19
- networkName: record.networkName.trim(), interfaceName: record.interfaceName.trim() };
20
- });
21
- }
22
-
23
- function privateIpv4(value) {
24
- if (typeof value !== 'string') return false;
25
- const parts = value.split('.').map(Number);
26
- if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return false;
27
- return parts[0] === 10 || (parts[0] === 192 && parts[1] === 168) ||
28
- (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
29
- (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127);
30
- }
31
-
32
- function plainLabel(value) {
33
- return typeof value === 'string' && value.trim().length > 0 && value.trim().length <= 80 && !/[\u0000-\u001f\u007f]/.test(value);
34
- }
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const exec = promisify(execFile);
7
+ const bridgeRoot = fileURLToPath(new URL('../../../', import.meta.url));
8
+
9
+ export async function lanNetworks() {
10
+ const executable = path.join(process.env.SystemRoot, 'System32/WindowsPowerShell/v1.0/powershell.exe');
11
+ const { stdout } = await exec(executable, ['-NoProfile', '-NonInteractive', '-File', path.join(bridgeRoot, 'get-lan-networks.ps1')],
12
+ { windowsHide: true, encoding: 'utf8' });
13
+ const records = JSON.parse(stdout.trim() || '[]');
14
+ if (!Array.isArray(records) || records.length > 8) throw new Error('Windows 返回了无效的局域网信息');
15
+ return records.map(record => {
16
+ if (!privateIpv4(record.address) || !['wifi', 'ethernet'].includes(record.networkType) ||
17
+ !plainLabel(record.networkName) || !plainLabel(record.interfaceName)) throw new Error('Windows 返回了无效的局域网信息');
18
+ return { address: record.address, networkType: record.networkType,
19
+ networkName: record.networkName.trim(), interfaceName: record.interfaceName.trim() };
20
+ });
21
+ }
22
+
23
+ function privateIpv4(value) {
24
+ if (typeof value !== 'string') return false;
25
+ const parts = value.split('.').map(Number);
26
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return false;
27
+ return parts[0] === 10 || (parts[0] === 192 && parts[1] === 168) ||
28
+ (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
29
+ (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127);
30
+ }
31
+
32
+ function plainLabel(value) {
33
+ return typeof value === 'string' && value.trim().length > 0 && value.trim().length <= 80 && !/[\u0000-\u001f\u007f]/.test(value);
34
+ }
@@ -1,16 +1,16 @@
1
- import { execFile } from 'node:child_process';
2
- import { promisify } from 'node:util';
3
- import path from 'node:path';
4
- import { powershell } from './desktop-host.mjs';
5
-
6
- const exec = promisify(execFile);
7
-
8
- export async function projectLocations() {
9
- const command = "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false); $projectDocuments = [Environment]::GetFolderPath('MyDocuments'); $projectUserDirectory = [Environment]::GetFolderPath('UserProfile'); $projectDownloads = [Environment]::ExpandEnvironmentVariables((Get-ItemPropertyValue -LiteralPath 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders' -Name '{374DE290-123F-4565-9164-39C4925E467B}' -ErrorAction Stop)); $projectDriveRoots = @(Get-PSDrive -PSProvider FileSystem | ForEach-Object { $_.Root }); @{ documents = $projectDocuments; home = $projectUserDirectory; downloads = $projectDownloads; roots = $projectDriveRoots } | ConvertTo-Json -Compress";
10
- const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-Command', command],
11
- { windowsHide: true, encoding: 'utf8', timeout: 10000 });
12
- const locations = JSON.parse(stdout);
13
- if (!path.isAbsolute(locations.documents)) throw new Error('无法取得电脑的文档目录。');
14
- if (!path.isAbsolute(locations.downloads)) throw new Error('无法取得电脑的下载目录。');
15
- return { defaultParent: path.join(locations.documents, 'ChatGPT'), home: locations.home, downloads: locations.downloads, roots: locations.roots };
16
- }
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import path from 'node:path';
4
+ import { powershell } from './desktop-host.mjs';
5
+
6
+ const exec = promisify(execFile);
7
+
8
+ export async function projectLocations() {
9
+ const command = "[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false); $projectDocuments = [Environment]::GetFolderPath('MyDocuments'); $projectUserDirectory = [Environment]::GetFolderPath('UserProfile'); $projectDownloads = [Environment]::ExpandEnvironmentVariables((Get-ItemPropertyValue -LiteralPath 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders' -Name '{374DE290-123F-4565-9164-39C4925E467B}' -ErrorAction Stop)); $projectDriveRoots = @(Get-PSDrive -PSProvider FileSystem | ForEach-Object { $_.Root }); @{ documents = $projectDocuments; home = $projectUserDirectory; downloads = $projectDownloads; roots = $projectDriveRoots } | ConvertTo-Json -Compress";
10
+ const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-Command', command],
11
+ { windowsHide: true, encoding: 'utf8', timeout: 10000 });
12
+ const locations = JSON.parse(stdout);
13
+ if (!path.isAbsolute(locations.documents)) throw new Error('无法取得电脑的文档目录。');
14
+ if (!path.isAbsolute(locations.downloads)) throw new Error('无法取得电脑的下载目录。');
15
+ return { defaultParent: path.join(locations.documents, 'ChatGPT'), home: locations.home, downloads: locations.downloads, roots: locations.roots };
16
+ }
@@ -1,20 +1,20 @@
1
- // Windows Script Host has no console. Keep the login task alive until the
2
- // short-lived CLI has handed the resident service to the background broker.
3
- if (WScript.Arguments.length !== 2) WScript.Quit(2);
4
- var shell = new ActiveXObject('WScript.Shell');
5
- var files = new ActiveXObject('Scripting.FileSystemObject');
6
- var node = WScript.Arguments.Item(0);
7
- var entry = WScript.Arguments.Item(1);
8
- if (!files.FileExists(node)) {
9
- var directories = shell.Environment('PROCESS').Item('PATH').split(';');
10
- node = '';
11
- for (var i = 0; i < directories.length; i++) {
12
- var directory = directories[i].replace(/^"|"$/g, '');
13
- if (!directory) continue;
14
- var candidate = files.BuildPath(directory, 'node.exe');
15
- if (files.FileExists(candidate)) { node = candidate; break; }
16
- }
17
- if (!node) WScript.Quit(2);
18
- }
19
- var command = '"' + node + '" "' + entry + '" start --json';
20
- WScript.Quit(shell.Run(command, 0, true));
1
+ // Windows Script Host has no console. Keep the login task alive until the
2
+ // short-lived CLI has handed the resident service to the background broker.
3
+ if (WScript.Arguments.length !== 2) WScript.Quit(2);
4
+ var shell = new ActiveXObject('WScript.Shell');
5
+ var files = new ActiveXObject('Scripting.FileSystemObject');
6
+ var node = WScript.Arguments.Item(0);
7
+ var entry = WScript.Arguments.Item(1);
8
+ if (!files.FileExists(node)) {
9
+ var directories = shell.Environment('PROCESS').Item('PATH').split(';');
10
+ node = '';
11
+ for (var i = 0; i < directories.length; i++) {
12
+ var directory = directories[i].replace(/^"|"$/g, '');
13
+ if (!directory) continue;
14
+ var candidate = files.BuildPath(directory, 'node.exe');
15
+ if (files.FileExists(candidate)) { node = candidate; break; }
16
+ }
17
+ if (!node) WScript.Quit(2);
18
+ }
19
+ var command = '"' + node + '" "' + entry + '" start --json';
20
+ WScript.Quit(shell.Run(command, 0, true));
@@ -1,7 +1,7 @@
1
- param([Parameter(Mandatory=$true)][string]$NodePath, [Parameter(Mandatory=$true)][string]$Entry)
2
- $ErrorActionPreference = 'Stop'
3
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
- $state = Join-Path $env:LOCALAPPDATA 'CodexRemote'
5
- New-Item -ItemType Directory -Path $state -Force | Out-Null
6
- & $NodePath $Entry start --json >> (Join-Path $state 'resident-start.log') 2>&1
7
- exit $LASTEXITCODE
1
+ param([Parameter(Mandatory=$true)][string]$NodePath, [Parameter(Mandatory=$true)][string]$Entry)
2
+ $ErrorActionPreference = 'Stop'
3
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
+ $state = Join-Path $env:LOCALAPPDATA 'CodexRemote'
5
+ New-Item -ItemType Directory -Path $state -Force | Out-Null
6
+ & $NodePath $Entry start --json >> (Join-Path $state 'resident-start.log') 2>&1
7
+ exit $LASTEXITCODE
@@ -1,83 +1,83 @@
1
- param([string]$NodePath, [string]$Entry, [ValidateSet('enable','disable','status')][string]$Action='status')
2
- $ErrorActionPreference = 'Stop'
3
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
-
5
- $taskName = 'Code Relax 桌面服务'
6
- $legacyTaskName = 'Code Relax Bridge'
7
- $taskSource = '@walkhi/code-relax'
8
- $taskPath = '\' + $taskName
9
- $launcher = Join-Path $PSScriptRoot 'resident-start.js'
10
- $wscript = Join-Path $env:WINDIR 'System32\wscript.exe'
11
- $arguments = '//B //Nologo "' + $launcher + '" "' + $NodePath + '" "' + $Entry + '"'
12
- $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
13
-
14
- $scheduler = New-Object -ComObject 'Schedule.Service'
15
- $scheduler.Connect()
16
- $root = $scheduler.GetFolder('\')
17
- $registered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
18
- $legacyRegistered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $legacyTaskName } | Select-Object -First 1)
19
- if ($registered.Count -gt 0 -and $registered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
20
- throw 'Scheduled task is not owned by this installation.'
21
- }
22
- if ($legacyRegistered.Count -gt 0 -and $legacyRegistered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
23
- throw 'Legacy scheduled task is not owned by this installation.'
24
- }
25
-
26
- # Remove only the legacy shortcut created by this package. A Startup LNK that
27
- # launches hidden PowerShell with ExecutionPolicy Bypass triggers AV heuristics.
28
- $shell = New-Object -ComObject WScript.Shell
29
- $legacyPath = Join-Path $shell.SpecialFolders.Item('Startup') 'Code Relax Bridge.lnk'
30
- $legacyLauncher = Join-Path $PSScriptRoot 'resident-start.ps1'
31
- $legacyExists = Test-Path -LiteralPath $legacyPath
32
- if ($Action -ne 'status' -and $legacyExists) {
33
- $legacy = $shell.CreateShortcut($legacyPath)
34
- if (-not $legacy.Arguments.Contains('"' + $legacyLauncher + '"')) {
35
- throw 'Legacy startup shortcut is not owned by this installation.'
36
- }
37
- }
38
-
39
- if ($Action -eq 'enable') {
40
- if (-not (Test-Path -LiteralPath $NodePath) -or -not (Test-Path -LiteralPath $Entry) -or
41
- -not (Test-Path -LiteralPath $launcher) -or -not (Test-Path -LiteralPath $wscript)) { throw 'Installed runtime is missing.' }
42
- $definition = $scheduler.NewTask(0)
43
- $definition.RegistrationInfo.Description = 'Start the Code Relax desktop service when this user signs in.'
44
- $definition.RegistrationInfo.Source = $taskSource
45
- $definition.Principal.UserId = $currentUser
46
- $definition.Principal.LogonType = 3 # TASK_LOGON_INTERACTIVE_TOKEN
47
- $definition.Principal.RunLevel = 0 # TASK_RUNLEVEL_LUA
48
- $definition.Settings.Enabled = $true
49
- $definition.Settings.Hidden = $true
50
- $definition.Settings.StartWhenAvailable = $true
51
- $definition.Settings.DisallowStartIfOnBatteries = $false
52
- $definition.Settings.StopIfGoingOnBatteries = $false
53
- $definition.Settings.ExecutionTimeLimit = 'PT5M'
54
- $definition.Settings.MultipleInstances = 2 # TASK_INSTANCES_IGNORE_NEW
55
- $trigger = $definition.Triggers.Create(9) # TASK_TRIGGER_LOGON
56
- $trigger.UserId = $currentUser
57
- $trigger.Enabled = $true
58
- $exec = $definition.Actions.Create(0) # TASK_ACTION_EXEC
59
- $exec.Path = $wscript
60
- $exec.Arguments = $arguments
61
- $exec.WorkingDirectory = $env:USERPROFILE
62
- [void]$root.RegisterTaskDefinition($taskName, $definition, 6, $null, $null, 3, $null) # TASK_CREATE_OR_UPDATE
63
- if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
64
- } elseif ($Action -eq 'disable') {
65
- if ($registered.Count -gt 0) { $root.DeleteTask($taskName, 0) }
66
- if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
67
- }
68
- if ($Action -ne 'status' -and $legacyExists) { Remove-Item -LiteralPath $legacyPath }
69
-
70
- $active = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
71
- $taskEnabled = $active.Count -gt 0 -and $active[0].Enabled
72
- $taskCurrent = $false
73
- if ($taskEnabled) {
74
- $taskAction = $active[0].Definition.Actions.Item(1)
75
- $taskCurrent = $taskAction.Path -ieq $wscript -and $taskAction.Arguments -ieq $arguments
76
- }
77
- @{
78
- enabled=($taskCurrent -and $legacyRegistered.Count -eq 0)
79
- taskPresent=$taskEnabled
80
- path=$(if ($taskEnabled) { 'Task Scheduler:' + $taskPath } elseif ($legacyRegistered.Count -gt 0) { 'Task Scheduler:\' + $legacyTaskName } elseif ($legacyExists) { $legacyPath } else { 'Task Scheduler:' + $taskPath })
81
- scope='current-user-logon'
82
- mechanism=$(if ($taskCurrent -and $legacyRegistered.Count -eq 0) { 'task-scheduler' } elseif ($taskEnabled -or $legacyRegistered.Count -gt 0) { 'stale-task-scheduler' } elseif ($legacyExists) { 'legacy-startup-shortcut' } else { 'task-scheduler' })
83
- } | ConvertTo-Json -Compress
1
+ param([string]$NodePath, [string]$Entry, [ValidateSet('enable','disable','status')][string]$Action='status')
2
+ $ErrorActionPreference = 'Stop'
3
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
+
5
+ $taskName = 'Code Relax 桌面服务'
6
+ $legacyTaskName = 'Code Relax Bridge'
7
+ $taskSource = '@walkhi/code-relax'
8
+ $taskPath = '\' + $taskName
9
+ $launcher = Join-Path $PSScriptRoot 'resident-start.js'
10
+ $wscript = Join-Path $env:WINDIR 'System32\wscript.exe'
11
+ $arguments = '//B //Nologo "' + $launcher + '" "' + $NodePath + '" "' + $Entry + '"'
12
+ $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
13
+
14
+ $scheduler = New-Object -ComObject 'Schedule.Service'
15
+ $scheduler.Connect()
16
+ $root = $scheduler.GetFolder('\')
17
+ $registered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
18
+ $legacyRegistered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $legacyTaskName } | Select-Object -First 1)
19
+ if ($registered.Count -gt 0 -and $registered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
20
+ throw 'Scheduled task is not owned by this installation.'
21
+ }
22
+ if ($legacyRegistered.Count -gt 0 -and $legacyRegistered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
23
+ throw 'Legacy scheduled task is not owned by this installation.'
24
+ }
25
+
26
+ # Remove only the legacy shortcut created by this package. A Startup LNK that
27
+ # launches hidden PowerShell with ExecutionPolicy Bypass triggers AV heuristics.
28
+ $shell = New-Object -ComObject WScript.Shell
29
+ $legacyPath = Join-Path $shell.SpecialFolders.Item('Startup') 'Code Relax Bridge.lnk'
30
+ $legacyLauncher = Join-Path $PSScriptRoot 'resident-start.ps1'
31
+ $legacyExists = Test-Path -LiteralPath $legacyPath
32
+ if ($Action -ne 'status' -and $legacyExists) {
33
+ $legacy = $shell.CreateShortcut($legacyPath)
34
+ if (-not $legacy.Arguments.Contains('"' + $legacyLauncher + '"')) {
35
+ throw 'Legacy startup shortcut is not owned by this installation.'
36
+ }
37
+ }
38
+
39
+ if ($Action -eq 'enable') {
40
+ if (-not (Test-Path -LiteralPath $NodePath) -or -not (Test-Path -LiteralPath $Entry) -or
41
+ -not (Test-Path -LiteralPath $launcher) -or -not (Test-Path -LiteralPath $wscript)) { throw 'Installed runtime is missing.' }
42
+ $definition = $scheduler.NewTask(0)
43
+ $definition.RegistrationInfo.Description = 'Start the Code Relax desktop service when this user signs in.'
44
+ $definition.RegistrationInfo.Source = $taskSource
45
+ $definition.Principal.UserId = $currentUser
46
+ $definition.Principal.LogonType = 3 # TASK_LOGON_INTERACTIVE_TOKEN
47
+ $definition.Principal.RunLevel = 0 # TASK_RUNLEVEL_LUA
48
+ $definition.Settings.Enabled = $true
49
+ $definition.Settings.Hidden = $true
50
+ $definition.Settings.StartWhenAvailable = $true
51
+ $definition.Settings.DisallowStartIfOnBatteries = $false
52
+ $definition.Settings.StopIfGoingOnBatteries = $false
53
+ $definition.Settings.ExecutionTimeLimit = 'PT5M'
54
+ $definition.Settings.MultipleInstances = 2 # TASK_INSTANCES_IGNORE_NEW
55
+ $trigger = $definition.Triggers.Create(9) # TASK_TRIGGER_LOGON
56
+ $trigger.UserId = $currentUser
57
+ $trigger.Enabled = $true
58
+ $exec = $definition.Actions.Create(0) # TASK_ACTION_EXEC
59
+ $exec.Path = $wscript
60
+ $exec.Arguments = $arguments
61
+ $exec.WorkingDirectory = $env:USERPROFILE
62
+ [void]$root.RegisterTaskDefinition($taskName, $definition, 6, $null, $null, 3, $null) # TASK_CREATE_OR_UPDATE
63
+ if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
64
+ } elseif ($Action -eq 'disable') {
65
+ if ($registered.Count -gt 0) { $root.DeleteTask($taskName, 0) }
66
+ if ($legacyRegistered.Count -gt 0) { $root.DeleteTask($legacyTaskName, 0) }
67
+ }
68
+ if ($Action -ne 'status' -and $legacyExists) { Remove-Item -LiteralPath $legacyPath }
69
+
70
+ $active = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
71
+ $taskEnabled = $active.Count -gt 0 -and $active[0].Enabled
72
+ $taskCurrent = $false
73
+ if ($taskEnabled) {
74
+ $taskAction = $active[0].Definition.Actions.Item(1)
75
+ $taskCurrent = $taskAction.Path -ieq $wscript -and $taskAction.Arguments -ieq $arguments
76
+ }
77
+ @{
78
+ enabled=($taskCurrent -and $legacyRegistered.Count -eq 0)
79
+ taskPresent=$taskEnabled
80
+ path=$(if ($taskEnabled) { 'Task Scheduler:' + $taskPath } elseif ($legacyRegistered.Count -gt 0) { 'Task Scheduler:\' + $legacyTaskName } elseif ($legacyExists) { $legacyPath } else { 'Task Scheduler:' + $taskPath })
81
+ scope='current-user-logon'
82
+ mechanism=$(if ($taskCurrent -and $legacyRegistered.Count -eq 0) { 'task-scheduler' } elseif ($taskEnabled -or $legacyRegistered.Count -gt 0) { 'stale-task-scheduler' } elseif ($legacyExists) { 'legacy-startup-shortcut' } else { 'task-scheduler' })
83
+ } | ConvertTo-Json -Compress
@@ -1,14 +1,14 @@
1
- param([Parameter(Mandatory=$true)][string]$StateRoot)
2
- $ErrorActionPreference='Stop'
3
- [Console]::OutputEncoding=New-Object Text.UTF8Encoding($false)
4
- Add-Type -Path (Join-Path $PSScriptRoot 'IsolatedProcess.cs')
5
- New-Item -ItemType Directory -Path $StateRoot -Force | Out-Null
6
- $anchor=Join-Path $StateRoot 'bridge-config.json'
7
- $temporary=$false
8
- if(-not(Test-Path -LiteralPath $anchor)) {
9
- $anchor=Join-Path $StateRoot ('.state-path-'+[Guid]::NewGuid().ToString()+'.tmp')
10
- Set-Content -LiteralPath $anchor -Value 'path-resolution' -Encoding ASCII
11
- $temporary=$true
12
- }
13
- try { [IO.Path]::GetDirectoryName([IsolatedProcess]::ResolvePath($anchor)) }
14
- finally { if($temporary){Remove-Item -LiteralPath $anchor} }
1
+ param([Parameter(Mandatory=$true)][string]$StateRoot)
2
+ $ErrorActionPreference='Stop'
3
+ [Console]::OutputEncoding=New-Object Text.UTF8Encoding($false)
4
+ Add-Type -Path (Join-Path $PSScriptRoot 'IsolatedProcess.cs')
5
+ New-Item -ItemType Directory -Path $StateRoot -Force | Out-Null
6
+ $anchor=Join-Path $StateRoot 'bridge-config.json'
7
+ $temporary=$false
8
+ if(-not(Test-Path -LiteralPath $anchor)) {
9
+ $anchor=Join-Path $StateRoot ('.state-path-'+[Guid]::NewGuid().ToString()+'.tmp')
10
+ Set-Content -LiteralPath $anchor -Value 'path-resolution' -Encoding ASCII
11
+ $temporary=$true
12
+ }
13
+ try { [IO.Path]::GetDirectoryName([IsolatedProcess]::ResolvePath($anchor)) }
14
+ finally { if($temporary){Remove-Item -LiteralPath $anchor} }
@@ -1,20 +1,20 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import fs from 'node:fs';
4
-
5
- // Windows host data locations. Keep existing paths and historical attachments.
6
- export function runtimePaths(environment = process.env, home = os.homedir()) {
7
- const codexDataRoot = environment.CODEX_HOME?.trim() || path.join(home, '.codex');
8
- const serviceRoot = path.join(environment.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Programs', 'CodexRemote', 'service');
9
- let installed;
10
- try { installed = JSON.parse(fs.readFileSync(path.join(serviceRoot, 'installation.json'), 'utf8').replace(/^\uFEFF/, '')); }
11
- catch (error) { if (error.code !== 'ENOENT') throw error; }
12
- return {
13
- codexDataRoot,
14
- sessionsRoot: path.join(codexDataRoot, 'sessions'),
15
- stateRoot: installed?.stateRoot || path.join(environment.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'CodexRemote'),
16
- serviceRoot,
17
- uploadsRoot: path.join(environment.ProgramData || 'C:\\ProgramData', 'CodexRemote', 'attachments'),
18
- credentialsRoot: path.join(environment.ProgramData || 'C:\\ProgramData', 'CodexRemote', 'credentials'),
19
- };
20
- }
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+
5
+ // Windows host data locations. Keep existing paths and historical attachments.
6
+ export function runtimePaths(environment = process.env, home = os.homedir()) {
7
+ const codexDataRoot = environment.CODEX_HOME?.trim() || path.join(home, '.codex');
8
+ const serviceRoot = path.join(environment.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Programs', 'CodexRemote', 'service');
9
+ let installed;
10
+ try { installed = JSON.parse(fs.readFileSync(path.join(serviceRoot, 'installation.json'), 'utf8').replace(/^\uFEFF/, '')); }
11
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
12
+ return {
13
+ codexDataRoot,
14
+ sessionsRoot: path.join(codexDataRoot, 'sessions'),
15
+ stateRoot: installed?.stateRoot || path.join(environment.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'CodexRemote'),
16
+ serviceRoot,
17
+ uploadsRoot: path.join(environment.ProgramData || 'C:\\ProgramData', 'CodexRemote', 'attachments'),
18
+ credentialsRoot: path.join(environment.ProgramData || 'C:\\ProgramData', 'CodexRemote', 'credentials'),
19
+ };
20
+ }