agentsw 0.3.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.
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { backupFile, home, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
4
+ import { slugFromBaseUrl } from "../slug.js";
5
+ function configDir() {
6
+ return process.env.WORKBUDDY_CONFIG_DIR ?? process.env.CODEBUDDY_CONFIG_DIR ?? path.join(home, ".workbuddy");
7
+ }
8
+ /**
9
+ * models.json accepts a raw array (desktop writes) or {models, availableModels} (CLI writes).
10
+ * Runtime-narrow both shapes; entries without a string id are dropped (WorkBuddy skips them too).
11
+ */
12
+ function readWorkbuddyModels(file) {
13
+ const raw = readJsonIfExists(file);
14
+ const rows = Array.isArray(raw)
15
+ ? raw
16
+ : raw && typeof raw === "object" && "models" in raw && Array.isArray(raw.models)
17
+ ? raw.models
18
+ : [];
19
+ const models = rows.filter((m) => !!m && typeof m === "object" && "id" in m && typeof m.id === "string");
20
+ const availableModels = !Array.isArray(raw) && raw && typeof raw === "object" && "availableModels" in raw && Array.isArray(raw.availableModels)
21
+ ? raw.availableModels.filter((id) => typeof id === "string")
22
+ : [];
23
+ return { models, availableModels, existed: raw !== undefined };
24
+ }
25
+ /**
26
+ * WorkBuddy (Tencent CodeBuddy family): custom models live in ~/.workbuddy/models.json.
27
+ * Each entry carries its own full OpenAI chat-completions `url` + `apiKey`; entries merge
28
+ * into the model selector ("custom" section). Default model via settings.json "model".
29
+ * Custom entries speak the OpenAI wire only — anthropic-protocol providers are skipped.
30
+ */
31
+ export const workbuddy = {
32
+ id: "workbuddy",
33
+ name: "WorkBuddy",
34
+ protocols: ["openai"],
35
+ configPaths: [path.join(configDir(), "models.json"), path.join(configDir(), "settings.json")],
36
+ detect: () => fs.existsSync(configDir()),
37
+ async apply(provider) {
38
+ const dir = configDir();
39
+ const modelsFile = path.join(dir, "models.json");
40
+ const settingsFile = path.join(dir, "settings.json");
41
+ const notes = [];
42
+ const base = provider.baseUrl.replace(/\/+$/, "");
43
+ const url = /\/v\d+$/.test(base) ? `${base}/chat/completions` : `${base}/v1/chat/completions`;
44
+ // models.json accepts a raw array (desktop) or {models, availableModels} (CLI); write the object shape.
45
+ const { models: existingModels, availableModels: existingAvailable } = readWorkbuddyModels(modelsFile);
46
+ const ourIds = provider.models.map((m) => m.id);
47
+ const kept = existingModels.filter((m) => !ourIds.includes(m.id));
48
+ const ours = provider.models.map((m) => ({
49
+ id: m.id,
50
+ name: m.name ?? m.id,
51
+ vendor: provider.name,
52
+ url,
53
+ apiKey: provider.apiKey,
54
+ ...(m.maxInput ?? m.contextWindow ? { maxInputTokens: m.maxInput ?? m.contextWindow } : {}),
55
+ ...(m.maxOutput ? { maxOutputTokens: m.maxOutput } : {}),
56
+ supportsToolCall: true,
57
+ ...(m.imageInput !== undefined ? { supportsImages: !!m.imageInput } : {}),
58
+ ...(m.reasoning !== undefined ? { supportsReasoning: !!m.reasoning } : {}),
59
+ }));
60
+ const availableModels = [...new Set([...existingAvailable, ...ourIds])];
61
+ const modelsBackup = backupFile(modelsFile);
62
+ if (modelsBackup)
63
+ notes.push(`backup: ${modelsBackup}`);
64
+ writeFileAtomic(modelsFile, JSON.stringify({ models: [...kept, ...ours], availableModels }, null, 2) + "\n");
65
+ const settings = readJsonIfExists(settingsFile) ?? {};
66
+ settings.model = provider.defaultModel;
67
+ const settingsBackup = backupFile(settingsFile);
68
+ if (settingsBackup)
69
+ notes.push(`backup: ${settingsBackup}`);
70
+ writeFileAtomic(settingsFile, JSON.stringify(settings, null, 2) + "\n");
71
+ notes.push("WorkBuddy watches models.json; models appear in the selector's custom section without restart");
72
+ return { app: this.id, changed: [modelsFile, settingsFile], notes };
73
+ },
74
+ async prune(provider) {
75
+ const dir = configDir();
76
+ const modelsFile = path.join(dir, "models.json");
77
+ const settingsFile = path.join(dir, "settings.json");
78
+ const { models, availableModels: available, existed } = readWorkbuddyModels(modelsFile);
79
+ if (!existed)
80
+ return { app: this.id, changed: [], notes: [], skipped: "no models.json" };
81
+ const ourIds = provider.models.map((m) => m.id);
82
+ // ours = matching id AND vendor (id alone could collide with entries from another provider)
83
+ const kept = models.filter((m) => !(ourIds.includes(m.id) && m.vendor === provider.name));
84
+ if (kept.length === models.length) {
85
+ return { app: this.id, changed: [], notes: [], skipped: "no entries from this provider" };
86
+ }
87
+ const keptIds = kept.map((m) => m.id);
88
+ const notes = [];
89
+ const modelsBackup = backupFile(modelsFile);
90
+ if (modelsBackup)
91
+ notes.push(`backup: ${modelsBackup}`);
92
+ writeFileAtomic(modelsFile, JSON.stringify({ models: kept, availableModels: available.filter((id) => keptIds.includes(id)) }, null, 2) + "\n");
93
+ const changed = [modelsFile];
94
+ const settings = readJsonIfExists(settingsFile);
95
+ if (settings && typeof settings.model === "string" && ourIds.includes(settings.model)) {
96
+ delete settings.model;
97
+ const settingsBackup = backupFile(settingsFile);
98
+ if (settingsBackup)
99
+ notes.push(`backup: ${settingsBackup}`);
100
+ writeFileAtomic(settingsFile, JSON.stringify(settings, null, 2) + "\n");
101
+ changed.push(settingsFile);
102
+ notes.push("default model reset (was one of this provider's models)");
103
+ }
104
+ return { app: this.id, changed, notes };
105
+ },
106
+ current() {
107
+ const settings = readJsonIfExists(path.join(configDir(), "settings.json"));
108
+ return settings?.model;
109
+ },
110
+ candidates() {
111
+ // every entry carries its own full chat/completions url + key; group by the underlying base URL
112
+ const { models } = readWorkbuddyModels(path.join(configDir(), "models.json"));
113
+ if (models.length === 0)
114
+ return [];
115
+ const settings = readJsonIfExists(path.join(configDir(), "settings.json"));
116
+ const groups = new Map();
117
+ for (const m of models) {
118
+ const row = m;
119
+ if (typeof row.url !== "string" || !row.url)
120
+ continue;
121
+ const baseUrl = row.url.replace(/\/+$/, "").replace(/\/chat\/completions$/, "");
122
+ if (!/^https?:\/\//.test(baseUrl))
123
+ continue;
124
+ let g = groups.get(baseUrl);
125
+ if (!g) {
126
+ g = { ids: [] };
127
+ groups.set(baseUrl, g);
128
+ }
129
+ if (!g.ids.includes(m.id))
130
+ g.ids.push(m.id);
131
+ if (!g.apiKey && typeof row.apiKey === "string")
132
+ g.apiKey = row.apiKey;
133
+ if (!g.vendor && typeof row.vendor === "string")
134
+ g.vendor = row.vendor;
135
+ }
136
+ const self = this.id;
137
+ return [...groups.entries()].map(([baseUrl, g]) => {
138
+ const id = slugFromBaseUrl(baseUrl);
139
+ const defaultModel = settings?.model && g.ids.includes(settings.model) ? settings.model : undefined;
140
+ return { id, name: g.vendor ?? id, protocol: "openai", baseUrl, apiKey: g.apiKey, models: g.ids, defaultModel, source: self };
141
+ });
142
+ },
143
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "agentsw",
3
+ "version": "0.3.0",
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
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/tchivs/agentsw.git"
9
+ },
10
+ "homepage": "https://github.com/tchivs/agentsw#readme",
11
+ "bugs": "https://github.com/tchivs/agentsw/issues",
12
+ "keywords": [
13
+ "cli",
14
+ "coding-agent",
15
+ "model-provider",
16
+ "provider-switcher",
17
+ "claude-code",
18
+ "codex",
19
+ "opencode",
20
+ "deepseek-harness",
21
+ "models.dev",
22
+ "cc-switch"
23
+ ],
24
+ "license": "MIT",
25
+ "bin": {
26
+ "agentsw": "dist/index.js",
27
+ "asw": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "engines": {
33
+ "node": ">=22.12.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "prepublishOnly": "npm run build",
38
+ "dev": "tsx src/index.ts",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "test": "tsx --test test/*.test.ts"
41
+ },
42
+ "dependencies": {
43
+ "commander": "^15.0.0",
44
+ "picocolors": "^1.1.1",
45
+ "prompts": "^2.4.2",
46
+ "smol-toml": "^1.8.0",
47
+ "yaml": "^2.9.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^26.4.0",
51
+ "@types/prompts": "^2.4.9",
52
+ "tsx": "^4.23.12",
53
+ "typescript": "^7.0.2"
54
+ }
55
+ }