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/targets/dsh.js
CHANGED
|
@@ -2,6 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import { backupFile, expandHome, home, localAppDataDir, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
5
|
+
import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "../provider-identity.js";
|
|
6
|
+
import { transactionalTarget } from "../target-transaction.js";
|
|
7
|
+
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
5
8
|
import { apiValue, classifyApi, mergeModels, sdkBaseUrl } from "./wire.js";
|
|
6
9
|
/** `$DSH_HOME`, or `~/.dsh` (packages/util/home-paths `resolveDshHome`). */
|
|
7
10
|
function dshHome() {
|
|
@@ -24,8 +27,6 @@ function settingsFile() {
|
|
|
24
27
|
const credentialsFile = () => path.join(dshHome(), ".credentials.yaml");
|
|
25
28
|
/** Layout version of `.credentials.yaml` this build reads and writes. */
|
|
26
29
|
const CREDENTIALS_VERSION = 1;
|
|
27
|
-
/** Credential reference: a POSIX identifier (`/^[A-Za-z_][A-Za-z0-9_]*$/`). */
|
|
28
|
-
const credentialRef = (id) => `AGENTSW_${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
29
30
|
/** Thinking levels a `reasoningEfforts` entry may offer (llm-pi-ai THINKING_LEVEL_GATE). */
|
|
30
31
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
31
32
|
/** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
|
|
@@ -56,25 +57,28 @@ function modelEntry(m) {
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
function parseSettings(file) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const doc = parseYamlMapping(file, readTextIfExists(file));
|
|
61
|
+
for (const at of [["llm-pi-ai"], ["llm-pi-ai", "providers"], ["agent-default-model"]]) {
|
|
62
|
+
if (doc.hasIn(at) && !YAML.isMap(doc.getIn(at)))
|
|
63
|
+
throw new Error(`${file}: expected ${at.join(".")} to be a mapping`);
|
|
64
|
+
}
|
|
65
|
+
const providers = doc.getIn(["llm-pi-ai", "providers"]);
|
|
66
|
+
if (YAML.isMap(providers) && providers.items.some((pair) => !YAML.isMap(pair.value))) {
|
|
67
|
+
throw new Error(`${file}: expected provider mappings`);
|
|
63
68
|
}
|
|
64
|
-
if (doc.contents == null)
|
|
65
|
-
doc.contents = doc.createNode({});
|
|
66
69
|
return doc;
|
|
67
70
|
}
|
|
68
71
|
/** JSON documents re-serialize without comments; YAML keeps them (dsh reads both). */
|
|
69
72
|
function renderSettings(file, doc) {
|
|
70
|
-
|
|
73
|
+
const text = serializeYamlMapping(file, doc);
|
|
74
|
+
return file.endsWith(".json") ? JSON.stringify(doc.toJS(), null, 2) + "\n" : text;
|
|
71
75
|
}
|
|
72
76
|
/**
|
|
73
77
|
* DeepSeek Harness (dsh): provider routes live in the `llm-pi-ai` section of
|
|
74
78
|
* `$DSH_HOME/settings.yaml`, the picked default in `agent-default-model`, and
|
|
75
79
|
* the key in `$DSH_HOME/.credentials.yaml` — settings carry only a reference.
|
|
76
80
|
*/
|
|
77
|
-
export const dsh = {
|
|
81
|
+
export const dsh = transactionalTarget({
|
|
78
82
|
id: "dsh",
|
|
79
83
|
name: "DeepSeek Harness",
|
|
80
84
|
protocols: ["openai", "anthropic"],
|
|
@@ -86,22 +90,42 @@ export const dsh = {
|
|
|
86
90
|
const doc = parseSettings(file);
|
|
87
91
|
const at = ["llm-pi-ai", "providers", provider.id];
|
|
88
92
|
const prev = doc.getIn(at)?.toJSON?.();
|
|
89
|
-
const ref =
|
|
93
|
+
const ref = managedCredentialRef(provider.id);
|
|
90
94
|
const entry = {
|
|
91
95
|
displayName: provider.name,
|
|
92
96
|
apiKeyEnv: ref, // credential reference, never the secret
|
|
93
97
|
api: apiValue(provider.protocol, provider.openaiApi, prev?.api),
|
|
94
98
|
baseURL: sdkBaseUrl(provider.protocol, provider.baseUrl),
|
|
95
|
-
models: mergeModels(prev?.models, provider.models.map(modelEntry), OWNED_MODEL_KEYS),
|
|
96
99
|
};
|
|
97
100
|
if (YAML.isMap(doc.getIn(at))) {
|
|
98
101
|
// Key-by-key: compat, headers, defaultInput, retryPolicy and their comments are the user's.
|
|
99
102
|
for (const [key, value] of Object.entries(entry))
|
|
100
|
-
doc.setIn([...at, key],
|
|
103
|
+
doc.setIn([...at, key], value);
|
|
101
104
|
}
|
|
102
105
|
else {
|
|
103
106
|
doc.setIn(at, doc.createNode(entry));
|
|
104
107
|
}
|
|
108
|
+
const modelsAt = [...at, "models"];
|
|
109
|
+
const previousModels = doc.getIn(modelsAt);
|
|
110
|
+
const models = YAML.isSeq(previousModels) ? previousModels : doc.createNode([]);
|
|
111
|
+
const previousById = new Map();
|
|
112
|
+
for (const node of models.items) {
|
|
113
|
+
if (YAML.isMap(node))
|
|
114
|
+
previousById.set(node.get("id"), node);
|
|
115
|
+
}
|
|
116
|
+
models.items = mergeModels(prev?.models, provider.models.map(modelEntry), OWNED_MODEL_KEYS).map((model) => {
|
|
117
|
+
const node = previousById.get(model.id);
|
|
118
|
+
if (!node)
|
|
119
|
+
return doc.createNode(model);
|
|
120
|
+
for (const key of OWNED_MODEL_KEYS) {
|
|
121
|
+
if (!(key in model))
|
|
122
|
+
node.delete(key);
|
|
123
|
+
else
|
|
124
|
+
node.set(key, typeof model[key] === "object" ? doc.createNode(model[key]) : model[key]);
|
|
125
|
+
}
|
|
126
|
+
return node;
|
|
127
|
+
});
|
|
128
|
+
doc.setIn(modelsAt, models);
|
|
105
129
|
if (doc.hasIn([...at, "modelOverrides"])) {
|
|
106
130
|
// llm-pi-ai refuses modelOverrides beside an explicit models list.
|
|
107
131
|
doc.deleteIn([...at, "modelOverrides"]);
|
|
@@ -109,30 +133,37 @@ export const dsh = {
|
|
|
109
133
|
}
|
|
110
134
|
// The picker writes this section whole; a partial write would keep a stale reasoningEffort.
|
|
111
135
|
doc.set("agent-default-model", doc.createNode({ provider: provider.id, model: provider.defaultModel }));
|
|
136
|
+
const referenced = credentialReferences(doc, provider.id);
|
|
137
|
+
const oldRef = typeof prev?.apiKeyEnv === "string" && isManagedCredentialRef(prev.apiKeyEnv, provider.id) && !referenced.has(prev.apiKeyEnv)
|
|
138
|
+
? prev.apiKeyEnv : undefined;
|
|
139
|
+
const credential = writeCredential(ref, provider.apiKey, notes, oldRef, referenced);
|
|
112
140
|
const backup = backupFile(file);
|
|
113
141
|
if (backup)
|
|
114
142
|
notes.push(`backup: ${backup}`);
|
|
115
143
|
writeFileAtomic(file, renderSettings(file, doc));
|
|
116
|
-
const changed = [file,
|
|
144
|
+
const changed = [file, credential];
|
|
117
145
|
notes.push(`select in dsh with the model picker, or run: dsh web`);
|
|
118
146
|
return { app: this.id, changed, notes };
|
|
119
147
|
},
|
|
120
148
|
async prune(provider) {
|
|
121
149
|
const notes = [];
|
|
122
|
-
// The stored key is removed even when the route is already gone, so a hand-deleted
|
|
123
|
-
// route never leaves its secret behind.
|
|
124
|
-
const droppedCredential = deleteCredential(credentialRef(provider.id), notes);
|
|
125
150
|
const file = settingsFile();
|
|
126
|
-
|
|
151
|
+
// Settings must be understood before any credential can be removed.
|
|
152
|
+
const doc = parseSettings(file);
|
|
127
153
|
const at = ["llm-pi-ai", "providers", provider.id];
|
|
128
|
-
|
|
129
|
-
|
|
154
|
+
const hasProvider = doc.hasIn(at);
|
|
155
|
+
if (hasProvider)
|
|
156
|
+
doc.deleteIn(at);
|
|
157
|
+
const referenced = credentialReferences(doc);
|
|
158
|
+
const removable = new Set([managedCredentialRef(provider.id), legacyManagedCredentialRef(provider.id)].filter((ref) => !referenced.has(ref)));
|
|
159
|
+
const droppedCredential = deleteCredentials(removable, notes);
|
|
160
|
+
if (!hasProvider) {
|
|
161
|
+
const why = `no llm-pi-ai.providers.${provider.id} entry`;
|
|
130
162
|
if (!droppedCredential)
|
|
131
163
|
return { app: this.id, changed: [], notes, skipped: why };
|
|
132
164
|
notes.push(why);
|
|
133
165
|
return { app: this.id, changed: [droppedCredential], notes };
|
|
134
166
|
}
|
|
135
|
-
doc.deleteIn(at);
|
|
136
167
|
if (doc.getIn(["agent-default-model", "provider"]) === provider.id) {
|
|
137
168
|
doc.delete("agent-default-model");
|
|
138
169
|
notes.push("default model selection reset (was pointing at this provider)");
|
|
@@ -147,10 +178,7 @@ export const dsh = {
|
|
|
147
178
|
if (backup)
|
|
148
179
|
notes.push(`backup: ${backup}`);
|
|
149
180
|
writeFileAtomic(file, renderSettings(file, doc));
|
|
150
|
-
|
|
151
|
-
if (droppedCredential)
|
|
152
|
-
changed.push(droppedCredential);
|
|
153
|
-
return { app: this.id, changed, notes };
|
|
181
|
+
return { app: this.id, changed: droppedCredential ? [file, droppedCredential] : [file], notes };
|
|
154
182
|
},
|
|
155
183
|
current() {
|
|
156
184
|
const text = readTextIfExists(settingsFile());
|
|
@@ -215,34 +243,38 @@ export const dsh = {
|
|
|
215
243
|
];
|
|
216
244
|
});
|
|
217
245
|
},
|
|
218
|
-
};
|
|
246
|
+
});
|
|
219
247
|
/** Read the managed store's `refs` section; an unreadable document yields no keys. */
|
|
220
248
|
function readCredentialRefs() {
|
|
221
|
-
const text = readTextIfExists(credentialsFile());
|
|
222
|
-
if (!text)
|
|
223
|
-
return {};
|
|
224
249
|
try {
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
return refs;
|
|
229
|
-
// pre-release flat layout: top-level reference-to-secret mapping
|
|
230
|
-
if (parsed && !("version" in parsed))
|
|
231
|
-
return parsed;
|
|
232
|
-
return {};
|
|
250
|
+
const doc = parseCredentials(credentialsFile());
|
|
251
|
+
const parsed = doc.toJS();
|
|
252
|
+
return (doc.get("version") === undefined ? parsed : parsed.refs ?? {});
|
|
233
253
|
}
|
|
234
254
|
catch {
|
|
235
255
|
return {};
|
|
236
256
|
}
|
|
237
257
|
}
|
|
238
258
|
function parseCredentials(file) {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
|
|
259
|
+
const doc = parseYamlMapping(file, readTextIfExists(file));
|
|
260
|
+
const version = doc.get("version");
|
|
261
|
+
if (version !== undefined && version !== CREDENTIALS_VERSION) {
|
|
262
|
+
const label = typeof version === "number" ? String(version) : "an unsupported value";
|
|
263
|
+
throw new Error(`${file} declares version ${label}; agentsw writes version ${CREDENTIALS_VERSION}`);
|
|
264
|
+
}
|
|
265
|
+
if (version === undefined) {
|
|
266
|
+
const flat = Object.entries(doc.toJS()).every(([key, value]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && typeof value === "string" && value.length > 0);
|
|
267
|
+
if (!flat)
|
|
268
|
+
throw new Error(`${file} is neither the version ${CREDENTIALS_VERSION} layout nor a pre-release flat document; refusing to rewrite it`);
|
|
269
|
+
}
|
|
270
|
+
else if (doc.has("refs") && !YAML.isMap(doc.get("refs"))) {
|
|
271
|
+
throw new Error(`${file}: expected refs to be a mapping`);
|
|
272
|
+
}
|
|
273
|
+
const refs = doc.get("refs");
|
|
274
|
+
if (version !== undefined && YAML.isMap(refs) && refs.items.some((pair) => !YAML.isScalar(pair.key) || typeof pair.key.value !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(pair.key.value) ||
|
|
275
|
+
!YAML.isScalar(pair.value) || typeof pair.value.value !== "string" || pair.value.value.length === 0)) {
|
|
276
|
+
throw new Error(`${file}: expected non-empty credential values with valid reference names`);
|
|
243
277
|
}
|
|
244
|
-
if (doc.contents == null)
|
|
245
|
-
doc.contents = doc.createNode({});
|
|
246
278
|
return doc;
|
|
247
279
|
}
|
|
248
280
|
/**
|
|
@@ -252,45 +284,65 @@ function parseCredentials(file) {
|
|
|
252
284
|
* looks unversioned (a hand-written `refs:` block, a future layout) is refused
|
|
253
285
|
* instead of being re-rooted one level deeper. Returns the file.
|
|
254
286
|
*/
|
|
255
|
-
function writeCredential(ref, apiKey, notes) {
|
|
287
|
+
function writeCredential(ref, apiKey, notes, oldRef, referenced) {
|
|
256
288
|
const file = credentialsFile();
|
|
257
289
|
const doc = parseCredentials(file);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
290
|
+
if (doc.get("version") === undefined) {
|
|
291
|
+
const refs = doc.contents;
|
|
292
|
+
const migrating = YAML.isMap(refs) && refs.items.length > 0;
|
|
293
|
+
doc.contents = doc.createNode({ version: CREDENTIALS_VERSION });
|
|
294
|
+
doc.set("refs", refs);
|
|
295
|
+
if (migrating)
|
|
296
|
+
notes.push(`migrated ${file} to the version ${CREDENTIALS_VERSION} layout`);
|
|
262
297
|
}
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
if (!flat) {
|
|
266
|
-
throw new Error(`${file} is neither the version ${CREDENTIALS_VERSION} layout nor a pre-release flat document; refusing to rewrite it`);
|
|
267
|
-
}
|
|
268
|
-
doc.contents = doc.createNode({ version: CREDENTIALS_VERSION, refs: root });
|
|
269
|
-
notes.push(`migrated ${file} to the version ${CREDENTIALS_VERSION} layout`);
|
|
298
|
+
if (referenced.has(ref) && doc.getIn(["refs", ref]) !== apiKey) {
|
|
299
|
+
throw new Error(`${file}: credential reference is shared by another provider; refusing to replace it`);
|
|
270
300
|
}
|
|
271
|
-
doc.set("version", CREDENTIALS_VERSION);
|
|
272
301
|
doc.setIn(["refs", ref], apiKey);
|
|
302
|
+
if (oldRef && oldRef !== ref)
|
|
303
|
+
doc.deleteIn(["refs", oldRef]);
|
|
304
|
+
const text = serializeYamlMapping(file, doc);
|
|
273
305
|
const backup = backupFile(file);
|
|
274
306
|
if (backup)
|
|
275
307
|
notes.push(`backup: ${backup}`);
|
|
276
308
|
// dsh refuses a credentials document readable beyond its owner.
|
|
277
|
-
writeFileAtomic(file,
|
|
309
|
+
writeFileAtomic(file, text, 0o600);
|
|
278
310
|
return file;
|
|
279
311
|
}
|
|
280
|
-
/** Drop
|
|
281
|
-
function
|
|
312
|
+
/** Drop only managed references that no surviving route uses. */
|
|
313
|
+
function deleteCredentials(refs, notes) {
|
|
282
314
|
const file = credentialsFile();
|
|
283
|
-
|
|
284
|
-
if (!text)
|
|
315
|
+
if (readTextIfExists(file) === undefined)
|
|
285
316
|
return undefined;
|
|
286
317
|
const doc = parseCredentials(file);
|
|
287
|
-
|
|
288
|
-
|
|
318
|
+
let changed = false;
|
|
319
|
+
for (const ref of refs) {
|
|
320
|
+
const at = doc.get("version") === undefined ? [ref] : ["refs", ref];
|
|
321
|
+
if (doc.hasIn(at)) {
|
|
322
|
+
doc.deleteIn(at);
|
|
323
|
+
changed = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (!changed)
|
|
289
327
|
return undefined;
|
|
290
|
-
doc
|
|
328
|
+
const text = serializeYamlMapping(file, doc);
|
|
291
329
|
const backup = backupFile(file);
|
|
292
330
|
if (backup)
|
|
293
331
|
notes.push(`backup: ${backup}`);
|
|
294
|
-
writeFileAtomic(file,
|
|
332
|
+
writeFileAtomic(file, text, 0o600);
|
|
295
333
|
return file;
|
|
296
334
|
}
|
|
335
|
+
function credentialReferences(doc, exceptId) {
|
|
336
|
+
const providers = doc.getIn(["llm-pi-ai", "providers"]);
|
|
337
|
+
const refs = new Set();
|
|
338
|
+
if (YAML.isMap(providers)) {
|
|
339
|
+
for (const pair of providers.items) {
|
|
340
|
+
if ((YAML.isScalar(pair.key) ? pair.key.value : pair.key) === exceptId)
|
|
341
|
+
continue;
|
|
342
|
+
const ref = YAML.isMap(pair.value) ? pair.value.get("apiKeyEnv") : undefined;
|
|
343
|
+
if (typeof ref === "string")
|
|
344
|
+
refs.add(ref);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return refs;
|
|
348
|
+
}
|
package/dist/targets/hermes.js
CHANGED
|
@@ -2,6 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import { backupFile, home, localAppDataDir, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
5
|
+
import { envAssignments, removeEnvAssignments, upsertEnvAssignment } from "../envfile.js";
|
|
6
|
+
import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "../provider-identity.js";
|
|
7
|
+
import { transactionalTarget } from "../target-transaction.js";
|
|
8
|
+
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
5
9
|
import { sdkBaseUrl } from "./wire.js";
|
|
6
10
|
function hermesHome() {
|
|
7
11
|
const env = process.env.HERMES_HOME?.trim();
|
|
@@ -12,12 +16,45 @@ function hermesHome() {
|
|
|
12
16
|
}
|
|
13
17
|
return path.join(home, ".hermes");
|
|
14
18
|
}
|
|
19
|
+
function parseConfig(file, text) {
|
|
20
|
+
const doc = parseYamlMapping(file, text);
|
|
21
|
+
for (const key of ["providers", "model"]) {
|
|
22
|
+
if (doc.has(key) && !YAML.isMap(doc.get(key)))
|
|
23
|
+
throw new Error(`${file}: expected ${key} to be a mapping`);
|
|
24
|
+
}
|
|
25
|
+
const providers = doc.get("providers");
|
|
26
|
+
if (YAML.isMap(providers)) {
|
|
27
|
+
for (const pair of providers.items) {
|
|
28
|
+
if (!YAML.isMap(pair.value))
|
|
29
|
+
throw new Error(`${file}: expected provider mappings`);
|
|
30
|
+
const models = pair.value.get("models");
|
|
31
|
+
if (models !== undefined && (!YAML.isMap(models) || models.items.some((model) => !YAML.isMap(model.value)))) {
|
|
32
|
+
throw new Error(`${file}: expected model mappings`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return doc;
|
|
37
|
+
}
|
|
38
|
+
function credentialReferences(doc, exceptId) {
|
|
39
|
+
const refs = new Set();
|
|
40
|
+
const providers = doc.get("providers");
|
|
41
|
+
if (YAML.isMap(providers)) {
|
|
42
|
+
for (const pair of providers.items) {
|
|
43
|
+
if ((YAML.isScalar(pair.key) ? pair.key.value : pair.key) === exceptId)
|
|
44
|
+
continue;
|
|
45
|
+
const ref = YAML.isMap(pair.value) ? pair.value.get("key_env") : undefined;
|
|
46
|
+
if (typeof ref === "string")
|
|
47
|
+
refs.add(ref);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return refs;
|
|
51
|
+
}
|
|
15
52
|
/**
|
|
16
53
|
* hermes (NousResearch): providers dict + model section in ~/.hermes/config.yaml,
|
|
17
54
|
* API key in ~/.hermes/.env referenced via key_env. hermes refuses to load an
|
|
18
55
|
* unparseable config.yaml, so we round-trip through YAML Document (comments kept).
|
|
19
56
|
*/
|
|
20
|
-
export const hermes = {
|
|
57
|
+
export const hermes = transactionalTarget({
|
|
21
58
|
id: "hermes",
|
|
22
59
|
name: "Hermes",
|
|
23
60
|
protocols: ["openai", "anthropic"],
|
|
@@ -29,56 +66,66 @@ export const hermes = {
|
|
|
29
66
|
const envFile = path.join(home, ".env");
|
|
30
67
|
const notes = [];
|
|
31
68
|
const text = readTextIfExists(configFile);
|
|
32
|
-
const doc =
|
|
33
|
-
|
|
34
|
-
throw new Error(`${configFile} has YAML errors; refusing to rewrite: ${doc.errors[0]?.message}`);
|
|
35
|
-
}
|
|
36
|
-
if (doc.contents == null)
|
|
37
|
-
doc.contents = doc.createNode({});
|
|
38
|
-
const keyVar = `AGENTSW_${provider.id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
69
|
+
const doc = parseConfig(configFile, text);
|
|
70
|
+
const keyVar = managedCredentialRef(provider.id);
|
|
39
71
|
const at = ["providers", provider.id];
|
|
40
72
|
const prev = doc.getIn(at)?.toJSON?.();
|
|
41
|
-
const prevModels = (prev?.models ?? {});
|
|
42
|
-
const models = {};
|
|
43
|
-
for (const m of provider.models) {
|
|
44
|
-
const old = { ...prevModels[m.id] };
|
|
45
|
-
delete old.context_length; // owned: a model that loses its size must not keep the old one
|
|
46
|
-
models[m.id] = { ...old, ...(m.contextWindow ? { context_length: m.contextWindow } : {}) };
|
|
47
|
-
}
|
|
48
73
|
const entry = {
|
|
49
74
|
name: provider.name,
|
|
50
75
|
api: sdkBaseUrl(provider.protocol, provider.baseUrl),
|
|
51
76
|
key_env: keyVar,
|
|
52
77
|
transport: provider.protocol === "anthropic"
|
|
53
78
|
? "anthropic_messages"
|
|
54
|
-
: provider.openaiApi === "responses"
|
|
79
|
+
: provider.openaiApi === "responses" || (provider.openaiApi === undefined && prev?.transport === "codex_responses")
|
|
55
80
|
? "codex_responses"
|
|
56
81
|
: "chat_completions",
|
|
57
82
|
default_model: provider.defaultModel,
|
|
58
|
-
models,
|
|
59
83
|
};
|
|
60
84
|
// Key-by-key when the route exists: provider-level keys agentsw does not model
|
|
61
85
|
// (and their comments) belong to the user.
|
|
62
86
|
if (YAML.isMap(doc.getIn(at))) {
|
|
63
87
|
for (const [key, value] of Object.entries(entry))
|
|
64
|
-
doc.setIn([...at, key],
|
|
88
|
+
doc.setIn([...at, key], value);
|
|
65
89
|
}
|
|
66
90
|
else {
|
|
67
91
|
doc.setIn(at, doc.createNode(entry));
|
|
68
92
|
}
|
|
93
|
+
const modelsAt = [...at, "models"];
|
|
94
|
+
if (!doc.hasIn(modelsAt))
|
|
95
|
+
doc.setIn(modelsAt, doc.createNode({}));
|
|
96
|
+
const models = doc.getIn(modelsAt);
|
|
97
|
+
const modelIds = new Set(provider.models.map((model) => model.id));
|
|
98
|
+
for (const pair of [...models.items]) {
|
|
99
|
+
if (YAML.isScalar(pair.key) && !modelIds.has(String(pair.key.value)))
|
|
100
|
+
models.delete(pair.key.value);
|
|
101
|
+
}
|
|
102
|
+
for (const model of provider.models) {
|
|
103
|
+
const modelAt = [...modelsAt, model.id];
|
|
104
|
+
if (!doc.hasIn(modelAt))
|
|
105
|
+
doc.setIn(modelAt, doc.createNode({}));
|
|
106
|
+
if (model.contextWindow)
|
|
107
|
+
doc.setIn([...modelAt, "context_length"], model.contextWindow);
|
|
108
|
+
else
|
|
109
|
+
doc.deleteIn([...modelAt, "context_length"]);
|
|
110
|
+
}
|
|
69
111
|
doc.setIn(["model", "provider"], provider.id);
|
|
70
112
|
doc.setIn(["model", "default"], provider.defaultModel);
|
|
113
|
+
const envText = readTextIfExists(envFile) ?? "";
|
|
114
|
+
const assignments = envAssignments(envFile, envText);
|
|
115
|
+
const referenced = credentialReferences(doc, provider.id);
|
|
116
|
+
if (referenced.has(keyVar) && assignments.filter((assignment) => assignment.name === keyVar).at(-1)?.value !== provider.apiKey) {
|
|
117
|
+
throw new Error(`${envFile}: credential reference is shared by another provider; refusing to replace it`);
|
|
118
|
+
}
|
|
119
|
+
let nextEnv = upsertEnvAssignment(envFile, envText, keyVar, provider.apiKey);
|
|
120
|
+
const oldRef = prev?.key_env;
|
|
121
|
+
if (typeof oldRef === "string" && oldRef !== keyVar && isManagedCredentialRef(oldRef, provider.id) && !referenced.has(oldRef)) {
|
|
122
|
+
nextEnv = removeEnvAssignments(envFile, nextEnv, new Set([oldRef]));
|
|
123
|
+
}
|
|
124
|
+
const nextConfig = serializeYamlMapping(configFile, doc);
|
|
71
125
|
const configBackup = backupFile(configFile);
|
|
72
126
|
if (configBackup)
|
|
73
127
|
notes.push(`backup: ${configBackup}`);
|
|
74
|
-
writeFileAtomic(configFile,
|
|
75
|
-
// upsert the key into ~/.hermes/.env
|
|
76
|
-
const envText = readTextIfExists(envFile) ?? "";
|
|
77
|
-
const line = `${keyVar}=${provider.apiKey}`;
|
|
78
|
-
const pattern = new RegExp(`^${keyVar}=.*$`, "m");
|
|
79
|
-
const nextEnv = pattern.test(envText)
|
|
80
|
-
? envText.replace(pattern, line)
|
|
81
|
-
: envText + (envText.endsWith("\n") || envText === "" ? "" : "\n") + line + "\n";
|
|
128
|
+
writeFileAtomic(configFile, nextConfig);
|
|
82
129
|
const envBackup = backupFile(envFile);
|
|
83
130
|
if (envBackup)
|
|
84
131
|
notes.push(`backup: ${envBackup}`);
|
|
@@ -89,18 +136,12 @@ export const hermes = {
|
|
|
89
136
|
const home = hermesHome();
|
|
90
137
|
const configFile = path.join(home, "config.yaml");
|
|
91
138
|
const envFile = path.join(home, ".env");
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const doc = YAML.parseDocument(text);
|
|
96
|
-
if (doc.errors.length) {
|
|
97
|
-
throw new Error(`${configFile} has YAML errors; refusing to rewrite: ${doc.errors[0]?.message}`);
|
|
98
|
-
}
|
|
99
|
-
if (!doc.hasIn(["providers", provider.id])) {
|
|
100
|
-
return { app: this.id, changed: [], notes: [], skipped: `no providers.${provider.id} entry` };
|
|
101
|
-
}
|
|
102
|
-
doc.deleteIn(["providers", provider.id]);
|
|
139
|
+
const doc = parseConfig(configFile, readTextIfExists(configFile));
|
|
140
|
+
const at = ["providers", provider.id];
|
|
141
|
+
const hasProvider = doc.hasIn(at);
|
|
103
142
|
const notes = [];
|
|
143
|
+
if (hasProvider)
|
|
144
|
+
doc.deleteIn(at);
|
|
104
145
|
if (doc.getIn(["model", "provider"]) === provider.id) {
|
|
105
146
|
doc.deleteIn(["model", "provider"]);
|
|
106
147
|
doc.deleteIn(["model", "default"]);
|
|
@@ -112,21 +153,27 @@ export const hermes = {
|
|
|
112
153
|
if (YAML.isMap(node) && node.items.length === 0)
|
|
113
154
|
doc.delete(key);
|
|
114
155
|
}
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
if (configBackup)
|
|
118
|
-
notes.push(`backup: ${configBackup}`);
|
|
119
|
-
writeFileAtomic(configFile, doc.toString());
|
|
120
|
-
const keyVar = `AGENTSW_${provider.id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
156
|
+
const referenced = credentialReferences(doc);
|
|
157
|
+
const removable = new Set([managedCredentialRef(provider.id), legacyManagedCredentialRef(provider.id)].filter((ref) => !referenced.has(ref)));
|
|
121
158
|
const envText = readTextIfExists(envFile);
|
|
122
|
-
|
|
159
|
+
const nextEnv = envText === undefined ? undefined : removeEnvAssignments(envFile, envText, removable);
|
|
160
|
+
const changed = [];
|
|
161
|
+
if (hasProvider) {
|
|
162
|
+
const config = serializeYamlMapping(configFile, doc);
|
|
163
|
+
const configBackup = backupFile(configFile);
|
|
164
|
+
if (configBackup)
|
|
165
|
+
notes.push(`backup: ${configBackup}`);
|
|
166
|
+
writeFileAtomic(configFile, config);
|
|
167
|
+
changed.push(configFile);
|
|
168
|
+
}
|
|
169
|
+
if (nextEnv !== undefined && nextEnv !== envText) {
|
|
123
170
|
const envBackup = backupFile(envFile);
|
|
124
171
|
if (envBackup)
|
|
125
172
|
notes.push(`backup: ${envBackup}`);
|
|
126
|
-
writeFileAtomic(envFile,
|
|
173
|
+
writeFileAtomic(envFile, nextEnv, 0o600);
|
|
127
174
|
changed.push(envFile);
|
|
128
175
|
}
|
|
129
|
-
return { app: this.id, changed, notes };
|
|
176
|
+
return changed.length ? { app: this.id, changed, notes } : { app: this.id, changed, notes, skipped: `no providers.${provider.id} entry or managed credential` };
|
|
130
177
|
},
|
|
131
178
|
current() {
|
|
132
179
|
const text = readTextIfExists(path.join(hermesHome(), "config.yaml"));
|
|
@@ -156,8 +203,10 @@ export const hermes = {
|
|
|
156
203
|
}
|
|
157
204
|
if (!parsed?.providers)
|
|
158
205
|
return [];
|
|
159
|
-
const
|
|
160
|
-
const
|
|
206
|
+
const envFile = path.join(hh, ".env");
|
|
207
|
+
const envText = readTextIfExists(envFile) ?? "";
|
|
208
|
+
const values = new Map(envAssignments(envFile, envText).map((assignment) => [assignment.name, assignment.value]));
|
|
209
|
+
const readEnv = (key) => process.env[key] ?? values.get(key);
|
|
161
210
|
const self = this.id;
|
|
162
211
|
return Object.entries(parsed.providers).flatMap(([id, entry]) => {
|
|
163
212
|
if (!entry || typeof entry.api !== "string" || !entry.api)
|
|
@@ -180,4 +229,4 @@ export const hermes = {
|
|
|
180
229
|
return [{ id, name: entry.name ?? id, protocol, openaiApi, baseUrl: entry.api, apiKey, keyEnv, models, defaultModel, source: self }];
|
|
181
230
|
});
|
|
182
231
|
},
|
|
183
|
-
};
|
|
232
|
+
});
|
package/dist/targets/omp.js
CHANGED
|
@@ -3,62 +3,26 @@ import path from "node:path";
|
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import { backupFile, home, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
5
5
|
import { looksLikeEnvName } from "../slug.js";
|
|
6
|
+
import { transactionalTarget } from "../target-transaction.js";
|
|
7
|
+
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
6
8
|
import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
|
|
7
9
|
/** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
|
|
8
10
|
const OWNED_MODEL_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
|
|
9
11
|
const agentDir = path.join(home, ".omp", "agent");
|
|
10
12
|
const modelsYml = path.join(agentDir, "models.yml");
|
|
11
13
|
const modelsYaml = path.join(agentDir, "models.yaml");
|
|
12
|
-
/** Resolve references before edits can remove their anchors or mutate shared providers. */
|
|
13
14
|
function parseModelsDocument(file, text) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
throw doc.errors[0];
|
|
18
|
-
if (doc.contents == null)
|
|
19
|
-
doc.contents = doc.createNode({});
|
|
20
|
-
// Validate references with the full document context and the default alias limit.
|
|
21
|
-
// Cycles are valid YAML, but not a usable model configuration; reject before expansion.
|
|
22
|
-
JSON.stringify(doc.toJS());
|
|
23
|
-
const expanded = new Map();
|
|
24
|
-
YAML.visit(doc, {
|
|
25
|
-
Alias(_key, alias) {
|
|
26
|
-
const node = doc.createNode(alias.toJS(doc), { aliasDuplicateObjects: false });
|
|
27
|
-
node.comment = alias.comment;
|
|
28
|
-
node.commentBefore = alias.commentBefore;
|
|
29
|
-
node.spaceBefore = alias.spaceBefore;
|
|
30
|
-
expanded.set(alias, node);
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
// Resolve all references before replacing any nodes (anchors may be reused).
|
|
34
|
-
YAML.visit(doc, { Alias: (_key, alias) => expanded.get(alias) });
|
|
35
|
-
if (!YAML.isMap(doc.contents))
|
|
36
|
-
throw new Error("expected a configuration mapping");
|
|
37
|
-
if (doc.hasIn(["providers"]) && !YAML.isMap(doc.getIn(["providers"]))) {
|
|
38
|
-
throw new Error("expected providers to be a mapping");
|
|
39
|
-
}
|
|
40
|
-
return doc;
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
throw new Error(`${file} has invalid YAML: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/** Validate the emitted document before any backup or write. */
|
|
47
|
-
function serializeModelsDocument(file, doc) {
|
|
48
|
-
try {
|
|
49
|
-
const text = doc.toString();
|
|
50
|
-
YAML.parse(text);
|
|
51
|
-
return text;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
throw new Error(`${file} has invalid YAML: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
+
const doc = parseYamlMapping(file, text);
|
|
16
|
+
if (doc.hasIn(["providers"]) && !YAML.isMap(doc.getIn(["providers"]))) {
|
|
17
|
+
throw new Error(`${file}: expected providers to be a mapping`);
|
|
55
18
|
}
|
|
19
|
+
return doc;
|
|
56
20
|
}
|
|
57
21
|
/**
|
|
58
22
|
* Oh My Pi: providers live in ~/.omp/agent/models.yml (models.yaml fallback).
|
|
59
23
|
* YAML Document round-trip preserves existing comments and unrelated providers.
|
|
60
24
|
*/
|
|
61
|
-
export const omp = {
|
|
25
|
+
export const omp = transactionalTarget({
|
|
62
26
|
id: "omp",
|
|
63
27
|
name: "Oh My Pi",
|
|
64
28
|
protocols: ["openai", "anthropic"],
|
|
@@ -72,7 +36,7 @@ export const omp = {
|
|
|
72
36
|
const doc = parseModelsDocument(file, text);
|
|
73
37
|
const prev = doc.getIn(["providers", provider.id])?.toJS(doc);
|
|
74
38
|
const anthropic = provider.protocol === "anthropic";
|
|
75
|
-
const api = apiValue(provider.protocol, provider.openaiApi, entryApi(prev ?? {}));
|
|
39
|
+
const api = apiValue(provider.protocol, provider.openaiApi, prev?.api ?? entryApi(prev ?? {}));
|
|
76
40
|
const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
|
|
77
41
|
const models = mergeModels(prev?.models, provider.models.map((m) => ({
|
|
78
42
|
id: m.id,
|
|
@@ -119,7 +83,7 @@ export const omp = {
|
|
|
119
83
|
else {
|
|
120
84
|
doc.setIn(at, doc.createNode(entry));
|
|
121
85
|
}
|
|
122
|
-
const output =
|
|
86
|
+
const output = serializeYamlMapping(file, doc);
|
|
123
87
|
const backup = backupFile(file);
|
|
124
88
|
if (backup)
|
|
125
89
|
notes.push(`backup: ${backup}`);
|
|
@@ -137,7 +101,7 @@ export const omp = {
|
|
|
137
101
|
}
|
|
138
102
|
doc.deleteIn(["providers", provider.id]);
|
|
139
103
|
const notes = [];
|
|
140
|
-
const output =
|
|
104
|
+
const output = serializeYamlMapping(file, doc);
|
|
141
105
|
const backup = backupFile(file);
|
|
142
106
|
if (backup)
|
|
143
107
|
notes.push(`backup: ${backup}`);
|
|
@@ -207,4 +171,4 @@ export const omp = {
|
|
|
207
171
|
];
|
|
208
172
|
});
|
|
209
173
|
},
|
|
210
|
-
};
|
|
174
|
+
});
|