agentsw 0.9.2 → 0.10.1
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 +50 -4
- package/README.zh-CN.md +44 -3
- 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 +77 -18
- package/dist/i18n.js +147 -3
- package/dist/index.js +14 -2
- package/dist/menu.js +141 -121
- package/dist/metadata.js +34 -9
- 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 +20 -35
- 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 +10 -21
- package/dist/targets/wire.js +22 -7
- package/dist/targets/workbuddy.js +6 -4
- package/dist/ui.js +157 -0
- package/package.json +2 -2
package/dist/targets/dsh.js
CHANGED
|
@@ -1,35 +1,16 @@
|
|
|
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). */
|
|
31
12
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
32
|
-
/** Per-model keys this adapter writes; one
|
|
13
|
+
/** Per-model keys this adapter writes; one a metadata-bearing entry stops emitting is cleared, not inherited. */
|
|
33
14
|
const OWNED_MODEL_KEYS = ["id", "name", "contextWindow", "maxTokens", "input", "reasoningEfforts"];
|
|
34
15
|
/** models.dev efforts -> `{ level: wire spelling }`; an unoffered level is omitted, not nulled. */
|
|
35
16
|
function reasoningEfforts(efforts) {
|
|
@@ -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);
|
package/dist/targets/hermes.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
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 { hermesConfigFile, hermesDir, hermesEnvFile } from "../app-paths.js";
|
|
5
5
|
import { envAssignments, removeEnvAssignments, upsertEnvAssignment } from "../envfile.js";
|
|
6
6
|
import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "../provider-identity.js";
|
|
7
7
|
import { transactionalTarget } from "../target-transaction.js";
|
|
8
8
|
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
9
9
|
import { sdkBaseUrl } from "./wire.js";
|
|
10
10
|
function hermesHome() {
|
|
11
|
-
|
|
12
|
-
if (env)
|
|
13
|
-
return env;
|
|
14
|
-
if (process.platform === "win32") {
|
|
15
|
-
return localAppDataDir("hermes");
|
|
16
|
-
}
|
|
17
|
-
return path.join(home, ".hermes");
|
|
11
|
+
return hermesDir();
|
|
18
12
|
}
|
|
19
13
|
function parseConfig(file, text) {
|
|
20
14
|
const doc = parseYamlMapping(file, text);
|
|
@@ -58,12 +52,11 @@ export const hermes = transactionalTarget({
|
|
|
58
52
|
id: "hermes",
|
|
59
53
|
name: "Hermes",
|
|
60
54
|
protocols: ["openai", "anthropic"],
|
|
61
|
-
configPaths: [
|
|
55
|
+
configPaths: [hermesConfigFile(), hermesEnvFile()],
|
|
62
56
|
detect: () => fs.existsSync(hermesHome()),
|
|
63
57
|
async apply(provider) {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const envFile = path.join(home, ".env");
|
|
58
|
+
const configFile = hermesConfigFile();
|
|
59
|
+
const envFile = hermesEnvFile();
|
|
67
60
|
const notes = [];
|
|
68
61
|
const text = readTextIfExists(configFile);
|
|
69
62
|
const doc = parseConfig(configFile, text);
|
|
@@ -133,9 +126,8 @@ export const hermes = transactionalTarget({
|
|
|
133
126
|
return { app: this.id, changed: [configFile, envFile], notes };
|
|
134
127
|
},
|
|
135
128
|
async prune(provider) {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const envFile = path.join(home, ".env");
|
|
129
|
+
const configFile = hermesConfigFile();
|
|
130
|
+
const envFile = hermesEnvFile();
|
|
139
131
|
const doc = parseConfig(configFile, readTextIfExists(configFile));
|
|
140
132
|
const at = ["providers", provider.id];
|
|
141
133
|
const hasProvider = doc.hasIn(at);
|
|
@@ -176,7 +168,7 @@ export const hermes = transactionalTarget({
|
|
|
176
168
|
return changed.length ? { app: this.id, changed, notes } : { app: this.id, changed, notes, skipped: `no providers.${provider.id} entry or managed credential` };
|
|
177
169
|
},
|
|
178
170
|
current() {
|
|
179
|
-
const text = readTextIfExists(
|
|
171
|
+
const text = readTextIfExists(hermesConfigFile());
|
|
180
172
|
if (!text)
|
|
181
173
|
return undefined;
|
|
182
174
|
try {
|
|
@@ -190,8 +182,7 @@ export const hermes = transactionalTarget({
|
|
|
190
182
|
}
|
|
191
183
|
},
|
|
192
184
|
candidates() {
|
|
193
|
-
const
|
|
194
|
-
const text = readTextIfExists(path.join(hh, "config.yaml"));
|
|
185
|
+
const text = readTextIfExists(hermesConfigFile());
|
|
195
186
|
if (!text)
|
|
196
187
|
return [];
|
|
197
188
|
let parsed;
|
|
@@ -203,7 +194,7 @@ export const hermes = transactionalTarget({
|
|
|
203
194
|
}
|
|
204
195
|
if (!parsed?.providers)
|
|
205
196
|
return [];
|
|
206
|
-
const envFile =
|
|
197
|
+
const envFile = hermesEnvFile();
|
|
207
198
|
const envText = readTextIfExists(envFile) ?? "";
|
|
208
199
|
const values = new Map(envAssignments(envFile, envText).map((assignment) => [assignment.name, assignment.value]));
|
|
209
200
|
const readEnv = (key) => process.env[key] ?? values.get(key);
|
package/dist/targets/omp.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
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 { ompAgentDir, ompModelsFiles } from "../app-paths.js";
|
|
5
5
|
import { looksLikeEnvName } from "../slug.js";
|
|
6
6
|
import { transactionalTarget } from "../target-transaction.js";
|
|
7
7
|
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
8
8
|
import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
|
|
9
9
|
/** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
|
|
10
10
|
const OWNED_MODEL_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
11
|
+
/** omp has no directory override; the file set is shared with remove/rename. */
|
|
12
|
+
function modelFiles() {
|
|
13
|
+
const [yml, yaml] = ompModelsFiles();
|
|
14
|
+
return [yml, yaml];
|
|
15
|
+
}
|
|
14
16
|
function parseModelsDocument(file, text) {
|
|
15
17
|
const doc = parseYamlMapping(file, text);
|
|
16
18
|
if (doc.hasIn(["providers"]) && !YAML.isMap(doc.getIn(["providers"]))) {
|
|
@@ -26,11 +28,14 @@ export const omp = transactionalTarget({
|
|
|
26
28
|
id: "omp",
|
|
27
29
|
name: "Oh My Pi",
|
|
28
30
|
protocols: ["openai", "anthropic"],
|
|
29
|
-
configPaths
|
|
30
|
-
|
|
31
|
+
get configPaths() {
|
|
32
|
+
return [modelFiles()[0]];
|
|
33
|
+
},
|
|
34
|
+
detect: () => fs.existsSync(ompAgentDir()),
|
|
31
35
|
async apply(provider) {
|
|
32
36
|
const notes = [];
|
|
33
37
|
// Respect an existing models.yaml if models.yml is absent (omp precedence: yml then yaml).
|
|
38
|
+
const [modelsYml, modelsYaml] = modelFiles();
|
|
34
39
|
const file = !fs.existsSync(modelsYml) && fs.existsSync(modelsYaml) ? modelsYaml : modelsYml;
|
|
35
40
|
const text = readTextIfExists(file);
|
|
36
41
|
const doc = parseModelsDocument(file, text);
|
|
@@ -92,6 +97,7 @@ export const omp = transactionalTarget({
|
|
|
92
97
|
return { app: this.id, changed: [file], notes };
|
|
93
98
|
},
|
|
94
99
|
async prune(provider) {
|
|
100
|
+
const [modelsYml, modelsYaml] = modelFiles();
|
|
95
101
|
const file = fs.existsSync(modelsYml) ? modelsYml : fs.existsSync(modelsYaml) ? modelsYaml : undefined;
|
|
96
102
|
if (!file)
|
|
97
103
|
return { app: this.id, changed: [], notes: [], skipped: "no models.yml" };
|
|
@@ -109,6 +115,7 @@ export const omp = transactionalTarget({
|
|
|
109
115
|
return { app: this.id, changed: [file], notes };
|
|
110
116
|
},
|
|
111
117
|
current() {
|
|
118
|
+
const [modelsYml, modelsYaml] = modelFiles();
|
|
112
119
|
const file = fs.existsSync(modelsYml) ? modelsYml : fs.existsSync(modelsYaml) ? modelsYaml : undefined;
|
|
113
120
|
if (!file)
|
|
114
121
|
return undefined;
|
|
@@ -122,6 +129,7 @@ export const omp = transactionalTarget({
|
|
|
122
129
|
}
|
|
123
130
|
},
|
|
124
131
|
candidates() {
|
|
132
|
+
const [modelsYml, modelsYaml] = modelFiles();
|
|
125
133
|
const file = fs.existsSync(modelsYml) ? modelsYml : fs.existsSync(modelsYaml) ? modelsYaml : undefined;
|
|
126
134
|
if (!file)
|
|
127
135
|
return [];
|
package/dist/targets/opencode.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { backupFile, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
3
|
+
import { isJsonObject, readJsoncObject, editJsoncObject } from "../jsonc.js";
|
|
4
|
+
import { opencodeConfigDirs, opencodeConfigFiles, opencodePrimaryConfigFile } from "../app-paths.js";
|
|
5
5
|
import { transactionalTarget } from "../target-transaction.js";
|
|
6
|
-
const customDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
7
|
-
const configFile = customDir
|
|
8
|
-
? path.join(expandHome(customDir), "opencode.json")
|
|
9
|
-
: path.join(appDataDir("opencode"), "opencode.json");
|
|
10
6
|
/**
|
|
11
|
-
* opencode: custom providers in
|
|
12
|
-
*
|
|
7
|
+
* opencode: custom providers in the "provider" map of its config document
|
|
8
|
+
* (opencode.json / opencode.jsonc / config.json under $OPENCODE_CONFIG_DIR or the
|
|
9
|
+
* default directory; $OPENCODE_CONFIG names one file explicitly), backed by
|
|
10
|
+
* @ai-sdk/openai-compatible or @ai-sdk/anthropic npm loaders. A JSONC round-trip
|
|
11
|
+
* keeps comments and every key agentsw does not own.
|
|
13
12
|
*/
|
|
14
13
|
export const opencode = transactionalTarget({
|
|
15
14
|
id: "opencode",
|
|
16
15
|
name: "opencode",
|
|
17
16
|
protocols: ["openai", "anthropic"],
|
|
18
|
-
configPaths
|
|
19
|
-
|
|
17
|
+
get configPaths() {
|
|
18
|
+
return [opencodePrimaryConfigFile()];
|
|
19
|
+
},
|
|
20
|
+
detect: () => opencodeConfigFiles().some((file) => fs.existsSync(file)) ||
|
|
21
|
+
opencodeConfigDirs().some((dir) => fs.existsSync(dir)),
|
|
20
22
|
async apply(provider) {
|
|
23
|
+
const configFile = opencodePrimaryConfigFile();
|
|
21
24
|
const notes = [];
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
+
const existed = readTextIfExists(configFile) !== undefined;
|
|
26
|
+
const document = readJsoncObject(configFile);
|
|
27
|
+
// Only a missing file is initialized with the schema hint; an existing {} stays as-is.
|
|
28
|
+
const config = existed
|
|
29
|
+
? { ...document.value }
|
|
30
|
+
: { $schema: "https://opencode.ai/config.json" };
|
|
31
|
+
if (config.provider !== undefined && !isJsonObject(config.provider)) {
|
|
25
32
|
throw new Error(`${configFile}: expected config and provider to be JSON objects`);
|
|
26
33
|
}
|
|
27
34
|
const anthropic = provider.protocol === "anthropic";
|
|
@@ -88,26 +95,36 @@ export const opencode = transactionalTarget({
|
|
|
88
95
|
};
|
|
89
96
|
config.provider = providerMap;
|
|
90
97
|
config.model = `${provider.id}/${provider.defaultModel}`;
|
|
91
|
-
if (provider.smallModel)
|
|
98
|
+
if (provider.smallModel) {
|
|
92
99
|
config.small_model = `${provider.id}/${provider.smallModel}`;
|
|
100
|
+
}
|
|
101
|
+
else if (typeof config.small_model === "string" && config.small_model.startsWith(`${provider.id}/`)) {
|
|
102
|
+
// This provider's own previous small model is obsolete; another provider's
|
|
103
|
+
// entry stays untouched, exactly as prune scopes its cleanup.
|
|
104
|
+
delete config.small_model;
|
|
105
|
+
}
|
|
93
106
|
const backup = backupFile(configFile);
|
|
94
107
|
if (backup)
|
|
95
108
|
notes.push(`backup: ${backup}`);
|
|
96
|
-
writeFileAtomic(configFile,
|
|
109
|
+
writeFileAtomic(configFile, editJsoncObject(document, config));
|
|
97
110
|
return { app: this.id, changed: [configFile], notes };
|
|
98
111
|
},
|
|
99
112
|
async prune(provider) {
|
|
100
|
-
const
|
|
101
|
-
|
|
113
|
+
const configFile = opencodePrimaryConfigFile();
|
|
114
|
+
const document = readJsoncObject(configFile);
|
|
115
|
+
const config = { ...document.value };
|
|
116
|
+
if (config.provider !== undefined && !isJsonObject(config.provider)) {
|
|
102
117
|
throw new Error(`${configFile}: expected config and provider to be JSON objects`);
|
|
103
118
|
}
|
|
104
|
-
const providerMap = config
|
|
105
|
-
if (!
|
|
119
|
+
const providerMap = { ...config.provider };
|
|
120
|
+
if (!providerMap[provider.id]) {
|
|
106
121
|
return { app: this.id, changed: [], notes: [], skipped: `no provider.${provider.id} entry` };
|
|
107
122
|
}
|
|
108
123
|
delete providerMap[provider.id];
|
|
109
124
|
if (Object.keys(providerMap).length === 0)
|
|
110
125
|
delete config.provider;
|
|
126
|
+
else
|
|
127
|
+
config.provider = providerMap;
|
|
111
128
|
const notes = [];
|
|
112
129
|
if (typeof config.model === "string" && config.model.startsWith(`${provider.id}/`)) {
|
|
113
130
|
delete config.model;
|
|
@@ -119,16 +136,16 @@ export const opencode = transactionalTarget({
|
|
|
119
136
|
const backup = backupFile(configFile);
|
|
120
137
|
if (backup)
|
|
121
138
|
notes.push(`backup: ${backup}`);
|
|
122
|
-
writeFileAtomic(configFile,
|
|
139
|
+
writeFileAtomic(configFile, editJsoncObject(document, config));
|
|
123
140
|
return { app: this.id, changed: [configFile], notes };
|
|
124
141
|
},
|
|
125
142
|
current() {
|
|
126
|
-
const config =
|
|
127
|
-
return config
|
|
143
|
+
const config = readJsoncObject(opencodePrimaryConfigFile()).value;
|
|
144
|
+
return typeof config.model === "string" ? config.model : undefined;
|
|
128
145
|
},
|
|
129
146
|
candidates() {
|
|
130
|
-
const config =
|
|
131
|
-
if (!config
|
|
147
|
+
const config = readJsoncObject(opencodePrimaryConfigFile()).value;
|
|
148
|
+
if (!config.provider)
|
|
132
149
|
return [];
|
|
133
150
|
const activeProvider = config.model?.includes("/") ? config.model.split("/")[0] : undefined;
|
|
134
151
|
const activeModel = activeProvider && config.model && config.model.includes("/") ? config.model.split("/").slice(1).join("/") : undefined;
|
package/dist/targets/pistyle.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { backupFile,
|
|
3
|
+
import { backupFile, writeFileAtomic } from "../fsutil.js";
|
|
4
|
+
import { piDir, piModelsFile, piSettingsFile } from "../app-paths.js";
|
|
4
5
|
import { editJsoncObject, isJsonObject, readJsoncObject } from "../jsonc.js";
|
|
5
6
|
import { looksLikeEnvName } from "../slug.js";
|
|
6
7
|
import { transactionalTarget } from "../target-transaction.js";
|
|
7
8
|
import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
|
|
8
|
-
/** Per-model keys this adapter writes; one
|
|
9
|
+
/** Per-model keys this adapter writes; one a metadata-bearing entry stops emitting is cleared, not inherited. */
|
|
9
10
|
const OWNED_MODEL_KEYS = [
|
|
10
11
|
"id",
|
|
11
12
|
"name",
|
|
@@ -57,17 +58,15 @@ function thinkingLevelMap(efforts) {
|
|
|
57
58
|
return map;
|
|
58
59
|
}
|
|
59
60
|
export function piStyleTarget(opts) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (env)
|
|
63
|
-
return env.startsWith("~") ? path.join(home, env.slice(1)) : env;
|
|
64
|
-
return path.join(home, opts.configDirName);
|
|
65
|
-
};
|
|
61
|
+
const id = opts.id;
|
|
62
|
+
const resolveDir = () => piDir(id);
|
|
66
63
|
return transactionalTarget({
|
|
67
64
|
id: opts.id,
|
|
68
65
|
name: opts.name,
|
|
69
66
|
protocols: ["openai", "anthropic"],
|
|
70
|
-
configPaths
|
|
67
|
+
get configPaths() {
|
|
68
|
+
return [piModelsFile(id), piSettingsFile(id)];
|
|
69
|
+
},
|
|
71
70
|
detect: () => fs.existsSync(path.dirname(resolveDir())),
|
|
72
71
|
async apply(provider) {
|
|
73
72
|
const dir = resolveDir();
|
|
@@ -232,15 +231,5 @@ export function piStyleTarget(opts) {
|
|
|
232
231
|
},
|
|
233
232
|
});
|
|
234
233
|
}
|
|
235
|
-
export const pi = piStyleTarget({
|
|
236
|
-
|
|
237
|
-
name: "pi",
|
|
238
|
-
configDirName: ".pi/agent",
|
|
239
|
-
dirEnvVar: "PI_CODING_AGENT_DIR",
|
|
240
|
-
});
|
|
241
|
-
export const prime = piStyleTarget({
|
|
242
|
-
id: "prime",
|
|
243
|
-
name: "prime-agent",
|
|
244
|
-
configDirName: ".prime/agent",
|
|
245
|
-
dirEnvVar: "PRIME_AGENT_CODING_AGENT_DIR",
|
|
246
|
-
});
|
|
234
|
+
export const pi = piStyleTarget({ id: "pi", name: "pi" });
|
|
235
|
+
export const prime = piStyleTarget({ id: "prime", name: "prime-agent" });
|
package/dist/targets/wire.js
CHANGED
|
@@ -69,12 +69,24 @@ export function entryApi(entry) {
|
|
|
69
69
|
/**
|
|
70
70
|
* Merge the model entries agentsw owns over the ones already in an app
|
|
71
71
|
* config: per-model keys the adapter does not model (`compat`, per-model wire
|
|
72
|
-
* overrides, ...) survive a re-sync,
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* overrides, ...) survive a re-sync, and an `owned` key the entry omits is
|
|
73
|
+
* cleared rather than lingering as last sync's value — a stale
|
|
74
|
+
* `thinkingLevelMap` beside `reasoning: false` is a state no fresh write
|
|
75
75
|
* produces.
|
|
76
|
+
*
|
|
77
|
+
* That clearing is only meaningful for an entry that carries metadata at all.
|
|
78
|
+
* A model the store knows nothing about arrives as a bare `{ id }` — that is
|
|
79
|
+
* exactly what enrichment does with an id no catalog row matched — and every
|
|
80
|
+
* adapter still adds the derived capability in `derived`. Absence in such a
|
|
81
|
+
* stub says nothing about the model, so the config's existing values are kept:
|
|
82
|
+
* deleting them would throw away limits an earlier lookup filled in, or that a
|
|
83
|
+
* person typed by hand, every time a catalog lookup misses. Once an entry
|
|
84
|
+
* carries any owned metadata of its own, the same absence is a statement about
|
|
85
|
+
* the model and is applied as before.
|
|
76
86
|
*/
|
|
77
|
-
export function mergeModels(previous, written, owned
|
|
87
|
+
export function mergeModels(previous, written, owned,
|
|
88
|
+
/** Owned keys every adapter derives instead of reading them from the model. */
|
|
89
|
+
derived = ["input"]) {
|
|
78
90
|
const prev = new Map();
|
|
79
91
|
if (Array.isArray(previous)) {
|
|
80
92
|
for (const m of previous) {
|
|
@@ -82,14 +94,17 @@ export function mergeModels(previous, written, owned) {
|
|
|
82
94
|
prev.set(m.id, m);
|
|
83
95
|
}
|
|
84
96
|
}
|
|
97
|
+
const informative = owned.filter((key) => key !== "id" && !derived.includes(key));
|
|
85
98
|
return written.map((m) => {
|
|
86
99
|
const old = prev.get(m.id);
|
|
87
100
|
if (!old)
|
|
88
101
|
return m;
|
|
89
102
|
const kept = { ...old };
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
const known = informative.some((key) => key in m);
|
|
104
|
+
if (known)
|
|
105
|
+
for (const key of owned)
|
|
106
|
+
if (!(key in m))
|
|
107
|
+
delete kept[key];
|
|
93
108
|
return { ...kept, ...m };
|
|
94
109
|
});
|
|
95
110
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { backupFile, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
|
|
4
4
|
import { isJsonObject } from "../jsonc.js";
|
|
5
|
+
import { workbuddyDir } from "../app-paths.js";
|
|
5
6
|
import { localProviderId } from "../provider-identity.js";
|
|
6
7
|
import { providerIdFromBaseUrl, providerNameFromBaseUrl } from "../slug.js";
|
|
7
8
|
import { transactionalTarget } from "../target-transaction.js";
|
|
8
9
|
function configDir() {
|
|
9
|
-
return
|
|
10
|
-
(process.platform === "win32" ? appDataDir("workbuddy") : path.join(home, ".workbuddy"));
|
|
10
|
+
return workbuddyDir();
|
|
11
11
|
}
|
|
12
12
|
function isModel(row) {
|
|
13
13
|
return isJsonObject(row) && typeof row.id === "string";
|
|
@@ -74,7 +74,9 @@ export const workbuddy = transactionalTarget({
|
|
|
74
74
|
id: "workbuddy",
|
|
75
75
|
name: "WorkBuddy",
|
|
76
76
|
protocols: ["openai"],
|
|
77
|
-
configPaths
|
|
77
|
+
get configPaths() {
|
|
78
|
+
return [path.join(configDir(), "models.json"), path.join(configDir(), "settings.json")];
|
|
79
|
+
},
|
|
78
80
|
detect: () => fs.existsSync(configDir()),
|
|
79
81
|
async apply(provider) {
|
|
80
82
|
const dir = configDir();
|