soulnet-dsh 0.1.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 +64 -0
- package/cordis.patch.yml +40 -0
- package/lib/client.js +5427 -0
- package/lib/client.js.map +1 -0
- package/lib/commands.js +94 -0
- package/lib/commands.js.map +1 -0
- package/lib/events-Cz7g9aXF.js +134 -0
- package/lib/events-Cz7g9aXF.js.map +1 -0
- package/lib/events-DHPK3pow.js +134 -0
- package/lib/events-DHPK3pow.js.map +1 -0
- package/lib/events-DViXNRAZ.js +9 -0
- package/lib/events-DViXNRAZ.js.map +1 -0
- package/lib/index.js +2547 -0
- package/lib/index.js.map +1 -0
- package/lib/policy-HQKLctKr.js +96 -0
- package/lib/policy-HQKLctKr.js.map +1 -0
- package/lib/send-BL_Yr40O.js +33 -0
- package/lib/send-BL_Yr40O.js.map +1 -0
- package/lib/send-DcYQC5CC.js +127 -0
- package/lib/send-DcYQC5CC.js.map +1 -0
- package/lib/sessions.js +1197 -0
- package/lib/sessions.js.map +1 -0
- package/lib/tools.js +342 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/alter-state.d.ts +185 -0
- package/lib/types/api/index.d.ts +52 -0
- package/lib/types/client/A2ANode.d.ts +4 -0
- package/lib/types/client/AlterPane.d.ts +9 -0
- package/lib/types/client/ChatPane.d.ts +13 -0
- package/lib/types/client/DraftCard.d.ts +11 -0
- package/lib/types/client/FriendComposer.d.ts +20 -0
- package/lib/types/client/FriendList.d.ts +12 -0
- package/lib/types/client/FriendPane.d.ts +13 -0
- package/lib/types/client/Inbox.d.ts +9 -0
- package/lib/types/client/InboxOverlay.d.ts +8 -0
- package/lib/types/client/Onboarding.d.ts +4 -0
- package/lib/types/client/SettingsSection.d.ts +29 -0
- package/lib/types/client/SidebarEntry.d.ts +9 -0
- package/lib/types/client/SoulmirrorPage.d.ts +12 -0
- package/lib/types/client/SoulmirrorView.d.ts +5 -0
- package/lib/types/client/a2a-node.d.ts +46 -0
- package/lib/types/client/alter-ui.d.ts +35 -0
- package/lib/types/client/api.d.ts +424 -0
- package/lib/types/client/inbox-state.d.ts +57 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/locales.d.ts +239 -0
- package/lib/types/client/page-state.d.ts +116 -0
- package/lib/types/client/page-store.d.ts +96 -0
- package/lib/types/client/panel-store.d.ts +30 -0
- package/lib/types/client/styles.d.ts +2 -0
- package/lib/types/commands/index.d.ts +24 -0
- package/lib/types/drafts.d.ts +50 -0
- package/lib/types/events.d.ts +61 -0
- package/lib/types/friend-settings.d.ts +41 -0
- package/lib/types/index.d.ts +43 -0
- package/lib/types/network/fake.d.ts +10 -0
- package/lib/types/network/jsonrpc.d.ts +52 -0
- package/lib/types/network/send.d.ts +19 -0
- package/lib/types/network/soulnet.d.ts +123 -0
- package/lib/types/network/types.d.ts +203 -0
- package/lib/types/persona.d.ts +45 -0
- package/lib/types/policy.d.ts +105 -0
- package/lib/types/sessions/index.d.ts +182 -0
- package/lib/types/settings.d.ts +58 -0
- package/lib/types/tools/define.d.ts +35 -0
- package/lib/types/tools/index.d.ts +57 -0
- package/lib/types-zwvjR1n5.js +29 -0
- package/lib/types-zwvjR1n5.js.map +1 -0
- package/package.json +133 -0
- package/presets/soulmirror-chat/agent.cordis.yml +24 -0
- package/presets/soulmirror-chat/preset.yml +3 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,2547 @@
|
|
|
1
|
+
import { a as isReplyTier, n as DEFAULT_REPLY_TIER, o as normalizeTier, t as sendAndArchive } from "./send-DcYQC5CC.js";
|
|
2
|
+
import { i as ProtocolFile, n as SOULMIRROR_PLUGIN, t as RELAY_FORM } from "./events-DHPK3pow.js";
|
|
3
|
+
import { n as NetworkErrorCode, t as NetworkError } from "./types-zwvjR1n5.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { accessSync, chmodSync, constants, realpathSync } from "node:fs";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
//#region src/api/index.ts
|
|
12
|
+
const API_PREFIX = "/soulmirror/api/";
|
|
13
|
+
function readJson(req, limit = 262144) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
req.on("data", (chunk) => {
|
|
18
|
+
size += chunk.length;
|
|
19
|
+
if (size > limit) {
|
|
20
|
+
reject(/* @__PURE__ */ new Error("request body too large"));
|
|
21
|
+
req.destroy();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(chunk);
|
|
25
|
+
});
|
|
26
|
+
req.on("end", () => {
|
|
27
|
+
if (chunks.length === 0) {
|
|
28
|
+
resolve({});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
33
|
+
resolve(typeof parsed === "object" && parsed !== null ? parsed : {});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
req.on("error", reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function send(res, status, body) {
|
|
42
|
+
const payload = JSON.stringify(body);
|
|
43
|
+
res.writeHead(status, {
|
|
44
|
+
"content-type": "application/json; charset=utf-8",
|
|
45
|
+
"cache-control": "no-store",
|
|
46
|
+
"content-length": Buffer.byteLength(payload)
|
|
47
|
+
});
|
|
48
|
+
res.end(payload);
|
|
49
|
+
}
|
|
50
|
+
function errorBody(error) {
|
|
51
|
+
if (error instanceof NetworkError) return { error: {
|
|
52
|
+
code: error.code,
|
|
53
|
+
message: error.message
|
|
54
|
+
} };
|
|
55
|
+
return { error: {
|
|
56
|
+
code: -32603,
|
|
57
|
+
message: error instanceof Error ? error.message : String(error)
|
|
58
|
+
} };
|
|
59
|
+
}
|
|
60
|
+
const bad = (message) => ({
|
|
61
|
+
status: 400,
|
|
62
|
+
body: { error: {
|
|
63
|
+
code: -32602,
|
|
64
|
+
message
|
|
65
|
+
} }
|
|
66
|
+
});
|
|
67
|
+
const text = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
68
|
+
const num = (value) => {
|
|
69
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
70
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) return Number(value);
|
|
71
|
+
};
|
|
72
|
+
/** `fps` as a JSON array (POST) or a comma-separated query value (GET). */
|
|
73
|
+
const fpList = (value) => {
|
|
74
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string" && v !== "");
|
|
75
|
+
if (typeof value === "string") return value.split(",").map((v) => v.trim()).filter((v) => v !== "");
|
|
76
|
+
return [];
|
|
77
|
+
};
|
|
78
|
+
/** GET routes whose query string stands in for the JSON body (`?fp=…&since=…&limit=…`, `?fps=a,b`). */
|
|
79
|
+
const QUERY_ROUTES = /* @__PURE__ */ new Set([
|
|
80
|
+
"state",
|
|
81
|
+
"conversation.get",
|
|
82
|
+
"presence",
|
|
83
|
+
"session.latest",
|
|
84
|
+
"session.history",
|
|
85
|
+
"protocol.get",
|
|
86
|
+
"drafts.list"
|
|
87
|
+
]);
|
|
88
|
+
function createApiHandler(options) {
|
|
89
|
+
const { client } = options;
|
|
90
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
91
|
+
const protocol = ProtocolFile.at(join(options.home, "a2a"));
|
|
92
|
+
const broadcast = (event) => {
|
|
93
|
+
if (sseClients.size === 0) return;
|
|
94
|
+
const frame = `event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
95
|
+
for (const res of sseClients) try {
|
|
96
|
+
res.write(frame);
|
|
97
|
+
} catch {
|
|
98
|
+
sseClients.delete(res);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const unsubscribe = client.subscribe(broadcast);
|
|
102
|
+
/** Direct send through the peer (the settings' debug "send as myself"), broadcast as `outbound` to every SSE client. */
|
|
103
|
+
const sendDirect = async (fp, body) => {
|
|
104
|
+
const result = await sendAndArchive(client, fp, body);
|
|
105
|
+
broadcast({
|
|
106
|
+
kind: "outbound",
|
|
107
|
+
fp,
|
|
108
|
+
entry: result.entry
|
|
109
|
+
});
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
/** The friend row as the browser sees it: the peer's record + the plugin's tier + pending draft count. */
|
|
113
|
+
const friendRow = (friend, sessions) => {
|
|
114
|
+
const fp = friend["fp"];
|
|
115
|
+
const tier = sessions?.tierOf(fp) ?? options.settings().defaultTier;
|
|
116
|
+
const explicit = sessions?.tierStored(fp) !== void 0;
|
|
117
|
+
const drafts = sessions?.drafts.count(fp) ?? 0;
|
|
118
|
+
return {
|
|
119
|
+
...friend,
|
|
120
|
+
tier,
|
|
121
|
+
...explicit ? { tierExplicit: true } : {},
|
|
122
|
+
...drafts > 0 ? { drafts } : {}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const state = async () => {
|
|
126
|
+
const status = client.status();
|
|
127
|
+
let identity = null;
|
|
128
|
+
let friends = [];
|
|
129
|
+
let pending = [];
|
|
130
|
+
let error;
|
|
131
|
+
const sessions = options.sessions();
|
|
132
|
+
try {
|
|
133
|
+
const id = await client.identity();
|
|
134
|
+
if (id !== void 0) {
|
|
135
|
+
identity = {
|
|
136
|
+
fp: id.fp,
|
|
137
|
+
name: id.name,
|
|
138
|
+
cardUri: id.cardUri,
|
|
139
|
+
...id.createdAt === void 0 ? {} : { createdAt: id.createdAt }
|
|
140
|
+
};
|
|
141
|
+
const [f, p] = await Promise.all([client.friends.list(), client.friends.pending()]);
|
|
142
|
+
pending = [...p];
|
|
143
|
+
let online = {};
|
|
144
|
+
if (f.length > 0) try {
|
|
145
|
+
online = await client.presence(f.map((x) => x.fp));
|
|
146
|
+
} catch {}
|
|
147
|
+
friends = f.map((x) => friendRow(online[x.fp] === void 0 ? x : {
|
|
148
|
+
...x,
|
|
149
|
+
online: online[x.fp]
|
|
150
|
+
}, sessions));
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
error = e instanceof Error ? e.message : String(e);
|
|
154
|
+
}
|
|
155
|
+
const settings = options.settings();
|
|
156
|
+
const alterState = sessions?.latest();
|
|
157
|
+
return {
|
|
158
|
+
backend: client.backend,
|
|
159
|
+
status,
|
|
160
|
+
home: options.home,
|
|
161
|
+
settingsNamespace: options.settingsNamespace,
|
|
162
|
+
identity,
|
|
163
|
+
friends,
|
|
164
|
+
pending,
|
|
165
|
+
drafts: sessions?.drafts.list() ?? [],
|
|
166
|
+
alter: {
|
|
167
|
+
sessionId: sessions?.sessionId() ?? null,
|
|
168
|
+
status: alterState?.status ?? "idle",
|
|
169
|
+
defaultTier: settings.defaultTier,
|
|
170
|
+
autoReplyPerHour: settings.autoReplyPerHour,
|
|
171
|
+
directSend: settings.directSend,
|
|
172
|
+
protocolPath: protocol.path,
|
|
173
|
+
protocolExists: protocol.exists(),
|
|
174
|
+
legacyFriendSessions: sessions?.legacyFriendSessions() ?? {}
|
|
175
|
+
},
|
|
176
|
+
...error === void 0 ? {} : { error }
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
const handle = async (route, method, body) => {
|
|
180
|
+
switch (route) {
|
|
181
|
+
case "state": return {
|
|
182
|
+
status: 200,
|
|
183
|
+
body: await state()
|
|
184
|
+
};
|
|
185
|
+
case "identity.create": {
|
|
186
|
+
const name = text(body["name"]);
|
|
187
|
+
if (name === void 0) return bad("name must not be empty");
|
|
188
|
+
return {
|
|
189
|
+
status: 200,
|
|
190
|
+
body: { identity: await client.createIdentity(name) }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
case "card.parse": {
|
|
194
|
+
const uri = text(body["uri"]);
|
|
195
|
+
if (uri === void 0) return bad("uri must not be empty");
|
|
196
|
+
return {
|
|
197
|
+
status: 200,
|
|
198
|
+
body: await client.parseCard(uri)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case "friends.add": {
|
|
202
|
+
const cardUri = text(body["card_uri"]);
|
|
203
|
+
if (cardUri === void 0) return bad("card_uri must not be empty");
|
|
204
|
+
const friend = await client.friends.add(cardUri, text(body["note"]));
|
|
205
|
+
options.log("info", `friend request sent to ${friend.name} (${friend.fp}) via settings/command`);
|
|
206
|
+
return {
|
|
207
|
+
status: 200,
|
|
208
|
+
body: { friend }
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
case "friends.accept": {
|
|
212
|
+
const id = text(body["id"]);
|
|
213
|
+
if (id === void 0) return bad("id must not be empty");
|
|
214
|
+
const friend = await client.friends.accept(id, text(body["note"]));
|
|
215
|
+
options.sessions()?.noteFriend(friend);
|
|
216
|
+
return {
|
|
217
|
+
status: 200,
|
|
218
|
+
body: { friend: friendRow(friend, options.sessions()) }
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
case "friends.reject": {
|
|
222
|
+
const id = text(body["id"]);
|
|
223
|
+
if (id === void 0) return bad("id must not be empty");
|
|
224
|
+
await client.friends.reject(id);
|
|
225
|
+
return {
|
|
226
|
+
status: 200,
|
|
227
|
+
body: { ok: true }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "friends.set": {
|
|
231
|
+
const fp = text(body["fp"]);
|
|
232
|
+
if (fp === void 0) return bad("fp must not be empty");
|
|
233
|
+
const note = text(body["note"]);
|
|
234
|
+
const protocolOverride = typeof body["protocol"] === "string" ? body["protocol"] : void 0;
|
|
235
|
+
const tierValue = body["tier"];
|
|
236
|
+
if (tierValue !== void 0 && tierValue !== null && tierValue !== "" && !isReplyTier(tierValue)) return bad("tier must be notify | draft | auto (empty = default)");
|
|
237
|
+
const sessions = options.sessions();
|
|
238
|
+
let friend = (await client.friends.list()).find((f) => f.fp === fp);
|
|
239
|
+
if (friend === void 0) return {
|
|
240
|
+
status: 404,
|
|
241
|
+
body: { error: {
|
|
242
|
+
code: -32002,
|
|
243
|
+
message: "not a friend"
|
|
244
|
+
} }
|
|
245
|
+
};
|
|
246
|
+
if (note !== void 0 || protocolOverride !== void 0) {
|
|
247
|
+
friend = await client.friends.set(fp, {
|
|
248
|
+
...note === void 0 ? {} : { remark: note },
|
|
249
|
+
...protocolOverride === void 0 ? {} : { protocol: protocolOverride }
|
|
250
|
+
});
|
|
251
|
+
sessions?.noteFriend(friend);
|
|
252
|
+
}
|
|
253
|
+
if (tierValue !== void 0 && sessions !== void 0) await sessions.setTier(fp, isReplyTier(tierValue) ? tierValue : void 0);
|
|
254
|
+
options.log("info", `friends.set ${fp}: ${[
|
|
255
|
+
note !== void 0 ? "note" : "",
|
|
256
|
+
protocolOverride !== void 0 ? "protocol" : "",
|
|
257
|
+
tierValue !== void 0 ? `tier=${String(tierValue)}` : ""
|
|
258
|
+
].filter((s) => s !== "").join(" ")}`);
|
|
259
|
+
return {
|
|
260
|
+
status: 200,
|
|
261
|
+
body: { friend: friendRow(friend, sessions) }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
case "friends.card": {
|
|
265
|
+
const fp = text(body["fp"]);
|
|
266
|
+
if (fp === void 0) return bad("fp must not be empty");
|
|
267
|
+
return {
|
|
268
|
+
status: 200,
|
|
269
|
+
body: await client.friends.card(fp)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
case "conversation.markRead": {
|
|
273
|
+
const fp = text(body["fp"]);
|
|
274
|
+
if (fp === void 0) return bad("fp required");
|
|
275
|
+
await client.markRead(fp, typeof body["seq"] === "number" ? body["seq"] : 0);
|
|
276
|
+
await options.sessions()?.markRead(fp);
|
|
277
|
+
return {
|
|
278
|
+
status: 200,
|
|
279
|
+
body: { ok: true }
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
case "conversation.get": {
|
|
283
|
+
const fp = text(body["fp"]);
|
|
284
|
+
if (fp === void 0) return bad("fp must not be empty");
|
|
285
|
+
const since = num(body["since"]);
|
|
286
|
+
const limit = num(body["limit"]);
|
|
287
|
+
return {
|
|
288
|
+
status: 200,
|
|
289
|
+
body: await client.conversation(fp, {
|
|
290
|
+
...since === void 0 ? {} : { since },
|
|
291
|
+
...limit === void 0 ? {} : { limit }
|
|
292
|
+
})
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
case "message.send": {
|
|
296
|
+
const fp = text(body["fp"]);
|
|
297
|
+
if (fp === void 0) return bad("fp must not be empty");
|
|
298
|
+
const msg = typeof body["body"] === "string" ? body["body"].replace(/\s+$/, "") : "";
|
|
299
|
+
if (msg === "") return bad("body must not be empty");
|
|
300
|
+
const result = await sendDirect(fp, msg);
|
|
301
|
+
options.log("info", `direct send to ${fp} (debug): ${result.receipt.id} (${result.receipt.status}, seq ${result.receipt.seq ?? "?"})`);
|
|
302
|
+
return {
|
|
303
|
+
status: 200,
|
|
304
|
+
body: result
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
case "message.typing": {
|
|
308
|
+
const fp = text(body["fp"]);
|
|
309
|
+
if (fp === void 0) return bad("fp must not be empty");
|
|
310
|
+
await client.typing(fp, body["on"] !== false && body["on"] !== "false" && body["on"] !== 0);
|
|
311
|
+
return {
|
|
312
|
+
status: 200,
|
|
313
|
+
body: { ok: true }
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
case "presence": {
|
|
317
|
+
const fps = fpList(body["fps"]);
|
|
318
|
+
return {
|
|
319
|
+
status: 200,
|
|
320
|
+
body: { online: await client.presence(fps) }
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
case "alter.instruct": {
|
|
324
|
+
const instruction = typeof body["text"] === "string" ? body["text"].replace(/\s+$/, "") : "";
|
|
325
|
+
if (instruction === "") return bad("text must not be empty");
|
|
326
|
+
const sessions = options.sessions();
|
|
327
|
+
if (sessions === void 0) return {
|
|
328
|
+
status: 503,
|
|
329
|
+
body: { error: {
|
|
330
|
+
code: -32603,
|
|
331
|
+
message: "sessions plugin not mounted"
|
|
332
|
+
} }
|
|
333
|
+
};
|
|
334
|
+
const result = await sessions.instruct(instruction);
|
|
335
|
+
options.log("info", `owner → alter: session ${result.sessionId}, message ${result.messageId}`);
|
|
336
|
+
return {
|
|
337
|
+
status: 200,
|
|
338
|
+
body: {
|
|
339
|
+
...result,
|
|
340
|
+
state: sessions.latest() ?? null
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
case "session.latest": return {
|
|
345
|
+
status: 200,
|
|
346
|
+
body: { state: options.sessions()?.latest() ?? null }
|
|
347
|
+
};
|
|
348
|
+
case "session.history": {
|
|
349
|
+
const sessions = options.sessions();
|
|
350
|
+
const limit = num(body["limit"]);
|
|
351
|
+
if (sessions === void 0) return {
|
|
352
|
+
status: 200,
|
|
353
|
+
body: {
|
|
354
|
+
sessionId: null,
|
|
355
|
+
status: "idle",
|
|
356
|
+
chat: {
|
|
357
|
+
items: [],
|
|
358
|
+
running: false,
|
|
359
|
+
seq: 0
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const h = sessions.history(limit);
|
|
364
|
+
return {
|
|
365
|
+
status: 200,
|
|
366
|
+
body: {
|
|
367
|
+
sessionId: h.sessionId ?? null,
|
|
368
|
+
status: h.status,
|
|
369
|
+
chat: h.chat
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
case "drafts.list": {
|
|
374
|
+
const fp = text(body["fp"]);
|
|
375
|
+
return {
|
|
376
|
+
status: 200,
|
|
377
|
+
body: { drafts: options.sessions()?.drafts.list(fp) ?? [] }
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
case "drafts.decide": {
|
|
381
|
+
const id = text(body["id"]);
|
|
382
|
+
if (id === void 0) return bad("id must not be empty");
|
|
383
|
+
const action = text(body["action"]);
|
|
384
|
+
const sessions = options.sessions();
|
|
385
|
+
if (sessions === void 0) return {
|
|
386
|
+
status: 503,
|
|
387
|
+
body: { error: {
|
|
388
|
+
code: -32603,
|
|
389
|
+
message: "sessions plugin not mounted"
|
|
390
|
+
} }
|
|
391
|
+
};
|
|
392
|
+
if (action === "approve") {
|
|
393
|
+
const edited = typeof body["body"] === "string" ? body["body"] : void 0;
|
|
394
|
+
return {
|
|
395
|
+
status: 200,
|
|
396
|
+
body: {
|
|
397
|
+
ok: true,
|
|
398
|
+
...await sessions.decideDraft(id, {
|
|
399
|
+
action: "approve",
|
|
400
|
+
...edited === void 0 ? {} : { body: edited }
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (action === "reject") return {
|
|
406
|
+
status: 200,
|
|
407
|
+
body: {
|
|
408
|
+
ok: true,
|
|
409
|
+
...await sessions.decideDraft(id, { action: "reject" })
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
if (action === "revise") {
|
|
413
|
+
const feedback = text(body["feedback"]);
|
|
414
|
+
if (feedback === void 0) return bad("feedback must not be empty");
|
|
415
|
+
return {
|
|
416
|
+
status: 200,
|
|
417
|
+
body: {
|
|
418
|
+
ok: true,
|
|
419
|
+
...await sessions.decideDraft(id, {
|
|
420
|
+
action: "revise",
|
|
421
|
+
feedback
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return bad("action must be approve | reject | revise");
|
|
427
|
+
}
|
|
428
|
+
case "protocol.get": return {
|
|
429
|
+
status: 200,
|
|
430
|
+
body: {
|
|
431
|
+
text: protocol.read(),
|
|
432
|
+
path: protocol.path,
|
|
433
|
+
exists: protocol.exists()
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
case "protocol.set":
|
|
437
|
+
if (typeof body["text"] !== "string") return bad("text must be a string");
|
|
438
|
+
protocol.write(body["text"]);
|
|
439
|
+
options.log("info", `diplomacy protocol saved (${body["text"].length} chars) → ${protocol.path}`);
|
|
440
|
+
return {
|
|
441
|
+
status: 200,
|
|
442
|
+
body: {
|
|
443
|
+
ok: true,
|
|
444
|
+
text: protocol.read(),
|
|
445
|
+
path: protocol.path
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
default: return {
|
|
449
|
+
status: 404,
|
|
450
|
+
body: { error: {
|
|
451
|
+
code: -32601,
|
|
452
|
+
message: `unknown route ${method} ${route}`
|
|
453
|
+
} }
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
const handler = (async (req, res) => {
|
|
458
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
459
|
+
const route = url.pathname.startsWith("/soulmirror/api/") ? url.pathname.slice(16) : "";
|
|
460
|
+
if (route === "events" && req.method === "GET") {
|
|
461
|
+
res.writeHead(200, {
|
|
462
|
+
"content-type": "text/event-stream",
|
|
463
|
+
"cache-control": "no-store",
|
|
464
|
+
connection: "keep-alive"
|
|
465
|
+
});
|
|
466
|
+
res.write(`event: status\ndata: ${JSON.stringify({
|
|
467
|
+
kind: "status",
|
|
468
|
+
status: client.status()
|
|
469
|
+
})}\n\n`);
|
|
470
|
+
sseClients.add(res);
|
|
471
|
+
const keepAlive = setInterval(() => {
|
|
472
|
+
try {
|
|
473
|
+
res.write(": keep-alive\n\n");
|
|
474
|
+
} catch {
|
|
475
|
+
clearInterval(keepAlive);
|
|
476
|
+
}
|
|
477
|
+
}, 25e3);
|
|
478
|
+
keepAlive.unref?.();
|
|
479
|
+
req.on("close", () => {
|
|
480
|
+
clearInterval(keepAlive);
|
|
481
|
+
sseClients.delete(res);
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (req.method !== "POST" && !(req.method === "GET" && QUERY_ROUTES.has(route))) {
|
|
486
|
+
send(res, 405, { error: {
|
|
487
|
+
code: -32600,
|
|
488
|
+
message: "use POST with application/json (GET only for state / conversation.get / presence / session.latest / session.history / drafts.list / protocol.get / events)"
|
|
489
|
+
} });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
const body = req.method === "POST" ? await readJson(req) : Object.fromEntries(url.searchParams.entries());
|
|
494
|
+
const result = await handle(route, req.method ?? "GET", body);
|
|
495
|
+
send(res, result.status, result.body);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
options.log("warn", `api ${route} failed: ${String(error)}`);
|
|
498
|
+
send(res, error instanceof NetworkError ? 400 : 500, errorBody(error));
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
handler.broadcast = broadcast;
|
|
502
|
+
handler.dispose = () => {
|
|
503
|
+
unsubscribe();
|
|
504
|
+
for (const res of sseClients) try {
|
|
505
|
+
res.end();
|
|
506
|
+
} catch {}
|
|
507
|
+
sseClients.clear();
|
|
508
|
+
};
|
|
509
|
+
return handler;
|
|
510
|
+
}
|
|
511
|
+
/** Mount the API on `ctx.webServer` when (and for as long as) that service exists. */
|
|
512
|
+
function mountApi(ctx, options) {
|
|
513
|
+
ctx.inject(["webServer"], (wctx) => {
|
|
514
|
+
const handler = createApiHandler(options);
|
|
515
|
+
const dispose = wctx.webServer.register({
|
|
516
|
+
kind: "prefix",
|
|
517
|
+
path: API_PREFIX.slice(0, -1),
|
|
518
|
+
handler
|
|
519
|
+
});
|
|
520
|
+
options.log("info", `browser API mounted at ${API_PREFIX} (port ${wctx.webServer.port})`);
|
|
521
|
+
wctx.inject(["soulmirrorSessions"], (sctx) => {
|
|
522
|
+
const off = sctx.soulmirrorSessions.on((event) => {
|
|
523
|
+
handler.broadcast(event);
|
|
524
|
+
});
|
|
525
|
+
sctx.effect(() => off, "soulmirror: alter events → SSE");
|
|
526
|
+
});
|
|
527
|
+
wctx.effect(() => () => {
|
|
528
|
+
dispose();
|
|
529
|
+
handler.dispose();
|
|
530
|
+
}, "soulmirror: browser API");
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/network/fake.ts
|
|
535
|
+
const fp$1 = (s) => s;
|
|
536
|
+
const mid$1 = () => `a2a-${crypto.randomUUID()}`;
|
|
537
|
+
const FAKE_FRIENDS = [{
|
|
538
|
+
fp: fp$1("fp-alice-1f2e3d4c5b6a7988"),
|
|
539
|
+
name: "college friend",
|
|
540
|
+
cardName: "Alice",
|
|
541
|
+
remark: "college friend",
|
|
542
|
+
online: true,
|
|
543
|
+
unread: 0,
|
|
544
|
+
count: 0
|
|
545
|
+
}, {
|
|
546
|
+
fp: fp$1("fp-bob-9a8b7c6d5e4f3021"),
|
|
547
|
+
name: "Bob",
|
|
548
|
+
cardName: "Bob",
|
|
549
|
+
online: false,
|
|
550
|
+
unread: 0,
|
|
551
|
+
count: 0
|
|
552
|
+
}];
|
|
553
|
+
const FAKE_PENDING = [{
|
|
554
|
+
id: "req-carol-1",
|
|
555
|
+
fp: fp$1("fp-carol-5566778899aabbcc"),
|
|
556
|
+
name: "Carol",
|
|
557
|
+
greeting: "Hi, Alice gave me your card."
|
|
558
|
+
}];
|
|
559
|
+
function createFakeNetworkClient(options = {}) {
|
|
560
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
561
|
+
const friends = new Map(FAKE_FRIENDS.map((f) => [f.fp, f]));
|
|
562
|
+
const pending = new Map(FAKE_PENDING.map((p) => [p.id, p]));
|
|
563
|
+
const conversations = /* @__PURE__ */ new Map();
|
|
564
|
+
let identity = options.noIdentity === true ? void 0 : {
|
|
565
|
+
fp: fp$1("fp-me-0000aaaabbbbcccc"),
|
|
566
|
+
name: "dsh tester",
|
|
567
|
+
cardUri: "soulmirror://card?v=1&pk=FAKE&xpk=FAKE&name=dsh%20tester"
|
|
568
|
+
};
|
|
569
|
+
let firstInboundFired = false;
|
|
570
|
+
let disposed = false;
|
|
571
|
+
const status = {
|
|
572
|
+
backend: "fake",
|
|
573
|
+
state: "ready",
|
|
574
|
+
restarts: 0
|
|
575
|
+
};
|
|
576
|
+
const emit = (event) => {
|
|
577
|
+
for (const l of listeners) try {
|
|
578
|
+
l(event);
|
|
579
|
+
} catch {}
|
|
580
|
+
};
|
|
581
|
+
const archive = (peer, entry) => {
|
|
582
|
+
const list = conversations.get(peer) ?? [];
|
|
583
|
+
const full = {
|
|
584
|
+
seq: list.length + 1,
|
|
585
|
+
...entry
|
|
586
|
+
};
|
|
587
|
+
list.push(full);
|
|
588
|
+
conversations.set(peer, list);
|
|
589
|
+
const friend = friends.get(peer);
|
|
590
|
+
if (friend !== void 0) friends.set(peer, {
|
|
591
|
+
...friend,
|
|
592
|
+
count: list.length,
|
|
593
|
+
unread: entry.dir === "in" ? friend.unread + 1 : friend.unread,
|
|
594
|
+
lastTs: entry.ts,
|
|
595
|
+
lastBody: entry.body
|
|
596
|
+
});
|
|
597
|
+
return full;
|
|
598
|
+
};
|
|
599
|
+
const deliver = (from, body, auto) => {
|
|
600
|
+
if (disposed) return;
|
|
601
|
+
const friend = friends.get(from);
|
|
602
|
+
const id = mid$1();
|
|
603
|
+
const ts = Date.now();
|
|
604
|
+
const entry = archive(from, {
|
|
605
|
+
dir: "in",
|
|
606
|
+
id,
|
|
607
|
+
body,
|
|
608
|
+
ts,
|
|
609
|
+
...auto ? { auto } : {}
|
|
610
|
+
});
|
|
611
|
+
emit({
|
|
612
|
+
kind: "message",
|
|
613
|
+
message: {
|
|
614
|
+
id,
|
|
615
|
+
from,
|
|
616
|
+
name: friend?.name ?? from,
|
|
617
|
+
body,
|
|
618
|
+
ts,
|
|
619
|
+
seq: entry.seq,
|
|
620
|
+
...auto ? { auto } : {}
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
const requireIdentity = () => {
|
|
625
|
+
if (identity === void 0) throw new NetworkError("no identity yet (identity.create first)", NetworkErrorCode.noIdentity);
|
|
626
|
+
return identity;
|
|
627
|
+
};
|
|
628
|
+
const friendFromCard = (uri, remark) => ({
|
|
629
|
+
fp: fp$1(`fp-${uri.slice(-8).replace(/[^a-z0-9]/gi, "") || "card"}`),
|
|
630
|
+
name: remark ?? `card ${uri.slice(-4)}`,
|
|
631
|
+
cardName: `card ${uri.slice(-4)}`,
|
|
632
|
+
...remark === void 0 ? {} : { remark },
|
|
633
|
+
unread: 0,
|
|
634
|
+
count: 0
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
backend: "fake",
|
|
638
|
+
status: () => status,
|
|
639
|
+
identity: () => Promise.resolve(identity),
|
|
640
|
+
createIdentity: (name) => {
|
|
641
|
+
if (identity !== void 0) return Promise.reject(new NetworkError("identity already exists", NetworkErrorCode.identityExists));
|
|
642
|
+
identity = {
|
|
643
|
+
fp: fp$1("fp-me-0000aaaabbbbcccc"),
|
|
644
|
+
name,
|
|
645
|
+
cardUri: `soulmirror://card?v=1&pk=FAKE&xpk=FAKE&name=${encodeURIComponent(name)}`
|
|
646
|
+
};
|
|
647
|
+
return Promise.resolve(identity);
|
|
648
|
+
},
|
|
649
|
+
card: () => Promise.resolve(requireIdentity().cardUri),
|
|
650
|
+
parseCard: (uri) => {
|
|
651
|
+
if (!uri.startsWith("soulmirror://card")) return Promise.reject(new NetworkError("invalid card link", NetworkErrorCode.badCard));
|
|
652
|
+
const f = friendFromCard(uri);
|
|
653
|
+
return Promise.resolve({
|
|
654
|
+
fp: f.fp,
|
|
655
|
+
name: f.cardName ?? f.name,
|
|
656
|
+
uri
|
|
657
|
+
});
|
|
658
|
+
},
|
|
659
|
+
friends: {
|
|
660
|
+
list: () => Promise.resolve([...friends.values()]),
|
|
661
|
+
pending: () => Promise.resolve([...pending.values()]),
|
|
662
|
+
add: (uri, remark) => {
|
|
663
|
+
requireIdentity();
|
|
664
|
+
if (!uri.startsWith("soulmirror://card")) return Promise.reject(new NetworkError("invalid card link", NetworkErrorCode.badCard));
|
|
665
|
+
const f = friendFromCard(uri, remark);
|
|
666
|
+
friends.set(f.fp, f);
|
|
667
|
+
setTimeout(() => {
|
|
668
|
+
if (!disposed) emit({
|
|
669
|
+
kind: "friend_accept",
|
|
670
|
+
friend: f
|
|
671
|
+
});
|
|
672
|
+
}, 300);
|
|
673
|
+
return Promise.resolve(f);
|
|
674
|
+
},
|
|
675
|
+
accept: (requestId, note) => {
|
|
676
|
+
const req = pending.get(requestId);
|
|
677
|
+
if (req === void 0) return Promise.reject(new NetworkError("no such pending request", NetworkErrorCode.notFound));
|
|
678
|
+
pending.delete(requestId);
|
|
679
|
+
const f = {
|
|
680
|
+
fp: req.fp,
|
|
681
|
+
name: note ?? req.name,
|
|
682
|
+
cardName: req.name,
|
|
683
|
+
...note === void 0 ? {} : { remark: note },
|
|
684
|
+
unread: 0,
|
|
685
|
+
count: 0
|
|
686
|
+
};
|
|
687
|
+
friends.set(f.fp, f);
|
|
688
|
+
return Promise.resolve(f);
|
|
689
|
+
},
|
|
690
|
+
reject: (requestId) => {
|
|
691
|
+
if (!pending.delete(requestId)) return Promise.reject(new NetworkError("no such pending request", NetworkErrorCode.notFound));
|
|
692
|
+
return Promise.resolve();
|
|
693
|
+
},
|
|
694
|
+
set: (id, patch) => {
|
|
695
|
+
const cur = friends.get(id);
|
|
696
|
+
if (cur === void 0) return Promise.reject(new NetworkError("not a friend", NetworkErrorCode.notFriend));
|
|
697
|
+
const { protocol: _old, ...rest } = cur;
|
|
698
|
+
const protocol = patch.protocol === void 0 ? cur.protocol : patch.protocol.trim() === "" ? void 0 : patch.protocol;
|
|
699
|
+
const next = {
|
|
700
|
+
...rest,
|
|
701
|
+
...patch.remark === void 0 ? {} : {
|
|
702
|
+
remark: patch.remark,
|
|
703
|
+
name: patch.remark
|
|
704
|
+
},
|
|
705
|
+
...protocol === void 0 ? {} : { protocol }
|
|
706
|
+
};
|
|
707
|
+
friends.set(id, next);
|
|
708
|
+
return Promise.resolve(next);
|
|
709
|
+
},
|
|
710
|
+
remove: (id) => {
|
|
711
|
+
if (!friends.delete(id)) return Promise.reject(new NetworkError("not a friend", NetworkErrorCode.notFriend));
|
|
712
|
+
return Promise.resolve();
|
|
713
|
+
},
|
|
714
|
+
card: (id) => {
|
|
715
|
+
const cur = friends.get(id);
|
|
716
|
+
if (cur === void 0) return Promise.reject(new NetworkError("not a friend", NetworkErrorCode.notFriend));
|
|
717
|
+
return Promise.resolve({
|
|
718
|
+
fp: cur.fp,
|
|
719
|
+
name: cur.cardName ?? cur.name,
|
|
720
|
+
uri: `soulmirror://card?v=1&pk=FAKE-${cur.fp}&xpk=FAKE&name=${encodeURIComponent(cur.cardName ?? cur.name)}`
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
send: (to, body, options) => {
|
|
725
|
+
requireIdentity();
|
|
726
|
+
if (!friends.has(to)) return Promise.reject(new NetworkError("not a friend (friends.add first)", NetworkErrorCode.notFriend));
|
|
727
|
+
const id = mid$1();
|
|
728
|
+
const entry = archive(to, {
|
|
729
|
+
dir: "out",
|
|
730
|
+
id,
|
|
731
|
+
body,
|
|
732
|
+
ts: Date.now(),
|
|
733
|
+
status: "sent",
|
|
734
|
+
...options?.auto === true ? { auto: true } : {}
|
|
735
|
+
});
|
|
736
|
+
setTimeout(() => {
|
|
737
|
+
deliver(to, `(auto-reply) got it: "${body.slice(0, 40)}"`, true);
|
|
738
|
+
}, 600);
|
|
739
|
+
return Promise.resolve({
|
|
740
|
+
id,
|
|
741
|
+
seq: entry.seq,
|
|
742
|
+
status: "sent"
|
|
743
|
+
});
|
|
744
|
+
},
|
|
745
|
+
typing: (to, on) => {
|
|
746
|
+
if (!friends.has(to)) return Promise.reject(new NetworkError("not a friend", NetworkErrorCode.notFriend));
|
|
747
|
+
return Promise.resolve();
|
|
748
|
+
},
|
|
749
|
+
conversation: (target, opts = {}) => {
|
|
750
|
+
let entries = conversations.get(target) ?? [];
|
|
751
|
+
if (opts.since !== void 0) entries = entries.filter((e) => e.seq > opts.since);
|
|
752
|
+
if (opts.limit !== void 0 && opts.limit > 0 && entries.length > opts.limit) entries = entries.slice(-opts.limit);
|
|
753
|
+
return Promise.resolve({
|
|
754
|
+
entries,
|
|
755
|
+
typing: false
|
|
756
|
+
});
|
|
757
|
+
},
|
|
758
|
+
markRead: (target) => {
|
|
759
|
+
const cur = friends.get(target);
|
|
760
|
+
if (cur === void 0) return Promise.reject(new NetworkError("not a friend", NetworkErrorCode.notFriend));
|
|
761
|
+
friends.set(target, {
|
|
762
|
+
...cur,
|
|
763
|
+
unread: 0
|
|
764
|
+
});
|
|
765
|
+
return Promise.resolve();
|
|
766
|
+
},
|
|
767
|
+
presence: (fps) => Promise.resolve(Object.fromEntries(fps.map((f) => [f, friends.get(f)?.online ?? false]))),
|
|
768
|
+
subscribe: (listener) => {
|
|
769
|
+
listeners.add(listener);
|
|
770
|
+
if (!firstInboundFired) {
|
|
771
|
+
firstInboundFired = true;
|
|
772
|
+
const delay = options.firstInboundDelayMs ?? 1500;
|
|
773
|
+
if (delay >= 0) {
|
|
774
|
+
const alice = FAKE_FRIENDS[0];
|
|
775
|
+
setTimeout(() => {
|
|
776
|
+
deliver(alice.fp, "Hey, are you around? Hiking this weekend - bring your alter ego too!");
|
|
777
|
+
}, delay);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return () => {
|
|
781
|
+
listeners.delete(listener);
|
|
782
|
+
};
|
|
783
|
+
},
|
|
784
|
+
dispose: () => {
|
|
785
|
+
disposed = true;
|
|
786
|
+
listeners.clear();
|
|
787
|
+
return Promise.resolve();
|
|
788
|
+
},
|
|
789
|
+
debug: { inject: (from, body) => {
|
|
790
|
+
deliver(from, body);
|
|
791
|
+
} }
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/network/jsonrpc.ts
|
|
796
|
+
/**
|
|
797
|
+
* Minimal line-delimited JSON-RPC 2.0 endpoint over a pair of Node streams.
|
|
798
|
+
*
|
|
799
|
+
* Used by the `soulnet` backend (stdin/stdout of the peer process) and by the
|
|
800
|
+
* unit tests (PassThrough streams standing in for a peer). One JSON object per
|
|
801
|
+
* line; requests carry an incrementing numeric id; frames without an id are
|
|
802
|
+
* notifications and are handed to `onNotification`.
|
|
803
|
+
*/
|
|
804
|
+
/** A JSON-RPC error response, or a transport failure (code -32099 family). */
|
|
805
|
+
var JsonRpcError = class extends Error {
|
|
806
|
+
code;
|
|
807
|
+
data;
|
|
808
|
+
name = "JsonRpcError";
|
|
809
|
+
constructor(message, code, data) {
|
|
810
|
+
super(message);
|
|
811
|
+
this.code = code;
|
|
812
|
+
this.data = data;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
/** Transport-level code: the endpoint closed before the response arrived. */
|
|
816
|
+
const JSONRPC_CLOSED = -32099;
|
|
817
|
+
/** Transport-level code: no response within the request timeout. */
|
|
818
|
+
const JSONRPC_TIMEOUT = -32098;
|
|
819
|
+
var JsonRpcEndpoint = class {
|
|
820
|
+
input;
|
|
821
|
+
output;
|
|
822
|
+
options;
|
|
823
|
+
pending = /* @__PURE__ */ new Map();
|
|
824
|
+
reader;
|
|
825
|
+
nextId = 1;
|
|
826
|
+
closed = false;
|
|
827
|
+
constructor(input, output, options = {}) {
|
|
828
|
+
this.input = input;
|
|
829
|
+
this.output = output;
|
|
830
|
+
this.options = options;
|
|
831
|
+
this.reader = createInterface({
|
|
832
|
+
input,
|
|
833
|
+
crlfDelay: Infinity
|
|
834
|
+
});
|
|
835
|
+
this.reader.on("line", (line) => {
|
|
836
|
+
this.handleLine(line);
|
|
837
|
+
});
|
|
838
|
+
this.reader.on("close", () => {
|
|
839
|
+
this.close();
|
|
840
|
+
});
|
|
841
|
+
input.on("error", (error) => {
|
|
842
|
+
this.close(error);
|
|
843
|
+
});
|
|
844
|
+
output.on("error", (error) => {
|
|
845
|
+
this.close(error);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
get isClosed() {
|
|
849
|
+
return this.closed;
|
|
850
|
+
}
|
|
851
|
+
/** Send a request and await its result; rejects with {@link JsonRpcError}. */
|
|
852
|
+
request(method, params, options = {}) {
|
|
853
|
+
if (this.closed) return Promise.reject(new JsonRpcError(`${method}: endpoint is closed`, JSONRPC_CLOSED));
|
|
854
|
+
const id = this.nextId++;
|
|
855
|
+
return new Promise((resolve, reject) => {
|
|
856
|
+
const timeoutMs = options.timeoutMs ?? this.options.timeoutMs ?? 0;
|
|
857
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
858
|
+
this.pending.delete(id);
|
|
859
|
+
reject(new JsonRpcError(`${method}: no response within ${timeoutMs} ms`, JSONRPC_TIMEOUT));
|
|
860
|
+
}, timeoutMs) : void 0;
|
|
861
|
+
timer?.unref?.();
|
|
862
|
+
const settle = (fn) => {
|
|
863
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
864
|
+
this.pending.delete(id);
|
|
865
|
+
fn();
|
|
866
|
+
};
|
|
867
|
+
this.pending.set(id, {
|
|
868
|
+
method,
|
|
869
|
+
timer,
|
|
870
|
+
resolve: (value) => {
|
|
871
|
+
settle(() => {
|
|
872
|
+
resolve(value);
|
|
873
|
+
});
|
|
874
|
+
},
|
|
875
|
+
reject: (error) => {
|
|
876
|
+
settle(() => {
|
|
877
|
+
reject(error);
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
if (options.signal !== void 0) {
|
|
882
|
+
const onAbort = () => {
|
|
883
|
+
this.pending.get(id)?.reject(new JsonRpcError(`${method}: aborted`, JSONRPC_CLOSED));
|
|
884
|
+
};
|
|
885
|
+
if (options.signal.aborted) onAbort();
|
|
886
|
+
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
887
|
+
}
|
|
888
|
+
if (!this.write({
|
|
889
|
+
jsonrpc: "2.0",
|
|
890
|
+
id,
|
|
891
|
+
method,
|
|
892
|
+
...params === void 0 ? {} : { params }
|
|
893
|
+
})) this.pending.get(id)?.reject(new JsonRpcError(`${method}: endpoint is closed`, JSONRPC_CLOSED));
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/** Fire-and-forget request without an id (no response expected). */
|
|
897
|
+
notify(method, params) {
|
|
898
|
+
this.write({
|
|
899
|
+
jsonrpc: "2.0",
|
|
900
|
+
method,
|
|
901
|
+
...params === void 0 ? {} : { params }
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
/** Reject every pending request and stop reading. Idempotent. */
|
|
905
|
+
close(error) {
|
|
906
|
+
if (this.closed) return;
|
|
907
|
+
this.closed = true;
|
|
908
|
+
this.reader.close();
|
|
909
|
+
const reason = new JsonRpcError(error === void 0 ? "endpoint closed" : `endpoint closed: ${error.message}`, JSONRPC_CLOSED);
|
|
910
|
+
for (const entry of [...this.pending.values()]) entry.reject(reason);
|
|
911
|
+
this.pending.clear();
|
|
912
|
+
this.options.onClose?.(error);
|
|
913
|
+
}
|
|
914
|
+
write(frame) {
|
|
915
|
+
if (this.closed) return false;
|
|
916
|
+
try {
|
|
917
|
+
this.output.write(`${JSON.stringify(frame)}\n`);
|
|
918
|
+
return true;
|
|
919
|
+
} catch (error) {
|
|
920
|
+
this.close(error instanceof Error ? error : new Error(String(error)));
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
handleLine(line) {
|
|
925
|
+
const trimmed = line.trim();
|
|
926
|
+
if (trimmed === "") return;
|
|
927
|
+
let frame;
|
|
928
|
+
try {
|
|
929
|
+
frame = JSON.parse(trimmed);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
this.options.onProtocolError?.(error instanceof Error ? error : new Error(String(error)), line);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (typeof frame !== "object" || frame === null) {
|
|
935
|
+
this.options.onProtocolError?.(/* @__PURE__ */ new Error("frame is not an object"), line);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const f = frame;
|
|
939
|
+
if (typeof f.method === "string" && (f.id === void 0 || f.id === null)) {
|
|
940
|
+
this.options.onNotification?.({
|
|
941
|
+
method: f.method,
|
|
942
|
+
params: f.params
|
|
943
|
+
});
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (typeof f.id !== "number") {
|
|
947
|
+
this.options.onProtocolError?.(/* @__PURE__ */ new Error("response without a numeric id"), line);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const entry = this.pending.get(f.id);
|
|
951
|
+
if (entry === void 0) {
|
|
952
|
+
this.options.onProtocolError?.(/* @__PURE__ */ new Error(`response for unknown request id ${f.id}`), line);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (f.error !== void 0 && f.error !== null) {
|
|
956
|
+
const e = f.error;
|
|
957
|
+
entry.reject(new JsonRpcError(typeof e.message === "string" ? e.message : `${entry.method} failed`, typeof e.code === "number" ? e.code : -32603, e.data));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
entry.resolve(f.result);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region src/network/soulnet.ts
|
|
965
|
+
/**
|
|
966
|
+
* `soulnet` backend: spawns the soulnet light peer (Go, ../cmd/soulnet) and
|
|
967
|
+
* drives it over line-delimited JSON-RPC 2.0 on stdio (protocol: cmd/soulnet/
|
|
968
|
+
* README.md, `initialize.protocol === "soulnet/1"`).
|
|
969
|
+
*
|
|
970
|
+
* - request/response by id, notifications → `subscribe` listeners;
|
|
971
|
+
* - the process is restarted with exponential backoff when it dies;
|
|
972
|
+
* - `dispose()` sends `shutdown`, then kills the process if it lingers;
|
|
973
|
+
* - calls issued while the peer is (re)starting wait for it up to the request
|
|
974
|
+
* timeout instead of failing immediately.
|
|
975
|
+
*
|
|
976
|
+
* Binary lookup (`resolveSoulnetBinary` / `locateSoulnetBinary`), in order:
|
|
977
|
+
* 1. the explicit `peerBinary` setting (absolute path, or a bare name looked up on PATH);
|
|
978
|
+
* 2. the platform package `soulnet-peer-<os>-<arch>` installed next to
|
|
979
|
+
* this plugin as an optional dependency (`require.resolve('<pkg>/package.json')`
|
|
980
|
+
* from this file and from its realpath, so pnpm's symlinked virtual store and
|
|
981
|
+
* the hoisted layout both work) -> `<pkg>/bin/soulnet[.exe]`;
|
|
982
|
+
* 3. `soulnet` on PATH;
|
|
983
|
+
* 4. `<plugin dir>/bin/soulnet[.exe]` (a hand-dropped binary for development).
|
|
984
|
+
* The winner and its source are reported in `BackendStatus.binary` / `binarySource`.
|
|
985
|
+
*/
|
|
986
|
+
const DEFAULT_RELAY = "https://relay.startupworld.cn";
|
|
987
|
+
const SOULNET_PROTOCOL = "soulnet/1";
|
|
988
|
+
/** Default home: `$SOULNET_HOME`, else `~/.soulnet` (same rule as the binary itself). */
|
|
989
|
+
function defaultSoulnetHome(env = process.env) {
|
|
990
|
+
const fromEnv = env["SOULNET_HOME"];
|
|
991
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
992
|
+
return join(homedir(), ".soulnet");
|
|
993
|
+
}
|
|
994
|
+
function isExecutable(path) {
|
|
995
|
+
try {
|
|
996
|
+
accessSync(path, constants.F_OK);
|
|
997
|
+
return true;
|
|
998
|
+
} catch {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/** npm scope of the platform packages that ship the binary. */
|
|
1003
|
+
/** Prefix of the per-platform binary packages on npm (`soulnet-peer-<os>-<arch>`). */
|
|
1004
|
+
const PLATFORM_PACKAGE_PREFIX = "soulnet-peer-";
|
|
1005
|
+
/** The `<os>-<arch>` pairs a platform package exists for (must match dsh/packages/soulnet-*). */
|
|
1006
|
+
const PLATFORM_PACKAGE_TARGETS = [
|
|
1007
|
+
"win32-x64",
|
|
1008
|
+
"darwin-arm64",
|
|
1009
|
+
"darwin-x64",
|
|
1010
|
+
"linux-x64",
|
|
1011
|
+
"linux-arm64"
|
|
1012
|
+
];
|
|
1013
|
+
/** `soulnet-peer-<os>-<arch>` for a supported pair, else `undefined`. */
|
|
1014
|
+
function platformPackageName(platform = process.platform, arch = process.arch) {
|
|
1015
|
+
const target = `${platform}-${arch}`;
|
|
1016
|
+
return PLATFORM_PACKAGE_TARGETS.includes(target) ? `${PLATFORM_PACKAGE_PREFIX}${target}` : void 0;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Resolve an installed package's directory from this plugin's location: first
|
|
1020
|
+
* from this file's URL (dsh loads lib/index.js from the profile's node_modules,
|
|
1021
|
+
* hoisted or symlinked), then from its realpath (pnpm's isolated virtual store
|
|
1022
|
+
* keeps the optional dependency next to the REAL plugin directory).
|
|
1023
|
+
*/
|
|
1024
|
+
function defaultResolvePackageDir(name) {
|
|
1025
|
+
const bases = [import.meta.url];
|
|
1026
|
+
try {
|
|
1027
|
+
const real = realpathSync(fileURLToPath(import.meta.url));
|
|
1028
|
+
const realUrl = pathToFileURL(real).href;
|
|
1029
|
+
if (realUrl !== import.meta.url) bases.push(realUrl);
|
|
1030
|
+
} catch {}
|
|
1031
|
+
for (const base of bases) try {
|
|
1032
|
+
return dirname(createRequire(base).resolve(`${name}/package.json`));
|
|
1033
|
+
} catch {}
|
|
1034
|
+
}
|
|
1035
|
+
function ensureExecutable(path, platform) {
|
|
1036
|
+
if (platform === "win32") return;
|
|
1037
|
+
try {
|
|
1038
|
+
accessSync(path, constants.X_OK);
|
|
1039
|
+
} catch {
|
|
1040
|
+
try {
|
|
1041
|
+
chmodSync(path, 493);
|
|
1042
|
+
} catch {}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Find the `soulnet` binary (order documented at the top of this file) and say
|
|
1047
|
+
* where it came from. `undefined` when nothing was found (the caller reports a
|
|
1048
|
+
* clear error).
|
|
1049
|
+
*/
|
|
1050
|
+
function locateSoulnetBinary(explicit, env = process.env, platform = process.platform, options = {}) {
|
|
1051
|
+
const names = platform === "win32" ? ["soulnet.exe", "soulnet"] : ["soulnet"];
|
|
1052
|
+
if (explicit !== void 0 && explicit.trim() !== "") {
|
|
1053
|
+
const candidate = explicit.trim();
|
|
1054
|
+
if (isAbsolute(candidate) || candidate.includes("/") || candidate.includes("\\")) return {
|
|
1055
|
+
path: candidate,
|
|
1056
|
+
source: "setting"
|
|
1057
|
+
};
|
|
1058
|
+
for (const dir of (env["PATH"] ?? "").split(delimiter)) {
|
|
1059
|
+
if (dir === "") continue;
|
|
1060
|
+
const full = join(dir, candidate);
|
|
1061
|
+
if (isExecutable(full)) return {
|
|
1062
|
+
path: full,
|
|
1063
|
+
source: "setting"
|
|
1064
|
+
};
|
|
1065
|
+
if (platform === "win32" && !candidate.toLowerCase().endsWith(".exe") && isExecutable(`${full}.exe`)) return {
|
|
1066
|
+
path: `${full}.exe`,
|
|
1067
|
+
source: "setting"
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
path: candidate,
|
|
1072
|
+
source: "setting"
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
const pkgName = platformPackageName(platform, options.arch ?? process.arch);
|
|
1076
|
+
if (pkgName !== void 0) {
|
|
1077
|
+
const dir = (options.resolvePackageDir ?? defaultResolvePackageDir)(pkgName);
|
|
1078
|
+
if (dir !== void 0) for (const name of names) {
|
|
1079
|
+
const full = join(dir, "bin", name);
|
|
1080
|
+
if (isExecutable(full)) {
|
|
1081
|
+
ensureExecutable(full, platform);
|
|
1082
|
+
return {
|
|
1083
|
+
path: full,
|
|
1084
|
+
source: "platform-package"
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
for (const dir of (env["PATH"] ?? "").split(delimiter)) {
|
|
1090
|
+
if (dir === "") continue;
|
|
1091
|
+
for (const name of names) {
|
|
1092
|
+
const full = join(dir, name);
|
|
1093
|
+
if (isExecutable(full)) return {
|
|
1094
|
+
path: full,
|
|
1095
|
+
source: "path"
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1100
|
+
for (const root of [join(here, ".."), join(here, "..", "..")]) for (const name of names) {
|
|
1101
|
+
const full = join(root, "bin", name);
|
|
1102
|
+
if (isExecutable(full)) return {
|
|
1103
|
+
path: full,
|
|
1104
|
+
source: "plugin-bin"
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const fp = (s) => s;
|
|
1109
|
+
const mid = (s) => s;
|
|
1110
|
+
function toMs(value) {
|
|
1111
|
+
if (typeof value === "number") return value;
|
|
1112
|
+
if (typeof value === "string") {
|
|
1113
|
+
const parsed = Date.parse(value);
|
|
1114
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
1115
|
+
}
|
|
1116
|
+
return Date.now();
|
|
1117
|
+
}
|
|
1118
|
+
function str(value, fallback = "") {
|
|
1119
|
+
return typeof value === "string" ? value : fallback;
|
|
1120
|
+
}
|
|
1121
|
+
function shortFp(value) {
|
|
1122
|
+
return value.length > 12 ? `${value.slice(0, 12)}…` : value;
|
|
1123
|
+
}
|
|
1124
|
+
function friendFromWire(w) {
|
|
1125
|
+
const fingerprint = str(w.fingerprint);
|
|
1126
|
+
const note = str(w.note);
|
|
1127
|
+
const cardName = str(w.card?.name);
|
|
1128
|
+
const name = note !== "" ? note : cardName !== "" ? cardName : shortFp(fingerprint);
|
|
1129
|
+
return {
|
|
1130
|
+
fp: fp(fingerprint),
|
|
1131
|
+
name,
|
|
1132
|
+
...note === "" ? {} : { remark: note },
|
|
1133
|
+
...cardName === "" ? {} : { cardName },
|
|
1134
|
+
...str(w.protocol) === "" ? {} : { protocol: str(w.protocol) },
|
|
1135
|
+
unread: typeof w.unread === "number" ? w.unread : 0,
|
|
1136
|
+
count: typeof w.count === "number" ? w.count : 0,
|
|
1137
|
+
...w.last?.ts === void 0 ? {} : { lastTs: toMs(w.last.ts) },
|
|
1138
|
+
...w.last?.body === void 0 ? {} : { lastBody: w.last.body },
|
|
1139
|
+
...w.typing === true ? { typing: true } : {},
|
|
1140
|
+
...w.added_at === void 0 ? {} : { addedAt: w.added_at }
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function pendingFromWire(w) {
|
|
1144
|
+
const peer = str(w.peer);
|
|
1145
|
+
const cardName = str(w.incoming?.card?.name);
|
|
1146
|
+
return {
|
|
1147
|
+
id: str(w.id),
|
|
1148
|
+
fp: fp(peer),
|
|
1149
|
+
name: cardName !== "" ? cardName : shortFp(peer),
|
|
1150
|
+
greeting: str(w.incoming?.body),
|
|
1151
|
+
...w.created_at === void 0 ? {} : { createdAt: w.created_at }
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function entryFromWire(w) {
|
|
1155
|
+
return {
|
|
1156
|
+
seq: typeof w.seq === "number" ? w.seq : 0,
|
|
1157
|
+
dir: w.dir === "out" ? "out" : "in",
|
|
1158
|
+
id: mid(str(w.id)),
|
|
1159
|
+
body: str(w.body),
|
|
1160
|
+
ts: toMs(w.ts),
|
|
1161
|
+
...w.type === void 0 || w.type === "" ? {} : { type: w.type },
|
|
1162
|
+
...w.auto === true ? { auto: true } : {},
|
|
1163
|
+
...w.status === void 0 || w.status === "" ? {} : { status: w.status },
|
|
1164
|
+
...w.artifact_name === void 0 || w.artifact_name === "" ? {} : { artifactName: w.artifact_name }
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function toNetworkError(error, method) {
|
|
1168
|
+
if (error instanceof NetworkError) return error;
|
|
1169
|
+
if (error instanceof JsonRpcError) {
|
|
1170
|
+
const code = error.code === -32099 || error.code === -32098 ? NetworkErrorCode.peerUnavailable : error.code;
|
|
1171
|
+
return new NetworkError(error.message, code, error.data);
|
|
1172
|
+
}
|
|
1173
|
+
return new NetworkError(`${method}: ${String(error)}`, -32603);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Create the soulnet-backed NetworkClient. The process is spawned lazily on
|
|
1177
|
+
* the first call or on `start()`; `dispose()` stops it.
|
|
1178
|
+
*/
|
|
1179
|
+
function createSoulnetNetworkClient(options) {
|
|
1180
|
+
const log = options.logger ?? (() => {});
|
|
1181
|
+
const relay = options.relay !== void 0 && options.relay.trim() !== "" ? options.relay.trim() : DEFAULT_RELAY;
|
|
1182
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
1183
|
+
const backoffInitial = options.backoff?.initialMs ?? 500;
|
|
1184
|
+
const backoffMax = options.backoff?.maxMs ?? 3e4;
|
|
1185
|
+
const backoffFactor = options.backoff?.factor ?? 2;
|
|
1186
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1187
|
+
let child;
|
|
1188
|
+
let endpoint;
|
|
1189
|
+
let disposed = false;
|
|
1190
|
+
let started = false;
|
|
1191
|
+
let restarts = 0;
|
|
1192
|
+
let backoffMs = backoffInitial;
|
|
1193
|
+
let restartTimer;
|
|
1194
|
+
let status = {
|
|
1195
|
+
backend: "soulnet",
|
|
1196
|
+
state: "stopped",
|
|
1197
|
+
restarts: 0,
|
|
1198
|
+
relay,
|
|
1199
|
+
home: options.home
|
|
1200
|
+
};
|
|
1201
|
+
let cachedCardUri;
|
|
1202
|
+
const friendNames = /* @__PURE__ */ new Map();
|
|
1203
|
+
let readyWaiters = [];
|
|
1204
|
+
const emit = (event) => {
|
|
1205
|
+
for (const listener of listeners) try {
|
|
1206
|
+
listener(event);
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
log("warn", `network listener failed: ${String(error)}`);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
const setStatus = (patch) => {
|
|
1212
|
+
status = {
|
|
1213
|
+
...status,
|
|
1214
|
+
...patch
|
|
1215
|
+
};
|
|
1216
|
+
emit({
|
|
1217
|
+
kind: "status",
|
|
1218
|
+
status
|
|
1219
|
+
});
|
|
1220
|
+
};
|
|
1221
|
+
const clearError = () => {
|
|
1222
|
+
const { lastError: _dropped, ...rest } = status;
|
|
1223
|
+
status = rest;
|
|
1224
|
+
};
|
|
1225
|
+
const handleNotification = (method, params) => {
|
|
1226
|
+
const p = typeof params === "object" && params !== null ? params : {};
|
|
1227
|
+
const peer = str(p.peer);
|
|
1228
|
+
switch (method) {
|
|
1229
|
+
case "message.received": {
|
|
1230
|
+
const m = p.message ?? {};
|
|
1231
|
+
const type = str(m.type, "text");
|
|
1232
|
+
const name = friendNames.get(peer) ?? shortFp(peer);
|
|
1233
|
+
const body = str(m.body) !== "" ? str(m.body) : type === "app_share" ? "[app share]" : "";
|
|
1234
|
+
emit({
|
|
1235
|
+
kind: "message",
|
|
1236
|
+
message: {
|
|
1237
|
+
id: mid(str(m.id)),
|
|
1238
|
+
from: fp(peer),
|
|
1239
|
+
name,
|
|
1240
|
+
body,
|
|
1241
|
+
ts: toMs(m.ts),
|
|
1242
|
+
...typeof p.seq === "number" ? { seq: p.seq } : {},
|
|
1243
|
+
...m.auto === true ? { auto: true } : {},
|
|
1244
|
+
...type === "text" ? {} : { type },
|
|
1245
|
+
...p.artifact_path === void 0 || p.artifact_path === "" ? {} : { artifactPath: p.artifact_path },
|
|
1246
|
+
...m.artifact_name === void 0 || m.artifact_name === "" ? {} : { artifactName: m.artifact_name }
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
case "friend.request": {
|
|
1252
|
+
const m = p.message ?? {};
|
|
1253
|
+
const cardName = str(m.card?.name);
|
|
1254
|
+
emit({
|
|
1255
|
+
kind: "friend_request",
|
|
1256
|
+
request: {
|
|
1257
|
+
id: str(p.pending_id),
|
|
1258
|
+
fp: fp(peer),
|
|
1259
|
+
name: cardName !== "" ? cardName : shortFp(peer),
|
|
1260
|
+
greeting: str(m.body)
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
case "friend.accepted": {
|
|
1266
|
+
const friend = friendFromWire(p.friend ?? { fingerprint: peer });
|
|
1267
|
+
friendNames.set(friend.fp, friend.name);
|
|
1268
|
+
emit({
|
|
1269
|
+
kind: "friend_accept",
|
|
1270
|
+
friend
|
|
1271
|
+
});
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
case "typing":
|
|
1275
|
+
emit({
|
|
1276
|
+
kind: "typing",
|
|
1277
|
+
fp: fp(peer),
|
|
1278
|
+
on: p.on === true
|
|
1279
|
+
});
|
|
1280
|
+
return;
|
|
1281
|
+
case "presence.changed":
|
|
1282
|
+
emit({
|
|
1283
|
+
kind: "presence",
|
|
1284
|
+
fp: fp(peer),
|
|
1285
|
+
online: p.on === true
|
|
1286
|
+
});
|
|
1287
|
+
return;
|
|
1288
|
+
case "mission.update":
|
|
1289
|
+
case "artifact.ready":
|
|
1290
|
+
log("info", `soulnet notification ${method} from ${shortFp(peer)} (not handled in M1)`);
|
|
1291
|
+
return;
|
|
1292
|
+
default: log("warn", `unknown soulnet notification ${method}`);
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
const failWaiters = (error) => {
|
|
1296
|
+
const waiters = readyWaiters;
|
|
1297
|
+
readyWaiters = [];
|
|
1298
|
+
for (const w of waiters) w.reject(error);
|
|
1299
|
+
};
|
|
1300
|
+
const scheduleRestart = (reason) => {
|
|
1301
|
+
if (disposed) return;
|
|
1302
|
+
restarts += 1;
|
|
1303
|
+
const delay = backoffMs;
|
|
1304
|
+
backoffMs = Math.min(backoffMax, Math.round(backoffMs * backoffFactor));
|
|
1305
|
+
setStatus({
|
|
1306
|
+
state: "restarting",
|
|
1307
|
+
restarts,
|
|
1308
|
+
lastError: reason
|
|
1309
|
+
});
|
|
1310
|
+
log("warn", `soulnet peer died (${reason}); restart #${restarts} in ${delay} ms`);
|
|
1311
|
+
restartTimer = setTimeout(() => {
|
|
1312
|
+
restartTimer = void 0;
|
|
1313
|
+
spawnPeer();
|
|
1314
|
+
}, delay);
|
|
1315
|
+
restartTimer.unref?.();
|
|
1316
|
+
};
|
|
1317
|
+
const spawnPeer = () => {
|
|
1318
|
+
if (disposed) return;
|
|
1319
|
+
const location = locateSoulnetBinary(options.peerBinary);
|
|
1320
|
+
if (location === void 0) {
|
|
1321
|
+
const message = `soulnet binary not found: the platform package ${platformPackageName() ?? `soulnet-peer-<os>-<arch> (none published for ${process.platform}-${process.arch})`} is not installed next to the plugin (reinstall with optional dependencies enabled), or set \`peerBinary\` in the SoulMirror network settings, put \`soulnet\` on PATH, or place it in the plugin's bin/ directory`;
|
|
1322
|
+
setStatus({
|
|
1323
|
+
state: "error",
|
|
1324
|
+
lastError: message
|
|
1325
|
+
});
|
|
1326
|
+
log("error", message);
|
|
1327
|
+
failWaiters(new NetworkError(message, NetworkErrorCode.peerUnavailable));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const binary = location.path;
|
|
1331
|
+
const args = [
|
|
1332
|
+
"--home",
|
|
1333
|
+
options.home,
|
|
1334
|
+
"--relay",
|
|
1335
|
+
relay
|
|
1336
|
+
];
|
|
1337
|
+
clearError();
|
|
1338
|
+
setStatus({
|
|
1339
|
+
state: "starting",
|
|
1340
|
+
binary,
|
|
1341
|
+
binarySource: location.source
|
|
1342
|
+
});
|
|
1343
|
+
log("info", `soulnet binary: ${binary} (${location.source})`);
|
|
1344
|
+
let proc;
|
|
1345
|
+
try {
|
|
1346
|
+
proc = options.spawn !== void 0 ? options.spawn({
|
|
1347
|
+
binary,
|
|
1348
|
+
args
|
|
1349
|
+
}) : spawn(binary, args, {
|
|
1350
|
+
stdio: [
|
|
1351
|
+
"pipe",
|
|
1352
|
+
"pipe",
|
|
1353
|
+
"pipe"
|
|
1354
|
+
],
|
|
1355
|
+
windowsHide: true,
|
|
1356
|
+
env: {
|
|
1357
|
+
...process.env,
|
|
1358
|
+
...options.env ?? {}
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
scheduleRestart(`spawn failed: ${String(error)}`);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
child = proc;
|
|
1366
|
+
if (proc.stdout === null || proc.stdin === null) {
|
|
1367
|
+
proc.kill();
|
|
1368
|
+
scheduleRestart("spawned process has no stdio pipes");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
proc.stderr?.setEncoding("utf8");
|
|
1372
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1373
|
+
for (const line of chunk.split(/\r?\n/)) if (line.trim() !== "") log("info", `[soulnet] ${line.trim()}`);
|
|
1374
|
+
});
|
|
1375
|
+
const ep = new JsonRpcEndpoint(proc.stdout, proc.stdin, {
|
|
1376
|
+
timeoutMs: requestTimeoutMs,
|
|
1377
|
+
onNotification: (n) => {
|
|
1378
|
+
handleNotification(n.method, n.params);
|
|
1379
|
+
},
|
|
1380
|
+
onProtocolError: (error, line) => {
|
|
1381
|
+
log("warn", `soulnet protocol: ${error.message}: ${line.slice(0, 200)}`);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
endpoint = ep;
|
|
1385
|
+
let exited = false;
|
|
1386
|
+
proc.on("error", (error) => {
|
|
1387
|
+
if (exited) return;
|
|
1388
|
+
exited = true;
|
|
1389
|
+
if (endpoint === ep) endpoint = void 0;
|
|
1390
|
+
ep.close(error);
|
|
1391
|
+
scheduleRestart(`process error: ${error.message}`);
|
|
1392
|
+
});
|
|
1393
|
+
proc.on("exit", (code, signal) => {
|
|
1394
|
+
if (exited) return;
|
|
1395
|
+
exited = true;
|
|
1396
|
+
if (endpoint === ep) endpoint = void 0;
|
|
1397
|
+
if (child === proc) child = void 0;
|
|
1398
|
+
ep.close();
|
|
1399
|
+
if (disposed) {
|
|
1400
|
+
setStatus({ state: "stopped" });
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
scheduleRestart(`exit code=${code ?? "null"} signal=${signal ?? "none"}`);
|
|
1404
|
+
});
|
|
1405
|
+
const name = options.displayName?.trim() ?? "";
|
|
1406
|
+
ep.request("initialize", name === "" ? {} : { name }, { timeoutMs: 15e3 }).then((result) => {
|
|
1407
|
+
const r = typeof result === "object" && result !== null ? result : {};
|
|
1408
|
+
if (r.protocol !== "soulnet/1") log("warn", `soulnet speaks ${String(r.protocol)}; this plugin was written for ${SOULNET_PROTOCOL}`);
|
|
1409
|
+
backoffMs = backoffInitial;
|
|
1410
|
+
cachedCardUri = void 0;
|
|
1411
|
+
setStatus({
|
|
1412
|
+
state: "ready",
|
|
1413
|
+
...proc.pid === void 0 ? {} : { pid: proc.pid },
|
|
1414
|
+
...r.protocol === void 0 ? {} : { protocol: r.protocol },
|
|
1415
|
+
...r.version === void 0 ? {} : { version: r.version },
|
|
1416
|
+
...r.home === void 0 ? {} : { home: r.home },
|
|
1417
|
+
...r.relay === void 0 ? {} : { relay: r.relay }
|
|
1418
|
+
});
|
|
1419
|
+
log("info", `soulnet peer ready pid=${proc.pid ?? "?"} protocol=${String(r.protocol)} identity=${r.identity?.fingerprint ?? "none"}`);
|
|
1420
|
+
const waiters = readyWaiters;
|
|
1421
|
+
readyWaiters = [];
|
|
1422
|
+
for (const w of waiters) w.resolve(ep);
|
|
1423
|
+
}).catch((error) => {
|
|
1424
|
+
if (exited || disposed) return;
|
|
1425
|
+
log("error", `soulnet initialize failed: ${String(error)}`);
|
|
1426
|
+
proc.kill();
|
|
1427
|
+
});
|
|
1428
|
+
};
|
|
1429
|
+
const ready = (timeoutMs) => {
|
|
1430
|
+
if (disposed) return Promise.reject(new NetworkError("soulnet backend is disposed", NetworkErrorCode.peerUnavailable));
|
|
1431
|
+
if (endpoint !== void 0 && !endpoint.isClosed && status.state === "ready") return Promise.resolve(endpoint);
|
|
1432
|
+
if (!started) start();
|
|
1433
|
+
if (status.state === "error") return Promise.reject(new NetworkError(status.lastError ?? "soulnet backend unavailable", NetworkErrorCode.peerUnavailable));
|
|
1434
|
+
return new Promise((resolve, reject) => {
|
|
1435
|
+
const timer = setTimeout(() => {
|
|
1436
|
+
readyWaiters = readyWaiters.filter((w) => w.resolve !== resolve);
|
|
1437
|
+
reject(new NetworkError(`soulnet peer not ready within ${timeoutMs} ms (state=${status.state})`, NetworkErrorCode.peerUnavailable));
|
|
1438
|
+
}, timeoutMs);
|
|
1439
|
+
timer.unref?.();
|
|
1440
|
+
readyWaiters.push({
|
|
1441
|
+
resolve: (ep) => {
|
|
1442
|
+
clearTimeout(timer);
|
|
1443
|
+
resolve(ep);
|
|
1444
|
+
},
|
|
1445
|
+
reject: (error) => {
|
|
1446
|
+
clearTimeout(timer);
|
|
1447
|
+
reject(error);
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
};
|
|
1452
|
+
const call = async (method, params, timeoutMs = requestTimeoutMs) => {
|
|
1453
|
+
const ep = await ready(timeoutMs);
|
|
1454
|
+
try {
|
|
1455
|
+
return await ep.request(method, params, { timeoutMs });
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
throw toNetworkError(error, method);
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
const start = () => {
|
|
1461
|
+
if (started || disposed) return;
|
|
1462
|
+
started = true;
|
|
1463
|
+
spawnPeer();
|
|
1464
|
+
};
|
|
1465
|
+
const identity = async () => {
|
|
1466
|
+
const r = await call("identity.get");
|
|
1467
|
+
if (r.identity === void 0 || r.identity === null) return void 0;
|
|
1468
|
+
const cardUri = await card();
|
|
1469
|
+
return {
|
|
1470
|
+
fp: fp(str(r.identity.fingerprint)),
|
|
1471
|
+
name: str(r.identity.name),
|
|
1472
|
+
cardUri,
|
|
1473
|
+
...r.identity.created_at === void 0 ? {} : { createdAt: r.identity.created_at }
|
|
1474
|
+
};
|
|
1475
|
+
};
|
|
1476
|
+
const card = async () => {
|
|
1477
|
+
if (cachedCardUri !== void 0) return cachedCardUri;
|
|
1478
|
+
cachedCardUri = str((await call("card.get")).uri);
|
|
1479
|
+
return cachedCardUri;
|
|
1480
|
+
};
|
|
1481
|
+
const rememberNames = (friends) => {
|
|
1482
|
+
for (const f of friends) friendNames.set(f.fp, f.name);
|
|
1483
|
+
};
|
|
1484
|
+
return {
|
|
1485
|
+
backend: "soulnet",
|
|
1486
|
+
start,
|
|
1487
|
+
status: () => status,
|
|
1488
|
+
identity,
|
|
1489
|
+
createIdentity: async (name) => {
|
|
1490
|
+
const r = await call("identity.create", { name });
|
|
1491
|
+
cachedCardUri = void 0;
|
|
1492
|
+
const cardUri = await card();
|
|
1493
|
+
return {
|
|
1494
|
+
fp: fp(str(r.identity?.fingerprint)),
|
|
1495
|
+
name: str(r.identity?.name, name),
|
|
1496
|
+
cardUri
|
|
1497
|
+
};
|
|
1498
|
+
},
|
|
1499
|
+
card,
|
|
1500
|
+
parseCard: async (uri) => {
|
|
1501
|
+
const r = await call("card.parse", { uri });
|
|
1502
|
+
return {
|
|
1503
|
+
fp: fp(str(r.fingerprint)),
|
|
1504
|
+
name: str(r.card?.name),
|
|
1505
|
+
uri: str(r.uri, uri)
|
|
1506
|
+
};
|
|
1507
|
+
},
|
|
1508
|
+
friends: {
|
|
1509
|
+
list: async () => {
|
|
1510
|
+
const friends = ((await call("friends.list")).friends ?? []).map(friendFromWire);
|
|
1511
|
+
rememberNames(friends);
|
|
1512
|
+
return friends;
|
|
1513
|
+
},
|
|
1514
|
+
pending: async () => {
|
|
1515
|
+
return ((await call("friends.pending")).pending ?? []).map(pendingFromWire);
|
|
1516
|
+
},
|
|
1517
|
+
add: async (cardUri, note) => {
|
|
1518
|
+
const friend = friendFromWire((await call("friends.add", {
|
|
1519
|
+
card_uri: cardUri,
|
|
1520
|
+
...note === void 0 ? {} : { note }
|
|
1521
|
+
})).friend ?? {});
|
|
1522
|
+
friendNames.set(friend.fp, friend.name);
|
|
1523
|
+
return friend;
|
|
1524
|
+
},
|
|
1525
|
+
accept: async (requestId, note) => {
|
|
1526
|
+
const friend = friendFromWire((await call("friends.accept", {
|
|
1527
|
+
id: requestId,
|
|
1528
|
+
...note === void 0 ? {} : { note }
|
|
1529
|
+
})).friend ?? {});
|
|
1530
|
+
friendNames.set(friend.fp, friend.name);
|
|
1531
|
+
return friend;
|
|
1532
|
+
},
|
|
1533
|
+
reject: async (requestId) => {
|
|
1534
|
+
await call("friends.reject", { id: requestId });
|
|
1535
|
+
},
|
|
1536
|
+
set: async (target, patch) => {
|
|
1537
|
+
const friend = friendFromWire((await call("friends.set", {
|
|
1538
|
+
fp: target,
|
|
1539
|
+
...patch.remark === void 0 ? {} : { note: patch.remark },
|
|
1540
|
+
...patch.protocol === void 0 ? {} : { protocol: patch.protocol }
|
|
1541
|
+
})).friend ?? {});
|
|
1542
|
+
friendNames.set(friend.fp, friend.name);
|
|
1543
|
+
return friend;
|
|
1544
|
+
},
|
|
1545
|
+
remove: async (target) => {
|
|
1546
|
+
await call("friends.remove", { fp: target });
|
|
1547
|
+
friendNames.delete(target);
|
|
1548
|
+
},
|
|
1549
|
+
card: async (target) => {
|
|
1550
|
+
const r = await call("friends.card", { fp: target });
|
|
1551
|
+
return {
|
|
1552
|
+
fp: fp(str(r.fingerprint, target)),
|
|
1553
|
+
name: str(r.card?.name),
|
|
1554
|
+
uri: str(r.uri)
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
send: async (to, body, options) => {
|
|
1559
|
+
const r = await call("message.send", {
|
|
1560
|
+
to,
|
|
1561
|
+
body,
|
|
1562
|
+
...options?.file === void 0 ? {} : { file: options.file },
|
|
1563
|
+
...options?.auto === true ? { auto: true } : {}
|
|
1564
|
+
});
|
|
1565
|
+
return {
|
|
1566
|
+
id: mid(str(r.id)),
|
|
1567
|
+
status: str(r.status, "sent"),
|
|
1568
|
+
...typeof r.seq === "number" ? { seq: r.seq } : {}
|
|
1569
|
+
};
|
|
1570
|
+
},
|
|
1571
|
+
typing: async (to, on) => {
|
|
1572
|
+
await call("message.typing", {
|
|
1573
|
+
to,
|
|
1574
|
+
on
|
|
1575
|
+
}, 1e4);
|
|
1576
|
+
},
|
|
1577
|
+
conversation: async (target, opts = {}) => {
|
|
1578
|
+
const r = await call("conversation.get", {
|
|
1579
|
+
fp: target,
|
|
1580
|
+
...opts.since === void 0 ? {} : { since: opts.since },
|
|
1581
|
+
...opts.limit === void 0 ? {} : { limit: opts.limit }
|
|
1582
|
+
});
|
|
1583
|
+
return {
|
|
1584
|
+
entries: (r.entries ?? []).map(entryFromWire),
|
|
1585
|
+
typing: r.typing === true
|
|
1586
|
+
};
|
|
1587
|
+
},
|
|
1588
|
+
markRead: async (target, seq) => {
|
|
1589
|
+
await call("conversation.markRead", {
|
|
1590
|
+
fp: target,
|
|
1591
|
+
seq
|
|
1592
|
+
});
|
|
1593
|
+
},
|
|
1594
|
+
presence: async (fps) => {
|
|
1595
|
+
return (await call("presence", { fps: [...fps] }, 15e3)).online ?? {};
|
|
1596
|
+
},
|
|
1597
|
+
subscribe: (listener) => {
|
|
1598
|
+
listeners.add(listener);
|
|
1599
|
+
return () => {
|
|
1600
|
+
listeners.delete(listener);
|
|
1601
|
+
};
|
|
1602
|
+
},
|
|
1603
|
+
dispose: async () => {
|
|
1604
|
+
if (disposed) return;
|
|
1605
|
+
disposed = true;
|
|
1606
|
+
if (restartTimer !== void 0) {
|
|
1607
|
+
clearTimeout(restartTimer);
|
|
1608
|
+
restartTimer = void 0;
|
|
1609
|
+
}
|
|
1610
|
+
failWaiters(new NetworkError("soulnet backend is disposed", NetworkErrorCode.peerUnavailable));
|
|
1611
|
+
const proc = child;
|
|
1612
|
+
const ep = endpoint;
|
|
1613
|
+
if (proc === void 0) {
|
|
1614
|
+
setStatus({ state: "stopped" });
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const exited = new Promise((resolve) => {
|
|
1618
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
1619
|
+
resolve();
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
proc.once("exit", () => {
|
|
1623
|
+
resolve();
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1626
|
+
if (ep !== void 0 && !ep.isClosed) try {
|
|
1627
|
+
await ep.request("shutdown", void 0, { timeoutMs: 2e3 });
|
|
1628
|
+
} catch {}
|
|
1629
|
+
const killTimer = setTimeout(() => {
|
|
1630
|
+
proc.kill();
|
|
1631
|
+
}, 2e3);
|
|
1632
|
+
killTimer.unref?.();
|
|
1633
|
+
await exited;
|
|
1634
|
+
clearTimeout(killTimer);
|
|
1635
|
+
ep?.close();
|
|
1636
|
+
setStatus({ state: "stopped" });
|
|
1637
|
+
log("info", "soulnet peer stopped");
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
//#endregion
|
|
1642
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
1643
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
1644
|
+
function isNullable(value) {
|
|
1645
|
+
return value === null || value === void 0;
|
|
1646
|
+
}
|
|
1647
|
+
/** Return true for non-array object values. */
|
|
1648
|
+
function isPlainObject(data) {
|
|
1649
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
1650
|
+
}
|
|
1651
|
+
/** Filter object entries and return a new object. */
|
|
1652
|
+
function filterKeys(object, filter) {
|
|
1653
|
+
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
|
|
1654
|
+
}
|
|
1655
|
+
/** Map object values while preserving the original key set. */
|
|
1656
|
+
function mapValues(object, transform) {
|
|
1657
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
|
|
1658
|
+
}
|
|
1659
|
+
/** Pick selected keys from an object, optionally including `undefined` values. */
|
|
1660
|
+
function pick(source, keys, forced) {
|
|
1661
|
+
if (!keys) return { ...source };
|
|
1662
|
+
const result = {};
|
|
1663
|
+
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
|
|
1664
|
+
return result;
|
|
1665
|
+
}
|
|
1666
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
1667
|
+
function is(type, value) {
|
|
1668
|
+
if (arguments.length === 1) return (value) => is(type, value);
|
|
1669
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
1670
|
+
}
|
|
1671
|
+
function isArrayBufferLike(value) {
|
|
1672
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
1673
|
+
}
|
|
1674
|
+
function isArrayBufferSource(value) {
|
|
1675
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
1676
|
+
}
|
|
1677
|
+
/** Binary source detection and base64/hex conversion helpers. */
|
|
1678
|
+
var Binary;
|
|
1679
|
+
(function(Binary) {
|
|
1680
|
+
Binary.is = isArrayBufferLike;
|
|
1681
|
+
Binary.isSource = isArrayBufferSource;
|
|
1682
|
+
function fromSource(source) {
|
|
1683
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
1684
|
+
else return source;
|
|
1685
|
+
}
|
|
1686
|
+
Binary.fromSource = fromSource;
|
|
1687
|
+
function toBase64(source) {
|
|
1688
|
+
source = fromSource(source);
|
|
1689
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
1690
|
+
let binary = "";
|
|
1691
|
+
const bytes = new Uint8Array(source);
|
|
1692
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
1693
|
+
return btoa(binary);
|
|
1694
|
+
}
|
|
1695
|
+
Binary.toBase64 = toBase64;
|
|
1696
|
+
function fromBase64(source) {
|
|
1697
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
1698
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
1699
|
+
}
|
|
1700
|
+
Binary.fromBase64 = fromBase64;
|
|
1701
|
+
function toHex(source) {
|
|
1702
|
+
source = fromSource(source);
|
|
1703
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
1704
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1705
|
+
}
|
|
1706
|
+
Binary.toHex = toHex;
|
|
1707
|
+
function fromHex(source) {
|
|
1708
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
1709
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
1710
|
+
const buffer = [];
|
|
1711
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
1712
|
+
return Uint8Array.from(buffer).buffer;
|
|
1713
|
+
}
|
|
1714
|
+
Binary.fromHex = fromHex;
|
|
1715
|
+
})(Binary || (Binary = {}));
|
|
1716
|
+
Binary.fromBase64;
|
|
1717
|
+
Binary.toBase64;
|
|
1718
|
+
Binary.fromHex;
|
|
1719
|
+
Binary.toHex;
|
|
1720
|
+
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
|
|
1721
|
+
function clone(source, refs = /* @__PURE__ */ new Map()) {
|
|
1722
|
+
if (!source || typeof source !== "object") return source;
|
|
1723
|
+
if (is("Date", source)) return new Date(source.valueOf());
|
|
1724
|
+
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
|
|
1725
|
+
if (isArrayBufferLike(source)) return source.slice(0);
|
|
1726
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
1727
|
+
const cached = refs.get(source);
|
|
1728
|
+
if (cached) return cached;
|
|
1729
|
+
if (Array.isArray(source)) {
|
|
1730
|
+
const result = [];
|
|
1731
|
+
refs.set(source, result);
|
|
1732
|
+
source.forEach((value, index) => {
|
|
1733
|
+
result[index] = Reflect.apply(clone, null, [value, refs]);
|
|
1734
|
+
});
|
|
1735
|
+
return result;
|
|
1736
|
+
}
|
|
1737
|
+
const result = Object.create(Object.getPrototypeOf(source));
|
|
1738
|
+
refs.set(source, result);
|
|
1739
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
1740
|
+
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
|
|
1741
|
+
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
|
|
1742
|
+
Reflect.defineProperty(result, key, descriptor);
|
|
1743
|
+
}
|
|
1744
|
+
return result;
|
|
1745
|
+
}
|
|
1746
|
+
/** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
|
|
1747
|
+
function deepEqual(a, b, strict) {
|
|
1748
|
+
if (a === b) return true;
|
|
1749
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
1750
|
+
if (typeof a !== typeof b) return false;
|
|
1751
|
+
if (typeof a !== "object") return false;
|
|
1752
|
+
if (!a || !b) return false;
|
|
1753
|
+
function check(test, then) {
|
|
1754
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
1755
|
+
}
|
|
1756
|
+
return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
|
|
1757
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
1758
|
+
const viewA = new Uint8Array(a);
|
|
1759
|
+
const viewB = new Uint8Array(b);
|
|
1760
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
1761
|
+
return true;
|
|
1762
|
+
}) ?? Object.keys({
|
|
1763
|
+
...a,
|
|
1764
|
+
...b
|
|
1765
|
+
}).every((key) => deepEqual(a[key], b[key], strict));
|
|
1766
|
+
}
|
|
1767
|
+
/** Time constants plus parsing and formatting helpers. */
|
|
1768
|
+
var Time;
|
|
1769
|
+
(function(Time) {
|
|
1770
|
+
Time.millisecond = 1;
|
|
1771
|
+
Time.second = 1e3;
|
|
1772
|
+
Time.minute = Time.second * 60;
|
|
1773
|
+
Time.hour = Time.minute * 60;
|
|
1774
|
+
Time.day = Time.hour * 24;
|
|
1775
|
+
Time.week = Time.day * 7;
|
|
1776
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
1777
|
+
function setTimezoneOffset(offset) {
|
|
1778
|
+
timezoneOffset = offset;
|
|
1779
|
+
}
|
|
1780
|
+
Time.setTimezoneOffset = setTimezoneOffset;
|
|
1781
|
+
function getTimezoneOffset() {
|
|
1782
|
+
return timezoneOffset;
|
|
1783
|
+
}
|
|
1784
|
+
Time.getTimezoneOffset = getTimezoneOffset;
|
|
1785
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
1786
|
+
if (typeof date === "number") date = new Date(date);
|
|
1787
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
1788
|
+
return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
|
|
1789
|
+
}
|
|
1790
|
+
Time.getDateNumber = getDateNumber;
|
|
1791
|
+
function fromDateNumber(value, offset) {
|
|
1792
|
+
const date = new Date(value * Time.day);
|
|
1793
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
1794
|
+
return new Date(+date + offset * Time.minute);
|
|
1795
|
+
}
|
|
1796
|
+
Time.fromDateNumber = fromDateNumber;
|
|
1797
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
1798
|
+
const timeRegExp = new RegExp(`^${[
|
|
1799
|
+
"w(?:eek(?:s)?)?",
|
|
1800
|
+
"d(?:ay(?:s)?)?",
|
|
1801
|
+
"h(?:our(?:s)?)?",
|
|
1802
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
1803
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
1804
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
1805
|
+
function parseTime(source) {
|
|
1806
|
+
const capture = timeRegExp.exec(source);
|
|
1807
|
+
if (!capture) return 0;
|
|
1808
|
+
return (parseFloat(capture[1]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
|
|
1809
|
+
}
|
|
1810
|
+
Time.parseTime = parseTime;
|
|
1811
|
+
function parseDate(date) {
|
|
1812
|
+
const parsed = parseTime(date);
|
|
1813
|
+
if (parsed) date = Date.now() + parsed;
|
|
1814
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
1815
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
1816
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
1817
|
+
}
|
|
1818
|
+
Time.parseDate = parseDate;
|
|
1819
|
+
function format(ms) {
|
|
1820
|
+
const abs = Math.abs(ms);
|
|
1821
|
+
if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
|
|
1822
|
+
else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
|
|
1823
|
+
else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
|
|
1824
|
+
else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
|
|
1825
|
+
return ms + "ms";
|
|
1826
|
+
}
|
|
1827
|
+
Time.format = format;
|
|
1828
|
+
function toDigits(source, length = 2) {
|
|
1829
|
+
return source.toString().padStart(length, "0");
|
|
1830
|
+
}
|
|
1831
|
+
Time.toDigits = toDigits;
|
|
1832
|
+
function template(template, time = /* @__PURE__ */ new Date()) {
|
|
1833
|
+
return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
1834
|
+
}
|
|
1835
|
+
Time.template = template;
|
|
1836
|
+
})(Time || (Time = {}));
|
|
1837
|
+
//#endregion
|
|
1838
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+schemastery@3.18.1/node_modules/@deepseek-ai/schemastery/lib/index.mjs
|
|
1839
|
+
const kSchema = Symbol.for("schemastery");
|
|
1840
|
+
const kValidationError = Symbol.for("ValidationError");
|
|
1841
|
+
globalThis.__schemastery_index__ ??= 0;
|
|
1842
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
1843
|
+
var ValidationError = class extends TypeError {
|
|
1844
|
+
options;
|
|
1845
|
+
name = "ValidationError";
|
|
1846
|
+
constructor(message, options) {
|
|
1847
|
+
let prefix = "$";
|
|
1848
|
+
for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
|
|
1849
|
+
else if (typeof segment === "number") prefix += "[" + segment + "]";
|
|
1850
|
+
else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
|
|
1851
|
+
if (prefix.startsWith(".")) prefix = prefix.slice(1);
|
|
1852
|
+
super((prefix === "$" ? "" : `${prefix} `) + message);
|
|
1853
|
+
this.options = options;
|
|
1854
|
+
}
|
|
1855
|
+
static is(error) {
|
|
1856
|
+
return !!error?.[kValidationError];
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
1860
|
+
const Schema = function(options) {
|
|
1861
|
+
const schema = function(data, options = {}) {
|
|
1862
|
+
return Schema.resolve(data, schema, options)[0];
|
|
1863
|
+
};
|
|
1864
|
+
if (options.refs) {
|
|
1865
|
+
const refs = mapValues(options.refs, (options) => new Schema(options));
|
|
1866
|
+
const getRef = (uid) => refs[uid];
|
|
1867
|
+
for (const key in refs) {
|
|
1868
|
+
const options = refs[key];
|
|
1869
|
+
options.sKey = getRef(options.sKey);
|
|
1870
|
+
options.inner = getRef(options.inner);
|
|
1871
|
+
options.list = options.list && options.list.map(getRef);
|
|
1872
|
+
options.dict = options.dict && mapValues(options.dict, getRef);
|
|
1873
|
+
}
|
|
1874
|
+
return refs[options.uid];
|
|
1875
|
+
}
|
|
1876
|
+
Object.assign(schema, options);
|
|
1877
|
+
if (typeof schema.callback === "string") try {
|
|
1878
|
+
schema.callback = new Function("return " + schema.callback)();
|
|
1879
|
+
} catch {}
|
|
1880
|
+
Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
|
|
1881
|
+
Object.setPrototypeOf(schema, Schema.prototype);
|
|
1882
|
+
schema.meta ||= {};
|
|
1883
|
+
schema.toString = schema.toString.bind(schema);
|
|
1884
|
+
return schema;
|
|
1885
|
+
};
|
|
1886
|
+
Schema.prototype = Object.create(Function.prototype);
|
|
1887
|
+
Schema.prototype[kSchema] = true;
|
|
1888
|
+
Object.defineProperty(Schema.prototype, "~standard", { get() {
|
|
1889
|
+
return {
|
|
1890
|
+
version: 1,
|
|
1891
|
+
vendor: "schemastery",
|
|
1892
|
+
validate: (value) => {
|
|
1893
|
+
try {
|
|
1894
|
+
return { value: Schema.resolve(value, this, {})[0] };
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
if (ValidationError.is(error)) return { issues: [{
|
|
1897
|
+
message: error.message,
|
|
1898
|
+
path: error.options.path
|
|
1899
|
+
}] };
|
|
1900
|
+
throw error;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
} });
|
|
1905
|
+
Schema.ValidationError = ValidationError;
|
|
1906
|
+
Schema.prototype.toJSON = function toJSON() {
|
|
1907
|
+
if (globalThis.__schemastery_refs__) {
|
|
1908
|
+
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
|
|
1909
|
+
return this.uid;
|
|
1910
|
+
}
|
|
1911
|
+
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
|
|
1912
|
+
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
|
|
1913
|
+
const result = {
|
|
1914
|
+
uid: this.uid,
|
|
1915
|
+
refs: globalThis.__schemastery_refs__
|
|
1916
|
+
};
|
|
1917
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
1918
|
+
return result;
|
|
1919
|
+
};
|
|
1920
|
+
Schema.prototype.set = function set(key, value) {
|
|
1921
|
+
this.dict[key] = value;
|
|
1922
|
+
return this;
|
|
1923
|
+
};
|
|
1924
|
+
Schema.prototype.push = function push(value) {
|
|
1925
|
+
this.list.push(value);
|
|
1926
|
+
return this;
|
|
1927
|
+
};
|
|
1928
|
+
function mergeDesc(original, messages) {
|
|
1929
|
+
const result = typeof original === "string" ? { "": original } : { ...original };
|
|
1930
|
+
for (const locale in messages) {
|
|
1931
|
+
const value = messages[locale];
|
|
1932
|
+
if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
|
|
1933
|
+
else if (typeof value === "string") result[locale] = value;
|
|
1934
|
+
}
|
|
1935
|
+
return result;
|
|
1936
|
+
}
|
|
1937
|
+
function getInner(value) {
|
|
1938
|
+
return value?.$value ?? value?.$inner;
|
|
1939
|
+
}
|
|
1940
|
+
function extractKeys(data) {
|
|
1941
|
+
return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
|
|
1942
|
+
}
|
|
1943
|
+
Schema.prototype.i18n = function i18n(messages) {
|
|
1944
|
+
const schema = Schema(this);
|
|
1945
|
+
const desc = mergeDesc(schema.meta.description, messages);
|
|
1946
|
+
if (Object.keys(desc).length) schema.meta.description = desc;
|
|
1947
|
+
if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
|
|
1948
|
+
return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
|
|
1949
|
+
});
|
|
1950
|
+
if (schema.list) schema.list = schema.list.map((inner, index) => {
|
|
1951
|
+
return inner.i18n(mapValues(messages, (data = {}) => {
|
|
1952
|
+
if (Array.isArray(getInner(data))) return getInner(data)[index];
|
|
1953
|
+
if (Array.isArray(data)) return data[index];
|
|
1954
|
+
return extractKeys(data);
|
|
1955
|
+
}));
|
|
1956
|
+
});
|
|
1957
|
+
if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
|
|
1958
|
+
if (getInner(data)) return getInner(data);
|
|
1959
|
+
return extractKeys(data);
|
|
1960
|
+
}));
|
|
1961
|
+
if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
|
|
1962
|
+
return schema;
|
|
1963
|
+
};
|
|
1964
|
+
Schema.prototype.extra = function extra(key, value) {
|
|
1965
|
+
const schema = Schema(this);
|
|
1966
|
+
schema.meta = {
|
|
1967
|
+
...schema.meta,
|
|
1968
|
+
[key]: value
|
|
1969
|
+
};
|
|
1970
|
+
return schema;
|
|
1971
|
+
};
|
|
1972
|
+
for (const key of [
|
|
1973
|
+
"required",
|
|
1974
|
+
"disabled",
|
|
1975
|
+
"collapse",
|
|
1976
|
+
"hidden",
|
|
1977
|
+
"loose"
|
|
1978
|
+
]) Object.assign(Schema.prototype, { [key](value = true) {
|
|
1979
|
+
const schema = Schema(this);
|
|
1980
|
+
schema.meta = {
|
|
1981
|
+
...schema.meta,
|
|
1982
|
+
[key]: value
|
|
1983
|
+
};
|
|
1984
|
+
return schema;
|
|
1985
|
+
} });
|
|
1986
|
+
Schema.prototype.deprecated = function deprecated() {
|
|
1987
|
+
const schema = Schema(this);
|
|
1988
|
+
schema.meta.badges ||= [];
|
|
1989
|
+
schema.meta.badges.push({
|
|
1990
|
+
text: "deprecated",
|
|
1991
|
+
type: "danger"
|
|
1992
|
+
});
|
|
1993
|
+
return schema;
|
|
1994
|
+
};
|
|
1995
|
+
Schema.prototype.experimental = function experimental() {
|
|
1996
|
+
const schema = Schema(this);
|
|
1997
|
+
schema.meta.badges ||= [];
|
|
1998
|
+
schema.meta.badges.push({
|
|
1999
|
+
text: "experimental",
|
|
2000
|
+
type: "warning"
|
|
2001
|
+
});
|
|
2002
|
+
return schema;
|
|
2003
|
+
};
|
|
2004
|
+
Schema.prototype.pattern = function pattern(regexp) {
|
|
2005
|
+
const schema = Schema(this);
|
|
2006
|
+
const pattern = pick(regexp, ["source", "flags"]);
|
|
2007
|
+
schema.meta = {
|
|
2008
|
+
...schema.meta,
|
|
2009
|
+
pattern
|
|
2010
|
+
};
|
|
2011
|
+
return schema;
|
|
2012
|
+
};
|
|
2013
|
+
Schema.prototype.simplify = function simplify(value) {
|
|
2014
|
+
if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
|
|
2015
|
+
if (isNullable(value)) return value;
|
|
2016
|
+
if (this.type === "object" || this.type === "dict") {
|
|
2017
|
+
const result = {};
|
|
2018
|
+
for (const key in value) {
|
|
2019
|
+
const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
|
|
2020
|
+
if (this.type === "dict" || !isNullable(item)) result[key] = item;
|
|
2021
|
+
}
|
|
2022
|
+
if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
|
|
2023
|
+
return result;
|
|
2024
|
+
} else if (this.type === "array" || this.type === "tuple") {
|
|
2025
|
+
const result = [];
|
|
2026
|
+
value.forEach((value, index) => {
|
|
2027
|
+
const schema = this.type === "array" ? this.inner : this.list[index];
|
|
2028
|
+
const item = schema ? schema.simplify(value) : value;
|
|
2029
|
+
result.push(item);
|
|
2030
|
+
});
|
|
2031
|
+
return result;
|
|
2032
|
+
} else if (this.type === "intersect") {
|
|
2033
|
+
const result = {};
|
|
2034
|
+
for (const item of this.list) Object.assign(result, item.simplify(value));
|
|
2035
|
+
return result;
|
|
2036
|
+
} else if (this.type === "union") for (const schema of this.list) try {
|
|
2037
|
+
Schema.resolve(value, schema, {});
|
|
2038
|
+
return schema.simplify(value);
|
|
2039
|
+
} catch {}
|
|
2040
|
+
return value;
|
|
2041
|
+
};
|
|
2042
|
+
Schema.prototype.toString = function toString(inline) {
|
|
2043
|
+
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
|
|
2044
|
+
};
|
|
2045
|
+
Schema.prototype.role = function role(role, extra) {
|
|
2046
|
+
const schema = Schema(this);
|
|
2047
|
+
schema.meta = {
|
|
2048
|
+
...schema.meta,
|
|
2049
|
+
role,
|
|
2050
|
+
extra
|
|
2051
|
+
};
|
|
2052
|
+
return schema;
|
|
2053
|
+
};
|
|
2054
|
+
for (const key of [
|
|
2055
|
+
"default",
|
|
2056
|
+
"link",
|
|
2057
|
+
"comment",
|
|
2058
|
+
"description",
|
|
2059
|
+
"max",
|
|
2060
|
+
"min",
|
|
2061
|
+
"step"
|
|
2062
|
+
]) Object.assign(Schema.prototype, { [key](value) {
|
|
2063
|
+
const schema = Schema(this);
|
|
2064
|
+
schema.meta = {
|
|
2065
|
+
...schema.meta,
|
|
2066
|
+
[key]: value
|
|
2067
|
+
};
|
|
2068
|
+
return schema;
|
|
2069
|
+
} });
|
|
2070
|
+
const resolvers = {};
|
|
2071
|
+
Schema.extend = function extend(type, resolve) {
|
|
2072
|
+
resolvers[type] = resolve;
|
|
2073
|
+
};
|
|
2074
|
+
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
|
|
2075
|
+
if (!schema) return [data];
|
|
2076
|
+
if (options.ignore?.(data, schema)) return [data];
|
|
2077
|
+
if (isNullable(data) && schema.type !== "lazy") {
|
|
2078
|
+
if (schema.meta.required) throw new ValidationError(`missing required value`, options);
|
|
2079
|
+
let current = schema;
|
|
2080
|
+
let fallback = schema.meta.default;
|
|
2081
|
+
while (current?.type === "intersect" && isNullable(fallback)) {
|
|
2082
|
+
current = current.list[0];
|
|
2083
|
+
fallback = current?.meta.default;
|
|
2084
|
+
}
|
|
2085
|
+
if (isNullable(fallback)) return [data];
|
|
2086
|
+
data = clone(fallback);
|
|
2087
|
+
}
|
|
2088
|
+
const callback = resolvers[schema.type];
|
|
2089
|
+
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
|
|
2090
|
+
try {
|
|
2091
|
+
return callback(data, schema, options, strict);
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
if (!schema.meta.loose) throw error;
|
|
2094
|
+
return [schema.meta.default];
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
Schema.from = function from(source) {
|
|
2098
|
+
if (isNullable(source)) return Schema.any();
|
|
2099
|
+
else if ([
|
|
2100
|
+
"string",
|
|
2101
|
+
"number",
|
|
2102
|
+
"boolean"
|
|
2103
|
+
].includes(typeof source)) return Schema.const(source).required();
|
|
2104
|
+
else if (source[kSchema]) return source;
|
|
2105
|
+
else if (typeof source === "function") switch (source) {
|
|
2106
|
+
case String: return Schema.string().required();
|
|
2107
|
+
case Number: return Schema.number().required();
|
|
2108
|
+
case Boolean: return Schema.boolean().required();
|
|
2109
|
+
case Function: return Schema.function().required();
|
|
2110
|
+
default: return Schema.is(source).required();
|
|
2111
|
+
}
|
|
2112
|
+
else throw new TypeError(`cannot infer schema from ${source}`);
|
|
2113
|
+
};
|
|
2114
|
+
Schema.lazy = function lazy(builder) {
|
|
2115
|
+
const toJSON = () => {
|
|
2116
|
+
if (!schema.inner[kSchema]) {
|
|
2117
|
+
schema.inner = schema.builder();
|
|
2118
|
+
schema.inner.meta = {
|
|
2119
|
+
...schema.meta,
|
|
2120
|
+
...schema.inner.meta
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
return schema.inner.toJSON();
|
|
2124
|
+
};
|
|
2125
|
+
const schema = new Schema({
|
|
2126
|
+
type: "lazy",
|
|
2127
|
+
builder,
|
|
2128
|
+
inner: { toJSON }
|
|
2129
|
+
});
|
|
2130
|
+
return schema;
|
|
2131
|
+
};
|
|
2132
|
+
Schema.natural = function natural() {
|
|
2133
|
+
return Schema.number().step(1).min(0);
|
|
2134
|
+
};
|
|
2135
|
+
Schema.percent = function percent() {
|
|
2136
|
+
return Schema.number().step(.01).min(0).max(1).role("slider");
|
|
2137
|
+
};
|
|
2138
|
+
Schema.date = function date() {
|
|
2139
|
+
return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
|
|
2140
|
+
const date = new Date(value);
|
|
2141
|
+
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
|
|
2142
|
+
return date;
|
|
2143
|
+
}, true)]);
|
|
2144
|
+
};
|
|
2145
|
+
Schema.regExp = function regExp(flag = "") {
|
|
2146
|
+
return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
|
|
2147
|
+
try {
|
|
2148
|
+
return new RegExp(value, flag);
|
|
2149
|
+
} catch (e) {
|
|
2150
|
+
throw new ValidationError(e.message, options);
|
|
2151
|
+
}
|
|
2152
|
+
}, true)]);
|
|
2153
|
+
};
|
|
2154
|
+
Schema.arrayBuffer = function arrayBuffer(encoding) {
|
|
2155
|
+
return Schema.union([
|
|
2156
|
+
Schema.is(ArrayBuffer),
|
|
2157
|
+
Schema.is(SharedArrayBuffer),
|
|
2158
|
+
Schema.transform(Schema.any(), (value, options) => {
|
|
2159
|
+
if (Binary.isSource(value)) return Binary.fromSource(value);
|
|
2160
|
+
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
|
|
2161
|
+
}, true),
|
|
2162
|
+
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
|
2163
|
+
try {
|
|
2164
|
+
return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
|
|
2165
|
+
} catch (e) {
|
|
2166
|
+
throw new ValidationError(e.message, options);
|
|
2167
|
+
}
|
|
2168
|
+
}, true)] : []
|
|
2169
|
+
]);
|
|
2170
|
+
};
|
|
2171
|
+
Schema.extend("lazy", (data, schema, options, strict) => {
|
|
2172
|
+
if (!schema.inner[kSchema]) {
|
|
2173
|
+
schema.inner = schema.builder();
|
|
2174
|
+
schema.inner.meta = {
|
|
2175
|
+
...schema.meta,
|
|
2176
|
+
...schema.inner.meta
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
return Schema.resolve(data, schema.inner, options, strict);
|
|
2180
|
+
});
|
|
2181
|
+
Schema.extend("any", (data) => {
|
|
2182
|
+
return [data];
|
|
2183
|
+
});
|
|
2184
|
+
Schema.extend("never", (data, _, options) => {
|
|
2185
|
+
throw new ValidationError(`expected nullable but got ${data}`, options);
|
|
2186
|
+
});
|
|
2187
|
+
Schema.extend("const", (data, { value }, options) => {
|
|
2188
|
+
if (deepEqual(data, value)) return [value];
|
|
2189
|
+
throw new ValidationError(`expected ${value} but got ${data}`, options);
|
|
2190
|
+
});
|
|
2191
|
+
function checkWithinRange(data, meta, description, options, skipMin = false) {
|
|
2192
|
+
const { max = Infinity, min = -Infinity } = meta;
|
|
2193
|
+
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
|
|
2194
|
+
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
|
|
2195
|
+
}
|
|
2196
|
+
Schema.extend("string", (data, { meta }, options) => {
|
|
2197
|
+
if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
|
|
2198
|
+
if (meta.pattern) {
|
|
2199
|
+
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
|
|
2200
|
+
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
|
|
2201
|
+
}
|
|
2202
|
+
checkWithinRange(data.length, meta, "string length", options);
|
|
2203
|
+
return [data];
|
|
2204
|
+
});
|
|
2205
|
+
function decimalShift(data, digits) {
|
|
2206
|
+
const str = data.toString();
|
|
2207
|
+
if (str.includes("e")) return data * Math.pow(10, digits);
|
|
2208
|
+
const index = str.indexOf(".");
|
|
2209
|
+
if (index === -1) return data * Math.pow(10, digits);
|
|
2210
|
+
const frac = str.slice(index + 1);
|
|
2211
|
+
const integer = str.slice(0, index);
|
|
2212
|
+
if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
|
|
2213
|
+
return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
|
|
2214
|
+
}
|
|
2215
|
+
function isMultipleOf(data, min, step) {
|
|
2216
|
+
step = Math.abs(step);
|
|
2217
|
+
if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
|
|
2218
|
+
const index = step.toString().indexOf(".");
|
|
2219
|
+
const digits = step.toString().slice(index + 1).length;
|
|
2220
|
+
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
|
|
2221
|
+
}
|
|
2222
|
+
Schema.extend("number", (data, { meta }, options) => {
|
|
2223
|
+
if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
|
|
2224
|
+
checkWithinRange(data, meta, "number", options);
|
|
2225
|
+
const { step } = meta;
|
|
2226
|
+
if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
|
|
2227
|
+
return [data];
|
|
2228
|
+
});
|
|
2229
|
+
Schema.extend("boolean", (data, _, options) => {
|
|
2230
|
+
if (typeof data === "boolean") return [data];
|
|
2231
|
+
throw new ValidationError(`expected boolean but got ${data}`, options);
|
|
2232
|
+
});
|
|
2233
|
+
Schema.extend("bitset", (data, { bits, meta }, options) => {
|
|
2234
|
+
let value = 0, keys = [];
|
|
2235
|
+
if (typeof data === "number") {
|
|
2236
|
+
value = data;
|
|
2237
|
+
for (const key in bits) if (data & bits[key]) keys.push(key);
|
|
2238
|
+
} else if (Array.isArray(data)) {
|
|
2239
|
+
keys = data;
|
|
2240
|
+
for (const key of keys) {
|
|
2241
|
+
if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
|
|
2242
|
+
if (key in bits) value |= bits[key];
|
|
2243
|
+
}
|
|
2244
|
+
} else throw new ValidationError(`expected number or array but got ${data}`, options);
|
|
2245
|
+
if (value === meta.default) return [value];
|
|
2246
|
+
return [value, keys];
|
|
2247
|
+
});
|
|
2248
|
+
Schema.extend("function", (data, _, options) => {
|
|
2249
|
+
if (typeof data === "function") return [data];
|
|
2250
|
+
throw new ValidationError(`expected function but got ${data}`, options);
|
|
2251
|
+
});
|
|
2252
|
+
Schema.extend("is", (data, { constructor }, options) => {
|
|
2253
|
+
if (typeof constructor === "function") {
|
|
2254
|
+
if (data instanceof constructor) return [data];
|
|
2255
|
+
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
|
|
2256
|
+
} else {
|
|
2257
|
+
if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
2258
|
+
let prototype = Object.getPrototypeOf(data);
|
|
2259
|
+
while (prototype) {
|
|
2260
|
+
if (prototype.constructor?.name === constructor) return [data];
|
|
2261
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
2262
|
+
}
|
|
2263
|
+
throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
function property(data, key, schema, options) {
|
|
2267
|
+
try {
|
|
2268
|
+
const [value, adapted] = Schema.resolve(data[key], schema, {
|
|
2269
|
+
...options,
|
|
2270
|
+
path: [...options.path || [], key]
|
|
2271
|
+
});
|
|
2272
|
+
if (adapted !== void 0) data[key] = adapted;
|
|
2273
|
+
return value;
|
|
2274
|
+
} catch (e) {
|
|
2275
|
+
if (!options?.autofix) throw e;
|
|
2276
|
+
delete data[key];
|
|
2277
|
+
return schema.meta.default;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
Schema.extend("array", (data, { inner, meta }, options) => {
|
|
2281
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
2282
|
+
checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
|
|
2283
|
+
return [data.map((_, index) => property(data, index, inner, options))];
|
|
2284
|
+
});
|
|
2285
|
+
Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
|
|
2286
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
2287
|
+
const result = {};
|
|
2288
|
+
for (const key in data) {
|
|
2289
|
+
let rKey;
|
|
2290
|
+
try {
|
|
2291
|
+
rKey = Schema.resolve(key, sKey, options)[0];
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
if (strict) continue;
|
|
2294
|
+
throw error;
|
|
2295
|
+
}
|
|
2296
|
+
result[rKey] = property(data, key, inner, options);
|
|
2297
|
+
data[rKey] = data[key];
|
|
2298
|
+
if (key !== rKey) delete data[key];
|
|
2299
|
+
}
|
|
2300
|
+
return [result];
|
|
2301
|
+
});
|
|
2302
|
+
Schema.extend("tuple", (data, { list }, options, strict) => {
|
|
2303
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
2304
|
+
const result = list.map((inner, index) => property(data, index, inner, options));
|
|
2305
|
+
if (strict) return [result];
|
|
2306
|
+
result.push(...data.slice(list.length));
|
|
2307
|
+
return [result];
|
|
2308
|
+
});
|
|
2309
|
+
function merge(result, data) {
|
|
2310
|
+
for (const key in data) {
|
|
2311
|
+
if (key in result) continue;
|
|
2312
|
+
result[key] = data[key];
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
Schema.extend("object", (data, { dict }, options, strict) => {
|
|
2316
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
2317
|
+
const result = {};
|
|
2318
|
+
for (const key in dict) {
|
|
2319
|
+
const value = property(data, key, dict[key], options);
|
|
2320
|
+
if (!isNullable(value) || key in data) result[key] = value;
|
|
2321
|
+
}
|
|
2322
|
+
if (!strict) merge(result, data);
|
|
2323
|
+
return [result];
|
|
2324
|
+
});
|
|
2325
|
+
Schema.extend("union", (data, { list, toString }, options, strict) => {
|
|
2326
|
+
const messages = [];
|
|
2327
|
+
for (const inner of list) try {
|
|
2328
|
+
return Schema.resolve(data, inner, options, strict);
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
messages.push(error);
|
|
2331
|
+
}
|
|
2332
|
+
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2333
|
+
});
|
|
2334
|
+
Schema.extend("intersect", (data, { list, toString }, options, strict) => {
|
|
2335
|
+
if (!list.length) return [data];
|
|
2336
|
+
let result;
|
|
2337
|
+
for (const inner of list) {
|
|
2338
|
+
const value = Schema.resolve(data, inner, options, true)[0];
|
|
2339
|
+
if (isNullable(value)) continue;
|
|
2340
|
+
if (isNullable(result)) result = value;
|
|
2341
|
+
else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2342
|
+
else if (typeof value === "object") merge(result ??= {}, value);
|
|
2343
|
+
else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2344
|
+
}
|
|
2345
|
+
if (!strict && isPlainObject(data)) merge(result, data);
|
|
2346
|
+
return [result];
|
|
2347
|
+
});
|
|
2348
|
+
Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
|
|
2349
|
+
const [result, adapted = data] = Schema.resolve(data, inner, options, true);
|
|
2350
|
+
if (preserve) return [callback(result)];
|
|
2351
|
+
else return [callback(result), callback(adapted)];
|
|
2352
|
+
});
|
|
2353
|
+
const formatters = {};
|
|
2354
|
+
function defineMethod(name, keys, format) {
|
|
2355
|
+
formatters[name] = format;
|
|
2356
|
+
Object.assign(Schema, { [name](...args) {
|
|
2357
|
+
const schema = new Schema({ type: name });
|
|
2358
|
+
keys.forEach((key, index) => {
|
|
2359
|
+
switch (key) {
|
|
2360
|
+
case "sKey":
|
|
2361
|
+
schema.sKey = args[index] ?? Schema.string();
|
|
2362
|
+
break;
|
|
2363
|
+
case "inner":
|
|
2364
|
+
schema.inner = Schema.from(args[index]);
|
|
2365
|
+
break;
|
|
2366
|
+
case "list":
|
|
2367
|
+
schema.list = args[index].map(Schema.from);
|
|
2368
|
+
break;
|
|
2369
|
+
case "dict":
|
|
2370
|
+
schema.dict = mapValues(args[index], Schema.from);
|
|
2371
|
+
break;
|
|
2372
|
+
case "bits":
|
|
2373
|
+
schema.bits = {};
|
|
2374
|
+
for (const key in args[index]) {
|
|
2375
|
+
if (typeof args[index][key] !== "number") continue;
|
|
2376
|
+
schema.bits[key] = args[index][key];
|
|
2377
|
+
}
|
|
2378
|
+
break;
|
|
2379
|
+
case "callback": {
|
|
2380
|
+
const callback = schema.callback = args[index];
|
|
2381
|
+
callback["toJSON"] ||= () => callback.toString();
|
|
2382
|
+
break;
|
|
2383
|
+
}
|
|
2384
|
+
case "constructor": {
|
|
2385
|
+
const constructor = schema.constructor = args[index];
|
|
2386
|
+
if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
|
|
2387
|
+
break;
|
|
2388
|
+
}
|
|
2389
|
+
default: schema[key] = args[index];
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
if (name === "object" || name === "dict") schema.meta.default = {};
|
|
2393
|
+
else if (name === "array" || name === "tuple") schema.meta.default = [];
|
|
2394
|
+
else if (name === "bitset") schema.meta.default = 0;
|
|
2395
|
+
return schema;
|
|
2396
|
+
} });
|
|
2397
|
+
}
|
|
2398
|
+
defineMethod("is", ["constructor"], ({ constructor }) => {
|
|
2399
|
+
if (typeof constructor === "function") return constructor.name;
|
|
2400
|
+
else return constructor;
|
|
2401
|
+
});
|
|
2402
|
+
defineMethod("any", [], () => "any");
|
|
2403
|
+
defineMethod("never", [], () => "never");
|
|
2404
|
+
defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
|
|
2405
|
+
defineMethod("string", [], () => "string");
|
|
2406
|
+
defineMethod("number", [], () => "number");
|
|
2407
|
+
defineMethod("boolean", [], () => "boolean");
|
|
2408
|
+
defineMethod("bitset", ["bits"], () => "bitset");
|
|
2409
|
+
defineMethod("function", [], () => "function");
|
|
2410
|
+
defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
|
|
2411
|
+
defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
|
|
2412
|
+
defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
|
|
2413
|
+
defineMethod("object", ["dict"], ({ dict }) => {
|
|
2414
|
+
if (Object.keys(dict).length === 0) return "{}";
|
|
2415
|
+
return `{ ${Object.entries(dict).map(([key, inner]) => {
|
|
2416
|
+
return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
|
|
2417
|
+
}).join(", ")} }`;
|
|
2418
|
+
});
|
|
2419
|
+
defineMethod("union", ["list"], ({ list }, inline) => {
|
|
2420
|
+
const result = list.map(({ toString: format }) => format()).join(" | ");
|
|
2421
|
+
return inline ? `(${result})` : result;
|
|
2422
|
+
});
|
|
2423
|
+
defineMethod("intersect", ["list"], ({ list }) => {
|
|
2424
|
+
return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
|
|
2425
|
+
});
|
|
2426
|
+
defineMethod("transform", [
|
|
2427
|
+
"inner",
|
|
2428
|
+
"callback",
|
|
2429
|
+
"preserve"
|
|
2430
|
+
], ({ inner }, isInner) => inner.toString(isInner));
|
|
2431
|
+
//#endregion
|
|
2432
|
+
//#region src/settings.ts
|
|
2433
|
+
/**
|
|
2434
|
+
* The `soulmirror` user-settings namespace (dsh `ctx.settings`): what the
|
|
2435
|
+
* browser settings section "SoulMirror network" edits and what the network
|
|
2436
|
+
* plugin reads when it spawns the backend. The connection fields apply on
|
|
2437
|
+
* the next plugin (re)load — the peer process is spawned with them
|
|
2438
|
+
* (`applies: 'restart'`); the alter fields (`defaultTier`, `autoReplyPerHour`,
|
|
2439
|
+
* `directSend`) are read live through `ctx.soulmirrorConfig.current()`. The
|
|
2440
|
+
* tiers decide how the alter handles a friend's mail (P4: `draft` = the
|
|
2441
|
+
* alter's reply waits as a pending draft on the SoulMirror page).
|
|
2442
|
+
*
|
|
2443
|
+
* `@deepseek-ai/schemastery` is a VALUE import here on purpose: the settings
|
|
2444
|
+
* seam needs a real schemastery schema (callable validator + `toJSON()` for
|
|
2445
|
+
* the browser form). It is a vendored, dependency-free library and gets
|
|
2446
|
+
* inlined into lib/index.js by tsdown, so the host half still has zero
|
|
2447
|
+
* `@deepseek-ai/*` runtime edges into the harness instance (see ../README.md).
|
|
2448
|
+
*/
|
|
2449
|
+
const SETTINGS_NAMESPACE = "soulmirror";
|
|
2450
|
+
const SOULMIRROR_SETTINGS_SCHEMA = Schema.object({
|
|
2451
|
+
relay: Schema.string().default(DEFAULT_RELAY).description("Relay URL (used when the identity is created)."),
|
|
2452
|
+
displayName: Schema.string().default("").description("Display name for a new identity (first start only)."),
|
|
2453
|
+
backend: Schema.union([Schema.const("soulnet"), Schema.const("fake")]).default("soulnet").description("soulnet = the light peer binary; fake = in-memory test backend."),
|
|
2454
|
+
peerBinary: Schema.string().default("").description("Path of the soulnet binary; empty = PATH, then the plugin bin/ directory."),
|
|
2455
|
+
home: Schema.string().default("").description("Data directory; empty = $SOULNET_HOME, then ~/.soulnet."),
|
|
2456
|
+
defaultTier: Schema.union([
|
|
2457
|
+
Schema.const("notify"),
|
|
2458
|
+
Schema.const("draft"),
|
|
2459
|
+
Schema.const("auto")
|
|
2460
|
+
]).default(DEFAULT_REPLY_TIER).description("Default reply tier for friends: notify = mail is only shown; draft = the alter drafts a reply you review on the SoulMirror page; auto = the alter replies by itself (rate-limited)."),
|
|
2461
|
+
autoReplyPerHour: Schema.number().default(20).description("Maximum automatic replies per friend per hour in the auto tier (0 disables)."),
|
|
2462
|
+
directSend: Schema.boolean().default(false).description("Debug: offer \"Send as myself\" in a friend thread (bypasses the alter); off by default.")
|
|
2463
|
+
});
|
|
2464
|
+
/** Fill in defaults for a partial section (plugin config or a stored user section). */
|
|
2465
|
+
function resolveSettings(partial) {
|
|
2466
|
+
const perHour = typeof partial?.autoReplyPerHour === "number" && Number.isFinite(partial.autoReplyPerHour) ? Math.max(0, Math.floor(partial.autoReplyPerHour)) : 20;
|
|
2467
|
+
return {
|
|
2468
|
+
relay: partial?.relay !== void 0 && partial.relay.trim() !== "" ? partial.relay.trim() : DEFAULT_RELAY,
|
|
2469
|
+
displayName: partial?.displayName ?? "",
|
|
2470
|
+
backend: partial?.backend === "fake" ? "fake" : "soulnet",
|
|
2471
|
+
peerBinary: partial?.peerBinary ?? "",
|
|
2472
|
+
home: partial?.home ?? "",
|
|
2473
|
+
defaultTier: normalizeTier(partial?.defaultTier),
|
|
2474
|
+
autoReplyPerHour: perHour,
|
|
2475
|
+
directSend: partial?.directSend === true
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
//#endregion
|
|
2479
|
+
//#region src/index.ts
|
|
2480
|
+
const name = "soulmirror-network";
|
|
2481
|
+
const inject = [];
|
|
2482
|
+
function apply(ctx, config = {}) {
|
|
2483
|
+
const log = (level, message) => {
|
|
2484
|
+
ctx.logger[level](`soulmirror-network: ${message}`);
|
|
2485
|
+
};
|
|
2486
|
+
const entry = resolveSettings(config);
|
|
2487
|
+
let effective = entry;
|
|
2488
|
+
let live = entry;
|
|
2489
|
+
const settingsNow = ctx.get("settings");
|
|
2490
|
+
if (settingsNow !== void 0) {
|
|
2491
|
+
const scope = settingsNow.register(SETTINGS_NAMESPACE, SOULMIRROR_SETTINGS_SCHEMA, {
|
|
2492
|
+
base: config,
|
|
2493
|
+
applies: "restart"
|
|
2494
|
+
});
|
|
2495
|
+
effective = resolveSettings(scope.get());
|
|
2496
|
+
live = effective;
|
|
2497
|
+
scope.watch(() => {
|
|
2498
|
+
live = resolveSettings(scope.get());
|
|
2499
|
+
log("info", `settings changed (tier=${live.defaultTier}, autoReplyPerHour=${live.autoReplyPerHour}, directSend=${String(live.directSend)} apply now; connection fields apply when the plugin reloads)`);
|
|
2500
|
+
});
|
|
2501
|
+
} else ctx.inject(["settings"], (sctx) => {
|
|
2502
|
+
const scope = sctx.settings.register(SETTINGS_NAMESPACE, SOULMIRROR_SETTINGS_SCHEMA, {
|
|
2503
|
+
base: config,
|
|
2504
|
+
applies: "restart"
|
|
2505
|
+
});
|
|
2506
|
+
live = resolveSettings(scope.get());
|
|
2507
|
+
scope.watch(() => {
|
|
2508
|
+
live = resolveSettings(scope.get());
|
|
2509
|
+
log("info", "settings changed; alter fields apply now, connection fields when the plugin reloads");
|
|
2510
|
+
});
|
|
2511
|
+
});
|
|
2512
|
+
ctx.provide("soulmirrorConfig", { current: () => live });
|
|
2513
|
+
const home = effective.home !== "" ? effective.home : defaultSoulnetHome();
|
|
2514
|
+
let client;
|
|
2515
|
+
if (effective.backend === "fake") client = createFakeNetworkClient();
|
|
2516
|
+
else {
|
|
2517
|
+
const peer = createSoulnetNetworkClient({
|
|
2518
|
+
home,
|
|
2519
|
+
relay: effective.relay,
|
|
2520
|
+
displayName: effective.displayName,
|
|
2521
|
+
...effective.peerBinary === "" ? {} : { peerBinary: effective.peerBinary },
|
|
2522
|
+
logger: log
|
|
2523
|
+
});
|
|
2524
|
+
peer.start();
|
|
2525
|
+
client = peer;
|
|
2526
|
+
}
|
|
2527
|
+
ctx.provide("soulmirrorHome", home);
|
|
2528
|
+
ctx.provide("soulmirror", client);
|
|
2529
|
+
ctx.effect(() => () => {
|
|
2530
|
+
client.dispose().catch((error) => {
|
|
2531
|
+
log("warn", `dispose failed: ${String(error)}`);
|
|
2532
|
+
});
|
|
2533
|
+
}, "soulmirror-network: backend process");
|
|
2534
|
+
mountApi(ctx, {
|
|
2535
|
+
client,
|
|
2536
|
+
home,
|
|
2537
|
+
settingsNamespace: SETTINGS_NAMESPACE,
|
|
2538
|
+
sessions: () => ctx.get("soulmirrorSessions"),
|
|
2539
|
+
settings: () => live,
|
|
2540
|
+
log
|
|
2541
|
+
});
|
|
2542
|
+
log("info", `backend=${client.backend} home=${home} relay=${effective.relay}`);
|
|
2543
|
+
}
|
|
2544
|
+
//#endregion
|
|
2545
|
+
export { RELAY_FORM, SETTINGS_NAMESPACE, SOULMIRROR_PLUGIN, apply, inject, name };
|
|
2546
|
+
|
|
2547
|
+
//# sourceMappingURL=index.js.map
|