agentsw 0.9.1 → 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 +404 -212
- package/dist/discover.js +21 -11
- package/dist/gateway.js +5 -4
- package/dist/i18n.js +160 -3
- package/dist/index.js +14 -2
- package/dist/menu.js +141 -121
- package/dist/metadata.js +69 -20
- 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/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);
|
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,6 +1,7 @@
|
|
|
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";
|
|
@@ -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" });
|
|
@@ -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();
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { t } from "./i18n.js";
|
|
3
|
+
/** Thrown by cancelInteractive() inside the menu so a Ctrl-C returns to the menu instead of exiting. */
|
|
4
|
+
export class ActionCancelled extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("cancelled");
|
|
7
|
+
this.name = "ActionCancelled";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
let json = false;
|
|
11
|
+
let quiet = false;
|
|
12
|
+
let version = "";
|
|
13
|
+
let command = "";
|
|
14
|
+
let inMenu = false;
|
|
15
|
+
export function configureOutput(config) {
|
|
16
|
+
if (config.json !== undefined)
|
|
17
|
+
json = config.json;
|
|
18
|
+
if (config.quiet !== undefined)
|
|
19
|
+
quiet = config.quiet;
|
|
20
|
+
if (config.version !== undefined)
|
|
21
|
+
version = config.version;
|
|
22
|
+
}
|
|
23
|
+
export function setCommandName(name) {
|
|
24
|
+
command = name;
|
|
25
|
+
}
|
|
26
|
+
export function setInMenu(value) {
|
|
27
|
+
inMenu = value;
|
|
28
|
+
}
|
|
29
|
+
export function isJson() {
|
|
30
|
+
return json;
|
|
31
|
+
}
|
|
32
|
+
export function isQuiet() {
|
|
33
|
+
return quiet;
|
|
34
|
+
}
|
|
35
|
+
/** Primary result channel. Machine mode suppresses it so stdout carries exactly one JSON document. */
|
|
36
|
+
export function out(line = "") {
|
|
37
|
+
if (json)
|
|
38
|
+
return;
|
|
39
|
+
console.log(line);
|
|
40
|
+
}
|
|
41
|
+
/** Dimmed guidance ("next: ...", "run agentsw sync"). Dropped by --quiet and --json. */
|
|
42
|
+
export function note(message) {
|
|
43
|
+
if (json || quiet)
|
|
44
|
+
return;
|
|
45
|
+
process.stderr.write(pc.dim(message) + "\n");
|
|
46
|
+
}
|
|
47
|
+
/** Non-fatal problem. Keeps the literal "warning: " prefix callers and tests rely on. */
|
|
48
|
+
export function warn(message) {
|
|
49
|
+
if (json || quiet)
|
|
50
|
+
return;
|
|
51
|
+
process.stderr.write(pc.yellow(`warning: ${message}`) + "\n");
|
|
52
|
+
}
|
|
53
|
+
function emitJsonError(message) {
|
|
54
|
+
process.stdout.write(JSON.stringify({ ok: false, command, version, error: { message } }) + "\n");
|
|
55
|
+
}
|
|
56
|
+
export function emitJson(data) {
|
|
57
|
+
process.stdout.write(JSON.stringify({ ok: true, command, version, data }) + "\n");
|
|
58
|
+
}
|
|
59
|
+
/** Fatal error. In machine mode the failure becomes the single stdout document, so pipes stay parseable. */
|
|
60
|
+
export function error(message) {
|
|
61
|
+
if (json)
|
|
62
|
+
emitJsonError(message);
|
|
63
|
+
else
|
|
64
|
+
process.stderr.write(pc.red(`error: ${message}`) + "\n");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ctrl-C / Esc at an interactive prompt. Inside the menu this unwinds to the dispatch loop, which keeps the
|
|
69
|
+
* session alive; standalone it exits 130 (the SIGINT convention), so a cancelled run is never mistaken for
|
|
70
|
+
* a successful one. Prompts awaits onCancel inside a catch, so throwing here reliably rejects the prompt.
|
|
71
|
+
*/
|
|
72
|
+
export function cancelInteractive() {
|
|
73
|
+
if (inMenu)
|
|
74
|
+
throw new ActionCancelled();
|
|
75
|
+
process.stderr.write(pc.dim(t("common.cancelled")) + "\n");
|
|
76
|
+
process.exit(130);
|
|
77
|
+
}
|
|
78
|
+
/** 0 means "no known width" (pipe, file, test runner), which disables truncation entirely. */
|
|
79
|
+
export function terminalWidth() {
|
|
80
|
+
const columns = process.stdout.columns;
|
|
81
|
+
return typeof columns === "number" && Number.isFinite(columns) && columns > 0 ? columns : 0;
|
|
82
|
+
}
|
|
83
|
+
const SGR = /\u001b\[[0-9;]*m/g;
|
|
84
|
+
export function visibleWidth(text) {
|
|
85
|
+
return text.replace(SGR, "").length;
|
|
86
|
+
}
|
|
87
|
+
/** Clip to a visible width without ever cutting inside an SGR sequence; closes color that was left open. */
|
|
88
|
+
export function truncateVisible(text, max) {
|
|
89
|
+
if (max <= 0)
|
|
90
|
+
return "";
|
|
91
|
+
if (visibleWidth(text) <= max)
|
|
92
|
+
return text;
|
|
93
|
+
const target = Math.max(0, max - 1);
|
|
94
|
+
let out = "";
|
|
95
|
+
let visible = 0;
|
|
96
|
+
let colored = false;
|
|
97
|
+
for (let i = 0; i < text.length && visible < target;) {
|
|
98
|
+
if (text.charAt(i) === "\u001b") {
|
|
99
|
+
const match = /^\u001b\[[0-9;]*m/.exec(text.slice(i));
|
|
100
|
+
if (match) {
|
|
101
|
+
out += match[0];
|
|
102
|
+
colored = true;
|
|
103
|
+
i += match[0].length;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
out += text.charAt(i);
|
|
108
|
+
visible++;
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
return `${out}…${colored ? "\u001b[0m" : ""}`;
|
|
112
|
+
}
|
|
113
|
+
export function padCell(text, width, align = "left") {
|
|
114
|
+
const padding = " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
115
|
+
return align === "right" ? padding + text : text + padding;
|
|
116
|
+
}
|
|
117
|
+
export function table(rows, header, options = {}) {
|
|
118
|
+
const all = header ? [header, ...rows] : rows;
|
|
119
|
+
if (all.length === 0)
|
|
120
|
+
return "";
|
|
121
|
+
const widths = [];
|
|
122
|
+
for (const row of all) {
|
|
123
|
+
row.forEach((cell, i) => {
|
|
124
|
+
widths[i] = Math.max(widths[i] ?? 0, visibleWidth(cell));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const minWidth = options.minWidth ?? 8;
|
|
128
|
+
const width = options.width ?? terminalWidth();
|
|
129
|
+
const eligible = (options.truncate ?? []).filter((i) => i < widths.length);
|
|
130
|
+
if (width > 0 && eligible.length > 0) {
|
|
131
|
+
const total = () => widths.reduce((sum, w, i) => sum + w + (i > 0 ? 2 : 0), 0);
|
|
132
|
+
while (total() > width) {
|
|
133
|
+
let widest = -1;
|
|
134
|
+
for (const i of eligible) {
|
|
135
|
+
if ((widths[i] ?? 0) <= minWidth)
|
|
136
|
+
continue;
|
|
137
|
+
if (widest === -1 || (widths[i] ?? 0) > (widths[widest] ?? 0))
|
|
138
|
+
widest = i;
|
|
139
|
+
}
|
|
140
|
+
if (widest === -1)
|
|
141
|
+
break;
|
|
142
|
+
widths[widest] = (widths[widest] ?? 0) - 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const render = (row) => row
|
|
146
|
+
.map((cell, i) => {
|
|
147
|
+
const column = widths[i] ?? 0;
|
|
148
|
+
const clipped = visibleWidth(cell) > column ? truncateVisible(cell, column) : cell;
|
|
149
|
+
return padCell(clipped, column, options.align?.[i] ?? "left");
|
|
150
|
+
})
|
|
151
|
+
.join(" ")
|
|
152
|
+
.trimEnd();
|
|
153
|
+
const lines = all.map(render);
|
|
154
|
+
if (header)
|
|
155
|
+
lines.splice(1, 0, widths.map((w) => "-".repeat(w)).join(" "));
|
|
156
|
+
return lines.join("\n");
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentsw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI provider switcher for coding agents: manage OpenAI/Anthropic-protocol providers in one place and sync them, with models.dev metadata, into Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy and DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build",
|
|
38
38
|
"dev": "tsx src/index.ts",
|
|
39
39
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
|
-
"test": "tsx --test test/*.test.ts"
|
|
40
|
+
"test": "tsx --import ./test/setup.ts --test test/*.test.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"commander": "^15.0.0",
|