mslxdff 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -94
- package/bin/mslxdff.js +466 -4
- package/package.json +26 -26
- package/src/auto.js +99 -0
- package/src/daemon.js +63 -63
- package/src/groups.js +189 -0
- package/src/logs.js +80 -0
- package/src/models.js +127 -91
- package/src/peers.js +83 -0
- package/src/reasoning.js +32 -32
- package/src/routes.js +309 -125
- package/src/server.js +37 -36
- package/src/state.js +119 -53
- package/src/upstream.js +67 -67
package/src/state.js
CHANGED
|
@@ -1,53 +1,119 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_PORT = 8989;
|
|
7
|
-
|
|
8
|
-
export function defaultStateFile() {
|
|
9
|
-
return process.env.MSLXDFF_STATE_FILE ||
|
|
10
|
-
join(os.homedir(), ".config", "mslxdff", "state.json");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function generateToken() {
|
|
14
|
-
return randomBytes(32).toString("hex");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function loadToken({ file = defaultStateFile() } = {}) {
|
|
18
|
-
const state = readState(file);
|
|
19
|
-
if (typeof state.token === "string" && state.token.length > 0) {
|
|
20
|
-
return { token: state.token, created: false };
|
|
21
|
-
}
|
|
22
|
-
return { token: writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token, created: true };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function refreshToken({ file = defaultStateFile() } = {}) {
|
|
26
|
-
return writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function setPort(port, { file = defaultStateFile() } = {}) {
|
|
30
|
-
const state = readState(file);
|
|
31
|
-
writeState(file, { ...state, port: Number(port) });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function getPort({ file = defaultStateFile() } = {}) {
|
|
35
|
-
const port = readState(file).port;
|
|
36
|
-
return typeof port === "number" && Number.isInteger(port) && port > 0 ? port : null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
}
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_PORT = 8989;
|
|
7
|
+
|
|
8
|
+
export function defaultStateFile() {
|
|
9
|
+
return process.env.MSLXDFF_STATE_FILE ||
|
|
10
|
+
join(os.homedir(), ".config", "mslxdff", "state.json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function generateToken() {
|
|
14
|
+
return randomBytes(32).toString("hex");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadToken({ file = defaultStateFile() } = {}) {
|
|
18
|
+
const state = readState(file);
|
|
19
|
+
if (typeof state.token === "string" && state.token.length > 0) {
|
|
20
|
+
return { token: state.token, created: false };
|
|
21
|
+
}
|
|
22
|
+
return { token: writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token, created: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function refreshToken({ file = defaultStateFile() } = {}) {
|
|
26
|
+
return writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setPort(port, { file = defaultStateFile() } = {}) {
|
|
30
|
+
const state = readState(file);
|
|
31
|
+
writeState(file, { ...state, port: Number(port) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getPort({ file = defaultStateFile() } = {}) {
|
|
35
|
+
const port = readState(file).port;
|
|
36
|
+
return typeof port === "number" && Number.isInteger(port) && port > 0 ? port : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadModelErrors({ file = defaultStateFile() } = {}) {
|
|
40
|
+
const errors = readState(file).modelErrors;
|
|
41
|
+
return errors && typeof errors === "object" && !Array.isArray(errors) ? errors : {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function saveModelErrors(errors, { file = defaultStateFile() } = {}) {
|
|
45
|
+
const state = readState(file);
|
|
46
|
+
writeState(file, { ...state, modelErrors: errors });
|
|
47
|
+
return errors;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function loadPeers({ file = defaultStateFile() } = {}) {
|
|
51
|
+
const peers = readState(file).peers;
|
|
52
|
+
return Array.isArray(peers) ? peers : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function savePeers(peers, { file = defaultStateFile() } = {}) {
|
|
56
|
+
const state = readState(file);
|
|
57
|
+
writeState(file, { ...state, peers });
|
|
58
|
+
return peers;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadPeerErrors({ file = defaultStateFile() } = {}) {
|
|
62
|
+
const errors = readState(file).peerErrors;
|
|
63
|
+
return errors && typeof errors === "object" && !Array.isArray(errors) ? errors : {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function savePeerErrors(errors, { file = defaultStateFile() } = {}) {
|
|
67
|
+
const state = readState(file);
|
|
68
|
+
writeState(file, { ...state, peerErrors: errors });
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function loadGroups({ file = defaultStateFile() } = {}) {
|
|
73
|
+
const groups = readState(file).groups;
|
|
74
|
+
return groups && typeof groups === "object" && !Array.isArray(groups) ? groups : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function loadGroupsJoined({ file = defaultStateFile() } = {}) {
|
|
78
|
+
const joined = readState(file).groupsJoined;
|
|
79
|
+
return Array.isArray(joined) ? joined : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function saveGroupsJoined(joined, { file = defaultStateFile() } = {}) {
|
|
83
|
+
const state = readState(file);
|
|
84
|
+
writeState(file, { ...state, groupsJoined: joined });
|
|
85
|
+
return joined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function loadBans({ file = defaultStateFile() } = {}) {
|
|
89
|
+
const bans = readState(file).bans;
|
|
90
|
+
return bans && typeof bans === "object" && !Array.isArray(bans) ? bans : {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function saveBans(bans, { file = defaultStateFile() } = {}) {
|
|
94
|
+
const state = readState(file);
|
|
95
|
+
writeState(file, { ...state, bans });
|
|
96
|
+
return bans;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function saveGroups(groups, { file = defaultStateFile() } = {}) {
|
|
100
|
+
const state = readState(file);
|
|
101
|
+
writeState(file, { ...state, groups });
|
|
102
|
+
return groups;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readState(file) {
|
|
106
|
+
try {
|
|
107
|
+
const saved = JSON.parse(readFileSync(file, "utf8"));
|
|
108
|
+
return typeof saved === "object" && saved !== null ? saved : {};
|
|
109
|
+
} catch {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeState(file, patch) {
|
|
115
|
+
const merged = { ...readState(file), ...patch };
|
|
116
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
117
|
+
writeFileSync(file, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
118
|
+
return merged;
|
|
119
|
+
}
|
package/src/upstream.js
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
export function createUpstreamClient({
|
|
2
|
-
baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
3
|
-
authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
|
|
4
|
-
connectTimeoutMs = Number(process.env.UPSTREAM_CONNECT_TIMEOUT_MS) || 30_000,
|
|
5
|
-
retry = {
|
|
6
|
-
network: { attempts: 2, delayMs: 1000 },
|
|
7
|
-
429: { attempts: 2, delayMs: 2000 },
|
|
8
|
-
502: { attempts: 2, delayMs: 2000 },
|
|
9
|
-
503: { attempts: 2, delayMs: 2000 },
|
|
10
|
-
504: { attempts: 2, delayMs: 3000 },
|
|
11
|
-
},
|
|
12
|
-
fetchImpl = fetch,
|
|
13
|
-
} = {}) {
|
|
14
|
-
const headers = {
|
|
15
|
-
"Content-Type": "application/json",
|
|
16
|
-
"Authorization": `Bearer ${authToken}`,
|
|
17
|
-
"x-opencode-client": "desktop",
|
|
18
|
-
"Accept": "text/event-stream",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
async function chat(body) {
|
|
22
|
-
const url = `${baseUrl}/zen/v1/chat/completions`;
|
|
23
|
-
for (let attempt = 0; ; attempt++) {
|
|
24
|
-
const result = await attemptOnce(url, body);
|
|
25
|
-
if (result instanceof Error) {
|
|
26
|
-
const entry = retry?.network;
|
|
27
|
-
if (entry && attempt < entry.attempts) {
|
|
28
|
-
await sleep(entry.delayMs);
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
throw result;
|
|
32
|
-
}
|
|
33
|
-
const entry = retry?.[result.status];
|
|
34
|
-
if (entry && attempt < entry.attempts) {
|
|
35
|
-
await sleep(entry.delayMs);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function attemptOnce(url, body) {
|
|
43
|
-
const controller = new AbortController();
|
|
44
|
-
const timer = setTimeout(() =>
|
|
45
|
-
controller.abort(new Error(`upstream timed out after ${connectTimeoutMs}ms`)),
|
|
46
|
-
connectTimeoutMs
|
|
47
|
-
);
|
|
48
|
-
try {
|
|
49
|
-
const res = await fetchImpl(url, {
|
|
50
|
-
method: "POST",
|
|
51
|
-
headers,
|
|
52
|
-
body: JSON.stringify(body),
|
|
53
|
-
signal: controller.signal,
|
|
54
|
-
});
|
|
55
|
-
return res;
|
|
56
|
-
} catch (err) {
|
|
57
|
-
return err;
|
|
58
|
-
} finally {
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { chat, headers };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function sleep(ms) {
|
|
67
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
1
|
+
export function createUpstreamClient({
|
|
2
|
+
baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
3
|
+
authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
|
|
4
|
+
connectTimeoutMs = Number(process.env.UPSTREAM_CONNECT_TIMEOUT_MS) || 30_000,
|
|
5
|
+
retry = {
|
|
6
|
+
network: { attempts: 2, delayMs: 1000 },
|
|
7
|
+
429: { attempts: 2, delayMs: 2000 },
|
|
8
|
+
502: { attempts: 2, delayMs: 2000 },
|
|
9
|
+
503: { attempts: 2, delayMs: 2000 },
|
|
10
|
+
504: { attempts: 2, delayMs: 3000 },
|
|
11
|
+
},
|
|
12
|
+
fetchImpl = fetch,
|
|
13
|
+
} = {}) {
|
|
14
|
+
const headers = {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"Authorization": `Bearer ${authToken}`,
|
|
17
|
+
"x-opencode-client": "desktop",
|
|
18
|
+
"Accept": "text/event-stream",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function chat(body) {
|
|
22
|
+
const url = `${baseUrl}/zen/v1/chat/completions`;
|
|
23
|
+
for (let attempt = 0; ; attempt++) {
|
|
24
|
+
const result = await attemptOnce(url, body);
|
|
25
|
+
if (result instanceof Error) {
|
|
26
|
+
const entry = retry?.network;
|
|
27
|
+
if (entry && attempt < entry.attempts) {
|
|
28
|
+
await sleep(entry.delayMs);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
throw result;
|
|
32
|
+
}
|
|
33
|
+
const entry = retry?.[result.status];
|
|
34
|
+
if (entry && attempt < entry.attempts) {
|
|
35
|
+
await sleep(entry.delayMs);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function attemptOnce(url, body) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() =>
|
|
45
|
+
controller.abort(new Error(`upstream timed out after ${connectTimeoutMs}ms`)),
|
|
46
|
+
connectTimeoutMs
|
|
47
|
+
);
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetchImpl(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
return res;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return err;
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { chat, headers };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sleep(ms) {
|
|
67
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
68
68
|
}
|