mslxdff 0.1.64 → 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 (47) hide show
  1. package/bin/mslxdff.js +3 -3122
  2. package/package.json +1 -1
  3. package/src/chat/prompt.js +7 -2
  4. package/src/chat/repl.js +8 -4
  5. package/src/chat/upstream.js +271 -36
  6. package/src/cli/bootstrap.js +408 -0
  7. package/src/cli/commands/daemon.js +151 -0
  8. package/src/cli/commands/group.js +272 -0
  9. package/src/cli/commands/model.js +405 -0
  10. package/src/cli/commands/provider/add.js +106 -0
  11. package/src/cli/commands/provider/allowlist.js +99 -0
  12. package/src/cli/commands/provider/config.js +82 -0
  13. package/src/cli/commands/provider/index.js +193 -0
  14. package/src/cli/commands/provider/keys.js +135 -0
  15. package/src/cli/commands/provider/models.js +99 -0
  16. package/src/cli/commands/provider.js +1 -0
  17. package/src/cli/commands/sync.js +143 -0
  18. package/src/cli/commands/system.js +236 -0
  19. package/src/cli/commands/workbuddy.js +91 -0
  20. package/src/cli/format.js +119 -0
  21. package/src/cli/group-helpers.js +69 -0
  22. package/src/cli/help.js +66 -0
  23. package/src/cli/index.js +59 -0
  24. package/src/cli/interactive.js +84 -0
  25. package/src/cli/policy.js +119 -0
  26. package/src/cli/status.js +332 -0
  27. package/src/cli/util.js +24 -0
  28. package/src/models.js +15 -5
  29. package/src/providers/base.js +159 -0
  30. package/src/providers/generic.js +26 -166
  31. package/src/providers/keyring.js +19 -1
  32. package/src/providers/openrouter.js +32 -162
  33. package/src/providers/workbuddy.js +200 -131
  34. package/src/routes/chat/gateway.js +301 -0
  35. package/src/routes/chat/index.js +1 -239
  36. package/src/state/facade.js +57 -0
  37. package/src/state/memory.js +180 -0
  38. package/src/state/persist.js +42 -0
  39. package/src/state/schemas/allowlist.js +87 -0
  40. package/src/state/schemas/group.js +31 -0
  41. package/src/state/schemas/model.js +53 -0
  42. package/src/state/schemas/peer.js +31 -0
  43. package/src/state/schemas/port.js +10 -0
  44. package/src/state/schemas/provider.js +254 -0
  45. package/src/state/schemas/token.js +43 -0
  46. package/src/state/store.js +202 -0
  47. package/src/state.js +1 -711
@@ -0,0 +1,180 @@
1
+ /**
2
+ * memory Store adapter — 零 IO 的 Store 第二实现,证明 Store seam 真实。
3
+ * 与 fileStore(src/state/store.js)共享同一 merge 语义与冷/热分层,
4
+ * 仅将 persist 层替换为 Map<file, {json, mtimeMs}>,不触 `node:fs`。
5
+ * 用于 `—test-concurrency>1` 并行与单测快速路径。
6
+ */
7
+ const memCache = new Map(); // file -> { data, dirty, timer, mtimeMs }
8
+ const memDisk = new Map(); // file -> { obj, mtimeMs }
9
+
10
+ function getEntry(file) {
11
+ let e = memCache.get(file);
12
+ if (!e) {
13
+ e = { data: null, dirty: false, timer: null, mtimeMs: 0 };
14
+ memCache.set(file, e);
15
+ }
16
+ return e;
17
+ }
18
+
19
+ function loadFromDiskMem(file) {
20
+ const v = memDisk.get(file);
21
+ return v ? structuredClone(v.obj) : {};
22
+ }
23
+ function getMtimeMem(file) {
24
+ const v = memDisk.get(file);
25
+ return v ? v.mtimeMs : 0;
26
+ }
27
+ function atomicWriteSyncMem(file, data) {
28
+ const now = Date.now();
29
+ // ensure monotonic mtime
30
+ const prev = memDisk.get(file)?.mtimeMs || 0;
31
+ const mtime = now <= prev ? prev + 1 : now;
32
+ memDisk.set(file, { obj: structuredClone(data), mtimeMs: mtime });
33
+ return mtime;
34
+ }
35
+
36
+ const COLD_WINS = new Set([
37
+ "providerConfigs",
38
+ "providerKeys",
39
+ "providerShareKeys",
40
+ "port",
41
+ "token",
42
+ "preferredModel",
43
+ "modelPicks",
44
+ "peers",
45
+ "groups",
46
+ "groupsJoined",
47
+ "bans",
48
+ "createdAt",
49
+ ]);
50
+
51
+ function mergeState(disk, mem) {
52
+ const merged = { ...disk, ...mem };
53
+ for (const k of COLD_WINS) if (disk[k] !== undefined) merged[k] = disk[k];
54
+ return merged;
55
+ }
56
+
57
+ function readState(file) {
58
+ const e = getEntry(file);
59
+ if (e.data !== null) {
60
+ const mtime = getMtimeMem(file);
61
+ if (mtime === e.mtimeMs) return e.data;
62
+ if (e.dirty) {
63
+ const disk = loadFromDiskMem(file);
64
+ const merged = mergeState(disk || {}, e.data);
65
+ e.data = merged;
66
+ e.mtimeMs = mtime;
67
+ return e.data;
68
+ }
69
+ }
70
+ const disk = loadFromDiskMem(file);
71
+ e.data = disk && typeof disk === "object" ? structuredClone(disk) : {};
72
+ e.mtimeMs = getMtimeMem(file) || Date.now();
73
+ e.dirty = false;
74
+ return e.data;
75
+ }
76
+
77
+ function writeStateImmediate(file, patch) {
78
+ const e = getEntry(file);
79
+ const base = e.data !== null ? e.data : readState(file);
80
+ const merged = { ...base, ...patch };
81
+ e.data = merged;
82
+ e.dirty = false;
83
+ if (e.timer) { clearTimeout(e.timer); e.timer = null; }
84
+ e.mtimeMs = atomicWriteSyncMem(file, merged);
85
+ return merged;
86
+ }
87
+
88
+ function writeStateDeferred(file, patch) {
89
+ const e = getEntry(file);
90
+ const base = e.data !== null ? e.data : readState(file);
91
+ const merged = { ...base, ...patch };
92
+ e.data = merged;
93
+ e.dirty = true;
94
+ // 内存版无需定时刷盘,延迟写即 dirty,下次 flush 或 read 时合并
95
+ return merged;
96
+ }
97
+
98
+ export function flushStateSync(file) {
99
+ const e = memCache.get(file);
100
+ if (!e || !e.dirty || !e.data) return;
101
+ e.mtimeMs = atomicWriteSyncMem(file || "mem://default", e.data);
102
+ e.dirty = false;
103
+ if (e.timer) { clearTimeout(e.timer); e.timer = null; }
104
+ }
105
+
106
+ export function clearStateCache(file) {
107
+ if (file) memCache.delete(file);
108
+ else memCache.clear();
109
+ }
110
+
111
+ // ---- domain 工厂(复用 schemas 的 normalize 逻辑,但闭包于 memory store) ----
112
+ // 为保持零依赖,memory 侧直接内联必要的 normalize(如 normalizeAllowedModel)
113
+ // 此处仅暴露与 fileStore 对等的最小子集用于测试 seam;完整 facade 可按需扩展
114
+
115
+ const WORKBUDDY_DEFAULT_BASE_URL = "https://copilot.tencent.com";
116
+ function normalizeAllowedModel(model, providerId) {
117
+ let s = String(model || "").trim();
118
+ if (!s) return "";
119
+ const idx = s.indexOf("/");
120
+ if (idx > 0 && s.slice(0, idx).toLowerCase() === String(providerId || "").toLowerCase()) s = s.slice(idx + 1);
121
+ return s;
122
+ }
123
+ function defaultStateFileMem(file) { return file || "mem://default"; }
124
+
125
+ export function createMemoryState(file = "mem://default") {
126
+ const f = defaultStateFileMem(file);
127
+ return {
128
+ loadModelErrors: () => {
129
+ const v = readState(f).modelErrors;
130
+ return v && typeof v === "object" && !Array.isArray(v) ? structuredClone(v) : {};
131
+ },
132
+ saveModelErrors: (errors) => { writeStateDeferred(f, { modelErrors: structuredClone(errors) }); return errors; },
133
+ loadModelLatencies: () => {
134
+ const v = readState(f).modelLatencies;
135
+ return v && typeof v === "object" && !Array.isArray(v) ? structuredClone(v) : {};
136
+ },
137
+ saveModelLatencies: (v) => { writeStateDeferred(f, { modelLatencies: structuredClone(v) }); return v; },
138
+ loadProviderKeys: (id) => {
139
+ const keys = readState(f).providerKeys;
140
+ const v = keys && typeof keys === "object" ? keys[id] : undefined;
141
+ if (typeof v === "string") return v.trim() ? [v.trim()] : [];
142
+ if (Array.isArray(v)) return [...new Set(v.filter((x) => typeof x === "string" && x.trim().length))];
143
+ return [];
144
+ },
145
+ saveProviderKeys: (id, list) => {
146
+ const keys = { ...(readState(f).providerKeys || {}) };
147
+ const clean = [...new Set((Array.isArray(list) ? list : []).map((k) => String(k || "").trim()).filter(Boolean))];
148
+ if (clean.length) keys[id] = clean; else delete keys[id];
149
+ writeStateImmediate(f, { providerKeys: keys });
150
+ return clean;
151
+ },
152
+ loadProviderAllowedModels: (id) => {
153
+ const cfg = readState(f).providerConfigs?.[id];
154
+ if (cfg && Array.isArray(cfg.allowedModels)) return [...cfg.allowedModels].map((m) => normalizeAllowedModel(m, id)).filter(Boolean);
155
+ return [];
156
+ },
157
+ saveProviderAllowedModels: (id, list) => {
158
+ const clean = [...new Set((Array.isArray(list) ? list : []).map((m) => normalizeAllowedModel(m, id)).filter(Boolean))];
159
+ const configs = { ...(readState(f).providerConfigs || {}) };
160
+ const cur = configs[id] || {};
161
+ configs[id] = { ...cur, allowedModels: clean };
162
+ if (!clean.length) delete configs[id].allowedModels;
163
+ if (!configs[id].baseUrl && !(configs[id].keys||[]).length && !(configs[id].allowedModels||[]).length) delete configs[id];
164
+ writeStateImmediate(f, { providerConfigs: configs });
165
+ return clean;
166
+ },
167
+ loadPeers: () => {
168
+ const v = readState(f).peers;
169
+ return Array.isArray(v) ? structuredClone(v) : [];
170
+ },
171
+ savePeers: (peers) => { writeStateImmediate(f, { peers: structuredClone(peers) }); return peers; },
172
+ flushSync: () => flushStateSync(f),
173
+ clear: () => clearStateCache(f),
174
+ _memDisk: memDisk,
175
+ _memCache: memCache,
176
+ };
177
+ }
178
+
179
+ // 单例 memory facade(便于直接 import 使用)
180
+ export const memoryState = createMemoryState("mem://default");
@@ -0,0 +1,42 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, statSync, renameSync, unlinkSync } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+
5
+ export function loadFromDisk(file) {
6
+ try {
7
+ return JSON.parse(readFileSync(file, "utf8"));
8
+ } catch {
9
+ return {};
10
+ }
11
+ }
12
+
13
+ export function ensureDir(file) {
14
+ mkdirSync(dirname(file), { recursive: true });
15
+ }
16
+
17
+ export function getMtimeMs(file) {
18
+ try {
19
+ return statSync(file).mtimeMs;
20
+ } catch {
21
+ return Date.now();
22
+ }
23
+ }
24
+
25
+ export function atomicWriteSync(file, data) {
26
+ ensureDir(file);
27
+ const tmp = `${file}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
28
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
29
+ try {
30
+ renameSync(tmp, file);
31
+ } catch {
32
+ try { writeFileSync(file, JSON.stringify(data, null, 2), { mode: 0o600 }); } catch {}
33
+ try { unlinkSync(tmp); } catch {}
34
+ }
35
+ return getMtimeMs(file);
36
+ }
37
+
38
+ export async function atomicWriteAsync(file, data) {
39
+ ensureDir(file);
40
+ await writeFile(file, JSON.stringify(data, null, 2), { mode: 0o600 });
41
+ return getMtimeMs(file);
42
+ }
@@ -0,0 +1,87 @@
1
+ import { defaultStateFile, readState, writeStateImmediate } from "../store.js";
2
+ import {
3
+ normalizeAllowedModel,
4
+ normalizeBaseUrl,
5
+ normalizeAuths,
6
+ normalizeEndpointPath,
7
+ loadProviderConfigs,
8
+ loadProviderConfig,
9
+ loadProviderBaseUrl,
10
+ loadProviderKeys,
11
+ } from "./provider.js";
12
+
13
+ export function loadProviderAllowedModels(id, { file = defaultStateFile() } = {}) {
14
+ const cfg = loadProviderConfig(id, { file });
15
+ if (cfg && Array.isArray(cfg.allowedModels)) return [...cfg.allowedModels];
16
+ const raw = readState(file).providerConfigs?.[id];
17
+ if (raw && Array.isArray(raw.allowedModels)) return [...new Set(raw.allowedModels.map((m) => normalizeAllowedModel(m, id)).filter(Boolean))];
18
+ return [];
19
+ }
20
+
21
+ export function saveProviderAllowedModels(id, list, { file = defaultStateFile() } = {}) {
22
+ const clean = [...new Set((Array.isArray(list) ? list : []).map((m) => normalizeAllowedModel(m, id)).filter(Boolean))];
23
+ const configs = { ...loadProviderConfigs({ file }) };
24
+ const cur = configs[id] && typeof configs[id] === "object" ? configs[id] : {};
25
+ const baseUrl = normalizeBaseUrl(cur.baseUrl || loadProviderBaseUrl(id, { file }) || "");
26
+ const keys = Array.isArray(cur.keys) ? cur.keys : loadProviderKeys(id, { file });
27
+ const auths = normalizeAuths(cur.auths);
28
+ const modelsPath = typeof cur.modelsPath === "string" ? normalizeEndpointPath(cur.modelsPath) : "";
29
+ const chatPath = typeof cur.chatPath === "string" ? normalizeEndpointPath(cur.chatPath) : "";
30
+ if (!baseUrl && !keys.length && !auths.length && !clean.length && !modelsPath && !chatPath) {
31
+ delete configs[id];
32
+ } else {
33
+ configs[id] = { baseUrl, keys };
34
+ if (auths.length) configs[id].auths = auths;
35
+ if (clean.length) configs[id].allowedModels = clean;
36
+ if (modelsPath) configs[id].modelsPath = modelsPath;
37
+ if (chatPath) configs[id].chatPath = chatPath;
38
+ }
39
+ writeStateImmediate(file, { providerConfigs: configs });
40
+ return clean;
41
+ }
42
+
43
+ export function addProviderAllowedModel(id, model, opts = {}) {
44
+ const cur = loadProviderAllowedModels(id, opts);
45
+ const norm = normalizeAllowedModel(model, id);
46
+ if (!norm || cur.includes(norm)) return cur;
47
+ return saveProviderAllowedModels(id, [...cur, norm], opts);
48
+ }
49
+
50
+ export function removeProviderAllowedModel(id, model, opts = {}) {
51
+ return removeProviderAllowedModels(id, [model], opts);
52
+ }
53
+
54
+ export function removeProviderAllowedModels(id, targets = [], opts = {}) {
55
+ const set = new Set((Array.isArray(targets) ? targets : [targets]).map((m) => normalizeAllowedModel(m, id)).filter(Boolean));
56
+ const cur = loadProviderAllowedModels(id, opts);
57
+ const next = cur.filter((m) => !set.has(m));
58
+ return saveProviderAllowedModels(id, next, opts);
59
+ }
60
+
61
+ function normalizeAllowAny(v, id) {
62
+ if (typeof v === "boolean") return v;
63
+ if (String(id || "").toLowerCase() === "opencode") return true;
64
+ return false;
65
+ }
66
+ export function loadProviderAllowAnyModels(id, { file = defaultStateFile() } = {}) {
67
+ const cfg = readState(file).providerConfigs?.[id];
68
+ if (cfg && typeof cfg.allowAnyModels === "boolean") return cfg.allowAnyModels;
69
+ return normalizeAllowAny(undefined, id);
70
+ }
71
+ export function saveProviderAllowAnyModels(id, allowAny, { file = defaultStateFile() } = {}) {
72
+ const state = readState(file);
73
+ const cfgs = { ...(state.providerConfigs || {}) };
74
+ const cur = cfgs[id] || {};
75
+ cfgs[id] = { ...cur, allowAnyModels: normalizeAllowAny(allowAny, id) };
76
+ writeStateImmediate(file, { providerConfigs: cfgs });
77
+ return cfgs[id].allowAnyModels;
78
+ }
79
+ export function isModelAllowed(id, rawModel, { file = defaultStateFile() } = {}) {
80
+ const raw = String(rawModel || "").trim();
81
+ if (!raw) return true;
82
+ const allowed = loadProviderAllowedModels(id, { file });
83
+ const allowAny = loadProviderAllowAnyModels(id, { file });
84
+ if (!allowed.length) return allowAny;
85
+ const norm = normalizeAllowedModel(raw, id);
86
+ return allowed.includes(norm);
87
+ }
@@ -0,0 +1,31 @@
1
+ import { defaultStateFile, readState, writeStateImmediate } from "../store.js";
2
+
3
+ export function loadGroups({ file = defaultStateFile() } = {}) {
4
+ const groups = readState(file).groups;
5
+ return groups && typeof groups === "object" && !Array.isArray(groups) ? groups : {};
6
+ }
7
+
8
+ export function loadGroupsJoined({ file = defaultStateFile() } = {}) {
9
+ const joined = readState(file).groupsJoined;
10
+ return Array.isArray(joined) ? joined : [];
11
+ }
12
+
13
+ export function saveGroupsJoined(joined, { file = defaultStateFile() } = {}) {
14
+ writeStateImmediate(file, { groupsJoined: joined });
15
+ return joined;
16
+ }
17
+
18
+ export function loadBans({ file = defaultStateFile() } = {}) {
19
+ const bans = readState(file).bans;
20
+ return bans && typeof bans === "object" && !Array.isArray(bans) ? bans : {};
21
+ }
22
+
23
+ export function saveBans(bans, { file = defaultStateFile() } = {}) {
24
+ writeStateImmediate(file, { bans });
25
+ return bans;
26
+ }
27
+
28
+ export function saveGroups(groups, { file = defaultStateFile() } = {}) {
29
+ writeStateImmediate(file, { groups });
30
+ return groups;
31
+ }
@@ -0,0 +1,53 @@
1
+ import { defaultStateFile, readState, writeStateImmediate, writeStateDeferred } from "../store.js";
2
+
3
+ export function loadModelErrors({ file = defaultStateFile() } = {}) {
4
+ const errors = readState(file).modelErrors;
5
+ return errors && typeof errors === "object" && !Array.isArray(errors) ? errors : {};
6
+ }
7
+
8
+ export function saveModelErrors(errors, { file = defaultStateFile() } = {}) {
9
+ writeStateDeferred(file, { modelErrors: errors });
10
+ return errors;
11
+ }
12
+
13
+ export function loadModelLatencies({ file = defaultStateFile() } = {}) {
14
+ const lat = readState(file).modelLatencies;
15
+ return lat && typeof lat === "object" && !Array.isArray(lat) ? lat : {};
16
+ }
17
+
18
+ export function saveModelLatencies(latencies, { file = defaultStateFile() } = {}) {
19
+ writeStateDeferred(file, { modelLatencies: latencies });
20
+ return latencies;
21
+ }
22
+
23
+ export function loadModelStats({ file = defaultStateFile() } = {}) {
24
+ const s = readState(file).modelStats;
25
+ return s && typeof s === "object" && !Array.isArray(s) ? s : {};
26
+ }
27
+
28
+ export function saveModelStats(stats, { file = defaultStateFile() } = {}) {
29
+ writeStateDeferred(file, { modelStats: stats });
30
+ return stats;
31
+ }
32
+
33
+ export function loadPreferredModel({ file = defaultStateFile() } = {}) {
34
+ const v = readState(file).preferredModel;
35
+ return typeof v === "string" && v.trim() ? v.trim() : null;
36
+ }
37
+
38
+ export function loadModelPicks({ file = defaultStateFile() } = {}) {
39
+ const picks = readState(file).modelPicks;
40
+ if (!Array.isArray(picks)) return [];
41
+ return [...new Set(picks.filter((x) => typeof x === "string" && x.trim().length))];
42
+ }
43
+
44
+ export function saveModelPicks(picks, { file = defaultStateFile() } = {}) {
45
+ const list = [...new Set((Array.isArray(picks) ? picks : []).filter((x) => typeof x === "string" && x.trim().length))];
46
+ writeStateImmediate(file, { modelPicks: list });
47
+ return list;
48
+ }
49
+
50
+ export function savePreferredModel(id, { file = defaultStateFile() } = {}) {
51
+ writeStateImmediate(file, { preferredModel: String(id || "").trim() });
52
+ return String(id || "").trim();
53
+ }
@@ -0,0 +1,31 @@
1
+ import { defaultStateFile, readState, writeStateImmediate, writeStateDeferred } from "../store.js";
2
+
3
+ export function loadPeers({ file = defaultStateFile() } = {}) {
4
+ const peers = readState(file).peers;
5
+ return Array.isArray(peers) ? peers : [];
6
+ }
7
+
8
+ export function savePeers(peers, { file = defaultStateFile() } = {}) {
9
+ writeStateImmediate(file, { peers });
10
+ return peers;
11
+ }
12
+
13
+ export function loadPeerErrors({ file = defaultStateFile() } = {}) {
14
+ const errors = readState(file).peerErrors;
15
+ return errors && typeof errors === "object" && !Array.isArray(errors) ? errors : {};
16
+ }
17
+
18
+ export function savePeerErrors(errors, { file = defaultStateFile() } = {}) {
19
+ writeStateDeferred(file, { peerErrors: errors });
20
+ return errors;
21
+ }
22
+
23
+ export function loadPeerStats({ file = defaultStateFile() } = {}) {
24
+ const stats = readState(file).peerStats;
25
+ return stats && typeof stats === "object" && !Array.isArray(stats) ? stats : {};
26
+ }
27
+
28
+ export function savePeerStats(stats, { file = defaultStateFile() } = {}) {
29
+ writeStateDeferred(file, { peerStats: stats });
30
+ return stats;
31
+ }
@@ -0,0 +1,10 @@
1
+ import { defaultStateFile, readState, writeStateImmediate } from "../store.js";
2
+
3
+ export function setPort(port, { file = defaultStateFile() } = {}) {
4
+ writeStateImmediate(file, { port: Number(port) });
5
+ }
6
+
7
+ export function getPort({ file = defaultStateFile() } = {}) {
8
+ const port = readState(file).port;
9
+ return typeof port === "number" && Number.isInteger(port) && port > 0 ? port : null;
10
+ }