volute 0.26.0 → 0.27.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 +13 -13
- package/dist/{activity-events-ZMBAKLUF.js → activity-events-BBIEA2F4.js} +2 -3
- package/dist/api.d.ts +363 -168
- package/dist/{archive-4ZQYK5MN.js → archive-UA4BDFXQ.js} +2 -2
- package/dist/{auth-4TV573WE.js → auth-D3OT2ARB.js} +3 -3
- package/dist/bridge-FQHZL3MC.js +206 -0
- package/dist/chat-MHJ3L6JQ.js +58 -0
- package/dist/{chunk-PHU4DEAJ.js → chunk-2WPW7OT6.js} +3 -3
- package/dist/{chunk-5Y3PBKW6.js → chunk-2YP2TVDT.js} +138 -56
- package/dist/{chunk-USNBKHYG.js → chunk-4WXYUOAK.js} +4 -6
- package/dist/{chunk-YJA7P64S.js → chunk-AW7PFDVN.js} +5 -5
- package/dist/{chunk-OZFKBXD6.js → chunk-EHYDTZTF.js} +6 -6
- package/dist/{chunk-LX22GRG7.js → chunk-GIE6CSN5.js} +11 -8
- package/dist/{chunk-WBHMQ5OZ.js → chunk-H7OZRFJB.js} +192 -12
- package/dist/{chunk-ON3FF5JA.js → chunk-HDN7MNGD.js} +3 -3
- package/dist/chunk-IAYBDWVG.js +477 -0
- package/dist/{chunk-TZKJLDQN.js → chunk-IKRVFPWU.js} +14 -9
- package/dist/{chunk-WGOGUMPO.js → chunk-JGFVMROS.js} +13 -6
- package/dist/{chunk-3TV4GLFO.js → chunk-JKOWNZ4P.js} +3 -3
- package/dist/{chunk-NWI2425I.js → chunk-K5NAC55T.js} +1 -1
- package/dist/{chunk-HFCBO2GL.js → chunk-KDGS53OS.js} +4 -4
- package/dist/chunk-KTLFDYPT.js +61 -0
- package/dist/{chunk-V63B7DX3.js → chunk-LAC664WU.js} +7 -4
- package/dist/{chunk-3CFRE2VC.js → chunk-OQZH4PBB.js} +337 -1061
- package/dist/{chunk-2VO7453N.js → chunk-PHSAT7YL.js} +30 -54
- package/dist/{chunk-XOXLRRR2.js → chunk-RKQEHRBB.js} +4 -3
- package/dist/chunk-T6HKBWXZ.js +23 -0
- package/dist/{chunk-UTL75LP6.js → chunk-USUXRNVD.js} +22 -22
- package/dist/{chunk-J2CO4WEV.js → chunk-VIVMW2H2.js} +4 -4
- package/dist/{chunk-KTJGZ7M7.js → chunk-XBLSAVJF.js} +1 -1
- package/dist/cli.js +31 -36
- package/dist/{cloud-sync-NI2K3C7G.js → cloud-sync-T7M3ESC3.js} +15 -14
- package/dist/connectors/discord-bridge.js +158 -0
- package/dist/connectors/slack-bridge.js +119 -0
- package/dist/connectors/telegram-bridge.js +133 -0
- package/dist/conversations-M2K4253F.js +55 -0
- package/dist/create-D7J73A6H.js +45 -0
- package/dist/{create-4YBRTTJS.js → create-QWV73WXD.js} +1 -1
- package/dist/{daemon-client-Z7FAJ6JW.js → daemon-client-I42FK2BF.js} +2 -2
- package/dist/{daemon-restart-BJZ3O4U4.js → daemon-restart-M2QTYMEG.js} +7 -7
- package/dist/daemon.js +1758 -1024
- package/dist/db-IC4J52XQ.js +8 -0
- package/dist/{delete-27OYNK25.js → delete-4JYGD4VN.js} +1 -1
- package/dist/down-LVBXEULC.js +14 -0
- package/dist/{env-M336ONDP.js → env-YJMUMFIY.js} +2 -2
- package/dist/{export-HP4G5DQC.js → export-BOJQWBMA.js} +4 -4
- package/dist/{file-HUDKTRAS.js → file-CR36YUPD.js} +4 -4
- package/dist/{history-B64GTFTD.js → history-XKRTAFS2.js} +5 -5
- package/dist/{import-XIB7UV4S.js → import-SRTQXBGH.js} +4 -4
- package/dist/join-J4QU42DL.js +66 -0
- package/dist/list-R73GENNL.js +40 -0
- package/dist/{log-PBFNILJ4.js → log-ABYNVYJ3.js} +4 -4
- package/dist/{login-B5E7N7MY.js → login-3QZNR2DF.js} +4 -4
- package/dist/{login-6U7U6BNG.js → login-XX37I52P.js} +2 -2
- package/dist/{logout-XSJRYS3U.js → logout-T53VKCPU.js} +4 -4
- package/dist/{logout-UKD5LA37.js → logout-W4KOOBIT.js} +2 -2
- package/dist/{logs-3CART7O7.js → logs-U35JR2KE.js} +5 -5
- package/dist/{merge-VK2HSKMA.js → merge-LNSMSAOF.js} +4 -4
- package/dist/message-delivery-LDXLGERA.js +25 -0
- package/dist/migrate-registry-to-db-XC7T5B7P.js +110 -0
- package/dist/{mind-HZ3QSDDJ.js → mind-DI33C74K.js} +25 -25
- package/dist/{mind-activity-tracker-4G6FURY2.js → mind-activity-tracker-EN6XNXPF.js} +3 -4
- package/dist/mind-manager-M6EMUW5I.js +18 -0
- package/dist/{mind-sleep-DTV7L44D.js → mind-sleep-BTSWQNAC.js} +4 -4
- package/dist/{mind-wake-PFN4FN3T.js → mind-wake-SBAKIDVP.js} +4 -4
- package/dist/{notes-37FW2UR2.js → notes-XCER3I7M.js} +11 -21
- package/dist/{package-VZWLXPHV.js → package-7WY6VKU3.js} +1 -1
- package/dist/{pages-DIIT5HMQ.js → pages-6EBS6CBR.js} +2 -2
- package/dist/{publish-HQV7YREB.js → publish-66UB2ZFY.js} +5 -5
- package/dist/{pull-2MB4SK3C.js → pull-XCHJTM5M.js} +4 -4
- package/dist/read-36UFXN3G.js +46 -0
- package/dist/{register-EFND67FQ.js → register-6B2CXTYM.js} +2 -2
- package/dist/{registry-D2BSQ2X5.js → registry-NDNOOYG4.js} +15 -9
- package/dist/{restart-CCK7D6TV.js → restart-6ESL3NBO.js} +5 -5
- package/dist/{sandbox-EHGFF52K.js → sandbox-TGBX22DS.js} +3 -3
- package/dist/{schedule-6F7ELB2M.js → schedule-QTJMFATP.js} +5 -5
- package/dist/{seed-E5OQGWX3.js → seed-SSUCYYDF.js} +2 -2
- package/dist/{send-IH6XZKPC.js → send-ZNCJDSRP.js} +25 -19
- package/dist/{service-LLBV3R7M.js → service-6LIN3F3K.js} +4 -4
- package/dist/{setup-F6TWFYGQ.js → setup-JG4QAEBV.js} +12 -12
- package/dist/{setup-YGAAIKKZ.js → setup-JHL5ZEST.js} +2 -2
- package/dist/{shared-UMO4S7CC.js → shared-ML5I4Q2A.js} +4 -4
- package/dist/{skill-42LGFBQC.js → skill-AUAQTSP5.js} +5 -5
- package/dist/skills/dreaming/references/INSTALL.md +2 -2
- package/dist/skills/orientation/SKILL.md +3 -3
- package/dist/skills/volute-mind/SKILL.md +32 -30
- package/dist/sleep-manager-MWYHM5HV.js +29 -0
- package/dist/split-TKJ5OT3P.js +63 -0
- package/dist/{sprout-QL74KR2X.js → sprout-IJVVKSJ2.js} +6 -7
- package/dist/{start-O5JQASRC.js → start-EUJSS5R4.js} +2 -2
- package/dist/{status-FZBEBM7Q.js → status-77YEPHMW.js} +5 -5
- package/dist/{status-WXD4HXRL.js → status-7GA4SM4Y.js} +4 -4
- package/dist/{status-LV34BG6G.js → status-THLOBLWG.js} +2 -2
- package/dist/{stop-2SOG5NYF.js → stop-3XAITBBF.js} +5 -5
- package/dist/{tailscale-AJ4VL5XK.js → tailscale-NY5MUMY3.js} +1 -1
- package/dist/up-NKSMXBWR.js +17 -0
- package/dist/{update-5VUDAI3D.js → update-PTSH22AZ.js} +9 -9
- package/dist/{update-check-F5Z3ALXX.js → update-check-64FWC4Y2.js} +2 -2
- package/dist/{upgrade-QCCO33BK.js → upgrade-HA47CS4C.js} +12 -5
- package/dist/variant-7TGZHOU3.js +41 -0
- package/dist/{version-notify-USFZBWMG.js → version-notify-5Z4MNR6M.js} +26 -30
- package/dist/web-assets/assets/index-CI5wgghI.css +1 -0
- package/dist/web-assets/assets/index-is5CvJWH.js +75 -0
- package/dist/web-assets/favicon.png +0 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0017_minds.sql +16 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/templates/_base/.init/.config/prompts.json +2 -2
- package/templates/_base/home/VOLUTE.md +5 -5
- package/templates/_base/src/lib/startup.ts +2 -2
- package/dist/channel-ZVZV42UD.js +0 -260
- package/dist/chunk-B2CPS4QU.js +0 -283
- package/dist/chunk-SIAG3QMM.js +0 -42
- package/dist/chunk-WSLPZF72.js +0 -173
- package/dist/connector-G722WXAU.js +0 -147
- package/dist/connectors/discord.js +0 -177
- package/dist/connectors/slack.js +0 -181
- package/dist/connectors/telegram.js +0 -187
- package/dist/down-7UKFMJJZ.js +0 -14
- package/dist/message-delivery-MS5JYPZX.js +0 -25
- package/dist/mind-manager-VVK67AY3.js +0 -19
- package/dist/sleep-manager-EE4NRN2Q.js +0 -29
- package/dist/up-SDMCSVI3.js +0 -17
- package/dist/variant-WWLDY6D5.js +0 -207
- package/dist/web-assets/assets/index-CUQ31ieL.js +0 -69
- package/dist/web-assets/assets/index-CW8NSl1o.css +0 -1
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
subscribe
|
|
4
|
+
} from "./chunk-VIVMW2H2.js";
|
|
5
|
+
import {
|
|
6
|
+
logger_default
|
|
7
|
+
} from "./chunk-YUIHSKR6.js";
|
|
8
|
+
import {
|
|
9
|
+
conversationParticipants,
|
|
10
|
+
conversationReads,
|
|
11
|
+
conversations,
|
|
12
|
+
getDb,
|
|
13
|
+
messages,
|
|
14
|
+
users
|
|
15
|
+
} from "./chunk-H7OZRFJB.js";
|
|
16
|
+
|
|
17
|
+
// src/lib/events/conversations.ts
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
import { and, desc, eq, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
20
|
+
|
|
21
|
+
// src/lib/webhook.ts
|
|
22
|
+
var slog = logger_default.child("webhook");
|
|
23
|
+
function getWebhookUrl() {
|
|
24
|
+
return process.env.VOLUTE_WEBHOOK_URL;
|
|
25
|
+
}
|
|
26
|
+
function getAuthHeaders() {
|
|
27
|
+
const headers = { "Content-Type": "application/json" };
|
|
28
|
+
const secret = process.env.VOLUTE_WEBHOOK_SECRET;
|
|
29
|
+
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
function fireWebhook(event) {
|
|
33
|
+
try {
|
|
34
|
+
const url = getWebhookUrl();
|
|
35
|
+
if (!url) return;
|
|
36
|
+
const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
37
|
+
fetch(url, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: getAuthHeaders(),
|
|
40
|
+
body: JSON.stringify(payload)
|
|
41
|
+
}).then((res) => {
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
|
|
44
|
+
}
|
|
45
|
+
}).catch((err) => {
|
|
46
|
+
slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function initWebhook() {
|
|
53
|
+
const url = getWebhookUrl();
|
|
54
|
+
if (!url) return () => {
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const parsed = new URL(url);
|
|
58
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
59
|
+
slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
|
|
60
|
+
return () => {
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
|
|
65
|
+
return () => {
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
slog.info("webhook enabled");
|
|
69
|
+
return subscribe((event) => {
|
|
70
|
+
try {
|
|
71
|
+
fireWebhook({
|
|
72
|
+
event: event.type,
|
|
73
|
+
mind: event.mind,
|
|
74
|
+
data: { summary: event.summary, ...event.metadata },
|
|
75
|
+
timestamp: event.created_at
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/lib/events/conversation-events.ts
|
|
84
|
+
var subscribers = /* @__PURE__ */ new Map();
|
|
85
|
+
function subscribe2(conversationId, callback) {
|
|
86
|
+
let set = subscribers.get(conversationId);
|
|
87
|
+
if (!set) {
|
|
88
|
+
set = /* @__PURE__ */ new Set();
|
|
89
|
+
subscribers.set(conversationId, set);
|
|
90
|
+
}
|
|
91
|
+
set.add(callback);
|
|
92
|
+
return () => {
|
|
93
|
+
set.delete(callback);
|
|
94
|
+
if (set.size === 0) subscribers.delete(conversationId);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function publish(conversationId, event) {
|
|
98
|
+
const set = subscribers.get(conversationId);
|
|
99
|
+
if (!set) return;
|
|
100
|
+
for (const cb of set) {
|
|
101
|
+
try {
|
|
102
|
+
cb(event);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error("[conversation-events] subscriber threw:", err);
|
|
105
|
+
set.delete(cb);
|
|
106
|
+
if (set.size === 0) subscribers.delete(conversationId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/lib/events/conversations.ts
|
|
112
|
+
async function createConversation(mindName, channel, opts) {
|
|
113
|
+
const db = await getDb();
|
|
114
|
+
const id = randomUUID();
|
|
115
|
+
const type = opts?.type ?? "dm";
|
|
116
|
+
const name = opts?.name ?? null;
|
|
117
|
+
await db.transaction(async (tx) => {
|
|
118
|
+
await tx.insert(conversations).values({
|
|
119
|
+
id,
|
|
120
|
+
mind_name: mindName,
|
|
121
|
+
channel,
|
|
122
|
+
type,
|
|
123
|
+
name,
|
|
124
|
+
user_id: opts?.userId ?? null,
|
|
125
|
+
title: opts?.title ?? null
|
|
126
|
+
});
|
|
127
|
+
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
128
|
+
await tx.insert(conversationParticipants).values(
|
|
129
|
+
opts.participantIds.map((uid, i) => ({
|
|
130
|
+
conversation_id: id,
|
|
131
|
+
user_id: uid,
|
|
132
|
+
role: i === 0 ? "owner" : "member"
|
|
133
|
+
}))
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
fireWebhook({
|
|
138
|
+
event: "conversation_created",
|
|
139
|
+
mind: mindName ?? "",
|
|
140
|
+
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
mind_name: mindName,
|
|
145
|
+
channel,
|
|
146
|
+
type,
|
|
147
|
+
name,
|
|
148
|
+
user_id: opts?.userId ?? null,
|
|
149
|
+
title: opts?.title ?? null,
|
|
150
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function getOrCreateConversation(mindName, channel, opts) {
|
|
155
|
+
const db = await getDb();
|
|
156
|
+
const existing = await db.select().from(conversations).where(
|
|
157
|
+
and(
|
|
158
|
+
eq(conversations.mind_name, mindName),
|
|
159
|
+
eq(conversations.channel, channel),
|
|
160
|
+
eq(conversations.type, "dm")
|
|
161
|
+
)
|
|
162
|
+
).orderBy(desc(conversations.updated_at)).limit(1).get();
|
|
163
|
+
if (existing) return existing;
|
|
164
|
+
return createConversation(mindName, channel, opts);
|
|
165
|
+
}
|
|
166
|
+
async function getConversation(id) {
|
|
167
|
+
const db = await getDb();
|
|
168
|
+
const row = await db.select().from(conversations).where(eq(conversations.id, id)).get();
|
|
169
|
+
return row ?? null;
|
|
170
|
+
}
|
|
171
|
+
async function addParticipant(conversationId, userId, role = "member") {
|
|
172
|
+
const db = await getDb();
|
|
173
|
+
await db.insert(conversationParticipants).values({
|
|
174
|
+
conversation_id: conversationId,
|
|
175
|
+
user_id: userId,
|
|
176
|
+
role
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function removeParticipant(conversationId, userId) {
|
|
180
|
+
const db = await getDb();
|
|
181
|
+
await db.delete(conversationParticipants).where(
|
|
182
|
+
and(
|
|
183
|
+
eq(conversationParticipants.conversation_id, conversationId),
|
|
184
|
+
eq(conversationParticipants.user_id, userId)
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async function getParticipants(conversationId) {
|
|
189
|
+
const db = await getDb();
|
|
190
|
+
const rows = await db.select({
|
|
191
|
+
userId: conversationParticipants.user_id,
|
|
192
|
+
username: users.username,
|
|
193
|
+
userType: users.user_type,
|
|
194
|
+
role: conversationParticipants.role,
|
|
195
|
+
displayName: users.display_name,
|
|
196
|
+
description: users.description,
|
|
197
|
+
avatar: users.avatar
|
|
198
|
+
}).from(conversationParticipants).innerJoin(users, eq(conversationParticipants.user_id, users.id)).where(eq(conversationParticipants.conversation_id, conversationId)).all();
|
|
199
|
+
return rows;
|
|
200
|
+
}
|
|
201
|
+
async function isParticipant(conversationId, userId) {
|
|
202
|
+
const db = await getDb();
|
|
203
|
+
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
204
|
+
and(
|
|
205
|
+
eq(conversationParticipants.conversation_id, conversationId),
|
|
206
|
+
eq(conversationParticipants.user_id, userId)
|
|
207
|
+
)
|
|
208
|
+
).get();
|
|
209
|
+
return row != null;
|
|
210
|
+
}
|
|
211
|
+
async function listConversationsForUser(userId) {
|
|
212
|
+
const db = await getDb();
|
|
213
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq(conversationParticipants.user_id, userId)).all();
|
|
214
|
+
if (participantRows.length === 0) return [];
|
|
215
|
+
const convIds = participantRows.map((r) => r.conversation_id);
|
|
216
|
+
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
217
|
+
}
|
|
218
|
+
async function isParticipantOrOwner(conversationId, userId) {
|
|
219
|
+
if (await isParticipant(conversationId, userId)) return true;
|
|
220
|
+
const db = await getDb();
|
|
221
|
+
const row = await db.select().from(conversations).where(and(eq(conversations.id, conversationId), eq(conversations.user_id, userId))).get();
|
|
222
|
+
return row != null;
|
|
223
|
+
}
|
|
224
|
+
async function deleteConversationForUser(id, userId) {
|
|
225
|
+
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
226
|
+
await deleteConversation(id);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
async function addMessage(conversationId, role, senderName, content) {
|
|
230
|
+
const db = await getDb();
|
|
231
|
+
const serialized = JSON.stringify(content);
|
|
232
|
+
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 });
|
|
233
|
+
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq(conversations.id, conversationId));
|
|
234
|
+
if (role === "user") {
|
|
235
|
+
const firstText = content.find((b) => b.type === "text");
|
|
236
|
+
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
237
|
+
if (title) {
|
|
238
|
+
await db.update(conversations).set({ title }).where(and(eq(conversations.id, conversationId), isNull(conversations.title)));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const msg = {
|
|
242
|
+
id: result.id,
|
|
243
|
+
conversation_id: conversationId,
|
|
244
|
+
role,
|
|
245
|
+
sender_name: senderName,
|
|
246
|
+
content,
|
|
247
|
+
created_at: result.created_at
|
|
248
|
+
};
|
|
249
|
+
publish(conversationId, {
|
|
250
|
+
type: "message",
|
|
251
|
+
id: msg.id,
|
|
252
|
+
role: msg.role,
|
|
253
|
+
senderName: msg.sender_name,
|
|
254
|
+
content: msg.content,
|
|
255
|
+
createdAt: msg.created_at
|
|
256
|
+
});
|
|
257
|
+
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq(conversations.id, conversationId)).get();
|
|
258
|
+
fireWebhook({
|
|
259
|
+
event: "message_created",
|
|
260
|
+
mind: conv?.mind_name ?? "",
|
|
261
|
+
data: {
|
|
262
|
+
conversationId,
|
|
263
|
+
messageId: result.id,
|
|
264
|
+
role,
|
|
265
|
+
senderName,
|
|
266
|
+
content: content.filter((b) => b.type !== "image"),
|
|
267
|
+
createdAt: result.created_at
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
return msg;
|
|
271
|
+
}
|
|
272
|
+
async function getMessages(conversationId) {
|
|
273
|
+
const db = await getDb();
|
|
274
|
+
const rows = await db.select().from(messages).where(eq(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
275
|
+
return rows.map(parseMessageRow);
|
|
276
|
+
}
|
|
277
|
+
async function getMessagesPaginated(conversationId, opts) {
|
|
278
|
+
const db = await getDb();
|
|
279
|
+
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
280
|
+
const conditions = [eq(messages.conversation_id, conversationId)];
|
|
281
|
+
if (opts?.before != null) {
|
|
282
|
+
conditions.push(lt(messages.id, opts.before));
|
|
283
|
+
}
|
|
284
|
+
const rows = await db.select().from(messages).where(and(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
|
|
285
|
+
const hasMore = rows.length > limit;
|
|
286
|
+
const page = rows.slice(0, limit).reverse();
|
|
287
|
+
return {
|
|
288
|
+
messages: page.map(parseMessageRow),
|
|
289
|
+
hasMore
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function parseMessageRow(row) {
|
|
293
|
+
let content;
|
|
294
|
+
try {
|
|
295
|
+
const parsed = JSON.parse(row.content);
|
|
296
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
297
|
+
} catch {
|
|
298
|
+
content = [{ type: "text", text: row.content }];
|
|
299
|
+
}
|
|
300
|
+
return { ...row, role: row.role, content };
|
|
301
|
+
}
|
|
302
|
+
async function listConversationsWithParticipants(userId) {
|
|
303
|
+
const convs = await listConversationsForUser(userId);
|
|
304
|
+
if (convs.length === 0) return [];
|
|
305
|
+
const db = await getDb();
|
|
306
|
+
const convIds = convs.map((c) => c.id);
|
|
307
|
+
const rows = await db.select({
|
|
308
|
+
conversationId: conversationParticipants.conversation_id,
|
|
309
|
+
userId: users.id,
|
|
310
|
+
username: users.username,
|
|
311
|
+
userType: users.user_type,
|
|
312
|
+
role: conversationParticipants.role,
|
|
313
|
+
displayName: users.display_name,
|
|
314
|
+
description: users.description,
|
|
315
|
+
avatar: users.avatar
|
|
316
|
+
}).from(conversationParticipants).innerJoin(users, eq(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
317
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
318
|
+
for (const r of rows) {
|
|
319
|
+
let arr = byConv.get(r.conversationId);
|
|
320
|
+
if (!arr) {
|
|
321
|
+
arr = [];
|
|
322
|
+
byConv.set(r.conversationId, arr);
|
|
323
|
+
}
|
|
324
|
+
arr.push({
|
|
325
|
+
userId: r.userId,
|
|
326
|
+
username: r.username,
|
|
327
|
+
userType: r.userType,
|
|
328
|
+
role: r.role,
|
|
329
|
+
displayName: r.displayName,
|
|
330
|
+
description: r.description,
|
|
331
|
+
avatar: r.avatar
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const lastMsgIds = await db.select({
|
|
335
|
+
conversationId: messages.conversation_id,
|
|
336
|
+
maxId: sql`MAX(${messages.id})`
|
|
337
|
+
}).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
338
|
+
const byLastMsg = /* @__PURE__ */ new Map();
|
|
339
|
+
if (lastMsgIds.length > 0) {
|
|
340
|
+
const msgRows = await db.select().from(messages).where(
|
|
341
|
+
inArray(
|
|
342
|
+
messages.id,
|
|
343
|
+
lastMsgIds.map((r) => r.maxId)
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
for (const m of msgRows) {
|
|
347
|
+
let text = "";
|
|
348
|
+
try {
|
|
349
|
+
const parsed = JSON.parse(m.content);
|
|
350
|
+
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
351
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
352
|
+
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
353
|
+
} catch {
|
|
354
|
+
text = m.content;
|
|
355
|
+
}
|
|
356
|
+
byLastMsg.set(m.conversation_id, {
|
|
357
|
+
role: m.role,
|
|
358
|
+
senderName: m.sender_name,
|
|
359
|
+
text,
|
|
360
|
+
createdAt: m.created_at
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return convs.map((c) => ({
|
|
365
|
+
...c,
|
|
366
|
+
participants: byConv.get(c.id) ?? [],
|
|
367
|
+
lastMessage: byLastMsg.get(c.id)
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
async function findDMConversation(mindName, participantIds) {
|
|
371
|
+
const db = await getDb();
|
|
372
|
+
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and(eq(conversations.mind_name, mindName), eq(conversations.type, "dm"))).all();
|
|
373
|
+
for (const conv of mindConvs) {
|
|
374
|
+
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq(conversationParticipants.conversation_id, conv.id)).all();
|
|
375
|
+
if (rows.length !== 2) continue;
|
|
376
|
+
const ids = new Set(rows.map((r) => r.user_id));
|
|
377
|
+
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
378
|
+
return conv.id;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
async function deleteConversation(id) {
|
|
384
|
+
const db = await getDb();
|
|
385
|
+
await db.delete(conversations).where(eq(conversations.id, id));
|
|
386
|
+
}
|
|
387
|
+
async function createChannel(name, creatorId) {
|
|
388
|
+
const participantIds = creatorId ? [creatorId] : [];
|
|
389
|
+
return createConversation(null, "volute", {
|
|
390
|
+
type: "channel",
|
|
391
|
+
name,
|
|
392
|
+
title: name,
|
|
393
|
+
participantIds
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async function getChannelByName(name) {
|
|
397
|
+
const db = await getDb();
|
|
398
|
+
const row = await db.select().from(conversations).where(and(eq(conversations.name, name), eq(conversations.type, "channel"))).get();
|
|
399
|
+
return row ?? null;
|
|
400
|
+
}
|
|
401
|
+
async function listChannels() {
|
|
402
|
+
const db = await getDb();
|
|
403
|
+
return await db.select().from(conversations).where(eq(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
404
|
+
}
|
|
405
|
+
async function joinChannel(conversationId, userId) {
|
|
406
|
+
if (await isParticipant(conversationId, userId)) return;
|
|
407
|
+
await addParticipant(conversationId, userId);
|
|
408
|
+
}
|
|
409
|
+
async function leaveChannel(conversationId, userId) {
|
|
410
|
+
await removeParticipant(conversationId, userId);
|
|
411
|
+
}
|
|
412
|
+
async function getUnreadCounts(userId, conversationIds) {
|
|
413
|
+
if (conversationIds.length === 0) return {};
|
|
414
|
+
const db = await getDb();
|
|
415
|
+
const rows = await db.select({
|
|
416
|
+
conversationId: messages.conversation_id,
|
|
417
|
+
count: sql`COUNT(*)`
|
|
418
|
+
}).from(messages).leftJoin(
|
|
419
|
+
conversationReads,
|
|
420
|
+
and(
|
|
421
|
+
eq(conversationReads.conversation_id, messages.conversation_id),
|
|
422
|
+
eq(conversationReads.user_id, userId)
|
|
423
|
+
)
|
|
424
|
+
).where(
|
|
425
|
+
and(
|
|
426
|
+
inArray(messages.conversation_id, conversationIds),
|
|
427
|
+
sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
|
|
428
|
+
)
|
|
429
|
+
).groupBy(messages.conversation_id);
|
|
430
|
+
const result = {};
|
|
431
|
+
for (const row of rows) {
|
|
432
|
+
result[row.conversationId] = row.count;
|
|
433
|
+
}
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
async function markConversationRead(userId, conversationId) {
|
|
437
|
+
const db = await getDb();
|
|
438
|
+
const maxRow = await db.select({ maxId: sql`MAX(${messages.id})` }).from(messages).where(eq(messages.conversation_id, conversationId)).get();
|
|
439
|
+
const maxId = maxRow?.maxId ?? 0;
|
|
440
|
+
if (maxId === 0) return;
|
|
441
|
+
await db.insert(conversationReads).values({ user_id: userId, conversation_id: conversationId, last_read_message_id: maxId }).onConflictDoUpdate({
|
|
442
|
+
target: [conversationReads.user_id, conversationReads.conversation_id],
|
|
443
|
+
set: { last_read_message_id: maxId }
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export {
|
|
448
|
+
getWebhookUrl,
|
|
449
|
+
getAuthHeaders,
|
|
450
|
+
fireWebhook,
|
|
451
|
+
initWebhook,
|
|
452
|
+
subscribe2 as subscribe,
|
|
453
|
+
publish,
|
|
454
|
+
createConversation,
|
|
455
|
+
getOrCreateConversation,
|
|
456
|
+
getConversation,
|
|
457
|
+
addParticipant,
|
|
458
|
+
removeParticipant,
|
|
459
|
+
getParticipants,
|
|
460
|
+
isParticipant,
|
|
461
|
+
listConversationsForUser,
|
|
462
|
+
isParticipantOrOwner,
|
|
463
|
+
deleteConversationForUser,
|
|
464
|
+
addMessage,
|
|
465
|
+
getMessages,
|
|
466
|
+
getMessagesPaginated,
|
|
467
|
+
listConversationsWithParticipants,
|
|
468
|
+
findDMConversation,
|
|
469
|
+
deleteConversation,
|
|
470
|
+
createChannel,
|
|
471
|
+
getChannelByName,
|
|
472
|
+
listChannels,
|
|
473
|
+
joinChannel,
|
|
474
|
+
leaveChannel,
|
|
475
|
+
getUnreadCounts,
|
|
476
|
+
markConversationRead
|
|
477
|
+
};
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
voluteHome
|
|
4
|
-
|
|
3
|
+
voluteHome,
|
|
4
|
+
voluteSystemDir
|
|
5
|
+
} from "./chunk-H7OZRFJB.js";
|
|
5
6
|
|
|
6
7
|
// src/lib/setup.ts
|
|
7
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
9
|
import { resolve } from "path";
|
|
9
10
|
function configPath() {
|
|
10
|
-
return resolve(
|
|
11
|
+
return resolve(voluteSystemDir(), "config.json");
|
|
11
12
|
}
|
|
12
13
|
function readGlobalConfig() {
|
|
13
14
|
const path = configPath();
|
|
14
|
-
|
|
15
|
+
const legacyPath = resolve(voluteHome(), "config.json");
|
|
16
|
+
const effectivePath = existsSync(path) ? path : legacyPath;
|
|
17
|
+
if (!existsSync(effectivePath)) return {};
|
|
15
18
|
try {
|
|
16
|
-
return JSON.parse(readFileSync(
|
|
19
|
+
return JSON.parse(readFileSync(effectivePath, "utf-8"));
|
|
17
20
|
} catch (err) {
|
|
18
|
-
console.error(`Failed to parse ${
|
|
21
|
+
console.error(`Failed to parse ${effectivePath}: ${err instanceof Error ? err.message : err}`);
|
|
19
22
|
return {};
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
function writeGlobalConfig(config) {
|
|
23
26
|
const path = configPath();
|
|
24
|
-
mkdirSync(
|
|
27
|
+
mkdirSync(voluteSystemDir(), { recursive: true });
|
|
25
28
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
|
|
26
29
|
}
|
|
27
30
|
function isSetupComplete() {
|
|
@@ -32,8 +35,10 @@ function migrateSetupConfig() {
|
|
|
32
35
|
const config = readGlobalConfig();
|
|
33
36
|
if (config.setup) return;
|
|
34
37
|
const home = voluteHome();
|
|
35
|
-
const
|
|
36
|
-
|
|
38
|
+
const systemDir = voluteSystemDir();
|
|
39
|
+
const registryPath = resolve(systemDir, "minds.json");
|
|
40
|
+
const legacyRegistryPath = resolve(home, "minds.json");
|
|
41
|
+
if (!existsSync(registryPath) && !existsSync(legacyRegistryPath)) return;
|
|
37
42
|
const isSystem = process.env.VOLUTE_ISOLATION === "user";
|
|
38
43
|
const mindsDir = process.env.VOLUTE_MINDS_DIR || resolve(home, "minds");
|
|
39
44
|
let hasService = false;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
voluteHome
|
|
4
|
-
|
|
3
|
+
voluteHome,
|
|
4
|
+
voluteSystemDir,
|
|
5
|
+
voluteUserHome
|
|
6
|
+
} from "./chunk-H7OZRFJB.js";
|
|
5
7
|
|
|
6
8
|
// src/lib/daemon-client.ts
|
|
7
9
|
import { existsSync, readFileSync } from "fs";
|
|
8
10
|
import { resolve } from "path";
|
|
9
11
|
function readCliSession() {
|
|
10
|
-
const sessionPath = resolve(
|
|
12
|
+
const sessionPath = resolve(voluteUserHome(), "cli-session.json");
|
|
11
13
|
if (!existsSync(sessionPath)) return null;
|
|
12
14
|
try {
|
|
13
15
|
return JSON.parse(readFileSync(sessionPath, "utf-8"));
|
|
@@ -16,7 +18,9 @@ function readCliSession() {
|
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
function readDaemonConfig() {
|
|
19
|
-
const
|
|
21
|
+
const newPath = resolve(voluteSystemDir(), "daemon.json");
|
|
22
|
+
const legacyPath = resolve(voluteHome(), "daemon.json");
|
|
23
|
+
const configPath = existsSync(newPath) ? newPath : legacyPath;
|
|
20
24
|
if (!existsSync(configPath)) {
|
|
21
25
|
if (existsSync("/etc/systemd/system/volute.service") && !process.env.VOLUTE_HOME) {
|
|
22
26
|
console.error("Volute is running as a system service but VOLUTE_HOME is not set.");
|
|
@@ -56,8 +60,11 @@ async function daemonFetch(path, options) {
|
|
|
56
60
|
const config = readDaemonConfig();
|
|
57
61
|
const url = buildUrl(config);
|
|
58
62
|
const headers = new Headers(options?.headers);
|
|
59
|
-
const
|
|
60
|
-
|
|
63
|
+
const daemonToken = process.env.VOLUTE_DAEMON_TOKEN;
|
|
64
|
+
const cliSession = daemonToken ? null : readCliSession();
|
|
65
|
+
if (daemonToken) {
|
|
66
|
+
headers.set("Authorization", `Bearer ${daemonToken}`);
|
|
67
|
+
} else if (cliSession?.sessionId) {
|
|
61
68
|
headers.set("Authorization", `Bearer ${cliSession.sessionId}`);
|
|
62
69
|
}
|
|
63
70
|
headers.set("Origin", url);
|
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
} from "./chunk-YUIHSKR6.js";
|
|
5
5
|
import {
|
|
6
6
|
gitExec
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AW7PFDVN.js";
|
|
8
8
|
import {
|
|
9
9
|
isIsolationEnabled,
|
|
10
10
|
mindUserName
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RKQEHRBB.js";
|
|
12
12
|
import {
|
|
13
13
|
voluteHome
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-H7OZRFJB.js";
|
|
15
15
|
|
|
16
16
|
// src/lib/shared.ts
|
|
17
17
|
import { execFileSync } from "child_process";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
voluteUserHome
|
|
4
|
+
} from "./chunk-H7OZRFJB.js";
|
|
5
5
|
|
|
6
6
|
// src/lib/systems-config.ts
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
8
8
|
import { resolve } from "path";
|
|
9
9
|
var DEFAULT_API_URL = "https://volute.systems";
|
|
10
10
|
function configPath() {
|
|
11
|
-
return resolve(
|
|
11
|
+
return resolve(voluteUserHome(), "systems.json");
|
|
12
12
|
}
|
|
13
13
|
function readSystemsConfig() {
|
|
14
14
|
const path = configPath();
|
|
@@ -29,7 +29,7 @@ function readSystemsConfig() {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
function writeSystemsConfig(config) {
|
|
32
|
-
mkdirSync(
|
|
32
|
+
mkdirSync(voluteUserHome(), { recursive: true });
|
|
33
33
|
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}
|
|
34
34
|
`, { mode: 384 });
|
|
35
35
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/connectors/bridge-sdk.ts
|
|
4
|
+
function loadBridgeEnv() {
|
|
5
|
+
const daemonUrl = process.env.VOLUTE_DAEMON_URL;
|
|
6
|
+
const daemonToken = process.env.VOLUTE_DAEMON_TOKEN;
|
|
7
|
+
const platform = process.env.VOLUTE_BRIDGE_PLATFORM;
|
|
8
|
+
if (!daemonUrl || !daemonToken || !platform) {
|
|
9
|
+
console.error(
|
|
10
|
+
"Missing required env vars: VOLUTE_DAEMON_URL, VOLUTE_DAEMON_TOKEN, VOLUTE_BRIDGE_PLATFORM"
|
|
11
|
+
);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return { daemonUrl, daemonToken, platform };
|
|
15
|
+
}
|
|
16
|
+
function getHeaders(env) {
|
|
17
|
+
return {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${env.daemonToken}`,
|
|
20
|
+
Origin: env.daemonUrl
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function sendToBridge(env, message) {
|
|
24
|
+
const url = `${env.daemonUrl}/api/bridges/${env.platform}/inbound`;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: getHeaders(env),
|
|
29
|
+
body: JSON.stringify(message)
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const body = await res.text().catch(() => "");
|
|
33
|
+
console.error(`Bridge inbound returned ${res.status}: ${body}`);
|
|
34
|
+
return { ok: false, error: `Bridge returned ${res.status}` };
|
|
35
|
+
}
|
|
36
|
+
return await res.json();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
39
|
+
console.error(`Failed to send bridge message: ${detail}`);
|
|
40
|
+
return { ok: false, error: `Failed to reach daemon: ${detail}` };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function onShutdown(cleanup) {
|
|
44
|
+
const handler = () => {
|
|
45
|
+
Promise.resolve(cleanup()).then(
|
|
46
|
+
() => process.exit(0),
|
|
47
|
+
(err) => {
|
|
48
|
+
console.error(`Shutdown error: ${err}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
process.on("SIGINT", handler);
|
|
54
|
+
process.on("SIGTERM", handler);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
loadBridgeEnv,
|
|
59
|
+
sendToBridge,
|
|
60
|
+
onShutdown
|
|
61
|
+
};
|