@minniexcode/codex-switch 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.AI.md CHANGED
@@ -6,8 +6,8 @@ This file summarizes the current operational contract for AI agents, automation
6
6
 
7
7
  - Package: `@minniexcode/codex-switch`
8
8
  - CLI name: `codexs`
9
- - Current repository version: `0.1.1`
10
- - Version status: stable release line
9
+ - Current repository version: `0.1.5`
10
+ - Version status: development line
11
11
  - Runtime contract target: Codex `0.134.0+`
12
12
 
13
13
  ## Product Role
@@ -22,7 +22,7 @@ Direct provider workflow:
22
22
 
23
23
  ```bash
24
24
  codexs init
25
- codexs add <provider> --model <model> --api-key <key> [--base-url <url>]
25
+ codexs add <provider> --profile <model-provider-id> --model <model> --api-key <key> [--base-url <url>]
26
26
  codexs switch <provider>
27
27
  codexs status
28
28
  codexs doctor
@@ -33,7 +33,7 @@ GitHub Copilot workflow:
33
33
  ```bash
34
34
  codexs init
35
35
  codexs login copilot
36
- codexs add <provider> --copilot --model <model>
36
+ codexs add <provider> --copilot --profile <model-provider-id> --model <model>
37
37
  codexs switch <provider>
38
38
  codexs status
39
39
  codexs doctor
@@ -130,9 +130,11 @@ Important behavioral constraints:
130
130
  - `login copilot` requires a real TTY and does not support `--json`.
131
131
  - `login copilot` currently installs the local Copilot SDK when needed, tries the bundled runtime CLI first, falls back to `PATH` when necessary, and rechecks auth readiness before reporting success.
132
132
  - `add --copilot` assumes SDK install and upstream Copilot auth are already ready.
133
+ - Non-interactive automation should pass `--profile` explicitly. In TTY mode, `add` and `edit` can prompt for missing required fields.
133
134
  - `migrate` remains interactive when provider adoption requires human input.
134
135
  - `status` is the main dual-path summary command.
135
136
  - `doctor` is the deeper repair-oriented diagnostic command.
137
+ - The current `0.1.5` line focuses on Copilot Bridge process visibility, Responses commentary/reasoning stream events, defensive SDK-event normalization, and unknown-event redaction hardening rather than command-surface expansion.
136
138
 
137
139
  ## Safety Notes
138
140
 
package/README.CN.md CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  ## 版本定位
8
8
 
9
- 当前包版本:`0.1.2`
9
+ 当前包版本:`0.1.5`
10
10
 
11
- 这是当前稳定发布线。`0.1.2` 是 Copilot runtime 修复版本,包含受管 SDK 固定版本与 Copilot 专用的 `stream_idle_timeout_ms = 300000` 投影,用于避免长 prompt 的空闲超时。
11
+ 这是当前仓库开发线。`0.1.5` 是 Copilot Bridge 过程可见性补丁,聚焦于 commentary/reasoning 流式信号、SDK 事件防御性归一化,以及未知运行态事件的更安全脱敏,同时不扩展 provider 命令面。
12
12
 
13
13
  ## 安装
14
14
 
@@ -34,7 +34,7 @@ Direct provider 主路径:
34
34
 
35
35
  ```bash
36
36
  codexs init
37
- codexs add my-provider --model gpt-5.5 --base-url https://gateway.example.com/v1 --api-key sk-xxx
37
+ codexs add my-provider --profile my-provider --model gpt-5.5 --base-url https://gateway.example.com/v1 --api-key sk-xxx
38
38
  codexs switch my-provider
39
39
  codexs status
40
40
  codexs doctor
@@ -45,7 +45,7 @@ GitHub Copilot 主路径:
45
45
  ```bash
46
46
  codexs init
47
47
  codexs login copilot
48
- codexs add copilot-main --copilot --model gpt-4.1
48
+ codexs add copilot-main --copilot --profile copilot-main --model gpt-4.1
49
49
  codexs switch copilot-main
50
50
  codexs status
51
51
  codexs doctor
@@ -56,6 +56,7 @@ codexs doctor
56
56
  - `init` 负责初始化 `codex-switch` 的 tool home 与受管状态文件。
57
57
  - `login copilot` 负责上游 Copilot onboarding 和登录可用性检查。
58
58
  - `add --copilot` 不负责替你登录,它假设上游 Copilot 已经 ready。
59
+ - 非交互调用请显式传入 `--profile`;在 TTY 模式下,`add` 和 `edit` 可以补问缺失的必填项。
59
60
  - `switch` 会把选中的 provider 投影到目标 Codex runtime 的顶层 `model` 与 `model_provider`。
60
61
  - `status` 是切换后的主读取命令。
61
62
  - `doctor` 是主诊断命令,用于解释问题和下一步修复动作。
@@ -116,8 +117,8 @@ codexs current
116
117
  codexs status
117
118
  codexs config show [profile]
118
119
  codexs config list-profiles
119
- codexs add <provider> --model <model> --api-key <key> [--base-url <url>]
120
- codexs add <provider> --copilot --model <model>
120
+ codexs add <provider> --profile <model-provider-id> --model <model> --api-key <key> [--base-url <url>]
121
+ codexs add <provider> --copilot --profile <model-provider-id> --model <model>
121
122
  codexs edit <provider>
122
123
  codexs switch <provider>
123
124
  codexs remove <provider> [--force] [--switch-to <provider>]
@@ -209,6 +210,8 @@ npm pack --dry-run
209
210
  - [Design 0.1.1](./docs/Design/codex-switch-v0.1.1-design.md)
210
211
  - [PRD 0.1.2](./docs/PRD/codex-switch-prd-v0.1.2.md)
211
212
  - [Design 0.1.2](./docs/Design/codex-switch-v0.1.2-design.md)
213
+ - [PRD 0.1.5](./docs/PRD/codex-switch-prd-v0.1.5.md)
214
+ - [Design 0.1.5](./docs/Design/codex-switch-v0.1.5-design.md)
212
215
 
213
216
  ## License
214
217
 
package/README.md CHANGED
@@ -8,9 +8,9 @@ Chinese version: [README.CN.md](./README.CN.md)
8
8
 
9
9
  ## Version
10
10
 
11
- Current package version: `0.1.3`
11
+ Current package version: `0.1.5`
12
12
 
13
- This is the current stable documentation line. `0.1.3` is the Copilot login hotfix release, repairing the managed SDK client construction against the current official Copilot SDK runtime while keeping the `stream_idle_timeout_ms = 300000` Copilot projection unchanged.
13
+ This is the current repository development line. `0.1.5` is a Copilot Bridge process-visibility patch, focused on streaming commentary/reasoning signals, defensive SDK-event normalization, and safer redaction for unknown runtime events while keeping the provider surface unchanged.
14
14
 
15
15
  ## Install
16
16
 
@@ -36,7 +36,7 @@ Direct provider workflow:
36
36
 
37
37
  ```bash
38
38
  codexs init
39
- codexs add my-provider --model gpt-5.5 --base-url https://gateway.example.com/v1 --api-key sk-xxx
39
+ codexs add my-provider --profile my-provider --model gpt-5.5 --base-url https://gateway.example.com/v1 --api-key sk-xxx
40
40
  codexs switch my-provider
41
41
  codexs status
42
42
  codexs doctor
@@ -47,7 +47,7 @@ GitHub Copilot workflow:
47
47
  ```bash
48
48
  codexs init
49
49
  codexs login copilot
50
- codexs add copilot-main --copilot --model gpt-4.1
50
+ codexs add copilot-main --copilot --profile copilot-main --model gpt-4.1
51
51
  codexs switch copilot-main
52
52
  codexs status
53
53
  codexs doctor
@@ -58,6 +58,7 @@ Notes:
58
58
  - `init` prepares the `codex-switch` tool home and managed state.
59
59
  - `login copilot` handles upstream Copilot onboarding and auth readiness.
60
60
  - `add --copilot` does not perform login for you; it assumes Copilot login is already ready.
61
+ - For non-interactive use, pass `--profile` explicitly. In TTY mode, `add` and `edit` can prompt for missing required fields.
61
62
  - Copilot support is an experimental local bridge. The managed installer defaults to `@github/copilot-sdk@1.0.2`, Copilot runtime paths require Node.js `>=20`, and runtime checks separately reject older or prerelease SDK installs while validating API shape when the client or session is used.
62
63
  - `switch` projects the selected provider into the target Codex runtime as top-level `model` plus `model_provider`.
63
64
  - `status` is the main read command after switching.
@@ -125,8 +126,8 @@ codexs current
125
126
  codexs status
126
127
  codexs config show [profile]
127
128
  codexs config list-profiles
128
- codexs add <provider> --model <model> --api-key <key> [--base-url <url>]
129
- codexs add <provider> --copilot --model <model>
129
+ codexs add <provider> --profile <model-provider-id> --model <model> --api-key <key> [--base-url <url>]
130
+ codexs add <provider> --copilot --profile <model-provider-id> --model <model>
130
131
  codexs edit <provider>
131
132
  codexs switch <provider>
132
133
  codexs remove <provider> [--force] [--switch-to <provider>]
@@ -218,8 +219,10 @@ npm pack --dry-run
218
219
  - [PRD 0.1.1](./docs/PRD/codex-switch-prd-v0.1.1.md)
219
220
  - [PRD 0.1.2](./docs/PRD/codex-switch-prd-v0.1.2.md)
220
221
  - [PRD 0.1.3](./docs/PRD/codex-switch-prd-v0.1.3.md)
222
+ - [PRD 0.1.5](./docs/PRD/codex-switch-prd-v0.1.5.md)
221
223
  - [Design 0.1.2](./docs/Design/codex-switch-v0.1.2-design.md)
222
224
  - [Design 0.1.3](./docs/Design/codex-switch-v0.1.3-design.md)
225
+ - [Design 0.1.5](./docs/Design/codex-switch-v0.1.5-design.md)
223
226
 
224
227
  ## License
225
228
 
@@ -58,6 +58,9 @@ async function startBridge(args) {
58
58
  port: bridge.port,
59
59
  reused: bridge.reused,
60
60
  portChanged: bridge.portChanged,
61
+ replaced: bridge.replaced,
62
+ restartReason: bridge.restartReason ?? null,
63
+ logPath: bridge.logPath,
61
64
  defaultPort: DEFAULT_BRIDGE_PORT,
62
65
  },
63
66
  };
@@ -74,6 +77,8 @@ async function stopBridge(args) {
74
77
  provider: null,
75
78
  stopped: true,
76
79
  hadRuntimeState: false,
80
+ logPath: null,
81
+ lastRestartReason: null,
77
82
  },
78
83
  };
79
84
  }
@@ -84,6 +89,8 @@ async function stopBridge(args) {
84
89
  provider: args.providerName,
85
90
  stopped: true,
86
91
  hadRuntimeState: false,
92
+ logPath: null,
93
+ lastRestartReason: null,
87
94
  },
88
95
  };
89
96
  }
@@ -109,6 +116,8 @@ async function stopBridge(args) {
109
116
  provider: target.providerName,
110
117
  stopped: true,
111
118
  hadRuntimeState: Boolean(state),
119
+ logPath: state?.logPath ?? null,
120
+ lastRestartReason: state?.lastRestartReason ?? null,
112
121
  },
113
122
  };
114
123
  }
@@ -146,6 +155,8 @@ async function statusBridge(args) {
146
155
  matches: Boolean(state && state.provider === target.providerName && state.baseUrl === expectedBaseUrl),
147
156
  active: runtimeStatus.ok,
148
157
  health: runtimeStatus,
158
+ logPath: state?.logPath ?? null,
159
+ lastRestartReason: state?.lastRestartReason ?? null,
149
160
  },
150
161
  };
151
162
  }
@@ -88,6 +88,9 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
88
88
  runtime: "copilot-bridge",
89
89
  reason: "failed",
90
90
  cause: runtimeStateInspection.parseError ?? "Failed to parse Copilot bridge runtime state.",
91
+ details: {
92
+ logPath: runtimeState?.logPath ?? null,
93
+ },
91
94
  }
92
95
  : bridgeProbeTarget
93
96
  ? await (0, copilot_bridge_1.probeCopilotBridgeRuntime)(bridgeProbeTarget, runtimeState, options?.runtimeDir)
@@ -97,7 +100,10 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
97
100
  runtime: "copilot-bridge",
98
101
  reason: "failed",
99
102
  cause: "Copilot bridge runtime state exists but no matching managed Copilot provider is active.",
100
- details: runtimeState,
103
+ details: {
104
+ ...runtimeState,
105
+ logPath: runtimeState.logPath ?? null,
106
+ },
101
107
  }
102
108
  : null;
103
109
  const copilotAuth = activeProvider && (0, providers_1.isCopilotBridgeProvider)(activeProvider)
@@ -148,6 +154,8 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
148
154
  copilotAuth,
149
155
  copilotBridge,
150
156
  copilotRuntimeState: runtimeState,
157
+ copilotBridgeLogPath: runtimeState?.logPath ?? null,
158
+ copilotBridgeRestartReason: runtimeState?.lastRestartReason ?? null,
151
159
  liveState,
152
160
  auth: authState,
153
161
  configProfiles: configViews,
@@ -117,6 +117,7 @@ async function runDoctor(args) {
117
117
  issues.push({
118
118
  code: "BRIDGE_STATE_STALE",
119
119
  message: `Copilot bridge runtime state is unreadable: ${runtimeStateInspection.parseError ?? "unknown parse failure"}`,
120
+ logPath: runtimeState?.logPath ?? null,
120
121
  });
121
122
  }
122
123
  if (document?.currentModelProvider && providers) {
@@ -209,6 +210,7 @@ async function runDoctor(args) {
209
210
  }),
210
211
  liveState: drift,
211
212
  auth: authState,
213
+ copilotRuntimeState: runtimeState,
212
214
  },
213
215
  warnings: issues.length === 0 ? [] : [`doctor found ${issues.length} issue(s)`],
214
216
  };
@@ -22,7 +22,9 @@ async function switchProvider(args) {
22
22
  });
23
23
  }
24
24
  const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
25
- const resolvedModel = provider.model ?? document.currentModel;
25
+ const providerProfileSection = document.profiles.find((entry) => entry.name === provider.profile) ?? null;
26
+ const providerModelProviderSection = document.modelProviders.find((entry) => entry.name === provider.profile) ?? null;
27
+ const resolvedModel = provider.model ?? providerProfileSection?.model ?? document.currentModel;
26
28
  if (!resolvedModel) {
27
29
  throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${args.providerName}" has no model to switch with.`, {
28
30
  provider: args.providerName,
@@ -90,6 +92,10 @@ async function switchProvider(args) {
90
92
  profile: nextProvider.profile,
91
93
  portChanged: bridge.portChanged,
92
94
  bridgePort: bridge.port,
95
+ bridgeReused: bridge.reused,
96
+ bridgeReplaced: bridge.replaced,
97
+ bridgeRestartReason: bridge.restartReason ?? null,
98
+ bridgeLogPath: bridge.logPath,
93
99
  };
94
100
  },
95
101
  });
@@ -112,7 +118,8 @@ async function switchProvider(args) {
112
118
  ],
113
119
  mutate: () => {
114
120
  const directBaseUrl = provider.baseUrl?.trim() ?? "";
115
- if (!directBaseUrl) {
121
+ const resolvedBaseUrl = directBaseUrl || providerModelProviderSection?.baseUrl?.trim() || "";
122
+ if (!resolvedBaseUrl) {
116
123
  throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${args.providerName}" requires base_url before switching.`, {
117
124
  provider: args.providerName,
118
125
  modelProvider: provider.profile,
@@ -123,7 +130,7 @@ async function switchProvider(args) {
123
130
  setCurrentModel: resolvedModel,
124
131
  setCurrentModelProvider: provider.profile,
125
132
  upsertModelProviders: {
126
- [provider.profile]: (0, providers_1.buildDirectModelProviderProjection)(provider.profile, directBaseUrl),
133
+ [provider.profile]: (0, providers_1.buildDirectModelProviderProjection)(provider.profile, resolvedBaseUrl),
127
134
  },
128
135
  deleteLegacyProfile: true,
129
136
  deleteLegacyProfilesByName: [provider.profile],
@@ -144,6 +144,12 @@ function renderHumanSuccess(command, data, warnings) {
144
144
  lines.push(` mapped provider: ${renderStatusMappedProvider(data)}`);
145
145
  lines.push(` provider path: ${renderStatusProviderPath(data)}`);
146
146
  lines.push(` runtime health: ${renderStatusHealth(data)}`);
147
+ if (data?.copilotBridgeLogPath) {
148
+ lines.push(` bridge log: ${String(data.copilotBridgeLogPath)}`);
149
+ }
150
+ if (data?.copilotBridgeRestartReason) {
151
+ lines.push(` bridge restart reason: ${String(data.copilotBridgeRestartReason)}`);
152
+ }
147
153
  lines.push(` warnings: ${warnings.length}`);
148
154
  lines.push(` next step: ${renderStatusNextStep(data, warnings)}`);
149
155
  break;
@@ -167,6 +173,15 @@ function renderHumanSuccess(command, data, warnings) {
167
173
  case "switch":
168
174
  lines.push(`Switched to provider ${String(data?.provider ?? "")} using model provider ${String(data?.modelProvider ?? data?.profile ?? "")}.`);
169
175
  lines.push(`Model: ${String(data?.model ?? "")}`);
176
+ if (data?.bridgeReplaced) {
177
+ lines.push(`Bridge replaced: true`);
178
+ }
179
+ if (data?.bridgeRestartReason) {
180
+ lines.push(`Bridge restart reason: ${String(data?.bridgeRestartReason)}`);
181
+ }
182
+ if (data?.bridgeLogPath) {
183
+ lines.push(`Bridge log: ${String(data?.bridgeLogPath)}`);
184
+ }
170
185
  lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
171
186
  break;
172
187
  case "import":
@@ -227,12 +242,49 @@ function renderHumanSuccess(command, data, warnings) {
227
242
  const issues = data?.issues ?? [];
228
243
  lines.push(healthy ? "Doctor summary: healthy. No action required." : `Doctor summary: ${issues.length} issue(s) need attention.`);
229
244
  lines.push(`target runtime: ${String(data?.codexDir ?? "")}`);
245
+ if (data?.copilotRuntimeState && data.copilotRuntimeState.logPath) {
246
+ lines.push(`bridge log: ${String(data.copilotRuntimeState.logPath)}`);
247
+ }
230
248
  for (const issue of issues) {
231
249
  lines.push(`- ${String(issue.code)}: ${String(issue.message)}`);
232
250
  lines.push(` next step: ${renderDoctorIssueNextStep(issue)}`);
233
251
  }
234
252
  break;
235
253
  }
254
+ case "bridge-start":
255
+ lines.push(`Bridge ready for provider ${String(data?.provider ?? "")}.`);
256
+ lines.push(`Endpoint: ${String(data?.baseUrl ?? "")}`);
257
+ lines.push(`Reused: ${String(data?.reused ?? false)}`);
258
+ lines.push(`Replaced existing: ${String(data?.replaced ?? false)}`);
259
+ if (data?.restartReason) {
260
+ lines.push(`Restart reason: ${String(data?.restartReason)}`);
261
+ }
262
+ if (data?.logPath) {
263
+ lines.push(`Bridge log: ${String(data?.logPath)}`);
264
+ }
265
+ break;
266
+ case "bridge-status":
267
+ lines.push(`Bridge provider: ${String(data?.provider ?? "")}`);
268
+ lines.push(`Active: ${String(data?.active ?? false)}`);
269
+ lines.push(`Expected base URL: ${String(data?.expectedBaseUrl ?? "")}`);
270
+ lines.push(`Matches runtime state: ${String(data?.matches ?? false)}`);
271
+ if (data?.lastRestartReason) {
272
+ lines.push(`Last restart reason: ${String(data?.lastRestartReason)}`);
273
+ }
274
+ if (data?.logPath) {
275
+ lines.push(`Bridge log: ${String(data?.logPath)}`);
276
+ }
277
+ break;
278
+ case "bridge-stop":
279
+ lines.push(`Bridge stopped for provider ${String(data?.provider ?? "(none)")}.`);
280
+ lines.push(`Had runtime state: ${String(data?.hadRuntimeState ?? false)}`);
281
+ if (data?.lastRestartReason) {
282
+ lines.push(`Last restart reason: ${String(data?.lastRestartReason)}`);
283
+ }
284
+ if (data?.logPath) {
285
+ lines.push(`Bridge log: ${String(data?.logPath)}`);
286
+ }
287
+ break;
236
288
  case "backups-list": {
237
289
  const backups = data?.backups ?? [];
238
290
  for (const backup of backups) {
@@ -344,7 +396,7 @@ function renderDoctorIssueNextStep(issue) {
344
396
  case "BRIDGE_STATE_STALE":
345
397
  case "BRIDGE_STATE_MISSING":
346
398
  case "BRIDGE_HEALTHCHECK_FAILED":
347
- return "reselect the provider with `codexs switch <provider>` or inspect bridge state";
399
+ return "reselect the provider with `codexs switch <provider>` or inspect the bridge log/state";
348
400
  case "UNMANAGED_ACTIVE_PROFILE":
349
401
  return "switch to a managed provider or adopt the active route with `codexs migrate`";
350
402
  case "LEGACY_PROFILE_SELECTOR":
@@ -70,18 +70,25 @@ function canPrompt(runtime, jsonMode) {
70
70
  */
71
71
  async function promptForProviderSelection(runtime, providersPath, configPath, message) {
72
72
  const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
73
- const currentModelProvider = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath).currentModelProvider : null;
73
+ const document = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath) : null;
74
+ const currentModelProvider = document?.currentModelProvider ?? null;
74
75
  const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentModelProvider, providers);
76
+ const legacyCurrentProvider = !liveState.providerResolvable &&
77
+ document?.legacyProfile &&
78
+ providers.providers[document.legacyProfile]
79
+ ? document.legacyProfile
80
+ : null;
75
81
  const choices = Object.entries(providers.providers)
76
82
  .sort(([left], [right]) => left.localeCompare(right))
77
83
  .map(([providerName, provider]) => {
78
84
  const providerType = (0, providers_1.isCopilotBridgeProvider)(provider) ? "copilot" : "direct";
79
85
  const currentMarker = liveState.providerResolvable && liveState.mappedProvider === providerName ? " | current" : "";
86
+ const legacyMarker = !currentMarker && legacyCurrentProvider === providerName ? " | current" : "";
80
87
  const ambiguousMarker = !liveState.providerResolvable && liveState.mappedProviders.includes(providerName) ? " | current=ambiguous" : "";
81
88
  return {
82
89
  value: providerName,
83
90
  label: providerName,
84
- hint: `profile=${provider.profile} | type=${providerType}${currentMarker}${ambiguousMarker}`,
91
+ hint: `profile=${provider.profile} | type=${providerType}${currentMarker}${legacyMarker}${ambiguousMarker}`,
85
92
  };
86
93
  });
87
94
  if (choices.length === 0) {
@@ -174,6 +174,7 @@ async function createCopilotSession(runtimeClient, payload) {
174
174
  try {
175
175
  const session = await Promise.resolve(createSession({
176
176
  model: typeof payload.model === "string" ? payload.model : undefined,
177
+ streaming: true,
177
178
  ...createSessionOptions(runtimeClient.sdk),
178
179
  }));
179
180
  if (!session || typeof session !== "object") {
@@ -208,7 +209,16 @@ async function sendSessionRequest(session, prompt, timeoutMs, onStreamEvent) {
208
209
  onStreamEvent?.({ type: "delta", delta });
209
210
  }
210
211
  };
212
+ const runtimeHandler = (event) => {
213
+ for (const runtimeEvent of mapCopilotRuntimeEvent(event)) {
214
+ onStreamEvent?.({ type: "runtime", event: runtimeEvent });
215
+ if (runtimeEvent.type === "assistant.message_delta" && runtimeEvent.text.length > 0) {
216
+ onStreamEvent?.({ type: "delta", delta: runtimeEvent.text });
217
+ }
218
+ }
219
+ };
211
220
  if (onStreamEvent && session.on) {
221
+ session.on("event", runtimeHandler);
212
222
  session.on("data", deltaHandler);
213
223
  session.on("message", deltaHandler);
214
224
  session.on("delta", deltaHandler);
@@ -225,6 +235,7 @@ async function sendSessionRequest(session, prompt, timeoutMs, onStreamEvent) {
225
235
  }
226
236
  finally {
227
237
  if (onStreamEvent && session.off) {
238
+ session.off("event", runtimeHandler);
228
239
  session.off("data", deltaHandler);
229
240
  session.off("message", deltaHandler);
230
241
  session.off("delta", deltaHandler);
@@ -395,6 +406,115 @@ function extractDelta(event) {
395
406
  }
396
407
  return null;
397
408
  }
409
+ /**
410
+ * Maps SDK session events into the bridge's stable process-event contract.
411
+ */
412
+ function mapCopilotRuntimeEvent(event) {
413
+ if (!event || typeof event !== "object") {
414
+ return [];
415
+ }
416
+ const record = event;
417
+ const sdkType = readString(record, ["type", "event", "name", "eventName"]) ?? "unknown";
418
+ const normalizedType = sdkType.replace(/_/g, ".").toLowerCase();
419
+ const text = readString(record, ["text", "delta", "content", "message", "summary", "description"]);
420
+ const name = readString(record, ["toolName", "tool", "name"]);
421
+ const requestId = readString(record, ["requestId", "id", "callId"]);
422
+ const kind = readString(record, ["kind", "permission", "permissionKind"]);
423
+ const success = readBoolean(record, ["success", "ok"]);
424
+ const approved = readBoolean(record, ["approved", "allowed", "accepted"]);
425
+ const summary = truncateForBridgeLog(text ?? summarizeUnknownObject(record), 600);
426
+ if (normalizedType === "assistant.intent") {
427
+ return [{ type: "assistant.intent", text: summary }];
428
+ }
429
+ if (normalizedType === "assistant.message.delta" || normalizedType === "assistant.message_delta") {
430
+ return [{ type: "assistant.message_delta", text: text ?? "" }];
431
+ }
432
+ if (normalizedType === "assistant.reasoning.delta" || normalizedType === "assistant.reasoning_delta" || normalizedType === "reasoning.delta") {
433
+ return [{ type: "assistant.reasoning_delta", text: summary }];
434
+ }
435
+ if (normalizedType === "tool.execution.start" || normalizedType === "tool.execution_start") {
436
+ return [{ type: "tool.execution_start", name, requestId, summary: summary || `Tool started: ${name ?? "unknown"}` }];
437
+ }
438
+ if (normalizedType === "tool.execution.progress" || normalizedType === "tool.execution_progress") {
439
+ return [{ type: "tool.execution_progress", name, requestId, summary }];
440
+ }
441
+ if (normalizedType === "tool.execution.partial.result" || normalizedType === "tool.execution.partial_result") {
442
+ return [{ type: "tool.execution_partial_result", name, requestId, summary }];
443
+ }
444
+ if (normalizedType === "tool.execution.complete" || normalizedType === "tool.execution_complete") {
445
+ return [{ type: "tool.execution_complete", name, requestId, success, summary: summary || `Tool completed: ${name ?? "unknown"}` }];
446
+ }
447
+ if (normalizedType === "permission.requested" || normalizedType === "permission.request") {
448
+ return [{ type: "permission.requested", kind, requestId, summary: summary || `Copilot requested permission: ${kind ?? "unknown"}` }];
449
+ }
450
+ if (normalizedType === "permission.completed" || normalizedType === "permission.complete") {
451
+ return [{ type: "permission.completed", kind, requestId, approved, summary }];
452
+ }
453
+ if (normalizedType === "user.input.requested" || normalizedType === "user.input_request" || normalizedType === "user_input.requested") {
454
+ return [{ type: "user_input.requested", requestId, summary }];
455
+ }
456
+ if (normalizedType === "exit.plan.mode.requested" || normalizedType === "exit_plan_mode.requested") {
457
+ return [{ type: "exit_plan_mode.requested", requestId, summary }];
458
+ }
459
+ if (normalizedType === "session.error" || normalizedType === "error") {
460
+ return [{ type: "session.error", summary }];
461
+ }
462
+ if (normalizedType === "session.idle" || normalizedType === "idle") {
463
+ return [{ type: "session.idle", summary: summary || "Copilot session is idle." }];
464
+ }
465
+ return [{ type: "session.unknown", sdkType, summary }];
466
+ }
467
+ function readString(record, keys) {
468
+ for (const key of keys) {
469
+ const value = record[key];
470
+ if (typeof value === "string" && value.length > 0) {
471
+ return value;
472
+ }
473
+ if (value && typeof value === "object") {
474
+ const nested = value;
475
+ for (const nestedKey of ["name", "id", "text", "content", "message", "summary"]) {
476
+ if (typeof nested[nestedKey] === "string" && nested[nestedKey].length > 0) {
477
+ return nested[nestedKey];
478
+ }
479
+ }
480
+ }
481
+ }
482
+ return undefined;
483
+ }
484
+ function readBoolean(record, keys) {
485
+ for (const key of keys) {
486
+ if (typeof record[key] === "boolean") {
487
+ return record[key];
488
+ }
489
+ }
490
+ return undefined;
491
+ }
492
+ function summarizeUnknownObject(record) {
493
+ return JSON.stringify(record, (key, value) => {
494
+ if (isSensitiveKey(key)) {
495
+ return "[redacted]";
496
+ }
497
+ if (typeof value === "string") {
498
+ return redactSensitiveText(value);
499
+ }
500
+ return value;
501
+ });
502
+ }
503
+ function redactSensitiveText(value) {
504
+ if (/api[_-]?key|token|authorization|bearer\s+|sk-[a-z0-9_-]+/i.test(value)) {
505
+ return "[redacted]";
506
+ }
507
+ return value;
508
+ }
509
+ function isSensitiveKey(key) {
510
+ return /^(api[_-]?key|token|access[_-]?token|refresh[_-]?token|authorization|secret|password)$/i.test(key);
511
+ }
512
+ function truncateForBridgeLog(value, maxLength) {
513
+ if (value.length <= maxLength) {
514
+ return value;
515
+ }
516
+ return `${value.slice(0, maxLength)}... [truncated]`;
517
+ }
398
518
  function isAuthReady(status) {
399
519
  if (status === true) {
400
520
  return true;
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const copilot_bridge_1 = require("./copilot-bridge");
4
4
  const copilot_adapter_1 = require("./copilot-adapter");
5
5
  let requestQueue = Promise.resolve();
6
+ /**
7
+ * Writes one worker lifecycle entry to stderr so the detached parent log capture persists it.
8
+ */
9
+ function logWorkerEvent(message) {
10
+ process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
11
+ }
6
12
  function enqueueRequest(task) {
7
13
  const run = requestQueue.then(task, task);
8
14
  requestQueue = run.catch(() => undefined);
@@ -14,9 +20,11 @@ async function main() {
14
20
  const port = Number(process.env.CODEX_SWITCH_BRIDGE_PORT ?? "41415");
15
21
  const apiKey = process.env.CODEX_SWITCH_BRIDGE_API_KEY ?? "";
16
22
  const runtimesDir = process.env.CODEX_SWITCH_RUNTIMES_DIR || undefined;
23
+ logWorkerEvent(`worker startup provider=${provider} host=${host} port=${String(port)}`);
17
24
  const runtimeClient = await (0, copilot_adapter_1.createCopilotRuntimeClient)(runtimesDir);
18
25
  await (0, copilot_adapter_1.startCopilotRuntimeClient)(runtimeClient);
19
26
  const stopRuntime = () => {
27
+ logWorkerEvent(`worker shutdown provider=${provider}`);
20
28
  void (0, copilot_adapter_1.stopCopilotRuntimeClient)(runtimeClient).finally(() => process.exit(0));
21
29
  };
22
30
  process.once("SIGINT", stopRuntime);
@@ -35,15 +43,26 @@ async function main() {
35
43
  if (event.type === "delta") {
36
44
  options?.onTextDelta?.(event.delta);
37
45
  }
46
+ else if (event.type === "runtime") {
47
+ options?.onRuntimeEvent?.(event.event);
48
+ }
38
49
  else {
39
50
  options?.onTextDone?.(event.text);
40
51
  }
41
52
  },
42
53
  })),
43
54
  });
55
+ logWorkerEvent(`worker ready provider=${provider} host=${host} port=${String(port)}`);
44
56
  }
45
57
  if (require.main === module) {
58
+ process.on("uncaughtException", (error) => {
59
+ logWorkerEvent(`worker uncaught exception: ${error.message}`);
60
+ });
61
+ process.on("unhandledRejection", (reason) => {
62
+ logWorkerEvent(`worker unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
63
+ });
46
64
  void main().catch((error) => {
65
+ logWorkerEvent(`worker startup failure: ${error instanceof Error ? error.message : String(error)}`);
47
66
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
48
67
  process.exit(1);
49
68
  });