agentsw 0.7.1 → 0.7.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.
- package/README.md +30 -11
- package/README.zh-CN.md +26 -11
- package/dist/apps.js +48 -44
- package/dist/commands.js +221 -74
- package/dist/config-transaction.js +169 -84
- package/dist/discover.js +55 -17
- package/dist/envfile.js +101 -20
- package/dist/fsutil.js +109 -14
- package/dist/i18n.js +2 -1
- package/dist/import.js +2 -2
- package/dist/index.js +16 -1
- package/dist/jsonc.js +4 -10
- package/dist/modelsdev.js +7 -3
- package/dist/provider-identity.js +25 -0
- package/dist/remove.js +84 -38
- package/dist/rename.js +91 -58
- package/dist/store.js +38 -7
- package/dist/target-transaction.js +25 -0
- package/dist/targets/claudecode.js +12 -3
- package/dist/targets/codex.js +34 -3
- package/dist/targets/dsh.js +117 -65
- package/dist/targets/hermes.js +98 -49
- package/dist/targets/omp.js +11 -47
- package/dist/targets/opencode.js +27 -6
- package/dist/targets/pistyle.js +12 -8
- package/dist/targets/wire.js +15 -9
- package/dist/targets/workbuddy.js +151 -76
- package/dist/yaml.js +39 -0
- package/package.json +4 -2
package/dist/rename.js
CHANGED
|
@@ -10,6 +10,8 @@ import { commitFileChanges } from "./config-transaction.js";
|
|
|
10
10
|
import { endpointKey } from "./import.js";
|
|
11
11
|
import { classifyApi, entryApi } from "./targets/wire.js";
|
|
12
12
|
import { envAssignments } from "./envfile.js";
|
|
13
|
+
import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "./provider-identity.js";
|
|
14
|
+
import { workbuddyBaseUrl } from "./targets/workbuddy.js";
|
|
13
15
|
function readExisting(file) {
|
|
14
16
|
try {
|
|
15
17
|
return fs.readFileSync(file, "utf8");
|
|
@@ -206,7 +208,6 @@ function tomlEditor(file, before) {
|
|
|
206
208
|
},
|
|
207
209
|
};
|
|
208
210
|
}
|
|
209
|
-
const managedKey = (id) => `AGENTSW_${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
210
211
|
/** Explicit provider ID migration; never recreates providers or edits session history. */
|
|
211
212
|
export async function renameProvider(oldId, newId, opts = {}) {
|
|
212
213
|
if (!/^[a-z0-9][a-z0-9_-]*$/.test(newId))
|
|
@@ -225,9 +226,9 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
225
226
|
const provider = object(providers[oldId], configFile);
|
|
226
227
|
const changes = [];
|
|
227
228
|
const visited = new Set();
|
|
228
|
-
const
|
|
229
|
-
const newKey =
|
|
230
|
-
const assertConnection = (editor, entry) => {
|
|
229
|
+
const oldKeys = [managedCredentialRef(oldId), legacyManagedCredentialRef(oldId)];
|
|
230
|
+
const newKey = managedCredentialRef(newId);
|
|
231
|
+
const assertConnection = (editor, entry, envLookup = false) => {
|
|
231
232
|
const options = isJsonObject(entry.options) ? entry.options : {};
|
|
232
233
|
const endpoint = entry.baseUrl ?? entry.baseURL ?? entry.base_url ?? options.baseURL ?? (entry.transport ? entry.api : undefined);
|
|
233
234
|
if (typeof endpoint === "string" && /^https?:\/\//i.test(endpoint) && typeof provider.baseUrl === "string" && endpointKey(endpoint) !== endpointKey(provider.baseUrl)) {
|
|
@@ -241,10 +242,10 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
241
242
|
if (protocol && provider.protocol && protocol !== provider.protocol)
|
|
242
243
|
throw new Error(`${editor.file}: source provider protocol conflicts with the store`);
|
|
243
244
|
const rawKey = entry.apiKey ?? options.apiKey;
|
|
244
|
-
if (typeof rawKey === "string"
|
|
245
|
+
if (typeof rawKey === "string") {
|
|
245
246
|
const ref = rawKey.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/)?.[1] ?? rawKey.match(/^\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/)?.[1];
|
|
246
247
|
// Command/file references remain unresolved; no shell execution or secret-file reads.
|
|
247
|
-
const key = ref ? process.env[ref] : /^(?:!|\{file:)/.test(rawKey) ? undefined :
|
|
248
|
+
const key = ref ? process.env[ref] : /^(?:!|\{file:)/.test(rawKey) ? undefined : envLookup ? process.env[rawKey] ?? rawKey : rawKey;
|
|
248
249
|
if (key !== undefined && typeof provider.apiKey === "string" && key !== provider.apiKey)
|
|
249
250
|
throw new Error(`${editor.file}: source provider credentials conflict with the store`);
|
|
250
251
|
}
|
|
@@ -262,7 +263,7 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
262
263
|
const value = get(editor.value, at);
|
|
263
264
|
return value === undefined ? [] : Object.entries(object(value, editor.file));
|
|
264
265
|
};
|
|
265
|
-
const renameMap = (editor, at, names = ["id", "name"]) => {
|
|
266
|
+
const renameMap = (editor, at, names = ["id", "name"], envLookup = false) => {
|
|
266
267
|
const map = get(editor.value, at);
|
|
267
268
|
if (map === undefined)
|
|
268
269
|
return;
|
|
@@ -271,7 +272,13 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
271
272
|
throw new Error(`${editor.file}: destination provider ID already exists`);
|
|
272
273
|
if (!Object.hasOwn(entries, oldId))
|
|
273
274
|
return;
|
|
274
|
-
|
|
275
|
+
const entry = object(entries[oldId], editor.file);
|
|
276
|
+
assertConnection(editor, entry, envLookup);
|
|
277
|
+
if (Array.isArray(entry.models))
|
|
278
|
+
for (const model of entry.models) {
|
|
279
|
+
if (isJsonObject(model))
|
|
280
|
+
assertConnection(editor, model, envLookup);
|
|
281
|
+
}
|
|
275
282
|
for (const key of names)
|
|
276
283
|
exact(editor, [...at, oldId, key]);
|
|
277
284
|
editor.move(at, oldId, newId);
|
|
@@ -323,7 +330,7 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
323
330
|
});
|
|
324
331
|
const ompDir = path.join(home, ".omp", "agent");
|
|
325
332
|
for (const name of ["models.yml", "models.yaml"])
|
|
326
|
-
visit(path.join(ompDir, name), "yaml", (editor) => renameMap(editor, ["providers"]));
|
|
333
|
+
visit(path.join(ompDir, name), "yaml", (editor) => renameMap(editor, ["providers"], undefined, true));
|
|
327
334
|
for (const name of ["config.yml", "config.yaml"])
|
|
328
335
|
visit(path.join(ompDir, name), "yaml", (editor) => {
|
|
329
336
|
compound(editor, ["model"]);
|
|
@@ -332,7 +339,7 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
332
339
|
});
|
|
333
340
|
for (const [env, fallback] of [["PI_CODING_AGENT_DIR", ".pi/agent"], ["PRIME_AGENT_CODING_AGENT_DIR", ".prime/agent"]]) {
|
|
334
341
|
const dir = process.env[env] ? expandHome(process.env[env]) : path.join(home, fallback);
|
|
335
|
-
visit(path.join(dir, "models.json"), "json", (editor) => renameMap(editor, ["providers"]));
|
|
342
|
+
visit(path.join(dir, "models.json"), "json", (editor) => renameMap(editor, ["providers"], undefined, env === "PRIME_AGENT_CODING_AGENT_DIR"));
|
|
336
343
|
visit(path.join(dir, "settings.json"), "json", (editor) => {
|
|
337
344
|
exact(editor, ["defaultProvider"]);
|
|
338
345
|
const recent = get(editor.value, ["recentModels"]);
|
|
@@ -370,71 +377,87 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
370
377
|
list.forEach((_value, index) => exact(editor, [key, index]));
|
|
371
378
|
}
|
|
372
379
|
});
|
|
380
|
+
const migrationKey = (file, local, maps, field) => {
|
|
381
|
+
const sourceRefs = maps.flatMap((map) => map.filter(([id]) => id === oldId).map(([, entry]) => get(entry, [field])));
|
|
382
|
+
const eligible = oldKeys.filter((key) => local.has(key) &&
|
|
383
|
+
(sourceRefs.includes(key) || sourceRefs.every((ref) => ref === undefined)) &&
|
|
384
|
+
!Object.keys(providers).some((id) => id !== oldId && isManagedCredentialRef(key, id)) &&
|
|
385
|
+
!maps.some((map) => map.some(([id, entry]) => id !== oldId &&
|
|
386
|
+
(get(entry, [field]) === key || isManagedCredentialRef(key, id)))));
|
|
387
|
+
if (eligible.length > 1)
|
|
388
|
+
throw new Error(`${file}: ambiguous managed credential references`);
|
|
389
|
+
const key = eligible[0];
|
|
390
|
+
if (!key)
|
|
391
|
+
return undefined;
|
|
392
|
+
if (local.has(newKey) || process.env[newKey] !== undefined ||
|
|
393
|
+
maps.some((map) => map.some(([, entry]) => get(entry, [field]) === newKey))) {
|
|
394
|
+
throw new Error(`${file}: destination credential reference already exists`);
|
|
395
|
+
}
|
|
396
|
+
if (process.env[key] !== undefined && process.env[key] !== local.get(key))
|
|
397
|
+
throw new Error(`${file}: inherited credential overrides the local definition`);
|
|
398
|
+
if (typeof provider.apiKey === "string" && local.get(key) !== provider.apiKey)
|
|
399
|
+
throw new Error(`${file}: source provider credentials conflict with the store`);
|
|
400
|
+
return key;
|
|
401
|
+
};
|
|
373
402
|
const hermesDir = process.env.HERMES_HOME?.trim() ? expandHome(process.env.HERMES_HOME.trim()) : localAppDataDir("hermes");
|
|
374
403
|
const envFile = path.join(hermesDir, ".env");
|
|
375
404
|
const envBefore = readExisting(envFile);
|
|
376
405
|
const assignments = envBefore === undefined ? [] : envAssignments(envFile, envBefore);
|
|
377
406
|
const envValues = new Map(assignments.map((assignment) => [assignment.name, assignment.value]));
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (assignments.some((assignment) => assignment.name === newKey) || process.env[newKey] !== undefined)
|
|
381
|
-
throw new Error(`${envFile}: destination credential reference already exists`);
|
|
382
|
-
if (process.env[oldKey] !== undefined && process.env[oldKey] !== envValues.get(oldKey))
|
|
383
|
-
throw new Error(`${envFile}: inherited credential overrides the local definition`);
|
|
384
|
-
const after = applyEdits(envBefore, assignments.filter((assignment) => assignment.name === oldKey).map((assignment) => ({ offset: assignment.offset, length: oldKey.length, content: newKey })));
|
|
385
|
-
changes.push({ file: envFile, before: envBefore, after, mode: 0o600 });
|
|
386
|
-
}
|
|
387
|
-
if (!migrateEnv)
|
|
388
|
-
changes.push({ file: envFile, before: envBefore, after: envBefore });
|
|
407
|
+
let envKey;
|
|
408
|
+
let hermesPresent = false;
|
|
389
409
|
visit(path.join(hermesDir, "config.yaml"), "yaml", (editor) => {
|
|
410
|
+
hermesPresent = true;
|
|
390
411
|
const keyRef = get(editor.value, ["providers", oldId, "key_env"]);
|
|
391
412
|
const key = typeof keyRef === "string" ? process.env[keyRef] ?? envValues.get(keyRef) : undefined;
|
|
392
413
|
if (key !== undefined && provider.apiKey !== undefined && key !== provider.apiKey)
|
|
393
414
|
throw new Error(`${editor.file}: source provider credentials conflict with the store`);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
throw new Error(`${editor.file}: destination credential reference already exists`);
|
|
398
|
-
exact(editor, ["providers", id, "key_env"], oldKey, newKey);
|
|
399
|
-
}
|
|
415
|
+
envKey = migrationKey(envFile, envValues, [entries(editor, ["providers"])], "key_env");
|
|
416
|
+
if (envKey)
|
|
417
|
+
exact(editor, ["providers", oldId, "key_env"], envKey, newKey);
|
|
400
418
|
renameMap(editor, ["providers"]);
|
|
401
419
|
exact(editor, ["model", "provider"]);
|
|
402
420
|
});
|
|
421
|
+
if (!hermesPresent)
|
|
422
|
+
envKey = migrationKey(envFile, envValues, [], "key_env");
|
|
423
|
+
const envAfter = envKey ? applyEdits(envBefore, assignments.filter((assignment) => assignment.name === envKey)
|
|
424
|
+
.map((assignment) => ({ offset: assignment.offset, length: envKey.length, content: newKey }))) : envBefore;
|
|
425
|
+
changes.push({ file: envFile, before: envBefore, after: envAfter, mode: 0o600 });
|
|
403
426
|
const dshDir = process.env.DSH_HOME?.trim() ? expandHome(process.env.DSH_HOME.trim()) : localAppDataDir("dsh");
|
|
427
|
+
const settings = [];
|
|
428
|
+
for (const name of ["settings.yaml", "settings.yml", "settings.json"]) {
|
|
429
|
+
const file = path.join(dshDir, name);
|
|
430
|
+
const before = readExisting(file);
|
|
431
|
+
if (before === undefined)
|
|
432
|
+
changes.push({ file, before, after: before });
|
|
433
|
+
else
|
|
434
|
+
settings.push({ editor: name.endsWith("json") ? jsonEditor(file, before) : yamlEditor(file, before), before });
|
|
435
|
+
}
|
|
436
|
+
const dshMaps = settings.map(({ editor }) => entries(editor, ["llm-pi-ai", "providers"]));
|
|
404
437
|
const credentialFile = path.join(dshDir, ".credentials.yaml");
|
|
405
|
-
let
|
|
438
|
+
let credentialKey;
|
|
406
439
|
let credentialRefs = {};
|
|
407
440
|
visit(credentialFile, "yaml", (editor) => {
|
|
408
441
|
const version = get(editor.value, ["version"]);
|
|
409
442
|
if (version !== undefined && version !== 1)
|
|
410
443
|
throw new Error(`${credentialFile}: unsupported credential layout version`);
|
|
411
444
|
const at = version === undefined && get(editor.value, ["refs"]) === undefined ? [] : ["refs"];
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
if (Object.hasOwn(refs, newKey) || process.env[newKey] !== undefined)
|
|
417
|
-
throw new Error(`${credentialFile}: destination credential reference already exists`);
|
|
418
|
-
if (process.env[oldKey] !== undefined && process.env[oldKey] !== refs[oldKey])
|
|
419
|
-
throw new Error(`${credentialFile}: inherited credential overrides the local definition`);
|
|
420
|
-
editor.move(at, oldKey, newKey);
|
|
421
|
-
migrateCredential = true;
|
|
445
|
+
credentialRefs = object(get(editor.value, at), credentialFile);
|
|
446
|
+
credentialKey = migrationKey(credentialFile, new Map(Object.entries(credentialRefs)), dshMaps, "apiKeyEnv");
|
|
447
|
+
if (credentialKey)
|
|
448
|
+
editor.move(at, credentialKey, newKey);
|
|
422
449
|
}, 0o600);
|
|
423
|
-
for (const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
renameMap(editor, ["llm-pi-ai", "providers"], ["id", "displayName"]);
|
|
436
|
-
exact(editor, ["agent-default-model", "provider"]);
|
|
437
|
-
});
|
|
450
|
+
for (const { editor, before } of settings) {
|
|
451
|
+
const keyRef = get(editor.value, ["llm-pi-ai", "providers", oldId, "apiKeyEnv"]);
|
|
452
|
+
const key = typeof keyRef === "string" ? process.env[keyRef] ?? credentialRefs[keyRef] : undefined;
|
|
453
|
+
if (key !== undefined && provider.apiKey !== undefined && key !== provider.apiKey)
|
|
454
|
+
throw new Error(`${editor.file}: source provider credentials conflict with the store`);
|
|
455
|
+
if (credentialKey)
|
|
456
|
+
exact(editor, ["llm-pi-ai", "providers", oldId, "apiKeyEnv"], credentialKey, newKey);
|
|
457
|
+
renameMap(editor, ["llm-pi-ai", "providers"], ["id", "displayName"]);
|
|
458
|
+
exact(editor, ["agent-default-model", "provider"]);
|
|
459
|
+
finish(editor, before);
|
|
460
|
+
}
|
|
438
461
|
const workbuddyEnv = process.env.WORKBUDDY_CONFIG_DIR?.trim() ?? process.env.CODEBUDDY_CONFIG_DIR?.trim();
|
|
439
462
|
const workbuddyDir = workbuddyEnv ? expandHome(workbuddyEnv) : process.platform === "win32" ? appDataDir("workbuddy") : path.join(home, ".workbuddy");
|
|
440
463
|
visit(path.join(workbuddyDir, "models.json"), "json", (editor) => {
|
|
@@ -444,12 +467,22 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
444
467
|
return;
|
|
445
468
|
if (!Array.isArray(rows))
|
|
446
469
|
throw new Error(`${editor.file}: expected WorkBuddy models array`);
|
|
447
|
-
const base = typeof provider.baseUrl === "string" ? provider.baseUrl.replace(/\/+$/, "") : "";
|
|
448
|
-
const url = /\/v\d+$/.test(base) ? `${base}/chat/completions` : `${base}/v1/chat/completions`;
|
|
449
|
-
if (provider.name !== oldId)
|
|
450
|
-
return;
|
|
451
470
|
rows.forEach((row, index) => {
|
|
452
|
-
if (isJsonObject(row)
|
|
471
|
+
if (!isJsonObject(row))
|
|
472
|
+
return;
|
|
473
|
+
if (row.agentswProviderId === newId)
|
|
474
|
+
throw new Error(`${editor.file}: destination provider ID already exists`);
|
|
475
|
+
const owned = row.agentswProviderId === oldId;
|
|
476
|
+
const endpoint = typeof row.url === "string" ? workbuddyBaseUrl(row.url) : undefined;
|
|
477
|
+
const matches = typeof endpoint === "string" && typeof provider.baseUrl === "string" &&
|
|
478
|
+
endpointKey(endpoint) === endpointKey(provider.baseUrl) && row.apiKey === provider.apiKey;
|
|
479
|
+
if (owned && !matches)
|
|
480
|
+
throw new Error(`${editor.file}: source provider connection conflicts with the store`);
|
|
481
|
+
if (!matches || (row.agentswProviderId !== undefined && !owned))
|
|
482
|
+
return;
|
|
483
|
+
if (owned)
|
|
484
|
+
exact(editor, [...at, index, "agentswProviderId"]);
|
|
485
|
+
if (provider.name === oldId)
|
|
453
486
|
exact(editor, [...at, index, "vendor"]);
|
|
454
487
|
});
|
|
455
488
|
}, undefined, true);
|
package/dist/store.js
CHANGED
|
@@ -1,17 +1,48 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { appDataDir,
|
|
2
|
+
import { appDataDir, isDryRun, readFileSnapshot, recordPendingWrites } from "./fsutil.js";
|
|
3
|
+
import { commitFileChanges } from "./config-transaction.js";
|
|
3
4
|
export const configDir = appDataDir("agentsw");
|
|
4
5
|
export const configFile = path.join(configDir, "config.json");
|
|
6
|
+
const loadedSnapshots = new WeakMap();
|
|
5
7
|
export function loadStore() {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const snapshot = readFileSnapshot(configFile);
|
|
9
|
+
let store;
|
|
10
|
+
if (snapshot.text === undefined)
|
|
11
|
+
store = { version: 1, providers: {} };
|
|
12
|
+
else {
|
|
13
|
+
try {
|
|
14
|
+
store = JSON.parse(snapshot.text);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`${configFile}: invalid provider store JSON`);
|
|
18
|
+
}
|
|
19
|
+
if (!store || typeof store !== "object" || Array.isArray(store)) {
|
|
20
|
+
throw new Error(`${configFile}: expected a provider store object`);
|
|
21
|
+
}
|
|
22
|
+
if (store.providers === undefined)
|
|
23
|
+
store.providers = {};
|
|
24
|
+
if (!store.providers || typeof store.providers !== "object" || Array.isArray(store.providers)) {
|
|
25
|
+
throw new Error(`${configFile}: expected a providers object`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
loadedSnapshots.set(store, snapshot);
|
|
11
29
|
return store;
|
|
12
30
|
}
|
|
13
31
|
export function saveStore(store) {
|
|
14
|
-
|
|
32
|
+
// A snapshot stays associated with this object across asynchronous discovery and repeated saves.
|
|
33
|
+
const expected = loadedSnapshots.get(store) ?? readFileSnapshot(configFile);
|
|
34
|
+
const after = JSON.stringify(store, null, 2) + "\n";
|
|
35
|
+
const changes = [{ file: configFile, before: expected.text, after, mode: 0o600, expected }];
|
|
36
|
+
const dryRun = isDryRun();
|
|
37
|
+
commitFileChanges(changes, { dryRun });
|
|
38
|
+
if (dryRun) {
|
|
39
|
+
recordPendingWrites(changes);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const saved = readFileSnapshot(configFile);
|
|
43
|
+
if (saved.text !== after)
|
|
44
|
+
throw new Error(`${configFile}: configuration changed during store save`);
|
|
45
|
+
loadedSnapshots.set(store, saved);
|
|
15
46
|
}
|
|
16
47
|
export function getProvider(store, id) {
|
|
17
48
|
const p = store.providers[id];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { commitFileChanges } from "./config-transaction.js";
|
|
2
|
+
import { recordPendingWrites, stageFileWrites } from "./fsutil.js";
|
|
3
|
+
const wrappedTargets = new WeakSet();
|
|
4
|
+
/** Validate every adapter input before committing any of its configuration writes. */
|
|
5
|
+
export function transactionalTarget(target) {
|
|
6
|
+
if (wrappedTargets.has(target))
|
|
7
|
+
return target;
|
|
8
|
+
const wrapped = Object.create(Object.getPrototypeOf(target), Object.getOwnPropertyDescriptors(target));
|
|
9
|
+
for (const method of ["apply", "prune"]) {
|
|
10
|
+
const operation = target[method];
|
|
11
|
+
wrapped[method] = async function (provider) {
|
|
12
|
+
const staged = await stageFileWrites(() => operation.call(this, provider));
|
|
13
|
+
const committed = commitFileChanges(staged.changes, { dryRun: staged.dryRun, reads: staged.reads });
|
|
14
|
+
if (staged.dryRun)
|
|
15
|
+
recordPendingWrites(staged.changes);
|
|
16
|
+
return {
|
|
17
|
+
...staged.result,
|
|
18
|
+
changed: committed.files,
|
|
19
|
+
notes: committed.backupDir ? [...staged.result.notes, `backup: ${committed.backupDir}`] : staged.result.notes,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
wrappedTargets.add(wrapped);
|
|
24
|
+
return wrapped;
|
|
25
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { backupFile, home, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
|
|
4
|
+
import { isJsonObject } from "../jsonc.js";
|
|
5
|
+
import { transactionalTarget } from "../target-transaction.js";
|
|
4
6
|
import { providerIdFromBaseUrl, providerNameFromBaseUrl } from "../slug.js";
|
|
5
7
|
import { stripApiVersion } from "./wire.js";
|
|
6
8
|
const dir = path.join(home, ".claude");
|
|
@@ -9,7 +11,7 @@ const settingsFile = path.join(dir, "settings.json");
|
|
|
9
11
|
* Claude Code reads Anthropic-protocol endpoints via env vars in ~/.claude/settings.json.
|
|
10
12
|
* We merge the "env" block and leave every other setting untouched.
|
|
11
13
|
*/
|
|
12
|
-
export const claudecode = {
|
|
14
|
+
export const claudecode = transactionalTarget({
|
|
13
15
|
id: "claude",
|
|
14
16
|
name: "Claude Code",
|
|
15
17
|
protocols: ["anthropic"],
|
|
@@ -17,7 +19,11 @@ export const claudecode = {
|
|
|
17
19
|
detect: () => fs.existsSync(dir),
|
|
18
20
|
async apply(provider) {
|
|
19
21
|
const notes = [];
|
|
20
|
-
const
|
|
22
|
+
const settingsValue = readJsonIfExists(settingsFile);
|
|
23
|
+
const settings = settingsValue === undefined ? {} : settingsValue;
|
|
24
|
+
if (!isJsonObject(settings) || (settings.env !== undefined && !isJsonObject(settings.env))) {
|
|
25
|
+
throw new Error(`${settingsFile}: expected settings and env to be JSON objects`);
|
|
26
|
+
}
|
|
21
27
|
const env = { ...settings.env };
|
|
22
28
|
env.ANTHROPIC_BASE_URL = stripApiVersion(provider.baseUrl);
|
|
23
29
|
env.ANTHROPIC_AUTH_TOKEN = provider.apiKey;
|
|
@@ -38,6 +44,9 @@ export const claudecode = {
|
|
|
38
44
|
},
|
|
39
45
|
async prune(provider) {
|
|
40
46
|
const settings = readJsonIfExists(settingsFile);
|
|
47
|
+
if (settings !== undefined && (!isJsonObject(settings) || (settings.env !== undefined && !isJsonObject(settings.env)))) {
|
|
48
|
+
throw new Error(`${settingsFile}: expected settings and env to be JSON objects`);
|
|
49
|
+
}
|
|
41
50
|
const env = settings?.env;
|
|
42
51
|
if (!settings || !env || env.ANTHROPIC_BASE_URL !== stripApiVersion(provider.baseUrl)) {
|
|
43
52
|
return { app: this.id, changed: [], notes: [], skipped: "claude env does not point at this provider" };
|
|
@@ -95,4 +104,4 @@ export const claudecode = {
|
|
|
95
104
|
},
|
|
96
105
|
];
|
|
97
106
|
},
|
|
98
|
-
};
|
|
107
|
+
});
|
package/dist/targets/codex.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4
4
|
import { backupFile, home, readJsonIfExists, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
5
|
+
import { isJsonObject } from "../jsonc.js";
|
|
6
|
+
import { transactionalTarget } from "../target-transaction.js";
|
|
5
7
|
const dir = path.join(home, ".codex");
|
|
6
8
|
const configFile = path.join(dir, "config.toml");
|
|
7
9
|
const authFile = path.join(dir, "auth.json");
|
|
@@ -10,7 +12,7 @@ const authFile = path.join(dir, "auth.json");
|
|
|
10
12
|
* OpenAI-protocol endpoint; the key ships via ~/.codex/auth.json OPENAI_API_KEY.
|
|
11
13
|
* TOML round-trip via smol-toml preserves data but not comments (backed up first).
|
|
12
14
|
*/
|
|
13
|
-
export const codex = {
|
|
15
|
+
export const codex = transactionalTarget({
|
|
14
16
|
id: "codex",
|
|
15
17
|
name: "Codex CLI",
|
|
16
18
|
protocols: ["openai"],
|
|
@@ -20,6 +22,13 @@ export const codex = {
|
|
|
20
22
|
const notes = [];
|
|
21
23
|
const text = readTextIfExists(configFile);
|
|
22
24
|
const config = (text ? parseToml(text) : {});
|
|
25
|
+
const authValue = readJsonIfExists(authFile);
|
|
26
|
+
const auth = authValue === undefined ? {} : authValue;
|
|
27
|
+
if (!isJsonObject(auth))
|
|
28
|
+
throw new Error(`${authFile}: expected a JSON object`);
|
|
29
|
+
if (config.model_providers !== undefined && !isJsonObject(config.model_providers)) {
|
|
30
|
+
throw new Error(`${configFile}: expected model_providers to be a table`);
|
|
31
|
+
}
|
|
23
32
|
config.model_provider = provider.id;
|
|
24
33
|
config.model = provider.defaultModel;
|
|
25
34
|
const model = provider.models.find((m) => m.id === provider.defaultModel);
|
|
@@ -29,7 +38,27 @@ export const codex = {
|
|
|
29
38
|
// Codex only speaks the OpenAI Responses API (wire_api = "chat" was removed Feb 2026).
|
|
30
39
|
// requires_openai_auth = true makes Codex take the key from auth.json instead of an env var.
|
|
31
40
|
const providers = (config.model_providers ?? {});
|
|
41
|
+
const previous = providers[provider.id];
|
|
42
|
+
if (previous !== undefined && !isJsonObject(previous)) {
|
|
43
|
+
throw new Error(`${configFile}: expected the selected model provider to be a table`);
|
|
44
|
+
}
|
|
45
|
+
const kept = { ...previous };
|
|
46
|
+
for (const key of ["env_key", "env_key_instructions", "experimental_bearer_token"])
|
|
47
|
+
delete kept[key];
|
|
48
|
+
for (const key of ["http_headers", "env_http_headers"]) {
|
|
49
|
+
if (kept[key] === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (!isJsonObject(kept[key]))
|
|
52
|
+
throw new Error(`${configFile}: expected ${key} to be a table`);
|
|
53
|
+
const headers = { ...kept[key] };
|
|
54
|
+
for (const name of Object.keys(headers)) {
|
|
55
|
+
if (["authorization", "x-api-key", "api-key"].includes(name.toLowerCase()))
|
|
56
|
+
delete headers[name];
|
|
57
|
+
}
|
|
58
|
+
kept[key] = headers;
|
|
59
|
+
}
|
|
32
60
|
providers[provider.id] = {
|
|
61
|
+
...kept,
|
|
33
62
|
name: provider.name,
|
|
34
63
|
base_url: provider.baseUrl.replace(/\/$/, ""),
|
|
35
64
|
wire_api: "responses",
|
|
@@ -43,7 +72,6 @@ export const codex = {
|
|
|
43
72
|
if (text?.includes("#"))
|
|
44
73
|
notes.push("config.toml comments are not preserved by TOML round-trip");
|
|
45
74
|
writeFileAtomic(configFile, stringifyToml(config) + "\n");
|
|
46
|
-
const auth = readJsonIfExists(authFile) ?? {};
|
|
47
75
|
auth.auth_mode = "apikey";
|
|
48
76
|
auth.OPENAI_API_KEY = provider.apiKey;
|
|
49
77
|
const authBackup = backupFile(authFile);
|
|
@@ -57,6 +85,9 @@ export const codex = {
|
|
|
57
85
|
if (!text)
|
|
58
86
|
return { app: this.id, changed: [], notes: [], skipped: "no config.toml" };
|
|
59
87
|
const config = parseToml(text);
|
|
88
|
+
if (config.model_providers !== undefined && !isJsonObject(config.model_providers)) {
|
|
89
|
+
throw new Error(`${configFile}: expected model_providers to be a table`);
|
|
90
|
+
}
|
|
60
91
|
const providers = config.model_providers;
|
|
61
92
|
if (!providers?.[provider.id]) {
|
|
62
93
|
return { app: this.id, changed: [], notes: [], skipped: `no model_providers.${provider.id} entry` };
|
|
@@ -133,4 +164,4 @@ export const codex = {
|
|
|
133
164
|
];
|
|
134
165
|
});
|
|
135
166
|
},
|
|
136
|
-
};
|
|
167
|
+
});
|