@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 +6 -4
- package/README.CN.md +9 -6
- package/README.md +9 -6
- package/dist/app/bridge.js +11 -0
- package/dist/app/get-status.js +9 -1
- package/dist/app/run-doctor.js +2 -0
- package/dist/app/switch-provider.js +10 -3
- package/dist/cli/output.js +53 -1
- package/dist/interaction/interactive.js +9 -2
- package/dist/runtime/copilot-adapter.js +120 -0
- package/dist/runtime/copilot-bridge-worker.js +19 -0
- package/dist/runtime/copilot-bridge.js +393 -80
- package/dist/storage/runtime-state-repo.js +8 -0
- package/docs/Design/codex-switch-v0.1.4-design.md +18 -0
- package/docs/Design/codex-switch-v0.1.5-design.md +17 -0
- package/docs/PRD/codex-switch-prd-v0.1.4.md +37 -0
- package/docs/PRD/codex-switch-prd-v0.1.5.md +42 -0
- package/docs/Tests/testing.md +4 -1
- package/docs/cli-usage.md +11 -5
- package/docs/codex-switch-product-overview.md +24 -19
- package/docs/codex-switch-technical-architecture.md +2 -0
- package/package.json +2 -2
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.
|
|
10
|
-
- Version status:
|
|
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.
|
|
9
|
+
当前包版本:`0.1.5`
|
|
10
10
|
|
|
11
|
-
|
|
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.
|
|
11
|
+
Current package version: `0.1.5`
|
|
12
12
|
|
|
13
|
-
This is the current
|
|
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
|
|
package/dist/app/bridge.js
CHANGED
|
@@ -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
|
}
|
package/dist/app/get-status.js
CHANGED
|
@@ -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:
|
|
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,
|
package/dist/app/run-doctor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
133
|
+
[provider.profile]: (0, providers_1.buildDirectModelProviderProjection)(provider.profile, resolvedBaseUrl),
|
|
127
134
|
},
|
|
128
135
|
deleteLegacyProfile: true,
|
|
129
136
|
deleteLegacyProfilesByName: [provider.profile],
|
package/dist/cli/output.js
CHANGED
|
@@ -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
|
|
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
|
});
|