mslxdff 0.1.65 → 0.1.66

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.
Files changed (42) hide show
  1. package/bin/mslxdff.js +3 -3159
  2. package/package.json +1 -1
  3. package/src/cli/bootstrap.js +408 -0
  4. package/src/cli/commands/daemon.js +151 -0
  5. package/src/cli/commands/group.js +272 -0
  6. package/src/cli/commands/model.js +405 -0
  7. package/src/cli/commands/provider/add.js +106 -0
  8. package/src/cli/commands/provider/allowlist.js +99 -0
  9. package/src/cli/commands/provider/config.js +82 -0
  10. package/src/cli/commands/provider/index.js +193 -0
  11. package/src/cli/commands/provider/keys.js +135 -0
  12. package/src/cli/commands/provider/models.js +99 -0
  13. package/src/cli/commands/provider.js +1 -0
  14. package/src/cli/commands/sync.js +143 -0
  15. package/src/cli/commands/system.js +236 -0
  16. package/src/cli/commands/workbuddy.js +91 -0
  17. package/src/cli/format.js +119 -0
  18. package/src/cli/group-helpers.js +69 -0
  19. package/src/cli/help.js +66 -0
  20. package/src/cli/index.js +59 -0
  21. package/src/cli/interactive.js +84 -0
  22. package/src/cli/policy.js +119 -0
  23. package/src/cli/status.js +332 -0
  24. package/src/cli/util.js +24 -0
  25. package/src/providers/base.js +159 -0
  26. package/src/providers/generic.js +26 -166
  27. package/src/providers/openrouter.js +32 -162
  28. package/src/providers/workbuddy.js +7 -32
  29. package/src/routes/chat/gateway.js +301 -0
  30. package/src/routes/chat/index.js +1 -384
  31. package/src/state/facade.js +57 -0
  32. package/src/state/memory.js +180 -0
  33. package/src/state/persist.js +42 -0
  34. package/src/state/schemas/allowlist.js +87 -0
  35. package/src/state/schemas/group.js +31 -0
  36. package/src/state/schemas/model.js +53 -0
  37. package/src/state/schemas/peer.js +31 -0
  38. package/src/state/schemas/port.js +10 -0
  39. package/src/state/schemas/provider.js +254 -0
  40. package/src/state/schemas/token.js +43 -0
  41. package/src/state/store.js +202 -0
  42. package/src/state.js +1 -711
@@ -0,0 +1,106 @@
1
+ import { join } from "node:path";
2
+
3
+ export async function handleProviderAdd(id, sub, rest) {
4
+ if (id !== "add") return false;
5
+ const gid = sub;
6
+ const gBase = rest[1];
7
+ const gKey = rest[2];
8
+ if (!gid || !gBase || !gKey) {
9
+ console.error("usage: mslxdff -provider add <id> <baseUrl> <key> [allowedModel...] [--models-path <path>] [--chat-path <path>]");
10
+ console.error(" e.g. mslxdff -provider add myapi https://api.example.com/v1 sk-xxx");
11
+ console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 gpt-3.5");
12
+ console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
13
+ process.exit(1);
14
+ }
15
+ if (gid === "opencode" || gid === "oc" || gid === "openrouter") {
16
+ console.error(`provider "${gid}" is built-in — use: mslxdff -provider ${gid} add <key> or mslxdff -provider ${gid} set-url <url>`);
17
+ process.exit(1);
18
+ }
19
+ const { saveProviderConfig, loadProviderConfig, loadProviderShareKeys } = await import("../../../state.js");
20
+ const { normalizeProviderId } = await import("../../../providers/model-id.js");
21
+ const nid = normalizeProviderId(gid);
22
+ if (!nid) { console.error(`invalid provider id: ${gid}`); process.exit(1); }
23
+ if (!/^https?:\/\/.+/.test(String(gBase).trim())) { console.error(`invalid baseUrl: ${gBase} (must start with http:// or https://)`); process.exit(1); }
24
+ const cur = loadProviderConfig(nid) || { baseUrl: "", keys: [], allowedModels: [], auths: [], modelsPath: "", chatPath: "" };
25
+ let keys, auths, baseUrl;
26
+ baseUrl = String(gBase).trim();
27
+ let parsedModelsPath = null;
28
+ let parsedChatPath = null;
29
+ const extraTokens = [];
30
+ for (let _i = 3; _i < rest.length; _i++) {
31
+ const tok = String(rest[_i] || "");
32
+ if (tok === "--models-path" || tok === "--modelsPath" || tok === "--models_path") { parsedModelsPath = String(rest[_i + 1] || "").trim() || null; _i++; }
33
+ else if (tok.startsWith("--models-path=")) { parsedModelsPath = tok.slice("--models-path=".length).trim() || null; }
34
+ else if (tok === "--chat-path" || tok === "--chatPath" || tok === "--chat_path") { parsedChatPath = String(rest[_i + 1] || "").trim() || null; _i++; }
35
+ else if (tok.startsWith("--chat-path=")) { parsedChatPath = tok.slice("--chat-path=".length).trim() || null; }
36
+ else extraTokens.push(tok);
37
+ }
38
+ if (parsedModelsPath && !String(parsedModelsPath).startsWith("/")) { console.error(`invalid --models-path: ${parsedModelsPath} (must start with /)`); process.exit(1); }
39
+ if (parsedChatPath && !String(parsedChatPath).startsWith("/")) { console.error(`invalid --chat-path: ${parsedChatPath} (must start with /)`); process.exit(1); }
40
+ const extraModels = extraTokens.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
41
+ const allowedModels = extraModels.length ? [...new Set([...(cur.allowedModels || []), ...extraModels])] : (cur.allowedModels || []);
42
+ if (nid === "workbuddy") {
43
+ const token = String(gKey).trim();
44
+ let uid = "";
45
+ try { const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString()); uid = payload.uid || payload.sub || payload.userId || payload.user_id || ""; } catch {}
46
+ if (!uid) uid = "manual-" + token.slice(-8);
47
+ const curKeys = Array.isArray(cur.keys) ? [...cur.keys] : [];
48
+ const curAuths = Array.isArray(cur.auths) ? [...cur.auths] : [];
49
+ let idx2 = curAuths.findIndex(a => a.uid === uid);
50
+ if (idx2 < 0) idx2 = curKeys.findIndex(k => k === token);
51
+ let newKeys, newAuths;
52
+ if (idx2 >= 0) {
53
+ newKeys = [...curKeys]; newKeys[idx2] = token;
54
+ newAuths = [...curAuths]; newAuths[idx2] = { uid, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" };
55
+ while (newAuths.length < newKeys.length) newAuths.push({ uid: `manual-${newKeys[newAuths.length].slice(-8)}`, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" });
56
+ } else {
57
+ newKeys = [...new Set([...curKeys, token].filter(Boolean))];
58
+ newAuths = [...curAuths, { uid, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" }];
59
+ while (newAuths.length < newKeys.length) newAuths.push({ uid: `manual-${newKeys[newAuths.length].slice(-8)}`, domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" });
60
+ while (newKeys.length < newAuths.length) newKeys.push(token);
61
+ }
62
+ keys = newKeys; auths = newAuths;
63
+ const cfgToSave = { baseUrl, keys, auths, allowedModels };
64
+ if (parsedModelsPath) cfgToSave.modelsPath = parsedModelsPath;
65
+ else if (cur.modelsPath) cfgToSave.modelsPath = cur.modelsPath;
66
+ if (parsedChatPath) cfgToSave.chatPath = parsedChatPath;
67
+ else if (cur.chatPath) cfgToSave.chatPath = cur.chatPath;
68
+ saveProviderConfig(nid, cfgToSave);
69
+ try {
70
+ const { writeFileSync, mkdirSync } = await import("node:fs");
71
+ const { join } = await import("node:path");
72
+ const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
73
+ mkdirSync(dir, { recursive: true });
74
+ const fp = join(dir, `workbuddy-${uid}.json`);
75
+ const doc = { account: { uid, enterpriseId: "", nickname: "" }, auth: { accessToken: token, refreshToken: "", expiresAt: Math.floor(Date.now()/1000)+3600, domain: "www.codebuddy.cn" } };
76
+ writeFileSync(fp + ".tmp", JSON.stringify(doc, null, 2), { mode: 0o600 });
77
+ try { const { renameSync, unlinkSync, existsSync } = await import("node:fs"); if (existsSync(fp)) unlinkSync(fp); renameSync(fp + ".tmp", fp); } catch { writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 }); }
78
+ } catch {}
79
+ } else {
80
+ const trimmed = String(gKey).trim();
81
+ const already = (cur.keys || []).some((k) => String(k).trim() === trimmed);
82
+ if (already) {
83
+ console.log(`key already exists for ${nid} (${trimmed.slice(0, 4)}…${trimmed.slice(-4)}), skipped — still ${cur.keys.length} key(s)`);
84
+ console.log(` use: mslxdff -provider ${nid} list to see keys`);
85
+ process.exit(0);
86
+ }
87
+ keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
88
+ auths = undefined;
89
+ const cfgToSave2 = { baseUrl, keys, allowedModels };
90
+ if (parsedModelsPath) cfgToSave2.modelsPath = parsedModelsPath;
91
+ else if (cur.modelsPath) cfgToSave2.modelsPath = cur.modelsPath;
92
+ if (parsedChatPath) cfgToSave2.chatPath = parsedChatPath;
93
+ else if (cur.chatPath) cfgToSave2.chatPath = cur.chatPath;
94
+ saveProviderConfig(nid, cfgToSave2);
95
+ }
96
+ console.log(`added generic provider: ${nid}`);
97
+ console.log(` baseUrl: ${String(gBase).trim().replace(/\/+$/, "")}`);
98
+ console.log(` keys: ${keys.length} (${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")})`);
99
+ if (allowedModels.length) console.log(` allowedModels: ${allowedModels.length} (${allowedModels.join(", ")})`);
100
+ else console.log(` allowedModels: (none — BLOCKED, otherwise unusable) → mslxdff -provider ${nid} allowlist set <model...> OR mslxdff -provider ${nid} allowAny on (allow all)`);
101
+ console.log(` share: ${loadProviderShareKeys(nid) ? "ON" : "off"} (mslxdff -provider ${nid} share on|off)`);
102
+ console.log(` allowAny: OFF (secure, empty allowlist = 403 block before upstream) — enable via: mslxdff -provider ${nid} allowAny on`);
103
+ console.log(` use as: ${nid}/<model-id> — restart daemon to activate`);
104
+ console.log(` NOTE: empty allowlist = 403 before upstream, no cost — must set allowlist to use`);
105
+ process.exit(0);
106
+ }
@@ -0,0 +1,99 @@
1
+ export async function handleProviderAllowlist(id, sub, rest) {
2
+ if (sub === "allowAny" || sub === "allow-any" || sub === "allow_any" || sub === "allowany") {
3
+ const { loadProviderAllowAnyModels, saveProviderAllowAnyModels } = await import("../../../state.js");
4
+ const on = rest[1];
5
+ if (!on) {
6
+ const cur = loadProviderAllowAnyModels(id);
7
+ console.log(`allowAnyModels: ${cur ? "ON (allow all when allowlist empty)" : "OFF (empty allowlist = block all)"}`);
8
+ console.log(` set: mslxdff -provider ${id} allowAny on|off`);
9
+ process.exit(0);
10
+ }
11
+ if (!["on", "off", "1", "0", "true", "false"].includes(String(on).toLowerCase())) {
12
+ console.error(`usage: mslxdff -provider ${id} allowAny on|off`);
13
+ process.exit(1);
14
+ }
15
+ const state = ["on", "1", "true"].includes(String(on).toLowerCase());
16
+ saveProviderAllowAnyModels(id, state);
17
+ console.log(`allowAnyModels: ${state ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = block all)"} — takes effect immediately (hot-reloaded)`);
18
+ process.exit(0);
19
+ }
20
+ if (sub === "allowlist" || sub === "allow" || sub === "allowed" || sub === "whitelist") {
21
+ const { loadProviderAllowedModels, saveProviderAllowedModels, loadProviderConfig, loadProviderAllowAnyModels } = await import("../../../state.js");
22
+ const action = rest[1];
23
+ const rawTargets = rest.slice(2);
24
+ if (!action || action === "list" || action === "status") {
25
+ const list = loadProviderAllowedModels(id);
26
+ const cfg = loadProviderConfig(id);
27
+ const baseUrl = cfg?.baseUrl || "";
28
+ const allowAny = loadProviderAllowAnyModels(id);
29
+ console.log(`provider: ${id}${baseUrl ? ` baseUrl: ${baseUrl}` : ""}`);
30
+ if (!list.length) {
31
+ if (allowAny) {
32
+ console.log(` allowedModels: (none — allow all, because allowAny=ON)`);
33
+ console.log(` to secure: mslxdff -provider ${id} allowAny off or mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
34
+ } else {
35
+ console.log(` allowedModels: (none — BLOCK ALL, provider disabled until allowlist set or allowAny ON)`);
36
+ console.log(` set via: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
37
+ console.log(` or: mslxdff -provider ${id} allowAny on (allow all when allowlist empty)`);
38
+ }
39
+ } else {
40
+ console.log(` allowedModels: ${list.length} model${list.length > 1 ? "s" : ""} (only these can be used)`);
41
+ list.forEach((m, i) => console.log(` [${i + 1}] ${m}`));
42
+ console.log(` manage: mslxdff -provider ${id} allowlist add <model> | remove <model> | set <m1> <m2> ... | clear`);
43
+ console.log(` allowAny: ${allowAny ? "ON" : "OFF"} (empty list behavior) — mslxdff -provider ${id} allowAny on|off`);
44
+ }
45
+ console.log(` NOTE: empty allowlist + allowAny OFF = 403 block (hot-reloaded, no restart needed)`);
46
+ process.exit(0);
47
+ }
48
+ if (action === "clear") {
49
+ saveProviderAllowedModels(id, []);
50
+ const allowAny = loadProviderAllowAnyModels(id);
51
+ console.log(`cleared ${id} allowlist (now ${allowAny ? "allow all (allowAny ON)" : "BLOCK ALL (allowAny OFF)"}) — takes effect immediately (hot-reloaded)`);
52
+ process.exit(0);
53
+ }
54
+ if (action === "set") {
55
+ const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
56
+ const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
57
+ if (!flat.length) {
58
+ console.error(`usage: mslxdff -provider ${id} allowlist set <model1> <model2> ...`);
59
+ process.exit(1);
60
+ }
61
+ const saved = saveProviderAllowedModels(id, flat);
62
+ console.log(`set ${id} allowlist: ${saved.length} model${saved.length > 1 ? "s" : ""} (${saved.join(", ")}) — takes effect immediately (hot-reloaded)`);
63
+ process.exit(0);
64
+ }
65
+ if (action === "add") {
66
+ const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
67
+ const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
68
+ if (!flat.length) {
69
+ console.error(`usage: mslxdff -provider ${id} allowlist add <model> [model2 ...]`);
70
+ process.exit(1);
71
+ }
72
+ const cur = loadProviderAllowedModels(id);
73
+ const next = [...new Set([...cur, ...flat])];
74
+ saveProviderAllowedModels(id, next);
75
+ console.log(`added ${flat.length} model${flat.length > 1 ? "s" : ""} to ${id} allowlist (now ${next.length}: ${next.join(", ")}) — takes effect immediately (hot-reloaded)`);
76
+ process.exit(0);
77
+ }
78
+ if (action === "remove" || action === "rm" || action === "del") {
79
+ const models = rawTargets.filter((x) => x && !String(x).startsWith("-")).map((m) => String(m).trim()).filter(Boolean);
80
+ const flat = models.flatMap((m) => String(m).split(",")).map((m) => m.trim()).filter(Boolean);
81
+ if (!flat.length) {
82
+ console.error(`usage: mslxdff -provider ${id} allowlist remove <model> [model2 ...]`);
83
+ process.exit(1);
84
+ }
85
+ const cur = loadProviderAllowedModels(id);
86
+ const set = new Set(flat);
87
+ const next = cur.filter((m) => !set.has(m));
88
+ saveProviderAllowedModels(id, next);
89
+ const { loadProviderAllowAnyModels: _la } = await import("../../../state.js");
90
+ const _allowAny = _la(id);
91
+ console.log(`removed ${cur.length - next.length} model${cur.length - next.length !== 1 ? "s" : ""} from ${id} allowlist (now ${next.length ? next.join(", ") : (_allowAny ? "(allow all)" : "(BLOCK ALL)")}) — takes effect immediately (hot-reloaded)`);
92
+ process.exit(0);
93
+ }
94
+ console.error(`usage: mslxdff -provider ${id} allowlist [list|set|add|remove|clear] [models...]`);
95
+ console.error(` mslxdff -provider ${id} allowAny on|off (empty allowlist = block or allow all)`);
96
+ process.exit(1);
97
+ }
98
+ return false;
99
+ }
@@ -0,0 +1,82 @@
1
+ export async function handleProviderConfig(id, sub, rest) {
2
+ if (sub === "set-models-path" || sub === "setModelsPath" || sub === "models-path") {
3
+ const p = rest[1];
4
+ if (!p) {
5
+ console.error(`usage: mslxdff -provider ${id} set-models-path <path> (e.g. /v1/models)`);
6
+ process.exit(1);
7
+ }
8
+ if (!String(p).trim().startsWith("/")) {
9
+ console.error(`invalid modelsPath: ${p} (must start with /)`);
10
+ process.exit(1);
11
+ }
12
+ const { loadProviderConfig, saveProviderConfig } = await import("../../../state.js");
13
+ const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
14
+ const { normalizeProviderId } = await import("../../../providers/model-id.js");
15
+ const nid = normalizeProviderId(id);
16
+ saveProviderConfig(nid || id, { baseUrl: cur.baseUrl || "", keys: cur.keys || [], modelsPath: String(p).trim() });
17
+ console.log(`set ${nid || id} modelsPath: ${String(p).trim()} — restart daemon to activate`);
18
+ process.exit(0);
19
+ }
20
+ if (sub === "set-chat-path" || sub === "setChatPath" || sub === "chat-path") {
21
+ const p = rest[1];
22
+ if (!p) {
23
+ console.error(`usage: mslxdff -provider ${id} set-chat-path <path> (e.g. /v1/chat/completions)`);
24
+ process.exit(1);
25
+ }
26
+ if (!String(p).trim().startsWith("/")) {
27
+ console.error(`invalid chatPath: ${p} (must start with /)`);
28
+ process.exit(1);
29
+ }
30
+ const { loadProviderConfig, saveProviderConfig } = await import("../../../state.js");
31
+ const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
32
+ const { normalizeProviderId } = await import("../../../providers/model-id.js");
33
+ const nid = normalizeProviderId(id);
34
+ saveProviderConfig(nid || id, { baseUrl: cur.baseUrl || "", keys: cur.keys || [], chatPath: String(p).trim() });
35
+ console.log(`set ${nid || id} chatPath: ${String(p).trim()} — restart daemon to activate`);
36
+ process.exit(0);
37
+ }
38
+ if (sub === "clear") {
39
+ const { loadProviderConfigs, loadProviderConfig, saveProviderConfig, saveProviderKeys } = await import("../../../state.js");
40
+ const configs = loadProviderConfigs();
41
+ if (configs[id]) {
42
+ saveProviderConfig(id, { baseUrl: "", keys: [] });
43
+ } else {
44
+ saveProviderKeys(id, []);
45
+ }
46
+ console.log(`cleared ${id} API keys (provider disabled on next daemon start)`);
47
+ process.exit(0);
48
+ }
49
+ if (sub === "set-url" || sub === "setUrl" || sub === "url") {
50
+ const url = rest[1];
51
+ if (!url) {
52
+ console.error(`usage: mslxdff -provider ${id} set-url <baseUrl>`);
53
+ process.exit(1);
54
+ }
55
+ if (!/^https?:\/\/.+/.test(String(url).trim())) {
56
+ console.error(`invalid baseUrl: ${url} (must start with http:// or https://)`);
57
+ process.exit(1);
58
+ }
59
+ const { loadProviderConfig, saveProviderConfig } = await import("../../../state.js");
60
+ const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
61
+ saveProviderConfig(id, { baseUrl: String(url).trim(), keys: cur.keys || [] });
62
+ console.log(`set ${id} baseUrl: ${String(url).trim().replace(/\/+$/, "")} — restart daemon to activate`);
63
+ process.exit(0);
64
+ }
65
+ if (sub === "share") {
66
+ const { loadProviderShareKeys, saveProviderShareKeys } = await import("../../../state.js");
67
+ const on = rest[1];
68
+ if (!on) {
69
+ console.log(`share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"}`);
70
+ process.exit(0);
71
+ }
72
+ if (!["on", "off", "1", "0", "true", "false"].includes(String(on).toLowerCase())) {
73
+ console.error("usage: mslxdff -provider openrouter share on|off");
74
+ process.exit(1);
75
+ }
76
+ const state = ["on", "1", "true"].includes(String(on).toLowerCase());
77
+ saveProviderShareKeys(id, state);
78
+ console.log(`share keys to peers: ${state ? "ON" : "off"} — restart daemon to activate`);
79
+ process.exit(0);
80
+ }
81
+ return false;
82
+ }
@@ -0,0 +1,193 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { defaultStateFile } from "../../../state.js";
3
+
4
+ export async function handleProviders(args) {
5
+ if (!(args.includes("-providers") || args.includes("--providers"))) return false;
6
+ const idx = args.findIndex((x) => x === "-providers" || x === "--providers");
7
+ const sub = args[idx + 1];
8
+ if (!sub || sub === "list" || sub === "status") {
9
+ const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl, loadProviderAllowedModels } = await import("../../../state.js");
10
+ const configs = loadProviderConfigs();
11
+ const opencodeEnabled = true;
12
+ const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
13
+ const orKeys = loadProviderKeys("openrouter");
14
+ const orBase = "https://openrouter.ai/api/v1";
15
+ const orShare = loadProviderShareKeys("openrouter");
16
+ const orAllowed = loadProviderAllowedModels("openrouter");
17
+ const genericIds = new Set(Object.keys(configs).filter((id) => id !== "opencode" && id !== "openrouter"));
18
+ try {
19
+ const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
20
+ const pk = raw.providerKeys || {};
21
+ for (const id of Object.keys(pk)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
22
+ const cfgRaw = raw.providerConfigs || {};
23
+ for (const id of Object.keys(cfgRaw)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
24
+ } catch {}
25
+ for (const k of Object.keys(process.env)) {
26
+ const m = k.match(/^MSLXDFF_(.+)_KEY$/);
27
+ if (m) {
28
+ const id = m[1].toLowerCase().replace(/__/g, "-");
29
+ if (id !== "openrouter" && id !== "opencode") genericIds.add(id);
30
+ }
31
+ }
32
+ try {
33
+ const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
34
+ const cfgs = raw.providerConfigs || {};
35
+ for (const id of Object.keys(cfgs)) {
36
+ if (id === "opencode" || id === "openrouter") continue;
37
+ const am = cfgs[id]?.allowedModels;
38
+ if (Array.isArray(am) && am.length) genericIds.add(id);
39
+ }
40
+ } catch {}
41
+ const list = [];
42
+ const { loadProviderAllowAnyModels: _la0 } = await import("../../../state.js");
43
+ const opAllowAny = _la0("opencode");
44
+ const orAllowAny = _la0("openrouter");
45
+ const opAllowed = loadProviderAllowedModels("opencode");
46
+ list.push({ id: "opencode", enabled: opencodeEnabled, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, allowAny: opAllowAny, note: "built-in, no key, cannot share" });
47
+ list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, allowAny: orAllowAny, note: orKeys.length ? "" : "no keys" });
48
+ for (const gid of [...genericIds].sort()) {
49
+ const cfg = configs[gid];
50
+ const keys = loadProviderKeys(gid);
51
+ const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
52
+ const share = loadProviderShareKeys(gid);
53
+ const allowed = loadProviderAllowedModels(gid);
54
+ const allowAny = _la0(gid);
55
+ const enabled = Boolean(baseUrl && keys.length);
56
+ let note = "";
57
+ if (!baseUrl && !keys.length && !allowed.length && allowAny === false) note = "no baseUrl, no keys, BLOCKED (allowAny OFF)";
58
+ else if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
59
+ else if (!baseUrl && !allowed.length && !allowAny) note = "missing baseUrl, BLOCKED";
60
+ else if (!keys.length && !allowed.length && !allowAny) note = "no keys, BLOCKED";
61
+ else if (!baseUrl) note = "missing baseUrl";
62
+ else if (!keys.length) note = "no keys";
63
+ list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, allowed, allowAny, note });
64
+ }
65
+ console.log(`providers (${list.length}):`);
66
+ for (const p of list) {
67
+ const state = p.enabled ? "enabled " : "disabled";
68
+ const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
69
+ const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
70
+ const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 3).join(",")}${p.allowed.length > 3 ? "..." : ""})` : (p.allowAny ? "allow=all" : "allow=none(BLOCKED)");
71
+ const note = p.note ? ` (${p.note})` : "";
72
+ console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} ${allowInfo.padEnd(22)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
73
+ }
74
+ console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider <id> allowlist set <model...> to restrict`);
75
+ console.log(` mslxdff -provider <id> allowAny on|off (empty allowlist = block or allow all)`);
76
+ process.exit(0);
77
+ }
78
+ console.error("usage: mslxdff -providers list");
79
+ process.exit(1);
80
+ }
81
+
82
+ export async function handleProvider(args) {
83
+ if (!(args.includes("-provider") || args.includes("--provider"))) return false;
84
+ const idx = args.findIndex((x) => x === "-provider" || x === "--provider");
85
+ const id = args[idx + 1];
86
+ const sub = args[idx + 2];
87
+ const rest = args.slice(idx + 2);
88
+ if (!id) {
89
+ console.error("usage: mslxdff -provider <id> [key...|add|remove|list|models|clear|share|set-url|allowlist|allowAny]");
90
+ console.error(" e.g. mslxdff -provider openrouter sk-1 sk-2 sk-3 set multiple keys (replaces all)");
91
+ console.error(" mslxdff -provider openrouter add sk-4 append one key");
92
+ console.error(" mslxdff -provider openrouter remove sk-1 remove a key by value");
93
+ console.error(" mslxdff -provider openrouter list list all keys (masked)");
94
+ console.error(" mslxdff -provider openrouter models [--json] list provider models (allowlist filtered)");
95
+ console.error(" mslxdff -provider openrouter share on|off share keys with peers on outgoing forward (ADR-0008)");
96
+ console.error(" mslxdff -provider openrouter set-url https://api.example.com/v1");
97
+ console.error(" mslxdff -provider openrouter set-models-path /v1/models");
98
+ console.error(" mslxdff -provider openrouter set-chat-path /v1/chat/completions");
99
+ console.error(" mslxdff -provider openrouter allowlist set gpt-4 gpt-3.5 manage allowed models (empty=block unless allowAny ON)");
100
+ console.error(" mslxdff -provider openrouter allowAny on|off empty allowlist = allow all or block all (default OFF, secure)");
101
+ console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx add generic OpenAI-compatible provider");
102
+ console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
103
+ console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
104
+ console.error(" mslxdff -provider openrouter interactive hidden input (append)");
105
+ console.error(" mslxdff -provider openrouter clear remove all keys");
106
+ process.exit(1);
107
+ }
108
+ if (id === "list" || id === "status") {
109
+ const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl } = await import("../../../state.js");
110
+ const configs = loadProviderConfigs();
111
+ const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
112
+ const orKeys = loadProviderKeys("openrouter");
113
+ const orBase = "https://openrouter.ai/api/v1";
114
+ const orShare = loadProviderShareKeys("openrouter");
115
+ const genericIds = new Set(Object.keys(configs).filter((x) => x !== "opencode" && x !== "openrouter"));
116
+ try {
117
+ const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
118
+ const pk = raw.providerKeys || {};
119
+ for (const k of Object.keys(pk)) if (k !== "opencode" && k !== "openrouter") genericIds.add(k);
120
+ const cfgRaw = raw.providerConfigs || {};
121
+ for (const k of Object.keys(cfgRaw)) if (k !== "opencode" && k !== "openrouter") genericIds.add(k);
122
+ } catch {}
123
+ for (const k of Object.keys(process.env)) {
124
+ const m = k.match(/^MSLXDFF_(.+)_KEY$/);
125
+ if (m) {
126
+ const gid = m[1].toLowerCase().replace(/__/g, "-");
127
+ if (gid !== "openrouter" && gid !== "opencode") genericIds.add(gid);
128
+ }
129
+ }
130
+ try {
131
+ const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
132
+ const cfgs = raw.providerConfigs || {};
133
+ for (const gid of Object.keys(cfgs)) {
134
+ if (gid === "opencode" || gid === "openrouter") continue;
135
+ const am = cfgs[gid]?.allowedModels;
136
+ if (Array.isArray(am) && am.length) genericIds.add(gid);
137
+ }
138
+ } catch {}
139
+ const list = [];
140
+ const { loadProviderAllowAnyModels: _la0 } = await import("../../../state.js");
141
+ const { loadProviderAllowedModels } = await import("../../../state.js");
142
+ const opAllowed = loadProviderAllowedModels("opencode");
143
+ const opAllowAny = _la0("opencode");
144
+ const orAllowed = loadProviderAllowedModels("openrouter");
145
+ const orAllowAny = _la0("openrouter");
146
+ list.push({ id: "opencode", enabled: true, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, allowAny: opAllowAny, note: "built-in, no key, cannot share" });
147
+ list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, allowAny: orAllowAny, note: orKeys.length ? "" : "no keys" });
148
+ for (const gid of [...genericIds].sort()) {
149
+ const cfg = configs[gid];
150
+ const keys = loadProviderKeys(gid);
151
+ const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
152
+ const share = loadProviderShareKeys(gid);
153
+ const allowed = loadProviderAllowedModels(gid);
154
+ const allowAny = _la0(gid);
155
+ const enabled = Boolean(baseUrl && keys.length);
156
+ let note = "";
157
+ if (!baseUrl && !keys.length) note = "no baseUrl, no keys";
158
+ else if (!baseUrl) note = "missing baseUrl";
159
+ else if (!keys.length) note = "no keys";
160
+ list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, note });
161
+ }
162
+ console.log(`providers (${list.length}):`);
163
+ for (const p of list) {
164
+ const state = p.enabled ? "enabled " : "disabled";
165
+ const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
166
+ const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
167
+ const note = p.note ? ` (${p.note})` : "";
168
+ console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
169
+ }
170
+ console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider add <id> <baseUrl> <key> to add generic`);
171
+ process.exit(0);
172
+ }
173
+ if (id === "add") {
174
+ const { handleProviderAdd } = await import("./add.js");
175
+ await handleProviderAdd(id, sub, rest);
176
+ return true;
177
+ }
178
+ if ((id === "opencode" || id === "oc") && sub !== "allowlist" && sub !== "allow" && sub !== "allowed" && sub !== "whitelist" && sub !== "list" && sub !== "status" && sub !== "allowAny" && sub !== "allow-any" && sub !== "allow_any" && sub !== "allowany") {
179
+ console.log("opencode is the default (bare) provider — it needs no API key and can never be shared with peers");
180
+ console.log("(its IP-based rate limit is spread by peer forwarding itself)");
181
+ console.log(` allowlist: mslxdff -provider opencode allowlist [list|set|add|remove|clear] — restrict models (empty=allow all)`);
182
+ process.exit(0);
183
+ }
184
+ const { handleProviderConfig } = await import("./config.js");
185
+ if (await handleProviderConfig(id, sub, rest)) return true;
186
+ const { handleProviderAllowlist } = await import("./allowlist.js");
187
+ if (await handleProviderAllowlist(id, sub, rest)) return true;
188
+ const { handleProviderModels } = await import("./models.js");
189
+ if (await handleProviderModels(id, sub, args, rest)) return true;
190
+ const { handleProviderKeys } = await import("./keys.js");
191
+ await handleProviderKeys(id, sub, rest, args);
192
+ return true;
193
+ }
@@ -0,0 +1,135 @@
1
+ export async function handleProviderKeys(id, sub, rest, args) {
2
+ if (sub === "list" || sub === "status") {
3
+ const { loadProviderKeys, loadProviderConfig, loadProviderShareKeys, loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("../../../state.js");
4
+ const keys = loadProviderKeys(id);
5
+ const cfg = loadProviderConfig(id);
6
+ const allowed = loadProviderAllowedModels(id);
7
+ const allowAny = loadProviderAllowAnyModels(id);
8
+ const baseUrl = cfg?.baseUrl || "";
9
+ if (baseUrl) console.log(`provider: ${id} baseUrl: ${baseUrl}`);
10
+ else console.log(`provider: ${id}${id === "openrouter" ? " (built-in baseUrl: https://openrouter.ai/api/v1)" : ""}`);
11
+ if (keys.length) {
12
+ console.log(` keys: ${keys.length} key${keys.length > 1 ? "s" : ""}`);
13
+ keys.forEach((k, i) => console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`));
14
+ console.log(` remove by: mslxdff -provider ${id} remove <seq> [seq...] | <key-value>`);
15
+ } else {
16
+ console.log(` keys: (no keys configured)`);
17
+ }
18
+ console.log(` share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"} (mslxdff -provider ${id} share on|off)`);
19
+ console.log(` allowAnyModels: ${allowAny ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = BLOCK ALL)"} (mslxdff -provider ${id} allowAny on|off)`);
20
+ if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
21
+ else console.log(` allowedModels: (none — ${allowAny ? "allow all" : "BLOCK ALL"}) (mslxdff -provider ${id} allowlist set <model...> or allowAny on)`);
22
+ if (baseUrl) console.log(` set url: mslxdff -provider ${id} set-url <baseUrl>`);
23
+ console.log(` NOTE: opencode is the default provider and can never be shared`);
24
+ process.exit(0);
25
+ }
26
+ if (sub === "add") {
27
+ const key = rest[1];
28
+ if (!key) {
29
+ console.error("usage: mslxdff -provider openrouter add <key>");
30
+ process.exit(1);
31
+ }
32
+ const trimmed = String(key).trim();
33
+ const { loadProviderKeys, loadProviderConfig, saveProviderConfig, addProviderKey, loadProviderConfigs } = await import("../../../state.js");
34
+ const curKeys = loadProviderKeys(id);
35
+ if (curKeys.some((k) => String(k).trim() === trimmed)) {
36
+ console.log(`key already exists for ${id} (${trimmed.slice(0, 4)}…${trimmed.slice(-4)}), skipped — still ${curKeys.length} key(s)`);
37
+ console.log(` use: mslxdff -provider ${id} list to see keys`);
38
+ process.exit(0);
39
+ }
40
+ const configs = loadProviderConfigs();
41
+ if (configs[id]) {
42
+ const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
43
+ const keys = [...new Set([...(cur.keys || []), trimmed].filter(Boolean))];
44
+ saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
45
+ console.log(`added ${id} API key (now ${keys.length} total) — restart daemon to activate`);
46
+ } else {
47
+ const added = addProviderKey(id, key);
48
+ console.log(`added ${id} API key (now ${added.length} total) — restart daemon to activate`);
49
+ }
50
+ process.exit(0);
51
+ }
52
+ if (sub === "remove") {
53
+ const { loadProviderKeys, loadProviderConfig, saveProviderConfig, removeProviderKeys, loadProviderConfigs } = await import("../../../state.js");
54
+ const targets = rest.slice(1).filter((k) => !k.startsWith("-"));
55
+ if (!targets.length) {
56
+ console.error("usage: mslxdff -provider openrouter remove <seq|key> [seq|key ...] (seq = index shown by 'list')");
57
+ process.exit(1);
58
+ }
59
+ const current = loadProviderKeys(id);
60
+ const toRemove = [];
61
+ for (const raw of targets.flatMap((t) => String(t).split(","))) {
62
+ const t = raw.trim();
63
+ if (!t) continue;
64
+ if (/^\d+$/.test(t)) {
65
+ const seq = Number(t);
66
+ const idx2 = seq - 1;
67
+ if (Number.isInteger(seq) && idx2 >= 0 && idx2 < current.length) toRemove.push(current[idx2]);
68
+ else console.log(` ! no key at sequence ${seq} (provider has ${current.length}) — skipped`);
69
+ } else {
70
+ toRemove.push(t);
71
+ }
72
+ }
73
+ if (!toRemove.length) {
74
+ console.log("nothing to remove");
75
+ process.exit(0);
76
+ }
77
+ const configs = loadProviderConfigs();
78
+ let remaining;
79
+ if (configs[id]) {
80
+ const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
81
+ const set = new Set([...new Set(toRemove)].map((k) => String(k).trim()));
82
+ const keys = (cur.keys || []).filter((k) => !set.has(k));
83
+ saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys });
84
+ remaining = keys;
85
+ } else {
86
+ remaining = removeProviderKeys(id, [...new Set(toRemove)]);
87
+ }
88
+ console.log(`removed ${current.length - remaining.length} ${id} API key(s) (now ${remaining.length} total) — restart daemon to activate`);
89
+ process.exit(0);
90
+ }
91
+ if (sub && !sub.startsWith("-")) {
92
+ const { loadProviderKeys, loadProviderConfig, saveProviderConfig, saveProviderKeys, loadProviderConfigs } = await import("../../../state.js");
93
+ const keys = rest.filter((k) => !k.startsWith("-"));
94
+ if (!keys.length) {
95
+ console.error("no key given");
96
+ process.exit(1);
97
+ }
98
+ const configs = loadProviderConfigs();
99
+ if (configs[id]) {
100
+ const cur = loadProviderConfig(id) || { baseUrl: "", keys: [] };
101
+ const clean = [...new Set(keys.map((k) => String(k).trim()).filter(Boolean))];
102
+ saveProviderConfig(id, { baseUrl: cur.baseUrl || "", keys: clean });
103
+ console.log(`set ${id} API keys (${clean.length}: ${clean.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}) — restart daemon to activate`);
104
+ } else {
105
+ saveProviderKeys(id, keys);
106
+ console.log(`set ${id} API keys (${keys.length}: ${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}) — restart daemon to activate`);
107
+ }
108
+ process.exit(0);
109
+ }
110
+ if (process.stdin.isTTY && process.stdout.isTTY) {
111
+ const { loadProviderKeys, addProviderKey } = await import("../../../state.js");
112
+ const readline = await import("node:readline/promises");
113
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
114
+ console.log(`Enter ${id} API keys, one per line (input hidden). Blank line to finish:`);
115
+ const existing = loadProviderKeys(id);
116
+ const collected = [];
117
+ for (;;) {
118
+ const key = await rl.question(existing.length || collected.length ? "" : "");
119
+ const clean = String(key || "").trim();
120
+ if (!clean) break;
121
+ collected.push(clean);
122
+ }
123
+ rl.close();
124
+ if (!collected.length) {
125
+ console.log("empty input — nothing changed");
126
+ process.exit(0);
127
+ }
128
+ for (const k of collected) addProviderKey(id, k);
129
+ const total = (await import("../../../state.js")).loadProviderKeys(id).length;
130
+ console.log(`added ${collected.length} ${id} API key(s) (now ${total} total) — restart daemon to activate`);
131
+ process.exit(0);
132
+ }
133
+ console.error("provide keys inline (non-TTY): mslxdff -provider openrouter <key1> [key2 ...]");
134
+ process.exit(1);
135
+ }