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

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 (187) 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 +63 -58
  7. package/dist/bin/publish-harmony-release.mjs +46 -0
  8. package/dist/bin/release-upload-server.mjs +174 -0
  9. package/dist/bin/self-relay-admin.mjs +8 -8
  10. package/dist/bin/self-relay-demo.mjs +33 -33
  11. package/dist/bin/self-relay-server.mjs +66 -66
  12. package/dist/build-platform.json +1 -1
  13. package/dist/get-lan-networks.ps1 +40 -40
  14. package/dist/get-shared-service-url.ps1 +6 -6
  15. package/dist/shared/app-server-events.cjs +422 -412
  16. package/dist/shared/code-browser.cjs +159 -159
  17. package/dist/shared/file-changes.cjs +31 -31
  18. package/dist/shared/file-citations.cjs +117 -117
  19. package/dist/shared/file-mentions.cjs +92 -92
  20. package/dist/shared/message-delivery.cjs +54 -54
  21. package/dist/shared/p2p-data-channel.cjs +72 -72
  22. package/dist/shared/p2p-diagnostics.cjs +28 -28
  23. package/dist/shared/p2p-file-channel.cjs +78 -74
  24. package/dist/shared/plan-pricing.cjs +32 -32
  25. package/dist/shared/question-attention.cjs +69 -0
  26. package/dist/shared/server-requests.cjs +219 -219
  27. package/dist/shared/timeline-sync.cjs +173 -173
  28. package/dist/skills/code-relax/SKILL.md +22 -22
  29. package/dist/src/archived-threads.mjs +22 -0
  30. package/dist/src/background-process.mjs +18 -18
  31. package/dist/src/bounded-process-log.mjs +63 -63
  32. package/dist/src/celsius-client.mjs +107 -107
  33. package/dist/src/code-browser.mjs +100 -100
  34. package/dist/src/desktop-launcher.mjs +46 -46
  35. package/dist/src/dots-approval-timeline.mjs +99 -99
  36. package/dist/src/dots-avatar.mjs +42 -42
  37. package/dist/src/dots-chat-projection.mjs +50 -50
  38. package/dist/src/dots-chat-server.mjs +461 -461
  39. package/dist/src/dots-message-images.mjs +319 -319
  40. package/dist/src/dots-profile.mjs +77 -77
  41. package/dist/src/dots-room-activity.mjs +94 -94
  42. package/dist/src/dots-room-client.mjs +135 -135
  43. package/dist/src/dots-room-projection.mjs +94 -94
  44. package/dist/src/dots-schedules.mjs +66 -66
  45. package/dist/src/dots-summary-tasks.mjs +32 -32
  46. package/dist/src/dots-task-catalog.mjs +430 -430
  47. package/dist/src/dots-task-files.mjs +52 -52
  48. package/dist/src/execution-health.mjs +26 -26
  49. package/dist/src/file-transfer-range.mjs +22 -22
  50. package/dist/src/file-upload-session.mjs +130 -130
  51. package/dist/src/hosted-history-app-server.mjs +103 -103
  52. package/dist/src/hosted-history-cache.mjs +113 -113
  53. package/dist/src/lan-socket-server.mjs +265 -263
  54. package/dist/src/managed-app-server.mjs +244 -244
  55. package/dist/src/message-images.mjs +9 -9
  56. package/dist/src/message-upload-files.mjs +74 -74
  57. package/dist/src/onboarding.mjs +12 -12
  58. package/dist/src/plan-pricing.mjs +47 -47
  59. package/dist/src/platform/README.md +12 -12
  60. package/dist/src/platform/codex-protocol.mjs +46 -46
  61. package/dist/src/platform/codex-runtime.mjs +3 -3
  62. package/dist/src/platform/credential-store.mjs +3 -3
  63. package/dist/src/platform/desktop-host.mjs +3 -3
  64. package/dist/src/platform/desktop-project-registration.mjs +3 -3
  65. package/dist/src/platform/desktop-project-store.mjs +208 -149
  66. package/dist/src/platform/file-opener.mjs +9 -0
  67. package/dist/src/platform/linux/README.md +3 -3
  68. package/dist/src/platform/linux/codex-runtime.mjs +56 -0
  69. package/dist/src/platform/linux/credential-store.mjs +28 -0
  70. package/dist/src/platform/linux/desktop-host.mjs +83 -0
  71. package/dist/src/platform/linux/desktop-installation.mjs +36 -0
  72. package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
  73. package/dist/src/platform/linux/network-info.mjs +11 -0
  74. package/dist/src/platform/linux/process.mjs +42 -0
  75. package/dist/src/platform/linux/project-locations.mjs +17 -0
  76. package/dist/src/platform/linux/resident-startup.mjs +52 -0
  77. package/dist/src/platform/linux/retained-runtime.mjs +76 -0
  78. package/dist/src/platform/linux/service-host.mjs +18 -0
  79. package/dist/src/platform/linux/system.mjs +49 -0
  80. package/dist/src/platform/linux/user-environment.mjs +7 -0
  81. package/dist/src/platform/macos/README.md +3 -3
  82. package/dist/src/platform/macos/codex-runtime.mjs +125 -125
  83. package/dist/src/platform/macos/credential-store.mjs +10 -10
  84. package/dist/src/platform/macos/desktop-host.mjs +82 -82
  85. package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
  86. package/dist/src/platform/macos/network-info.mjs +8 -28
  87. package/dist/src/platform/macos/project-locations.mjs +8 -8
  88. package/dist/src/platform/macos/resident-startup.mjs +84 -84
  89. package/dist/src/platform/macos/retained-runtime.mjs +127 -127
  90. package/dist/src/platform/macos/service-host.mjs +18 -18
  91. package/dist/src/platform/macos/system.mjs +29 -29
  92. package/dist/src/platform/macos/user-environment.mjs +7 -7
  93. package/dist/src/platform/native-package.mjs +58 -46
  94. package/dist/src/platform/network-info.mjs +3 -3
  95. package/dist/src/platform/path-identity.mjs +5 -5
  96. package/dist/src/platform/posix/network-candidates.mjs +23 -0
  97. package/dist/src/platform/posix/runtime-paths.mjs +25 -25
  98. package/dist/src/platform/project-locations.mjs +3 -3
  99. package/dist/src/platform/runtime-paths.mjs +8 -8
  100. package/dist/src/platform/select.mjs +5 -5
  101. package/dist/src/platform/service-host.mjs +3 -3
  102. package/dist/src/platform/user-environment.mjs +3 -3
  103. package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
  104. package/dist/src/platform/windows/activate-desktop.ps1 +48 -48
  105. package/dist/src/platform/windows/background-process.mjs +30 -30
  106. package/dist/src/platform/windows/codex-runtime.mjs +215 -215
  107. package/dist/src/platform/windows/credential-store.mjs +27 -27
  108. package/dist/src/platform/windows/credential-store.ps1 +49 -49
  109. package/dist/src/platform/windows/desktop-helper.mjs +179 -179
  110. package/dist/src/platform/windows/desktop-host.mjs +58 -58
  111. package/dist/src/platform/windows/desktop-info.ps1 +14 -14
  112. package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
  113. package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
  114. package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
  115. package/dist/src/platform/windows/dots-profile.mjs +49 -49
  116. package/dist/src/platform/windows/launch-worker.mjs +41 -41
  117. package/dist/src/platform/windows/network-info.mjs +34 -34
  118. package/dist/src/platform/windows/project-locations.mjs +16 -16
  119. package/dist/src/platform/windows/resident-start.js +20 -20
  120. package/dist/src/platform/windows/resident-start.ps1 +7 -7
  121. package/dist/src/platform/windows/resident-startup.ps1 +83 -83
  122. package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
  123. package/dist/src/platform/windows/runtime-paths.mjs +20 -20
  124. package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
  125. package/dist/src/platform/windows/service-host.mjs +31 -31
  126. package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
  127. package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
  128. package/dist/src/platform/windows/user-environment.mjs +50 -50
  129. package/dist/src/platform/windows/user-environment.ps1 +41 -41
  130. package/dist/src/project-archive.mjs +30 -0
  131. package/dist/src/project-create.mjs +121 -119
  132. package/dist/src/project-management.mjs +59 -69
  133. package/dist/src/project-remove.mjs +55 -0
  134. package/dist/src/project-update.mjs +56 -0
  135. package/dist/src/runtime-discovery.mjs +47 -47
  136. package/dist/src/self-relay/account-cleanup.mjs +30 -30
  137. package/dist/src/self-relay/account-connections.mjs +52 -52
  138. package/dist/src/self-relay/admin-cloud.mjs +101 -101
  139. package/dist/src/self-relay/admin-page.mjs +80 -80
  140. package/dist/src/self-relay/admin-server.mjs +74 -74
  141. package/dist/src/self-relay/admin-state.mjs +127 -127
  142. package/dist/src/self-relay/admin-style.mjs +14 -14
  143. package/dist/src/self-relay/android-releases.mjs +61 -61
  144. package/dist/src/self-relay/client.mjs +61 -61
  145. package/dist/src/self-relay/connector.mjs +439 -438
  146. package/dist/src/self-relay/debug-allowlist.mjs +33 -33
  147. package/dist/src/self-relay/demo.mjs +37 -37
  148. package/dist/src/self-relay/device-info.mjs +27 -0
  149. package/dist/src/self-relay/device-store.mjs +92 -84
  150. package/dist/src/self-relay/diagnostics.mjs +54 -54
  151. package/dist/src/self-relay/email-accounts.mjs +183 -183
  152. package/dist/src/self-relay/file-wire.mjs +13 -13
  153. package/dist/src/self-relay/lan-authorizations.mjs +71 -42
  154. package/dist/src/self-relay/lifecycle.mjs +264 -250
  155. package/dist/src/self-relay/long-pairing.mjs +40 -40
  156. package/dist/src/self-relay/mysql-state.mjs +118 -105
  157. package/dist/src/self-relay/p2p-probe.mjs +159 -159
  158. package/dist/src/self-relay/privacy-page.mjs +26 -26
  159. package/dist/src/self-relay/public-releases.mjs +52 -0
  160. package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
  161. package/dist/src/self-relay/server.mjs +1006 -932
  162. package/dist/src/self-relay/usage-daily.mjs +55 -55
  163. package/dist/src/self-relay/usage-meter.mjs +141 -141
  164. package/dist/src/self-relay/windows-route-helper.mjs +75 -75
  165. package/dist/src/self-relay/wire.mjs +36 -36
  166. package/dist/src/server.mjs +1861 -1778
  167. package/dist/src/service-control.mjs +82 -82
  168. package/dist/src/service-doctor.mjs +110 -110
  169. package/dist/src/service-health.mjs +33 -33
  170. package/dist/src/service-install.mjs +91 -91
  171. package/dist/src/service-lifecycle.mjs +138 -138
  172. package/dist/src/service-restart.mjs +81 -81
  173. package/dist/src/shared-app-server.mjs +615 -611
  174. package/dist/src/shared-catalog.mjs +132 -132
  175. package/dist/src/shared-recovery.mjs +146 -146
  176. package/dist/src/shared-thread-stream.mjs +164 -164
  177. package/dist/src/skill-install.mjs +51 -51
  178. package/dist/src/thread-title-generation.mjs +150 -150
  179. package/dist/src/turn-model-history.mjs +47 -47
  180. package/dist/src/usage-reset.mjs +17 -17
  181. package/dist/src/workstation-downloads.mjs +120 -120
  182. package/dist/tools/rtc-route-helper.ps1 +99 -99
  183. package/dist/tools/start-rtc-route-helper.ps1 +15 -15
  184. package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
  185. package/dist/web/vendor/markdown-it.umd.min.js +13 -13
  186. package/package.json +7 -6
  187. 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
+ }