volute 0.24.0 → 0.26.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/README.md +15 -20
- package/dist/{activity-events-4O37J7PD.js → activity-events-ZMBAKLUF.js} +2 -2
- package/dist/api.d.ts +590 -10
- package/dist/{auth-HM2RSPY7.js → auth-4TV573WE.js} +2 -2
- package/dist/{channel-HZOSHGNF.js → channel-ZVZV42UD.js} +3 -3
- package/dist/{chunk-NOBRGACV.js → chunk-2VO7453N.js} +56 -19
- package/dist/{chunk-OOW675I3.js → chunk-3CFRE2VC.js} +931 -775
- package/dist/{chunk-PHHKNGA3.js → chunk-3TV4GLFO.js} +2 -2
- package/dist/{chunk-4TJ72QQ3.js → chunk-5Y3PBKW6.js} +3 -3
- package/dist/{chunk-BFK6SOEJ.js → chunk-J2CO4WEV.js} +1 -1
- package/dist/{chunk-TQDITGES.js → chunk-LX22GRG7.js} +10 -13
- package/dist/{chunk-E7GOKNOT.js → chunk-NWI2425I.js} +1 -1
- package/dist/{chunk-2767L2RZ.js → chunk-OZFKBXD6.js} +1 -1
- package/dist/{chunk-XLC342FO.js → chunk-SIAG3QMM.js} +14 -1
- package/dist/{chunk-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
- package/dist/chunk-TZKJLDQN.js +78 -0
- package/dist/{chunk-P3W36ZGD.js → chunk-USNBKHYG.js} +33 -5
- package/dist/chunk-UTL75LP6.js +113 -0
- package/dist/{chunk-3AIBT4TW.js → chunk-V63B7DX3.js} +24 -1
- package/dist/{chunk-33XAVCS4.js → chunk-WBHMQ5OZ.js} +49 -0
- package/dist/{chunk-TRQEV3CD.js → chunk-WGOGUMPO.js} +22 -3
- package/dist/chunk-XOXLRRR2.js +176 -0
- package/dist/{chunk-JTDFJWI2.js → chunk-YJA7P64S.js} +1 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/cli.js +44 -20
- package/dist/{cloud-sync-DIU3OCPV.js → cloud-sync-NI2K3C7G.js} +11 -9
- package/dist/{connector-M6XFI6GM.js → connector-G722WXAU.js} +4 -4
- package/dist/{create-VDQJER52.js → create-4YBRTTJS.js} +1 -1
- package/dist/{daemon-client-JOVQZ52X.js → daemon-client-Z7FAJ6JW.js} +1 -1
- package/dist/{daemon-restart-YMPEATQH.js → daemon-restart-BJZ3O4U4.js} +6 -5
- package/dist/daemon.js +982 -340
- package/dist/{delete-2MRR4JX5.js → delete-27OYNK25.js} +1 -1
- package/dist/{down-674SX2IZ.js → down-7UKFMJJZ.js} +4 -4
- package/dist/{env-2FPOZK37.js → env-M336ONDP.js} +4 -4
- package/dist/{export-IKFAPRAO.js → export-HP4G5DQC.js} +1 -1
- package/dist/{file-KT3UIQM3.js → file-HUDKTRAS.js} +3 -3
- package/dist/{history-46WZN5CN.js → history-B64GTFTD.js} +3 -3
- package/dist/{import-FRDPQPJ2.js → import-XIB7UV4S.js} +2 -2
- package/dist/{log-6SGSSR3D.js → log-PBFNILJ4.js} +3 -3
- package/dist/{login-UO6AOVEA.js → login-6U7U6BNG.js} +1 -1
- package/dist/login-B5E7N7MY.js +46 -0
- package/dist/logout-XSJRYS3U.js +39 -0
- package/dist/{logs-HRBONI5I.js → logs-3CART7O7.js} +3 -3
- package/dist/{merge-KSFJKX6T.js → merge-VK2HSKMA.js} +3 -3
- package/dist/{message-delivery-S7BCNV6Y.js → message-delivery-MS5JYPZX.js} +11 -9
- package/dist/{mind-KPLCRKQA.js → mind-HZ3QSDDJ.js} +17 -17
- package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-4G6FURY2.js} +3 -3
- package/dist/{mind-manager-ZNRIYEK3.js → mind-manager-VVK67AY3.js} +6 -4
- package/dist/{mind-sleep-GHPTSAYN.js → mind-sleep-DTV7L44D.js} +3 -3
- package/dist/{mind-wake-BJDJFMDF.js → mind-wake-PFN4FN3T.js} +3 -3
- package/dist/notes-37FW2UR2.js +230 -0
- package/dist/{package-S5YF25XV.js → package-VZWLXPHV.js} +3 -1
- package/dist/{pages-TWR6U7DS.js → pages-DIIT5HMQ.js} +1 -1
- package/dist/{publish-BZNHKUUK.js → publish-HQV7YREB.js} +4 -4
- package/dist/{pull-D32SPFVU.js → pull-2MB4SK3C.js} +3 -3
- package/dist/{register-U2UO6TC4.js → register-EFND67FQ.js} +1 -1
- package/dist/{restart-5BMNV7KU.js → restart-CCK7D6TV.js} +3 -3
- package/dist/sandbox-EHGFF52K.js +19 -0
- package/dist/{schedule-YEFDLVMJ.js → schedule-6F7ELB2M.js} +3 -3
- package/dist/{seed-6FEKB3YC.js → seed-E5OQGWX3.js} +1 -1
- package/dist/{send-IISDYFCL.js → send-IH6XZKPC.js} +6 -20
- package/dist/service-LLBV3R7M.js +122 -0
- package/dist/setup-F6TWFYGQ.js +371 -0
- package/dist/setup-YGAAIKKZ.js +17 -0
- package/dist/{shared-LWMNTTZN.js → shared-UMO4S7CC.js} +4 -4
- package/dist/{skill-BQOFACEI.js → skill-42LGFBQC.js} +13 -5
- package/dist/skills/dreaming/SKILL.md +68 -0
- package/dist/skills/dreaming/references/INSTALL.md +56 -0
- package/dist/skills/dreaming/scripts/dream.ts +289 -0
- package/dist/skills/dreaming/scripts/wake-context-dreams.sh +30 -0
- package/dist/skills/imagegen/SKILL.md +37 -0
- package/dist/skills/imagegen/references/INSTALL.md +13 -0
- package/dist/skills/imagegen/scripts/imagegen.ts +136 -0
- package/dist/skills/notes/SKILL.md +34 -0
- package/dist/skills/resonance/SKILL.md +73 -0
- package/dist/skills/resonance/assets/default-config.json +21 -0
- package/dist/skills/resonance/references/INSTALL.md +23 -0
- package/dist/skills/resonance/scripts/resonance.ts +1250 -0
- package/dist/skills/volute-mind/SKILL.md +23 -3
- package/dist/{sleep-manager-XXSWQQLE.js → sleep-manager-EE4NRN2Q.js} +11 -9
- package/dist/{sprout-CGSW4CF5.js → sprout-QL74KR2X.js} +5 -5
- package/dist/{start-C7XITZ5O.js → start-O5JQASRC.js} +3 -3
- package/dist/{status-SIRPLEZC.js → status-FZBEBM7Q.js} +3 -3
- package/dist/{status-LYS4NUOZ.js → status-WXD4HXRL.js} +3 -3
- package/dist/{stop-CVKBSLXY.js → stop-2SOG5NYF.js} +3 -3
- package/dist/up-SDMCSVI3.js +17 -0
- package/dist/{update-7XCZMYBT.js → update-5VUDAI3D.js} +6 -6
- package/dist/{upgrade-7RUIXGOO.js → upgrade-QCCO33BK.js} +1 -1
- package/dist/{variant-UGREB4G5.js → variant-WWLDY6D5.js} +4 -4
- package/dist/{version-notify-SZ75QRGO.js → version-notify-USFZBWMG.js} +11 -9
- package/dist/web-assets/assets/index-CUQ31ieL.js +69 -0
- package/dist/web-assets/assets/index-CW8NSl1o.css +1 -0
- package/dist/web-assets/favicon.png +0 -0
- package/dist/web-assets/index.html +5 -4
- package/dist/web-assets/logo.png +0 -0
- package/drizzle/0015_notes.sql +23 -0
- package/drizzle/0016_note_reactions_and_replies.sql +15 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +3 -1
- package/templates/_base/.init/.config/hooks/wake-context.sh +7 -0
- package/templates/_base/home/public/.gitkeep +0 -0
- package/templates/_base/src/lib/startup.ts +8 -0
- package/templates/claude/src/agent.ts +51 -1
- package/templates/claude/src/server.ts +1 -0
- package/templates/pi/package.json.tmpl +1 -0
- package/templates/pi/src/agent.ts +48 -1
- package/templates/pi/src/lib/subagents.ts +150 -0
- package/templates/pi/src/server.ts +1 -0
- package/dist/chunk-NWPT4ASZ.js +0 -89
- package/dist/service-FASYWLTC.js +0 -247
- package/dist/setup-BMLM2UTK.js +0 -230
- package/dist/up-OMHACRJL.js +0 -15
- package/dist/web-assets/assets/index-Bx9WDoaQ.js +0 -69
- package/dist/web-assets/assets/index-Clz8OhmJ.css +0 -1
|
@@ -4,26 +4,26 @@ import {
|
|
|
4
4
|
} from "./chunk-HFCBO2GL.js";
|
|
5
5
|
import {
|
|
6
6
|
markIdle
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-NWI2425I.js";
|
|
8
8
|
import {
|
|
9
|
+
broadcast,
|
|
9
10
|
publish,
|
|
10
11
|
subscribe
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-J2CO4WEV.js";
|
|
12
13
|
import {
|
|
13
14
|
RestartTracker,
|
|
14
15
|
RotatingLog,
|
|
15
16
|
clearJsonMap,
|
|
16
17
|
getMindManager,
|
|
18
|
+
getMindToken,
|
|
17
19
|
getPrompt,
|
|
18
20
|
loadJsonMap,
|
|
19
21
|
saveJsonMap
|
|
20
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-2VO7453N.js";
|
|
21
23
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
loadMergedEnv
|
|
26
|
-
} from "./chunk-PHU4DEAJ.js";
|
|
24
|
+
isSandboxEnabled,
|
|
25
|
+
wrapForSandbox
|
|
26
|
+
} from "./chunk-UTL75LP6.js";
|
|
27
27
|
import {
|
|
28
28
|
conversationParticipants,
|
|
29
29
|
conversationReads,
|
|
@@ -33,18 +33,24 @@ import {
|
|
|
33
33
|
messages,
|
|
34
34
|
mindHistory,
|
|
35
35
|
users
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-WBHMQ5OZ.js";
|
|
37
37
|
import {
|
|
38
38
|
logger_default
|
|
39
39
|
} from "./chunk-YUIHSKR6.js";
|
|
40
|
+
import {
|
|
41
|
+
readVoluteConfig
|
|
42
|
+
} from "./chunk-SIAG3QMM.js";
|
|
43
|
+
import {
|
|
44
|
+
loadMergedEnv
|
|
45
|
+
} from "./chunk-PHU4DEAJ.js";
|
|
40
46
|
import {
|
|
41
47
|
exec
|
|
42
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-YJA7P64S.js";
|
|
43
49
|
import {
|
|
44
50
|
chownMindDir,
|
|
45
51
|
isIsolationEnabled,
|
|
46
52
|
wrapForIsolation
|
|
47
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-XOXLRRR2.js";
|
|
48
54
|
import {
|
|
49
55
|
daemonLoopback,
|
|
50
56
|
findMind,
|
|
@@ -56,7 +62,7 @@ import {
|
|
|
56
62
|
} from "./chunk-B2CPS4QU.js";
|
|
57
63
|
|
|
58
64
|
// src/lib/daemon/sleep-manager.ts
|
|
59
|
-
import { execFile } from "child_process";
|
|
65
|
+
import { execFile, spawn as spawnChild } from "child_process";
|
|
60
66
|
import {
|
|
61
67
|
existsSync as existsSync5,
|
|
62
68
|
mkdirSync as mkdirSync3,
|
|
@@ -69,11 +75,11 @@ import {
|
|
|
69
75
|
import { resolve as resolve8 } from "path";
|
|
70
76
|
import { promisify } from "util";
|
|
71
77
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
72
|
-
import { and as and4, eq as eq4, inArray as
|
|
78
|
+
import { and as and4, eq as eq4, inArray as inArray3 } from "drizzle-orm";
|
|
73
79
|
|
|
74
80
|
// src/lib/auth.ts
|
|
75
81
|
import { compareSync, hashSync } from "bcryptjs";
|
|
76
|
-
import { and, count, eq } from "drizzle-orm";
|
|
82
|
+
import { and, count, eq, inArray } from "drizzle-orm";
|
|
77
83
|
var userSelectFields = {
|
|
78
84
|
id: users.id,
|
|
79
85
|
username: users.username,
|
|
@@ -131,7 +137,7 @@ async function getOrCreateMindUser(mindName) {
|
|
|
131
137
|
const [result] = await db.insert(users).values({
|
|
132
138
|
username: mindName,
|
|
133
139
|
password_hash: "!mind",
|
|
134
|
-
role: "
|
|
140
|
+
role: "user",
|
|
135
141
|
user_type: "mind"
|
|
136
142
|
}).returning(userSelectFields);
|
|
137
143
|
return result;
|
|
@@ -185,12 +191,20 @@ async function updateUserProfile(userId, profile) {
|
|
|
185
191
|
}
|
|
186
192
|
async function syncMindProfile(mindName, config) {
|
|
187
193
|
const user = await getOrCreateMindUser(mindName);
|
|
188
|
-
const
|
|
189
|
-
await db.update(users).set({
|
|
194
|
+
const newProfile = {
|
|
190
195
|
display_name: config.displayName ?? null,
|
|
191
196
|
description: config.description ?? null,
|
|
192
197
|
avatar: config.avatar ?? null
|
|
193
|
-
}
|
|
198
|
+
};
|
|
199
|
+
const changed = user.display_name !== newProfile.display_name || user.description !== newProfile.description || user.avatar !== newProfile.avatar;
|
|
200
|
+
if (!changed) return;
|
|
201
|
+
const db = await getDb();
|
|
202
|
+
await db.update(users).set(newProfile).where(eq(users.id, user.id));
|
|
203
|
+
broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
|
|
204
|
+
}
|
|
205
|
+
async function migrateMindRoles() {
|
|
206
|
+
const db = await getDb();
|
|
207
|
+
await db.update(users).set({ role: "user" }).where(and(eq(users.user_type, "mind"), inArray(users.role, ["mind", "agent"])));
|
|
194
208
|
}
|
|
195
209
|
|
|
196
210
|
// src/lib/pages-watcher.ts
|
|
@@ -231,16 +245,16 @@ function startPagesWatcher(mindName, pagesDir) {
|
|
|
231
245
|
}
|
|
232
246
|
function startWatcher(mindName) {
|
|
233
247
|
if (watchers.has(mindName)) return;
|
|
234
|
-
const pagesDir = resolve(mindDir(mindName), "home", "pages");
|
|
248
|
+
const pagesDir = resolve(mindDir(mindName), "home", "public", "pages");
|
|
235
249
|
if (existsSync(pagesDir)) {
|
|
236
250
|
startPagesWatcher(mindName, pagesDir);
|
|
237
251
|
return;
|
|
238
252
|
}
|
|
239
253
|
if (homeWatchers.has(mindName)) return;
|
|
240
|
-
const
|
|
241
|
-
if (!existsSync(
|
|
254
|
+
const publicDir = resolve(mindDir(mindName), "home", "public");
|
|
255
|
+
if (!existsSync(publicDir)) return;
|
|
242
256
|
try {
|
|
243
|
-
const hw = watch(
|
|
257
|
+
const hw = watch(publicDir, (_eventType, filename) => {
|
|
244
258
|
if (filename !== "pages") return;
|
|
245
259
|
if (!existsSync(pagesDir)) return;
|
|
246
260
|
hw.close();
|
|
@@ -337,7 +351,7 @@ function buildSites() {
|
|
|
337
351
|
}
|
|
338
352
|
const entries = readRegistry();
|
|
339
353
|
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
340
|
-
const pagesDir = resolve(mindDir(entry.name), "home", "pages");
|
|
354
|
+
const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
|
|
341
355
|
if (!existsSync(pagesDir)) continue;
|
|
342
356
|
const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
|
|
343
357
|
if (mindPages.length > 0) {
|
|
@@ -350,7 +364,7 @@ function buildRecentPages() {
|
|
|
350
364
|
const entries = readRegistry();
|
|
351
365
|
const pages = [];
|
|
352
366
|
for (const entry of entries) {
|
|
353
|
-
const pagesDir = resolve(mindDir(entry.name), "home", "pages");
|
|
367
|
+
const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
|
|
354
368
|
if (!existsSync(pagesDir)) continue;
|
|
355
369
|
let items;
|
|
356
370
|
try {
|
|
@@ -398,159 +412,605 @@ function getCachedRecentPages() {
|
|
|
398
412
|
return recentPagesCache;
|
|
399
413
|
}
|
|
400
414
|
|
|
401
|
-
// src/lib/
|
|
402
|
-
import {
|
|
403
|
-
import {
|
|
404
|
-
import { dirname, resolve as resolve3 } from "path";
|
|
415
|
+
// src/lib/events/conversations.ts
|
|
416
|
+
import { randomUUID } from "crypto";
|
|
417
|
+
import { and as and2, desc, eq as eq2, inArray as inArray2, isNull, lt, sql } from "drizzle-orm";
|
|
405
418
|
|
|
406
|
-
// src/lib/
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
displayName: "Slack",
|
|
430
|
-
description: "Connect to Slack via Socket Mode",
|
|
431
|
-
envVars: [
|
|
432
|
-
{
|
|
433
|
-
name: "SLACK_BOT_TOKEN",
|
|
434
|
-
required: true,
|
|
435
|
-
description: "Slack bot token (xoxb-...)",
|
|
436
|
-
scope: "mind"
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
name: "SLACK_APP_TOKEN",
|
|
440
|
-
required: true,
|
|
441
|
-
description: "Slack app-level token (xapp-...) for Socket Mode",
|
|
442
|
-
scope: "mind"
|
|
443
|
-
}
|
|
444
|
-
]
|
|
445
|
-
},
|
|
446
|
-
telegram: {
|
|
447
|
-
displayName: "Telegram",
|
|
448
|
-
description: "Connect to Telegram via long polling",
|
|
449
|
-
envVars: [
|
|
450
|
-
{
|
|
451
|
-
name: "TELEGRAM_BOT_TOKEN",
|
|
452
|
-
required: true,
|
|
453
|
-
description: "Telegram bot token from BotFather",
|
|
454
|
-
scope: "mind"
|
|
419
|
+
// src/lib/webhook.ts
|
|
420
|
+
var slog = logger_default.child("webhook");
|
|
421
|
+
function getWebhookUrl() {
|
|
422
|
+
return process.env.VOLUTE_WEBHOOK_URL;
|
|
423
|
+
}
|
|
424
|
+
function getAuthHeaders() {
|
|
425
|
+
const headers = { "Content-Type": "application/json" };
|
|
426
|
+
const secret = process.env.VOLUTE_WEBHOOK_SECRET;
|
|
427
|
+
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
428
|
+
return headers;
|
|
429
|
+
}
|
|
430
|
+
function fireWebhook(event) {
|
|
431
|
+
try {
|
|
432
|
+
const url = getWebhookUrl();
|
|
433
|
+
if (!url) return;
|
|
434
|
+
const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
435
|
+
fetch(url, {
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers: getAuthHeaders(),
|
|
438
|
+
body: JSON.stringify(payload)
|
|
439
|
+
}).then((res) => {
|
|
440
|
+
if (!res.ok) {
|
|
441
|
+
slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
|
|
455
442
|
}
|
|
456
|
-
|
|
443
|
+
}).catch((err) => {
|
|
444
|
+
slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
|
|
445
|
+
});
|
|
446
|
+
} catch (err) {
|
|
447
|
+
slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
|
|
457
448
|
}
|
|
458
|
-
}
|
|
459
|
-
function
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
449
|
+
}
|
|
450
|
+
function initWebhook() {
|
|
451
|
+
const url = getWebhookUrl();
|
|
452
|
+
if (!url) return () => {
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
const parsed = new URL(url);
|
|
456
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
457
|
+
slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
|
|
458
|
+
return () => {
|
|
459
|
+
};
|
|
470
460
|
}
|
|
461
|
+
} catch {
|
|
462
|
+
slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
|
|
463
|
+
return () => {
|
|
464
|
+
};
|
|
471
465
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
466
|
+
slog.info("webhook enabled");
|
|
467
|
+
return subscribe((event) => {
|
|
468
|
+
try {
|
|
469
|
+
fireWebhook({
|
|
470
|
+
event: event.type,
|
|
471
|
+
mind: event.mind,
|
|
472
|
+
data: { summary: event.summary, ...event.metadata },
|
|
473
|
+
timestamp: event.created_at
|
|
474
|
+
});
|
|
475
|
+
} catch (err) {
|
|
476
|
+
slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
|
|
477
|
+
}
|
|
478
|
+
});
|
|
476
479
|
}
|
|
477
480
|
|
|
478
|
-
// src/lib/
|
|
479
|
-
var
|
|
480
|
-
function
|
|
481
|
-
let
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
searchDir = dirname(searchDir);
|
|
481
|
+
// src/lib/events/conversation-events.ts
|
|
482
|
+
var subscribers = /* @__PURE__ */ new Map();
|
|
483
|
+
function subscribe2(conversationId, callback) {
|
|
484
|
+
let set = subscribers.get(conversationId);
|
|
485
|
+
if (!set) {
|
|
486
|
+
set = /* @__PURE__ */ new Set();
|
|
487
|
+
subscribers.set(conversationId, set);
|
|
486
488
|
}
|
|
487
|
-
|
|
489
|
+
set.add(callback);
|
|
490
|
+
return () => {
|
|
491
|
+
set.delete(callback);
|
|
492
|
+
if (set.size === 0) subscribers.delete(conversationId);
|
|
493
|
+
};
|
|
488
494
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
(type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
501
|
-
clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
502
|
-
})
|
|
503
|
-
)
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
checkConnectorEnv(type, mindName, mindDir2) {
|
|
507
|
-
const mindConnectorDir = resolve3(mindDir2, "connectors", type);
|
|
508
|
-
const userConnectorDir = resolve3(voluteHome(), "connectors", type);
|
|
509
|
-
const connectorDir = existsSync3(mindConnectorDir) ? mindConnectorDir : existsSync3(userConnectorDir) ? userConnectorDir : void 0;
|
|
510
|
-
const def = getConnectorDef(type, connectorDir);
|
|
511
|
-
if (!def) return null;
|
|
512
|
-
const env = loadMergedEnv(mindName);
|
|
513
|
-
const missing = checkMissingEnvVars(def, env);
|
|
514
|
-
if (missing.length === 0) return null;
|
|
515
|
-
return {
|
|
516
|
-
missing: missing.map((v) => ({ name: v.name, description: v.description })),
|
|
517
|
-
connectorName: def.displayName
|
|
518
|
-
};
|
|
495
|
+
function publish2(conversationId, event) {
|
|
496
|
+
const set = subscribers.get(conversationId);
|
|
497
|
+
if (!set) return;
|
|
498
|
+
for (const cb of set) {
|
|
499
|
+
try {
|
|
500
|
+
cb(event);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
console.error("[conversation-events] subscriber threw:", err);
|
|
503
|
+
set.delete(cb);
|
|
504
|
+
if (set.size === 0) subscribers.delete(conversationId);
|
|
505
|
+
}
|
|
519
506
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
this.connectors.get(mindName)?.delete(type);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/lib/events/conversations.ts
|
|
510
|
+
async function createConversation(mindName, channel, opts) {
|
|
511
|
+
const db = await getDb();
|
|
512
|
+
const id = randomUUID();
|
|
513
|
+
const type = opts?.type ?? "dm";
|
|
514
|
+
const name = opts?.name ?? null;
|
|
515
|
+
await db.transaction(async (tx) => {
|
|
516
|
+
await tx.insert(conversations).values({
|
|
517
|
+
id,
|
|
518
|
+
mind_name: mindName,
|
|
519
|
+
channel,
|
|
520
|
+
type,
|
|
521
|
+
name,
|
|
522
|
+
user_id: opts?.userId ?? null,
|
|
523
|
+
title: opts?.title ?? null
|
|
524
|
+
});
|
|
525
|
+
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
526
|
+
await tx.insert(conversationParticipants).values(
|
|
527
|
+
opts.participantIds.map((uid, i) => ({
|
|
528
|
+
conversation_id: id,
|
|
529
|
+
user_id: uid,
|
|
530
|
+
role: i === 0 ? "owner" : "member"
|
|
531
|
+
}))
|
|
532
|
+
);
|
|
547
533
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
534
|
+
});
|
|
535
|
+
fireWebhook({
|
|
536
|
+
event: "conversation_created",
|
|
537
|
+
mind: mindName ?? "",
|
|
538
|
+
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
id,
|
|
542
|
+
mind_name: mindName,
|
|
543
|
+
channel,
|
|
544
|
+
type,
|
|
545
|
+
name,
|
|
546
|
+
user_id: opts?.userId ?? null,
|
|
547
|
+
title: opts?.title ?? null,
|
|
548
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
549
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
async function getConversation(id) {
|
|
553
|
+
const db = await getDb();
|
|
554
|
+
const row = await db.select().from(conversations).where(eq2(conversations.id, id)).get();
|
|
555
|
+
return row ?? null;
|
|
556
|
+
}
|
|
557
|
+
async function addParticipant(conversationId, userId, role = "member") {
|
|
558
|
+
const db = await getDb();
|
|
559
|
+
await db.insert(conversationParticipants).values({
|
|
560
|
+
conversation_id: conversationId,
|
|
561
|
+
user_id: userId,
|
|
562
|
+
role
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
async function removeParticipant(conversationId, userId) {
|
|
566
|
+
const db = await getDb();
|
|
567
|
+
await db.delete(conversationParticipants).where(
|
|
568
|
+
and2(
|
|
569
|
+
eq2(conversationParticipants.conversation_id, conversationId),
|
|
570
|
+
eq2(conversationParticipants.user_id, userId)
|
|
571
|
+
)
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
async function getParticipants(conversationId) {
|
|
575
|
+
const db = await getDb();
|
|
576
|
+
const rows = await db.select({
|
|
577
|
+
userId: conversationParticipants.user_id,
|
|
578
|
+
username: users.username,
|
|
579
|
+
userType: users.user_type,
|
|
580
|
+
role: conversationParticipants.role,
|
|
581
|
+
displayName: users.display_name,
|
|
582
|
+
description: users.description,
|
|
583
|
+
avatar: users.avatar
|
|
584
|
+
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(eq2(conversationParticipants.conversation_id, conversationId)).all();
|
|
585
|
+
return rows;
|
|
586
|
+
}
|
|
587
|
+
async function isParticipant(conversationId, userId) {
|
|
588
|
+
const db = await getDb();
|
|
589
|
+
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
590
|
+
and2(
|
|
591
|
+
eq2(conversationParticipants.conversation_id, conversationId),
|
|
592
|
+
eq2(conversationParticipants.user_id, userId)
|
|
593
|
+
)
|
|
594
|
+
).get();
|
|
595
|
+
return row != null;
|
|
596
|
+
}
|
|
597
|
+
async function listConversationsForUser(userId) {
|
|
598
|
+
const db = await getDb();
|
|
599
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq2(conversationParticipants.user_id, userId)).all();
|
|
600
|
+
if (participantRows.length === 0) return [];
|
|
601
|
+
const convIds = participantRows.map((r) => r.conversation_id);
|
|
602
|
+
return await db.select().from(conversations).where(inArray2(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
603
|
+
}
|
|
604
|
+
async function isParticipantOrOwner(conversationId, userId) {
|
|
605
|
+
if (await isParticipant(conversationId, userId)) return true;
|
|
606
|
+
const db = await getDb();
|
|
607
|
+
const row = await db.select().from(conversations).where(and2(eq2(conversations.id, conversationId), eq2(conversations.user_id, userId))).get();
|
|
608
|
+
return row != null;
|
|
609
|
+
}
|
|
610
|
+
async function deleteConversationForUser(id, userId) {
|
|
611
|
+
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
612
|
+
await deleteConversation(id);
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
async function addMessage(conversationId, role, senderName, content) {
|
|
616
|
+
const db = await getDb();
|
|
617
|
+
const serialized = JSON.stringify(content);
|
|
618
|
+
const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
|
|
619
|
+
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq2(conversations.id, conversationId));
|
|
620
|
+
if (role === "user") {
|
|
621
|
+
const firstText = content.find((b) => b.type === "text");
|
|
622
|
+
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
623
|
+
if (title) {
|
|
624
|
+
await db.update(conversations).set({ title }).where(and2(eq2(conversations.id, conversationId), isNull(conversations.title)));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const msg = {
|
|
628
|
+
id: result.id,
|
|
629
|
+
conversation_id: conversationId,
|
|
630
|
+
role,
|
|
631
|
+
sender_name: senderName,
|
|
632
|
+
content,
|
|
633
|
+
created_at: result.created_at
|
|
634
|
+
};
|
|
635
|
+
publish2(conversationId, {
|
|
636
|
+
type: "message",
|
|
637
|
+
id: msg.id,
|
|
638
|
+
role: msg.role,
|
|
639
|
+
senderName: msg.sender_name,
|
|
640
|
+
content: msg.content,
|
|
641
|
+
createdAt: msg.created_at
|
|
642
|
+
});
|
|
643
|
+
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq2(conversations.id, conversationId)).get();
|
|
644
|
+
fireWebhook({
|
|
645
|
+
event: "message_created",
|
|
646
|
+
mind: conv?.mind_name ?? "",
|
|
647
|
+
data: {
|
|
648
|
+
conversationId,
|
|
649
|
+
messageId: result.id,
|
|
650
|
+
role,
|
|
651
|
+
senderName,
|
|
652
|
+
content: content.filter((b) => b.type !== "image"),
|
|
653
|
+
createdAt: result.created_at
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
return msg;
|
|
657
|
+
}
|
|
658
|
+
async function getMessages(conversationId) {
|
|
659
|
+
const db = await getDb();
|
|
660
|
+
const rows = await db.select().from(messages).where(eq2(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
661
|
+
return rows.map(parseMessageRow);
|
|
662
|
+
}
|
|
663
|
+
async function getMessagesPaginated(conversationId, opts) {
|
|
664
|
+
const db = await getDb();
|
|
665
|
+
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
666
|
+
const conditions = [eq2(messages.conversation_id, conversationId)];
|
|
667
|
+
if (opts?.before != null) {
|
|
668
|
+
conditions.push(lt(messages.id, opts.before));
|
|
669
|
+
}
|
|
670
|
+
const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
|
|
671
|
+
const hasMore = rows.length > limit;
|
|
672
|
+
const page = rows.slice(0, limit).reverse();
|
|
673
|
+
return {
|
|
674
|
+
messages: page.map(parseMessageRow),
|
|
675
|
+
hasMore
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function parseMessageRow(row) {
|
|
679
|
+
let content;
|
|
680
|
+
try {
|
|
681
|
+
const parsed = JSON.parse(row.content);
|
|
682
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
683
|
+
} catch {
|
|
684
|
+
content = [{ type: "text", text: row.content }];
|
|
685
|
+
}
|
|
686
|
+
return { ...row, role: row.role, content };
|
|
687
|
+
}
|
|
688
|
+
async function listConversationsWithParticipants(userId) {
|
|
689
|
+
const convs = await listConversationsForUser(userId);
|
|
690
|
+
if (convs.length === 0) return [];
|
|
691
|
+
const db = await getDb();
|
|
692
|
+
const convIds = convs.map((c) => c.id);
|
|
693
|
+
const rows = await db.select({
|
|
694
|
+
conversationId: conversationParticipants.conversation_id,
|
|
695
|
+
userId: users.id,
|
|
696
|
+
username: users.username,
|
|
697
|
+
userType: users.user_type,
|
|
698
|
+
role: conversationParticipants.role,
|
|
699
|
+
displayName: users.display_name,
|
|
700
|
+
description: users.description,
|
|
701
|
+
avatar: users.avatar
|
|
702
|
+
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(inArray2(conversationParticipants.conversation_id, convIds));
|
|
703
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const r of rows) {
|
|
705
|
+
let arr = byConv.get(r.conversationId);
|
|
706
|
+
if (!arr) {
|
|
707
|
+
arr = [];
|
|
708
|
+
byConv.set(r.conversationId, arr);
|
|
709
|
+
}
|
|
710
|
+
arr.push({
|
|
711
|
+
userId: r.userId,
|
|
712
|
+
username: r.username,
|
|
713
|
+
userType: r.userType,
|
|
714
|
+
role: r.role,
|
|
715
|
+
displayName: r.displayName,
|
|
716
|
+
description: r.description,
|
|
717
|
+
avatar: r.avatar
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
const lastMsgIds = await db.select({
|
|
721
|
+
conversationId: messages.conversation_id,
|
|
722
|
+
maxId: sql`MAX(${messages.id})`
|
|
723
|
+
}).from(messages).where(inArray2(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
724
|
+
const byLastMsg = /* @__PURE__ */ new Map();
|
|
725
|
+
if (lastMsgIds.length > 0) {
|
|
726
|
+
const msgRows = await db.select().from(messages).where(
|
|
727
|
+
inArray2(
|
|
728
|
+
messages.id,
|
|
729
|
+
lastMsgIds.map((r) => r.maxId)
|
|
730
|
+
)
|
|
731
|
+
);
|
|
732
|
+
for (const m of msgRows) {
|
|
733
|
+
let text = "";
|
|
734
|
+
try {
|
|
735
|
+
const parsed = JSON.parse(m.content);
|
|
736
|
+
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
737
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
738
|
+
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
739
|
+
} catch {
|
|
740
|
+
text = m.content;
|
|
741
|
+
}
|
|
742
|
+
byLastMsg.set(m.conversation_id, {
|
|
743
|
+
role: m.role,
|
|
744
|
+
senderName: m.sender_name,
|
|
745
|
+
text,
|
|
746
|
+
createdAt: m.created_at
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return convs.map((c) => ({
|
|
751
|
+
...c,
|
|
752
|
+
participants: byConv.get(c.id) ?? [],
|
|
753
|
+
lastMessage: byLastMsg.get(c.id)
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
async function findDMConversation(mindName, participantIds) {
|
|
757
|
+
const db = await getDb();
|
|
758
|
+
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq2(conversations.mind_name, mindName), eq2(conversations.type, "dm"))).all();
|
|
759
|
+
for (const conv of mindConvs) {
|
|
760
|
+
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq2(conversationParticipants.conversation_id, conv.id)).all();
|
|
761
|
+
if (rows.length !== 2) continue;
|
|
762
|
+
const ids = new Set(rows.map((r) => r.user_id));
|
|
763
|
+
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
764
|
+
return conv.id;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
async function deleteConversation(id) {
|
|
770
|
+
const db = await getDb();
|
|
771
|
+
await db.delete(conversations).where(eq2(conversations.id, id));
|
|
772
|
+
}
|
|
773
|
+
async function createChannel(name, creatorId) {
|
|
774
|
+
const participantIds = creatorId ? [creatorId] : [];
|
|
775
|
+
return createConversation(null, "volute", {
|
|
776
|
+
type: "channel",
|
|
777
|
+
name,
|
|
778
|
+
title: name,
|
|
779
|
+
participantIds
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
async function getChannelByName(name) {
|
|
783
|
+
const db = await getDb();
|
|
784
|
+
const row = await db.select().from(conversations).where(and2(eq2(conversations.name, name), eq2(conversations.type, "channel"))).get();
|
|
785
|
+
return row ?? null;
|
|
786
|
+
}
|
|
787
|
+
async function listChannels() {
|
|
788
|
+
const db = await getDb();
|
|
789
|
+
return await db.select().from(conversations).where(eq2(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
790
|
+
}
|
|
791
|
+
async function joinChannel(conversationId, userId) {
|
|
792
|
+
if (await isParticipant(conversationId, userId)) return;
|
|
793
|
+
await addParticipant(conversationId, userId);
|
|
794
|
+
}
|
|
795
|
+
async function leaveChannel(conversationId, userId) {
|
|
796
|
+
await removeParticipant(conversationId, userId);
|
|
797
|
+
}
|
|
798
|
+
async function getUnreadCounts(userId, conversationIds) {
|
|
799
|
+
if (conversationIds.length === 0) return {};
|
|
800
|
+
const db = await getDb();
|
|
801
|
+
const rows = await db.select({
|
|
802
|
+
conversationId: messages.conversation_id,
|
|
803
|
+
count: sql`COUNT(*)`
|
|
804
|
+
}).from(messages).leftJoin(
|
|
805
|
+
conversationReads,
|
|
806
|
+
and2(
|
|
807
|
+
eq2(conversationReads.conversation_id, messages.conversation_id),
|
|
808
|
+
eq2(conversationReads.user_id, userId)
|
|
809
|
+
)
|
|
810
|
+
).where(
|
|
811
|
+
and2(
|
|
812
|
+
inArray2(messages.conversation_id, conversationIds),
|
|
813
|
+
sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
|
|
814
|
+
)
|
|
815
|
+
).groupBy(messages.conversation_id);
|
|
816
|
+
const result = {};
|
|
817
|
+
for (const row of rows) {
|
|
818
|
+
result[row.conversationId] = row.count;
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
async function markConversationRead(userId, conversationId) {
|
|
823
|
+
const db = await getDb();
|
|
824
|
+
const maxRow = await db.select({ maxId: sql`MAX(${messages.id})` }).from(messages).where(eq2(messages.conversation_id, conversationId)).get();
|
|
825
|
+
const maxId = maxRow?.maxId ?? 0;
|
|
826
|
+
if (maxId === 0) return;
|
|
827
|
+
await db.insert(conversationReads).values({ user_id: userId, conversation_id: conversationId, last_read_message_id: maxId }).onConflictDoUpdate({
|
|
828
|
+
target: [conversationReads.user_id, conversationReads.conversation_id],
|
|
829
|
+
set: { last_read_message_id: maxId }
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/lib/system-channel.ts
|
|
834
|
+
var SYSTEM_CHANNEL_NAME = "system";
|
|
835
|
+
var cachedChannelId = null;
|
|
836
|
+
async function ensureSystemChannel() {
|
|
837
|
+
if (cachedChannelId) return cachedChannelId;
|
|
838
|
+
const existing = await getChannelByName(SYSTEM_CHANNEL_NAME);
|
|
839
|
+
if (existing) {
|
|
840
|
+
cachedChannelId = existing.id;
|
|
841
|
+
return existing.id;
|
|
842
|
+
}
|
|
843
|
+
const conv = await createChannel(SYSTEM_CHANNEL_NAME);
|
|
844
|
+
cachedChannelId = conv.id;
|
|
845
|
+
logger_default.info("created #system channel");
|
|
846
|
+
return conv.id;
|
|
847
|
+
}
|
|
848
|
+
async function joinSystemChannel(userId) {
|
|
849
|
+
const channelId = await ensureSystemChannel();
|
|
850
|
+
await joinChannel(channelId, userId);
|
|
851
|
+
}
|
|
852
|
+
async function joinSystemChannelForMind(mindName) {
|
|
853
|
+
const user = await getOrCreateMindUser(mindName);
|
|
854
|
+
await joinSystemChannel(user.id);
|
|
855
|
+
}
|
|
856
|
+
async function announceToSystem(text) {
|
|
857
|
+
const channelId = await ensureSystemChannel();
|
|
858
|
+
await addMessage(channelId, "system", "system", [{ type: "text", text }]);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/lib/daemon/connector-manager.ts
|
|
862
|
+
import { spawn } from "child_process";
|
|
863
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
864
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
865
|
+
|
|
866
|
+
// src/lib/connector-defs.ts
|
|
867
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
868
|
+
import { resolve as resolve2 } from "path";
|
|
869
|
+
var BUILTIN_DEFS = {
|
|
870
|
+
discord: {
|
|
871
|
+
displayName: "Discord",
|
|
872
|
+
description: "Connect to Discord as a bot",
|
|
873
|
+
envVars: [
|
|
874
|
+
{
|
|
875
|
+
name: "DISCORD_TOKEN",
|
|
876
|
+
required: true,
|
|
877
|
+
description: "Discord bot token",
|
|
878
|
+
scope: "mind"
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: "DISCORD_GUILD_ID",
|
|
882
|
+
required: false,
|
|
883
|
+
description: "Discord server ID (optional, for slash commands)",
|
|
884
|
+
scope: "mind"
|
|
885
|
+
}
|
|
886
|
+
]
|
|
887
|
+
},
|
|
888
|
+
slack: {
|
|
889
|
+
displayName: "Slack",
|
|
890
|
+
description: "Connect to Slack via Socket Mode",
|
|
891
|
+
envVars: [
|
|
892
|
+
{
|
|
893
|
+
name: "SLACK_BOT_TOKEN",
|
|
894
|
+
required: true,
|
|
895
|
+
description: "Slack bot token (xoxb-...)",
|
|
896
|
+
scope: "mind"
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: "SLACK_APP_TOKEN",
|
|
900
|
+
required: true,
|
|
901
|
+
description: "Slack app-level token (xapp-...) for Socket Mode",
|
|
902
|
+
scope: "mind"
|
|
903
|
+
}
|
|
904
|
+
]
|
|
905
|
+
},
|
|
906
|
+
telegram: {
|
|
907
|
+
displayName: "Telegram",
|
|
908
|
+
description: "Connect to Telegram via long polling",
|
|
909
|
+
envVars: [
|
|
910
|
+
{
|
|
911
|
+
name: "TELEGRAM_BOT_TOKEN",
|
|
912
|
+
required: true,
|
|
913
|
+
description: "Telegram bot token from BotFather",
|
|
914
|
+
scope: "mind"
|
|
915
|
+
}
|
|
916
|
+
]
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
function getConnectorDef(type, connectorDir) {
|
|
920
|
+
if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
|
|
921
|
+
if (connectorDir) {
|
|
922
|
+
const jsonPath = resolve2(connectorDir, "connector.json");
|
|
923
|
+
if (existsSync2(jsonPath)) {
|
|
924
|
+
try {
|
|
925
|
+
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
926
|
+
} catch (err) {
|
|
927
|
+
console.warn(`Failed to parse ${jsonPath}: ${err}`);
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
function checkMissingEnvVars(def, env) {
|
|
935
|
+
return def.envVars.filter((v) => v.required && !env[v.name]);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/lib/daemon/connector-manager.ts
|
|
939
|
+
var clog = logger_default.child("connectors");
|
|
940
|
+
function searchUpwards(...segments) {
|
|
941
|
+
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
942
|
+
for (let i = 0; i < 5; i++) {
|
|
943
|
+
const candidate = resolve3(searchDir, ...segments);
|
|
944
|
+
if (existsSync3(candidate)) return candidate;
|
|
945
|
+
searchDir = dirname(searchDir);
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
var ConnectorManager = class {
|
|
950
|
+
connectors = /* @__PURE__ */ new Map();
|
|
951
|
+
stopping = /* @__PURE__ */ new Set();
|
|
952
|
+
// "mind:type" keys currently being explicitly stopped
|
|
953
|
+
shuttingDown = false;
|
|
954
|
+
restartTracker = new RestartTracker();
|
|
955
|
+
async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
|
|
956
|
+
const config = readVoluteConfig(mindDir2) ?? {};
|
|
957
|
+
const types = config.connectors ?? [];
|
|
958
|
+
await Promise.all(
|
|
959
|
+
types.map(
|
|
960
|
+
(type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
961
|
+
clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
962
|
+
})
|
|
963
|
+
)
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
checkConnectorEnv(type, mindName, mindDir2) {
|
|
967
|
+
const mindConnectorDir = resolve3(mindDir2, "connectors", type);
|
|
968
|
+
const userConnectorDir = resolve3(voluteHome(), "connectors", type);
|
|
969
|
+
const connectorDir = existsSync3(mindConnectorDir) ? mindConnectorDir : existsSync3(userConnectorDir) ? userConnectorDir : void 0;
|
|
970
|
+
const def = getConnectorDef(type, connectorDir);
|
|
971
|
+
if (!def) return null;
|
|
972
|
+
const env = loadMergedEnv(mindName);
|
|
973
|
+
const missing = checkMissingEnvVars(def, env);
|
|
974
|
+
if (missing.length === 0) return null;
|
|
975
|
+
return {
|
|
976
|
+
missing: missing.map((v) => ({ name: v.name, description: v.description })),
|
|
977
|
+
connectorName: def.displayName
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
async startConnector(mindName, mindDir2, mindPort, type, daemonPort) {
|
|
981
|
+
const existing = this.connectors.get(mindName)?.get(type);
|
|
982
|
+
if (existing) {
|
|
983
|
+
await new Promise((res) => {
|
|
984
|
+
existing.child.on("exit", () => res());
|
|
985
|
+
try {
|
|
986
|
+
if (existing.child.pid) {
|
|
987
|
+
process.kill(-existing.child.pid, "SIGTERM");
|
|
988
|
+
} else {
|
|
989
|
+
existing.child.kill("SIGTERM");
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
res();
|
|
993
|
+
}
|
|
994
|
+
setTimeout(() => {
|
|
995
|
+
try {
|
|
996
|
+
if (existing.child.pid) {
|
|
997
|
+
process.kill(-existing.child.pid, "SIGKILL");
|
|
998
|
+
} else {
|
|
999
|
+
existing.child.kill("SIGKILL");
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
res();
|
|
1004
|
+
}, 3e3);
|
|
1005
|
+
});
|
|
1006
|
+
this.connectors.get(mindName)?.delete(type);
|
|
1007
|
+
}
|
|
1008
|
+
this.killOrphanConnector(mindName, type);
|
|
1009
|
+
const mindConnector = resolve3(mindDir2, "connectors", type, "index.ts");
|
|
1010
|
+
const userConnector = resolve3(voluteHome(), "connectors", type, "index.ts");
|
|
1011
|
+
const builtinConnector = this.resolveBuiltinConnector(type);
|
|
1012
|
+
let connectorScript;
|
|
1013
|
+
let runtime;
|
|
554
1014
|
if (existsSync3(mindConnector)) {
|
|
555
1015
|
connectorScript = mindConnector;
|
|
556
1016
|
runtime = resolve3(mindDir2, "node_modules", ".bin", "tsx");
|
|
@@ -592,12 +1052,24 @@ var ConnectorManager = class {
|
|
|
592
1052
|
VOLUTE_MIND_DIR: mindDir2,
|
|
593
1053
|
...daemonPort ? {
|
|
594
1054
|
VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
|
|
595
|
-
VOLUTE_DAEMON_TOKEN:
|
|
1055
|
+
VOLUTE_DAEMON_TOKEN: getMindToken(mindName) ?? void 0
|
|
596
1056
|
} : {},
|
|
597
1057
|
...connectorEnv
|
|
598
1058
|
}
|
|
599
1059
|
};
|
|
600
|
-
|
|
1060
|
+
let spawnCmd;
|
|
1061
|
+
let spawnArgs;
|
|
1062
|
+
if (isIsolationEnabled()) {
|
|
1063
|
+
[spawnCmd, spawnArgs] = wrapForIsolation(runtime, [connectorScript], mindName);
|
|
1064
|
+
} else if (isSandboxEnabled()) {
|
|
1065
|
+
[spawnCmd, spawnArgs] = await wrapForSandbox(runtime, [connectorScript], mindDir2, mindName, [
|
|
1066
|
+
mindDir2,
|
|
1067
|
+
mindStateDir
|
|
1068
|
+
]);
|
|
1069
|
+
} else {
|
|
1070
|
+
spawnCmd = runtime;
|
|
1071
|
+
spawnArgs = [connectorScript];
|
|
1072
|
+
}
|
|
601
1073
|
const child = spawn(spawnCmd, spawnArgs, spawnOpts);
|
|
602
1074
|
let lastStderr = "";
|
|
603
1075
|
child.stdout?.pipe(logStream);
|
|
@@ -644,558 +1116,140 @@ var ConnectorManager = class {
|
|
|
644
1116
|
const mindMap = this.connectors.get(mindName);
|
|
645
1117
|
if (!mindMap) return;
|
|
646
1118
|
const tracked = mindMap.get(type);
|
|
647
|
-
if (!tracked) return;
|
|
648
|
-
const stopKey = `${mindName}:${type}`;
|
|
649
|
-
this.stopping.add(stopKey);
|
|
650
|
-
mindMap.delete(type);
|
|
651
|
-
await new Promise((resolve9) => {
|
|
652
|
-
tracked.child.on("exit", () => resolve9());
|
|
653
|
-
try {
|
|
654
|
-
process.kill(-tracked.child.pid, "SIGTERM");
|
|
655
|
-
} catch {
|
|
656
|
-
resolve9();
|
|
657
|
-
}
|
|
658
|
-
setTimeout(() => {
|
|
659
|
-
try {
|
|
660
|
-
process.kill(-tracked.child.pid, "SIGKILL");
|
|
661
|
-
} catch {
|
|
662
|
-
}
|
|
663
|
-
resolve9();
|
|
664
|
-
}, 5e3);
|
|
665
|
-
});
|
|
666
|
-
this.stopping.delete(stopKey);
|
|
667
|
-
this.restartTracker.reset(stopKey);
|
|
668
|
-
try {
|
|
669
|
-
this.removeConnectorPid(mindName, type);
|
|
670
|
-
} catch (err) {
|
|
671
|
-
clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
|
|
672
|
-
}
|
|
673
|
-
clog.info(`stopped connector ${type} for ${mindName}`);
|
|
674
|
-
}
|
|
675
|
-
async stopConnectors(mindName) {
|
|
676
|
-
const mindMap = this.connectors.get(mindName);
|
|
677
|
-
if (!mindMap) return;
|
|
678
|
-
const types = [...mindMap.keys()];
|
|
679
|
-
await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
|
|
680
|
-
this.connectors.delete(mindName);
|
|
681
|
-
}
|
|
682
|
-
async stopAll() {
|
|
683
|
-
this.shuttingDown = true;
|
|
684
|
-
const minds = [...this.connectors.keys()];
|
|
685
|
-
await Promise.all(minds.map((name) => this.stopConnectors(name)));
|
|
686
|
-
}
|
|
687
|
-
getConnectorStatus(mindName) {
|
|
688
|
-
const mindMap = this.connectors.get(mindName);
|
|
689
|
-
if (!mindMap) return [];
|
|
690
|
-
return [...mindMap.entries()].map(([type, tracked]) => ({
|
|
691
|
-
type,
|
|
692
|
-
running: !tracked.child.killed
|
|
693
|
-
}));
|
|
694
|
-
}
|
|
695
|
-
connectorPidPath(mindName, type) {
|
|
696
|
-
return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
|
|
697
|
-
}
|
|
698
|
-
saveConnectorPid(mindName, type, pid) {
|
|
699
|
-
const pidPath = this.connectorPidPath(mindName, type);
|
|
700
|
-
mkdirSync(dirname(pidPath), { recursive: true });
|
|
701
|
-
writeFileSync(pidPath, String(pid));
|
|
702
|
-
}
|
|
703
|
-
removeConnectorPid(mindName, type) {
|
|
704
|
-
try {
|
|
705
|
-
unlinkSync(this.connectorPidPath(mindName, type));
|
|
706
|
-
} catch {
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
killOrphanConnector(mindName, type) {
|
|
710
|
-
const pidPath = this.connectorPidPath(mindName, type);
|
|
711
|
-
if (!existsSync3(pidPath)) return;
|
|
712
|
-
try {
|
|
713
|
-
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
714
|
-
if (pid > 0) {
|
|
715
|
-
try {
|
|
716
|
-
process.kill(-pid, "SIGTERM");
|
|
717
|
-
} catch {
|
|
718
|
-
process.kill(pid, "SIGTERM");
|
|
719
|
-
}
|
|
720
|
-
clog.warn(`killed orphan connector ${type} (pid ${pid})`);
|
|
721
|
-
}
|
|
722
|
-
} catch {
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
unlinkSync(pidPath);
|
|
726
|
-
} catch {
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
resolveBuiltinConnector(type) {
|
|
730
|
-
return searchUpwards("connectors", `${type}.js`);
|
|
731
|
-
}
|
|
732
|
-
resolveVoluteTsx() {
|
|
733
|
-
return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
var instance = null;
|
|
737
|
-
function initConnectorManager() {
|
|
738
|
-
if (instance) throw new Error("ConnectorManager already initialized");
|
|
739
|
-
instance = new ConnectorManager();
|
|
740
|
-
return instance;
|
|
741
|
-
}
|
|
742
|
-
function getConnectorManager() {
|
|
743
|
-
if (!instance)
|
|
744
|
-
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
745
|
-
return instance;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// src/lib/events/mind-events.ts
|
|
749
|
-
var subscribers = /* @__PURE__ */ new Map();
|
|
750
|
-
function subscribe2(mind, callback) {
|
|
751
|
-
let set = subscribers.get(mind);
|
|
752
|
-
if (!set) {
|
|
753
|
-
set = /* @__PURE__ */ new Set();
|
|
754
|
-
subscribers.set(mind, set);
|
|
755
|
-
}
|
|
756
|
-
set.add(callback);
|
|
757
|
-
return () => {
|
|
758
|
-
set.delete(callback);
|
|
759
|
-
if (set.size === 0) subscribers.delete(mind);
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
function publish2(mind, event) {
|
|
763
|
-
const set = subscribers.get(mind);
|
|
764
|
-
if (!set) return;
|
|
765
|
-
for (const cb of set) {
|
|
766
|
-
try {
|
|
767
|
-
cb(event);
|
|
768
|
-
} catch (err) {
|
|
769
|
-
console.error("[mind-events] subscriber threw:", err);
|
|
770
|
-
set.delete(cb);
|
|
771
|
-
if (set.size === 0) subscribers.delete(mind);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// src/lib/delivery/delivery-manager.ts
|
|
777
|
-
import { readFile } from "fs/promises";
|
|
778
|
-
import { extname, resolve as resolve5 } from "path";
|
|
779
|
-
import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
|
|
780
|
-
|
|
781
|
-
// src/lib/events/conversations.ts
|
|
782
|
-
import { randomUUID } from "crypto";
|
|
783
|
-
import { and as and2, desc, eq as eq2, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
784
|
-
|
|
785
|
-
// src/lib/webhook.ts
|
|
786
|
-
var slog = logger_default.child("webhook");
|
|
787
|
-
function getWebhookUrl() {
|
|
788
|
-
return process.env.VOLUTE_WEBHOOK_URL;
|
|
789
|
-
}
|
|
790
|
-
function getAuthHeaders() {
|
|
791
|
-
const headers = { "Content-Type": "application/json" };
|
|
792
|
-
const secret = process.env.VOLUTE_WEBHOOK_SECRET;
|
|
793
|
-
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
794
|
-
return headers;
|
|
795
|
-
}
|
|
796
|
-
function fireWebhook(event) {
|
|
797
|
-
try {
|
|
798
|
-
const url = getWebhookUrl();
|
|
799
|
-
if (!url) return;
|
|
800
|
-
const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
801
|
-
fetch(url, {
|
|
802
|
-
method: "POST",
|
|
803
|
-
headers: getAuthHeaders(),
|
|
804
|
-
body: JSON.stringify(payload)
|
|
805
|
-
}).then((res) => {
|
|
806
|
-
if (!res.ok) {
|
|
807
|
-
slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
|
|
808
|
-
}
|
|
809
|
-
}).catch((err) => {
|
|
810
|
-
slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
|
|
811
|
-
});
|
|
812
|
-
} catch (err) {
|
|
813
|
-
slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
function initWebhook() {
|
|
817
|
-
const url = getWebhookUrl();
|
|
818
|
-
if (!url) return () => {
|
|
819
|
-
};
|
|
820
|
-
try {
|
|
821
|
-
const parsed = new URL(url);
|
|
822
|
-
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
823
|
-
slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
|
|
824
|
-
return () => {
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
} catch {
|
|
828
|
-
slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
|
|
829
|
-
return () => {
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
slog.info("webhook enabled");
|
|
833
|
-
return subscribe((event) => {
|
|
834
|
-
try {
|
|
835
|
-
fireWebhook({
|
|
836
|
-
event: event.type,
|
|
837
|
-
mind: event.mind,
|
|
838
|
-
data: { summary: event.summary, ...event.metadata },
|
|
839
|
-
timestamp: event.created_at
|
|
840
|
-
});
|
|
841
|
-
} catch (err) {
|
|
842
|
-
slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// src/lib/events/conversation-events.ts
|
|
848
|
-
var subscribers2 = /* @__PURE__ */ new Map();
|
|
849
|
-
function subscribe3(conversationId, callback) {
|
|
850
|
-
let set = subscribers2.get(conversationId);
|
|
851
|
-
if (!set) {
|
|
852
|
-
set = /* @__PURE__ */ new Set();
|
|
853
|
-
subscribers2.set(conversationId, set);
|
|
854
|
-
}
|
|
855
|
-
set.add(callback);
|
|
856
|
-
return () => {
|
|
857
|
-
set.delete(callback);
|
|
858
|
-
if (set.size === 0) subscribers2.delete(conversationId);
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
function publish3(conversationId, event) {
|
|
862
|
-
const set = subscribers2.get(conversationId);
|
|
863
|
-
if (!set) return;
|
|
864
|
-
for (const cb of set) {
|
|
1119
|
+
if (!tracked) return;
|
|
1120
|
+
const stopKey = `${mindName}:${type}`;
|
|
1121
|
+
this.stopping.add(stopKey);
|
|
1122
|
+
mindMap.delete(type);
|
|
1123
|
+
await new Promise((resolve9) => {
|
|
1124
|
+
tracked.child.on("exit", () => resolve9());
|
|
1125
|
+
try {
|
|
1126
|
+
process.kill(-tracked.child.pid, "SIGTERM");
|
|
1127
|
+
} catch {
|
|
1128
|
+
resolve9();
|
|
1129
|
+
}
|
|
1130
|
+
setTimeout(() => {
|
|
1131
|
+
try {
|
|
1132
|
+
process.kill(-tracked.child.pid, "SIGKILL");
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
resolve9();
|
|
1136
|
+
}, 5e3);
|
|
1137
|
+
});
|
|
1138
|
+
this.stopping.delete(stopKey);
|
|
1139
|
+
this.restartTracker.reset(stopKey);
|
|
865
1140
|
try {
|
|
866
|
-
|
|
1141
|
+
this.removeConnectorPid(mindName, type);
|
|
867
1142
|
} catch (err) {
|
|
868
|
-
|
|
869
|
-
set.delete(cb);
|
|
870
|
-
if (set.size === 0) subscribers2.delete(conversationId);
|
|
1143
|
+
clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
|
|
871
1144
|
}
|
|
1145
|
+
clog.info(`stopped connector ${type} for ${mindName}`);
|
|
872
1146
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1147
|
+
async stopConnectors(mindName) {
|
|
1148
|
+
const mindMap = this.connectors.get(mindName);
|
|
1149
|
+
if (!mindMap) return;
|
|
1150
|
+
const types = [...mindMap.keys()];
|
|
1151
|
+
await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
|
|
1152
|
+
this.connectors.delete(mindName);
|
|
1153
|
+
}
|
|
1154
|
+
async stopAll() {
|
|
1155
|
+
this.shuttingDown = true;
|
|
1156
|
+
const minds = [...this.connectors.keys()];
|
|
1157
|
+
await Promise.all(minds.map((name) => this.stopConnectors(name)));
|
|
1158
|
+
}
|
|
1159
|
+
getConnectorStatus(mindName) {
|
|
1160
|
+
const mindMap = this.connectors.get(mindName);
|
|
1161
|
+
if (!mindMap) return [];
|
|
1162
|
+
return [...mindMap.entries()].map(([type, tracked]) => ({
|
|
886
1163
|
type,
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
title: opts?.title ?? null
|
|
890
|
-
});
|
|
891
|
-
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
892
|
-
await tx.insert(conversationParticipants).values(
|
|
893
|
-
opts.participantIds.map((uid, i) => ({
|
|
894
|
-
conversation_id: id,
|
|
895
|
-
user_id: uid,
|
|
896
|
-
role: i === 0 ? "owner" : "member"
|
|
897
|
-
}))
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
});
|
|
901
|
-
fireWebhook({
|
|
902
|
-
event: "conversation_created",
|
|
903
|
-
mind: mindName ?? "",
|
|
904
|
-
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
905
|
-
});
|
|
906
|
-
return {
|
|
907
|
-
id,
|
|
908
|
-
mind_name: mindName,
|
|
909
|
-
channel,
|
|
910
|
-
type,
|
|
911
|
-
name,
|
|
912
|
-
user_id: opts?.userId ?? null,
|
|
913
|
-
title: opts?.title ?? null,
|
|
914
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
915
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
async function getConversation(id) {
|
|
919
|
-
const db = await getDb();
|
|
920
|
-
const row = await db.select().from(conversations).where(eq2(conversations.id, id)).get();
|
|
921
|
-
return row ?? null;
|
|
922
|
-
}
|
|
923
|
-
async function addParticipant(conversationId, userId, role = "member") {
|
|
924
|
-
const db = await getDb();
|
|
925
|
-
await db.insert(conversationParticipants).values({
|
|
926
|
-
conversation_id: conversationId,
|
|
927
|
-
user_id: userId,
|
|
928
|
-
role
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
async function removeParticipant(conversationId, userId) {
|
|
932
|
-
const db = await getDb();
|
|
933
|
-
await db.delete(conversationParticipants).where(
|
|
934
|
-
and2(
|
|
935
|
-
eq2(conversationParticipants.conversation_id, conversationId),
|
|
936
|
-
eq2(conversationParticipants.user_id, userId)
|
|
937
|
-
)
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
async function getParticipants(conversationId) {
|
|
941
|
-
const db = await getDb();
|
|
942
|
-
const rows = await db.select({
|
|
943
|
-
userId: conversationParticipants.user_id,
|
|
944
|
-
username: users.username,
|
|
945
|
-
userType: users.user_type,
|
|
946
|
-
role: conversationParticipants.role,
|
|
947
|
-
displayName: users.display_name,
|
|
948
|
-
description: users.description,
|
|
949
|
-
avatar: users.avatar
|
|
950
|
-
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(eq2(conversationParticipants.conversation_id, conversationId)).all();
|
|
951
|
-
return rows;
|
|
952
|
-
}
|
|
953
|
-
async function isParticipant(conversationId, userId) {
|
|
954
|
-
const db = await getDb();
|
|
955
|
-
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
956
|
-
and2(
|
|
957
|
-
eq2(conversationParticipants.conversation_id, conversationId),
|
|
958
|
-
eq2(conversationParticipants.user_id, userId)
|
|
959
|
-
)
|
|
960
|
-
).get();
|
|
961
|
-
return row != null;
|
|
962
|
-
}
|
|
963
|
-
async function listConversationsForUser(userId) {
|
|
964
|
-
const db = await getDb();
|
|
965
|
-
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq2(conversationParticipants.user_id, userId)).all();
|
|
966
|
-
if (participantRows.length === 0) return [];
|
|
967
|
-
const convIds = participantRows.map((r) => r.conversation_id);
|
|
968
|
-
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
969
|
-
}
|
|
970
|
-
async function isParticipantOrOwner(conversationId, userId) {
|
|
971
|
-
if (await isParticipant(conversationId, userId)) return true;
|
|
972
|
-
const db = await getDb();
|
|
973
|
-
const row = await db.select().from(conversations).where(and2(eq2(conversations.id, conversationId), eq2(conversations.user_id, userId))).get();
|
|
974
|
-
return row != null;
|
|
975
|
-
}
|
|
976
|
-
async function deleteConversationForUser(id, userId) {
|
|
977
|
-
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
978
|
-
await deleteConversation(id);
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
async function addMessage(conversationId, role, senderName, content) {
|
|
982
|
-
const db = await getDb();
|
|
983
|
-
const serialized = JSON.stringify(content);
|
|
984
|
-
const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
|
|
985
|
-
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq2(conversations.id, conversationId));
|
|
986
|
-
if (role === "user") {
|
|
987
|
-
const firstText = content.find((b) => b.type === "text");
|
|
988
|
-
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
989
|
-
if (title) {
|
|
990
|
-
await db.update(conversations).set({ title }).where(and2(eq2(conversations.id, conversationId), isNull(conversations.title)));
|
|
991
|
-
}
|
|
1164
|
+
running: !tracked.child.killed
|
|
1165
|
+
}));
|
|
992
1166
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
conversation_id: conversationId,
|
|
996
|
-
role,
|
|
997
|
-
sender_name: senderName,
|
|
998
|
-
content,
|
|
999
|
-
created_at: result.created_at
|
|
1000
|
-
};
|
|
1001
|
-
publish3(conversationId, {
|
|
1002
|
-
type: "message",
|
|
1003
|
-
id: msg.id,
|
|
1004
|
-
role: msg.role,
|
|
1005
|
-
senderName: msg.sender_name,
|
|
1006
|
-
content: msg.content,
|
|
1007
|
-
createdAt: msg.created_at
|
|
1008
|
-
});
|
|
1009
|
-
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq2(conversations.id, conversationId)).get();
|
|
1010
|
-
fireWebhook({
|
|
1011
|
-
event: "message_created",
|
|
1012
|
-
mind: conv?.mind_name ?? "",
|
|
1013
|
-
data: {
|
|
1014
|
-
conversationId,
|
|
1015
|
-
messageId: result.id,
|
|
1016
|
-
role,
|
|
1017
|
-
senderName,
|
|
1018
|
-
content: content.filter((b) => b.type !== "image"),
|
|
1019
|
-
createdAt: result.created_at
|
|
1020
|
-
}
|
|
1021
|
-
});
|
|
1022
|
-
return msg;
|
|
1023
|
-
}
|
|
1024
|
-
async function getMessages(conversationId) {
|
|
1025
|
-
const db = await getDb();
|
|
1026
|
-
const rows = await db.select().from(messages).where(eq2(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1027
|
-
return rows.map(parseMessageRow);
|
|
1028
|
-
}
|
|
1029
|
-
async function getMessagesPaginated(conversationId, opts) {
|
|
1030
|
-
const db = await getDb();
|
|
1031
|
-
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
1032
|
-
const conditions = [eq2(messages.conversation_id, conversationId)];
|
|
1033
|
-
if (opts?.before != null) {
|
|
1034
|
-
conditions.push(lt(messages.id, opts.before));
|
|
1167
|
+
connectorPidPath(mindName, type) {
|
|
1168
|
+
return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
|
|
1035
1169
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
messages: page.map(parseMessageRow),
|
|
1041
|
-
hasMore
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
function parseMessageRow(row) {
|
|
1045
|
-
let content;
|
|
1046
|
-
try {
|
|
1047
|
-
const parsed = JSON.parse(row.content);
|
|
1048
|
-
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
1049
|
-
} catch {
|
|
1050
|
-
content = [{ type: "text", text: row.content }];
|
|
1170
|
+
saveConnectorPid(mindName, type, pid) {
|
|
1171
|
+
const pidPath = this.connectorPidPath(mindName, type);
|
|
1172
|
+
mkdirSync(dirname(pidPath), { recursive: true });
|
|
1173
|
+
writeFileSync(pidPath, String(pid));
|
|
1051
1174
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const db = await getDb();
|
|
1058
|
-
const convIds = convs.map((c) => c.id);
|
|
1059
|
-
const rows = await db.select({
|
|
1060
|
-
conversationId: conversationParticipants.conversation_id,
|
|
1061
|
-
userId: users.id,
|
|
1062
|
-
username: users.username,
|
|
1063
|
-
userType: users.user_type,
|
|
1064
|
-
role: conversationParticipants.role,
|
|
1065
|
-
displayName: users.display_name,
|
|
1066
|
-
description: users.description,
|
|
1067
|
-
avatar: users.avatar
|
|
1068
|
-
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
1069
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
1070
|
-
for (const r of rows) {
|
|
1071
|
-
let arr = byConv.get(r.conversationId);
|
|
1072
|
-
if (!arr) {
|
|
1073
|
-
arr = [];
|
|
1074
|
-
byConv.set(r.conversationId, arr);
|
|
1075
|
-
}
|
|
1076
|
-
arr.push({
|
|
1077
|
-
userId: r.userId,
|
|
1078
|
-
username: r.username,
|
|
1079
|
-
userType: r.userType,
|
|
1080
|
-
role: r.role,
|
|
1081
|
-
displayName: r.displayName,
|
|
1082
|
-
description: r.description,
|
|
1083
|
-
avatar: r.avatar
|
|
1084
|
-
});
|
|
1175
|
+
removeConnectorPid(mindName, type) {
|
|
1176
|
+
try {
|
|
1177
|
+
unlinkSync(this.connectorPidPath(mindName, type));
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1085
1180
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
for (const m of msgRows) {
|
|
1099
|
-
let text = "";
|
|
1100
|
-
try {
|
|
1101
|
-
const parsed = JSON.parse(m.content);
|
|
1102
|
-
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
1103
|
-
const textBlock = blocks.find((b) => b.type === "text");
|
|
1104
|
-
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
1105
|
-
} catch {
|
|
1106
|
-
text = m.content;
|
|
1181
|
+
killOrphanConnector(mindName, type) {
|
|
1182
|
+
const pidPath = this.connectorPidPath(mindName, type);
|
|
1183
|
+
if (!existsSync3(pidPath)) return;
|
|
1184
|
+
try {
|
|
1185
|
+
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
1186
|
+
if (pid > 0) {
|
|
1187
|
+
try {
|
|
1188
|
+
process.kill(-pid, "SIGTERM");
|
|
1189
|
+
} catch {
|
|
1190
|
+
process.kill(pid, "SIGTERM");
|
|
1191
|
+
}
|
|
1192
|
+
clog.warn(`killed orphan connector ${type} (pid ${pid})`);
|
|
1107
1193
|
}
|
|
1108
|
-
|
|
1109
|
-
role: m.role,
|
|
1110
|
-
senderName: m.sender_name,
|
|
1111
|
-
text,
|
|
1112
|
-
createdAt: m.created_at
|
|
1113
|
-
});
|
|
1194
|
+
} catch {
|
|
1114
1195
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
participants: byConv.get(c.id) ?? [],
|
|
1119
|
-
lastMessage: byLastMsg.get(c.id)
|
|
1120
|
-
}));
|
|
1121
|
-
}
|
|
1122
|
-
async function findDMConversation(mindName, participantIds) {
|
|
1123
|
-
const db = await getDb();
|
|
1124
|
-
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq2(conversations.mind_name, mindName), eq2(conversations.type, "dm"))).all();
|
|
1125
|
-
for (const conv of mindConvs) {
|
|
1126
|
-
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq2(conversationParticipants.conversation_id, conv.id)).all();
|
|
1127
|
-
if (rows.length !== 2) continue;
|
|
1128
|
-
const ids = new Set(rows.map((r) => r.user_id));
|
|
1129
|
-
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
1130
|
-
return conv.id;
|
|
1196
|
+
try {
|
|
1197
|
+
unlinkSync(pidPath);
|
|
1198
|
+
} catch {
|
|
1131
1199
|
}
|
|
1132
1200
|
}
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
participantIds
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
async function getChannelByName(name) {
|
|
1149
|
-
const db = await getDb();
|
|
1150
|
-
const row = await db.select().from(conversations).where(and2(eq2(conversations.name, name), eq2(conversations.type, "channel"))).get();
|
|
1151
|
-
return row ?? null;
|
|
1152
|
-
}
|
|
1153
|
-
async function listChannels() {
|
|
1154
|
-
const db = await getDb();
|
|
1155
|
-
return await db.select().from(conversations).where(eq2(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
1156
|
-
}
|
|
1157
|
-
async function joinChannel(conversationId, userId) {
|
|
1158
|
-
if (await isParticipant(conversationId, userId)) return;
|
|
1159
|
-
await addParticipant(conversationId, userId);
|
|
1201
|
+
resolveBuiltinConnector(type) {
|
|
1202
|
+
return searchUpwards("connectors", `${type}.js`);
|
|
1203
|
+
}
|
|
1204
|
+
resolveVoluteTsx() {
|
|
1205
|
+
return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
var instance = null;
|
|
1209
|
+
function initConnectorManager() {
|
|
1210
|
+
if (instance) throw new Error("ConnectorManager already initialized");
|
|
1211
|
+
instance = new ConnectorManager();
|
|
1212
|
+
return instance;
|
|
1160
1213
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1214
|
+
function getConnectorManager() {
|
|
1215
|
+
if (!instance)
|
|
1216
|
+
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
1217
|
+
return instance;
|
|
1163
1218
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
and2(
|
|
1173
|
-
eq2(conversationReads.conversation_id, messages.conversation_id),
|
|
1174
|
-
eq2(conversationReads.user_id, userId)
|
|
1175
|
-
)
|
|
1176
|
-
).where(
|
|
1177
|
-
and2(
|
|
1178
|
-
inArray(messages.conversation_id, conversationIds),
|
|
1179
|
-
sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
|
|
1180
|
-
)
|
|
1181
|
-
).groupBy(messages.conversation_id);
|
|
1182
|
-
const result = {};
|
|
1183
|
-
for (const row of rows) {
|
|
1184
|
-
result[row.conversationId] = row.count;
|
|
1219
|
+
|
|
1220
|
+
// src/lib/events/mind-events.ts
|
|
1221
|
+
var subscribers2 = /* @__PURE__ */ new Map();
|
|
1222
|
+
function subscribe3(mind, callback) {
|
|
1223
|
+
let set = subscribers2.get(mind);
|
|
1224
|
+
if (!set) {
|
|
1225
|
+
set = /* @__PURE__ */ new Set();
|
|
1226
|
+
subscribers2.set(mind, set);
|
|
1185
1227
|
}
|
|
1186
|
-
|
|
1228
|
+
set.add(callback);
|
|
1229
|
+
return () => {
|
|
1230
|
+
set.delete(callback);
|
|
1231
|
+
if (set.size === 0) subscribers2.delete(mind);
|
|
1232
|
+
};
|
|
1187
1233
|
}
|
|
1188
|
-
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1234
|
+
function publish3(mind, event) {
|
|
1235
|
+
const set = subscribers2.get(mind);
|
|
1236
|
+
if (!set) return;
|
|
1237
|
+
for (const cb of set) {
|
|
1238
|
+
try {
|
|
1239
|
+
cb(event);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
console.error("[mind-events] subscriber threw:", err);
|
|
1242
|
+
set.delete(cb);
|
|
1243
|
+
if (set.size === 0) subscribers2.delete(mind);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1197
1246
|
}
|
|
1198
1247
|
|
|
1248
|
+
// src/lib/delivery/delivery-manager.ts
|
|
1249
|
+
import { readFile, realpath } from "fs/promises";
|
|
1250
|
+
import { extname, resolve as resolve5 } from "path";
|
|
1251
|
+
import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
|
|
1252
|
+
|
|
1199
1253
|
// src/lib/typing.ts
|
|
1200
1254
|
var DEFAULT_TTL_MS = 1e4;
|
|
1201
1255
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -1281,7 +1335,7 @@ function publishTypingForChannels(channels, map) {
|
|
|
1281
1335
|
for (const channel of channels) {
|
|
1282
1336
|
if (channel.startsWith(VOLUTE_PREFIX)) {
|
|
1283
1337
|
const conversationId = channel.slice(VOLUTE_PREFIX.length);
|
|
1284
|
-
|
|
1338
|
+
publish2(conversationId, { type: "typing", senders: map.get(channel) });
|
|
1285
1339
|
}
|
|
1286
1340
|
}
|
|
1287
1341
|
}
|
|
@@ -1990,8 +2044,26 @@ var DeliveryManager = class {
|
|
|
1990
2044
|
if (p.userType === "mind") {
|
|
1991
2045
|
const dir = mindDir(p.username);
|
|
1992
2046
|
const config = readVoluteConfig(dir);
|
|
1993
|
-
if (!config?.avatar) continue;
|
|
1994
|
-
filePath = resolve5(dir, "home", config.avatar);
|
|
2047
|
+
if (!config?.profile?.avatar) continue;
|
|
2048
|
+
filePath = resolve5(dir, "home", config.profile.avatar);
|
|
2049
|
+
const homeDir = resolve5(dir, "home");
|
|
2050
|
+
if (!filePath.startsWith(`${homeDir}/`)) {
|
|
2051
|
+
dlog2.warn(`avatar path for ${p.username} escapes home directory, skipping`);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
try {
|
|
2055
|
+
const realHome = await realpath(homeDir);
|
|
2056
|
+
const realAvatar = await realpath(filePath);
|
|
2057
|
+
if (!realAvatar.startsWith(`${realHome}/`)) {
|
|
2058
|
+
dlog2.warn(
|
|
2059
|
+
`avatar symlink for ${p.username} resolves outside home directory, skipping`
|
|
2060
|
+
);
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
if (err.code === "ENOENT") continue;
|
|
2065
|
+
throw err;
|
|
2066
|
+
}
|
|
1995
2067
|
} else {
|
|
1996
2068
|
filePath = resolve5(voluteHome(), "avatars", p.avatar);
|
|
1997
2069
|
}
|
|
@@ -2082,7 +2154,7 @@ async function recordInbound(mind, channel, sender, content) {
|
|
|
2082
2154
|
} catch (err) {
|
|
2083
2155
|
dlog3.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
|
|
2084
2156
|
}
|
|
2085
|
-
|
|
2157
|
+
publish3(mind, {
|
|
2086
2158
|
mind,
|
|
2087
2159
|
type: "inbound",
|
|
2088
2160
|
channel,
|
|
@@ -2433,6 +2505,18 @@ var Scheduler = class {
|
|
|
2433
2505
|
return false;
|
|
2434
2506
|
}
|
|
2435
2507
|
async fire(mindName, schedule) {
|
|
2508
|
+
const sleepManager = getSleepManagerIfReady();
|
|
2509
|
+
const sleepState = sleepManager?.getState(mindName);
|
|
2510
|
+
if (sleepState?.sleeping) {
|
|
2511
|
+
if (schedule.skipWhenSleeping) {
|
|
2512
|
+
slog2.info(`skipped "${schedule.id}" for ${mindName} (sleeping)`);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (sleepState.wokenByTrigger) {
|
|
2516
|
+
slog2.info(`skipped "${schedule.id}" for ${mindName} (trigger-woken)`);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2436
2520
|
try {
|
|
2437
2521
|
let text;
|
|
2438
2522
|
if (schedule.script) {
|
|
@@ -2458,7 +2542,7 @@ ${stderr}` : ""}`;
|
|
|
2458
2542
|
}
|
|
2459
2543
|
await this.deliver(mindName, {
|
|
2460
2544
|
content: [{ type: "text", text }],
|
|
2461
|
-
channel: "system:scheduler",
|
|
2545
|
+
channel: schedule.channel ?? "system:scheduler",
|
|
2462
2546
|
sender: schedule.id
|
|
2463
2547
|
});
|
|
2464
2548
|
slog2.info(`fired "${schedule.id}" for ${mindName}`);
|
|
@@ -2702,14 +2786,13 @@ async function startMindFull(name) {
|
|
|
2702
2786
|
);
|
|
2703
2787
|
const config = readVoluteConfig(dir);
|
|
2704
2788
|
if (config) {
|
|
2705
|
-
syncMindProfile(baseName, {
|
|
2706
|
-
displayName: config.displayName,
|
|
2707
|
-
description: config.description,
|
|
2708
|
-
avatar: config.avatar
|
|
2709
|
-
}).catch(
|
|
2789
|
+
syncMindProfile(baseName, config.profile ?? {}).catch(
|
|
2710
2790
|
(err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
|
|
2711
2791
|
);
|
|
2712
2792
|
}
|
|
2793
|
+
joinSystemChannelForMind(baseName).catch(
|
|
2794
|
+
(err) => logger_default.error(`failed to join #system for ${baseName}`, logger_default.errorData(err))
|
|
2795
|
+
);
|
|
2713
2796
|
if (config?.tokenBudget) {
|
|
2714
2797
|
getTokenBudget().setBudget(
|
|
2715
2798
|
baseName,
|
|
@@ -2762,7 +2845,8 @@ function defaultState() {
|
|
|
2762
2845
|
scheduledWakeAt: null,
|
|
2763
2846
|
wokenByTrigger: false,
|
|
2764
2847
|
voluntaryWakeAt: null,
|
|
2765
|
-
queuedMessageCount: 0
|
|
2848
|
+
queuedMessageCount: 0,
|
|
2849
|
+
triggerWakeHistory: []
|
|
2766
2850
|
};
|
|
2767
2851
|
}
|
|
2768
2852
|
function formatCurrentDate() {
|
|
@@ -2809,6 +2893,7 @@ var SleepManager = class {
|
|
|
2809
2893
|
if (existsSync5(this.statePath)) {
|
|
2810
2894
|
const data = JSON.parse(readFileSync5(this.statePath, "utf-8"));
|
|
2811
2895
|
for (const [name, state] of Object.entries(data)) {
|
|
2896
|
+
state.triggerWakeHistory ??= [];
|
|
2812
2897
|
this.states.set(name, state);
|
|
2813
2898
|
}
|
|
2814
2899
|
}
|
|
@@ -2838,6 +2923,16 @@ var SleepManager = class {
|
|
|
2838
2923
|
getState(name) {
|
|
2839
2924
|
return this.states.get(name) ?? defaultState();
|
|
2840
2925
|
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Convert a trigger-wake into a full wake. The mind is already running;
|
|
2928
|
+
* this just clears the sleep state so onActivityEvent won't return it to sleep.
|
|
2929
|
+
*/
|
|
2930
|
+
convertTriggerToFullWake(name) {
|
|
2931
|
+
const state = this.states.get(name);
|
|
2932
|
+
if (!state?.sleeping || !state.wokenByTrigger) return;
|
|
2933
|
+
this.markAwake(name);
|
|
2934
|
+
slog3.info(`${name} trigger-wake converted to full wake`);
|
|
2935
|
+
}
|
|
2841
2936
|
getSleepConfig(name) {
|
|
2842
2937
|
const dir = mindDir(name);
|
|
2843
2938
|
const config = readVoluteConfig(dir);
|
|
@@ -2906,15 +3001,6 @@ var SleepManager = class {
|
|
|
2906
3001
|
if (this.transitioning.has(name)) return;
|
|
2907
3002
|
this.transitioning.add(name);
|
|
2908
3003
|
try {
|
|
2909
|
-
const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
|
|
2910
|
-
const now = /* @__PURE__ */ new Date();
|
|
2911
|
-
const duration = formatDuration(sleepingSince, now);
|
|
2912
|
-
const currentDate = formatCurrentDate();
|
|
2913
|
-
const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
|
|
2914
|
-
hour: "numeric",
|
|
2915
|
-
minute: "2-digit"
|
|
2916
|
-
});
|
|
2917
|
-
const queuedSummary = await this.buildQueuedSummary(name);
|
|
2918
3004
|
try {
|
|
2919
3005
|
await wakeMind(name);
|
|
2920
3006
|
} catch (err) {
|
|
@@ -2923,46 +3009,59 @@ var SleepManager = class {
|
|
|
2923
3009
|
}
|
|
2924
3010
|
const entry = findMind(name);
|
|
2925
3011
|
if (!entry) return;
|
|
2926
|
-
let summaryText;
|
|
2927
3012
|
if (opts?.trigger) {
|
|
2928
3013
|
state.wokenByTrigger = true;
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
sleepTime,
|
|
2933
|
-
duration,
|
|
2934
|
-
queuedSummary
|
|
3014
|
+
state.triggerWakeHistory.push({
|
|
3015
|
+
channel: opts.trigger.channel,
|
|
3016
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2935
3017
|
});
|
|
3018
|
+
this.saveState();
|
|
2936
3019
|
} else {
|
|
2937
|
-
|
|
3020
|
+
const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
|
|
3021
|
+
const now = /* @__PURE__ */ new Date();
|
|
3022
|
+
const duration = formatDuration(sleepingSince, now);
|
|
3023
|
+
const currentDate = formatCurrentDate();
|
|
3024
|
+
const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
|
|
3025
|
+
hour: "numeric",
|
|
3026
|
+
minute: "2-digit"
|
|
3027
|
+
});
|
|
3028
|
+
const triggerWakeSummary = this.buildTriggerWakeSummary(state);
|
|
3029
|
+
const wakeContext = await this.runWakeContextScript(
|
|
3030
|
+
name,
|
|
3031
|
+
state.sleepingSince ?? sleepingSince.toISOString(),
|
|
3032
|
+
duration
|
|
3033
|
+
);
|
|
3034
|
+
const queuedSummary = await this.buildQueuedSummary(name);
|
|
3035
|
+
const sleepActivity = [triggerWakeSummary, wakeContext, queuedSummary].filter(Boolean).join("\n\n");
|
|
3036
|
+
const summaryText = await getPrompt("wake_summary", {
|
|
2938
3037
|
currentDate,
|
|
2939
3038
|
sleepTime,
|
|
2940
3039
|
duration,
|
|
2941
|
-
|
|
2942
|
-
});
|
|
2943
|
-
}
|
|
2944
|
-
try {
|
|
2945
|
-
const db = await getDb();
|
|
2946
|
-
await db.insert(mindHistory).values({
|
|
2947
|
-
mind: name,
|
|
2948
|
-
type: "inbound",
|
|
2949
|
-
channel: "system:sleep",
|
|
2950
|
-
content: summaryText
|
|
2951
|
-
});
|
|
2952
|
-
} catch (err) {
|
|
2953
|
-
slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
|
|
2954
|
-
}
|
|
2955
|
-
try {
|
|
2956
|
-
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
2957
|
-
method: "POST",
|
|
2958
|
-
headers: { "Content-Type": "application/json" },
|
|
2959
|
-
body: JSON.stringify({
|
|
2960
|
-
content: [{ type: "text", text: summaryText }],
|
|
2961
|
-
channel: "system:sleep"
|
|
2962
|
-
})
|
|
3040
|
+
sleepActivity
|
|
2963
3041
|
});
|
|
2964
|
-
|
|
2965
|
-
|
|
3042
|
+
try {
|
|
3043
|
+
const db = await getDb();
|
|
3044
|
+
await db.insert(mindHistory).values({
|
|
3045
|
+
mind: name,
|
|
3046
|
+
type: "inbound",
|
|
3047
|
+
channel: "system:sleep",
|
|
3048
|
+
content: summaryText
|
|
3049
|
+
});
|
|
3050
|
+
} catch (err) {
|
|
3051
|
+
slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
|
|
3052
|
+
}
|
|
3053
|
+
try {
|
|
3054
|
+
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
3055
|
+
method: "POST",
|
|
3056
|
+
headers: { "Content-Type": "application/json" },
|
|
3057
|
+
body: JSON.stringify({
|
|
3058
|
+
content: [{ type: "text", text: summaryText }],
|
|
3059
|
+
channel: "system:sleep"
|
|
3060
|
+
})
|
|
3061
|
+
});
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
|
|
3064
|
+
}
|
|
2966
3065
|
}
|
|
2967
3066
|
const flushed = await this.flushQueuedMessages(name);
|
|
2968
3067
|
if (flushed > 0) {
|
|
@@ -3028,7 +3127,7 @@ var SleepManager = class {
|
|
|
3028
3127
|
const db = await getDb();
|
|
3029
3128
|
const rows = await db.select().from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
3030
3129
|
if (rows.length === 0) return 0;
|
|
3031
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
3130
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-MS5JYPZX.js");
|
|
3032
3131
|
const delivered = [];
|
|
3033
3132
|
for (const row of rows) {
|
|
3034
3133
|
try {
|
|
@@ -3039,7 +3138,7 @@ var SleepManager = class {
|
|
|
3039
3138
|
}
|
|
3040
3139
|
}
|
|
3041
3140
|
if (delivered.length > 0) {
|
|
3042
|
-
await db.delete(deliveryQueue).where(
|
|
3141
|
+
await db.delete(deliveryQueue).where(inArray3(deliveryQueue.id, delivered));
|
|
3043
3142
|
}
|
|
3044
3143
|
const state = this.states.get(name);
|
|
3045
3144
|
if (state) {
|
|
@@ -3060,7 +3159,8 @@ var SleepManager = class {
|
|
|
3060
3159
|
scheduledWakeAt: this.getNextWakeTime(sleepConfig),
|
|
3061
3160
|
wokenByTrigger: false,
|
|
3062
3161
|
voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
|
|
3063
|
-
queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0
|
|
3162
|
+
queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
|
|
3163
|
+
triggerWakeHistory: []
|
|
3064
3164
|
};
|
|
3065
3165
|
this.states.set(name, state);
|
|
3066
3166
|
this.saveState();
|
|
@@ -3177,18 +3277,70 @@ var SleepManager = class {
|
|
|
3177
3277
|
}
|
|
3178
3278
|
}
|
|
3179
3279
|
}
|
|
3280
|
+
async runWakeContextScript(name, sleepingSince, duration) {
|
|
3281
|
+
const scriptPath = resolve8(mindDir(name), "home", ".config", "hooks", "wake-context.sh");
|
|
3282
|
+
if (!existsSync5(scriptPath)) return "";
|
|
3283
|
+
const input = JSON.stringify({
|
|
3284
|
+
sleepingSince,
|
|
3285
|
+
duration,
|
|
3286
|
+
wakeTime: (/* @__PURE__ */ new Date()).toISOString()
|
|
3287
|
+
});
|
|
3288
|
+
try {
|
|
3289
|
+
const result = await new Promise((resolvePromise, reject) => {
|
|
3290
|
+
const child = spawnChild("bash", [scriptPath], {
|
|
3291
|
+
cwd: mindDir(name),
|
|
3292
|
+
timeout: 5e3,
|
|
3293
|
+
env: { ...process.env, VOLUTE_MIND: name },
|
|
3294
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3295
|
+
});
|
|
3296
|
+
let stdout = "";
|
|
3297
|
+
let stderr = "";
|
|
3298
|
+
child.stdout.on("data", (data) => {
|
|
3299
|
+
stdout += data.toString();
|
|
3300
|
+
});
|
|
3301
|
+
child.stderr.on("data", (data) => {
|
|
3302
|
+
stderr += data.toString();
|
|
3303
|
+
});
|
|
3304
|
+
child.on("close", (code) => {
|
|
3305
|
+
if (code === 0) resolvePromise(stdout);
|
|
3306
|
+
else
|
|
3307
|
+
reject(
|
|
3308
|
+
new Error(
|
|
3309
|
+
`wake-context script exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`
|
|
3310
|
+
)
|
|
3311
|
+
);
|
|
3312
|
+
});
|
|
3313
|
+
child.on("error", reject);
|
|
3314
|
+
child.stdin.end(input);
|
|
3315
|
+
});
|
|
3316
|
+
return result.trim();
|
|
3317
|
+
} catch (err) {
|
|
3318
|
+
slog3.warn(`wake-context script failed for ${name}`, logger_default.errorData(err));
|
|
3319
|
+
return "";
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
buildTriggerWakeSummary(state) {
|
|
3323
|
+
const history = state.triggerWakeHistory;
|
|
3324
|
+
if (!history || history.length === 0) return "";
|
|
3325
|
+
const channels = [...new Set(history.map((h) => h.channel))];
|
|
3326
|
+
const times = history.length === 1 ? "once" : `${history.length} times`;
|
|
3327
|
+
return `You were briefly woken ${times} during sleep to handle messages on ${channels.join(", ")} (sessions were archived after each).`;
|
|
3328
|
+
}
|
|
3180
3329
|
async buildQueuedSummary(name) {
|
|
3181
3330
|
try {
|
|
3182
3331
|
const db = await getDb();
|
|
3183
|
-
const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
3332
|
+
const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
3184
3333
|
if (rows.length === 0) return "No messages arrived while you slept.";
|
|
3185
3334
|
const channelCounts = /* @__PURE__ */ new Map();
|
|
3335
|
+
const senders = /* @__PURE__ */ new Set();
|
|
3186
3336
|
for (const row of rows) {
|
|
3187
3337
|
const ch = row.channel ?? "unknown";
|
|
3188
3338
|
channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
|
|
3339
|
+
if (row.sender) senders.add(row.sender);
|
|
3189
3340
|
}
|
|
3190
3341
|
const parts = [...channelCounts.entries()].map(([ch, count2]) => `${count2} on ${ch}`);
|
|
3191
|
-
|
|
3342
|
+
const senderNote = senders.size > 0 ? ` from ${[...senders].join(", ")}` : "";
|
|
3343
|
+
return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept${senderNote} (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
|
|
3192
3344
|
} catch (err) {
|
|
3193
3345
|
slog3.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
|
|
3194
3346
|
return "Unable to check for queued messages \u2014 there may be messages waiting.";
|
|
@@ -3291,8 +3443,6 @@ function getSleepManagerIfReady() {
|
|
|
3291
3443
|
}
|
|
3292
3444
|
|
|
3293
3445
|
export {
|
|
3294
|
-
initConnectorManager,
|
|
3295
|
-
getConnectorManager,
|
|
3296
3446
|
createUser,
|
|
3297
3447
|
verifyUser,
|
|
3298
3448
|
getUser,
|
|
@@ -3308,28 +3458,18 @@ export {
|
|
|
3308
3458
|
setUserRole,
|
|
3309
3459
|
deleteUser,
|
|
3310
3460
|
updateUserProfile,
|
|
3461
|
+
migrateMindRoles,
|
|
3462
|
+
initConnectorManager,
|
|
3463
|
+
getConnectorManager,
|
|
3311
3464
|
stopAllWatchers,
|
|
3312
3465
|
getCachedSites,
|
|
3313
3466
|
getCachedRecentPages,
|
|
3314
|
-
initScheduler,
|
|
3315
|
-
getScheduler,
|
|
3316
|
-
initTokenBudget,
|
|
3317
|
-
getTokenBudget,
|
|
3318
|
-
startMindFull,
|
|
3319
|
-
stopMindFull,
|
|
3320
|
-
matchesGlob,
|
|
3321
|
-
SleepManager,
|
|
3322
|
-
initSleepManager,
|
|
3323
|
-
getSleepManager,
|
|
3324
|
-
getSleepManagerIfReady,
|
|
3325
|
-
subscribe2 as subscribe,
|
|
3326
|
-
publish2 as publish,
|
|
3327
3467
|
getWebhookUrl,
|
|
3328
3468
|
getAuthHeaders,
|
|
3329
3469
|
fireWebhook,
|
|
3330
3470
|
initWebhook,
|
|
3331
|
-
|
|
3332
|
-
|
|
3471
|
+
subscribe2 as subscribe,
|
|
3472
|
+
publish2 as publish,
|
|
3333
3473
|
createConversation,
|
|
3334
3474
|
getConversation,
|
|
3335
3475
|
getParticipants,
|
|
@@ -3349,6 +3489,22 @@ export {
|
|
|
3349
3489
|
leaveChannel,
|
|
3350
3490
|
getUnreadCounts,
|
|
3351
3491
|
markConversationRead,
|
|
3492
|
+
ensureSystemChannel,
|
|
3493
|
+
joinSystemChannel,
|
|
3494
|
+
announceToSystem,
|
|
3495
|
+
initScheduler,
|
|
3496
|
+
getScheduler,
|
|
3497
|
+
initTokenBudget,
|
|
3498
|
+
getTokenBudget,
|
|
3499
|
+
startMindFull,
|
|
3500
|
+
stopMindFull,
|
|
3501
|
+
matchesGlob,
|
|
3502
|
+
SleepManager,
|
|
3503
|
+
initSleepManager,
|
|
3504
|
+
getSleepManager,
|
|
3505
|
+
getSleepManagerIfReady,
|
|
3506
|
+
subscribe3 as subscribe2,
|
|
3507
|
+
publish3 as publish2,
|
|
3352
3508
|
getTypingMap,
|
|
3353
3509
|
publishTypingForChannels,
|
|
3354
3510
|
extractTextContent,
|