@ccpocket/bridge 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/websocket.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile, execFileSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { readFile, unlink } from "node:fs/promises";
3
+ import { lstat, readFile, readlink, stat, unlink } from "node:fs/promises";
4
4
  import { resolve, extname } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { WebSocketServer, WebSocket } from "ws";
@@ -148,6 +148,9 @@ export class BridgeWebSocketServer {
148
148
  debugEvents = new Map();
149
149
  notifiedPermissionToolUses = new Map();
150
150
  archiveStore;
151
+ codexProfiles = [];
152
+ defaultCodexProfile;
153
+ codexProfilesRequest = null;
151
154
  /** FCM token → push notification locale */
152
155
  tokenLocales = new Map();
153
156
  tokenPrivacyMode = new Map();
@@ -344,6 +347,7 @@ export class BridgeWebSocketServer {
344
347
  }
345
348
  handleConnection(ws) {
346
349
  // Send session list and project history on connect
350
+ void this.refreshCodexProfiles();
347
351
  this.sendSessionList(ws);
348
352
  const projects = this.projectHistory?.getProjects() ?? [];
349
353
  this.send(ws, { type: "project_history", projects });
@@ -422,6 +426,14 @@ export class BridgeWebSocketServer {
422
426
  const legacyPermissionMode = modesToLegacyPermissionMode(provider, executionMode, planMode);
423
427
  if (provider === "codex") {
424
428
  console.log(`[ws] start(codex): execution=${executionMode} plan=${planMode}`);
429
+ if (msg.profile &&
430
+ !(await this.validateCodexProfile(msg.profile, projectPath))) {
431
+ this.send(ws, {
432
+ type: "error",
433
+ message: `Codex profile not found: ${msg.profile}`,
434
+ });
435
+ break;
436
+ }
425
437
  }
426
438
  const cached = provider === "claude"
427
439
  ? this.sessionManager.getCachedCommands(projectPath)
@@ -447,6 +459,7 @@ export class BridgeWebSocketServer {
447
459
  existingWorktreePath: msg.existingWorktreePath,
448
460
  }, provider, provider === "codex"
449
461
  ? {
462
+ profile: msg.profile,
450
463
  approvalPolicy: codexApprovalPolicy ??
451
464
  normalizeCodexApprovalPolicy(executionMode === "fullAccess" ? "never" : "on-request"),
452
465
  sandboxMode: sandboxModeToInternal(msg.sandboxMode),
@@ -1535,6 +1548,14 @@ export class BridgeWebSocketServer {
1535
1548
  // Resume flow: keep past history in SessionInfo and deliver it only
1536
1549
  // via get_history(sessionId) to avoid duplicate/missed replay races.
1537
1550
  if (provider === "codex") {
1551
+ if (msg.profile &&
1552
+ !(await this.validateCodexProfile(msg.profile, resumeProjectPath))) {
1553
+ this.send(ws, {
1554
+ type: "error",
1555
+ message: `Codex profile not found: ${msg.profile}`,
1556
+ });
1557
+ break;
1558
+ }
1538
1559
  const wtMapping = this.worktreeStore.get(sessionRefId);
1539
1560
  const effectiveProjectPath = resolvePlatformPath(wtMapping?.projectPath ?? resumeProjectPath, this.platform);
1540
1561
  let worktreeOpts;
@@ -1556,6 +1577,7 @@ export class BridgeWebSocketServer {
1556
1577
  .then((pastMessages) => {
1557
1578
  const sessionId = this.sessionManager.create(effectiveProjectPath, undefined, pastMessages, worktreeOpts, "codex", {
1558
1579
  threadId: sessionRefId,
1580
+ profile: msg.profile,
1559
1581
  approvalPolicy: codexApprovalPolicy ??
1560
1582
  normalizeCodexApprovalPolicy(executionMode === "fullAccess" ? "never" : "on-request"),
1561
1583
  sandboxMode: sandboxModeToInternal(msg.sandboxMode),
@@ -1767,6 +1789,51 @@ export class BridgeWebSocketServer {
1767
1789
  });
1768
1790
  return;
1769
1791
  }
1792
+ const fileStat = await lstat(absPath);
1793
+ if (fileStat.isSymbolicLink()) {
1794
+ let targetPath = "";
1795
+ try {
1796
+ targetPath = await readlink(absPath);
1797
+ }
1798
+ catch {
1799
+ // Best effort only; the user-facing error still works without it.
1800
+ }
1801
+ let resolvedTargetStat;
1802
+ try {
1803
+ resolvedTargetStat = await stat(absPath);
1804
+ }
1805
+ catch {
1806
+ this.send(ws, {
1807
+ type: "file_content",
1808
+ filePath: msg.filePath,
1809
+ content: "",
1810
+ error: targetPath.length > 0
1811
+ ? `This symbolic link points to a missing target: ${targetPath}`
1812
+ : "This symbolic link points to a missing target.",
1813
+ });
1814
+ return;
1815
+ }
1816
+ if (resolvedTargetStat.isDirectory()) {
1817
+ this.send(ws, {
1818
+ type: "file_content",
1819
+ filePath: msg.filePath,
1820
+ content: "",
1821
+ error: targetPath.length > 0
1822
+ ? `This symbolic link points to a directory (${targetPath}). Open the target directory instead.`
1823
+ : "This symbolic link points to a directory. Open the target directory instead.",
1824
+ });
1825
+ return;
1826
+ }
1827
+ }
1828
+ else if (fileStat.isDirectory()) {
1829
+ this.send(ws, {
1830
+ type: "file_content",
1831
+ filePath: msg.filePath,
1832
+ content: "",
1833
+ error: "This path is a directory. Open a file instead.",
1834
+ });
1835
+ return;
1836
+ }
1770
1837
  const maxLines = typeof msg.maxLines === "number" && msg.maxLines > 0
1771
1838
  ? msg.maxLines
1772
1839
  : 5000;
@@ -2774,6 +2841,8 @@ export class BridgeWebSocketServer {
2774
2841
  allowedDirs: this.allowedDirs,
2775
2842
  claudeModels: CLAUDE_MODELS,
2776
2843
  codexModels: CODEX_MODELS,
2844
+ codexProfiles: this.codexProfiles,
2845
+ defaultCodexProfile: this.defaultCodexProfile,
2777
2846
  bridgeVersion: getPackageVersion(),
2778
2847
  });
2779
2848
  }
@@ -2787,6 +2856,8 @@ export class BridgeWebSocketServer {
2787
2856
  allowedDirs: this.allowedDirs,
2788
2857
  claudeModels: CLAUDE_MODELS,
2789
2858
  codexModels: CODEX_MODELS,
2859
+ codexProfiles: this.codexProfiles,
2860
+ defaultCodexProfile: this.defaultCodexProfile,
2790
2861
  bridgeVersion: getPackageVersion(),
2791
2862
  });
2792
2863
  }
@@ -2840,6 +2911,46 @@ export class BridgeWebSocketServer {
2840
2911
  archivedSessionIds: this.archiveStore.archivedIds(),
2841
2912
  });
2842
2913
  }
2914
+ async refreshCodexProfiles(projectPath) {
2915
+ if (this.codexProfilesRequest)
2916
+ return this.codexProfilesRequest;
2917
+ this.codexProfilesRequest = this.loadCodexProfiles(projectPath)
2918
+ .then(({ profiles, defaultProfile }) => {
2919
+ this.codexProfiles = profiles;
2920
+ this.defaultCodexProfile = defaultProfile;
2921
+ this.broadcastSessionList();
2922
+ })
2923
+ .catch((err) => {
2924
+ console.warn(`[ws] Failed to load Codex profiles: ${err}`);
2925
+ this.codexProfiles = [];
2926
+ this.defaultCodexProfile = undefined;
2927
+ })
2928
+ .finally(() => {
2929
+ this.codexProfilesRequest = null;
2930
+ });
2931
+ return this.codexProfilesRequest;
2932
+ }
2933
+ async loadCodexProfiles(projectPath) {
2934
+ const process = this.getActiveCodexProcess() ??
2935
+ (await this.createStandaloneCodexProcess(projectPath));
2936
+ const isStandalone = process !== this.getActiveCodexProcess();
2937
+ try {
2938
+ return await process.readProfileConfig(projectPath);
2939
+ }
2940
+ finally {
2941
+ if (isStandalone) {
2942
+ process.stop();
2943
+ }
2944
+ }
2945
+ }
2946
+ async validateCodexProfile(profile, projectPath) {
2947
+ if (!profile)
2948
+ return true;
2949
+ const snapshot = await this.loadCodexProfiles(projectPath);
2950
+ this.codexProfiles = snapshot.profiles;
2951
+ this.defaultCodexProfile = snapshot.defaultProfile;
2952
+ return snapshot.profiles.includes(profile);
2953
+ }
2843
2954
  getActiveCodexProcess() {
2844
2955
  const summary = this.sessionManager
2845
2956
  .list()