agentsw 0.9.2 → 0.10.0
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 +42 -1
- package/README.zh-CN.md +38 -1
- package/dist/app-paths.js +127 -0
- package/dist/apps.js +11 -15
- package/dist/commands.js +375 -205
- package/dist/discover.js +21 -11
- package/dist/gateway.js +5 -4
- package/dist/i18n.js +147 -3
- package/dist/index.js +14 -2
- package/dist/menu.js +141 -121
- package/dist/modelsdev.js +51 -21
- package/dist/progress.js +116 -0
- package/dist/provider-actions.js +29 -9
- package/dist/remove.js +15 -30
- package/dist/rename.js +17 -32
- package/dist/report.js +67 -0
- package/dist/store.js +25 -0
- package/dist/targets/claudecode.js +12 -9
- package/dist/targets/codex.js +22 -13
- package/dist/targets/dsh.js +19 -34
- package/dist/targets/hermes.js +11 -20
- package/dist/targets/omp.js +15 -7
- package/dist/targets/opencode.js +42 -25
- package/dist/targets/pistyle.js +9 -20
- package/dist/targets/workbuddy.js +6 -4
- package/dist/ui.js +157 -0
- package/package.json +2 -2
package/dist/remove.js
CHANGED
|
@@ -4,7 +4,8 @@ import { applyEdits, parse as parseJsonc } from "jsonc-parser";
|
|
|
4
4
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
5
5
|
import YAML from "yaml";
|
|
6
6
|
import { commitFileChanges } from "./config-transaction.js";
|
|
7
|
-
import {
|
|
7
|
+
import { drainPendingWrites, setDryRun } from "./fsutil.js";
|
|
8
|
+
import { dshCredentialsFile, dshSettingsFiles, hermesConfigFile, hermesEnvFile, ompConfigFiles, ompModelsFiles, opencodeConfigFiles, piModelsFile, piSettingsFile, workbuddyDir, workbuddyModelsFile, } from "./app-paths.js";
|
|
8
9
|
import { editJsoncObject, isJsonObject, readJsoncObject } from "./jsonc.js";
|
|
9
10
|
import { configFile } from "./store.js";
|
|
10
11
|
import { endpointKey } from "./import.js";
|
|
@@ -164,33 +165,20 @@ function targetFiles(target, listing = false) {
|
|
|
164
165
|
};
|
|
165
166
|
switch (target.id) {
|
|
166
167
|
case "omp": {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
refs(path.join(dir, name));
|
|
168
|
+
for (const file of ompModelsFiles())
|
|
169
|
+
add(file, ["providers"]);
|
|
170
|
+
for (const file of ompConfigFiles())
|
|
171
|
+
refs(file);
|
|
172
172
|
break;
|
|
173
173
|
}
|
|
174
174
|
case "pi":
|
|
175
175
|
case "prime": {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
add(path.join(dir, "models.json"), ["providers"]);
|
|
179
|
-
refs(path.join(dir, "settings.json"));
|
|
176
|
+
add(piModelsFile(target.id), ["providers"]);
|
|
177
|
+
refs(piSettingsFile(target.id));
|
|
180
178
|
break;
|
|
181
179
|
}
|
|
182
180
|
case "opencode": {
|
|
183
|
-
const
|
|
184
|
-
if (process.env.OPENCODE_CONFIG_DIR?.trim())
|
|
185
|
-
dirs.add(expandHome(process.env.OPENCODE_CONFIG_DIR.trim()));
|
|
186
|
-
const files = new Set();
|
|
187
|
-
for (const dir of dirs) {
|
|
188
|
-
for (const name of ["config.json", "opencode.json", "opencode.jsonc"])
|
|
189
|
-
files.add(path.join(dir, name));
|
|
190
|
-
}
|
|
191
|
-
if (process.env.OPENCODE_CONFIG?.trim())
|
|
192
|
-
files.add(expandHome(process.env.OPENCODE_CONFIG.trim()));
|
|
193
|
-
for (const file of files)
|
|
181
|
+
for (const file of opencodeConfigFiles())
|
|
194
182
|
add(file, ["provider"]);
|
|
195
183
|
break;
|
|
196
184
|
}
|
|
@@ -198,20 +186,18 @@ function targetFiles(target, listing = false) {
|
|
|
198
186
|
add(target.configPaths[0], ["model_providers"]);
|
|
199
187
|
break;
|
|
200
188
|
case "hermes": {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const file = path.resolve(dir, ".env");
|
|
189
|
+
add(hermesConfigFile(), ["providers"]);
|
|
190
|
+
const file = path.resolve(hermesEnvFile());
|
|
204
191
|
if (!listing)
|
|
205
192
|
result.extras.set(file, readText(file));
|
|
206
193
|
break;
|
|
207
194
|
}
|
|
208
195
|
case "dsh": {
|
|
209
|
-
const
|
|
210
|
-
const file = ["settings.yaml", "settings.yml", "settings.json"].map((name) => path.join(dir, name)).find((file) => fs.existsSync(file));
|
|
196
|
+
const file = dshSettingsFiles().find((candidate) => fs.existsSync(candidate));
|
|
211
197
|
if (file)
|
|
212
198
|
add(file, ["llm-pi-ai", "providers"]);
|
|
213
199
|
if (!listing) {
|
|
214
|
-
const credentials = add(
|
|
200
|
+
const credentials = add(dshCredentialsFile());
|
|
215
201
|
if (credentials) {
|
|
216
202
|
const { version, refs } = credentials.value;
|
|
217
203
|
const flat = version === undefined && refs === undefined && Object.values(credentials.value).every((value) => typeof value === "string");
|
|
@@ -589,9 +575,8 @@ async function planTargetRemoval(target, id, plans, stored) {
|
|
|
589
575
|
return found;
|
|
590
576
|
}
|
|
591
577
|
function planWorkbuddyRemoval(target, id, plans, stored) {
|
|
592
|
-
const
|
|
593
|
-
const
|
|
594
|
-
const file = path.join(dir, "models.json");
|
|
578
|
+
const dir = workbuddyDir();
|
|
579
|
+
const file = workbuddyModelsFile();
|
|
595
580
|
const before = readText(file);
|
|
596
581
|
if (before === undefined)
|
|
597
582
|
return false;
|
package/dist/rename.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import { applyEdits, findNodeAtLocation, getNodeValue, parseTree } from "jsonc-parser";
|
|
5
5
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
6
|
-
import {
|
|
6
|
+
import { codexAuthFile, codexConfigFile, dshCredentialsFile, dshSettingsFiles, hermesConfigFile, hermesEnvFile, ompConfigFiles, ompModelsFiles, opencodeConfigFiles, piModelsFile, piSettingsFile, workbuddyModelsFile, } from "./app-paths.js";
|
|
7
7
|
import { isJsonObject, readJsoncObject } from "./jsonc.js";
|
|
8
8
|
import { configFile } from "./store.js";
|
|
9
9
|
import { commitFileChanges } from "./config-transaction.js";
|
|
@@ -305,12 +305,12 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
305
305
|
store.move(["providers"], oldId, newId);
|
|
306
306
|
exact(store, ["active"]);
|
|
307
307
|
finish(store, beforeStore, 0o600);
|
|
308
|
-
visit(
|
|
308
|
+
visit(codexConfigFile(), "toml", (editor) => {
|
|
309
309
|
const entry = get(editor.value, ["model_providers", oldId]);
|
|
310
310
|
if (isJsonObject(entry)) {
|
|
311
311
|
let key = typeof entry.env_key === "string" ? process.env[entry.env_key] : undefined;
|
|
312
312
|
if (!entry.env_key && entry.requires_openai_auth && get(editor.value, ["model_provider"]) === oldId) {
|
|
313
|
-
const authFile =
|
|
313
|
+
const authFile = codexAuthFile();
|
|
314
314
|
const authText = readExisting(authFile);
|
|
315
315
|
changes.push({ file: authFile, before: authText, after: authText });
|
|
316
316
|
if (authText !== undefined) {
|
|
@@ -328,19 +328,17 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
328
328
|
exact(editor, ["profiles", profile, "model_provider"]);
|
|
329
329
|
renameMap(editor, ["model_providers"]);
|
|
330
330
|
});
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
visit(path.join(ompDir, name), "yaml", (editor) => {
|
|
331
|
+
for (const file of ompModelsFiles())
|
|
332
|
+
visit(file, "yaml", (editor) => renameMap(editor, ["providers"], undefined, true));
|
|
333
|
+
for (const file of ompConfigFiles())
|
|
334
|
+
visit(file, "yaml", (editor) => {
|
|
336
335
|
compound(editor, ["model"]);
|
|
337
336
|
for (const [role] of entries(editor, ["modelRoles"]))
|
|
338
337
|
compound(editor, ["modelRoles", role]);
|
|
339
338
|
});
|
|
340
|
-
for (const
|
|
341
|
-
|
|
342
|
-
visit(
|
|
343
|
-
visit(path.join(dir, "settings.json"), "json", (editor) => {
|
|
339
|
+
for (const id of ["pi", "prime"]) {
|
|
340
|
+
visit(piModelsFile(id), "json", (editor) => renameMap(editor, ["providers"], undefined, id === "prime"));
|
|
341
|
+
visit(piSettingsFile(id), "json", (editor) => {
|
|
344
342
|
exact(editor, ["defaultProvider"]);
|
|
345
343
|
const recent = get(editor.value, ["recentModels"]);
|
|
346
344
|
if (Array.isArray(recent))
|
|
@@ -352,15 +350,7 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
352
350
|
});
|
|
353
351
|
});
|
|
354
352
|
}
|
|
355
|
-
|
|
356
|
-
const customOpenCode = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
357
|
-
if (customOpenCode)
|
|
358
|
-
for (const name of ["opencode.json", "opencode.jsonc"])
|
|
359
|
-
opencodeFiles.add(path.join(expandHome(customOpenCode), name));
|
|
360
|
-
const sharedOpenCode = process.env.OPENCODE_CONFIG?.trim();
|
|
361
|
-
if (sharedOpenCode)
|
|
362
|
-
opencodeFiles.add(expandHome(sharedOpenCode));
|
|
363
|
-
for (const file of opencodeFiles)
|
|
353
|
+
for (const file of opencodeConfigFiles())
|
|
364
354
|
visit(file, "json", (editor) => {
|
|
365
355
|
renameMap(editor, ["provider"]);
|
|
366
356
|
for (const key of ["model", "small_model"])
|
|
@@ -399,14 +389,13 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
399
389
|
throw new Error(`${file}: source provider credentials conflict with the store`);
|
|
400
390
|
return key;
|
|
401
391
|
};
|
|
402
|
-
const
|
|
403
|
-
const envFile = path.join(hermesDir, ".env");
|
|
392
|
+
const envFile = hermesEnvFile();
|
|
404
393
|
const envBefore = readExisting(envFile);
|
|
405
394
|
const assignments = envBefore === undefined ? [] : envAssignments(envFile, envBefore);
|
|
406
395
|
const envValues = new Map(assignments.map((assignment) => [assignment.name, assignment.value]));
|
|
407
396
|
let envKey;
|
|
408
397
|
let hermesPresent = false;
|
|
409
|
-
visit(
|
|
398
|
+
visit(hermesConfigFile(), "yaml", (editor) => {
|
|
410
399
|
hermesPresent = true;
|
|
411
400
|
const keyRef = get(editor.value, ["providers", oldId, "key_env"]);
|
|
412
401
|
const key = typeof keyRef === "string" ? process.env[keyRef] ?? envValues.get(keyRef) : undefined;
|
|
@@ -423,18 +412,16 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
423
412
|
const envAfter = envKey ? applyEdits(envBefore, assignments.filter((assignment) => assignment.name === envKey)
|
|
424
413
|
.map((assignment) => ({ offset: assignment.offset, length: envKey.length, content: newKey }))) : envBefore;
|
|
425
414
|
changes.push({ file: envFile, before: envBefore, after: envAfter, mode: 0o600 });
|
|
426
|
-
const dshDir = process.env.DSH_HOME?.trim() ? expandHome(process.env.DSH_HOME.trim()) : localAppDataDir("dsh");
|
|
427
415
|
const settings = [];
|
|
428
|
-
for (const
|
|
429
|
-
const file = path.join(dshDir, name);
|
|
416
|
+
for (const file of dshSettingsFiles()) {
|
|
430
417
|
const before = readExisting(file);
|
|
431
418
|
if (before === undefined)
|
|
432
419
|
changes.push({ file, before, after: before });
|
|
433
420
|
else
|
|
434
|
-
settings.push({ editor:
|
|
421
|
+
settings.push({ editor: file.endsWith("json") ? jsonEditor(file, before) : yamlEditor(file, before), before });
|
|
435
422
|
}
|
|
436
423
|
const dshMaps = settings.map(({ editor }) => entries(editor, ["llm-pi-ai", "providers"]));
|
|
437
|
-
const credentialFile =
|
|
424
|
+
const credentialFile = dshCredentialsFile();
|
|
438
425
|
let credentialKey;
|
|
439
426
|
let credentialRefs = {};
|
|
440
427
|
visit(credentialFile, "yaml", (editor) => {
|
|
@@ -458,9 +445,7 @@ export async function renameProvider(oldId, newId, opts = {}) {
|
|
|
458
445
|
exact(editor, ["agent-default-model", "provider"]);
|
|
459
446
|
finish(editor, before);
|
|
460
447
|
}
|
|
461
|
-
|
|
462
|
-
const workbuddyDir = workbuddyEnv ? expandHome(workbuddyEnv) : process.platform === "win32" ? appDataDir("workbuddy") : path.join(home, ".workbuddy");
|
|
463
|
-
visit(path.join(workbuddyDir, "models.json"), "json", (editor) => {
|
|
448
|
+
visit(workbuddyModelsFile(), "json", (editor) => {
|
|
464
449
|
const at = Array.isArray(editor.value) ? [] : ["models"];
|
|
465
450
|
const rows = get(editor.value, at);
|
|
466
451
|
if (rows === undefined)
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getMetadataMode } from "./metadata.js";
|
|
2
|
+
function modelSpecReport(spec) {
|
|
3
|
+
return {
|
|
4
|
+
id: spec.id,
|
|
5
|
+
name: spec.name ?? null,
|
|
6
|
+
contextWindow: spec.contextWindow ?? null,
|
|
7
|
+
maxInput: spec.maxInput ?? null,
|
|
8
|
+
maxOutput: spec.maxOutput ?? null,
|
|
9
|
+
reasoning: spec.reasoning ?? null,
|
|
10
|
+
reasoningEfforts: spec.reasoningEfforts ?? null,
|
|
11
|
+
imageInput: spec.imageInput ?? null,
|
|
12
|
+
cost: spec.cost ?? null,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function providerSummary(provider) {
|
|
16
|
+
return {
|
|
17
|
+
id: provider.id,
|
|
18
|
+
name: provider.name,
|
|
19
|
+
protocol: provider.protocol,
|
|
20
|
+
baseUrl: provider.baseUrl,
|
|
21
|
+
defaultModel: provider.defaultModel,
|
|
22
|
+
modelCount: provider.models.length,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function statusReport(configFile, store, apps) {
|
|
26
|
+
return { configFile, active: store.active ?? null, apps };
|
|
27
|
+
}
|
|
28
|
+
export function listReport(store) {
|
|
29
|
+
const providers = Object.keys(store.providers).map((id) => providerSummary(store.providers[id]));
|
|
30
|
+
return { active: store.active ?? null, providers };
|
|
31
|
+
}
|
|
32
|
+
export function listAppsReport(apps, entries) {
|
|
33
|
+
return {
|
|
34
|
+
apps,
|
|
35
|
+
entries: entries.map((entry) => ({ app: entry.app ?? null, id: entry.id, name: entry.name ?? null })),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function appsReport(rows) {
|
|
39
|
+
return { apps: rows };
|
|
40
|
+
}
|
|
41
|
+
/** Mirrors the object `models --provider --metadata` printed before --json existed, for compatibility. */
|
|
42
|
+
export function providerMetadataReport(provider) {
|
|
43
|
+
return {
|
|
44
|
+
provider: provider.id,
|
|
45
|
+
gatewayMetadata: provider.gatewayMetadata ?? "auto",
|
|
46
|
+
metadataMode: getMetadataMode(provider),
|
|
47
|
+
gatewayModelAliases: provider.gatewayModelAliases ?? {},
|
|
48
|
+
models: provider.models.map((spec) => ({ id: spec.id, metadata: spec.metadata })),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function providerModelsReport(provider, metadata) {
|
|
52
|
+
if (metadata)
|
|
53
|
+
return providerMetadataReport(provider);
|
|
54
|
+
return {
|
|
55
|
+
provider: provider.id,
|
|
56
|
+
protocol: provider.protocol,
|
|
57
|
+
baseUrl: provider.baseUrl,
|
|
58
|
+
defaultModel: provider.defaultModel,
|
|
59
|
+
models: provider.models.map(modelSpecReport),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function modelsQueryReport(query, hits) {
|
|
63
|
+
return {
|
|
64
|
+
query,
|
|
65
|
+
models: hits.map((hit) => ({ provider: hit.provider, ...modelSpecReport(hit.spec) })),
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/store.js
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { appDataDir, isDryRun, readFileSnapshot, recordPendingWrites } from "./fsutil.js";
|
|
3
|
+
import { isJsonObject } from "./jsonc.js";
|
|
3
4
|
import { commitFileChanges } from "./config-transaction.js";
|
|
4
5
|
export const configDir = appDataDir("agentsw");
|
|
5
6
|
export const configFile = path.join(configDir, "config.json");
|
|
6
7
|
const loadedSnapshots = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* Reject only shapes that would otherwise crash later with an opaque TypeError.
|
|
10
|
+
* Messages name the file and the provider id, never a field value, so no
|
|
11
|
+
* credential can leak through an error. Extra/unknown fields stay allowed.
|
|
12
|
+
*/
|
|
13
|
+
function validateStore(store) {
|
|
14
|
+
const raw = store;
|
|
15
|
+
if (raw.version !== undefined && raw.version !== 1) {
|
|
16
|
+
const version = typeof raw.version === "number" ? ` ${raw.version}` : "";
|
|
17
|
+
throw new Error(`${configFile}: unsupported provider store version${version}`);
|
|
18
|
+
}
|
|
19
|
+
for (const [id, provider] of Object.entries(store.providers)) {
|
|
20
|
+
if (!isJsonObject(provider))
|
|
21
|
+
throw new Error(`${configFile}: provider "${id}" is not an object`);
|
|
22
|
+
if (!Array.isArray(provider.models)) {
|
|
23
|
+
throw new Error(`${configFile}: provider "${id}" has no models array`);
|
|
24
|
+
}
|
|
25
|
+
for (const model of provider.models) {
|
|
26
|
+
if (!isJsonObject(model))
|
|
27
|
+
throw new Error(`${configFile}: provider "${id}" has a model that is not an object`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
7
31
|
export function loadStore() {
|
|
8
32
|
const snapshot = readFileSnapshot(configFile);
|
|
9
33
|
let store;
|
|
@@ -24,6 +48,7 @@ export function loadStore() {
|
|
|
24
48
|
if (!store.providers || typeof store.providers !== "object" || Array.isArray(store.providers)) {
|
|
25
49
|
throw new Error(`${configFile}: expected a providers object`);
|
|
26
50
|
}
|
|
51
|
+
validateStore(store);
|
|
27
52
|
}
|
|
28
53
|
loadedSnapshots.set(store, snapshot);
|
|
29
54
|
return store;
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import { backupFile, home, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
|
|
2
|
+
import { backupFile, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
|
|
4
3
|
import { isJsonObject } from "../jsonc.js";
|
|
4
|
+
import { claudeDir, claudeSettingsFile } from "../app-paths.js";
|
|
5
5
|
import { transactionalTarget } from "../target-transaction.js";
|
|
6
6
|
import { providerIdFromBaseUrl, providerNameFromBaseUrl } from "../slug.js";
|
|
7
7
|
import { stripApiVersion } from "./wire.js";
|
|
8
|
-
const dir = path.join(home, ".claude");
|
|
9
|
-
const settingsFile = path.join(dir, "settings.json");
|
|
10
8
|
/**
|
|
11
|
-
* Claude Code reads Anthropic-protocol endpoints via env vars in
|
|
9
|
+
* Claude Code reads Anthropic-protocol endpoints via env vars in
|
|
10
|
+
* $CLAUDE_CONFIG_DIR/settings.json (default ~/.claude/settings.json).
|
|
12
11
|
* We merge the "env" block and leave every other setting untouched.
|
|
13
12
|
*/
|
|
14
13
|
export const claudecode = transactionalTarget({
|
|
15
14
|
id: "claude",
|
|
16
15
|
name: "Claude Code",
|
|
17
16
|
protocols: ["anthropic"],
|
|
18
|
-
configPaths
|
|
19
|
-
|
|
17
|
+
get configPaths() {
|
|
18
|
+
return [claudeSettingsFile()];
|
|
19
|
+
},
|
|
20
|
+
detect: () => fs.existsSync(claudeDir()),
|
|
20
21
|
async apply(provider) {
|
|
22
|
+
const settingsFile = claudeSettingsFile();
|
|
21
23
|
const notes = [];
|
|
22
24
|
const settingsValue = readJsonIfExists(settingsFile);
|
|
23
25
|
const settings = settingsValue === undefined ? {} : settingsValue;
|
|
@@ -43,6 +45,7 @@ export const claudecode = transactionalTarget({
|
|
|
43
45
|
return { app: this.id, changed: [settingsFile], notes };
|
|
44
46
|
},
|
|
45
47
|
async prune(provider) {
|
|
48
|
+
const settingsFile = claudeSettingsFile();
|
|
46
49
|
const settings = readJsonIfExists(settingsFile);
|
|
47
50
|
if (settings !== undefined && (!isJsonObject(settings) || (settings.env !== undefined && !isJsonObject(settings.env)))) {
|
|
48
51
|
throw new Error(`${settingsFile}: expected settings and env to be JSON objects`);
|
|
@@ -71,13 +74,13 @@ export const claudecode = transactionalTarget({
|
|
|
71
74
|
return { app: this.id, changed: [settingsFile], notes };
|
|
72
75
|
},
|
|
73
76
|
current() {
|
|
74
|
-
const env = readJsonIfExists(
|
|
77
|
+
const env = readJsonIfExists(claudeSettingsFile())?.env;
|
|
75
78
|
if (!env?.ANTHROPIC_BASE_URL)
|
|
76
79
|
return undefined;
|
|
77
80
|
return `${env.ANTHROPIC_BASE_URL} · ${env.ANTHROPIC_MODEL ?? "?"}`;
|
|
78
81
|
},
|
|
79
82
|
candidates() {
|
|
80
|
-
const env = readJsonIfExists(
|
|
83
|
+
const env = readJsonIfExists(claudeSettingsFile())?.env;
|
|
81
84
|
const baseUrl = env?.ANTHROPIC_BASE_URL;
|
|
82
85
|
if (!baseUrl)
|
|
83
86
|
return [];
|
package/dist/targets/codex.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4
|
-
import { SafeConfigError, backupFile,
|
|
3
|
+
import { SafeConfigError, backupFile, readJsonIfExists, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
5
4
|
import { isJsonObject } from "../jsonc.js";
|
|
5
|
+
import { codexAuthFile, codexConfigFile, codexDir } from "../app-paths.js";
|
|
6
6
|
import { transactionalTarget } from "../target-transaction.js";
|
|
7
|
-
const dir = path.join(home, ".codex");
|
|
8
|
-
const configFile = path.join(dir, "config.toml");
|
|
9
|
-
const authFile = path.join(dir, "auth.json");
|
|
10
7
|
/** smol-toml quotes the offending source line, which may hold a key: report only the position. */
|
|
11
8
|
function parseConfigToml(file, text) {
|
|
12
9
|
try {
|
|
@@ -19,17 +16,22 @@ function parseConfigToml(file, text) {
|
|
|
19
16
|
}
|
|
20
17
|
}
|
|
21
18
|
/**
|
|
22
|
-
* Codex CLI: [model_providers.<id>] in
|
|
23
|
-
* OpenAI-protocol endpoint; the key ships
|
|
24
|
-
* TOML round-trip via smol-toml preserves
|
|
19
|
+
* Codex CLI: [model_providers.<id>] in $CODEX_HOME/config.toml (default
|
|
20
|
+
* ~/.codex/config.toml) selects a custom OpenAI-protocol endpoint; the key ships
|
|
21
|
+
* via auth.json in the same directory. TOML round-trip via smol-toml preserves
|
|
22
|
+
* data but not comments (backed up first).
|
|
25
23
|
*/
|
|
26
24
|
export const codex = transactionalTarget({
|
|
27
25
|
id: "codex",
|
|
28
26
|
name: "Codex CLI",
|
|
29
27
|
protocols: ["openai"],
|
|
30
|
-
configPaths
|
|
31
|
-
|
|
28
|
+
get configPaths() {
|
|
29
|
+
return [codexConfigFile(), codexAuthFile()];
|
|
30
|
+
},
|
|
31
|
+
detect: () => fs.existsSync(codexDir()),
|
|
32
32
|
async apply(provider) {
|
|
33
|
+
const configFile = codexConfigFile();
|
|
34
|
+
const authFile = codexAuthFile();
|
|
33
35
|
const notes = [];
|
|
34
36
|
const text = readTextIfExists(configFile);
|
|
35
37
|
const config = text ? parseConfigToml(configFile, text) : {};
|
|
@@ -46,6 +48,11 @@ export const codex = transactionalTarget({
|
|
|
46
48
|
if (provider.reasoningEffort && model?.reasoning !== false) {
|
|
47
49
|
config.model_reasoning_effort = provider.reasoningEffort;
|
|
48
50
|
}
|
|
51
|
+
else {
|
|
52
|
+
// The effort belongs to the active model: switching providers must not
|
|
53
|
+
// inherit the previous provider's level (prune treats it as owned too).
|
|
54
|
+
delete config.model_reasoning_effort;
|
|
55
|
+
}
|
|
49
56
|
// Codex only speaks the OpenAI Responses API (wire_api = "chat" was removed Feb 2026).
|
|
50
57
|
// requires_openai_auth = true makes Codex take the key from auth.json instead of an env var.
|
|
51
58
|
const providers = (config.model_providers ?? {});
|
|
@@ -92,6 +99,7 @@ export const codex = transactionalTarget({
|
|
|
92
99
|
return { app: this.id, changed: [configFile, authFile], notes };
|
|
93
100
|
},
|
|
94
101
|
async prune(provider) {
|
|
102
|
+
const configFile = codexConfigFile();
|
|
95
103
|
const text = readTextIfExists(configFile);
|
|
96
104
|
if (!text)
|
|
97
105
|
return { app: this.id, changed: [], notes: [], skipped: "no config.toml" };
|
|
@@ -121,11 +129,11 @@ export const codex = transactionalTarget({
|
|
|
121
129
|
return { app: this.id, changed: [configFile], notes };
|
|
122
130
|
},
|
|
123
131
|
current() {
|
|
124
|
-
const text = readTextIfExists(
|
|
132
|
+
const text = readTextIfExists(codexConfigFile());
|
|
125
133
|
if (!text)
|
|
126
134
|
return undefined;
|
|
127
135
|
try {
|
|
128
|
-
const config = parseConfigToml(
|
|
136
|
+
const config = parseConfigToml(codexConfigFile(), text);
|
|
129
137
|
if (!config.model_provider)
|
|
130
138
|
return undefined;
|
|
131
139
|
return `${String(config.model_provider)} · ${String(config.model ?? "?")}`;
|
|
@@ -135,6 +143,7 @@ export const codex = transactionalTarget({
|
|
|
135
143
|
}
|
|
136
144
|
},
|
|
137
145
|
candidates() {
|
|
146
|
+
const configFile = codexConfigFile();
|
|
138
147
|
const text = readTextIfExists(configFile);
|
|
139
148
|
if (!text)
|
|
140
149
|
return [];
|
|
@@ -148,7 +157,7 @@ export const codex = transactionalTarget({
|
|
|
148
157
|
const providers = config.model_providers;
|
|
149
158
|
if (!providers)
|
|
150
159
|
return [];
|
|
151
|
-
const auth = readJsonIfExists(
|
|
160
|
+
const auth = readJsonIfExists(codexAuthFile());
|
|
152
161
|
const authKey = auth?.OPENAI_API_KEY || auth?.tokens?.access_token;
|
|
153
162
|
const active = typeof config.model_provider === "string" ? config.model_provider : undefined;
|
|
154
163
|
const self = this.id;
|
package/dist/targets/dsh.js
CHANGED
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import YAML from "yaml";
|
|
4
|
-
import { backupFile,
|
|
3
|
+
import { backupFile, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
4
|
+
import { dshCredentialsFile, dshDir, dshSettingsFile } from "../app-paths.js";
|
|
5
5
|
import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "../provider-identity.js";
|
|
6
6
|
import { transactionalTarget } from "../target-transaction.js";
|
|
7
7
|
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
8
|
-
import { apiValue, classifyApi, mergeModels, sdkBaseUrl } from "./wire.js";
|
|
9
|
-
/** `$DSH_HOME`, or `~/.dsh` (packages/util/home-paths `resolveDshHome`). */
|
|
10
|
-
function dshHome() {
|
|
11
|
-
const env = process.env.DSH_HOME?.trim();
|
|
12
|
-
return env ? expandHome(env) : process.platform === "win32" ? localAppDataDir("dsh") : path.join(home, ".dsh");
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Settings document: `<harness home>/settings.yaml` by default; the extension
|
|
16
|
-
* picks the format, so an existing `.yml`/`.json` document is written in place.
|
|
17
|
-
*/
|
|
18
|
-
function settingsFile() {
|
|
19
|
-
const dir = dshHome();
|
|
20
|
-
for (const name of ["settings.yaml", "settings.yml", "settings.json"]) {
|
|
21
|
-
const file = path.join(dir, name);
|
|
22
|
-
if (fs.existsSync(file))
|
|
23
|
-
return file;
|
|
24
|
-
}
|
|
25
|
-
return path.join(dir, "settings.yaml");
|
|
26
|
-
}
|
|
27
|
-
const credentialsFile = () => path.join(dshHome(), ".credentials.yaml");
|
|
8
|
+
import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl } from "./wire.js";
|
|
28
9
|
/** Layout version of `.credentials.yaml` this build reads and writes. */
|
|
29
10
|
const CREDENTIALS_VERSION = 1;
|
|
30
11
|
/** Thinking levels a `reasoningEfforts` entry may offer (llm-pi-ai THINKING_LEVEL_GATE). */
|
|
@@ -82,20 +63,23 @@ export const dsh = transactionalTarget({
|
|
|
82
63
|
id: "dsh",
|
|
83
64
|
name: "DeepSeek Harness",
|
|
84
65
|
protocols: ["openai", "anthropic"],
|
|
85
|
-
configPaths
|
|
86
|
-
|
|
66
|
+
get configPaths() {
|
|
67
|
+
return [dshSettingsFile(), dshCredentialsFile()];
|
|
68
|
+
},
|
|
69
|
+
detect: () => fs.existsSync(dshDir()),
|
|
87
70
|
async apply(provider) {
|
|
88
71
|
const notes = [];
|
|
89
|
-
const file =
|
|
72
|
+
const file = dshSettingsFile();
|
|
90
73
|
const doc = parseSettings(file);
|
|
91
74
|
const at = ["llm-pi-ai", "providers", provider.id];
|
|
92
75
|
const prev = doc.getIn(at)?.toJSON?.();
|
|
93
76
|
const ref = managedCredentialRef(provider.id);
|
|
77
|
+
const baseURL = sdkBaseUrl(provider.protocol, provider.baseUrl);
|
|
94
78
|
const entry = {
|
|
95
79
|
displayName: provider.name,
|
|
96
80
|
apiKeyEnv: ref, // credential reference, never the secret
|
|
97
|
-
api: apiValue(provider.protocol, provider.openaiApi, prev?.api),
|
|
98
|
-
baseURL
|
|
81
|
+
api: apiValue(provider.protocol, provider.openaiApi, prev?.api ?? entryApi(prev ?? {})),
|
|
82
|
+
baseURL,
|
|
99
83
|
};
|
|
100
84
|
if (YAML.isMap(doc.getIn(at))) {
|
|
101
85
|
// Key-by-key: compat, headers, defaultInput, retryPolicy and their comments are the user's.
|
|
@@ -147,7 +131,7 @@ export const dsh = transactionalTarget({
|
|
|
147
131
|
},
|
|
148
132
|
async prune(provider) {
|
|
149
133
|
const notes = [];
|
|
150
|
-
const file =
|
|
134
|
+
const file = dshSettingsFile();
|
|
151
135
|
// Settings must be understood before any credential can be removed.
|
|
152
136
|
const doc = parseSettings(file);
|
|
153
137
|
const at = ["llm-pi-ai", "providers", provider.id];
|
|
@@ -181,7 +165,7 @@ export const dsh = transactionalTarget({
|
|
|
181
165
|
return { app: this.id, changed: droppedCredential ? [file, droppedCredential] : [file], notes };
|
|
182
166
|
},
|
|
183
167
|
current() {
|
|
184
|
-
const text = readTextIfExists(
|
|
168
|
+
const text = readTextIfExists(dshSettingsFile());
|
|
185
169
|
if (!text)
|
|
186
170
|
return undefined;
|
|
187
171
|
try {
|
|
@@ -196,7 +180,7 @@ export const dsh = transactionalTarget({
|
|
|
196
180
|
}
|
|
197
181
|
},
|
|
198
182
|
candidates() {
|
|
199
|
-
const text = readTextIfExists(
|
|
183
|
+
const text = readTextIfExists(dshSettingsFile());
|
|
200
184
|
if (!text)
|
|
201
185
|
return [];
|
|
202
186
|
let parsed;
|
|
@@ -215,7 +199,8 @@ export const dsh = transactionalTarget({
|
|
|
215
199
|
return Object.entries(providers).flatMap(([id, entry]) => {
|
|
216
200
|
if (!entry || typeof entry.baseURL !== "string")
|
|
217
201
|
return [];
|
|
218
|
-
|
|
202
|
+
// llm-pi-ai declares the wire at provider level or, agreeing, on every model.
|
|
203
|
+
const wire = classifyApi(entryApi(entry));
|
|
219
204
|
if (!wire)
|
|
220
205
|
return [];
|
|
221
206
|
const keyEnv = typeof entry.apiKeyEnv === "string" && entry.apiKeyEnv ? entry.apiKeyEnv : undefined;
|
|
@@ -247,7 +232,7 @@ export const dsh = transactionalTarget({
|
|
|
247
232
|
/** Read the managed store's `refs` section; an unreadable document yields no keys. */
|
|
248
233
|
function readCredentialRefs() {
|
|
249
234
|
try {
|
|
250
|
-
const doc = parseCredentials(
|
|
235
|
+
const doc = parseCredentials(dshCredentialsFile());
|
|
251
236
|
const parsed = doc.toJS();
|
|
252
237
|
return (doc.get("version") === undefined ? parsed : parsed.refs ?? {});
|
|
253
238
|
}
|
|
@@ -285,7 +270,7 @@ function parseCredentials(file) {
|
|
|
285
270
|
* instead of being re-rooted one level deeper. Returns the file.
|
|
286
271
|
*/
|
|
287
272
|
function writeCredential(ref, apiKey, notes, oldRef, referenced) {
|
|
288
|
-
const file =
|
|
273
|
+
const file = dshCredentialsFile();
|
|
289
274
|
const doc = parseCredentials(file);
|
|
290
275
|
if (doc.get("version") === undefined) {
|
|
291
276
|
const refs = doc.contents;
|
|
@@ -311,7 +296,7 @@ function writeCredential(ref, apiKey, notes, oldRef, referenced) {
|
|
|
311
296
|
}
|
|
312
297
|
/** Drop only managed references that no surviving route uses. */
|
|
313
298
|
function deleteCredentials(refs, notes) {
|
|
314
|
-
const file =
|
|
299
|
+
const file = dshCredentialsFile();
|
|
315
300
|
if (readTextIfExists(file) === undefined)
|
|
316
301
|
return undefined;
|
|
317
302
|
const doc = parseCredentials(file);
|