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.
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/README.zh-CN.md +221 -0
- package/dist/apps.js +178 -0
- package/dist/commands.js +661 -0
- package/dist/discover.js +21 -0
- package/dist/filter.js +61 -0
- package/dist/fsutil.js +56 -0
- package/dist/i18n.js +214 -0
- package/dist/import.js +66 -0
- package/dist/index.js +113 -0
- package/dist/menu.js +157 -0
- package/dist/modelsdev.js +110 -0
- package/dist/slug.js +18 -0
- package/dist/sources/ccswitch.js +151 -0
- package/dist/store.js +23 -0
- package/dist/targets/claudecode.js +96 -0
- package/dist/targets/codex.js +136 -0
- package/dist/targets/dsh.js +296 -0
- package/dist/targets/hermes.js +173 -0
- package/dist/targets/index.js +26 -0
- package/dist/targets/omp.js +170 -0
- package/dist/targets/opencode.js +135 -0
- package/dist/targets/pistyle.js +208 -0
- package/dist/targets/types.js +1 -0
- package/dist/targets/wire.js +109 -0
- package/dist/targets/workbuddy.js +143 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
package/dist/menu.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
import { cmdAdd, cmdApps, cmdDiscover, cmdImport, cmdList, cmdRemove, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
|
|
4
|
+
import { getLocale, setLocale, t } from "./i18n.js";
|
|
5
|
+
import { loadStore, saveStore } from "./store.js";
|
|
6
|
+
function bye() {
|
|
7
|
+
console.log(pc.dim(`\n${t("menu.bye")}`));
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
const cancel = { onCancel: () => bye() };
|
|
11
|
+
async function askToggle(message, initial = false) {
|
|
12
|
+
const { v } = await prompts({ type: "toggle", name: "v", message, active: t("menu.yes"), inactive: t("menu.no"), initial }, cancel);
|
|
13
|
+
return v === true;
|
|
14
|
+
}
|
|
15
|
+
async function chooseLanguage() {
|
|
16
|
+
const { language } = await prompts({
|
|
17
|
+
type: "select",
|
|
18
|
+
name: "language",
|
|
19
|
+
message: t("language.prompt"),
|
|
20
|
+
hint: t("menu.selectInstructions"),
|
|
21
|
+
initial: getLocale() === "zh-CN" ? 1 : 0,
|
|
22
|
+
choices: [
|
|
23
|
+
{ title: "English", value: "en" },
|
|
24
|
+
{ title: "简体中文", value: "zh-CN" },
|
|
25
|
+
],
|
|
26
|
+
}, cancel);
|
|
27
|
+
setLocale(language);
|
|
28
|
+
const store = loadStore();
|
|
29
|
+
store.language = language;
|
|
30
|
+
saveStore(store);
|
|
31
|
+
console.log(pc.green(t("language.saved")));
|
|
32
|
+
}
|
|
33
|
+
/** select an existing provider; prints a hint and returns undefined when none configured */
|
|
34
|
+
async function pickProvider(message) {
|
|
35
|
+
const store = loadStore();
|
|
36
|
+
const ids = Object.keys(store.providers);
|
|
37
|
+
if (ids.length === 0) {
|
|
38
|
+
console.log(pc.yellow(t("menu.noProvidersHint")));
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const { id } = await prompts({
|
|
42
|
+
type: "select",
|
|
43
|
+
name: "id",
|
|
44
|
+
message,
|
|
45
|
+
hint: t("menu.selectInstructions"),
|
|
46
|
+
choices: ids.map((pid) => {
|
|
47
|
+
const p = store.providers[pid];
|
|
48
|
+
return {
|
|
49
|
+
title: `${pid} · ${p.protocol} · ${t("menu.defaultModel")} ${p.defaultModel}${store.active === pid ? ` (${t("menu.active")})` : ""}`,
|
|
50
|
+
value: pid,
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
53
|
+
}, cancel);
|
|
54
|
+
if (!id)
|
|
55
|
+
return undefined;
|
|
56
|
+
const p = store.providers[id];
|
|
57
|
+
return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
|
|
58
|
+
}
|
|
59
|
+
export async function cmdMenu() {
|
|
60
|
+
if (!loadStore().language)
|
|
61
|
+
await chooseLanguage();
|
|
62
|
+
console.log(pc.bold("agentsw") + pc.dim(t("menu.title")));
|
|
63
|
+
if (Object.keys(loadStore().providers).length === 0) {
|
|
64
|
+
console.log(pc.yellow(t("menu.noProviders")));
|
|
65
|
+
if (await askToggle(t("menu.firstScan"), true))
|
|
66
|
+
await cmdImport({});
|
|
67
|
+
}
|
|
68
|
+
for (;;) {
|
|
69
|
+
console.log("");
|
|
70
|
+
const { action } = await prompts({
|
|
71
|
+
type: "select",
|
|
72
|
+
name: "action",
|
|
73
|
+
message: t("menu.what"),
|
|
74
|
+
hint: t("menu.selectInstructions"),
|
|
75
|
+
choices: [
|
|
76
|
+
{ title: t("menu.add"), value: "add" },
|
|
77
|
+
{ title: t("menu.import"), value: "import" },
|
|
78
|
+
{ title: t("menu.use"), value: "use" },
|
|
79
|
+
{ title: t("menu.status"), value: "status" },
|
|
80
|
+
{ title: t("menu.list"), value: "list" },
|
|
81
|
+
{ title: t("menu.sync"), value: "sync" },
|
|
82
|
+
{ title: t("menu.discover"), value: "discover" },
|
|
83
|
+
{ title: t("menu.remove"), value: "remove" },
|
|
84
|
+
{ title: t("menu.apps"), value: "apps" },
|
|
85
|
+
{ title: t("menu.language"), value: "language" },
|
|
86
|
+
{ title: t("menu.quit"), value: "quit" },
|
|
87
|
+
],
|
|
88
|
+
}, cancel);
|
|
89
|
+
if (action === undefined || action === "quit")
|
|
90
|
+
return;
|
|
91
|
+
if (action === "add") {
|
|
92
|
+
const { src } = await prompts({
|
|
93
|
+
type: "select",
|
|
94
|
+
name: "src",
|
|
95
|
+
message: t("menu.modelSource"),
|
|
96
|
+
hint: t("menu.selectInstructions"),
|
|
97
|
+
choices: [
|
|
98
|
+
{ title: t("menu.modelDiscover"), value: "discover" },
|
|
99
|
+
{ title: t("menu.modelManual"), value: "manual" },
|
|
100
|
+
],
|
|
101
|
+
}, cancel);
|
|
102
|
+
await cmdAdd({ discover: src === "discover" });
|
|
103
|
+
}
|
|
104
|
+
else if (action === "import") {
|
|
105
|
+
await cmdImport({});
|
|
106
|
+
}
|
|
107
|
+
else if (action === "use") {
|
|
108
|
+
const picked = await pickProvider(t("menu.pickProvider"));
|
|
109
|
+
if (!picked)
|
|
110
|
+
continue;
|
|
111
|
+
const { model } = await prompts({
|
|
112
|
+
type: picked.models.length > 1 ? "select" : null,
|
|
113
|
+
name: "model",
|
|
114
|
+
message: t("menu.defaultModel"),
|
|
115
|
+
hint: t("menu.selectInstructions"),
|
|
116
|
+
choices: [
|
|
117
|
+
{ title: t("menu.keepDefault", { model: picked.defaultModel }), value: "" },
|
|
118
|
+
...picked.models.filter((m) => m !== picked.defaultModel).map((m) => ({ title: m, value: m })),
|
|
119
|
+
],
|
|
120
|
+
}, cancel);
|
|
121
|
+
await cmdUse(picked.id, { model: model || undefined });
|
|
122
|
+
}
|
|
123
|
+
else if (action === "status") {
|
|
124
|
+
cmdStatus();
|
|
125
|
+
}
|
|
126
|
+
else if (action === "list") {
|
|
127
|
+
cmdList();
|
|
128
|
+
}
|
|
129
|
+
else if (action === "sync") {
|
|
130
|
+
await cmdSync({});
|
|
131
|
+
}
|
|
132
|
+
else if (action === "discover") {
|
|
133
|
+
const picked = await pickProvider(t("menu.discoverFor"));
|
|
134
|
+
if (!picked)
|
|
135
|
+
continue;
|
|
136
|
+
const sync = await askToggle(t("menu.pushRefresh"));
|
|
137
|
+
await cmdDiscover(picked.id, { sync });
|
|
138
|
+
}
|
|
139
|
+
else if (action === "remove") {
|
|
140
|
+
const picked = await pickProvider(t("menu.removeProvider"));
|
|
141
|
+
if (!picked)
|
|
142
|
+
continue;
|
|
143
|
+
if (!(await askToggle(t("menu.reallyRemove", { id: pc.bold(picked.id) }))))
|
|
144
|
+
continue;
|
|
145
|
+
const prune = await askToggle(t("menu.pruneConfigs"));
|
|
146
|
+
await cmdRemove(picked.id, { prune });
|
|
147
|
+
}
|
|
148
|
+
else if (action === "apps") {
|
|
149
|
+
await cmdApps();
|
|
150
|
+
if (await askToggle(t("menu.upgrade")))
|
|
151
|
+
await cmdUpgrade([]);
|
|
152
|
+
}
|
|
153
|
+
else if (action === "language") {
|
|
154
|
+
await chooseLanguage();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { configDir } from "./store.js";
|
|
4
|
+
import { readJsonIfExists, writeFileAtomic } from "./fsutil.js";
|
|
5
|
+
const API_URL = "https://models.dev/api.json";
|
|
6
|
+
const CACHE_FILE = path.join(configDir, "models-dev.json");
|
|
7
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
export async function loadCatalog(opts = {}) {
|
|
9
|
+
if (!opts.refresh) {
|
|
10
|
+
try {
|
|
11
|
+
const stat = fs.statSync(CACHE_FILE);
|
|
12
|
+
const fresh = Date.now() - stat.mtimeMs < CACHE_TTL_MS;
|
|
13
|
+
if (fresh || opts.offline)
|
|
14
|
+
return readJsonIfExists(CACHE_FILE);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* no cache */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (opts.offline)
|
|
21
|
+
return readJsonIfExists(CACHE_FILE);
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(API_URL, { signal: AbortSignal.timeout(15000) });
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(`models.dev responded ${res.status}`);
|
|
26
|
+
const text = await res.text();
|
|
27
|
+
JSON.parse(text); // validate before caching
|
|
28
|
+
writeFileAtomic(CACHE_FILE, text);
|
|
29
|
+
return JSON.parse(text);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
// network failure: fall back to stale cache if present
|
|
33
|
+
const cached = readJsonIfExists(CACHE_FILE);
|
|
34
|
+
if (cached)
|
|
35
|
+
return cached;
|
|
36
|
+
process.stderr.write(`warning: could not fetch models.dev catalog: ${err.message}\n`);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function toModelSpec(id, m) {
|
|
41
|
+
const efforts = m.reasoning_options?.find((o) => o.type === "effort")?.values;
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
name: m.name,
|
|
45
|
+
contextWindow: m.limit?.context,
|
|
46
|
+
maxInput: m.limit?.input,
|
|
47
|
+
maxOutput: m.limit?.output,
|
|
48
|
+
reasoning: m.reasoning,
|
|
49
|
+
reasoningEfforts: efforts?.length ? efforts : undefined,
|
|
50
|
+
imageInput: m.attachment || m.modalities?.input?.includes("image") || undefined,
|
|
51
|
+
cost: m.cost
|
|
52
|
+
? { input: m.cost.input, output: m.cost.output, cacheRead: m.cost.cache_read, cacheWrite: m.cost.cache_write }
|
|
53
|
+
: undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Find catalog metadata for a model id.
|
|
58
|
+
* Match order: hinted provider exact → hinted provider basename → any provider exact → any basename.
|
|
59
|
+
* Basename = id segment after the last "/" (gateways list ids like "anthropic/claude-opus-4.7").
|
|
60
|
+
*/
|
|
61
|
+
export function findModelMeta(catalog, modelId, providerHint) {
|
|
62
|
+
const base = modelId.slice(modelId.lastIndexOf("/") + 1).toLowerCase();
|
|
63
|
+
const scan = (p) => {
|
|
64
|
+
if (p.models[modelId])
|
|
65
|
+
return p.models[modelId];
|
|
66
|
+
for (const [id, m] of Object.entries(p.models)) {
|
|
67
|
+
if (id.slice(id.lastIndexOf("/") + 1).toLowerCase() === base)
|
|
68
|
+
return m;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
const hinted = providerHint ? catalog[providerHint] : undefined;
|
|
73
|
+
if (hinted) {
|
|
74
|
+
const m = scan(hinted);
|
|
75
|
+
if (m)
|
|
76
|
+
return { spec: toModelSpec(modelId, m), provider: hinted.id };
|
|
77
|
+
}
|
|
78
|
+
for (const p of Object.values(catalog)) {
|
|
79
|
+
if (p === hinted)
|
|
80
|
+
continue;
|
|
81
|
+
const m = scan(p);
|
|
82
|
+
if (m)
|
|
83
|
+
return { spec: toModelSpec(modelId, m), provider: p.id };
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Enrich a list of model ids with models.dev metadata; unmatched ids pass through bare. */
|
|
88
|
+
export function enrichModels(catalog, ids, providerHint) {
|
|
89
|
+
return ids.map((id) => {
|
|
90
|
+
const hit = catalog ? findModelMeta(catalog, id, providerHint) : undefined;
|
|
91
|
+
return hit?.spec ?? { id };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Search catalog models by substring across provider/model ids and names. */
|
|
95
|
+
export function searchCatalog(catalog, query, limit = 50) {
|
|
96
|
+
const q = query.toLowerCase();
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const p of Object.values(catalog)) {
|
|
99
|
+
for (const [id, m] of Object.entries(p.models)) {
|
|
100
|
+
if (id.toLowerCase().includes(q) ||
|
|
101
|
+
(m.name?.toLowerCase().includes(q) ?? false) ||
|
|
102
|
+
p.id.toLowerCase().includes(q)) {
|
|
103
|
+
out.push({ provider: p.id, spec: toModelSpec(id, m) });
|
|
104
|
+
if (out.length >= limit)
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
package/dist/slug.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Derive a provider-id slug from a base URL host, for apps whose config carries no provider name. */
|
|
2
|
+
export function slugFromBaseUrl(baseUrl) {
|
|
3
|
+
let host = "";
|
|
4
|
+
try {
|
|
5
|
+
host = new URL(baseUrl).hostname;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return "imported";
|
|
9
|
+
}
|
|
10
|
+
const labels = host.split(".").filter((l) => l && !["api", "www"].includes(l.toLowerCase()));
|
|
11
|
+
const first = labels.find((l) => /[a-z]/i.test(l)) ?? labels[0] ?? host;
|
|
12
|
+
const slug = first.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
|
|
13
|
+
return /^[a-z0-9]/.test(slug) ? slug : "imported";
|
|
14
|
+
}
|
|
15
|
+
/** True for bare UPPER_SNAKE names that apps may treat as env-var references (omp, prime). */
|
|
16
|
+
export function looksLikeEnvName(v) {
|
|
17
|
+
return /^[A-Z][A-Z0-9_]*$/.test(v);
|
|
18
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse as parseToml } from "smol-toml";
|
|
5
|
+
import { home } from "../fsutil.js";
|
|
6
|
+
import { slugFromBaseUrl } from "../slug.js";
|
|
7
|
+
import { classifyApi } from "../targets/wire.js";
|
|
8
|
+
/** cc-switch (the Tauri desktop switcher) keeps every managed app's providers in one SQLite file. */
|
|
9
|
+
const storeFile = path.join(home, ".cc-switch", "cc-switch.db");
|
|
10
|
+
/** Source id shown in the `import` preview. */
|
|
11
|
+
export const CC_SWITCH_SOURCE = "cc-switch";
|
|
12
|
+
/** Claude Code / Claude Desktop rows: the env block, same shape agentsw's claude adapter writes. */
|
|
13
|
+
function fromEnvShape(settings) {
|
|
14
|
+
const env = settings.env;
|
|
15
|
+
if (!env?.ANTHROPIC_BASE_URL)
|
|
16
|
+
return undefined;
|
|
17
|
+
const models = [
|
|
18
|
+
typeof settings.model === "string" ? settings.model : undefined,
|
|
19
|
+
env.ANTHROPIC_MODEL,
|
|
20
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
|
21
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
|
22
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
|
23
|
+
].filter((m) => Boolean(m));
|
|
24
|
+
return {
|
|
25
|
+
protocol: "anthropic",
|
|
26
|
+
baseUrl: env.ANTHROPIC_BASE_URL,
|
|
27
|
+
apiKey: env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || undefined,
|
|
28
|
+
models: [...new Set(models)],
|
|
29
|
+
defaultModel: models[0],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Codex rows: `settings_config.config` is literal `config.toml` text, the key rides in `auth`. */
|
|
33
|
+
function fromCodexShape(settings) {
|
|
34
|
+
const text = typeof settings.config === "string" ? settings.config : "";
|
|
35
|
+
if (!text)
|
|
36
|
+
return undefined;
|
|
37
|
+
let config;
|
|
38
|
+
try {
|
|
39
|
+
config = parseToml(text);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const providers = config.model_providers;
|
|
45
|
+
const active = typeof config.model_provider === "string" ? config.model_provider : undefined;
|
|
46
|
+
const entry = (active ? providers?.[active] : undefined) ?? Object.values(providers ?? {})[0];
|
|
47
|
+
if (!entry || typeof entry.base_url !== "string")
|
|
48
|
+
return undefined;
|
|
49
|
+
const model = typeof config.model === "string" ? config.model : undefined;
|
|
50
|
+
const auth = settings.auth;
|
|
51
|
+
return {
|
|
52
|
+
protocol: "openai",
|
|
53
|
+
// codex speaks the Responses API; only a legacy row still says "chat"
|
|
54
|
+
openaiApi: entry.wire_api === "chat" ? "completions" : "responses",
|
|
55
|
+
baseUrl: entry.base_url,
|
|
56
|
+
apiKey: auth?.OPENAI_API_KEY || undefined,
|
|
57
|
+
models: model ? [model] : [],
|
|
58
|
+
defaultModel: model,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** opencode / OpenClaw / Hermes / pi rows carry the pi-family `api` + `baseUrl` + `models` shape. */
|
|
62
|
+
function fromPiShape(settings, meta) {
|
|
63
|
+
if (typeof settings.baseUrl !== "string" || !settings.baseUrl)
|
|
64
|
+
return undefined;
|
|
65
|
+
const wire = classifyApi(settings.api) ?? (meta.apiFormat === "anthropic" ? { protocol: "anthropic" } : undefined);
|
|
66
|
+
if (!wire)
|
|
67
|
+
return undefined;
|
|
68
|
+
const models = Array.isArray(settings.models)
|
|
69
|
+
? settings.models.map((m) => (typeof m?.id === "string" ? m.id : "")).filter(Boolean)
|
|
70
|
+
: [];
|
|
71
|
+
return {
|
|
72
|
+
protocol: wire.protocol,
|
|
73
|
+
openaiApi: wire.openaiApi,
|
|
74
|
+
baseUrl: settings.baseUrl,
|
|
75
|
+
apiKey: typeof settings.apiKey === "string" && settings.apiKey ? settings.apiKey : undefined,
|
|
76
|
+
models,
|
|
77
|
+
defaultModel: typeof settings.model === "string" ? settings.model : models[0],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Load `node:sqlite`, swallowing the one-off "SQLite is an experimental feature"
|
|
82
|
+
* warning it emits asynchronously on load: that this reader uses the built-in
|
|
83
|
+
* driver is an implementation detail, not something an `import` run should shout
|
|
84
|
+
* about. Every other warning is handed straight back to Node's own printer.
|
|
85
|
+
*/
|
|
86
|
+
function requireSqlite() {
|
|
87
|
+
const printers = process.listeners("warning");
|
|
88
|
+
process.removeAllListeners("warning");
|
|
89
|
+
process.on("warning", (warning) => {
|
|
90
|
+
if (warning.name === "ExperimentalWarning" && /sqlite/i.test(warning.message))
|
|
91
|
+
return;
|
|
92
|
+
for (const printer of printers)
|
|
93
|
+
printer(warning);
|
|
94
|
+
});
|
|
95
|
+
return createRequire(import.meta.url)("node:sqlite");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read cc-switch's provider store so a user of the desktop app does not have to
|
|
99
|
+
* re-enter anything. The database is opened read-only and nothing is ever written
|
|
100
|
+
* back; a store this build cannot read (no `node:sqlite`, a newer schema, a locked
|
|
101
|
+
* file) yields no candidates instead of failing the whole scan.
|
|
102
|
+
*/
|
|
103
|
+
export function ccSwitchCandidates() {
|
|
104
|
+
if (!fs.existsSync(storeFile))
|
|
105
|
+
return [];
|
|
106
|
+
let rows;
|
|
107
|
+
try {
|
|
108
|
+
const db = new (requireSqlite().DatabaseSync)(storeFile, { readOnly: true });
|
|
109
|
+
try {
|
|
110
|
+
rows = db.prepare("SELECT app_type, name, settings_config, meta FROM providers").all();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
db.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
let settings;
|
|
122
|
+
let meta;
|
|
123
|
+
try {
|
|
124
|
+
settings = JSON.parse(row.settings_config || "{}");
|
|
125
|
+
meta = JSON.parse(row.meta || "{}");
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Rows are matched by shape rather than by app name, so an app cc-switch adds
|
|
131
|
+
// later still imports as long as it stores one of these three layouts.
|
|
132
|
+
const parsed = fromEnvShape(settings) ?? fromCodexShape(settings) ?? fromPiShape(settings, meta);
|
|
133
|
+
if (!parsed)
|
|
134
|
+
continue;
|
|
135
|
+
const name = row.name || row.app_type;
|
|
136
|
+
// cc-switch ids are UUIDs; the display name is what the user recognizes.
|
|
137
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
|
|
138
|
+
out.push({
|
|
139
|
+
id: /^[a-z0-9]/.test(slug) ? slug : slugFromBaseUrl(parsed.baseUrl),
|
|
140
|
+
name,
|
|
141
|
+
protocol: parsed.protocol,
|
|
142
|
+
...(parsed.openaiApi ? { openaiApi: parsed.openaiApi } : {}),
|
|
143
|
+
baseUrl: parsed.baseUrl,
|
|
144
|
+
apiKey: parsed.apiKey,
|
|
145
|
+
models: parsed.models,
|
|
146
|
+
defaultModel: parsed.defaultModel,
|
|
147
|
+
source: CC_SWITCH_SOURCE,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { home, readJsonIfExists, writeFileAtomic } from "./fsutil.js";
|
|
3
|
+
export const configDir = path.join(home, ".config", "agentsw");
|
|
4
|
+
export const configFile = path.join(configDir, "config.json");
|
|
5
|
+
export function loadStore() {
|
|
6
|
+
const store = readJsonIfExists(configFile);
|
|
7
|
+
if (!store)
|
|
8
|
+
return { version: 1, providers: {} };
|
|
9
|
+
if (!store.providers)
|
|
10
|
+
store.providers = {};
|
|
11
|
+
return store;
|
|
12
|
+
}
|
|
13
|
+
export function saveStore(store) {
|
|
14
|
+
writeFileAtomic(configFile, JSON.stringify(store, null, 2) + "\n", 0o600);
|
|
15
|
+
}
|
|
16
|
+
export function getProvider(store, id) {
|
|
17
|
+
const p = store.providers[id];
|
|
18
|
+
if (!p) {
|
|
19
|
+
const known = Object.keys(store.providers);
|
|
20
|
+
throw new Error(`unknown provider "${id}"${known.length ? ` (known: ${known.join(", ")})` : " (none configured; run: agentsw add)"}`);
|
|
21
|
+
}
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
const dir = path.join(home, ".claude");
|
|
6
|
+
const settingsFile = path.join(dir, "settings.json");
|
|
7
|
+
/**
|
|
8
|
+
* Claude Code reads Anthropic-protocol endpoints via env vars in ~/.claude/settings.json.
|
|
9
|
+
* We merge the "env" block and leave every other setting untouched.
|
|
10
|
+
*/
|
|
11
|
+
export const claudecode = {
|
|
12
|
+
id: "claude",
|
|
13
|
+
name: "Claude Code",
|
|
14
|
+
protocols: ["anthropic"],
|
|
15
|
+
configPaths: [settingsFile],
|
|
16
|
+
detect: () => fs.existsSync(dir),
|
|
17
|
+
async apply(provider) {
|
|
18
|
+
const notes = [];
|
|
19
|
+
const settings = readJsonIfExists(settingsFile) ?? {};
|
|
20
|
+
const env = { ...settings.env };
|
|
21
|
+
env.ANTHROPIC_BASE_URL = provider.baseUrl;
|
|
22
|
+
env.ANTHROPIC_AUTH_TOKEN = provider.apiKey;
|
|
23
|
+
delete env.ANTHROPIC_API_KEY; // AUTH_TOKEN and API_KEY conflict; keep one
|
|
24
|
+
env.ANTHROPIC_MODEL = provider.defaultModel;
|
|
25
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = provider.defaultModel;
|
|
26
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = provider.defaultModel;
|
|
27
|
+
env.ANTHROPIC_REASONING_MODEL = provider.defaultModel;
|
|
28
|
+
const small = provider.smallModel ?? provider.defaultModel;
|
|
29
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = small;
|
|
30
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = small;
|
|
31
|
+
settings.env = env;
|
|
32
|
+
const backup = backupFile(settingsFile);
|
|
33
|
+
if (backup)
|
|
34
|
+
notes.push(`backup: ${backup}`);
|
|
35
|
+
writeFileAtomic(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
36
|
+
return { app: this.id, changed: [settingsFile], notes };
|
|
37
|
+
},
|
|
38
|
+
async prune(provider) {
|
|
39
|
+
const settings = readJsonIfExists(settingsFile);
|
|
40
|
+
const env = settings?.env;
|
|
41
|
+
if (!settings || !env || env.ANTHROPIC_BASE_URL !== provider.baseUrl) {
|
|
42
|
+
return { app: this.id, changed: [], notes: [], skipped: "claude env does not point at this provider" };
|
|
43
|
+
}
|
|
44
|
+
for (const key of [
|
|
45
|
+
"ANTHROPIC_BASE_URL",
|
|
46
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
47
|
+
"ANTHROPIC_MODEL",
|
|
48
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
49
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
50
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
51
|
+
"ANTHROPIC_REASONING_MODEL",
|
|
52
|
+
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
53
|
+
]) {
|
|
54
|
+
delete env[key];
|
|
55
|
+
}
|
|
56
|
+
const notes = [];
|
|
57
|
+
const backup = backupFile(settingsFile);
|
|
58
|
+
if (backup)
|
|
59
|
+
notes.push(`backup: ${backup}`);
|
|
60
|
+
writeFileAtomic(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
61
|
+
return { app: this.id, changed: [settingsFile], notes };
|
|
62
|
+
},
|
|
63
|
+
current() {
|
|
64
|
+
const env = readJsonIfExists(settingsFile)?.env;
|
|
65
|
+
if (!env?.ANTHROPIC_BASE_URL)
|
|
66
|
+
return undefined;
|
|
67
|
+
return `${env.ANTHROPIC_BASE_URL} · ${env.ANTHROPIC_MODEL ?? "?"}`;
|
|
68
|
+
},
|
|
69
|
+
candidates() {
|
|
70
|
+
const env = readJsonIfExists(settingsFile)?.env;
|
|
71
|
+
const baseUrl = env?.ANTHROPIC_BASE_URL;
|
|
72
|
+
if (!baseUrl)
|
|
73
|
+
return [];
|
|
74
|
+
const models = [
|
|
75
|
+
env.ANTHROPIC_MODEL,
|
|
76
|
+
env.ANTHROPIC_DEFAULT_OPUS_MODEL,
|
|
77
|
+
env.ANTHROPIC_DEFAULT_SONNET_MODEL,
|
|
78
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
|
79
|
+
env.ANTHROPIC_REASONING_MODEL,
|
|
80
|
+
env.ANTHROPIC_SMALL_FAST_MODEL,
|
|
81
|
+
].filter((m) => !!m);
|
|
82
|
+
const id = slugFromBaseUrl(baseUrl);
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
id,
|
|
86
|
+
name: id,
|
|
87
|
+
protocol: "anthropic",
|
|
88
|
+
baseUrl,
|
|
89
|
+
apiKey: env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || undefined,
|
|
90
|
+
models: [...new Set(models)],
|
|
91
|
+
defaultModel: env.ANTHROPIC_MODEL,
|
|
92
|
+
source: this.id,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
},
|
|
96
|
+
};
|