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/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",
|