@minniexcode/codex-switch 0.0.10 → 0.0.12

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 (51) hide show
  1. package/README.AI.md +68 -73
  2. package/README.CN.md +108 -111
  3. package/README.md +87 -80
  4. package/dist/app/add-provider.js +29 -15
  5. package/dist/app/bridge.js +15 -14
  6. package/dist/app/edit-provider.js +1 -1
  7. package/dist/app/get-status.js +21 -9
  8. package/dist/app/import-providers.js +1 -1
  9. package/dist/app/init-codex.js +13 -14
  10. package/dist/app/list-providers.js +48 -1
  11. package/dist/app/remove-provider.js +1 -1
  12. package/dist/app/run-doctor.js +12 -5
  13. package/dist/app/run-mutation.js +3 -2
  14. package/dist/app/setup-codex.js +3 -1
  15. package/dist/app/switch-provider.js +11 -13
  16. package/dist/cli/output.js +145 -18
  17. package/dist/cli.js +34 -2
  18. package/dist/commands/args.js +2 -2
  19. package/dist/commands/dispatch.js +40 -0
  20. package/dist/commands/handlers.js +130 -161
  21. package/dist/commands/help.js +11 -5
  22. package/dist/commands/registry.js +42 -20
  23. package/dist/domain/backups.js +4 -4
  24. package/dist/domain/config.js +110 -5
  25. package/dist/domain/providers.js +12 -0
  26. package/dist/domain/runtime-state.js +111 -13
  27. package/dist/infra/config-repo.js +16 -206
  28. package/dist/interaction/interactive.js +16 -6
  29. package/dist/runtime/copilot-adapter.js +12 -12
  30. package/dist/runtime/copilot-bridge.js +394 -45
  31. package/dist/runtime/copilot-cli.js +84 -12
  32. package/dist/runtime/copilot-installer.js +10 -9
  33. package/dist/runtime/copilot-sdk-loader.js +5 -5
  34. package/dist/storage/backup-repo.js +4 -4
  35. package/dist/storage/codex-paths.js +34 -8
  36. package/dist/storage/config-repo.js +0 -23
  37. package/dist/storage/lock-repo.js +2 -4
  38. package/dist/storage/runtime-state-repo.js +14 -13
  39. package/dist/storage/tool-config-repo.js +111 -0
  40. package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
  41. package/docs/Design/codex-switch-v0.0.12-design.md +343 -0
  42. package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
  43. package/docs/PRD/codex-switch-prd-v0.0.12.md +279 -0
  44. package/docs/PRD/codex-switch-prd-v0.1.0.md +125 -237
  45. package/docs/Tests/testing.md +39 -112
  46. package/docs/cli-usage.md +135 -565
  47. package/docs/codex-switch-command-design.md +3 -0
  48. package/docs/codex-switch-product-overview.md +52 -207
  49. package/docs/codex-switch-technical-architecture.md +3 -0
  50. package/package.json +1 -1
  51. package/dist/app/rollback-latest.js +0 -26
@@ -10,8 +10,8 @@ const copilot_installer_1 = require("./copilot-installer");
10
10
  /**
11
11
  * Probes whether the optional Copilot SDK runtime is installed and loadable.
12
12
  */
13
- function probeCopilotSdkRuntime() {
14
- const status = (0, copilot_installer_1.probeCopilotSdkInstall)();
13
+ function probeCopilotSdkRuntime(runtimesDir) {
14
+ const status = (0, copilot_installer_1.probeCopilotSdkInstall)(runtimesDir);
15
15
  if (!status.installed) {
16
16
  return {
17
17
  ok: false,
@@ -37,18 +37,18 @@ function probeCopilotSdkRuntime() {
37
37
  /**
38
38
  * Loads the lazily installed Copilot SDK and returns the module.
39
39
  */
40
- async function requireCopilotSdk() {
41
- return (0, copilot_sdk_loader_1.loadCopilotSdk)();
40
+ async function requireCopilotSdk(runtimesDir) {
41
+ return (0, copilot_sdk_loader_1.loadCopilotSdk)(runtimesDir);
42
42
  }
43
43
  /**
44
44
  * Probes whether the lazily installed Copilot SDK can create a usable session.
45
45
  */
46
- async function readCopilotAuthState() {
47
- const runtime = probeCopilotSdkRuntime();
46
+ async function readCopilotAuthState(runtimesDir) {
47
+ const runtime = probeCopilotSdkRuntime(runtimesDir);
48
48
  if (!runtime.ok) {
49
49
  throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed.", runtime.details);
50
50
  }
51
- const { client, session } = await createCopilotSession();
51
+ const { client, session } = await createCopilotSession(runtimesDir);
52
52
  await stopCopilotClient(client);
53
53
  return {
54
54
  ready: Boolean(session),
@@ -60,7 +60,7 @@ async function readCopilotAuthState() {
60
60
  * Executes a single chat-completions style request through the optional Copilot SDK when available.
61
61
  */
62
62
  async function sendCopilotChatCompletion(args) {
63
- const { client, session, sdk } = await createCopilotSession();
63
+ const { client, session, sdk } = await createCopilotSession(args.runtimesDir);
64
64
  try {
65
65
  const sendAndWait = resolveCallable(session, "sendAndWait") ?? resolveCallable(sdk, "sendAndWait");
66
66
  if (!sendAndWait) {
@@ -106,8 +106,8 @@ async function sendCopilotChatCompletion(args) {
106
106
  await stopCopilotClient(client);
107
107
  }
108
108
  }
109
- async function createCopilotSession() {
110
- const sdk = (await requireCopilotSdk());
109
+ async function createCopilotSession(runtimesDir) {
110
+ const sdk = (await requireCopilotSdk(runtimesDir));
111
111
  const client = createCopilotClient(sdk);
112
112
  const createSession = resolveCallable(client ? client : null, "createSession") ?? resolveCallable(sdk, "createSession");
113
113
  if (!createSession) {
@@ -181,11 +181,11 @@ function resolveCallable(target, name) {
181
181
  }
182
182
  const direct = target[name];
183
183
  if (typeof direct === "function") {
184
- return direct;
184
+ return direct.bind(target);
185
185
  }
186
186
  const nestedDefault = target.default;
187
187
  if (nestedDefault && typeof nestedDefault[name] === "function") {
188
- return nestedDefault[name];
188
+ return nestedDefault[name].bind(nestedDefault);
189
189
  }
190
190
  return null;
191
191
  }
@@ -45,11 +45,13 @@ exports.stopCopilotBridge = stopCopilotBridge;
45
45
  const http = __importStar(require("node:http"));
46
46
  const net = __importStar(require("node:net"));
47
47
  const node_child_process_1 = require("node:child_process");
48
+ const fs = __importStar(require("node:fs"));
48
49
  const path = __importStar(require("node:path"));
49
50
  const providers_1 = require("../domain/providers");
50
51
  const errors_1 = require("../domain/errors");
51
52
  const runtime_state_repo_1 = require("../storage/runtime-state-repo");
52
53
  let spawnImplementation = node_child_process_1.spawn;
54
+ let cachedBridgeWorkerBuildId = null;
53
55
  /**
54
56
  * Overrides the spawn implementation for bridge runtime tests.
55
57
  */
@@ -65,8 +67,8 @@ function resetCopilotBridgeSpawnImplementation() {
65
67
  /**
66
68
  * Returns the last known Copilot bridge runtime status.
67
69
  */
68
- async function probeCopilotBridgeRuntime(provider, persistedState) {
69
- const state = persistedState === undefined ? (0, runtime_state_repo_1.readCopilotBridgeState)() : persistedState;
70
+ async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
71
+ const state = persistedState === undefined ? (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir) : persistedState;
70
72
  if (state && (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider))) {
71
73
  return {
72
74
  ok: false,
@@ -126,7 +128,7 @@ async function probeCopilotBridgeRuntime(provider, persistedState) {
126
128
  (0, runtime_state_repo_1.writeCopilotBridgeState)({
127
129
  ...state,
128
130
  lastHealthcheckAt: new Date().toISOString(),
129
- });
131
+ }, runtimeDir);
130
132
  return {
131
133
  ok: true,
132
134
  runtime: "copilot-bridge",
@@ -136,13 +138,13 @@ async function probeCopilotBridgeRuntime(provider, persistedState) {
136
138
  /**
137
139
  * Starts or reuses a Copilot bridge worker, then verifies its health before returning.
138
140
  */
139
- async function ensureCopilotBridge(providerName, provider) {
140
- return startOrReuseCopilotBridge(providerName, provider);
141
+ async function ensureCopilotBridge(providerName, provider, runtimeDir) {
142
+ return startOrReuseCopilotBridge(providerName, provider, runtimeDir);
141
143
  }
142
144
  /**
143
145
  * Starts or reuses a Copilot bridge worker and reports the chosen port.
144
146
  */
145
- async function startOrReuseCopilotBridge(providerName, provider) {
147
+ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir) {
146
148
  if (!(0, providers_1.isCopilotBridgeProvider)(provider)) {
147
149
  throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider is not backed by a Copilot bridge runtime.", {
148
150
  provider: providerName,
@@ -155,27 +157,40 @@ async function startOrReuseCopilotBridge(providerName, provider) {
155
157
  });
156
158
  }
157
159
  const expectedBaseUrl = (0, providers_1.buildCopilotBridgeBaseUrl)(runtime);
158
- const current = (0, runtime_state_repo_1.readCopilotBridgeState)();
160
+ const current = (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir);
161
+ const workerBuildId = getCopilotBridgeWorkerBuildId();
159
162
  let replaced = false;
160
163
  if (current && current.provider === providerName && current.baseUrl === expectedBaseUrl) {
161
- const healthy = await healthcheckCopilotBridge(current.host, current.port);
162
- if (healthy.ok) {
163
- (0, runtime_state_repo_1.writeCopilotBridgeState)({
164
- ...current,
165
- lastHealthcheckAt: new Date().toISOString(),
166
- });
167
- return {
168
- baseUrl: expectedBaseUrl,
169
- host: current.host,
170
- port: current.port,
171
- reused: true,
172
- portChanged: false,
173
- replaced: false,
174
- };
164
+ if (current.workerBuildId === workerBuildId) {
165
+ const healthy = await healthcheckCopilotBridge(current.host, current.port);
166
+ if (healthy.ok) {
167
+ const compatible = await verifyCopilotBridgeAuthorization(current.host, current.port, provider.apiKey);
168
+ if (compatible.ok) {
169
+ (0, runtime_state_repo_1.writeCopilotBridgeState)({
170
+ ...current,
171
+ lastHealthcheckAt: new Date().toISOString(),
172
+ workerBuildId,
173
+ }, runtimeDir);
174
+ return {
175
+ baseUrl: expectedBaseUrl,
176
+ host: current.host,
177
+ port: current.port,
178
+ reused: true,
179
+ portChanged: false,
180
+ replaced: false,
181
+ };
182
+ }
183
+ }
184
+ stopCopilotBridge(runtimeDir);
185
+ replaced = true;
186
+ }
187
+ else {
188
+ stopCopilotBridge(runtimeDir);
189
+ replaced = true;
175
190
  }
176
191
  }
177
192
  if (current && current.provider !== providerName) {
178
- stopCopilotBridge();
193
+ stopCopilotBridge(runtimeDir);
179
194
  replaced = true;
180
195
  }
181
196
  const selectedPort = await selectBridgePort(runtime.bridgeHost, runtime.bridgePort);
@@ -206,9 +221,10 @@ async function startOrReuseCopilotBridge(providerName, provider) {
206
221
  }
207
222
  child.unref();
208
223
  const startedAt = new Date().toISOString();
209
- const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, selectedPort, 15, 200);
224
+ // The worker can take a little longer to become healthy on Windows or under loaded test runs.
225
+ const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, selectedPort, 25, 200);
210
226
  if (!healthy.ok) {
211
- (0, runtime_state_repo_1.clearCopilotBridgeState)();
227
+ (0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
212
228
  if (healthy.reason === "start-failed") {
213
229
  throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
214
230
  provider: providerName,
@@ -232,8 +248,9 @@ async function startOrReuseCopilotBridge(providerName, provider) {
232
248
  baseUrl: selectedBaseUrl,
233
249
  startedAt,
234
250
  lastHealthcheckAt: new Date().toISOString(),
251
+ workerBuildId,
235
252
  };
236
- (0, runtime_state_repo_1.writeCopilotBridgeState)(state);
253
+ (0, runtime_state_repo_1.writeCopilotBridgeState)(state, runtimeDir);
237
254
  return {
238
255
  baseUrl: selectedBaseUrl,
239
256
  host: runtime.bridgeHost,
@@ -266,34 +283,329 @@ function createCopilotBridgeRequestHandler(context) {
266
283
  response.end(JSON.stringify({ object: "list", data: [] }));
267
284
  return;
268
285
  }
269
- if (method !== "POST" || url !== "/v1/chat/completions") {
270
- response.writeHead(404, { "content-type": "application/json" });
271
- response.end(JSON.stringify({ error: { message: "Not found" } }));
286
+ if (method === "POST" && url === "/v1/chat/completions") {
287
+ const body = await readJsonBody(request);
288
+ const stream = Boolean(body.stream);
289
+ const payload = await context.executeChatCompletion(body);
290
+ if (stream) {
291
+ response.writeHead(200, {
292
+ "content-type": "text/event-stream",
293
+ "cache-control": "no-cache",
294
+ connection: "keep-alive",
295
+ });
296
+ response.write(`data: ${JSON.stringify(payload)}\n\n`);
297
+ response.write("data: [DONE]\n\n");
298
+ response.end();
299
+ return;
300
+ }
301
+ response.writeHead(200, { "content-type": "application/json" });
302
+ response.end(JSON.stringify(payload));
272
303
  return;
273
304
  }
274
- const body = await readJsonBody(request);
275
- const stream = Boolean(body.stream);
276
- const payload = await context.executeChatCompletion(body);
277
- if (stream) {
278
- response.writeHead(200, {
279
- "content-type": "text/event-stream",
280
- "cache-control": "no-cache",
281
- connection: "keep-alive",
305
+ if (method === "POST" && url === "/v1/responses") {
306
+ const body = await readJsonBody(request);
307
+ const normalized = normalizeResponsesRequest(body);
308
+ const payload = await context.executeChatCompletion({
309
+ model: normalized.model,
310
+ messages: normalized.messages,
282
311
  });
283
- response.write(`data: ${JSON.stringify(payload)}\n\n`);
284
- response.write("data: [DONE]\n\n");
285
- response.end();
312
+ if (normalized.stream) {
313
+ response.writeHead(200, {
314
+ "content-type": "text/event-stream",
315
+ "cache-control": "no-cache",
316
+ connection: "keep-alive",
317
+ });
318
+ writeResponsesStream(response, payload);
319
+ response.end();
320
+ return;
321
+ }
322
+ response.writeHead(200, { "content-type": "application/json" });
323
+ response.end(JSON.stringify(buildResponsesPayload(payload)));
324
+ return;
325
+ }
326
+ if (method !== "POST") {
327
+ response.writeHead(404, { "content-type": "application/json" });
328
+ response.end(JSON.stringify({ error: { message: "Not found" } }));
286
329
  return;
287
330
  }
288
- response.writeHead(200, { "content-type": "application/json" });
289
- response.end(JSON.stringify(payload));
331
+ response.writeHead(404, { "content-type": "application/json" });
332
+ response.end(JSON.stringify({ error: { message: "Not found" } }));
290
333
  }
291
334
  catch (error) {
292
- response.writeHead(500, { "content-type": "application/json" });
335
+ const statusCode = isCliError(error) && error.code === "BRIDGE_UNSUPPORTED_REQUEST" ? 400 : 500;
336
+ response.writeHead(statusCode, { "content-type": "application/json" });
293
337
  response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
294
338
  }
295
339
  };
296
340
  }
341
+ /**
342
+ * Converts one minimal Responses API payload into the existing chat-completions bridge call shape.
343
+ */
344
+ function normalizeResponsesRequest(body) {
345
+ const payload = body;
346
+ if (typeof payload.model !== "string" || payload.model.trim() === "") {
347
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses requires a non-empty string model.");
348
+ }
349
+ const messages = normalizeResponsesInput(payload.input);
350
+ if (messages.length === 0) {
351
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses requires at least one input message.");
352
+ }
353
+ return {
354
+ model: payload.model,
355
+ messages,
356
+ stream: payload.stream === true,
357
+ };
358
+ }
359
+ function normalizeResponsesInput(input) {
360
+ if (typeof input === "string") {
361
+ return [{ role: "user", content: input }];
362
+ }
363
+ if (!Array.isArray(input)) {
364
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses expects input as a string or message array.");
365
+ }
366
+ if (input.length === 0) {
367
+ return [];
368
+ }
369
+ const entryKinds = input.map(classifyResponsesInputEntry);
370
+ const hasMessages = entryKinds.includes("message");
371
+ const hasContentItems = entryKinds.includes("content-item");
372
+ if (hasMessages && hasContentItems) {
373
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input array must contain either message objects or content items, not both.");
374
+ }
375
+ if (hasContentItems) {
376
+ return [
377
+ {
378
+ role: "user",
379
+ content: extractResponsesTextContent(input, 0),
380
+ },
381
+ ];
382
+ }
383
+ return input.map((entry, index) => normalizeResponsesMessage(entry, index));
384
+ }
385
+ function normalizeResponsesMessage(entry, index) {
386
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
387
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}] must be an object message.`);
388
+ }
389
+ const message = entry;
390
+ const role = typeof message.role === "string" && message.role.trim() !== "" ? message.role : "user";
391
+ const content = extractResponsesTextContent(message.content, index);
392
+ return { role, content };
393
+ }
394
+ /**
395
+ * Classifies one top-level Responses input entry so mixed array shapes can be rejected clearly.
396
+ */
397
+ function classifyResponsesInputEntry(entry) {
398
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
399
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input entries must be objects.");
400
+ }
401
+ const record = entry;
402
+ if ("content" in record || "role" in record) {
403
+ return "message";
404
+ }
405
+ if (typeof record.type === "string") {
406
+ return "content-item";
407
+ }
408
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input entries must be message objects or typed content items.");
409
+ }
410
+ function extractResponsesTextContent(content, index) {
411
+ if (typeof content === "string") {
412
+ return content;
413
+ }
414
+ if (!Array.isArray(content)) {
415
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}].content must be a string or content item array.`);
416
+ }
417
+ const parts = content.map((item, itemIndex) => {
418
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
419
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}].content[${String(itemIndex)}] must be an object item.`);
420
+ }
421
+ return renderResponsesContentItem(item);
422
+ });
423
+ return parts.join("\n");
424
+ }
425
+ /**
426
+ * Converts one Responses content item into the text-only prompt representation required by the Copilot SDK bridge.
427
+ */
428
+ function renderResponsesContentItem(item) {
429
+ const type = typeof item.type === "string" ? item.type : null;
430
+ const text = typeof item.text === "string" ? item.text : null;
431
+ if ((type === "input_text" || type === "text" || type === "output_text") && text !== null) {
432
+ return text;
433
+ }
434
+ if (type === "input_image") {
435
+ return buildResponsesPlaceholder("input_image", item.image_url, item.file_id);
436
+ }
437
+ if (type === "input_file") {
438
+ return buildResponsesPlaceholder("input_file", item.filename, item.file_id);
439
+ }
440
+ if (type !== null) {
441
+ return `[unsupported content type: ${type}]`;
442
+ }
443
+ throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses content items must declare a string type.");
444
+ }
445
+ /**
446
+ * Builds a readable placeholder for non-text Responses content items preserved as text-only context.
447
+ */
448
+ function buildResponsesPlaceholder(type, ...candidates) {
449
+ for (const candidate of candidates) {
450
+ if (typeof candidate === "string" && candidate.trim() !== "") {
451
+ return `[${type}: ${candidate}]`;
452
+ }
453
+ }
454
+ return `[${type} omitted]`;
455
+ }
456
+ /**
457
+ * Converts the existing chat-completions response into a minimal Responses API payload.
458
+ */
459
+ function buildResponsesPayload(payload) {
460
+ const firstChoice = Array.isArray(payload.choices) ? payload.choices[0] : null;
461
+ const outputText = firstChoice?.message?.content ?? "";
462
+ return {
463
+ id: payload.id ?? `resp_${Date.now()}`,
464
+ object: "response",
465
+ created_at: payload.created ?? Math.floor(Date.now() / 1000),
466
+ model: payload.model ?? "copilot",
467
+ status: "completed",
468
+ output: [
469
+ {
470
+ type: "message",
471
+ id: `${payload.id ?? "resp"}_msg_0`,
472
+ role: "assistant",
473
+ content: [
474
+ {
475
+ type: "output_text",
476
+ text: outputText,
477
+ },
478
+ ],
479
+ },
480
+ ],
481
+ output_text: outputText,
482
+ };
483
+ }
484
+ /**
485
+ * Emits a minimal OpenAI-compatible Responses API event stream.
486
+ */
487
+ function writeResponsesStream(response, payload) {
488
+ const responsePayload = buildResponsesPayload(payload);
489
+ const responseId = typeof responsePayload.id === "string" ? responsePayload.id : `resp_${Date.now()}`;
490
+ const messageId = buildResponsesMessageId(responseId);
491
+ const outputText = typeof responsePayload.output_text === "string" ? responsePayload.output_text : "";
492
+ const inProgressResponse = {
493
+ ...responsePayload,
494
+ status: "in_progress",
495
+ output: [],
496
+ };
497
+ const completedMessage = {
498
+ id: messageId,
499
+ type: "message",
500
+ status: "completed",
501
+ role: "assistant",
502
+ content: [
503
+ {
504
+ type: "output_text",
505
+ text: outputText,
506
+ annotations: [],
507
+ },
508
+ ],
509
+ };
510
+ writeSseEvent(response, "response.created", {
511
+ type: "response.created",
512
+ response: inProgressResponse,
513
+ });
514
+ writeSseEvent(response, "response.in_progress", {
515
+ type: "response.in_progress",
516
+ response: inProgressResponse,
517
+ });
518
+ writeSseEvent(response, "response.output_item.added", {
519
+ type: "response.output_item.added",
520
+ output_index: 0,
521
+ item: {
522
+ id: messageId,
523
+ type: "message",
524
+ status: "in_progress",
525
+ role: "assistant",
526
+ content: [],
527
+ },
528
+ });
529
+ writeSseEvent(response, "response.content_part.added", {
530
+ type: "response.content_part.added",
531
+ item_id: messageId,
532
+ output_index: 0,
533
+ content_index: 0,
534
+ part: {
535
+ type: "output_text",
536
+ text: "",
537
+ annotations: [],
538
+ },
539
+ });
540
+ writeSseEvent(response, "response.output_text.delta", {
541
+ type: "response.output_text.delta",
542
+ item_id: messageId,
543
+ output_index: 0,
544
+ content_index: 0,
545
+ delta: outputText,
546
+ });
547
+ writeSseEvent(response, "response.output_text.done", {
548
+ type: "response.output_text.done",
549
+ item_id: messageId,
550
+ output_index: 0,
551
+ content_index: 0,
552
+ text: outputText,
553
+ });
554
+ writeSseEvent(response, "response.content_part.done", {
555
+ type: "response.content_part.done",
556
+ item_id: messageId,
557
+ output_index: 0,
558
+ content_index: 0,
559
+ part: {
560
+ type: "output_text",
561
+ text: outputText,
562
+ annotations: [],
563
+ },
564
+ });
565
+ writeSseEvent(response, "response.output_item.done", {
566
+ type: "response.output_item.done",
567
+ output_index: 0,
568
+ item: completedMessage,
569
+ });
570
+ writeSseEvent(response, "response.completed", {
571
+ type: "response.completed",
572
+ response: {
573
+ ...responsePayload,
574
+ output: [completedMessage],
575
+ },
576
+ });
577
+ }
578
+ /**
579
+ * Formats and writes one server-sent event frame.
580
+ */
581
+ function writeSseEvent(response, eventName, data) {
582
+ response.write(`event: ${eventName}\n`);
583
+ response.write(`data: ${JSON.stringify(data)}\n\n`);
584
+ }
585
+ /**
586
+ * Derives a stable message identifier for synthesized Responses output items.
587
+ */
588
+ function buildResponsesMessageId(responseId) {
589
+ if (responseId.startsWith("resp_")) {
590
+ return `msg_${responseId.slice("resp_".length)}_0`;
591
+ }
592
+ return `${responseId}_msg_0`;
593
+ }
594
+ function isCliError(error) {
595
+ return Boolean(error && typeof error === "object" && typeof error.code === "string");
596
+ }
597
+ /**
598
+ * Returns a stable build identifier for the compiled bridge worker bundle.
599
+ */
600
+ function getCopilotBridgeWorkerBuildId() {
601
+ if (cachedBridgeWorkerBuildId) {
602
+ return cachedBridgeWorkerBuildId;
603
+ }
604
+ const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
605
+ const stats = fs.statSync(workerPath);
606
+ cachedBridgeWorkerBuildId = `${stats.size}:${stats.mtimeMs}`;
607
+ return cachedBridgeWorkerBuildId;
608
+ }
297
609
  /**
298
610
  * Starts an in-process local bridge server. Primarily used by the worker entrypoint and tests.
299
611
  */
@@ -329,8 +641,8 @@ async function waitForCopilotBridgeHealth(host, port, attempts = 10, delayMs = 1
329
641
  /**
330
642
  * Stops the currently persisted Copilot bridge worker when possible.
331
643
  */
332
- function stopCopilotBridge() {
333
- const state = (0, runtime_state_repo_1.readCopilotBridgeState)();
644
+ function stopCopilotBridge(runtimeDir) {
645
+ const state = (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir);
334
646
  if (state?.pid) {
335
647
  try {
336
648
  process.kill(state.pid);
@@ -339,7 +651,7 @@ function stopCopilotBridge() {
339
651
  // Ignore best-effort bridge cleanup failures.
340
652
  }
341
653
  }
342
- (0, runtime_state_repo_1.clearCopilotBridgeState)();
654
+ (0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
343
655
  }
344
656
  async function checkPortAvailability(host, port) {
345
657
  return new Promise((resolve) => {
@@ -457,6 +769,43 @@ async function healthcheckCopilotBridge(host, port) {
457
769
  request.end();
458
770
  });
459
771
  }
772
+ /**
773
+ * Checks whether a healthy bridge still accepts the provider's current bearer secret.
774
+ */
775
+ async function verifyCopilotBridgeAuthorization(host, port, apiKey) {
776
+ return new Promise((resolve) => {
777
+ const request = http.request({
778
+ host,
779
+ port,
780
+ method: "GET",
781
+ path: "/v1/models",
782
+ timeout: 1000,
783
+ headers: {
784
+ authorization: `Bearer ${apiKey}`,
785
+ },
786
+ }, (response) => {
787
+ response.resume();
788
+ if (response.statusCode === 200) {
789
+ resolve({ ok: true });
790
+ return;
791
+ }
792
+ resolve({
793
+ ok: false,
794
+ cause: `Authorization probe returned status ${String(response.statusCode ?? 0)}.`,
795
+ });
796
+ });
797
+ request.on("error", (error) => {
798
+ resolve({
799
+ ok: false,
800
+ cause: error.message,
801
+ });
802
+ });
803
+ request.on("timeout", () => {
804
+ request.destroy(new Error("Authorization probe timed out."));
805
+ });
806
+ request.end();
807
+ });
808
+ }
460
809
  async function readJsonBody(request) {
461
810
  const chunks = [];
462
811
  for await (const chunk of request) {