@minniexcode/codex-switch 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.AI.md +141 -110
  2. package/README.CN.md +215 -179
  3. package/README.md +224 -183
  4. package/dist/app/add-provider.js +16 -23
  5. package/dist/app/bridge.js +2 -1
  6. package/dist/app/edit-provider.js +30 -65
  7. package/dist/app/get-current-profile.js +15 -3
  8. package/dist/app/get-status.js +11 -8
  9. package/dist/app/list-config-profiles.js +3 -1
  10. package/dist/app/list-providers.js +10 -4
  11. package/dist/app/remove-provider.js +52 -19
  12. package/dist/app/run-doctor.js +26 -29
  13. package/dist/app/setup-codex.js +3 -3
  14. package/dist/app/show-config.js +3 -1
  15. package/dist/app/switch-provider.js +38 -6
  16. package/dist/cli/output.js +29 -19
  17. package/dist/commands/handlers.js +3 -2
  18. package/dist/commands/help.js +3 -3
  19. package/dist/commands/registry.js +29 -29
  20. package/dist/domain/config.js +293 -209
  21. package/dist/domain/providers.js +8 -0
  22. package/dist/domain/runtime-state.js +15 -15
  23. package/dist/domain/setup.js +3 -1
  24. package/dist/interaction/interactive.js +2 -2
  25. package/dist/runtime/codex-version.js +7 -0
  26. package/dist/runtime/copilot-adapter.js +326 -70
  27. package/dist/runtime/copilot-bridge-worker.js +27 -2
  28. package/dist/runtime/copilot-bridge.js +192 -10
  29. package/dist/runtime/copilot-cli.js +7 -0
  30. package/dist/runtime/copilot-installer.js +59 -1
  31. package/dist/runtime/copilot-sdk-loader.js +4 -1
  32. package/dist/storage/config-repo.js +6 -14
  33. package/docs/Design/codex-switch-v0.1.0-design.md +32 -152
  34. package/docs/Design/codex-switch-v0.1.1-design.md +22 -0
  35. package/docs/Design/codex-switch-v0.1.2-design.md +65 -0
  36. package/docs/PRD/codex-switch-prd-v0.1.0.md +65 -217
  37. package/docs/PRD/codex-switch-prd-v0.1.1.md +26 -0
  38. package/docs/PRD/codex-switch-prd-v0.1.2.md +41 -0
  39. package/docs/Reference/codex-config-reference.md +41 -0
  40. package/docs/Reference/codex-config-reference.zh-CN.md +41 -0
  41. package/docs/Tests/testing.md +1 -1
  42. package/docs/cli-usage.md +290 -223
  43. package/docs/codex-switch-command-design.md +2 -2
  44. package/docs/codex-switch-product-overview.md +18 -13
  45. package/docs/codex-switch-product-research.md +2 -2
  46. package/docs/codex-switch-technical-architecture.md +84 -1115
  47. package/package.json +2 -2
  48. package/docs/Design/codex-switch-copilot-integration-design.md +0 -517
  49. package/docs/Design/codex-switch-v0.0.10-design.md +0 -669
  50. package/docs/Design/codex-switch-v0.0.11-design.md +0 -824
  51. package/docs/Design/codex-switch-v0.0.12-design.md +0 -343
  52. package/docs/Design/codex-switch-v0.0.4-design.md +0 -874
  53. package/docs/Design/codex-switch-v0.0.5-design.md +0 -932
  54. package/docs/Design/codex-switch-v0.0.6-design.md +0 -708
  55. package/docs/Design/codex-switch-v0.0.7-design.md +0 -862
  56. package/docs/Design/codex-switch-v0.0.8-design.md +0 -132
  57. package/docs/Design/codex-switch-v0.0.9-design.md +0 -182
  58. package/docs/Design/codex-switch-v0.0.9-to-v0.0.12-roadmap.md +0 -413
  59. package/docs/PRD/codex-switch-prd-v0.0.10.md +0 -406
  60. package/docs/PRD/codex-switch-prd-v0.0.11.md +0 -577
  61. package/docs/PRD/codex-switch-prd-v0.0.12.md +0 -279
  62. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +0 -446
  63. package/docs/PRD/codex-switch-prd-v0.0.8.md +0 -62
  64. package/docs/PRD/codex-switch-prd-v0.0.9.md +0 -166
  65. package/docs/PRD/codex-switch-prd.md +0 -650
  66. package/docs/Tests/test-report-0.0.5.md +0 -163
  67. package/docs/Tests/test-report-0.0.7.md +0 -118
  68. package/docs/Tests/testing-bridge-v0.0.9.md +0 -367
@@ -47,22 +47,22 @@ exports.applyPatchOperations = applyPatchOperations;
47
47
  const os = __importStar(require("node:os"));
48
48
  const errors_1 = require("./errors");
49
49
  /**
50
- * Reads the active top-level profile from config.toml content.
50
+ * Reads the legacy top-level profile selector from config.toml content.
51
51
  */
52
52
  function parseTopLevelProfile(configContent) {
53
- return parseStructuredConfig(configContent).activeProfile;
53
+ return parseStructuredConfig(configContent).legacyProfile;
54
54
  }
55
55
  /**
56
- * Collects all named profile sections declared in config.toml content.
56
+ * Collects all named legacy profile sections declared in config.toml content.
57
57
  */
58
58
  function parseProfileNames(configContent) {
59
59
  return new Set(parseStructuredConfig(configContent).profiles.map((profile) => profile.name));
60
60
  }
61
61
  /**
62
- * Replaces or inserts the top-level profile assignment while preserving the rest of the file.
62
+ * Replaces or inserts the legacy top-level profile assignment while preserving the rest of the file.
63
63
  */
64
64
  function replaceTopLevelProfile(configContent, profile) {
65
- const plan = planConfigMutation(parseStructuredConfig(configContent), { setActiveProfile: profile });
65
+ const plan = planConfigMutation(parseStructuredConfig(configContent), { setLegacyProfile: profile });
66
66
  return applyPatchOperations(configContent, plan.operations);
67
67
  }
68
68
  /**
@@ -71,12 +71,19 @@ function replaceTopLevelProfile(configContent, profile) {
71
71
  function parseStructuredConfig(configContent) {
72
72
  const lineEnding = configContent.includes("\r\n") ? "\r\n" : "\n";
73
73
  const lines = splitWithOffsets(configContent);
74
- let activeProfile = null;
75
- let activeProfileRange = null;
74
+ let currentModel = null;
75
+ let currentModelRange = null;
76
+ let currentModelLineRange = null;
77
+ let currentModelProvider = null;
78
+ let currentModelProviderRange = null;
79
+ let currentModelProviderLineRange = null;
80
+ let legacyProfile = null;
81
+ let legacyProfileRange = null;
82
+ let legacyProfileLineRange = null;
76
83
  const profiles = [];
77
84
  const modelProviders = [];
78
85
  let currentProfile = null;
79
- let currentModelProvider = null;
86
+ let currentModelProviderSection = null;
80
87
  let inRoot = true;
81
88
  for (const line of lines) {
82
89
  const trimmed = line.content.trim();
@@ -85,9 +92,9 @@ function parseStructuredConfig(configContent) {
85
92
  if (currentProfile) {
86
93
  currentProfile.sectionEnd = line.start;
87
94
  }
88
- if (currentModelProvider) {
89
- currentModelProvider.sectionEnd = line.start;
90
- currentModelProvider = null;
95
+ if (currentModelProviderSection) {
96
+ currentModelProviderSection.sectionEnd = line.start;
97
+ currentModelProviderSection = null;
91
98
  }
92
99
  currentProfile = {
93
100
  name: headerMatch[1],
@@ -110,10 +117,10 @@ function parseStructuredConfig(configContent) {
110
117
  currentProfile.sectionEnd = line.start;
111
118
  currentProfile = null;
112
119
  }
113
- if (currentModelProvider) {
114
- currentModelProvider.sectionEnd = line.start;
120
+ if (currentModelProviderSection) {
121
+ currentModelProviderSection.sectionEnd = line.start;
115
122
  }
116
- currentModelProvider = {
123
+ currentModelProviderSection = {
117
124
  name: modelProviderHeaderMatch[1],
118
125
  sectionStart: line.start,
119
126
  sectionEnd: configContent.length,
@@ -126,8 +133,16 @@ function parseStructuredConfig(configContent) {
126
133
  requiresOpenAiAuth: null,
127
134
  wireApiValueRange: null,
128
135
  wireApi: null,
136
+ streamIdleTimeoutMsValueRange: null,
137
+ streamIdleTimeoutMs: null,
138
+ envKeyValueRange: null,
139
+ envKey: null,
140
+ envKeyInstructionsValueRange: null,
141
+ envKeyInstructions: null,
142
+ envKeyLineRange: null,
143
+ envKeyInstructionsLineRange: null,
129
144
  };
130
- modelProviders.push(currentModelProvider);
145
+ modelProviders.push(currentModelProviderSection);
131
146
  inRoot = false;
132
147
  continue;
133
148
  }
@@ -136,81 +151,97 @@ function parseStructuredConfig(configContent) {
136
151
  currentProfile.sectionEnd = line.start;
137
152
  currentProfile = null;
138
153
  }
139
- if (currentModelProvider) {
140
- currentModelProvider.sectionEnd = line.start;
141
- currentModelProvider = null;
154
+ if (currentModelProviderSection) {
155
+ currentModelProviderSection.sectionEnd = line.start;
156
+ currentModelProviderSection = null;
142
157
  }
143
158
  inRoot = false;
144
159
  continue;
145
160
  }
146
161
  if (inRoot) {
162
+ const modelMatch = matchKeyValueLine(line.content, "model");
163
+ if (modelMatch && !currentModel) {
164
+ currentModel = modelMatch.value;
165
+ currentModelRange = toAbsoluteRange(line.start, modelMatch.valueStart, modelMatch.valueEnd);
166
+ currentModelLineRange = { start: line.start, end: line.end };
167
+ }
168
+ const modelProviderMatch = matchKeyValueLine(line.content, "model_provider");
169
+ if (modelProviderMatch && !currentModelProvider) {
170
+ currentModelProvider = modelProviderMatch.value;
171
+ currentModelProviderRange = toAbsoluteRange(line.start, modelProviderMatch.valueStart, modelProviderMatch.valueEnd);
172
+ currentModelProviderLineRange = { start: line.start, end: line.end };
173
+ }
147
174
  const profileMatch = matchKeyValueLine(line.content, "profile");
148
- if (profileMatch && !activeProfile) {
149
- activeProfile = profileMatch.value;
150
- activeProfileRange = {
151
- start: line.start + profileMatch.valueStart,
152
- end: line.start + profileMatch.valueEnd,
153
- };
175
+ if (profileMatch && !legacyProfile) {
176
+ legacyProfile = profileMatch.value;
177
+ legacyProfileRange = toAbsoluteRange(line.start, profileMatch.valueStart, profileMatch.valueEnd);
178
+ legacyProfileLineRange = { start: line.start, end: line.end };
154
179
  }
155
180
  }
156
181
  if (currentProfile) {
157
182
  const modelMatch = matchKeyValueLine(line.content, "model");
158
183
  if (modelMatch) {
159
184
  currentProfile.model = modelMatch.value;
160
- currentProfile.modelValueRange = {
161
- start: line.start + modelMatch.valueStart,
162
- end: line.start + modelMatch.valueEnd,
163
- };
185
+ currentProfile.modelValueRange = toAbsoluteRange(line.start, modelMatch.valueStart, modelMatch.valueEnd);
164
186
  }
165
187
  const modelProviderMatch = matchKeyValueLine(line.content, "model_provider");
166
188
  if (modelProviderMatch) {
167
189
  currentProfile.modelProvider = modelProviderMatch.value;
168
- currentProfile.modelProviderValueRange = {
169
- start: line.start + modelProviderMatch.valueStart,
170
- end: line.start + modelProviderMatch.valueEnd,
171
- };
190
+ currentProfile.modelProviderValueRange = toAbsoluteRange(line.start, modelProviderMatch.valueStart, modelProviderMatch.valueEnd);
172
191
  }
173
192
  }
174
- if (currentModelProvider) {
193
+ if (currentModelProviderSection) {
175
194
  const baseUrlMatch = matchKeyValueLine(line.content, "base_url");
176
195
  if (baseUrlMatch) {
177
- currentModelProvider.baseUrl = baseUrlMatch.value;
178
- currentModelProvider.baseUrlValueRange = {
179
- start: line.start + baseUrlMatch.valueStart,
180
- end: line.start + baseUrlMatch.valueEnd,
181
- };
196
+ currentModelProviderSection.baseUrl = baseUrlMatch.value;
197
+ currentModelProviderSection.baseUrlValueRange = toAbsoluteRange(line.start, baseUrlMatch.valueStart, baseUrlMatch.valueEnd);
182
198
  }
183
199
  const nameMatch = matchKeyValueLine(line.content, "name");
184
200
  if (nameMatch) {
185
- currentModelProvider.providerName = nameMatch.value;
186
- currentModelProvider.nameValueRange = {
187
- start: line.start + nameMatch.valueStart,
188
- end: line.start + nameMatch.valueEnd,
189
- };
201
+ currentModelProviderSection.providerName = nameMatch.value;
202
+ currentModelProviderSection.nameValueRange = toAbsoluteRange(line.start, nameMatch.valueStart, nameMatch.valueEnd);
190
203
  }
191
204
  const requiresOpenAiAuthMatch = matchBooleanKeyValueLine(line.content, "requires_openai_auth");
192
205
  if (requiresOpenAiAuthMatch) {
193
- currentModelProvider.requiresOpenAiAuth = requiresOpenAiAuthMatch.value;
194
- currentModelProvider.requiresOpenAiAuthValueRange = {
195
- start: line.start + requiresOpenAiAuthMatch.valueStart,
196
- end: line.start + requiresOpenAiAuthMatch.valueEnd,
197
- };
206
+ currentModelProviderSection.requiresOpenAiAuth = requiresOpenAiAuthMatch.value;
207
+ currentModelProviderSection.requiresOpenAiAuthValueRange = toAbsoluteRange(line.start, requiresOpenAiAuthMatch.valueStart, requiresOpenAiAuthMatch.valueEnd);
198
208
  }
199
209
  const wireApiMatch = matchKeyValueLine(line.content, "wire_api");
200
210
  if (wireApiMatch) {
201
- currentModelProvider.wireApi = wireApiMatch.value;
202
- currentModelProvider.wireApiValueRange = {
203
- start: line.start + wireApiMatch.valueStart,
204
- end: line.start + wireApiMatch.valueEnd,
205
- };
211
+ currentModelProviderSection.wireApi = wireApiMatch.value;
212
+ currentModelProviderSection.wireApiValueRange = toAbsoluteRange(line.start, wireApiMatch.valueStart, wireApiMatch.valueEnd);
213
+ }
214
+ const streamIdleTimeoutMsMatch = matchNumberKeyValueLine(line.content, "stream_idle_timeout_ms");
215
+ if (streamIdleTimeoutMsMatch) {
216
+ currentModelProviderSection.streamIdleTimeoutMs = streamIdleTimeoutMsMatch.value;
217
+ currentModelProviderSection.streamIdleTimeoutMsValueRange = toAbsoluteRange(line.start, streamIdleTimeoutMsMatch.valueStart, streamIdleTimeoutMsMatch.valueEnd);
218
+ }
219
+ const envKeyMatch = matchKeyValueLine(line.content, "env_key");
220
+ if (envKeyMatch) {
221
+ currentModelProviderSection.envKey = envKeyMatch.value;
222
+ currentModelProviderSection.envKeyValueRange = toAbsoluteRange(line.start, envKeyMatch.valueStart, envKeyMatch.valueEnd);
223
+ currentModelProviderSection.envKeyLineRange = { start: line.start, end: line.end };
224
+ }
225
+ const envKeyInstructionsMatch = matchKeyValueLine(line.content, "env_key_instructions");
226
+ if (envKeyInstructionsMatch) {
227
+ currentModelProviderSection.envKeyInstructions = envKeyInstructionsMatch.value;
228
+ currentModelProviderSection.envKeyInstructionsValueRange = toAbsoluteRange(line.start, envKeyInstructionsMatch.valueStart, envKeyInstructionsMatch.valueEnd);
229
+ currentModelProviderSection.envKeyInstructionsLineRange = { start: line.start, end: line.end };
206
230
  }
207
231
  }
208
232
  }
209
233
  return {
210
234
  rawText: configContent,
211
235
  lineEnding,
212
- activeProfile,
213
- activeProfileRange,
236
+ currentModel,
237
+ currentModelRange,
238
+ currentModelLineRange,
239
+ currentModelProvider,
240
+ currentModelProviderRange,
241
+ currentModelProviderLineRange,
242
+ legacyProfile,
243
+ legacyProfileRange,
244
+ legacyProfileLineRange,
214
245
  profiles: profiles.map((profile) => ({
215
246
  ...profile,
216
247
  managedFieldInsertIndex: findManagedFieldInsertIndex(configContent, profile.sectionStart, profile.sectionEnd),
@@ -222,7 +253,7 @@ function parseStructuredConfig(configContent) {
222
253
  };
223
254
  }
224
255
  /**
225
- * Builds the managed/unmanaged/orphaned profile views used by config commands and diagnostics.
256
+ * Builds the legacy profile inspection views used by config commands and diagnostics.
226
257
  */
227
258
  function buildManagedProfileViews(document, providers) {
228
259
  const linkMap = buildProfileLinkMap(providers);
@@ -236,7 +267,7 @@ function buildManagedProfileViews(document, providers) {
236
267
  views.push({
237
268
  name: section.name,
238
269
  managed: linkInfo.managed,
239
- isActive: document.activeProfile === section.name,
270
+ isActive: document.currentModelProvider === section.name,
240
271
  linkedProviders: [...linkInfo.linkedProviders].sort(),
241
272
  model: section.model,
242
273
  modelProvider: section.modelProvider,
@@ -252,7 +283,7 @@ function buildManagedProfileViews(document, providers) {
252
283
  views.push({
253
284
  name: profile,
254
285
  managed: true,
255
- isActive: document.activeProfile === profile,
286
+ isActive: document.currentModelProvider === profile,
256
287
  linkedProviders: [...linkInfo.linkedProviders].sort(),
257
288
  model: null,
258
289
  modelProvider: null,
@@ -269,109 +300,60 @@ function buildManagedProfileViews(document, providers) {
269
300
  function collectConfigConsistencyIssues(document, providers) {
270
301
  const issues = [];
271
302
  const providerMap = providers?.providers ?? null;
272
- const profileLinkMap = buildProfileLinkMap(providers);
273
- for (const view of buildManagedProfileViews(document, providers)) {
274
- if (view.source === "orphaned-reference") {
275
- issues.push({
276
- code: "ORPHANED_PROFILE_REFERENCE",
277
- profile: view.name,
278
- providers: [...view.linkedProviders],
279
- });
280
- }
281
- if (view.source === "unmanaged" && view.linkedProviders.length === 0) {
282
- issues.push({
283
- code: "ORPHANED_PROFILE_SECTION",
284
- profile: view.name,
285
- });
286
- }
287
- if (view.linkedProviders.length > 1) {
288
- issues.push({
289
- code: "SHARED_PROFILE_REFERENCE",
290
- profile: view.name,
291
- providers: [...view.linkedProviders],
292
- });
293
- }
294
- if (view.source !== "orphaned-reference") {
295
- if (!view.modelProvider) {
303
+ const activeModelProvider = document.currentModelProvider;
304
+ const activeProviderSection = activeModelProvider
305
+ ? document.modelProviders.find((entry) => entry.name === activeModelProvider) ?? null
306
+ : null;
307
+ if (!document.currentModel) {
308
+ issues.push({ code: "MODEL_MISSING", modelProvider: activeModelProvider ?? "(none)" });
309
+ }
310
+ if (!document.currentModelProvider) {
311
+ issues.push({ code: "MODEL_PROVIDER_MISSING" });
312
+ }
313
+ if (document.legacyProfile) {
314
+ issues.push({ code: "LEGACY_PROFILE_SELECTOR", profile: document.legacyProfile });
315
+ }
316
+ for (const profile of document.profiles) {
317
+ issues.push({ code: "LEGACY_PROFILE_SECTION", profile: profile.name });
318
+ }
319
+ if (activeModelProvider && !activeProviderSection) {
320
+ issues.push({ code: "MODEL_PROVIDER_SECTION_MISSING", modelProvider: activeModelProvider });
321
+ }
322
+ if (activeModelProvider && activeProviderSection && !activeProviderSection.baseUrl) {
323
+ issues.push({ code: "MODEL_PROVIDER_BASE_URL_MISSING", modelProvider: activeModelProvider });
324
+ }
325
+ if (activeProviderSection?.envKey) {
326
+ issues.push({
327
+ code: "LEGACY_MODEL_PROVIDER_ENV_KEY",
328
+ modelProvider: activeProviderSection.name,
329
+ envKey: activeProviderSection.envKey,
330
+ });
331
+ }
332
+ if (activeModelProvider && providerMap) {
333
+ const linkedProviders = Object.entries(providerMap)
334
+ .filter(([, provider]) => provider.profile === activeModelProvider)
335
+ .sort(([left], [right]) => left.localeCompare(right));
336
+ if (linkedProviders.length === 1 && activeProviderSection?.baseUrl) {
337
+ const [providerName, provider] = linkedProviders[0];
338
+ if (!provider.runtime &&
339
+ typeof provider.baseUrl === "string" &&
340
+ provider.baseUrl.trim() !== "" &&
341
+ provider.baseUrl !== activeProviderSection.baseUrl) {
296
342
  issues.push({
297
- code: "MODEL_PROVIDER_MISSING",
298
- profile: view.name,
343
+ code: "PROVIDER_BASE_URL_MISMATCH",
344
+ modelProvider: activeModelProvider,
345
+ provider: providerName,
346
+ providerBaseUrl: provider.baseUrl,
347
+ configBaseUrl: activeProviderSection.baseUrl,
348
+ providerType: "direct",
299
349
  });
300
350
  }
301
- else {
302
- if (view.modelProvider !== view.name) {
303
- issues.push({
304
- code: "MODEL_PROVIDER_NAME_MISMATCH",
305
- profile: view.name,
306
- modelProvider: view.modelProvider,
307
- });
308
- }
309
- const modelProviderSection = document.modelProviders.find((entry) => entry.name === view.modelProvider);
310
- if (!modelProviderSection) {
311
- issues.push({
312
- code: "MODEL_PROVIDER_SECTION_MISSING",
313
- profile: view.name,
314
- modelProvider: view.modelProvider,
315
- });
316
- }
317
- else if (!modelProviderSection.baseUrl) {
318
- issues.push({
319
- code: "MODEL_PROVIDER_BASE_URL_MISSING",
320
- profile: view.name,
321
- modelProvider: view.modelProvider,
322
- });
323
- }
324
- else {
325
- const profileLinkInfo = profileLinkMap.get(view.name);
326
- if (profileLinkInfo &&
327
- profileLinkInfo.linkedProviders.length === 1 &&
328
- providerMap) {
329
- const providerName = profileLinkInfo.linkedProviders[0];
330
- const provider = providerMap[providerName];
331
- if (provider &&
332
- !provider.runtime &&
333
- typeof provider.baseUrl === "string" &&
334
- provider.baseUrl.trim() !== "" &&
335
- provider.baseUrl !== modelProviderSection.baseUrl) {
336
- issues.push({
337
- code: "PROVIDER_BASE_URL_MISMATCH",
338
- profile: view.name,
339
- provider: providerName,
340
- providerBaseUrl: provider.baseUrl,
341
- configBaseUrl: modelProviderSection.baseUrl,
342
- providerType: "direct",
343
- });
344
- }
345
- }
346
- }
347
- }
348
- }
349
- }
350
- if (document.activeProfile) {
351
- const activeLinkInfo = profileLinkMap.get(document.activeProfile);
352
- if (!activeLinkInfo) {
353
- issues.push({
354
- code: "UNMANAGED_ACTIVE_PROFILE",
355
- profile: document.activeProfile,
356
- });
357
- }
358
- else if (activeLinkInfo.linkedProviders.length > 1) {
359
- issues.push({
360
- code: "ACTIVE_PROVIDER_UNRESOLVED",
361
- profile: document.activeProfile,
362
- providers: [...activeLinkInfo.linkedProviders],
363
- });
364
351
  }
365
352
  }
366
- return issues.sort((left, right) => {
367
- if (left.profile === right.profile) {
368
- return left.code.localeCompare(right.code);
369
- }
370
- return left.profile.localeCompare(right.profile);
371
- });
353
+ return issues.sort((left, right) => left.code.localeCompare(right.code));
372
354
  }
373
355
  /**
374
- * Ensures the minimal managed profile fields are available before a new section is created.
356
+ * Ensures the minimal managed profile fields are available before a new legacy section is created.
375
357
  */
376
358
  function validateManagedProfileCreation(profile, fields) {
377
359
  const model = fields.model?.trim() ?? "";
@@ -391,7 +373,7 @@ function validateManagedProfileCreation(profile, fields) {
391
373
  };
392
374
  }
393
375
  /**
394
- * Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
376
+ * Computes keep/delete/switch outcomes when a provider leaves or changes model-provider bindings.
395
377
  */
396
378
  function planProfileLifecycleOutcome(args) {
397
379
  if (!args.oldProfile || args.oldProfile === args.newProfile) {
@@ -435,7 +417,7 @@ function planProfileLifecycleOutcome(args) {
435
417
  };
436
418
  }
437
419
  /**
438
- * Builds a text patch plan for top-level profile changes and profile section lifecycle changes.
420
+ * Builds a text patch plan for route fields, legacy selectors, and provider-section mutations.
439
421
  */
440
422
  function planConfigMutation(document, args) {
441
423
  const operations = [];
@@ -446,27 +428,19 @@ function planConfigMutation(document, args) {
446
428
  const updatedModelProviders = [];
447
429
  const sectionMap = new Map(document.profiles.map((profile) => [profile.name, profile]));
448
430
  const modelProviderSectionMap = new Map(document.modelProviders.map((entry) => [entry.name, entry]));
449
- if (args.setActiveProfile && args.setActiveProfile !== document.activeProfile) {
450
- const quoted = `"${args.setActiveProfile}"`;
451
- if (document.activeProfileRange) {
452
- operations.push({
453
- kind: "replace-range",
454
- start: document.activeProfileRange.start,
455
- end: document.activeProfileRange.end,
456
- text: quoted,
457
- });
458
- }
459
- else {
460
- const insertAt = findTopLevelInsertIndex(document.rawText);
461
- const text = `profile = ${quoted}${document.lineEnding}`;
462
- operations.push({
463
- kind: "insert-at",
464
- index: insertAt,
465
- text,
466
- });
467
- }
431
+ planRootFieldMutation(document, "model", document.currentModel, document.currentModelRange, document.currentModelLineRange, args.setCurrentModel, operations);
432
+ planRootFieldMutation(document, "model_provider", document.currentModelProvider, document.currentModelProviderRange, document.currentModelProviderLineRange, args.setCurrentModelProvider, operations);
433
+ if (args.setLegacyProfile !== undefined) {
434
+ planRootFieldMutation(document, "profile", document.legacyProfile, document.legacyProfileRange, document.legacyProfileLineRange, args.setLegacyProfile, operations);
435
+ }
436
+ if (args.deleteLegacyProfile && document.legacyProfileLineRange) {
437
+ operations.push({
438
+ kind: "delete-range",
439
+ start: document.legacyProfileLineRange.start,
440
+ end: expandLineDeletionStart(document.rawText, document.legacyProfileLineRange.start, document.legacyProfileLineRange.end),
441
+ });
468
442
  }
469
- for (const profileName of args.deleteProfiles ?? []) {
443
+ for (const profileName of [...(args.deleteProfiles ?? []), ...(args.deleteLegacyProfilesByName ?? [])]) {
470
444
  const section = sectionMap.get(profileName);
471
445
  if (!section) {
472
446
  continue;
@@ -503,39 +477,50 @@ function planConfigMutation(document, args) {
503
477
  for (const [profileName, fields] of Object.entries(args.upsertModelProviders ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
504
478
  const section = modelProviderSectionMap.get(profileName);
505
479
  if (!section) {
506
- const baseUrl = fields.baseUrl?.trim() ?? "";
507
- const providerName = fields.name?.trim() ?? "";
508
- if (!baseUrl) {
509
- throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
510
- profile: profileName,
511
- modelProvider: profileName,
512
- missingFields: [
513
- !baseUrl ? "base_url" : null,
514
- ].filter((value) => Boolean(value)),
515
- });
516
- }
480
+ const normalizedFields = normalizeManagedModelProviderFields(profileName, fields);
517
481
  const prefix = document.rawText.length > 0 && !document.rawText.endsWith(document.lineEnding)
518
482
  ? document.lineEnding
519
483
  : "";
520
- const requiresOpenAiAuth = fields.requiresOpenAiAuth;
521
- const wireApi = fields.wireApi?.trim() ?? "";
522
484
  operations.push({
523
485
  kind: "insert-at",
524
486
  index: document.rawText.length,
525
487
  text: `${prefix}[model_providers.${profileName}]${document.lineEnding}` +
526
- `base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}` +
527
- (providerName ? `name = ${JSON.stringify(providerName)}${document.lineEnding}` : "") +
528
- (requiresOpenAiAuth !== undefined ? `requires_openai_auth = ${String(requiresOpenAiAuth)}${document.lineEnding}` : "") +
529
- (wireApi ? `wire_api = ${JSON.stringify(wireApi)}${document.lineEnding}` : ""),
488
+ `base_url = ${JSON.stringify(normalizedFields.baseUrl)}${document.lineEnding}` +
489
+ `name = ${JSON.stringify(normalizedFields.name)}${document.lineEnding}` +
490
+ `requires_openai_auth = ${String(normalizedFields.requiresOpenAiAuth)}${document.lineEnding}` +
491
+ `wire_api = ${JSON.stringify(normalizedFields.wireApi)}${document.lineEnding}` +
492
+ (normalizedFields.streamIdleTimeoutMs !== undefined
493
+ ? `stream_idle_timeout_ms = ${String(normalizedFields.streamIdleTimeoutMs)}${document.lineEnding}`
494
+ : ""),
530
495
  });
531
496
  createdModelProviderSections.push(profileName);
532
497
  continue;
533
498
  }
534
- const sectionUpdated = planModelProviderFieldMutation(section, fields, operations);
499
+ const sectionUpdated = planModelProviderFieldMutation(document, section, normalizeManagedModelProviderFields(profileName, fields), operations);
535
500
  if (sectionUpdated) {
536
501
  updatedModelProviders.push(profileName);
537
502
  }
538
503
  }
504
+ for (const profileName of args.scrubModelProviderEnvKeys ?? []) {
505
+ const section = modelProviderSectionMap.get(profileName);
506
+ if (!section) {
507
+ continue;
508
+ }
509
+ if (section.envKeyLineRange) {
510
+ operations.push({
511
+ kind: "delete-range",
512
+ start: section.envKeyLineRange.start,
513
+ end: expandLineDeletionStart(document.rawText, section.envKeyLineRange.start, section.envKeyLineRange.end),
514
+ });
515
+ }
516
+ if (section.envKeyInstructionsLineRange) {
517
+ operations.push({
518
+ kind: "delete-range",
519
+ start: section.envKeyInstructionsLineRange.start,
520
+ end: expandLineDeletionStart(document.rawText, section.envKeyInstructionsLineRange.start, section.envKeyInstructionsLineRange.end),
521
+ });
522
+ }
523
+ }
539
524
  return {
540
525
  operations,
541
526
  createdProfileSections,
@@ -543,7 +528,8 @@ function planConfigMutation(document, args) {
543
528
  deletedProfileSections,
544
529
  updatedProfiles,
545
530
  updatedModelProviders,
546
- switchedActiveProfile: Boolean(args.setActiveProfile && args.setActiveProfile !== document.activeProfile),
531
+ switchedActiveProfile: Boolean(args.setCurrentModelProvider !== undefined && args.setCurrentModelProvider !== document.currentModelProvider) ||
532
+ Boolean(args.setLegacyProfile !== undefined && args.setLegacyProfile !== document.legacyProfile),
547
533
  };
548
534
  }
549
535
  /**
@@ -565,6 +551,9 @@ function applyPatchOperations(rawText, operations) {
565
551
  }
566
552
  return nextText;
567
553
  }
554
+ /**
555
+ * Plans managed field updates for one legacy profile section.
556
+ */
568
557
  function planSectionFieldMutation(document, section, fields, operations) {
569
558
  let updated = false;
570
559
  const modelText = fields.model !== undefined ? JSON.stringify(fields.model) : null;
@@ -612,14 +601,15 @@ function planSectionFieldMutation(document, section, fields, operations) {
612
601
  /**
613
602
  * Plans managed field updates for one model_providers section.
614
603
  */
615
- function planModelProviderFieldMutation(section, fields, operations) {
604
+ function planModelProviderFieldMutation(document, section, fields, operations) {
616
605
  let updated = false;
617
- const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
618
- const nameText = fields.name !== undefined ? JSON.stringify(fields.name) : null;
619
- const requiresOpenAiAuthText = fields.requiresOpenAiAuth !== undefined ? String(fields.requiresOpenAiAuth) : null;
620
- const wireApiText = fields.wireApi !== undefined ? JSON.stringify(fields.wireApi) : null;
606
+ const baseUrlText = JSON.stringify(fields.baseUrl);
607
+ const nameText = JSON.stringify(fields.name);
608
+ const requiresOpenAiAuthText = String(fields.requiresOpenAiAuth);
609
+ const wireApiText = JSON.stringify(fields.wireApi);
610
+ const streamIdleTimeoutMsText = fields.streamIdleTimeoutMs !== undefined ? String(fields.streamIdleTimeoutMs) : null;
621
611
  const inserts = [];
622
- if (baseUrlText !== null && section.baseUrlValueRange) {
612
+ if (section.baseUrlValueRange) {
623
613
  if (section.baseUrl !== fields.baseUrl) {
624
614
  operations.push({
625
615
  kind: "replace-range",
@@ -630,11 +620,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
630
620
  updated = true;
631
621
  }
632
622
  }
633
- else if (baseUrlText !== null) {
623
+ else {
634
624
  inserts.push(`base_url = ${baseUrlText}`);
635
625
  updated = true;
636
626
  }
637
- if (nameText !== null && section.nameValueRange) {
627
+ if (section.nameValueRange) {
638
628
  if (section.providerName !== fields.name) {
639
629
  operations.push({
640
630
  kind: "replace-range",
@@ -645,11 +635,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
645
635
  updated = true;
646
636
  }
647
637
  }
648
- else if (nameText !== null) {
638
+ else {
649
639
  inserts.push(`name = ${nameText}`);
650
640
  updated = true;
651
641
  }
652
- if (requiresOpenAiAuthText !== null && section.requiresOpenAiAuthValueRange) {
642
+ if (section.requiresOpenAiAuthValueRange) {
653
643
  if (section.requiresOpenAiAuth !== fields.requiresOpenAiAuth) {
654
644
  operations.push({
655
645
  kind: "replace-range",
@@ -660,11 +650,11 @@ function planModelProviderFieldMutation(section, fields, operations) {
660
650
  updated = true;
661
651
  }
662
652
  }
663
- else if (requiresOpenAiAuthText !== null) {
653
+ else {
664
654
  inserts.push(`requires_openai_auth = ${requiresOpenAiAuthText}`);
665
655
  updated = true;
666
656
  }
667
- if (wireApiText !== null && section.wireApiValueRange) {
657
+ if (section.wireApiValueRange) {
668
658
  if (section.wireApi !== fields.wireApi) {
669
659
  operations.push({
670
660
  kind: "replace-range",
@@ -675,19 +665,85 @@ function planModelProviderFieldMutation(section, fields, operations) {
675
665
  updated = true;
676
666
  }
677
667
  }
678
- else if (wireApiText !== null) {
668
+ else {
679
669
  inserts.push(`wire_api = ${wireApiText}`);
680
670
  updated = true;
681
671
  }
672
+ if (streamIdleTimeoutMsText !== null) {
673
+ if (section.streamIdleTimeoutMsValueRange) {
674
+ if (section.streamIdleTimeoutMs !== fields.streamIdleTimeoutMs) {
675
+ operations.push({
676
+ kind: "replace-range",
677
+ start: section.streamIdleTimeoutMsValueRange.start,
678
+ end: section.streamIdleTimeoutMsValueRange.end,
679
+ text: streamIdleTimeoutMsText,
680
+ });
681
+ updated = true;
682
+ }
683
+ }
684
+ else {
685
+ inserts.push(`stream_idle_timeout_ms = ${streamIdleTimeoutMsText}`);
686
+ updated = true;
687
+ }
688
+ }
682
689
  if (inserts.length > 0) {
683
690
  operations.push({
684
691
  kind: "insert-at",
685
692
  index: section.managedFieldInsertIndex,
686
- text: `${inserts.join("\n")}\n`,
693
+ text: `${inserts.join(document.lineEnding)}${document.lineEnding}`,
687
694
  });
688
695
  }
689
696
  return updated;
690
697
  }
698
+ function planRootFieldMutation(document, key, currentValue, currentValueRange, currentLineRange, nextValue, operations) {
699
+ if (nextValue === undefined) {
700
+ return;
701
+ }
702
+ if (nextValue === null) {
703
+ if (currentLineRange) {
704
+ operations.push({
705
+ kind: "delete-range",
706
+ start: currentLineRange.start,
707
+ end: expandLineDeletionStart(document.rawText, currentLineRange.start, currentLineRange.end),
708
+ });
709
+ }
710
+ return;
711
+ }
712
+ if (currentValueRange) {
713
+ if (currentValue !== nextValue) {
714
+ operations.push({
715
+ kind: "replace-range",
716
+ start: currentValueRange.start,
717
+ end: currentValueRange.end,
718
+ text: JSON.stringify(nextValue),
719
+ });
720
+ }
721
+ return;
722
+ }
723
+ const insertAt = findTopLevelInsertIndex(document.rawText);
724
+ operations.push({
725
+ kind: "insert-at",
726
+ index: insertAt,
727
+ text: `${key} = ${JSON.stringify(nextValue)}${document.lineEnding}`,
728
+ });
729
+ }
730
+ function normalizeManagedModelProviderFields(profileName, fields) {
731
+ const baseUrl = fields.baseUrl?.trim() ?? "";
732
+ if (!baseUrl) {
733
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
734
+ profile: profileName,
735
+ modelProvider: profileName,
736
+ missingFields: ["base_url"],
737
+ });
738
+ }
739
+ return {
740
+ baseUrl,
741
+ name: fields.name?.trim() || profileName,
742
+ requiresOpenAiAuth: fields.requiresOpenAiAuth ?? true,
743
+ wireApi: fields.wireApi?.trim() || "responses",
744
+ streamIdleTimeoutMs: fields.streamIdleTimeoutMs,
745
+ };
746
+ }
691
747
  function splitWithOffsets(value) {
692
748
  if (value.length === 0) {
693
749
  return [];
@@ -746,6 +802,21 @@ function matchBooleanKeyValueLine(line, key) {
746
802
  valueEnd,
747
803
  };
748
804
  }
805
+ function matchNumberKeyValueLine(line, key) {
806
+ const match = line.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*(\\d+)\\s*(#.*)?$`));
807
+ if (!match || match.index === undefined) {
808
+ return null;
809
+ }
810
+ const valueStart = line.indexOf(match[1], match.index);
811
+ if (valueStart === -1) {
812
+ return null;
813
+ }
814
+ return {
815
+ value: Number(match[1]),
816
+ valueStart,
817
+ valueEnd: valueStart + match[1].length,
818
+ };
819
+ }
749
820
  function findManagedFieldInsertIndex(rawText, sectionStart, sectionEnd) {
750
821
  const sectionText = rawText.slice(sectionStart, sectionEnd);
751
822
  const lines = splitWithOffsets(sectionText);
@@ -812,6 +883,19 @@ function expandDeletionEnd(rawText, sectionStart, sectionEnd) {
812
883
  }
813
884
  return end;
814
885
  }
886
+ function expandLineDeletionStart(rawText, start, end) {
887
+ let nextEnd = end;
888
+ while (nextEnd < rawText.length && (rawText[nextEnd] === "\r" || rawText[nextEnd] === "\n")) {
889
+ nextEnd += 1;
890
+ }
891
+ return nextEnd;
892
+ }
893
+ function toAbsoluteRange(lineStart, valueStart, valueEnd) {
894
+ return {
895
+ start: lineStart + valueStart,
896
+ end: lineStart + valueEnd,
897
+ };
898
+ }
815
899
  function escapeRegExp(value) {
816
900
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
817
901
  }