volute 0.25.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 +477 -6
- package/dist/{auth-HM2RSPY7.js → auth-4TV573WE.js} +2 -2
- package/dist/{channel-HZOSHGNF.js → channel-ZVZV42UD.js} +3 -3
- package/dist/{chunk-SHSWYG2J.js → chunk-2VO7453N.js} +56 -19
- package/dist/{chunk-PMX4EIJK.js → chunk-3CFRE2VC.js} +878 -741
- package/dist/{chunk-PHHKNGA3.js → chunk-3TV4GLFO.js} +2 -2
- package/dist/{chunk-BOTQ25QT.js → chunk-5Y3PBKW6.js} +2 -2
- package/dist/{chunk-BFK6SOEJ.js → chunk-J2CO4WEV.js} +1 -1
- package/dist/{chunk-ZSH4G2P5.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-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
- package/dist/chunk-TZKJLDQN.js +78 -0
- package/dist/{chunk-DG7TO7EE.js → chunk-USNBKHYG.js} +3 -3
- 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-PPBBJDY6.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-FDNOZEAD.js → daemon-restart-BJZ3O4U4.js} +6 -5
- package/dist/daemon.js +693 -265
- 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-TH26J76F.js → import-XIB7UV4S.js} +1 -1
- 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-XMGV3FUM.js → message-delivery-MS5JYPZX.js} +10 -8
- package/dist/{mind-YVWAHL2A.js → mind-HZ3QSDDJ.js} +17 -17
- package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-4G6FURY2.js} +3 -3
- package/dist/{mind-manager-4NDNAYAB.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-3HF5MXU2.js → package-VZWLXPHV.js} +2 -1
- package/dist/{pages-Y6DRWUOJ.js → pages-DIIT5HMQ.js} +1 -1
- package/dist/{publish-EEKTZBHW.js → publish-HQV7YREB.js} +3 -3
- 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-T3EMR6IR.js → skill-42LGFBQC.js} +3 -3
- 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/notes/SKILL.md +34 -0
- package/dist/{sleep-manager-RKTFZPD3.js → sleep-manager-EE4NRN2Q.js} +10 -8
- package/dist/{sprout-QJVGJDSH.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-AZQMC32A.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/index.html +2 -2
- 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 +2 -1
- package/templates/_base/.init/.config/hooks/wake-context.sh +7 -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-CJ26KQLN.js +0 -15
- package/dist/web-assets/assets/index-CGPSVu19.js +0 -69
- package/dist/web-assets/assets/index-V_rNDsM8.css +0 -1
|
@@ -4,27 +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
9
|
broadcast,
|
|
10
10
|
publish,
|
|
11
11
|
subscribe
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-J2CO4WEV.js";
|
|
13
13
|
import {
|
|
14
14
|
RestartTracker,
|
|
15
15
|
RotatingLog,
|
|
16
16
|
clearJsonMap,
|
|
17
17
|
getMindManager,
|
|
18
|
+
getMindToken,
|
|
18
19
|
getPrompt,
|
|
19
20
|
loadJsonMap,
|
|
20
21
|
saveJsonMap
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-2VO7453N.js";
|
|
22
23
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
loadMergedEnv
|
|
27
|
-
} from "./chunk-PHU4DEAJ.js";
|
|
24
|
+
isSandboxEnabled,
|
|
25
|
+
wrapForSandbox
|
|
26
|
+
} from "./chunk-UTL75LP6.js";
|
|
28
27
|
import {
|
|
29
28
|
conversationParticipants,
|
|
30
29
|
conversationReads,
|
|
@@ -34,18 +33,24 @@ import {
|
|
|
34
33
|
messages,
|
|
35
34
|
mindHistory,
|
|
36
35
|
users
|
|
37
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-WBHMQ5OZ.js";
|
|
38
37
|
import {
|
|
39
38
|
logger_default
|
|
40
39
|
} from "./chunk-YUIHSKR6.js";
|
|
40
|
+
import {
|
|
41
|
+
readVoluteConfig
|
|
42
|
+
} from "./chunk-SIAG3QMM.js";
|
|
43
|
+
import {
|
|
44
|
+
loadMergedEnv
|
|
45
|
+
} from "./chunk-PHU4DEAJ.js";
|
|
41
46
|
import {
|
|
42
47
|
exec
|
|
43
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-YJA7P64S.js";
|
|
44
49
|
import {
|
|
45
50
|
chownMindDir,
|
|
46
51
|
isIsolationEnabled,
|
|
47
52
|
wrapForIsolation
|
|
48
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-XOXLRRR2.js";
|
|
49
54
|
import {
|
|
50
55
|
daemonLoopback,
|
|
51
56
|
findMind,
|
|
@@ -57,7 +62,7 @@ import {
|
|
|
57
62
|
} from "./chunk-B2CPS4QU.js";
|
|
58
63
|
|
|
59
64
|
// src/lib/daemon/sleep-manager.ts
|
|
60
|
-
import { execFile } from "child_process";
|
|
65
|
+
import { execFile, spawn as spawnChild } from "child_process";
|
|
61
66
|
import {
|
|
62
67
|
existsSync as existsSync5,
|
|
63
68
|
mkdirSync as mkdirSync3,
|
|
@@ -70,11 +75,11 @@ import {
|
|
|
70
75
|
import { resolve as resolve8 } from "path";
|
|
71
76
|
import { promisify } from "util";
|
|
72
77
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
73
|
-
import { and as and4, eq as eq4, inArray as
|
|
78
|
+
import { and as and4, eq as eq4, inArray as inArray3 } from "drizzle-orm";
|
|
74
79
|
|
|
75
80
|
// src/lib/auth.ts
|
|
76
81
|
import { compareSync, hashSync } from "bcryptjs";
|
|
77
|
-
import { and, count, eq } from "drizzle-orm";
|
|
82
|
+
import { and, count, eq, inArray } from "drizzle-orm";
|
|
78
83
|
var userSelectFields = {
|
|
79
84
|
id: users.id,
|
|
80
85
|
username: users.username,
|
|
@@ -132,7 +137,7 @@ async function getOrCreateMindUser(mindName) {
|
|
|
132
137
|
const [result] = await db.insert(users).values({
|
|
133
138
|
username: mindName,
|
|
134
139
|
password_hash: "!mind",
|
|
135
|
-
role: "
|
|
140
|
+
role: "user",
|
|
136
141
|
user_type: "mind"
|
|
137
142
|
}).returning(userSelectFields);
|
|
138
143
|
return result;
|
|
@@ -197,6 +202,10 @@ async function syncMindProfile(mindName, config) {
|
|
|
197
202
|
await db.update(users).set(newProfile).where(eq(users.id, user.id));
|
|
198
203
|
broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
|
|
199
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"])));
|
|
208
|
+
}
|
|
200
209
|
|
|
201
210
|
// src/lib/pages-watcher.ts
|
|
202
211
|
import { existsSync, readdirSync, statSync, watch } from "fs";
|
|
@@ -403,160 +412,606 @@ function getCachedRecentPages() {
|
|
|
403
412
|
return recentPagesCache;
|
|
404
413
|
}
|
|
405
414
|
|
|
406
|
-
// src/lib/
|
|
407
|
-
import {
|
|
408
|
-
import {
|
|
409
|
-
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";
|
|
410
418
|
|
|
411
|
-
// src/lib/
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
displayName: "Slack",
|
|
435
|
-
description: "Connect to Slack via Socket Mode",
|
|
436
|
-
envVars: [
|
|
437
|
-
{
|
|
438
|
-
name: "SLACK_BOT_TOKEN",
|
|
439
|
-
required: true,
|
|
440
|
-
description: "Slack bot token (xoxb-...)",
|
|
441
|
-
scope: "mind"
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
name: "SLACK_APP_TOKEN",
|
|
445
|
-
required: true,
|
|
446
|
-
description: "Slack app-level token (xapp-...) for Socket Mode",
|
|
447
|
-
scope: "mind"
|
|
448
|
-
}
|
|
449
|
-
]
|
|
450
|
-
},
|
|
451
|
-
telegram: {
|
|
452
|
-
displayName: "Telegram",
|
|
453
|
-
description: "Connect to Telegram via long polling",
|
|
454
|
-
envVars: [
|
|
455
|
-
{
|
|
456
|
-
name: "TELEGRAM_BOT_TOKEN",
|
|
457
|
-
required: true,
|
|
458
|
-
description: "Telegram bot token from BotFather",
|
|
459
|
-
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}`);
|
|
460
442
|
}
|
|
461
|
-
|
|
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));
|
|
462
448
|
}
|
|
463
|
-
}
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
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
|
+
};
|
|
475
460
|
}
|
|
461
|
+
} catch {
|
|
462
|
+
slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
|
|
463
|
+
return () => {
|
|
464
|
+
};
|
|
476
465
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
});
|
|
481
479
|
}
|
|
482
480
|
|
|
483
|
-
// src/lib/
|
|
484
|
-
var
|
|
485
|
-
function
|
|
486
|
-
let
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
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);
|
|
491
488
|
}
|
|
492
|
-
|
|
489
|
+
set.add(callback);
|
|
490
|
+
return () => {
|
|
491
|
+
set.delete(callback);
|
|
492
|
+
if (set.size === 0) subscribers.delete(conversationId);
|
|
493
|
+
};
|
|
493
494
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
(type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
506
|
-
clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
507
|
-
})
|
|
508
|
-
)
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
checkConnectorEnv(type, mindName, mindDir2) {
|
|
512
|
-
const mindConnectorDir = resolve3(mindDir2, "connectors", type);
|
|
513
|
-
const userConnectorDir = resolve3(voluteHome(), "connectors", type);
|
|
514
|
-
const connectorDir = existsSync3(mindConnectorDir) ? mindConnectorDir : existsSync3(userConnectorDir) ? userConnectorDir : void 0;
|
|
515
|
-
const def = getConnectorDef(type, connectorDir);
|
|
516
|
-
if (!def) return null;
|
|
517
|
-
const env = loadMergedEnv(mindName);
|
|
518
|
-
const missing = checkMissingEnvVars(def, env);
|
|
519
|
-
if (missing.length === 0) return null;
|
|
520
|
-
return {
|
|
521
|
-
missing: missing.map((v) => ({ name: v.name, description: v.description })),
|
|
522
|
-
connectorName: def.displayName
|
|
523
|
-
};
|
|
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
|
+
}
|
|
524
506
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
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
|
+
);
|
|
552
533
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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;
|
|
1014
|
+
if (existsSync3(mindConnector)) {
|
|
560
1015
|
connectorScript = mindConnector;
|
|
561
1016
|
runtime = resolve3(mindDir2, "node_modules", ".bin", "tsx");
|
|
562
1017
|
} else if (existsSync3(userConnector)) {
|
|
@@ -597,12 +1052,24 @@ var ConnectorManager = class {
|
|
|
597
1052
|
VOLUTE_MIND_DIR: mindDir2,
|
|
598
1053
|
...daemonPort ? {
|
|
599
1054
|
VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
|
|
600
|
-
VOLUTE_DAEMON_TOKEN:
|
|
1055
|
+
VOLUTE_DAEMON_TOKEN: getMindToken(mindName) ?? void 0
|
|
601
1056
|
} : {},
|
|
602
1057
|
...connectorEnv
|
|
603
1058
|
}
|
|
604
1059
|
};
|
|
605
|
-
|
|
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
|
+
}
|
|
606
1073
|
const child = spawn(spawnCmd, spawnArgs, spawnOpts);
|
|
607
1074
|
let lastStderr = "";
|
|
608
1075
|
child.stdout?.pipe(logStream);
|
|
@@ -666,541 +1133,123 @@ var ConnectorManager = class {
|
|
|
666
1133
|
} catch {
|
|
667
1134
|
}
|
|
668
1135
|
resolve9();
|
|
669
|
-
}, 5e3);
|
|
670
|
-
});
|
|
671
|
-
this.stopping.delete(stopKey);
|
|
672
|
-
this.restartTracker.reset(stopKey);
|
|
673
|
-
try {
|
|
674
|
-
this.removeConnectorPid(mindName, type);
|
|
675
|
-
} catch (err) {
|
|
676
|
-
clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
|
|
677
|
-
}
|
|
678
|
-
clog.info(`stopped connector ${type} for ${mindName}`);
|
|
679
|
-
}
|
|
680
|
-
async stopConnectors(mindName) {
|
|
681
|
-
const mindMap = this.connectors.get(mindName);
|
|
682
|
-
if (!mindMap) return;
|
|
683
|
-
const types = [...mindMap.keys()];
|
|
684
|
-
await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
|
|
685
|
-
this.connectors.delete(mindName);
|
|
686
|
-
}
|
|
687
|
-
async stopAll() {
|
|
688
|
-
this.shuttingDown = true;
|
|
689
|
-
const minds = [...this.connectors.keys()];
|
|
690
|
-
await Promise.all(minds.map((name) => this.stopConnectors(name)));
|
|
691
|
-
}
|
|
692
|
-
getConnectorStatus(mindName) {
|
|
693
|
-
const mindMap = this.connectors.get(mindName);
|
|
694
|
-
if (!mindMap) return [];
|
|
695
|
-
return [...mindMap.entries()].map(([type, tracked]) => ({
|
|
696
|
-
type,
|
|
697
|
-
running: !tracked.child.killed
|
|
698
|
-
}));
|
|
699
|
-
}
|
|
700
|
-
connectorPidPath(mindName, type) {
|
|
701
|
-
return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
|
|
702
|
-
}
|
|
703
|
-
saveConnectorPid(mindName, type, pid) {
|
|
704
|
-
const pidPath = this.connectorPidPath(mindName, type);
|
|
705
|
-
mkdirSync(dirname(pidPath), { recursive: true });
|
|
706
|
-
writeFileSync(pidPath, String(pid));
|
|
707
|
-
}
|
|
708
|
-
removeConnectorPid(mindName, type) {
|
|
709
|
-
try {
|
|
710
|
-
unlinkSync(this.connectorPidPath(mindName, type));
|
|
711
|
-
} catch {
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
killOrphanConnector(mindName, type) {
|
|
715
|
-
const pidPath = this.connectorPidPath(mindName, type);
|
|
716
|
-
if (!existsSync3(pidPath)) return;
|
|
717
|
-
try {
|
|
718
|
-
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
719
|
-
if (pid > 0) {
|
|
720
|
-
try {
|
|
721
|
-
process.kill(-pid, "SIGTERM");
|
|
722
|
-
} catch {
|
|
723
|
-
process.kill(pid, "SIGTERM");
|
|
724
|
-
}
|
|
725
|
-
clog.warn(`killed orphan connector ${type} (pid ${pid})`);
|
|
726
|
-
}
|
|
727
|
-
} catch {
|
|
728
|
-
}
|
|
729
|
-
try {
|
|
730
|
-
unlinkSync(pidPath);
|
|
731
|
-
} catch {
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
resolveBuiltinConnector(type) {
|
|
735
|
-
return searchUpwards("connectors", `${type}.js`);
|
|
736
|
-
}
|
|
737
|
-
resolveVoluteTsx() {
|
|
738
|
-
return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
var instance = null;
|
|
742
|
-
function initConnectorManager() {
|
|
743
|
-
if (instance) throw new Error("ConnectorManager already initialized");
|
|
744
|
-
instance = new ConnectorManager();
|
|
745
|
-
return instance;
|
|
746
|
-
}
|
|
747
|
-
function getConnectorManager() {
|
|
748
|
-
if (!instance)
|
|
749
|
-
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
750
|
-
return instance;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// src/lib/events/mind-events.ts
|
|
754
|
-
var subscribers = /* @__PURE__ */ new Map();
|
|
755
|
-
function subscribe2(mind, callback) {
|
|
756
|
-
let set = subscribers.get(mind);
|
|
757
|
-
if (!set) {
|
|
758
|
-
set = /* @__PURE__ */ new Set();
|
|
759
|
-
subscribers.set(mind, set);
|
|
760
|
-
}
|
|
761
|
-
set.add(callback);
|
|
762
|
-
return () => {
|
|
763
|
-
set.delete(callback);
|
|
764
|
-
if (set.size === 0) subscribers.delete(mind);
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
function publish2(mind, event) {
|
|
768
|
-
const set = subscribers.get(mind);
|
|
769
|
-
if (!set) return;
|
|
770
|
-
for (const cb of set) {
|
|
771
|
-
try {
|
|
772
|
-
cb(event);
|
|
773
|
-
} catch (err) {
|
|
774
|
-
console.error("[mind-events] subscriber threw:", err);
|
|
775
|
-
set.delete(cb);
|
|
776
|
-
if (set.size === 0) subscribers.delete(mind);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// src/lib/delivery/delivery-manager.ts
|
|
782
|
-
import { readFile, realpath } from "fs/promises";
|
|
783
|
-
import { extname, resolve as resolve5 } from "path";
|
|
784
|
-
import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
|
|
785
|
-
|
|
786
|
-
// src/lib/events/conversations.ts
|
|
787
|
-
import { randomUUID } from "crypto";
|
|
788
|
-
import { and as and2, desc, eq as eq2, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
789
|
-
|
|
790
|
-
// src/lib/webhook.ts
|
|
791
|
-
var slog = logger_default.child("webhook");
|
|
792
|
-
function getWebhookUrl() {
|
|
793
|
-
return process.env.VOLUTE_WEBHOOK_URL;
|
|
794
|
-
}
|
|
795
|
-
function getAuthHeaders() {
|
|
796
|
-
const headers = { "Content-Type": "application/json" };
|
|
797
|
-
const secret = process.env.VOLUTE_WEBHOOK_SECRET;
|
|
798
|
-
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
799
|
-
return headers;
|
|
800
|
-
}
|
|
801
|
-
function fireWebhook(event) {
|
|
802
|
-
try {
|
|
803
|
-
const url = getWebhookUrl();
|
|
804
|
-
if (!url) return;
|
|
805
|
-
const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
806
|
-
fetch(url, {
|
|
807
|
-
method: "POST",
|
|
808
|
-
headers: getAuthHeaders(),
|
|
809
|
-
body: JSON.stringify(payload)
|
|
810
|
-
}).then((res) => {
|
|
811
|
-
if (!res.ok) {
|
|
812
|
-
slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
|
|
813
|
-
}
|
|
814
|
-
}).catch((err) => {
|
|
815
|
-
slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
|
|
816
|
-
});
|
|
817
|
-
} catch (err) {
|
|
818
|
-
slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
function initWebhook() {
|
|
822
|
-
const url = getWebhookUrl();
|
|
823
|
-
if (!url) return () => {
|
|
824
|
-
};
|
|
825
|
-
try {
|
|
826
|
-
const parsed = new URL(url);
|
|
827
|
-
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
828
|
-
slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
|
|
829
|
-
return () => {
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
} catch {
|
|
833
|
-
slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
|
|
834
|
-
return () => {
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
slog.info("webhook enabled");
|
|
838
|
-
return subscribe((event) => {
|
|
1136
|
+
}, 5e3);
|
|
1137
|
+
});
|
|
1138
|
+
this.stopping.delete(stopKey);
|
|
1139
|
+
this.restartTracker.reset(stopKey);
|
|
839
1140
|
try {
|
|
840
|
-
|
|
841
|
-
event: event.type,
|
|
842
|
-
mind: event.mind,
|
|
843
|
-
data: { summary: event.summary, ...event.metadata },
|
|
844
|
-
timestamp: event.created_at
|
|
845
|
-
});
|
|
1141
|
+
this.removeConnectorPid(mindName, type);
|
|
846
1142
|
} catch (err) {
|
|
847
|
-
|
|
1143
|
+
clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
|
|
848
1144
|
}
|
|
849
|
-
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/lib/events/conversation-events.ts
|
|
853
|
-
var subscribers2 = /* @__PURE__ */ new Map();
|
|
854
|
-
function subscribe3(conversationId, callback) {
|
|
855
|
-
let set = subscribers2.get(conversationId);
|
|
856
|
-
if (!set) {
|
|
857
|
-
set = /* @__PURE__ */ new Set();
|
|
858
|
-
subscribers2.set(conversationId, set);
|
|
1145
|
+
clog.info(`stopped connector ${type} for ${mindName}`);
|
|
859
1146
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
function publish3(conversationId, event) {
|
|
867
|
-
const set = subscribers2.get(conversationId);
|
|
868
|
-
if (!set) return;
|
|
869
|
-
for (const cb of set) {
|
|
870
|
-
try {
|
|
871
|
-
cb(event);
|
|
872
|
-
} catch (err) {
|
|
873
|
-
console.error("[conversation-events] subscriber threw:", err);
|
|
874
|
-
set.delete(cb);
|
|
875
|
-
if (set.size === 0) subscribers2.delete(conversationId);
|
|
876
|
-
}
|
|
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);
|
|
877
1153
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
await tx.insert(conversations).values({
|
|
888
|
-
id,
|
|
889
|
-
mind_name: mindName,
|
|
890
|
-
channel,
|
|
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]) => ({
|
|
891
1163
|
type,
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
title: opts?.title ?? null
|
|
895
|
-
});
|
|
896
|
-
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
897
|
-
await tx.insert(conversationParticipants).values(
|
|
898
|
-
opts.participantIds.map((uid, i) => ({
|
|
899
|
-
conversation_id: id,
|
|
900
|
-
user_id: uid,
|
|
901
|
-
role: i === 0 ? "owner" : "member"
|
|
902
|
-
}))
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
});
|
|
906
|
-
fireWebhook({
|
|
907
|
-
event: "conversation_created",
|
|
908
|
-
mind: mindName ?? "",
|
|
909
|
-
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
910
|
-
});
|
|
911
|
-
return {
|
|
912
|
-
id,
|
|
913
|
-
mind_name: mindName,
|
|
914
|
-
channel,
|
|
915
|
-
type,
|
|
916
|
-
name,
|
|
917
|
-
user_id: opts?.userId ?? null,
|
|
918
|
-
title: opts?.title ?? null,
|
|
919
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
920
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
async function getConversation(id) {
|
|
924
|
-
const db = await getDb();
|
|
925
|
-
const row = await db.select().from(conversations).where(eq2(conversations.id, id)).get();
|
|
926
|
-
return row ?? null;
|
|
927
|
-
}
|
|
928
|
-
async function addParticipant(conversationId, userId, role = "member") {
|
|
929
|
-
const db = await getDb();
|
|
930
|
-
await db.insert(conversationParticipants).values({
|
|
931
|
-
conversation_id: conversationId,
|
|
932
|
-
user_id: userId,
|
|
933
|
-
role
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
async function removeParticipant(conversationId, userId) {
|
|
937
|
-
const db = await getDb();
|
|
938
|
-
await db.delete(conversationParticipants).where(
|
|
939
|
-
and2(
|
|
940
|
-
eq2(conversationParticipants.conversation_id, conversationId),
|
|
941
|
-
eq2(conversationParticipants.user_id, userId)
|
|
942
|
-
)
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
async function getParticipants(conversationId) {
|
|
946
|
-
const db = await getDb();
|
|
947
|
-
const rows = await db.select({
|
|
948
|
-
userId: conversationParticipants.user_id,
|
|
949
|
-
username: users.username,
|
|
950
|
-
userType: users.user_type,
|
|
951
|
-
role: conversationParticipants.role,
|
|
952
|
-
displayName: users.display_name,
|
|
953
|
-
description: users.description,
|
|
954
|
-
avatar: users.avatar
|
|
955
|
-
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(eq2(conversationParticipants.conversation_id, conversationId)).all();
|
|
956
|
-
return rows;
|
|
957
|
-
}
|
|
958
|
-
async function isParticipant(conversationId, userId) {
|
|
959
|
-
const db = await getDb();
|
|
960
|
-
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
961
|
-
and2(
|
|
962
|
-
eq2(conversationParticipants.conversation_id, conversationId),
|
|
963
|
-
eq2(conversationParticipants.user_id, userId)
|
|
964
|
-
)
|
|
965
|
-
).get();
|
|
966
|
-
return row != null;
|
|
967
|
-
}
|
|
968
|
-
async function listConversationsForUser(userId) {
|
|
969
|
-
const db = await getDb();
|
|
970
|
-
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq2(conversationParticipants.user_id, userId)).all();
|
|
971
|
-
if (participantRows.length === 0) return [];
|
|
972
|
-
const convIds = participantRows.map((r) => r.conversation_id);
|
|
973
|
-
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
974
|
-
}
|
|
975
|
-
async function isParticipantOrOwner(conversationId, userId) {
|
|
976
|
-
if (await isParticipant(conversationId, userId)) return true;
|
|
977
|
-
const db = await getDb();
|
|
978
|
-
const row = await db.select().from(conversations).where(and2(eq2(conversations.id, conversationId), eq2(conversations.user_id, userId))).get();
|
|
979
|
-
return row != null;
|
|
980
|
-
}
|
|
981
|
-
async function deleteConversationForUser(id, userId) {
|
|
982
|
-
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
983
|
-
await deleteConversation(id);
|
|
984
|
-
return true;
|
|
985
|
-
}
|
|
986
|
-
async function addMessage(conversationId, role, senderName, content) {
|
|
987
|
-
const db = await getDb();
|
|
988
|
-
const serialized = JSON.stringify(content);
|
|
989
|
-
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 });
|
|
990
|
-
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq2(conversations.id, conversationId));
|
|
991
|
-
if (role === "user") {
|
|
992
|
-
const firstText = content.find((b) => b.type === "text");
|
|
993
|
-
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
994
|
-
if (title) {
|
|
995
|
-
await db.update(conversations).set({ title }).where(and2(eq2(conversations.id, conversationId), isNull(conversations.title)));
|
|
996
|
-
}
|
|
1164
|
+
running: !tracked.child.killed
|
|
1165
|
+
}));
|
|
997
1166
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
conversation_id: conversationId,
|
|
1001
|
-
role,
|
|
1002
|
-
sender_name: senderName,
|
|
1003
|
-
content,
|
|
1004
|
-
created_at: result.created_at
|
|
1005
|
-
};
|
|
1006
|
-
publish3(conversationId, {
|
|
1007
|
-
type: "message",
|
|
1008
|
-
id: msg.id,
|
|
1009
|
-
role: msg.role,
|
|
1010
|
-
senderName: msg.sender_name,
|
|
1011
|
-
content: msg.content,
|
|
1012
|
-
createdAt: msg.created_at
|
|
1013
|
-
});
|
|
1014
|
-
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq2(conversations.id, conversationId)).get();
|
|
1015
|
-
fireWebhook({
|
|
1016
|
-
event: "message_created",
|
|
1017
|
-
mind: conv?.mind_name ?? "",
|
|
1018
|
-
data: {
|
|
1019
|
-
conversationId,
|
|
1020
|
-
messageId: result.id,
|
|
1021
|
-
role,
|
|
1022
|
-
senderName,
|
|
1023
|
-
content: content.filter((b) => b.type !== "image"),
|
|
1024
|
-
createdAt: result.created_at
|
|
1025
|
-
}
|
|
1026
|
-
});
|
|
1027
|
-
return msg;
|
|
1028
|
-
}
|
|
1029
|
-
async function getMessages(conversationId) {
|
|
1030
|
-
const db = await getDb();
|
|
1031
|
-
const rows = await db.select().from(messages).where(eq2(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1032
|
-
return rows.map(parseMessageRow);
|
|
1033
|
-
}
|
|
1034
|
-
async function getMessagesPaginated(conversationId, opts) {
|
|
1035
|
-
const db = await getDb();
|
|
1036
|
-
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
1037
|
-
const conditions = [eq2(messages.conversation_id, conversationId)];
|
|
1038
|
-
if (opts?.before != null) {
|
|
1039
|
-
conditions.push(lt(messages.id, opts.before));
|
|
1167
|
+
connectorPidPath(mindName, type) {
|
|
1168
|
+
return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
|
|
1040
1169
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
messages: page.map(parseMessageRow),
|
|
1046
|
-
hasMore
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
function parseMessageRow(row) {
|
|
1050
|
-
let content;
|
|
1051
|
-
try {
|
|
1052
|
-
const parsed = JSON.parse(row.content);
|
|
1053
|
-
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
1054
|
-
} catch {
|
|
1055
|
-
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));
|
|
1056
1174
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (convs.length === 0) return [];
|
|
1062
|
-
const db = await getDb();
|
|
1063
|
-
const convIds = convs.map((c) => c.id);
|
|
1064
|
-
const rows = await db.select({
|
|
1065
|
-
conversationId: conversationParticipants.conversation_id,
|
|
1066
|
-
userId: users.id,
|
|
1067
|
-
username: users.username,
|
|
1068
|
-
userType: users.user_type,
|
|
1069
|
-
role: conversationParticipants.role,
|
|
1070
|
-
displayName: users.display_name,
|
|
1071
|
-
description: users.description,
|
|
1072
|
-
avatar: users.avatar
|
|
1073
|
-
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
1074
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
1075
|
-
for (const r of rows) {
|
|
1076
|
-
let arr = byConv.get(r.conversationId);
|
|
1077
|
-
if (!arr) {
|
|
1078
|
-
arr = [];
|
|
1079
|
-
byConv.set(r.conversationId, arr);
|
|
1175
|
+
removeConnectorPid(mindName, type) {
|
|
1176
|
+
try {
|
|
1177
|
+
unlinkSync(this.connectorPidPath(mindName, type));
|
|
1178
|
+
} catch {
|
|
1080
1179
|
}
|
|
1081
|
-
arr.push({
|
|
1082
|
-
userId: r.userId,
|
|
1083
|
-
username: r.username,
|
|
1084
|
-
userType: r.userType,
|
|
1085
|
-
role: r.role,
|
|
1086
|
-
displayName: r.displayName,
|
|
1087
|
-
description: r.description,
|
|
1088
|
-
avatar: r.avatar
|
|
1089
|
-
});
|
|
1090
1180
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
for (const m of msgRows) {
|
|
1104
|
-
let text = "";
|
|
1105
|
-
try {
|
|
1106
|
-
const parsed = JSON.parse(m.content);
|
|
1107
|
-
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
1108
|
-
const textBlock = blocks.find((b) => b.type === "text");
|
|
1109
|
-
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
1110
|
-
} catch {
|
|
1111
|
-
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})`);
|
|
1112
1193
|
}
|
|
1113
|
-
|
|
1114
|
-
role: m.role,
|
|
1115
|
-
senderName: m.sender_name,
|
|
1116
|
-
text,
|
|
1117
|
-
createdAt: m.created_at
|
|
1118
|
-
});
|
|
1194
|
+
} catch {
|
|
1119
1195
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
participants: byConv.get(c.id) ?? [],
|
|
1124
|
-
lastMessage: byLastMsg.get(c.id)
|
|
1125
|
-
}));
|
|
1126
|
-
}
|
|
1127
|
-
async function findDMConversation(mindName, participantIds) {
|
|
1128
|
-
const db = await getDb();
|
|
1129
|
-
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq2(conversations.mind_name, mindName), eq2(conversations.type, "dm"))).all();
|
|
1130
|
-
for (const conv of mindConvs) {
|
|
1131
|
-
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq2(conversationParticipants.conversation_id, conv.id)).all();
|
|
1132
|
-
if (rows.length !== 2) continue;
|
|
1133
|
-
const ids = new Set(rows.map((r) => r.user_id));
|
|
1134
|
-
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
1135
|
-
return conv.id;
|
|
1196
|
+
try {
|
|
1197
|
+
unlinkSync(pidPath);
|
|
1198
|
+
} catch {
|
|
1136
1199
|
}
|
|
1137
1200
|
}
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
participantIds
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
async function getChannelByName(name) {
|
|
1154
|
-
const db = await getDb();
|
|
1155
|
-
const row = await db.select().from(conversations).where(and2(eq2(conversations.name, name), eq2(conversations.type, "channel"))).get();
|
|
1156
|
-
return row ?? null;
|
|
1157
|
-
}
|
|
1158
|
-
async function listChannels() {
|
|
1159
|
-
const db = await getDb();
|
|
1160
|
-
return await db.select().from(conversations).where(eq2(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
1161
|
-
}
|
|
1162
|
-
async function joinChannel(conversationId, userId) {
|
|
1163
|
-
if (await isParticipant(conversationId, userId)) return;
|
|
1164
|
-
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;
|
|
1165
1213
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1214
|
+
function getConnectorManager() {
|
|
1215
|
+
if (!instance)
|
|
1216
|
+
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
1217
|
+
return instance;
|
|
1168
1218
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
and2(
|
|
1178
|
-
eq2(conversationReads.conversation_id, messages.conversation_id),
|
|
1179
|
-
eq2(conversationReads.user_id, userId)
|
|
1180
|
-
)
|
|
1181
|
-
).where(
|
|
1182
|
-
and2(
|
|
1183
|
-
inArray(messages.conversation_id, conversationIds),
|
|
1184
|
-
sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
|
|
1185
|
-
)
|
|
1186
|
-
).groupBy(messages.conversation_id);
|
|
1187
|
-
const result = {};
|
|
1188
|
-
for (const row of rows) {
|
|
1189
|
-
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);
|
|
1190
1227
|
}
|
|
1191
|
-
|
|
1228
|
+
set.add(callback);
|
|
1229
|
+
return () => {
|
|
1230
|
+
set.delete(callback);
|
|
1231
|
+
if (set.size === 0) subscribers2.delete(mind);
|
|
1232
|
+
};
|
|
1192
1233
|
}
|
|
1193
|
-
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
+
}
|
|
1202
1246
|
}
|
|
1203
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
|
+
|
|
1204
1253
|
// src/lib/typing.ts
|
|
1205
1254
|
var DEFAULT_TTL_MS = 1e4;
|
|
1206
1255
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -1286,7 +1335,7 @@ function publishTypingForChannels(channels, map) {
|
|
|
1286
1335
|
for (const channel of channels) {
|
|
1287
1336
|
if (channel.startsWith(VOLUTE_PREFIX)) {
|
|
1288
1337
|
const conversationId = channel.slice(VOLUTE_PREFIX.length);
|
|
1289
|
-
|
|
1338
|
+
publish2(conversationId, { type: "typing", senders: map.get(channel) });
|
|
1290
1339
|
}
|
|
1291
1340
|
}
|
|
1292
1341
|
}
|
|
@@ -2105,7 +2154,7 @@ async function recordInbound(mind, channel, sender, content) {
|
|
|
2105
2154
|
} catch (err) {
|
|
2106
2155
|
dlog3.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
|
|
2107
2156
|
}
|
|
2108
|
-
|
|
2157
|
+
publish3(mind, {
|
|
2109
2158
|
mind,
|
|
2110
2159
|
type: "inbound",
|
|
2111
2160
|
channel,
|
|
@@ -2456,6 +2505,18 @@ var Scheduler = class {
|
|
|
2456
2505
|
return false;
|
|
2457
2506
|
}
|
|
2458
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
|
+
}
|
|
2459
2520
|
try {
|
|
2460
2521
|
let text;
|
|
2461
2522
|
if (schedule.script) {
|
|
@@ -2481,7 +2542,7 @@ ${stderr}` : ""}`;
|
|
|
2481
2542
|
}
|
|
2482
2543
|
await this.deliver(mindName, {
|
|
2483
2544
|
content: [{ type: "text", text }],
|
|
2484
|
-
channel: "system:scheduler",
|
|
2545
|
+
channel: schedule.channel ?? "system:scheduler",
|
|
2485
2546
|
sender: schedule.id
|
|
2486
2547
|
});
|
|
2487
2548
|
slog2.info(`fired "${schedule.id}" for ${mindName}`);
|
|
@@ -2729,6 +2790,9 @@ async function startMindFull(name) {
|
|
|
2729
2790
|
(err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
|
|
2730
2791
|
);
|
|
2731
2792
|
}
|
|
2793
|
+
joinSystemChannelForMind(baseName).catch(
|
|
2794
|
+
(err) => logger_default.error(`failed to join #system for ${baseName}`, logger_default.errorData(err))
|
|
2795
|
+
);
|
|
2732
2796
|
if (config?.tokenBudget) {
|
|
2733
2797
|
getTokenBudget().setBudget(
|
|
2734
2798
|
baseName,
|
|
@@ -2781,7 +2845,8 @@ function defaultState() {
|
|
|
2781
2845
|
scheduledWakeAt: null,
|
|
2782
2846
|
wokenByTrigger: false,
|
|
2783
2847
|
voluntaryWakeAt: null,
|
|
2784
|
-
queuedMessageCount: 0
|
|
2848
|
+
queuedMessageCount: 0,
|
|
2849
|
+
triggerWakeHistory: []
|
|
2785
2850
|
};
|
|
2786
2851
|
}
|
|
2787
2852
|
function formatCurrentDate() {
|
|
@@ -2828,6 +2893,7 @@ var SleepManager = class {
|
|
|
2828
2893
|
if (existsSync5(this.statePath)) {
|
|
2829
2894
|
const data = JSON.parse(readFileSync5(this.statePath, "utf-8"));
|
|
2830
2895
|
for (const [name, state] of Object.entries(data)) {
|
|
2896
|
+
state.triggerWakeHistory ??= [];
|
|
2831
2897
|
this.states.set(name, state);
|
|
2832
2898
|
}
|
|
2833
2899
|
}
|
|
@@ -2857,6 +2923,16 @@ var SleepManager = class {
|
|
|
2857
2923
|
getState(name) {
|
|
2858
2924
|
return this.states.get(name) ?? defaultState();
|
|
2859
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
|
+
}
|
|
2860
2936
|
getSleepConfig(name) {
|
|
2861
2937
|
const dir = mindDir(name);
|
|
2862
2938
|
const config = readVoluteConfig(dir);
|
|
@@ -2925,15 +3001,6 @@ var SleepManager = class {
|
|
|
2925
3001
|
if (this.transitioning.has(name)) return;
|
|
2926
3002
|
this.transitioning.add(name);
|
|
2927
3003
|
try {
|
|
2928
|
-
const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
|
|
2929
|
-
const now = /* @__PURE__ */ new Date();
|
|
2930
|
-
const duration = formatDuration(sleepingSince, now);
|
|
2931
|
-
const currentDate = formatCurrentDate();
|
|
2932
|
-
const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
|
|
2933
|
-
hour: "numeric",
|
|
2934
|
-
minute: "2-digit"
|
|
2935
|
-
});
|
|
2936
|
-
const queuedSummary = await this.buildQueuedSummary(name);
|
|
2937
3004
|
try {
|
|
2938
3005
|
await wakeMind(name);
|
|
2939
3006
|
} catch (err) {
|
|
@@ -2942,46 +3009,59 @@ var SleepManager = class {
|
|
|
2942
3009
|
}
|
|
2943
3010
|
const entry = findMind(name);
|
|
2944
3011
|
if (!entry) return;
|
|
2945
|
-
let summaryText;
|
|
2946
3012
|
if (opts?.trigger) {
|
|
2947
3013
|
state.wokenByTrigger = true;
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
sleepTime,
|
|
2952
|
-
duration,
|
|
2953
|
-
queuedSummary
|
|
3014
|
+
state.triggerWakeHistory.push({
|
|
3015
|
+
channel: opts.trigger.channel,
|
|
3016
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2954
3017
|
});
|
|
3018
|
+
this.saveState();
|
|
2955
3019
|
} else {
|
|
2956
|
-
|
|
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", {
|
|
2957
3037
|
currentDate,
|
|
2958
3038
|
sleepTime,
|
|
2959
3039
|
duration,
|
|
2960
|
-
|
|
2961
|
-
});
|
|
2962
|
-
}
|
|
2963
|
-
try {
|
|
2964
|
-
const db = await getDb();
|
|
2965
|
-
await db.insert(mindHistory).values({
|
|
2966
|
-
mind: name,
|
|
2967
|
-
type: "inbound",
|
|
2968
|
-
channel: "system:sleep",
|
|
2969
|
-
content: summaryText
|
|
2970
|
-
});
|
|
2971
|
-
} catch (err) {
|
|
2972
|
-
slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
|
|
2973
|
-
}
|
|
2974
|
-
try {
|
|
2975
|
-
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
2976
|
-
method: "POST",
|
|
2977
|
-
headers: { "Content-Type": "application/json" },
|
|
2978
|
-
body: JSON.stringify({
|
|
2979
|
-
content: [{ type: "text", text: summaryText }],
|
|
2980
|
-
channel: "system:sleep"
|
|
2981
|
-
})
|
|
3040
|
+
sleepActivity
|
|
2982
3041
|
});
|
|
2983
|
-
|
|
2984
|
-
|
|
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
|
+
}
|
|
2985
3065
|
}
|
|
2986
3066
|
const flushed = await this.flushQueuedMessages(name);
|
|
2987
3067
|
if (flushed > 0) {
|
|
@@ -3047,7 +3127,7 @@ var SleepManager = class {
|
|
|
3047
3127
|
const db = await getDb();
|
|
3048
3128
|
const rows = await db.select().from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
3049
3129
|
if (rows.length === 0) return 0;
|
|
3050
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
3130
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-MS5JYPZX.js");
|
|
3051
3131
|
const delivered = [];
|
|
3052
3132
|
for (const row of rows) {
|
|
3053
3133
|
try {
|
|
@@ -3058,7 +3138,7 @@ var SleepManager = class {
|
|
|
3058
3138
|
}
|
|
3059
3139
|
}
|
|
3060
3140
|
if (delivered.length > 0) {
|
|
3061
|
-
await db.delete(deliveryQueue).where(
|
|
3141
|
+
await db.delete(deliveryQueue).where(inArray3(deliveryQueue.id, delivered));
|
|
3062
3142
|
}
|
|
3063
3143
|
const state = this.states.get(name);
|
|
3064
3144
|
if (state) {
|
|
@@ -3079,7 +3159,8 @@ var SleepManager = class {
|
|
|
3079
3159
|
scheduledWakeAt: this.getNextWakeTime(sleepConfig),
|
|
3080
3160
|
wokenByTrigger: false,
|
|
3081
3161
|
voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
|
|
3082
|
-
queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0
|
|
3162
|
+
queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
|
|
3163
|
+
triggerWakeHistory: []
|
|
3083
3164
|
};
|
|
3084
3165
|
this.states.set(name, state);
|
|
3085
3166
|
this.saveState();
|
|
@@ -3196,18 +3277,70 @@ var SleepManager = class {
|
|
|
3196
3277
|
}
|
|
3197
3278
|
}
|
|
3198
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
|
+
}
|
|
3199
3329
|
async buildQueuedSummary(name) {
|
|
3200
3330
|
try {
|
|
3201
3331
|
const db = await getDb();
|
|
3202
|
-
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();
|
|
3203
3333
|
if (rows.length === 0) return "No messages arrived while you slept.";
|
|
3204
3334
|
const channelCounts = /* @__PURE__ */ new Map();
|
|
3335
|
+
const senders = /* @__PURE__ */ new Set();
|
|
3205
3336
|
for (const row of rows) {
|
|
3206
3337
|
const ch = row.channel ?? "unknown";
|
|
3207
3338
|
channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
|
|
3339
|
+
if (row.sender) senders.add(row.sender);
|
|
3208
3340
|
}
|
|
3209
3341
|
const parts = [...channelCounts.entries()].map(([ch, count2]) => `${count2} on ${ch}`);
|
|
3210
|
-
|
|
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.`;
|
|
3211
3344
|
} catch (err) {
|
|
3212
3345
|
slog3.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
|
|
3213
3346
|
return "Unable to check for queued messages \u2014 there may be messages waiting.";
|
|
@@ -3310,8 +3443,6 @@ function getSleepManagerIfReady() {
|
|
|
3310
3443
|
}
|
|
3311
3444
|
|
|
3312
3445
|
export {
|
|
3313
|
-
initConnectorManager,
|
|
3314
|
-
getConnectorManager,
|
|
3315
3446
|
createUser,
|
|
3316
3447
|
verifyUser,
|
|
3317
3448
|
getUser,
|
|
@@ -3327,28 +3458,18 @@ export {
|
|
|
3327
3458
|
setUserRole,
|
|
3328
3459
|
deleteUser,
|
|
3329
3460
|
updateUserProfile,
|
|
3461
|
+
migrateMindRoles,
|
|
3462
|
+
initConnectorManager,
|
|
3463
|
+
getConnectorManager,
|
|
3330
3464
|
stopAllWatchers,
|
|
3331
3465
|
getCachedSites,
|
|
3332
3466
|
getCachedRecentPages,
|
|
3333
|
-
initScheduler,
|
|
3334
|
-
getScheduler,
|
|
3335
|
-
initTokenBudget,
|
|
3336
|
-
getTokenBudget,
|
|
3337
|
-
startMindFull,
|
|
3338
|
-
stopMindFull,
|
|
3339
|
-
matchesGlob,
|
|
3340
|
-
SleepManager,
|
|
3341
|
-
initSleepManager,
|
|
3342
|
-
getSleepManager,
|
|
3343
|
-
getSleepManagerIfReady,
|
|
3344
|
-
subscribe2 as subscribe,
|
|
3345
|
-
publish2 as publish,
|
|
3346
3467
|
getWebhookUrl,
|
|
3347
3468
|
getAuthHeaders,
|
|
3348
3469
|
fireWebhook,
|
|
3349
3470
|
initWebhook,
|
|
3350
|
-
|
|
3351
|
-
|
|
3471
|
+
subscribe2 as subscribe,
|
|
3472
|
+
publish2 as publish,
|
|
3352
3473
|
createConversation,
|
|
3353
3474
|
getConversation,
|
|
3354
3475
|
getParticipants,
|
|
@@ -3368,6 +3489,22 @@ export {
|
|
|
3368
3489
|
leaveChannel,
|
|
3369
3490
|
getUnreadCounts,
|
|
3370
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,
|
|
3371
3508
|
getTypingMap,
|
|
3372
3509
|
publishTypingForChannels,
|
|
3373
3510
|
extractTextContent,
|