@minniexcode/codex-switch 0.0.7 → 0.0.9
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.md +1 -1
- package/dist/app/add-provider.js +70 -3
- package/dist/app/bridge.js +296 -0
- package/dist/app/get-status.js +39 -1
- package/dist/app/run-doctor.js +68 -1
- package/dist/app/setup-codex.js +2 -2
- package/dist/app/switch-provider.js +75 -1
- package/dist/cli.js +1 -1
- package/dist/commands/handlers.js +58 -2
- package/dist/commands/help.js +1 -0
- package/dist/commands/registry.js +56 -2
- package/dist/domain/providers.js +74 -0
- package/dist/runtime/copilot-adapter.js +173 -0
- package/dist/runtime/copilot-bridge-worker.js +25 -0
- package/dist/runtime/copilot-bridge.js +474 -0
- package/dist/runtime/copilot-installer.js +125 -0
- package/dist/runtime/copilot-sdk-loader.js +59 -0
- package/dist/storage/fs-utils.js +3 -0
- package/dist/storage/runtime-state-repo.js +80 -0
- package/docs/Design/codex-switch-v0.0.8-design.md +132 -0
- package/docs/Design/codex-switch-v0.0.9-design.md +182 -0
- package/docs/Design/codex-switch-v0.0.9-to-v0.0.12-roadmap.md +413 -0
- package/docs/PRD/codex-switch-prd-v0.0.8.md +62 -0
- package/docs/PRD/codex-switch-prd-v0.0.9.md +166 -0
- package/docs/Tests/testing-bridge-v0.0.9.md +367 -0
- package/package.json +1 -1
- /package/docs/{test-report-0.0.5.md → Tests/test-report-0.0.5.md} +0 -0
- /package/docs/{test-report-0.0.7.md → Tests/test-report-0.0.7.md} +0 -0
- /package/docs/{testing.md → Tests/testing.md} +0 -0
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.switchProvider = switchProvider;
|
|
4
4
|
const errors_1 = require("../domain/errors");
|
|
5
|
+
const providers_1 = require("../domain/providers");
|
|
5
6
|
const config_repo_1 = require("../storage/config-repo");
|
|
6
7
|
const providers_repo_1 = require("../storage/providers-repo");
|
|
7
8
|
const auth_repo_1 = require("../storage/auth-repo");
|
|
9
|
+
const copilot_bridge_1 = require("../runtime/copilot-bridge");
|
|
10
|
+
const copilot_installer_1 = require("../runtime/copilot-installer");
|
|
11
|
+
const copilot_adapter_1 = require("../runtime/copilot-adapter");
|
|
8
12
|
const run_mutation_1 = require("./run-mutation");
|
|
9
13
|
/**
|
|
10
14
|
* Switches the active Codex profile and rewrites auth.json for the target provider.
|
|
11
15
|
*/
|
|
12
|
-
function switchProvider(args) {
|
|
16
|
+
async function switchProvider(args) {
|
|
13
17
|
const providers = (0, providers_repo_1.readProvidersFile)(args.providersPath);
|
|
14
18
|
const provider = providers.providers[args.providerName];
|
|
15
19
|
if (!provider) {
|
|
@@ -27,6 +31,76 @@ function switchProvider(args) {
|
|
|
27
31
|
runtimeEnvKey: envKey,
|
|
28
32
|
});
|
|
29
33
|
}
|
|
34
|
+
if ((0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
35
|
+
const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
|
|
36
|
+
if (!installStatus.installed) {
|
|
37
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed.", {
|
|
38
|
+
installDir: installStatus.installDir,
|
|
39
|
+
packageName: installStatus.packageName,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
await (0, copilot_adapter_1.readCopilotAuthState)();
|
|
43
|
+
const bridge = await (0, copilot_bridge_1.ensureCopilotBridge)(args.providerName, provider);
|
|
44
|
+
const nextProvider = bridge.portChanged
|
|
45
|
+
? (0, providers_1.cleanProviderRecord)({
|
|
46
|
+
...provider,
|
|
47
|
+
baseUrl: bridge.baseUrl,
|
|
48
|
+
runtime: {
|
|
49
|
+
...provider.runtime,
|
|
50
|
+
bridgePort: bridge.port,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
: provider;
|
|
54
|
+
try {
|
|
55
|
+
return (0, run_mutation_1.runMutation)({
|
|
56
|
+
codexDir: args.codexDir,
|
|
57
|
+
backupsDir: args.backupsDir,
|
|
58
|
+
latestBackupPath: args.latestBackupPath,
|
|
59
|
+
operation: "switch",
|
|
60
|
+
files: [
|
|
61
|
+
{ absolutePath: args.configPath, relativePath: "config.toml" },
|
|
62
|
+
{ absolutePath: args.authPath, relativePath: "auth.json" },
|
|
63
|
+
],
|
|
64
|
+
mutate: () => {
|
|
65
|
+
const configPlan = (0, config_repo_1.createConfigMutationPlan)(document, {
|
|
66
|
+
setActiveProfile: provider.profile,
|
|
67
|
+
upsertModelProviders: bridge.portChanged
|
|
68
|
+
? {
|
|
69
|
+
[provider.profile]: {
|
|
70
|
+
baseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(nextProvider.runtime),
|
|
71
|
+
envKey,
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: undefined,
|
|
75
|
+
});
|
|
76
|
+
if (bridge.portChanged) {
|
|
77
|
+
(0, providers_repo_1.writeProvidersFile)(args.providersPath, {
|
|
78
|
+
providers: {
|
|
79
|
+
...providers.providers,
|
|
80
|
+
[args.providerName]: nextProvider,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
(0, config_repo_1.applyConfigMutation)(args.configPath, document, configPlan);
|
|
85
|
+
const existingAuth = (0, auth_repo_1.readAuthFileIfExists)(args.authPath);
|
|
86
|
+
(0, auth_repo_1.writeAuthFile)(args.authPath, nextProvider, existingAuth ?? undefined);
|
|
87
|
+
return {
|
|
88
|
+
provider: args.providerName,
|
|
89
|
+
profile: nextProvider.profile,
|
|
90
|
+
envKey: nextProvider.envKey,
|
|
91
|
+
portChanged: bridge.portChanged,
|
|
92
|
+
bridgePort: bridge.port,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!bridge.reused) {
|
|
99
|
+
(0, copilot_bridge_1.stopCopilotBridge)();
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
30
104
|
return (0, run_mutation_1.runMutation)({
|
|
31
105
|
codexDir: args.codexDir,
|
|
32
106
|
backupsDir: args.backupsDir,
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const args_1 = require("./commands/args");
|
|
|
9
9
|
const help_1 = require("./commands/help");
|
|
10
10
|
const errors_1 = require("./domain/errors");
|
|
11
11
|
const output_1 = require("./cli/output");
|
|
12
|
-
const VERSION = "0.0.
|
|
12
|
+
const VERSION = "0.0.8";
|
|
13
13
|
/**
|
|
14
14
|
* Prints the command help text to stdout.
|
|
15
15
|
*/
|
|
@@ -48,6 +48,7 @@ const list_providers_1 = require("../app/list-providers");
|
|
|
48
48
|
const remove_provider_1 = require("../app/remove-provider");
|
|
49
49
|
const rollback_backup_1 = require("../app/rollback-backup");
|
|
50
50
|
const run_doctor_1 = require("../app/run-doctor");
|
|
51
|
+
const bridge_1 = require("../app/bridge");
|
|
51
52
|
const setup_codex_1 = require("../app/setup-codex");
|
|
52
53
|
const show_config_1 = require("../app/show-config");
|
|
53
54
|
const show_provider_1 = require("../app/show-provider");
|
|
@@ -58,6 +59,7 @@ const providers_1 = require("../domain/providers");
|
|
|
58
59
|
const add_interactive_1 = require("../interaction/add-interactive");
|
|
59
60
|
const interactive_1 = require("../interaction/interactive");
|
|
60
61
|
const prompt_1 = require("../interaction/prompt");
|
|
62
|
+
const copilot_installer_1 = require("../runtime/copilot-installer");
|
|
61
63
|
const config_repo_1 = require("../storage/config-repo");
|
|
62
64
|
const codex_paths_1 = require("../storage/codex-paths");
|
|
63
65
|
const providers_repo_1 = require("../storage/providers-repo");
|
|
@@ -89,6 +91,36 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
89
91
|
return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
|
|
90
92
|
case "status":
|
|
91
93
|
return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath, paths.authPath);
|
|
94
|
+
case "bridge-start": {
|
|
95
|
+
const providerName = parsed.positionals[0] ?? null;
|
|
96
|
+
return (0, bridge_1.startBridge)({
|
|
97
|
+
providersPath: paths.providersPath,
|
|
98
|
+
configPath: paths.configPath,
|
|
99
|
+
providerName,
|
|
100
|
+
runtime,
|
|
101
|
+
json: ctx.options.json,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
case "bridge-stop": {
|
|
105
|
+
const providerName = parsed.positionals[0] ?? null;
|
|
106
|
+
return (0, bridge_1.stopBridge)({
|
|
107
|
+
providersPath: paths.providersPath,
|
|
108
|
+
configPath: paths.configPath,
|
|
109
|
+
providerName,
|
|
110
|
+
runtime,
|
|
111
|
+
json: ctx.options.json,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
case "bridge-status": {
|
|
115
|
+
const providerName = parsed.positionals[0] ?? null;
|
|
116
|
+
return (0, bridge_1.statusBridge)({
|
|
117
|
+
providersPath: paths.providersPath,
|
|
118
|
+
configPath: paths.configPath,
|
|
119
|
+
providerName,
|
|
120
|
+
runtime,
|
|
121
|
+
json: ctx.options.json,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
92
124
|
case "init": {
|
|
93
125
|
let codexDir = ctx.options.codexDir;
|
|
94
126
|
const candidates = (0, config_repo_1.findCodexDirCandidates)(ctx.options.codexDirExplicit ? ctx.options.codexDir : null);
|
|
@@ -155,6 +187,9 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
155
187
|
if (!providerName) {
|
|
156
188
|
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
|
|
157
189
|
}
|
|
190
|
+
if ((0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk")) {
|
|
191
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is only supported with add --copilot.");
|
|
192
|
+
}
|
|
158
193
|
return (0, switch_provider_1.switchProvider)({
|
|
159
194
|
codexDir: paths.codexDir,
|
|
160
195
|
backupsDir: paths.backupsDir,
|
|
@@ -221,7 +256,22 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
221
256
|
let note = (0, args_1.getSingleOption)(parsed.commandOptions, "--note", false);
|
|
222
257
|
let tags = parsed.commandOptions.get("--tag") ?? [];
|
|
223
258
|
let createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
|
|
224
|
-
|
|
259
|
+
const copilot = (0, args_1.hasFlag)(parsed.commandOptions, "--copilot");
|
|
260
|
+
const bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
|
|
261
|
+
const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
|
|
262
|
+
const bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
|
|
263
|
+
let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
|
|
264
|
+
const bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
|
|
265
|
+
if (copilot && apiKey) {
|
|
266
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
|
|
267
|
+
}
|
|
268
|
+
if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
|
|
269
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
|
|
270
|
+
}
|
|
271
|
+
if (copilot && !installCopilotSdk && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && !(0, copilot_installer_1.probeCopilotSdkInstall)().installed) {
|
|
272
|
+
installCopilotSdk = await runtime.confirmAction("The optional Copilot SDK runtime is not installed. Install it now?");
|
|
273
|
+
}
|
|
274
|
+
if (!providerName || !profile || (!apiKey && !copilot)) {
|
|
225
275
|
if (ctx.options.json || !runtime.isInteractive()) {
|
|
226
276
|
throw (0, add_interactive_1.createNonInteractiveAddError)();
|
|
227
277
|
}
|
|
@@ -251,12 +301,18 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
251
301
|
authPath: paths.authPath,
|
|
252
302
|
providerName,
|
|
253
303
|
profile,
|
|
254
|
-
apiKey,
|
|
304
|
+
apiKey: apiKey ?? "",
|
|
255
305
|
baseUrl,
|
|
256
306
|
model,
|
|
257
307
|
note,
|
|
258
308
|
tags,
|
|
259
309
|
createProfile,
|
|
310
|
+
copilot,
|
|
311
|
+
bridgeHost,
|
|
312
|
+
bridgePort,
|
|
313
|
+
bridgeApiKey,
|
|
314
|
+
installCopilotSdk,
|
|
315
|
+
interactive: (0, interactive_1.canPrompt)(runtime, ctx.options.json),
|
|
260
316
|
});
|
|
261
317
|
}
|
|
262
318
|
case "edit": {
|
package/dist/commands/help.js
CHANGED
|
@@ -41,6 +41,48 @@ exports.COMMANDS = [
|
|
|
41
41
|
],
|
|
42
42
|
examples: ["codexs config list-profiles", "codexs config list-profiles --json"],
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
id: "bridge-start",
|
|
46
|
+
tokens: ["bridge", "start"],
|
|
47
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
48
|
+
group: "write",
|
|
49
|
+
summary: "Start or reuse the managed Copilot bridge.",
|
|
50
|
+
usage: ["codexs bridge start [provider] [--json] [--codex-dir <path>]"],
|
|
51
|
+
details: [
|
|
52
|
+
"Resolves a Copilot bridge provider by explicit name, active provider, sole provider, or TTY selection.",
|
|
53
|
+
"Reuses a healthy bridge for the same provider and replaces a different managed provider when needed.",
|
|
54
|
+
"If the preferred port is occupied, automatically selects another free 5-digit port and persists it.",
|
|
55
|
+
],
|
|
56
|
+
examples: ["codexs bridge start", "codexs bridge start copilot-main"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "bridge-stop",
|
|
60
|
+
tokens: ["bridge", "stop"],
|
|
61
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
62
|
+
group: "recovery",
|
|
63
|
+
summary: "Stop the managed Copilot bridge.",
|
|
64
|
+
usage: ["codexs bridge stop [provider] [--json] [--codex-dir <path>]"],
|
|
65
|
+
details: [
|
|
66
|
+
"Prefers the runtime-state instance when present and uses an explicit provider as a guard.",
|
|
67
|
+
"Clears the runtime-state manifest without mutating providers.json or auth.json.",
|
|
68
|
+
"Is idempotent when no managed bridge is currently running.",
|
|
69
|
+
],
|
|
70
|
+
examples: ["codexs bridge stop", "codexs bridge stop copilot-main"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "bridge-status",
|
|
74
|
+
tokens: ["bridge", "status"],
|
|
75
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
76
|
+
group: "read",
|
|
77
|
+
summary: "Inspect the managed Copilot bridge.",
|
|
78
|
+
usage: ["codexs bridge status [provider] [--json] [--codex-dir <path>]"],
|
|
79
|
+
details: [
|
|
80
|
+
"Reports runtime-state, provider binding, and whether the live worker matches the expected provider.",
|
|
81
|
+
"Prefers the runtime-state instance when one is present.",
|
|
82
|
+
"Uses an explicit provider as a guard instead of silently switching targets.",
|
|
83
|
+
],
|
|
84
|
+
examples: ["codexs bridge status", "codexs bridge status copilot-main"],
|
|
85
|
+
},
|
|
44
86
|
{
|
|
45
87
|
id: "init",
|
|
46
88
|
tokens: ["init"],
|
|
@@ -128,6 +170,7 @@ exports.COMMANDS = [
|
|
|
128
170
|
usage: ["codexs status [--json] [--codex-dir <path>]"],
|
|
129
171
|
details: [
|
|
130
172
|
"Reports file presence, current profile, and whether the live profile is mapped.",
|
|
173
|
+
"When the active provider uses a local runtime bridge, status also reports bridge and SDK state.",
|
|
131
174
|
"Surfaces config consistency signals without mutating any files.",
|
|
132
175
|
"Use doctor for deeper diagnostics.",
|
|
133
176
|
],
|
|
@@ -160,6 +203,7 @@ exports.COMMANDS = [
|
|
|
160
203
|
summary: "Add a provider with explicit flags or progressive TTY prompts.",
|
|
161
204
|
usage: [
|
|
162
205
|
"codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
206
|
+
"codexs add <provider> --copilot --profile <name> [--bridge-host <host>] [--bridge-port <port>] [--bridge-api-key <secret>] [--install-copilot-sdk]",
|
|
163
207
|
"codexs add <provider> --profile <name> --api-key <key> --create-profile --model <name> --base-url <url>",
|
|
164
208
|
"codexs add [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
165
209
|
],
|
|
@@ -170,8 +214,13 @@ exports.COMMANDS = [
|
|
|
170
214
|
"Interactive tags use preset multi-select only.",
|
|
171
215
|
"Automation and non-TTY environments must pass all required values explicitly.",
|
|
172
216
|
"Creating a missing profile section requires --create-profile together with --model and --base-url.",
|
|
217
|
+
"Use --copilot to create a GitHub Copilot bridge provider backed by the official SDK.",
|
|
218
|
+
],
|
|
219
|
+
examples: [
|
|
220
|
+
"codexs add packycode --profile packycode --api-key sk-xxx",
|
|
221
|
+
"codexs add copilot-main --copilot --profile copilot-main --install-copilot-sdk",
|
|
222
|
+
"codexs add",
|
|
173
223
|
],
|
|
174
|
-
examples: ["codexs add packycode --profile packycode --api-key sk-xxx", "codexs add packycode --profile packycode", "codexs add"],
|
|
175
224
|
},
|
|
176
225
|
{
|
|
177
226
|
id: "switch",
|
|
@@ -184,6 +233,7 @@ exports.COMMANDS = [
|
|
|
184
233
|
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
185
234
|
"When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
|
|
186
235
|
"Switch updates the active config profile and rewrites auth.json from the provider envKey/apiKey pair.",
|
|
236
|
+
"Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
|
|
187
237
|
"Backs up config.toml and auth.json, then rolls back on failure.",
|
|
188
238
|
],
|
|
189
239
|
examples: ["codexs switch freemodel", "codexs switch packycode --json"],
|
|
@@ -252,7 +302,11 @@ exports.COMMANDS = [
|
|
|
252
302
|
group: "recovery",
|
|
253
303
|
summary: "Run configuration and environment diagnostics.",
|
|
254
304
|
usage: ["codexs doctor [--json] [--codex-dir <path>]"],
|
|
255
|
-
details: [
|
|
305
|
+
details: [
|
|
306
|
+
"Checks the expected config files, provider/profile consistency, and Codex CLI availability.",
|
|
307
|
+
"Copilot bridge providers add runtime dependency, auth, and bridge health diagnostics.",
|
|
308
|
+
"Returns structured issues so users and AI agents can act on them.",
|
|
309
|
+
],
|
|
256
310
|
examples: ["codexs doctor", "codexs doctor --json"],
|
|
257
311
|
},
|
|
258
312
|
{
|
package/dist/domain/providers.js
CHANGED
|
@@ -6,6 +6,9 @@ exports.sortProviders = sortProviders;
|
|
|
6
6
|
exports.findProviderByProfile = findProviderByProfile;
|
|
7
7
|
exports.findProvidersByProfile = findProvidersByProfile;
|
|
8
8
|
exports.maskSecret = maskSecret;
|
|
9
|
+
exports.isRuntimeBackedProvider = isRuntimeBackedProvider;
|
|
10
|
+
exports.isCopilotBridgeProvider = isCopilotBridgeProvider;
|
|
11
|
+
exports.buildCopilotBridgeBaseUrl = buildCopilotBridgeBaseUrl;
|
|
9
12
|
/**
|
|
10
13
|
* Validates and normalizes unknown JSON into the providers.json domain model.
|
|
11
14
|
*/
|
|
@@ -42,6 +45,13 @@ function validateProvidersShape(input) {
|
|
|
42
45
|
(!Array.isArray(provider.tags) || provider.tags.some((tag) => typeof tag !== "string"))) {
|
|
43
46
|
throw new Error(`Provider "${name}" has invalid tags.`);
|
|
44
47
|
}
|
|
48
|
+
if (provider.runtime !== undefined) {
|
|
49
|
+
validateProviderRuntime(name, provider.runtime);
|
|
50
|
+
const expectedBaseUrl = buildCopilotBridgeBaseUrl(provider.runtime);
|
|
51
|
+
if (typeof provider.baseUrl !== "string" || provider.baseUrl.trim() !== expectedBaseUrl) {
|
|
52
|
+
throw new Error(`Provider "${name}" baseUrl must match runtime bridge base URL "${expectedBaseUrl}".`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
45
55
|
// Normalize provider fields during validation so the persisted format stays clean.
|
|
46
56
|
providers[name] = cleanProviderRecord({
|
|
47
57
|
profile: provider.profile,
|
|
@@ -50,6 +60,7 @@ function validateProvidersShape(input) {
|
|
|
50
60
|
baseUrl: provider.baseUrl,
|
|
51
61
|
note: provider.note,
|
|
52
62
|
tags: provider.tags,
|
|
63
|
+
runtime: provider.runtime,
|
|
53
64
|
});
|
|
54
65
|
}
|
|
55
66
|
return { providers };
|
|
@@ -72,6 +83,18 @@ function cleanProviderRecord(record) {
|
|
|
72
83
|
if (record.tags && record.tags.length > 0) {
|
|
73
84
|
next.tags = record.tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
74
85
|
}
|
|
86
|
+
if (record.runtime) {
|
|
87
|
+
next.runtime = {
|
|
88
|
+
kind: record.runtime.kind,
|
|
89
|
+
upstream: record.runtime.upstream,
|
|
90
|
+
bridgeHost: record.runtime.bridgeHost.trim(),
|
|
91
|
+
bridgePort: record.runtime.bridgePort,
|
|
92
|
+
bridgePath: record.runtime.bridgePath,
|
|
93
|
+
premiumRequests: record.runtime.premiumRequests,
|
|
94
|
+
authSource: record.runtime.authSource,
|
|
95
|
+
sdkInstallMode: record.runtime.sdkInstallMode,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
75
98
|
return next;
|
|
76
99
|
}
|
|
77
100
|
/**
|
|
@@ -114,3 +137,54 @@ function maskSecret(value) {
|
|
|
114
137
|
}
|
|
115
138
|
return `${value.slice(0, 3)}***${value.slice(-2)}`;
|
|
116
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Returns whether one provider record relies on an auxiliary runtime component.
|
|
142
|
+
*/
|
|
143
|
+
function isRuntimeBackedProvider(provider) {
|
|
144
|
+
return Boolean(provider.runtime);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns whether one provider uses the GitHub Copilot SDK bridge runtime.
|
|
148
|
+
*/
|
|
149
|
+
function isCopilotBridgeProvider(provider) {
|
|
150
|
+
return provider.runtime?.kind === "copilot-sdk-bridge";
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Builds the canonical local bridge URL for one Copilot runtime provider.
|
|
154
|
+
*/
|
|
155
|
+
function buildCopilotBridgeBaseUrl(runtime) {
|
|
156
|
+
return `http://${runtime.bridgeHost}:${runtime.bridgePort}${runtime.bridgePath}`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Validates one runtime-backed provider block.
|
|
160
|
+
*/
|
|
161
|
+
function validateProviderRuntime(name, runtime) {
|
|
162
|
+
if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
|
|
163
|
+
throw new Error(`Provider "${name}" has an invalid runtime block.`);
|
|
164
|
+
}
|
|
165
|
+
const record = runtime;
|
|
166
|
+
if (record.kind !== "copilot-sdk-bridge") {
|
|
167
|
+
throw new Error(`Provider "${name}" has an unsupported runtime kind.`);
|
|
168
|
+
}
|
|
169
|
+
if (record.upstream !== "github-copilot") {
|
|
170
|
+
throw new Error(`Provider "${name}" has an invalid runtime upstream.`);
|
|
171
|
+
}
|
|
172
|
+
if (typeof record.bridgeHost !== "string" || record.bridgeHost.trim() === "") {
|
|
173
|
+
throw new Error(`Provider "${name}" has an invalid runtime bridgeHost.`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof record.bridgePort !== "number" || !Number.isInteger(record.bridgePort) || record.bridgePort <= 0) {
|
|
176
|
+
throw new Error(`Provider "${name}" has an invalid runtime bridgePort.`);
|
|
177
|
+
}
|
|
178
|
+
if (record.bridgePath !== "/v1") {
|
|
179
|
+
throw new Error(`Provider "${name}" has an invalid runtime bridgePath.`);
|
|
180
|
+
}
|
|
181
|
+
if (record.premiumRequests !== true) {
|
|
182
|
+
throw new Error(`Provider "${name}" must enable runtime premiumRequests.`);
|
|
183
|
+
}
|
|
184
|
+
if (record.authSource !== "official-sdk") {
|
|
185
|
+
throw new Error(`Provider "${name}" has an invalid runtime authSource.`);
|
|
186
|
+
}
|
|
187
|
+
if (record.sdkInstallMode !== "lazy") {
|
|
188
|
+
throw new Error(`Provider "${name}" has an invalid runtime sdkInstallMode.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.probeCopilotSdkRuntime = probeCopilotSdkRuntime;
|
|
4
|
+
exports.requireCopilotSdk = requireCopilotSdk;
|
|
5
|
+
exports.readCopilotAuthState = readCopilotAuthState;
|
|
6
|
+
exports.sendCopilotChatCompletion = sendCopilotChatCompletion;
|
|
7
|
+
const errors_1 = require("../domain/errors");
|
|
8
|
+
const copilot_sdk_loader_1 = require("./copilot-sdk-loader");
|
|
9
|
+
const copilot_installer_1 = require("./copilot-installer");
|
|
10
|
+
/**
|
|
11
|
+
* Probes whether the optional Copilot SDK runtime is installed and loadable.
|
|
12
|
+
*/
|
|
13
|
+
function probeCopilotSdkRuntime() {
|
|
14
|
+
const status = (0, copilot_installer_1.probeCopilotSdkInstall)();
|
|
15
|
+
if (!status.installed) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
runtime: "copilot-sdk",
|
|
19
|
+
reason: "missing",
|
|
20
|
+
cause: "The optional Copilot SDK runtime is not installed.",
|
|
21
|
+
details: {
|
|
22
|
+
installDir: status.installDir,
|
|
23
|
+
packageName: status.packageName,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
runtime: "copilot-sdk",
|
|
30
|
+
version: status.packageVersion ?? undefined,
|
|
31
|
+
details: {
|
|
32
|
+
installDir: status.installDir,
|
|
33
|
+
packageName: status.packageName,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Loads the lazily installed Copilot SDK and returns the module.
|
|
39
|
+
*/
|
|
40
|
+
async function requireCopilotSdk() {
|
|
41
|
+
return (0, copilot_sdk_loader_1.loadCopilotSdk)();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Probes whether the lazily installed Copilot SDK can create a usable session.
|
|
45
|
+
*/
|
|
46
|
+
async function readCopilotAuthState() {
|
|
47
|
+
const runtime = probeCopilotSdkRuntime();
|
|
48
|
+
if (!runtime.ok) {
|
|
49
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed.", runtime.details);
|
|
50
|
+
}
|
|
51
|
+
const { client, session } = await createCopilotSession();
|
|
52
|
+
await stopCopilotClient(client);
|
|
53
|
+
return {
|
|
54
|
+
ready: Boolean(session),
|
|
55
|
+
source: "official-sdk",
|
|
56
|
+
mode: "session",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Executes a single chat-completions style request through the optional Copilot SDK when available.
|
|
61
|
+
*/
|
|
62
|
+
async function sendCopilotChatCompletion(args) {
|
|
63
|
+
const { client, session, sdk } = await createCopilotSession();
|
|
64
|
+
try {
|
|
65
|
+
const sendAndWait = resolveCallable(session, "sendAndWait") ?? resolveCallable(sdk, "sendAndWait");
|
|
66
|
+
if (!sendAndWait) {
|
|
67
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK does not expose a supported sendAndWait API.", {
|
|
68
|
+
provider: args.provider,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const prompt = Array.isArray(args.payload.messages)
|
|
72
|
+
? args.payload.messages
|
|
73
|
+
.map((entry) => {
|
|
74
|
+
const message = entry;
|
|
75
|
+
return `${String(message.role ?? "user")}: ${String(message.content ?? "")}`;
|
|
76
|
+
})
|
|
77
|
+
.join("\n")
|
|
78
|
+
: "";
|
|
79
|
+
const result = await Promise.resolve(sendAndWait({ model: args.payload.model, prompt }));
|
|
80
|
+
const content = typeof result === "string"
|
|
81
|
+
? result
|
|
82
|
+
: typeof result?.content === "string"
|
|
83
|
+
? String(result.content)
|
|
84
|
+
: typeof result?.data === "object" &&
|
|
85
|
+
typeof result.data.content === "string"
|
|
86
|
+
? String(result.data.content)
|
|
87
|
+
: JSON.stringify(result);
|
|
88
|
+
return {
|
|
89
|
+
id: `copilot-${Date.now()}`,
|
|
90
|
+
object: "chat.completion",
|
|
91
|
+
created: Math.floor(Date.now() / 1000),
|
|
92
|
+
model: args.payload.model ?? "copilot",
|
|
93
|
+
choices: [
|
|
94
|
+
{
|
|
95
|
+
index: 0,
|
|
96
|
+
message: {
|
|
97
|
+
role: "assistant",
|
|
98
|
+
content,
|
|
99
|
+
},
|
|
100
|
+
finish_reason: "stop",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await stopCopilotClient(client);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function createCopilotSession() {
|
|
110
|
+
const sdk = (await requireCopilotSdk());
|
|
111
|
+
const client = createCopilotClient(sdk);
|
|
112
|
+
const createSession = resolveCallable(client ? client : null, "createSession") ?? resolveCallable(sdk, "createSession");
|
|
113
|
+
if (!createSession) {
|
|
114
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK does not expose a supported createSession API.", {});
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const session = (await Promise.resolve(createSession({})));
|
|
118
|
+
return {
|
|
119
|
+
sdk,
|
|
120
|
+
client,
|
|
121
|
+
session,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be used.", {
|
|
126
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function createCopilotClient(sdk) {
|
|
131
|
+
const ClientCtor = resolveConstructor(sdk, "CopilotClient");
|
|
132
|
+
if (!ClientCtor) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return new ClientCtor();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_UNSUPPORTED", "The installed Copilot SDK CopilotClient could not be constructed.", {
|
|
140
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function stopCopilotClient(client) {
|
|
145
|
+
if (client && typeof client.stop === "function") {
|
|
146
|
+
await Promise.resolve(client.stop());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function resolveCallable(target, name) {
|
|
150
|
+
if (!target) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const direct = target[name];
|
|
154
|
+
if (typeof direct === "function") {
|
|
155
|
+
return direct;
|
|
156
|
+
}
|
|
157
|
+
const nestedDefault = target.default;
|
|
158
|
+
if (nestedDefault && typeof nestedDefault[name] === "function") {
|
|
159
|
+
return nestedDefault[name];
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function resolveConstructor(target, name) {
|
|
164
|
+
const direct = target[name];
|
|
165
|
+
if (typeof direct === "function") {
|
|
166
|
+
return direct;
|
|
167
|
+
}
|
|
168
|
+
const nestedDefault = target.default;
|
|
169
|
+
if (nestedDefault && typeof nestedDefault[name] === "function") {
|
|
170
|
+
return nestedDefault[name];
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const copilot_bridge_1 = require("./copilot-bridge");
|
|
4
|
+
const copilot_adapter_1 = require("./copilot-adapter");
|
|
5
|
+
async function main() {
|
|
6
|
+
const provider = process.env.CODEX_SWITCH_BRIDGE_PROVIDER ?? "copilot";
|
|
7
|
+
const host = process.env.CODEX_SWITCH_BRIDGE_HOST ?? "127.0.0.1";
|
|
8
|
+
const port = Number(process.env.CODEX_SWITCH_BRIDGE_PORT ?? "41415");
|
|
9
|
+
const apiKey = process.env.CODEX_SWITCH_BRIDGE_API_KEY ?? "";
|
|
10
|
+
await (0, copilot_bridge_1.startCopilotBridgeServer)({
|
|
11
|
+
host,
|
|
12
|
+
port,
|
|
13
|
+
apiKey,
|
|
14
|
+
executeChatCompletion: async (payload) => (0, copilot_adapter_1.sendCopilotChatCompletion)({
|
|
15
|
+
provider,
|
|
16
|
+
payload,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (require.main === module) {
|
|
21
|
+
void main().catch((error) => {
|
|
22
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
25
|
+
}
|