@minniexcode/codex-switch 0.0.10 → 0.0.11
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 +52 -15
- package/README.CN.md +81 -48
- package/README.md +75 -34
- package/dist/app/add-provider.js +29 -15
- package/dist/app/bridge.js +15 -14
- package/dist/app/edit-provider.js +1 -1
- package/dist/app/get-status.js +13 -6
- package/dist/app/import-providers.js +1 -1
- package/dist/app/init-codex.js +13 -14
- package/dist/app/remove-provider.js +1 -1
- package/dist/app/run-doctor.js +12 -5
- package/dist/app/run-mutation.js +3 -2
- package/dist/app/setup-codex.js +3 -1
- package/dist/app/switch-provider.js +11 -13
- package/dist/cli.js +34 -2
- package/dist/commands/args.js +2 -2
- package/dist/commands/dispatch.js +40 -0
- package/dist/commands/handlers.js +121 -156
- package/dist/commands/help.js +2 -0
- package/dist/commands/registry.js +28 -9
- package/dist/domain/backups.js +4 -4
- package/dist/domain/config.js +110 -5
- package/dist/domain/providers.js +12 -0
- package/dist/domain/runtime-state.js +81 -5
- package/dist/runtime/copilot-adapter.js +12 -12
- package/dist/runtime/copilot-bridge.js +392 -44
- package/dist/runtime/copilot-cli.js +84 -12
- package/dist/runtime/copilot-installer.js +10 -9
- package/dist/runtime/copilot-sdk-loader.js +5 -5
- package/dist/storage/backup-repo.js +4 -4
- package/dist/storage/codex-paths.js +34 -8
- package/dist/storage/lock-repo.js +2 -4
- package/dist/storage/runtime-state-repo.js +14 -13
- package/dist/storage/tool-config-repo.js +111 -0
- package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
- package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
- package/docs/cli-usage.md +166 -295
- package/package.json +1 -1
|
@@ -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
|
-
|
|
162
|
-
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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);
|
|
@@ -208,7 +223,7 @@ async function startOrReuseCopilotBridge(providerName, provider) {
|
|
|
208
223
|
const startedAt = new Date().toISOString();
|
|
209
224
|
const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, selectedPort, 15, 200);
|
|
210
225
|
if (!healthy.ok) {
|
|
211
|
-
(0, runtime_state_repo_1.clearCopilotBridgeState)();
|
|
226
|
+
(0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
|
|
212
227
|
if (healthy.reason === "start-failed") {
|
|
213
228
|
throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
|
|
214
229
|
provider: providerName,
|
|
@@ -232,8 +247,9 @@ async function startOrReuseCopilotBridge(providerName, provider) {
|
|
|
232
247
|
baseUrl: selectedBaseUrl,
|
|
233
248
|
startedAt,
|
|
234
249
|
lastHealthcheckAt: new Date().toISOString(),
|
|
250
|
+
workerBuildId,
|
|
235
251
|
};
|
|
236
|
-
(0, runtime_state_repo_1.writeCopilotBridgeState)(state);
|
|
252
|
+
(0, runtime_state_repo_1.writeCopilotBridgeState)(state, runtimeDir);
|
|
237
253
|
return {
|
|
238
254
|
baseUrl: selectedBaseUrl,
|
|
239
255
|
host: runtime.bridgeHost,
|
|
@@ -266,34 +282,329 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
266
282
|
response.end(JSON.stringify({ object: "list", data: [] }));
|
|
267
283
|
return;
|
|
268
284
|
}
|
|
269
|
-
if (method
|
|
270
|
-
|
|
271
|
-
|
|
285
|
+
if (method === "POST" && url === "/v1/chat/completions") {
|
|
286
|
+
const body = await readJsonBody(request);
|
|
287
|
+
const stream = Boolean(body.stream);
|
|
288
|
+
const payload = await context.executeChatCompletion(body);
|
|
289
|
+
if (stream) {
|
|
290
|
+
response.writeHead(200, {
|
|
291
|
+
"content-type": "text/event-stream",
|
|
292
|
+
"cache-control": "no-cache",
|
|
293
|
+
connection: "keep-alive",
|
|
294
|
+
});
|
|
295
|
+
response.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
296
|
+
response.write("data: [DONE]\n\n");
|
|
297
|
+
response.end();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
301
|
+
response.end(JSON.stringify(payload));
|
|
272
302
|
return;
|
|
273
303
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"cache-control": "no-cache",
|
|
281
|
-
connection: "keep-alive",
|
|
304
|
+
if (method === "POST" && url === "/v1/responses") {
|
|
305
|
+
const body = await readJsonBody(request);
|
|
306
|
+
const normalized = normalizeResponsesRequest(body);
|
|
307
|
+
const payload = await context.executeChatCompletion({
|
|
308
|
+
model: normalized.model,
|
|
309
|
+
messages: normalized.messages,
|
|
282
310
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
311
|
+
if (normalized.stream) {
|
|
312
|
+
response.writeHead(200, {
|
|
313
|
+
"content-type": "text/event-stream",
|
|
314
|
+
"cache-control": "no-cache",
|
|
315
|
+
connection: "keep-alive",
|
|
316
|
+
});
|
|
317
|
+
writeResponsesStream(response, payload);
|
|
318
|
+
response.end();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
322
|
+
response.end(JSON.stringify(buildResponsesPayload(payload)));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (method !== "POST") {
|
|
326
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
327
|
+
response.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
286
328
|
return;
|
|
287
329
|
}
|
|
288
|
-
response.writeHead(
|
|
289
|
-
response.end(JSON.stringify(
|
|
330
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
331
|
+
response.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
290
332
|
}
|
|
291
333
|
catch (error) {
|
|
292
|
-
|
|
334
|
+
const statusCode = isCliError(error) && error.code === "BRIDGE_UNSUPPORTED_REQUEST" ? 400 : 500;
|
|
335
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
293
336
|
response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
|
|
294
337
|
}
|
|
295
338
|
};
|
|
296
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Converts one minimal Responses API payload into the existing chat-completions bridge call shape.
|
|
342
|
+
*/
|
|
343
|
+
function normalizeResponsesRequest(body) {
|
|
344
|
+
const payload = body;
|
|
345
|
+
if (typeof payload.model !== "string" || payload.model.trim() === "") {
|
|
346
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses requires a non-empty string model.");
|
|
347
|
+
}
|
|
348
|
+
const messages = normalizeResponsesInput(payload.input);
|
|
349
|
+
if (messages.length === 0) {
|
|
350
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses requires at least one input message.");
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
model: payload.model,
|
|
354
|
+
messages,
|
|
355
|
+
stream: payload.stream === true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function normalizeResponsesInput(input) {
|
|
359
|
+
if (typeof input === "string") {
|
|
360
|
+
return [{ role: "user", content: input }];
|
|
361
|
+
}
|
|
362
|
+
if (!Array.isArray(input)) {
|
|
363
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses expects input as a string or message array.");
|
|
364
|
+
}
|
|
365
|
+
if (input.length === 0) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
const entryKinds = input.map(classifyResponsesInputEntry);
|
|
369
|
+
const hasMessages = entryKinds.includes("message");
|
|
370
|
+
const hasContentItems = entryKinds.includes("content-item");
|
|
371
|
+
if (hasMessages && hasContentItems) {
|
|
372
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input array must contain either message objects or content items, not both.");
|
|
373
|
+
}
|
|
374
|
+
if (hasContentItems) {
|
|
375
|
+
return [
|
|
376
|
+
{
|
|
377
|
+
role: "user",
|
|
378
|
+
content: extractResponsesTextContent(input, 0),
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
return input.map((entry, index) => normalizeResponsesMessage(entry, index));
|
|
383
|
+
}
|
|
384
|
+
function normalizeResponsesMessage(entry, index) {
|
|
385
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
386
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}] must be an object message.`);
|
|
387
|
+
}
|
|
388
|
+
const message = entry;
|
|
389
|
+
const role = typeof message.role === "string" && message.role.trim() !== "" ? message.role : "user";
|
|
390
|
+
const content = extractResponsesTextContent(message.content, index);
|
|
391
|
+
return { role, content };
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Classifies one top-level Responses input entry so mixed array shapes can be rejected clearly.
|
|
395
|
+
*/
|
|
396
|
+
function classifyResponsesInputEntry(entry) {
|
|
397
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
398
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input entries must be objects.");
|
|
399
|
+
}
|
|
400
|
+
const record = entry;
|
|
401
|
+
if ("content" in record || "role" in record) {
|
|
402
|
+
return "message";
|
|
403
|
+
}
|
|
404
|
+
if (typeof record.type === "string") {
|
|
405
|
+
return "content-item";
|
|
406
|
+
}
|
|
407
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses input entries must be message objects or typed content items.");
|
|
408
|
+
}
|
|
409
|
+
function extractResponsesTextContent(content, index) {
|
|
410
|
+
if (typeof content === "string") {
|
|
411
|
+
return content;
|
|
412
|
+
}
|
|
413
|
+
if (!Array.isArray(content)) {
|
|
414
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}].content must be a string or content item array.`);
|
|
415
|
+
}
|
|
416
|
+
const parts = content.map((item, itemIndex) => {
|
|
417
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
418
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge /v1/responses input[${String(index)}].content[${String(itemIndex)}] must be an object item.`);
|
|
419
|
+
}
|
|
420
|
+
return renderResponsesContentItem(item);
|
|
421
|
+
});
|
|
422
|
+
return parts.join("\n");
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Converts one Responses content item into the text-only prompt representation required by the Copilot SDK bridge.
|
|
426
|
+
*/
|
|
427
|
+
function renderResponsesContentItem(item) {
|
|
428
|
+
const type = typeof item.type === "string" ? item.type : null;
|
|
429
|
+
const text = typeof item.text === "string" ? item.text : null;
|
|
430
|
+
if ((type === "input_text" || type === "text" || type === "output_text") && text !== null) {
|
|
431
|
+
return text;
|
|
432
|
+
}
|
|
433
|
+
if (type === "input_image") {
|
|
434
|
+
return buildResponsesPlaceholder("input_image", item.image_url, item.file_id);
|
|
435
|
+
}
|
|
436
|
+
if (type === "input_file") {
|
|
437
|
+
return buildResponsesPlaceholder("input_file", item.filename, item.file_id);
|
|
438
|
+
}
|
|
439
|
+
if (type !== null) {
|
|
440
|
+
return `[unsupported content type: ${type}]`;
|
|
441
|
+
}
|
|
442
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", "Copilot bridge /v1/responses content items must declare a string type.");
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Builds a readable placeholder for non-text Responses content items preserved as text-only context.
|
|
446
|
+
*/
|
|
447
|
+
function buildResponsesPlaceholder(type, ...candidates) {
|
|
448
|
+
for (const candidate of candidates) {
|
|
449
|
+
if (typeof candidate === "string" && candidate.trim() !== "") {
|
|
450
|
+
return `[${type}: ${candidate}]`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return `[${type} omitted]`;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Converts the existing chat-completions response into a minimal Responses API payload.
|
|
457
|
+
*/
|
|
458
|
+
function buildResponsesPayload(payload) {
|
|
459
|
+
const firstChoice = Array.isArray(payload.choices) ? payload.choices[0] : null;
|
|
460
|
+
const outputText = firstChoice?.message?.content ?? "";
|
|
461
|
+
return {
|
|
462
|
+
id: payload.id ?? `resp_${Date.now()}`,
|
|
463
|
+
object: "response",
|
|
464
|
+
created_at: payload.created ?? Math.floor(Date.now() / 1000),
|
|
465
|
+
model: payload.model ?? "copilot",
|
|
466
|
+
status: "completed",
|
|
467
|
+
output: [
|
|
468
|
+
{
|
|
469
|
+
type: "message",
|
|
470
|
+
id: `${payload.id ?? "resp"}_msg_0`,
|
|
471
|
+
role: "assistant",
|
|
472
|
+
content: [
|
|
473
|
+
{
|
|
474
|
+
type: "output_text",
|
|
475
|
+
text: outputText,
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
output_text: outputText,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Emits a minimal OpenAI-compatible Responses API event stream.
|
|
485
|
+
*/
|
|
486
|
+
function writeResponsesStream(response, payload) {
|
|
487
|
+
const responsePayload = buildResponsesPayload(payload);
|
|
488
|
+
const responseId = typeof responsePayload.id === "string" ? responsePayload.id : `resp_${Date.now()}`;
|
|
489
|
+
const messageId = buildResponsesMessageId(responseId);
|
|
490
|
+
const outputText = typeof responsePayload.output_text === "string" ? responsePayload.output_text : "";
|
|
491
|
+
const inProgressResponse = {
|
|
492
|
+
...responsePayload,
|
|
493
|
+
status: "in_progress",
|
|
494
|
+
output: [],
|
|
495
|
+
};
|
|
496
|
+
const completedMessage = {
|
|
497
|
+
id: messageId,
|
|
498
|
+
type: "message",
|
|
499
|
+
status: "completed",
|
|
500
|
+
role: "assistant",
|
|
501
|
+
content: [
|
|
502
|
+
{
|
|
503
|
+
type: "output_text",
|
|
504
|
+
text: outputText,
|
|
505
|
+
annotations: [],
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
writeSseEvent(response, "response.created", {
|
|
510
|
+
type: "response.created",
|
|
511
|
+
response: inProgressResponse,
|
|
512
|
+
});
|
|
513
|
+
writeSseEvent(response, "response.in_progress", {
|
|
514
|
+
type: "response.in_progress",
|
|
515
|
+
response: inProgressResponse,
|
|
516
|
+
});
|
|
517
|
+
writeSseEvent(response, "response.output_item.added", {
|
|
518
|
+
type: "response.output_item.added",
|
|
519
|
+
output_index: 0,
|
|
520
|
+
item: {
|
|
521
|
+
id: messageId,
|
|
522
|
+
type: "message",
|
|
523
|
+
status: "in_progress",
|
|
524
|
+
role: "assistant",
|
|
525
|
+
content: [],
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
writeSseEvent(response, "response.content_part.added", {
|
|
529
|
+
type: "response.content_part.added",
|
|
530
|
+
item_id: messageId,
|
|
531
|
+
output_index: 0,
|
|
532
|
+
content_index: 0,
|
|
533
|
+
part: {
|
|
534
|
+
type: "output_text",
|
|
535
|
+
text: "",
|
|
536
|
+
annotations: [],
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
writeSseEvent(response, "response.output_text.delta", {
|
|
540
|
+
type: "response.output_text.delta",
|
|
541
|
+
item_id: messageId,
|
|
542
|
+
output_index: 0,
|
|
543
|
+
content_index: 0,
|
|
544
|
+
delta: outputText,
|
|
545
|
+
});
|
|
546
|
+
writeSseEvent(response, "response.output_text.done", {
|
|
547
|
+
type: "response.output_text.done",
|
|
548
|
+
item_id: messageId,
|
|
549
|
+
output_index: 0,
|
|
550
|
+
content_index: 0,
|
|
551
|
+
text: outputText,
|
|
552
|
+
});
|
|
553
|
+
writeSseEvent(response, "response.content_part.done", {
|
|
554
|
+
type: "response.content_part.done",
|
|
555
|
+
item_id: messageId,
|
|
556
|
+
output_index: 0,
|
|
557
|
+
content_index: 0,
|
|
558
|
+
part: {
|
|
559
|
+
type: "output_text",
|
|
560
|
+
text: outputText,
|
|
561
|
+
annotations: [],
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
writeSseEvent(response, "response.output_item.done", {
|
|
565
|
+
type: "response.output_item.done",
|
|
566
|
+
output_index: 0,
|
|
567
|
+
item: completedMessage,
|
|
568
|
+
});
|
|
569
|
+
writeSseEvent(response, "response.completed", {
|
|
570
|
+
type: "response.completed",
|
|
571
|
+
response: {
|
|
572
|
+
...responsePayload,
|
|
573
|
+
output: [completedMessage],
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Formats and writes one server-sent event frame.
|
|
579
|
+
*/
|
|
580
|
+
function writeSseEvent(response, eventName, data) {
|
|
581
|
+
response.write(`event: ${eventName}\n`);
|
|
582
|
+
response.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Derives a stable message identifier for synthesized Responses output items.
|
|
586
|
+
*/
|
|
587
|
+
function buildResponsesMessageId(responseId) {
|
|
588
|
+
if (responseId.startsWith("resp_")) {
|
|
589
|
+
return `msg_${responseId.slice("resp_".length)}_0`;
|
|
590
|
+
}
|
|
591
|
+
return `${responseId}_msg_0`;
|
|
592
|
+
}
|
|
593
|
+
function isCliError(error) {
|
|
594
|
+
return Boolean(error && typeof error === "object" && typeof error.code === "string");
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Returns a stable build identifier for the compiled bridge worker bundle.
|
|
598
|
+
*/
|
|
599
|
+
function getCopilotBridgeWorkerBuildId() {
|
|
600
|
+
if (cachedBridgeWorkerBuildId) {
|
|
601
|
+
return cachedBridgeWorkerBuildId;
|
|
602
|
+
}
|
|
603
|
+
const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
|
|
604
|
+
const stats = fs.statSync(workerPath);
|
|
605
|
+
cachedBridgeWorkerBuildId = `${stats.size}:${stats.mtimeMs}`;
|
|
606
|
+
return cachedBridgeWorkerBuildId;
|
|
607
|
+
}
|
|
297
608
|
/**
|
|
298
609
|
* Starts an in-process local bridge server. Primarily used by the worker entrypoint and tests.
|
|
299
610
|
*/
|
|
@@ -329,8 +640,8 @@ async function waitForCopilotBridgeHealth(host, port, attempts = 10, delayMs = 1
|
|
|
329
640
|
/**
|
|
330
641
|
* Stops the currently persisted Copilot bridge worker when possible.
|
|
331
642
|
*/
|
|
332
|
-
function stopCopilotBridge() {
|
|
333
|
-
const state = (0, runtime_state_repo_1.readCopilotBridgeState)();
|
|
643
|
+
function stopCopilotBridge(runtimeDir) {
|
|
644
|
+
const state = (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir);
|
|
334
645
|
if (state?.pid) {
|
|
335
646
|
try {
|
|
336
647
|
process.kill(state.pid);
|
|
@@ -339,7 +650,7 @@ function stopCopilotBridge() {
|
|
|
339
650
|
// Ignore best-effort bridge cleanup failures.
|
|
340
651
|
}
|
|
341
652
|
}
|
|
342
|
-
(0, runtime_state_repo_1.clearCopilotBridgeState)();
|
|
653
|
+
(0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
|
|
343
654
|
}
|
|
344
655
|
async function checkPortAvailability(host, port) {
|
|
345
656
|
return new Promise((resolve) => {
|
|
@@ -457,6 +768,43 @@ async function healthcheckCopilotBridge(host, port) {
|
|
|
457
768
|
request.end();
|
|
458
769
|
});
|
|
459
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Checks whether a healthy bridge still accepts the provider's current bearer secret.
|
|
773
|
+
*/
|
|
774
|
+
async function verifyCopilotBridgeAuthorization(host, port, apiKey) {
|
|
775
|
+
return new Promise((resolve) => {
|
|
776
|
+
const request = http.request({
|
|
777
|
+
host,
|
|
778
|
+
port,
|
|
779
|
+
method: "GET",
|
|
780
|
+
path: "/v1/models",
|
|
781
|
+
timeout: 1000,
|
|
782
|
+
headers: {
|
|
783
|
+
authorization: `Bearer ${apiKey}`,
|
|
784
|
+
},
|
|
785
|
+
}, (response) => {
|
|
786
|
+
response.resume();
|
|
787
|
+
if (response.statusCode === 200) {
|
|
788
|
+
resolve({ ok: true });
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
resolve({
|
|
792
|
+
ok: false,
|
|
793
|
+
cause: `Authorization probe returned status ${String(response.statusCode ?? 0)}.`,
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
request.on("error", (error) => {
|
|
797
|
+
resolve({
|
|
798
|
+
ok: false,
|
|
799
|
+
cause: error.message,
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
request.on("timeout", () => {
|
|
803
|
+
request.destroy(new Error("Authorization probe timed out."));
|
|
804
|
+
});
|
|
805
|
+
request.end();
|
|
806
|
+
});
|
|
807
|
+
}
|
|
460
808
|
async function readJsonBody(request) {
|
|
461
809
|
const chunks = [];
|
|
462
810
|
for await (const chunk of request) {
|
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.setCopilotCliSpawnImplementation = setCopilotCliSpawnImplementation;
|
|
4
37
|
exports.resetCopilotCliSpawnImplementation = resetCopilotCliSpawnImplementation;
|
|
5
38
|
exports.checkCopilotCliAvailable = checkCopilotCliAvailable;
|
|
6
39
|
exports.runCopilotLogin = runCopilotLogin;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
7
42
|
const node_child_process_1 = require("node:child_process");
|
|
43
|
+
const copilot_installer_1 = require("./copilot-installer");
|
|
8
44
|
let spawnImplementation = node_child_process_1.spawnSync;
|
|
9
45
|
/**
|
|
10
46
|
* Overrides the spawn implementation for Copilot CLI tests.
|
|
@@ -19,22 +55,28 @@ function resetCopilotCliSpawnImplementation() {
|
|
|
19
55
|
spawnImplementation = node_child_process_1.spawnSync;
|
|
20
56
|
}
|
|
21
57
|
/**
|
|
22
|
-
* Checks whether the GitHub Copilot CLI is available on PATH.
|
|
58
|
+
* Checks whether the GitHub Copilot CLI is available either from the bundled runtime or on PATH.
|
|
23
59
|
*/
|
|
24
|
-
function checkCopilotCliAvailable() {
|
|
25
|
-
const invocation = getCopilotInvocation(["--help"]);
|
|
60
|
+
function checkCopilotCliAvailable(runtimesDir) {
|
|
61
|
+
const invocation = getCopilotInvocation(["--help"], runtimesDir);
|
|
26
62
|
const result = spawnImplementation(invocation.command, invocation.args, {
|
|
27
63
|
stdio: "pipe",
|
|
28
64
|
encoding: "utf8",
|
|
29
|
-
shell:
|
|
65
|
+
shell: invocation.shell,
|
|
30
66
|
});
|
|
31
67
|
if (result.error || result.status !== 0) {
|
|
32
68
|
return {
|
|
33
69
|
ok: false,
|
|
34
70
|
cause: result.error?.message ?? (result.stderr.trim() || "Unknown failure"),
|
|
71
|
+
source: invocation.source,
|
|
72
|
+
command: formatInvocation(invocation),
|
|
35
73
|
};
|
|
36
74
|
}
|
|
37
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
source: invocation.source,
|
|
78
|
+
command: formatInvocation(invocation),
|
|
79
|
+
};
|
|
38
80
|
}
|
|
39
81
|
/**
|
|
40
82
|
* Launches the official `copilot login` flow in the current terminal.
|
|
@@ -44,27 +86,57 @@ function runCopilotLogin(options) {
|
|
|
44
86
|
if (options?.host) {
|
|
45
87
|
args.push("--hostname", options.host);
|
|
46
88
|
}
|
|
47
|
-
const invocation = getCopilotInvocation(args);
|
|
89
|
+
const invocation = getCopilotInvocation(args, options?.runtimesDir);
|
|
48
90
|
const result = spawnImplementation(invocation.command, invocation.args, {
|
|
49
91
|
stdio: "inherit",
|
|
50
|
-
shell:
|
|
92
|
+
shell: invocation.shell,
|
|
51
93
|
});
|
|
52
94
|
if (result.error || result.status !== 0) {
|
|
53
|
-
throw new Error(result.error?.message ??
|
|
95
|
+
throw new Error(result.error?.message ??
|
|
96
|
+
`${formatInvocation(invocation)} exited with status ${String(result.status)}`);
|
|
54
97
|
}
|
|
55
98
|
}
|
|
56
99
|
/**
|
|
57
100
|
* Resolves a cross-platform invocation for the Copilot CLI.
|
|
58
101
|
*/
|
|
59
|
-
function getCopilotInvocation(args) {
|
|
102
|
+
function getCopilotInvocation(args, runtimesDir) {
|
|
103
|
+
const bundledCommand = resolveBundledCopilotCommand(runtimesDir);
|
|
104
|
+
const executable = bundledCommand ?? "copilot";
|
|
60
105
|
if (process.platform === "win32") {
|
|
61
106
|
return {
|
|
62
|
-
command:
|
|
63
|
-
args
|
|
107
|
+
command: executable,
|
|
108
|
+
args,
|
|
109
|
+
source: bundledCommand ? "bundled" : "path",
|
|
110
|
+
shell: true,
|
|
64
111
|
};
|
|
65
112
|
}
|
|
66
113
|
return {
|
|
67
|
-
command:
|
|
114
|
+
command: executable,
|
|
68
115
|
args,
|
|
116
|
+
source: bundledCommand ? "bundled" : "path",
|
|
117
|
+
shell: false,
|
|
69
118
|
};
|
|
70
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolves the bundled Copilot CLI shim installed alongside the optional runtime.
|
|
122
|
+
*/
|
|
123
|
+
function resolveBundledCopilotCommand(runtimesDir) {
|
|
124
|
+
const installDir = (0, copilot_installer_1.getCopilotRuntimeInstallDir)(runtimesDir);
|
|
125
|
+
const candidates = process.platform === "win32"
|
|
126
|
+
? [path.join(installDir, "node_modules", ".bin", "copilot.cmd")]
|
|
127
|
+
: [path.join(installDir, "node_modules", ".bin", "copilot")];
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
if (fs.existsSync(candidate)) {
|
|
130
|
+
return candidate;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Renders the invocation into a short human-readable string for diagnostics.
|
|
137
|
+
*/
|
|
138
|
+
function formatInvocation(invocation) {
|
|
139
|
+
return invocation.command === "copilot"
|
|
140
|
+
? ["copilot", ...invocation.args].join(" ")
|
|
141
|
+
: [invocation.command, ...invocation.args].join(" ");
|
|
142
|
+
}
|