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.
Files changed (127) hide show
  1. package/README.md +13 -13
  2. package/dist/{activity-events-ZMBAKLUF.js → activity-events-BBIEA2F4.js} +2 -3
  3. package/dist/api.d.ts +363 -168
  4. package/dist/{archive-4ZQYK5MN.js → archive-UA4BDFXQ.js} +2 -2
  5. package/dist/{auth-4TV573WE.js → auth-D3OT2ARB.js} +3 -3
  6. package/dist/bridge-FQHZL3MC.js +206 -0
  7. package/dist/chat-MHJ3L6JQ.js +58 -0
  8. package/dist/{chunk-PHU4DEAJ.js → chunk-2WPW7OT6.js} +3 -3
  9. package/dist/{chunk-5Y3PBKW6.js → chunk-2YP2TVDT.js} +138 -56
  10. package/dist/{chunk-USNBKHYG.js → chunk-4WXYUOAK.js} +4 -6
  11. package/dist/{chunk-YJA7P64S.js → chunk-AW7PFDVN.js} +5 -5
  12. package/dist/{chunk-OZFKBXD6.js → chunk-EHYDTZTF.js} +6 -6
  13. package/dist/{chunk-LX22GRG7.js → chunk-GIE6CSN5.js} +11 -8
  14. package/dist/{chunk-WBHMQ5OZ.js → chunk-H7OZRFJB.js} +192 -12
  15. package/dist/{chunk-ON3FF5JA.js → chunk-HDN7MNGD.js} +3 -3
  16. package/dist/chunk-IAYBDWVG.js +477 -0
  17. package/dist/{chunk-TZKJLDQN.js → chunk-IKRVFPWU.js} +14 -9
  18. package/dist/{chunk-WGOGUMPO.js → chunk-JGFVMROS.js} +13 -6
  19. package/dist/{chunk-3TV4GLFO.js → chunk-JKOWNZ4P.js} +3 -3
  20. package/dist/{chunk-NWI2425I.js → chunk-K5NAC55T.js} +1 -1
  21. package/dist/{chunk-HFCBO2GL.js → chunk-KDGS53OS.js} +4 -4
  22. package/dist/chunk-KTLFDYPT.js +61 -0
  23. package/dist/{chunk-V63B7DX3.js → chunk-LAC664WU.js} +7 -4
  24. package/dist/{chunk-3CFRE2VC.js → chunk-OQZH4PBB.js} +337 -1061
  25. package/dist/{chunk-2VO7453N.js → chunk-PHSAT7YL.js} +30 -54
  26. package/dist/{chunk-XOXLRRR2.js → chunk-RKQEHRBB.js} +4 -3
  27. package/dist/chunk-T6HKBWXZ.js +23 -0
  28. package/dist/{chunk-UTL75LP6.js → chunk-USUXRNVD.js} +22 -22
  29. package/dist/{chunk-J2CO4WEV.js → chunk-VIVMW2H2.js} +4 -4
  30. package/dist/{chunk-KTJGZ7M7.js → chunk-XBLSAVJF.js} +1 -1
  31. package/dist/cli.js +31 -36
  32. package/dist/{cloud-sync-NI2K3C7G.js → cloud-sync-T7M3ESC3.js} +15 -14
  33. package/dist/connectors/discord-bridge.js +158 -0
  34. package/dist/connectors/slack-bridge.js +119 -0
  35. package/dist/connectors/telegram-bridge.js +133 -0
  36. package/dist/conversations-M2K4253F.js +55 -0
  37. package/dist/create-D7J73A6H.js +45 -0
  38. package/dist/{create-4YBRTTJS.js → create-QWV73WXD.js} +1 -1
  39. package/dist/{daemon-client-Z7FAJ6JW.js → daemon-client-I42FK2BF.js} +2 -2
  40. package/dist/{daemon-restart-BJZ3O4U4.js → daemon-restart-M2QTYMEG.js} +7 -7
  41. package/dist/daemon.js +1758 -1024
  42. package/dist/db-IC4J52XQ.js +8 -0
  43. package/dist/{delete-27OYNK25.js → delete-4JYGD4VN.js} +1 -1
  44. package/dist/down-LVBXEULC.js +14 -0
  45. package/dist/{env-M336ONDP.js → env-YJMUMFIY.js} +2 -2
  46. package/dist/{export-HP4G5DQC.js → export-BOJQWBMA.js} +4 -4
  47. package/dist/{file-HUDKTRAS.js → file-CR36YUPD.js} +4 -4
  48. package/dist/{history-B64GTFTD.js → history-XKRTAFS2.js} +5 -5
  49. package/dist/{import-XIB7UV4S.js → import-SRTQXBGH.js} +4 -4
  50. package/dist/join-J4QU42DL.js +66 -0
  51. package/dist/list-R73GENNL.js +40 -0
  52. package/dist/{log-PBFNILJ4.js → log-ABYNVYJ3.js} +4 -4
  53. package/dist/{login-B5E7N7MY.js → login-3QZNR2DF.js} +4 -4
  54. package/dist/{login-6U7U6BNG.js → login-XX37I52P.js} +2 -2
  55. package/dist/{logout-XSJRYS3U.js → logout-T53VKCPU.js} +4 -4
  56. package/dist/{logout-UKD5LA37.js → logout-W4KOOBIT.js} +2 -2
  57. package/dist/{logs-3CART7O7.js → logs-U35JR2KE.js} +5 -5
  58. package/dist/{merge-VK2HSKMA.js → merge-LNSMSAOF.js} +4 -4
  59. package/dist/message-delivery-LDXLGERA.js +25 -0
  60. package/dist/migrate-registry-to-db-XC7T5B7P.js +110 -0
  61. package/dist/{mind-HZ3QSDDJ.js → mind-DI33C74K.js} +25 -25
  62. package/dist/{mind-activity-tracker-4G6FURY2.js → mind-activity-tracker-EN6XNXPF.js} +3 -4
  63. package/dist/mind-manager-M6EMUW5I.js +18 -0
  64. package/dist/{mind-sleep-DTV7L44D.js → mind-sleep-BTSWQNAC.js} +4 -4
  65. package/dist/{mind-wake-PFN4FN3T.js → mind-wake-SBAKIDVP.js} +4 -4
  66. package/dist/{notes-37FW2UR2.js → notes-XCER3I7M.js} +11 -21
  67. package/dist/{package-VZWLXPHV.js → package-7WY6VKU3.js} +1 -1
  68. package/dist/{pages-DIIT5HMQ.js → pages-6EBS6CBR.js} +2 -2
  69. package/dist/{publish-HQV7YREB.js → publish-66UB2ZFY.js} +5 -5
  70. package/dist/{pull-2MB4SK3C.js → pull-XCHJTM5M.js} +4 -4
  71. package/dist/read-36UFXN3G.js +46 -0
  72. package/dist/{register-EFND67FQ.js → register-6B2CXTYM.js} +2 -2
  73. package/dist/{registry-D2BSQ2X5.js → registry-NDNOOYG4.js} +15 -9
  74. package/dist/{restart-CCK7D6TV.js → restart-6ESL3NBO.js} +5 -5
  75. package/dist/{sandbox-EHGFF52K.js → sandbox-TGBX22DS.js} +3 -3
  76. package/dist/{schedule-6F7ELB2M.js → schedule-QTJMFATP.js} +5 -5
  77. package/dist/{seed-E5OQGWX3.js → seed-SSUCYYDF.js} +2 -2
  78. package/dist/{send-IH6XZKPC.js → send-ZNCJDSRP.js} +25 -19
  79. package/dist/{service-LLBV3R7M.js → service-6LIN3F3K.js} +4 -4
  80. package/dist/{setup-F6TWFYGQ.js → setup-JG4QAEBV.js} +12 -12
  81. package/dist/{setup-YGAAIKKZ.js → setup-JHL5ZEST.js} +2 -2
  82. package/dist/{shared-UMO4S7CC.js → shared-ML5I4Q2A.js} +4 -4
  83. package/dist/{skill-42LGFBQC.js → skill-AUAQTSP5.js} +5 -5
  84. package/dist/skills/dreaming/references/INSTALL.md +2 -2
  85. package/dist/skills/orientation/SKILL.md +3 -3
  86. package/dist/skills/volute-mind/SKILL.md +32 -30
  87. package/dist/sleep-manager-MWYHM5HV.js +29 -0
  88. package/dist/split-TKJ5OT3P.js +63 -0
  89. package/dist/{sprout-QL74KR2X.js → sprout-IJVVKSJ2.js} +6 -7
  90. package/dist/{start-O5JQASRC.js → start-EUJSS5R4.js} +2 -2
  91. package/dist/{status-FZBEBM7Q.js → status-77YEPHMW.js} +5 -5
  92. package/dist/{status-WXD4HXRL.js → status-7GA4SM4Y.js} +4 -4
  93. package/dist/{status-LV34BG6G.js → status-THLOBLWG.js} +2 -2
  94. package/dist/{stop-2SOG5NYF.js → stop-3XAITBBF.js} +5 -5
  95. package/dist/{tailscale-AJ4VL5XK.js → tailscale-NY5MUMY3.js} +1 -1
  96. package/dist/up-NKSMXBWR.js +17 -0
  97. package/dist/{update-5VUDAI3D.js → update-PTSH22AZ.js} +9 -9
  98. package/dist/{update-check-F5Z3ALXX.js → update-check-64FWC4Y2.js} +2 -2
  99. package/dist/{upgrade-QCCO33BK.js → upgrade-HA47CS4C.js} +12 -5
  100. package/dist/variant-7TGZHOU3.js +41 -0
  101. package/dist/{version-notify-USFZBWMG.js → version-notify-5Z4MNR6M.js} +26 -30
  102. package/dist/web-assets/assets/index-CI5wgghI.css +1 -0
  103. package/dist/web-assets/assets/index-is5CvJWH.js +75 -0
  104. package/dist/web-assets/favicon.png +0 -0
  105. package/dist/web-assets/index.html +2 -2
  106. package/drizzle/0017_minds.sql +16 -0
  107. package/drizzle/meta/_journal.json +7 -0
  108. package/package.json +1 -1
  109. package/templates/_base/.init/.config/prompts.json +2 -2
  110. package/templates/_base/home/VOLUTE.md +5 -5
  111. package/templates/_base/src/lib/startup.ts +2 -2
  112. package/dist/channel-ZVZV42UD.js +0 -260
  113. package/dist/chunk-B2CPS4QU.js +0 -283
  114. package/dist/chunk-SIAG3QMM.js +0 -42
  115. package/dist/chunk-WSLPZF72.js +0 -173
  116. package/dist/connector-G722WXAU.js +0 -147
  117. package/dist/connectors/discord.js +0 -177
  118. package/dist/connectors/slack.js +0 -181
  119. package/dist/connectors/telegram.js +0 -187
  120. package/dist/down-7UKFMJJZ.js +0 -14
  121. package/dist/message-delivery-MS5JYPZX.js +0 -25
  122. package/dist/mind-manager-VVK67AY3.js +0 -19
  123. package/dist/sleep-manager-EE4NRN2Q.js +0 -29
  124. package/dist/up-SDMCSVI3.js +0 -17
  125. package/dist/variant-WWLDY6D5.js +0 -207
  126. package/dist/web-assets/assets/index-CUQ31ieL.js +0 -69
  127. 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
- } from "./chunk-B2CPS4QU.js";
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(voluteHome(), "config.json");
11
+ return resolve(voluteSystemDir(), "config.json");
11
12
  }
12
13
  function readGlobalConfig() {
13
14
  const path = configPath();
14
- if (!existsSync(path)) return {};
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(path, "utf-8"));
19
+ return JSON.parse(readFileSync(effectivePath, "utf-8"));
17
20
  } catch (err) {
18
- console.error(`Failed to parse ${path}: ${err instanceof Error ? err.message : err}`);
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(voluteHome(), { recursive: true });
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 registryPath = resolve(home, "minds.json");
36
- if (!existsSync(registryPath)) return;
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
- } from "./chunk-B2CPS4QU.js";
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(voluteHome(), "cli-session.json");
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 configPath = resolve(voluteHome(), "daemon.json");
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 cliSession = readCliSession();
60
- if (cliSession?.sessionId) {
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-YJA7P64S.js";
7
+ } from "./chunk-AW7PFDVN.js";
8
8
  import {
9
9
  isIsolationEnabled,
10
10
  mindUserName
11
- } from "./chunk-XOXLRRR2.js";
11
+ } from "./chunk-RKQEHRBB.js";
12
12
  import {
13
13
  voluteHome
14
- } from "./chunk-B2CPS4QU.js";
14
+ } from "./chunk-H7OZRFJB.js";
15
15
 
16
16
  // src/lib/shared.ts
17
17
  import { execFileSync } from "child_process";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  publish
4
- } from "./chunk-J2CO4WEV.js";
4
+ } from "./chunk-VIVMW2H2.js";
5
5
  import {
6
6
  logger_default
7
7
  } from "./chunk-YUIHSKR6.js";
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- voluteHome
4
- } from "./chunk-B2CPS4QU.js";
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(voluteHome(), "systems.json");
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(voluteHome(), { recursive: true });
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
+ };