volute 0.29.0 → 0.30.1

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 (122) hide show
  1. package/README.md +112 -13
  2. package/dist/{accept-666DIZX2.js → accept-E3PAH3QJ.js} +2 -2
  3. package/dist/{activity-events-BBIEA2F4.js → activity-events-BKBPPUBP.js} +2 -2
  4. package/dist/ai-service-VAJT5UBS.js +29 -0
  5. package/dist/api.d.ts +351 -477
  6. package/dist/{archive-UA4BDFXQ.js → archive-WWDBWYN2.js} +2 -2
  7. package/dist/{bridge-FQHZL3MC.js → bridge-RO37CUFM.js} +2 -2
  8. package/dist/{chat-KTPOR2JT.js → chat-TCUNPFGO.js} +8 -8
  9. package/dist/{chunk-FLZGS4QH.js → chunk-2C2VXEBB.js} +2 -2
  10. package/dist/chunk-2NDZC3S7.js +1330 -0
  11. package/dist/{chunk-IKRVFPWU.js → chunk-7D47T4RB.js} +3 -2
  12. package/dist/{chunk-AW7PFDVN.js → chunk-CVH6Y2YG.js} +1 -1
  13. package/dist/{chunk-XBLSAVJF.js → chunk-DTC6EH5I.js} +1 -1
  14. package/dist/{chunk-THUUIU3E.js → chunk-EFP3PE6C.js} +5 -5
  15. package/dist/{chunk-JGFVMROS.js → chunk-EFVHR7KH.js} +1 -1
  16. package/dist/{chunk-CQ7SNKNI.js → chunk-FSM45XD5.js} +1 -1
  17. package/dist/{chunk-LAC664WU.js → chunk-FXHXHI2A.js} +42 -24
  18. package/dist/{chunk-RKQEHRBB.js → chunk-G3GBKZGG.js} +1 -1
  19. package/dist/{chunk-H7OZRFJB.js → chunk-HHTXM4JT.js} +0 -49
  20. package/dist/{chunk-J4IBNXGJ.js → chunk-IKHDUZRH.js} +4 -3
  21. package/dist/{chunk-MD4C26II.js → chunk-JGFRDMR6.js} +1 -1
  22. package/dist/{chunk-EHZKEMMV.js → chunk-LIRWLNAK.js} +24 -10
  23. package/dist/{chunk-NI5FFCCS.js → chunk-MDPCSXZ4.js} +35 -11
  24. package/dist/chunk-NSBFETWP.js +188 -0
  25. package/dist/{chunk-VIVMW2H2.js → chunk-P27RV5WM.js} +1 -1
  26. package/dist/{chunk-EHYDTZTF.js → chunk-P7VFDSSG.js} +2 -2
  27. package/dist/{chunk-CMILSHZD.js → chunk-QVAQ5454.js} +84 -300
  28. package/dist/{chunk-HDN7MNGD.js → chunk-S5LR3XYJ.js} +1 -1
  29. package/dist/{chunk-2YP2TVDT.js → chunk-UPA6COHU.js} +5 -5
  30. package/dist/{chunk-AKPFNL7L.js → chunk-VGWJSNHS.js} +1 -1
  31. package/dist/{chunk-DUAUMCEE.js → chunk-W5OOPLNP.js} +3 -3
  32. package/dist/{chunk-2WPW7OT6.js → chunk-ZWKTUQEL.js} +1 -1
  33. package/dist/cli.js +22 -26
  34. package/dist/{clock-DGCBVGYA.js → clock-G3ALCMLJ.js} +10 -6
  35. package/dist/{cloud-sync-KILFGV5Q.js → cloud-sync-JV4LJOK3.js} +13 -12
  36. package/dist/{conversations-P5BL7RMX.js → conversations-7KVQV7EZ.js} +3 -3
  37. package/dist/{create-DFCAGEE5.js → create-JTLS7GX3.js} +2 -2
  38. package/dist/{create-QWV73WXD.js → create-VQSQHJQW.js} +1 -1
  39. package/dist/{daemon-client-I42FK2BF.js → daemon-client-BCTFGVCZ.js} +2 -2
  40. package/dist/{daemon-restart-UHOMICXT.js → daemon-restart-4JGBHEJ4.js} +7 -7
  41. package/dist/daemon.js +1257 -1022
  42. package/dist/{db-IC4J52XQ.js → db-HMFPIRO2.js} +1 -1
  43. package/dist/{delete-4JYGD4VN.js → delete-JESHKE7F.js} +1 -1
  44. package/dist/down-NGBMGORS.js +14 -0
  45. package/dist/{env-YJMUMFIY.js → env-CLXXT7M2.js} +2 -2
  46. package/dist/{export-BOJQWBMA.js → export-EGA5M5PB.js} +3 -3
  47. package/dist/extension-WZ4SUPJB.js +174 -0
  48. package/dist/extensions-ECO4RPFQ.js +27 -0
  49. package/dist/{files-M546TKVN.js → files-4VEJDASH.js} +3 -3
  50. package/dist/{history-ALPTNB3I.js → history-EJMMLXDO.js} +17 -2
  51. package/dist/{import-SRTQXBGH.js → import-YCGPMBSI.js} +3 -3
  52. package/dist/{join-J4QU42DL.js → join-2GBJKZEN.js} +1 -1
  53. package/dist/{list-R73GENNL.js → list-Q6O7FGAN.js} +2 -2
  54. package/dist/{login-3QZNR2DF.js → login-RET5WESK.js} +2 -2
  55. package/dist/{login-BKP3AFWN.js → login-RL6AU2SM.js} +3 -3
  56. package/dist/{logout-T53VKCPU.js → logout-CGAGJN3L.js} +2 -2
  57. package/dist/{logout-IQK7FNEK.js → logout-JRPBEMMR.js} +3 -3
  58. package/dist/message-delivery-6YMVNOEC.js +28 -0
  59. package/dist/{migrate-registry-to-db-XC7T5B7P.js → migrate-registry-to-db-FK35IPEH.js} +1 -1
  60. package/dist/{mind-S5V6CK5W.js → mind-LUWRQUQ5.js} +17 -17
  61. package/dist/{mind-activity-tracker-WRHFI3YW.js → mind-activity-tracker-VYN2ZZ2M.js} +3 -3
  62. package/dist/{mind-list-UPJ75GPI.js → mind-list-V5WW5DUA.js} +2 -2
  63. package/dist/{mind-manager-P66HQDNE.js → mind-manager-YFCOIAAX.js} +6 -6
  64. package/dist/{mind-sleep-BTSWQNAC.js → mind-sleep-R6PTNNW4.js} +2 -2
  65. package/dist/{mind-status-TK5AETEM.js → mind-status-I4ISFJ6I.js} +2 -2
  66. package/dist/{mind-wake-SBAKIDVP.js → mind-wake-67ZQEWAV.js} +2 -2
  67. package/dist/{package-OFKXNKJF.js → package-OYUD4ZJ4.js} +12 -6
  68. package/dist/{pages-watcher-P7QECRE2.js → pages-watcher-Z3PKNROC.js} +3 -3
  69. package/dist/{read-36UFXN3G.js → read-WQMPTSN2.js} +2 -2
  70. package/dist/{register-CHREOMJ3.js → register-NZDSTLP3.js} +3 -3
  71. package/dist/{registry-NDNOOYG4.js → registry-ODSALQQL.js} +1 -1
  72. package/dist/{reject-LXIZFJ4Q.js → reject-2HZOJEIJ.js} +2 -2
  73. package/dist/{restart-6ESL3NBO.js → restart-QHS3NT64.js} +2 -2
  74. package/dist/{sandbox-5BW5HPXM.js → sandbox-O5FUSF43.js} +3 -3
  75. package/dist/{seed-SSUCYYDF.js → seed-WUQMPLDM.js} +1 -1
  76. package/dist/{send-TAOEZ4NH.js → send-OAN3RYYY.js} +20 -6
  77. package/dist/{setup-JHL5ZEST.js → setup-QMDK5RZX.js} +2 -2
  78. package/dist/{setup-RXYVGGT7.js → setup-XJH3E7YM.js} +45 -14
  79. package/dist/{skill-AUAQTSP5.js → skill-FZIN4W4Q.js} +65 -3
  80. package/dist/skills/volute-mind/SKILL.md +10 -19
  81. package/dist/sleep-manager-O7YQFCV5.js +30 -0
  82. package/dist/{split-TKJ5OT3P.js → split-EXYGGGQN.js} +1 -1
  83. package/dist/{sprout-UNT7LKKE.js → sprout-AXQ6H5DB.js} +8 -7
  84. package/dist/{start-EUJSS5R4.js → start-MTOVL6SY.js} +2 -2
  85. package/dist/{status-NQJYR4BG.js → status-ZRO37MWR.js} +5 -5
  86. package/dist/{stop-3XAITBBF.js → stop-OK5WEPVC.js} +2 -2
  87. package/dist/{systems-SMEFSHTA.js → systems-W3BBMSOZ.js} +5 -5
  88. package/dist/{tailscale-NY5MUMY3.js → tailscale-BM72RXCJ.js} +1 -1
  89. package/dist/{template-hash-BIMA4ILT.js → template-hash-3HOR4UAJ.js} +1 -1
  90. package/dist/up-BXUAIDXB.js +17 -0
  91. package/dist/{update-PTSH22AZ.js → update-PLPHMMZ2.js} +5 -5
  92. package/dist/{update-check-64FWC4Y2.js → update-check-CVCN7MF6.js} +2 -2
  93. package/dist/{upgrade-HA47CS4C.js → upgrade-I6NPCYUU.js} +1 -1
  94. package/dist/{version-notify-WDHRO3XD.js → version-notify-2NTWVEHL.js} +15 -14
  95. package/dist/web-assets/assets/index--kREqKl9.js +72 -0
  96. package/dist/web-assets/assets/index-BXYTG0nJ.css +1 -0
  97. package/dist/web-assets/ext-theme.css +111 -0
  98. package/dist/web-assets/index.html +2 -2
  99. package/package.json +12 -6
  100. package/packages/extensions/notes/dist/ui/assets/index-DgawVO5g.css +1 -0
  101. package/packages/extensions/notes/dist/ui/assets/index-qUWoeC4c.js +2 -0
  102. package/packages/extensions/notes/dist/ui/index.html +14 -0
  103. package/packages/extensions/notes/skills/notes/SKILL.md +62 -0
  104. package/packages/extensions/notes/skills/notes/scripts/notes.mjs +185 -0
  105. package/packages/extensions/pages/dist/ui/assets/index-D0HyS-xQ.css +1 -0
  106. package/packages/extensions/pages/dist/ui/assets/index-tLTROSk5.js +2 -0
  107. package/packages/extensions/pages/dist/ui/index.html +14 -0
  108. package/packages/extensions/pages/skills/pages/SKILL.md +58 -0
  109. package/templates/_base/home/VOLUTE.md +1 -1
  110. package/dist/chunk-P72MVS4R.js +0 -188
  111. package/dist/chunk-ZYGKG6VC.js +0 -22
  112. package/dist/down-LVBXEULC.js +0 -14
  113. package/dist/message-delivery-Q7VUMIEI.js +0 -27
  114. package/dist/notes-XCER3I7M.js +0 -220
  115. package/dist/pages-EUJR52AH.js +0 -36
  116. package/dist/publish-ZZB33WP4.js +0 -86
  117. package/dist/skills/notes/SKILL.md +0 -34
  118. package/dist/sleep-manager-G4B5GW5P.js +0 -29
  119. package/dist/status-S7UUPNRW.js +0 -38
  120. package/dist/up-W6VAK2XE.js +0 -17
  121. package/dist/web-assets/assets/index-BmKDnWDB.css +0 -1
  122. package/dist/web-assets/assets/index-CLJMx-GA.js +0 -71
@@ -0,0 +1,1330 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ broadcast,
4
+ publish
5
+ } from "./chunk-P27RV5WM.js";
6
+ import {
7
+ hashSkillDir,
8
+ importSkillFromDir,
9
+ sharedSkillsDir
10
+ } from "./chunk-MDPCSXZ4.js";
11
+ import {
12
+ logger_default
13
+ } from "./chunk-YUIHSKR6.js";
14
+ import {
15
+ getDb,
16
+ mindDir,
17
+ users,
18
+ voluteHome,
19
+ voluteSystemDir,
20
+ voluteUserHome
21
+ } from "./chunk-HHTXM4JT.js";
22
+
23
+ // src/lib/extensions.ts
24
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
25
+ import { dirname, resolve as resolve5 } from "path";
26
+
27
+ // packages/extensions/notes/src/index.ts
28
+ import { resolve } from "path";
29
+
30
+ // packages/extensions/sdk/src/index.ts
31
+ var VALID_EXTENSION_ID = /^[a-z0-9][a-z0-9_-]*$/;
32
+ function createExtension(manifest) {
33
+ if (!manifest.id) throw new Error("Extension manifest requires an id");
34
+ if (!VALID_EXTENSION_ID.test(manifest.id))
35
+ throw new Error(
36
+ "Extension id must be lowercase alphanumeric with hyphens/underscores, starting with a letter or digit"
37
+ );
38
+ if (typeof manifest.routes !== "function")
39
+ throw new Error("Extension manifest requires a routes function");
40
+ return manifest;
41
+ }
42
+
43
+ // packages/extensions/notes/src/db.ts
44
+ function initDb(db) {
45
+ db.exec(`
46
+ CREATE TABLE IF NOT EXISTS notes (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ author_id INTEGER NOT NULL,
49
+ title TEXT NOT NULL,
50
+ slug TEXT NOT NULL,
51
+ content TEXT NOT NULL,
52
+ reply_to_id INTEGER,
53
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
54
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
55
+ );
56
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
57
+ CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
58
+ CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
59
+
60
+ CREATE TABLE IF NOT EXISTS note_comments (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
63
+ author_id INTEGER NOT NULL,
64
+ content TEXT NOT NULL,
65
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
66
+ );
67
+ CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
68
+
69
+ CREATE TABLE IF NOT EXISTS note_reactions (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
72
+ user_id INTEGER NOT NULL,
73
+ emoji TEXT NOT NULL,
74
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
75
+ );
76
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
77
+ CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
78
+ `);
79
+ }
80
+
81
+ // packages/extensions/notes/src/routes.ts
82
+ import { Hono } from "hono";
83
+
84
+ // packages/extensions/notes/src/notes.ts
85
+ function slugify(text) {
86
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
87
+ }
88
+ async function createNote(db, getUser2, authorId, title, content, replyToId) {
89
+ let slug = slugify(title) || "untitled";
90
+ const existing = db.prepare("SELECT slug FROM notes WHERE author_id = ?").all(authorId);
91
+ const existingSlugs = new Set(existing.map((r) => r.slug));
92
+ if (existingSlugs.has(slug)) {
93
+ let i = 2;
94
+ while (existingSlugs.has(`${slug}-${i}`)) i++;
95
+ slug = `${slug}-${i}`;
96
+ }
97
+ const row = db.prepare(
98
+ `INSERT INTO notes (author_id, title, slug, content, reply_to_id)
99
+ VALUES (?, ?, ?, ?, ?)
100
+ RETURNING *`
101
+ ).get(authorId, title, slug, content, replyToId ?? null);
102
+ const author = await getUser2(authorId);
103
+ return {
104
+ ...row,
105
+ author_username: author?.username ?? "unknown",
106
+ author_display_name: author?.display_name ?? null,
107
+ comment_count: 0
108
+ };
109
+ }
110
+ async function getNote(db, getUser2, getUserByUsername2, authorUsername, slug) {
111
+ const author = await getUserByUsername2(authorUsername);
112
+ if (!author) return null;
113
+ const row = db.prepare("SELECT * FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
114
+ if (!row) return null;
115
+ const comments = await getComments(db, getUser2, row.id);
116
+ const reactions = await getReactions(db, getUser2, row.id);
117
+ let reply_to = null;
118
+ if (row.reply_to_id) {
119
+ const parent = db.prepare("SELECT * FROM notes WHERE id = ?").get(row.reply_to_id);
120
+ if (parent) {
121
+ const parentAuthor = await getUser2(parent.author_id);
122
+ reply_to = {
123
+ author_username: parentAuthor?.username ?? "unknown",
124
+ slug: parent.slug,
125
+ title: parent.title
126
+ };
127
+ }
128
+ }
129
+ const replyRows = db.prepare("SELECT * FROM notes WHERE reply_to_id = ? ORDER BY created_at").all(row.id);
130
+ const replies = [];
131
+ for (const r of replyRows) {
132
+ const replyAuthor = await getUser2(r.author_id);
133
+ replies.push({
134
+ author_username: replyAuthor?.username ?? "unknown",
135
+ slug: r.slug,
136
+ title: r.title,
137
+ created_at: r.created_at
138
+ });
139
+ }
140
+ return {
141
+ ...row,
142
+ author_username: authorUsername,
143
+ author_display_name: author.display_name ?? null,
144
+ comment_count: comments.length,
145
+ comments,
146
+ reactions,
147
+ reply_to,
148
+ replies
149
+ };
150
+ }
151
+ async function listNotes(db, getUser2, getUserByUsername2, opts) {
152
+ const limit = opts?.limit ?? 50;
153
+ const offset = opts?.offset ?? 0;
154
+ let authorId;
155
+ if (opts?.authorUsername) {
156
+ const author = await getUserByUsername2(opts.authorUsername);
157
+ if (!author) return [];
158
+ authorId = author.id;
159
+ }
160
+ const rows = authorId ? db.prepare(
161
+ "SELECT * FROM notes WHERE author_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
162
+ ).all(authorId, limit, offset) : db.prepare("SELECT * FROM notes ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
163
+ if (rows.length === 0) return [];
164
+ const noteIds = rows.map((r) => r.id);
165
+ const commentCounts = db.prepare(
166
+ `SELECT note_id, COUNT(*) as count FROM note_comments
167
+ WHERE note_id IN (${noteIds.map(() => "?").join(",")})
168
+ GROUP BY note_id`
169
+ ).all(...noteIds);
170
+ const countMap = new Map(commentCounts.map((r) => [r.note_id, r.count]));
171
+ const allReactions = db.prepare(
172
+ `SELECT note_id, emoji, COUNT(*) as count FROM note_reactions
173
+ WHERE note_id IN (${noteIds.map(() => "?").join(",")})
174
+ GROUP BY note_id, emoji`
175
+ ).all(...noteIds);
176
+ const reactionMap = /* @__PURE__ */ new Map();
177
+ for (const r of allReactions) {
178
+ if (!reactionMap.has(r.note_id)) reactionMap.set(r.note_id, []);
179
+ reactionMap.get(r.note_id).push({ emoji: r.emoji, count: r.count });
180
+ }
181
+ const replyToIds = [
182
+ ...new Set(rows.filter((r) => r.reply_to_id).map((r) => r.reply_to_id))
183
+ ];
184
+ const replyToMap = /* @__PURE__ */ new Map();
185
+ if (replyToIds.length > 0) {
186
+ const parents = db.prepare(`SELECT * FROM notes WHERE id IN (${replyToIds.map(() => "?").join(",")})`).all(...replyToIds);
187
+ for (const parent of parents) {
188
+ const parentAuthor = await getUser2(parent.author_id);
189
+ replyToMap.set(parent.id, {
190
+ author_username: parentAuthor?.username ?? "unknown",
191
+ slug: parent.slug,
192
+ title: parent.title
193
+ });
194
+ }
195
+ }
196
+ const authorCache = /* @__PURE__ */ new Map();
197
+ const result = [];
198
+ for (const r of rows) {
199
+ if (!authorCache.has(r.author_id)) {
200
+ const u = await getUser2(r.author_id);
201
+ authorCache.set(r.author_id, {
202
+ username: u?.username ?? "unknown",
203
+ display_name: u?.display_name ?? null
204
+ });
205
+ }
206
+ const authorInfo = authorCache.get(r.author_id);
207
+ const reactions = reactionMap.get(r.id);
208
+ const topReactions = reactions ? reactions.sort((a, b) => b.count - a.count).slice(0, 3).map((rx) => ({ ...rx, usernames: [] })) : void 0;
209
+ result.push({
210
+ ...r,
211
+ author_username: authorInfo.username,
212
+ author_display_name: authorInfo.display_name,
213
+ comment_count: countMap.get(r.id) ?? 0,
214
+ reactions: topReactions,
215
+ reply_to: r.reply_to_id ? replyToMap.get(r.reply_to_id) ?? null : null
216
+ });
217
+ }
218
+ return result;
219
+ }
220
+ async function updateNote(db, getUser2, getUserByUsername2, authorUsername, slug, updates) {
221
+ const author = await getUserByUsername2(authorUsername);
222
+ if (!author) return null;
223
+ const existing = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
224
+ if (!existing) return null;
225
+ const sets = ["updated_at = datetime('now')"];
226
+ const params = [];
227
+ if (updates.title !== void 0) {
228
+ sets.push("title = ?");
229
+ params.push(updates.title);
230
+ }
231
+ if (updates.content !== void 0) {
232
+ sets.push("content = ?");
233
+ params.push(updates.content);
234
+ }
235
+ params.push(existing.id);
236
+ db.prepare(`UPDATE notes SET ${sets.join(", ")} WHERE id = ?`).run(...params);
237
+ const full = await getNote(db, getUser2, getUserByUsername2, authorUsername, slug);
238
+ if (!full) return null;
239
+ const { comments, replies, ...note } = full;
240
+ return note;
241
+ }
242
+ async function deleteNote(db, getUserByUsername2, authorUsername, slug, authorId) {
243
+ const author = await getUserByUsername2(authorUsername);
244
+ if (!author) return false;
245
+ const existing = db.prepare("SELECT id, author_id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
246
+ if (!existing || existing.author_id !== authorId) return false;
247
+ db.prepare("DELETE FROM notes WHERE id = ?").run(existing.id);
248
+ return true;
249
+ }
250
+ async function addComment(db, getUser2, noteId, authorId, content) {
251
+ const row = db.prepare(`INSERT INTO note_comments (note_id, author_id, content) VALUES (?, ?, ?) RETURNING *`).get(noteId, authorId, content);
252
+ const author = await getUser2(authorId);
253
+ return {
254
+ ...row,
255
+ author_username: author?.username ?? "unknown",
256
+ author_display_name: author?.display_name ?? null
257
+ };
258
+ }
259
+ async function getComments(db, getUser2, noteId) {
260
+ const rows = db.prepare("SELECT * FROM note_comments WHERE note_id = ? ORDER BY created_at").all(noteId);
261
+ const result = [];
262
+ for (const row of rows) {
263
+ const author = await getUser2(row.author_id);
264
+ result.push({
265
+ ...row,
266
+ author_username: author?.username ?? "unknown",
267
+ author_display_name: author?.display_name ?? null
268
+ });
269
+ }
270
+ return result;
271
+ }
272
+ async function deleteComment(db, commentId, authorId) {
273
+ const existing = db.prepare("SELECT id, author_id FROM note_comments WHERE id = ?").get(commentId);
274
+ if (!existing || existing.author_id !== authorId) return false;
275
+ db.prepare("DELETE FROM note_comments WHERE id = ?").run(existing.id);
276
+ return true;
277
+ }
278
+ function toggleReaction(db, noteId, userId, emoji) {
279
+ const existing = db.prepare("SELECT id FROM note_reactions WHERE note_id = ? AND user_id = ? AND emoji = ?").get(noteId, userId, emoji);
280
+ if (existing) {
281
+ db.prepare("DELETE FROM note_reactions WHERE id = ?").run(existing.id);
282
+ return { added: false };
283
+ }
284
+ db.prepare("INSERT INTO note_reactions (note_id, user_id, emoji) VALUES (?, ?, ?)").run(
285
+ noteId,
286
+ userId,
287
+ emoji
288
+ );
289
+ return { added: true };
290
+ }
291
+ async function getReactions(db, getUser2, noteId) {
292
+ const rows = db.prepare("SELECT * FROM note_reactions WHERE note_id = ? ORDER BY emoji").all(noteId);
293
+ const userCache = /* @__PURE__ */ new Map();
294
+ const grouped = /* @__PURE__ */ new Map();
295
+ for (const r of rows) {
296
+ if (!grouped.has(r.emoji)) grouped.set(r.emoji, []);
297
+ grouped.get(r.emoji).push(r.user_id);
298
+ }
299
+ const result = [];
300
+ for (const [emoji, userIds] of grouped) {
301
+ const usernames = [];
302
+ for (const uid of userIds) {
303
+ if (!userCache.has(uid)) {
304
+ const u = await getUser2(uid);
305
+ userCache.set(uid, u?.username ?? "unknown");
306
+ }
307
+ usernames.push(userCache.get(uid));
308
+ }
309
+ result.push({ emoji, count: userIds.length, usernames });
310
+ }
311
+ return result;
312
+ }
313
+ async function resolveNoteId(db, getUserByUsername2, authorSlug) {
314
+ const [authorName, slug] = authorSlug.split("/", 2);
315
+ if (!authorName || !slug) return null;
316
+ const author = await getUserByUsername2(authorName);
317
+ if (!author) return null;
318
+ const row = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
319
+ return row?.id ?? null;
320
+ }
321
+
322
+ // packages/extensions/notes/src/routes.ts
323
+ async function parseJson(c) {
324
+ try {
325
+ return await c.req.json();
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+ function resolveUserId(c) {
331
+ const user = c.get("user");
332
+ if (!user || user.id === 0) return null;
333
+ return { id: user.id, username: user.username };
334
+ }
335
+ function createRoutes(ctx) {
336
+ if (!ctx.db) throw new Error("Notes extension requires a database");
337
+ const db = ctx.db;
338
+ const { getUser: getUser2, getUserByUsername: getUserByUsername2 } = ctx;
339
+ const app = new Hono().get("/", async (c) => {
340
+ const author = c.req.query("author");
341
+ const rawLimit = c.req.query("limit");
342
+ const rawOffset = c.req.query("offset");
343
+ const limit = rawLimit ? parseInt(rawLimit, 10) : void 0;
344
+ const offset = rawOffset ? parseInt(rawOffset, 10) : void 0;
345
+ if (limit !== void 0 && Number.isNaN(limit) || offset !== void 0 && Number.isNaN(offset)) {
346
+ return c.json({ error: "Invalid limit or offset parameter" }, 400);
347
+ }
348
+ const result = await listNotes(db, getUser2, getUserByUsername2, {
349
+ authorUsername: author,
350
+ limit,
351
+ offset
352
+ });
353
+ return c.json(result);
354
+ }).post("/", async (c) => {
355
+ const actor = resolveUserId(c);
356
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
357
+ const body = await parseJson(c);
358
+ if (!body) return c.json({ error: "Invalid JSON body" }, 400);
359
+ if (!body.title || !body.content) {
360
+ return c.json({ error: "title and content are required" }, 400);
361
+ }
362
+ let replyToId;
363
+ if (body.reply_to) {
364
+ const id = await resolveNoteId(db, getUserByUsername2, body.reply_to);
365
+ if (id === null) return c.json({ error: `Reply target not found: ${body.reply_to}` }, 404);
366
+ replyToId = id;
367
+ }
368
+ const note = await createNote(db, getUser2, actor.id, body.title, body.content, replyToId);
369
+ ctx.publishActivity({
370
+ type: "note_created",
371
+ mindName: actor.username,
372
+ title: body.title,
373
+ data: { author: actor.username, slug: note.slug }
374
+ });
375
+ return c.json(note, 201);
376
+ }).get("/:author/:slug", async (c) => {
377
+ const { author, slug } = c.req.param();
378
+ const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
379
+ if (!note) return c.json({ error: "Note not found" }, 404);
380
+ return c.json(note);
381
+ }).put("/:author/:slug", async (c) => {
382
+ const actor = resolveUserId(c);
383
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
384
+ const { author, slug } = c.req.param();
385
+ if (actor.username !== author) return c.json({ error: "Forbidden" }, 403);
386
+ const body = await parseJson(c);
387
+ if (!body) return c.json({ error: "Invalid JSON body" }, 400);
388
+ const note = await updateNote(db, getUser2, getUserByUsername2, author, slug, body);
389
+ if (!note) return c.json({ error: "Note not found" }, 404);
390
+ return c.json(note);
391
+ }).delete("/:author/:slug", async (c) => {
392
+ const actor = resolveUserId(c);
393
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
394
+ const { author, slug } = c.req.param();
395
+ const deleted = await deleteNote(db, getUserByUsername2, author, slug, actor.id);
396
+ if (!deleted) return c.json({ error: "Note not found or not authorized" }, 404);
397
+ return c.json({ ok: true });
398
+ }).post("/:author/:slug/reactions", async (c) => {
399
+ const actor = resolveUserId(c);
400
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
401
+ const { author, slug } = c.req.param();
402
+ const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
403
+ if (!note) return c.json({ error: "Note not found" }, 404);
404
+ const body = await parseJson(c);
405
+ if (!body) return c.json({ error: "Invalid JSON body" }, 400);
406
+ if (!body.emoji) return c.json({ error: "emoji is required" }, 400);
407
+ const result = toggleReaction(db, note.id, actor.id, body.emoji);
408
+ const reactions = await getReactions(db, getUser2, note.id);
409
+ return c.json({ ...result, reactions });
410
+ }).post("/:author/:slug/comments", async (c) => {
411
+ const actor = resolveUserId(c);
412
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
413
+ const { author, slug } = c.req.param();
414
+ const note = await getNote(db, getUser2, getUserByUsername2, author, slug);
415
+ if (!note) return c.json({ error: "Note not found" }, 404);
416
+ const body = await parseJson(c);
417
+ if (!body) return c.json({ error: "Invalid JSON body" }, 400);
418
+ if (!body.content) return c.json({ error: "content is required" }, 400);
419
+ const comment = await addComment(db, getUser2, note.id, actor.id, body.content);
420
+ return c.json(comment, 201);
421
+ }).delete("/:author/:slug/comments/:id", async (c) => {
422
+ const actor = resolveUserId(c);
423
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
424
+ const commentId = parseInt(c.req.param("id"), 10);
425
+ if (Number.isNaN(commentId)) return c.json({ error: "Invalid comment ID" }, 400);
426
+ const deleted = await deleteComment(db, commentId, actor.id);
427
+ if (!deleted) return c.json({ error: "Comment not found or not authorized" }, 404);
428
+ return c.json({ ok: true });
429
+ }).get("/feed", async (c) => {
430
+ const rawFeedLimit = c.req.query("limit");
431
+ const limit = rawFeedLimit ? parseInt(rawFeedLimit, 10) : 8;
432
+ if (Number.isNaN(limit)) return c.json({ error: "Invalid limit parameter" }, 400);
433
+ const mind = c.req.query("mind");
434
+ const notes = await listNotes(db, getUser2, getUserByUsername2, {
435
+ limit,
436
+ ...mind ? { authorUsername: mind } : {}
437
+ });
438
+ return c.json(
439
+ notes.map((n) => ({
440
+ id: `note-${n.author_username}-${n.slug}`,
441
+ title: n.title,
442
+ url: `/minds/${n.author_username}/notes/${n.slug}`,
443
+ date: n.created_at,
444
+ author: n.author_username,
445
+ bodyHtml: n.content,
446
+ icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2h6l4 4v8H4V2z"/><path d="M10 2v4h4"/><path d="M6 9h6M6 12h4"/></svg>',
447
+ color: "yellow"
448
+ }))
449
+ );
450
+ });
451
+ return app;
452
+ }
453
+
454
+ // packages/extensions/notes/src/index.ts
455
+ var assetsDir = resolve(import.meta.dirname, "../dist/ui");
456
+ var skillsDir = resolve(import.meta.dirname, "../skills");
457
+ var src_default = createExtension({
458
+ id: "notes",
459
+ name: "Notes",
460
+ version: "0.1.0",
461
+ description: "Public notes for sharing thoughts, reflections, and ideas",
462
+ routes: (ctx) => createRoutes(ctx),
463
+ initDb,
464
+ skillsDir,
465
+ standardSkill: true,
466
+ ui: {
467
+ assetsDir,
468
+ systemSection: { id: "notes", label: "Notes", urlPatterns: ["/notes", "/notes/:author/:slug"] },
469
+ mindSections: [{ id: "notes", label: "Notes" }],
470
+ feedSource: {
471
+ endpoint: "/api/ext/notes/feed"
472
+ }
473
+ }
474
+ });
475
+
476
+ // packages/extensions/pages/src/index.ts
477
+ import { resolve as resolve3 } from "path";
478
+
479
+ // packages/extensions/pages/src/routes.ts
480
+ import { readFile, stat } from "fs/promises";
481
+ import { extname, resolve as resolve2 } from "path";
482
+ import { Hono as Hono2 } from "hono";
483
+ var MIME_TYPES = {
484
+ ".html": "text/html",
485
+ ".js": "application/javascript",
486
+ ".css": "text/css",
487
+ ".json": "application/json",
488
+ ".svg": "image/svg+xml",
489
+ ".png": "image/png",
490
+ ".jpg": "image/jpeg",
491
+ ".jpeg": "image/jpeg",
492
+ ".gif": "image/gif",
493
+ ".ico": "image/x-icon",
494
+ ".woff": "font/woff",
495
+ ".woff2": "font/woff2",
496
+ ".txt": "text/plain",
497
+ ".xml": "application/xml"
498
+ };
499
+ var _pagesWatcher = null;
500
+ async function getPagesWatcher() {
501
+ if (_pagesWatcher) return _pagesWatcher;
502
+ const mod = await import("./pages-watcher-Z3PKNROC.js");
503
+ _pagesWatcher = mod;
504
+ return _pagesWatcher;
505
+ }
506
+ function createRoutes2(ctx) {
507
+ return new Hono2().get("/", async (c) => {
508
+ const pw = await getPagesWatcher();
509
+ const sites = await pw.getCachedSites();
510
+ const recentPages = await pw.getCachedRecentPages();
511
+ return c.json({ sites, recentPages });
512
+ }).get("/feed", async (c) => {
513
+ const pw = await getPagesWatcher();
514
+ let recentPages = await pw.getCachedRecentPages();
515
+ const mind = c.req.query("mind");
516
+ if (mind) recentPages = recentPages.filter((p) => p.mind === mind);
517
+ const rawLimit = c.req.query("limit");
518
+ const limit = rawLimit ? parseInt(rawLimit, 10) : 8;
519
+ return c.json(
520
+ recentPages.slice(0, limit).map((p) => ({
521
+ id: `page-${p.mind}-${p.file}`,
522
+ title: `${p.mind}/${p.file}`,
523
+ url: p.url ?? `/minds/${p.mind}/pages/${p.file}`,
524
+ date: p.modified,
525
+ author: p.mind,
526
+ bodyHtml: `<p>Page updated</p>`,
527
+ iframeUrl: `/ext/pages/public/${p.mind}/${p.file}`,
528
+ icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M2 8h12M8 2c-2 2-2 10 0 12M8 2c2 2 2 10 0 12"/></svg>',
529
+ color: "purple"
530
+ }))
531
+ );
532
+ }).put("/publish/:name", async (c) => {
533
+ const user = ctx.resolveUser(c);
534
+ if (!user) return c.json({ error: "Unauthorized" }, 401);
535
+ const name = c.req.param("name");
536
+ if (user.role !== "admin" && user.username !== name) {
537
+ return c.json({ error: "Forbidden" }, 403);
538
+ }
539
+ const config = ctx.getSystemsConfig();
540
+ if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
541
+ const body = await c.req.text();
542
+ try {
543
+ const res = await fetch(`${config.apiUrl}/api/pages/publish/${name}`, {
544
+ method: "PUT",
545
+ headers: {
546
+ "Content-Type": "application/json",
547
+ Authorization: `Bearer ${config.apiKey}`
548
+ },
549
+ body
550
+ });
551
+ const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
552
+ return c.json(data, res.status);
553
+ } catch (err) {
554
+ return c.json({ error: `Connection failed: ${err.message}` }, 502);
555
+ }
556
+ }).get("/status/:name", async (c) => {
557
+ const user = ctx.resolveUser(c);
558
+ if (!user) return c.json({ error: "Unauthorized" }, 401);
559
+ const name = c.req.param("name");
560
+ if (user.role !== "admin" && user.username !== name) {
561
+ return c.json({ error: "Forbidden" }, 403);
562
+ }
563
+ const config = ctx.getSystemsConfig();
564
+ if (!config) return c.json({ error: "Not connected to volute.systems" }, 400);
565
+ try {
566
+ const res = await fetch(`${config.apiUrl}/api/pages/status/${name}`, {
567
+ headers: { Authorization: `Bearer ${config.apiKey}` }
568
+ });
569
+ const data = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
570
+ return c.json(data, res.status);
571
+ } catch (err) {
572
+ return c.json({ error: `Connection failed: ${err.message}` }, 502);
573
+ }
574
+ });
575
+ }
576
+ var _voluteHome = null;
577
+ async function getVoluteHome() {
578
+ if (_voluteHome) return _voluteHome();
579
+ const mod = await import("./registry-ODSALQQL.js");
580
+ _voluteHome = mod.voluteHome;
581
+ return _voluteHome();
582
+ }
583
+ function createPublicRoutes(ctx) {
584
+ return new Hono2().get("/:name/*", async (c) => {
585
+ const name = c.req.param("name");
586
+ let pagesRoot;
587
+ if (name === "_system") {
588
+ const home = await getVoluteHome();
589
+ pagesRoot = resolve2(home, "shared", "pages");
590
+ } else {
591
+ const mindDirPath = ctx.getMindDir(name);
592
+ if (!mindDirPath) return c.text("Not found", 404);
593
+ pagesRoot = resolve2(mindDirPath, "home", "public", "pages");
594
+ }
595
+ const prefix = `/public/${name}`;
596
+ const idx = c.req.path.indexOf(prefix);
597
+ const wildcard = idx >= 0 ? c.req.path.slice(idx + prefix.length) : "/";
598
+ const requestedPath = resolve2(pagesRoot, wildcard.slice(1));
599
+ if (requestedPath !== pagesRoot && !requestedPath.startsWith(pagesRoot + "/"))
600
+ return c.text("Forbidden", 403);
601
+ let fileStat = await stat(requestedPath).catch(() => null);
602
+ if (fileStat?.isDirectory()) {
603
+ const indexPath = resolve2(requestedPath, "index.html");
604
+ fileStat = await stat(indexPath).catch(() => null);
605
+ if (fileStat?.isFile()) {
606
+ const body = await readFile(indexPath);
607
+ return c.body(body, 200, { "Content-Type": "text/html" });
608
+ }
609
+ return c.text("Not found", 404);
610
+ }
611
+ if (fileStat?.isFile()) {
612
+ const ext = extname(requestedPath);
613
+ const mime = MIME_TYPES[ext] || "application/octet-stream";
614
+ const body = await readFile(requestedPath);
615
+ return c.body(body, 200, { "Content-Type": mime });
616
+ }
617
+ return c.text("Not found", 404);
618
+ });
619
+ }
620
+
621
+ // packages/extensions/pages/src/index.ts
622
+ var assetsDir2 = resolve3(import.meta.dirname, "../dist/ui");
623
+ var skillsDir2 = resolve3(import.meta.dirname, "../skills");
624
+ var _watcher = null;
625
+ async function getWatcher() {
626
+ if (_watcher) return _watcher;
627
+ _watcher = await import("./pages-watcher-Z3PKNROC.js");
628
+ return _watcher;
629
+ }
630
+ var src_default2 = createExtension({
631
+ id: "pages",
632
+ name: "Pages",
633
+ version: "0.1.0",
634
+ description: "Publish and serve web pages from mind directories",
635
+ routes: (ctx) => createRoutes2(ctx),
636
+ publicRoutes: (ctx) => createPublicRoutes(ctx),
637
+ skillsDir: skillsDir2,
638
+ standardSkill: true,
639
+ ui: {
640
+ assetsDir: assetsDir2,
641
+ systemSection: {
642
+ id: "pages",
643
+ label: "Pages",
644
+ urlPatterns: ["/pages", "/pages/:site", "/pages/:site/:path"]
645
+ },
646
+ mindSections: [{ id: "pages", label: "Pages" }],
647
+ feedSource: {
648
+ endpoint: "/api/ext/pages/feed"
649
+ }
650
+ },
651
+ onDaemonStart: () => {
652
+ getWatcher().then((w) => w.startSystemWatcher()).catch(
653
+ (err) => console.error("[pages] failed to start system watcher:", err.message)
654
+ );
655
+ },
656
+ onDaemonStop: () => {
657
+ getWatcher().then((w) => w.stopAllWatchers()).catch((err) => console.error("[pages] failed to stop watchers:", err.message));
658
+ },
659
+ onMindStart: (mindName) => {
660
+ getWatcher().then((w) => w.startWatcher(mindName)).catch(
661
+ (err) => console.error(`[pages] failed to start watcher for ${mindName}:`, err.message)
662
+ );
663
+ },
664
+ onMindStop: (mindName) => {
665
+ getWatcher().then((w) => w.stopWatcher(mindName)).catch(
666
+ (err) => console.error(`[pages] failed to stop watcher for ${mindName}:`, err.message)
667
+ );
668
+ }
669
+ });
670
+
671
+ // src/lib/auth.ts
672
+ import { compareSync, hashSync } from "bcryptjs";
673
+ import { and, count, eq, inArray } from "drizzle-orm";
674
+ var userSelectFields = {
675
+ id: users.id,
676
+ username: users.username,
677
+ role: users.role,
678
+ user_type: users.user_type,
679
+ display_name: users.display_name,
680
+ description: users.description,
681
+ avatar: users.avatar,
682
+ created_at: users.created_at
683
+ };
684
+ async function createUser(username, password) {
685
+ const db = await getDb();
686
+ const hash = hashSync(password, 10);
687
+ const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
688
+ const role = value === 0 ? "admin" : "pending";
689
+ const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning(userSelectFields);
690
+ return result;
691
+ }
692
+ async function verifyUser(username, password) {
693
+ const db = await getDb();
694
+ const row = await db.select().from(users).where(eq(users.username, username)).get();
695
+ if (!row) return null;
696
+ if (row.user_type === "mind") return null;
697
+ if (!compareSync(password, row.password_hash)) return null;
698
+ const { password_hash: _, ...user } = row;
699
+ return user;
700
+ }
701
+ async function getUser(id) {
702
+ const db = await getDb();
703
+ const row = await db.select(userSelectFields).from(users).where(eq(users.id, id)).get();
704
+ return row ?? null;
705
+ }
706
+ async function getUserByUsername(username) {
707
+ const db = await getDb();
708
+ const row = await db.select(userSelectFields).from(users).where(eq(users.username, username)).get();
709
+ return row ?? null;
710
+ }
711
+ async function listUsers() {
712
+ const db = await getDb();
713
+ return db.select(userSelectFields).from(users).orderBy(users.created_at).all();
714
+ }
715
+ async function listPendingUsers() {
716
+ const db = await getDb();
717
+ return db.select(userSelectFields).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
718
+ }
719
+ async function listUsersByType(userType) {
720
+ const db = await getDb();
721
+ return db.select(userSelectFields).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
722
+ }
723
+ async function getOrCreateMindUser(mindName) {
724
+ const db = await getDb();
725
+ const existing = await db.select(userSelectFields).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
726
+ if (existing) return existing;
727
+ try {
728
+ const [result] = await db.insert(users).values({
729
+ username: mindName,
730
+ password_hash: "!mind",
731
+ role: "user",
732
+ user_type: "mind"
733
+ }).returning(userSelectFields);
734
+ return result;
735
+ } catch (err) {
736
+ if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
737
+ const retried = await db.select(userSelectFields).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
738
+ if (retried) return retried;
739
+ }
740
+ throw err;
741
+ }
742
+ }
743
+ async function deleteMindUser(mindName) {
744
+ const db = await getDb();
745
+ await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
746
+ }
747
+ async function changePassword(userId, currentPassword, newPassword) {
748
+ const db = await getDb();
749
+ const row = await db.select().from(users).where(eq(users.id, userId)).get();
750
+ if (!row) return false;
751
+ if (!compareSync(currentPassword, row.password_hash)) return false;
752
+ const hash = hashSync(newPassword, 10);
753
+ await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
754
+ return true;
755
+ }
756
+ async function approveUser(id) {
757
+ const db = await getDb();
758
+ await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
759
+ }
760
+ async function countAdmins() {
761
+ const db = await getDb();
762
+ const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.role, "admin"));
763
+ return value;
764
+ }
765
+ async function setUserRole(id, role) {
766
+ const db = await getDb();
767
+ const target = await db.select({ id: users.id }).from(users).where(eq(users.id, id)).get();
768
+ if (!target) throw new Error("User not found");
769
+ await db.update(users).set({ role }).where(eq(users.id, id));
770
+ }
771
+ async function deleteUser(id) {
772
+ const db = await getDb();
773
+ const target = await db.select({ id: users.id }).from(users).where(and(eq(users.id, id), eq(users.user_type, "brain"))).get();
774
+ if (!target) throw new Error("User not found");
775
+ await db.delete(users).where(and(eq(users.id, id), eq(users.user_type, "brain")));
776
+ }
777
+ async function updateUserProfile(userId, profile) {
778
+ const db = await getDb();
779
+ const target = await db.select({ id: users.id }).from(users).where(eq(users.id, userId)).get();
780
+ if (!target) throw new Error("User not found");
781
+ await db.update(users).set(profile).where(eq(users.id, userId));
782
+ }
783
+ async function syncMindProfile(mindName, config) {
784
+ const user = await getOrCreateMindUser(mindName);
785
+ const newProfile = {
786
+ display_name: config.displayName ?? null,
787
+ description: config.description ?? null,
788
+ avatar: config.avatar ?? null
789
+ };
790
+ const changed = user.display_name !== newProfile.display_name || user.description !== newProfile.description || user.avatar !== newProfile.avatar;
791
+ if (!changed) return;
792
+ const db = await getDb();
793
+ await db.update(users).set(newProfile).where(eq(users.id, user.id));
794
+ broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
795
+ }
796
+ async function migrateMindRoles() {
797
+ const db = await getDb();
798
+ await db.update(users).set({ role: "user" }).where(and(eq(users.user_type, "mind"), inArray(users.role, ["mind", "agent"])));
799
+ }
800
+
801
+ // src/lib/systems-config.ts
802
+ import {
803
+ existsSync,
804
+ mkdirSync,
805
+ readFileSync,
806
+ renameSync,
807
+ unlinkSync,
808
+ writeFileSync
809
+ } from "fs";
810
+ import { resolve as resolve4 } from "path";
811
+ var DEFAULT_API_URL = "https://volute.systems";
812
+ function configPath() {
813
+ return resolve4(voluteSystemDir(), "systems.json");
814
+ }
815
+ function migrateIfNeeded() {
816
+ const target = configPath();
817
+ if (existsSync(target)) return;
818
+ const oldPaths = [
819
+ resolve4(voluteUserHome(), "systems.json"),
820
+ resolve4(voluteHome(), "systems.json")
821
+ ];
822
+ for (const old of oldPaths) {
823
+ if (old !== target && existsSync(old)) {
824
+ try {
825
+ mkdirSync(voluteSystemDir(), { recursive: true });
826
+ renameSync(old, target);
827
+ } catch {
828
+ }
829
+ return;
830
+ }
831
+ }
832
+ }
833
+ function readSystemsConfig() {
834
+ migrateIfNeeded();
835
+ const path = configPath();
836
+ if (!existsSync(path)) return null;
837
+ const raw = readFileSync(path, "utf-8");
838
+ let data;
839
+ try {
840
+ data = JSON.parse(raw);
841
+ } catch {
842
+ console.error(
843
+ `Warning: ${path} contains invalid JSON. Run "volute systems logout" and re-login.`
844
+ );
845
+ return null;
846
+ }
847
+ if (!data.apiKey || !data.system) return null;
848
+ return {
849
+ apiKey: data.apiKey,
850
+ system: data.system,
851
+ apiUrl: data.apiUrl || DEFAULT_API_URL
852
+ };
853
+ }
854
+ function writeSystemsConfig(config) {
855
+ mkdirSync(voluteSystemDir(), { recursive: true });
856
+ writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}
857
+ `, { mode: 384 });
858
+ }
859
+ function deleteSystemsConfig() {
860
+ try {
861
+ unlinkSync(configPath());
862
+ return true;
863
+ } catch (err) {
864
+ if (err.code === "ENOENT") return false;
865
+ throw err;
866
+ }
867
+ }
868
+
869
+ // src/lib/extensions.ts
870
+ var VALID_EXTENSION_ID2 = /^[a-z0-9][a-z0-9_-]*$/;
871
+ var loaded = [];
872
+ function extensionsBaseDir() {
873
+ return resolve5(voluteHome(), "extensions");
874
+ }
875
+ function extensionDataDir(id) {
876
+ return resolve5(voluteSystemDir(), "extension-data", id);
877
+ }
878
+ function extensionsConfigPath() {
879
+ return resolve5(voluteHome(), "system", "extensions.json");
880
+ }
881
+ function readExtensionsConfig() {
882
+ const configPath2 = extensionsConfigPath();
883
+ if (!existsSync2(configPath2)) return [];
884
+ try {
885
+ const data = JSON.parse(readFileSync2(configPath2, "utf-8"));
886
+ return Array.isArray(data) ? data : [];
887
+ } catch (err) {
888
+ logger_default.warn("failed to read extensions config, ignoring installed extensions", {
889
+ path: configPath2,
890
+ error: err.message
891
+ });
892
+ return [];
893
+ }
894
+ }
895
+ var _LibsqlDatabase = null;
896
+ async function getLibsqlDatabase() {
897
+ if (_LibsqlDatabase) return _LibsqlDatabase;
898
+ const mod = await import("libsql");
899
+ _LibsqlDatabase = mod.default ?? mod;
900
+ return _LibsqlDatabase;
901
+ }
902
+ async function openExtensionDb(_id, dataDir) {
903
+ const dbPath = resolve5(dataDir, "data.db");
904
+ const Database = await getLibsqlDatabase();
905
+ return new Database(dbPath);
906
+ }
907
+ async function migrateNotesFromCoreDb(extDb) {
908
+ const coreDbPath = process.env.VOLUTE_DB_PATH || resolve5(voluteSystemDir(), "volute.db");
909
+ if (!existsSync2(coreDbPath)) return;
910
+ const existing = extDb.prepare("SELECT COUNT(*) as c FROM notes").get();
911
+ if (existing.c > 0) return;
912
+ const Database = await getLibsqlDatabase();
913
+ const coreDb = new Database(coreDbPath);
914
+ try {
915
+ const tableExists = coreDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'").get();
916
+ if (!tableExists) return;
917
+ const coreNotes = coreDb.prepare(
918
+ "SELECT id, author_id, title, slug, content, reply_to_id, created_at, updated_at FROM notes ORDER BY id"
919
+ ).all();
920
+ if (coreNotes.length === 0) return;
921
+ logger_default.info(`migrating ${coreNotes.length} notes from core DB to extension DB`);
922
+ const coreComments = coreDb.prepare("SELECT id, note_id, author_id, content, created_at FROM note_comments ORDER BY id").all();
923
+ const coreReactions = coreDb.prepare("SELECT id, note_id, user_id, emoji, created_at FROM note_reactions ORDER BY id").all();
924
+ extDb.exec("BEGIN TRANSACTION");
925
+ try {
926
+ for (const note of coreNotes) {
927
+ extDb.prepare(
928
+ "INSERT OR IGNORE INTO notes (id, author_id, title, slug, content, reply_to_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
929
+ ).run(
930
+ note.id,
931
+ note.author_id,
932
+ note.title,
933
+ note.slug,
934
+ note.content,
935
+ note.reply_to_id,
936
+ note.created_at,
937
+ note.updated_at
938
+ );
939
+ }
940
+ for (const comment of coreComments) {
941
+ extDb.prepare(
942
+ "INSERT OR IGNORE INTO note_comments (id, note_id, author_id, content, created_at) VALUES (?, ?, ?, ?, ?)"
943
+ ).run(comment.id, comment.note_id, comment.author_id, comment.content, comment.created_at);
944
+ }
945
+ for (const reaction of coreReactions) {
946
+ extDb.prepare(
947
+ "INSERT OR IGNORE INTO note_reactions (id, note_id, user_id, emoji, created_at) VALUES (?, ?, ?, ?, ?)"
948
+ ).run(
949
+ reaction.id,
950
+ reaction.note_id,
951
+ reaction.user_id,
952
+ reaction.emoji,
953
+ reaction.created_at
954
+ );
955
+ }
956
+ extDb.exec("COMMIT");
957
+ } catch (txErr) {
958
+ extDb.exec("ROLLBACK");
959
+ throw txErr;
960
+ }
961
+ logger_default.info(
962
+ `migrated ${coreNotes.length} notes, ${coreComments.length} comments, ${coreReactions.length} reactions`
963
+ );
964
+ } catch (err) {
965
+ logger_default.error("failed to migrate notes from core DB", logger_default.errorData(err));
966
+ } finally {
967
+ coreDb.close();
968
+ }
969
+ }
970
+ async function buildContext(manifest, dataDir, authMw) {
971
+ let db = null;
972
+ if (manifest.initDb) {
973
+ const realDb = await openExtensionDb(manifest.id, dataDir);
974
+ try {
975
+ manifest.initDb(realDb);
976
+ } catch (err) {
977
+ realDb.close();
978
+ throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
979
+ }
980
+ if (manifest.id === "notes") {
981
+ await migrateNotesFromCoreDb(realDb);
982
+ }
983
+ db = realDb;
984
+ }
985
+ return {
986
+ db,
987
+ authMiddleware: authMw,
988
+ resolveUser: (c) => {
989
+ const user = c.get("user");
990
+ if (!user || typeof user !== "object") return null;
991
+ return user;
992
+ },
993
+ getUser: async (id) => getUser(id),
994
+ getUserByUsername: async (username) => getUserByUsername(username),
995
+ publishActivity: (event) => {
996
+ publish(event).catch(
997
+ (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
998
+ );
999
+ },
1000
+ getMindDir: (name) => {
1001
+ try {
1002
+ const dir = mindDir(name);
1003
+ return existsSync2(dir) ? dir : null;
1004
+ } catch (err) {
1005
+ logger_default.warn(
1006
+ `extension ${manifest.id}: failed to resolve mind dir for ${name}`,
1007
+ logger_default.errorData(err)
1008
+ );
1009
+ return null;
1010
+ }
1011
+ },
1012
+ getSystemsConfig: () => readSystemsConfig(),
1013
+ dataDir
1014
+ };
1015
+ }
1016
+ async function loadExtension(manifest, app, authMw) {
1017
+ if (!VALID_EXTENSION_ID2.test(manifest.id)) {
1018
+ logger_default.error(`invalid extension ID "${manifest.id}", skipping (must match ${VALID_EXTENSION_ID2})`);
1019
+ return;
1020
+ }
1021
+ const dataDir = extensionDataDir(manifest.id);
1022
+ mkdirSync2(dataDir, { recursive: true });
1023
+ const context = await buildContext(manifest, dataDir, authMw);
1024
+ const routesApp = manifest.routes(context);
1025
+ const extApiPath = `/api/ext/${manifest.id}`;
1026
+ app.use(extApiPath, authMw);
1027
+ app.use(`${extApiPath}/*`, authMw);
1028
+ app.route(extApiPath, routesApp);
1029
+ if (manifest.publicRoutes) {
1030
+ const publicApp = manifest.publicRoutes(context);
1031
+ app.route(`/ext/${manifest.id}/public`, publicApp);
1032
+ }
1033
+ let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
1034
+ if (resolvedAssetsDir && !existsSync2(resolvedAssetsDir)) {
1035
+ let searchDir = dirname(new URL(import.meta.url).pathname);
1036
+ for (let i = 0; i < 5; i++) {
1037
+ const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
1038
+ if (existsSync2(candidate)) {
1039
+ resolvedAssetsDir = candidate;
1040
+ break;
1041
+ }
1042
+ searchDir = dirname(searchDir);
1043
+ }
1044
+ }
1045
+ if (resolvedAssetsDir && existsSync2(resolvedAssetsDir)) {
1046
+ const assetsDir3 = resolvedAssetsDir;
1047
+ const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
1048
+ const { extname: ext } = await import("path");
1049
+ const mimeTypes = {
1050
+ ".html": "text/html",
1051
+ ".js": "application/javascript",
1052
+ ".css": "text/css",
1053
+ ".json": "application/json",
1054
+ ".svg": "image/svg+xml",
1055
+ ".png": "image/png",
1056
+ ".jpg": "image/jpeg",
1057
+ ".ico": "image/x-icon",
1058
+ ".woff": "font/woff",
1059
+ ".woff2": "font/woff2"
1060
+ };
1061
+ const prefix = `/ext/${manifest.id}`;
1062
+ const indexPath = resolve5(assetsDir3, "index.html");
1063
+ const serveExtAssets = async (c) => {
1064
+ const urlPath = new URL(c.req.url).pathname;
1065
+ const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
1066
+ const filePath = resolve5(assetsDir3, relativePath);
1067
+ if (filePath !== assetsDir3 && !filePath.startsWith(assetsDir3 + "/"))
1068
+ return c.text("Forbidden", 403);
1069
+ const s = await fsStat(filePath).catch(() => null);
1070
+ if (s?.isFile()) {
1071
+ const mime = mimeTypes[ext(filePath)] || "application/octet-stream";
1072
+ const body = await readFile2(filePath);
1073
+ return c.body(body, 200, { "Content-Type": mime });
1074
+ }
1075
+ if (existsSync2(indexPath)) {
1076
+ const body = await readFile2(indexPath, "utf-8");
1077
+ return c.html(body);
1078
+ }
1079
+ return c.text("Not found", 404);
1080
+ };
1081
+ app.get(`${prefix}/*`, serveExtAssets);
1082
+ app.get(prefix, serveExtAssets);
1083
+ }
1084
+ const skillsDir3 = resolveSkillsDir(manifest);
1085
+ if (skillsDir3) {
1086
+ let entries;
1087
+ try {
1088
+ entries = readdirSync(skillsDir3, { withFileTypes: true });
1089
+ } catch (err) {
1090
+ logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
1091
+ entries = [];
1092
+ }
1093
+ for (const entry of entries) {
1094
+ if (!entry.isDirectory()) continue;
1095
+ try {
1096
+ const skillPath = resolve5(skillsDir3, entry.name);
1097
+ const sourceHash = hashSkillDir(skillPath);
1098
+ const destDir = resolve5(sharedSkillsDir(), entry.name);
1099
+ if (existsSync2(destDir)) {
1100
+ const destHash = hashSkillDir(destDir);
1101
+ if (sourceHash === destHash) continue;
1102
+ }
1103
+ await importSkillFromDir(skillPath, `ext:${manifest.id}`);
1104
+ logger_default.info(`synced skill "${entry.name}" for extension: ${manifest.id}`);
1105
+ } catch (err) {
1106
+ logger_default.error(
1107
+ `failed to sync skill "${entry.name}" for extension ${manifest.id}`,
1108
+ logger_default.errorData(err)
1109
+ );
1110
+ }
1111
+ }
1112
+ }
1113
+ if (manifest.standardSkill && !manifest.skillsDir) {
1114
+ logger_default.warn(`extension ${manifest.id}: standardSkill is true but no skillsDir declared`);
1115
+ }
1116
+ loaded.push({ manifest, context });
1117
+ logger_default.info(`loaded extension: ${manifest.id} v${manifest.version}`);
1118
+ }
1119
+ function resolveSkillsDir(manifest) {
1120
+ if (!manifest.skillsDir) return null;
1121
+ let searchDir = dirname(new URL(import.meta.url).pathname);
1122
+ for (let i = 0; i < 5; i++) {
1123
+ const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "skills");
1124
+ if (existsSync2(candidate)) return candidate;
1125
+ searchDir = dirname(searchDir);
1126
+ }
1127
+ if (existsSync2(manifest.skillsDir)) return manifest.skillsDir;
1128
+ logger_default.warn(`skills dir not found for extension ${manifest.id}: ${manifest.skillsDir}`);
1129
+ return null;
1130
+ }
1131
+ function discoverBuiltinExtensions() {
1132
+ return [src_default, src_default2];
1133
+ }
1134
+ async function discoverInstalledExtensions() {
1135
+ const manifests = [];
1136
+ const packages = readExtensionsConfig();
1137
+ const npmDir = resolve5(voluteHome(), "extensions", "_npm");
1138
+ const { createRequire } = await import("module");
1139
+ for (const pkg of packages) {
1140
+ try {
1141
+ let resolved = pkg;
1142
+ const npmPkgDir = resolve5(npmDir, "node_modules", pkg);
1143
+ if (existsSync2(npmPkgDir)) {
1144
+ const require2 = createRequire(resolve5(npmDir, "noop.js"));
1145
+ resolved = require2.resolve(pkg);
1146
+ }
1147
+ const mod = await import(resolved);
1148
+ const manifest = mod.default ?? mod.extension ?? mod;
1149
+ if (!validateManifest(manifest, `package ${pkg}`)) continue;
1150
+ manifests.push(manifest);
1151
+ } catch (err) {
1152
+ logger_default.error(`failed to load extension package: ${pkg}`, logger_default.errorData(err));
1153
+ }
1154
+ }
1155
+ return manifests;
1156
+ }
1157
+ function validateManifest(manifest, source) {
1158
+ if (!manifest || typeof manifest !== "object") {
1159
+ logger_default.warn(`extension from ${source} does not export a valid manifest`);
1160
+ return false;
1161
+ }
1162
+ const m = manifest;
1163
+ if (!m.id || typeof m.id !== "string") {
1164
+ logger_default.warn(`extension from ${source} is missing a valid id`);
1165
+ return false;
1166
+ }
1167
+ if (!VALID_EXTENSION_ID2.test(m.id)) {
1168
+ logger_default.warn(`extension from ${source} has invalid id "${m.id}"`);
1169
+ return false;
1170
+ }
1171
+ if (typeof m.routes !== "function") {
1172
+ logger_default.warn(`extension from ${source} is missing a routes function`);
1173
+ return false;
1174
+ }
1175
+ if (!m.name || typeof m.name !== "string") {
1176
+ logger_default.warn(`extension "${m.id}" from ${source} is missing a name`);
1177
+ return false;
1178
+ }
1179
+ if (!m.version || typeof m.version !== "string") {
1180
+ logger_default.warn(`extension "${m.id}" from ${source} is missing a version`);
1181
+ return false;
1182
+ }
1183
+ return true;
1184
+ }
1185
+ async function discoverLocalExtensions() {
1186
+ const baseDir = extensionsBaseDir();
1187
+ if (!existsSync2(baseDir)) return [];
1188
+ const manifests = [];
1189
+ let entries;
1190
+ try {
1191
+ entries = readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
1192
+ } catch (err) {
1193
+ logger_default.error("failed to read local extensions directory", logger_default.errorData(err));
1194
+ return [];
1195
+ }
1196
+ for (const dir of entries) {
1197
+ const extDir = resolve5(baseDir, dir);
1198
+ const candidates = [resolve5(extDir, "src", "index.js"), resolve5(extDir, "index.js")];
1199
+ const entryPoint = candidates.find((p) => existsSync2(p));
1200
+ if (!entryPoint) continue;
1201
+ try {
1202
+ const mod = await import(entryPoint);
1203
+ const manifest = mod.default ?? mod.extension ?? mod;
1204
+ if (!validateManifest(manifest, `local dir ${extDir}`)) continue;
1205
+ manifests.push(manifest);
1206
+ logger_default.info(`discovered local extension: ${manifest.id} from ${extDir}`);
1207
+ } catch (err) {
1208
+ logger_default.error(`failed to load local extension from ${extDir}`, logger_default.errorData(err));
1209
+ }
1210
+ }
1211
+ return manifests;
1212
+ }
1213
+ async function loadAllExtensions(app, authMw) {
1214
+ const builtins = discoverBuiltinExtensions();
1215
+ const installed = await discoverInstalledExtensions();
1216
+ const local = await discoverLocalExtensions();
1217
+ const all = [...builtins, ...installed, ...local];
1218
+ const seen = /* @__PURE__ */ new Set();
1219
+ for (const manifest of all) {
1220
+ if (seen.has(manifest.id)) {
1221
+ logger_default.warn(`duplicate extension ID: ${manifest.id}, skipping`);
1222
+ continue;
1223
+ }
1224
+ seen.add(manifest.id);
1225
+ try {
1226
+ await loadExtension(manifest, app, authMw);
1227
+ } catch (err) {
1228
+ logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
1229
+ }
1230
+ }
1231
+ }
1232
+ function getLoadedExtensions() {
1233
+ return loaded.map(({ manifest }) => ({
1234
+ id: manifest.id,
1235
+ name: manifest.name,
1236
+ version: manifest.version,
1237
+ description: manifest.description,
1238
+ systemSection: manifest.ui?.systemSection,
1239
+ mindSections: manifest.ui?.mindSections,
1240
+ feedSource: manifest.ui?.feedSource
1241
+ }));
1242
+ }
1243
+ function getExtensionStandardSkills() {
1244
+ const skills = [];
1245
+ for (const { manifest } of loaded) {
1246
+ if (!manifest.standardSkill) continue;
1247
+ const dir = resolveSkillsDir(manifest);
1248
+ if (!dir) continue;
1249
+ try {
1250
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1251
+ if (entry.isDirectory()) skills.push(entry.name);
1252
+ }
1253
+ } catch (err) {
1254
+ logger_default.warn(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
1255
+ }
1256
+ }
1257
+ return skills;
1258
+ }
1259
+ function notifyExtensionsDaemonStart() {
1260
+ for (const { manifest } of loaded) {
1261
+ try {
1262
+ manifest.onDaemonStart?.();
1263
+ } catch (err) {
1264
+ logger_default.error(`extension ${manifest.id}: onDaemonStart failed`, logger_default.errorData(err));
1265
+ }
1266
+ }
1267
+ }
1268
+ function notifyExtensionsDaemonStop() {
1269
+ for (const { manifest, context } of loaded) {
1270
+ try {
1271
+ manifest.onDaemonStop?.();
1272
+ } catch (err) {
1273
+ logger_default.error(`extension ${manifest.id}: onDaemonStop failed`, logger_default.errorData(err));
1274
+ }
1275
+ try {
1276
+ context.db?.close();
1277
+ } catch (err) {
1278
+ logger_default.warn(`extension ${manifest.id}: failed to close db`, logger_default.errorData(err));
1279
+ }
1280
+ }
1281
+ loaded.length = 0;
1282
+ }
1283
+ function notifyExtensionsMindStart(mindName) {
1284
+ for (const { manifest } of loaded) {
1285
+ try {
1286
+ manifest.onMindStart?.(mindName);
1287
+ } catch (err) {
1288
+ logger_default.error(`extension ${manifest.id}: onMindStart failed for ${mindName}`, logger_default.errorData(err));
1289
+ }
1290
+ }
1291
+ }
1292
+ function notifyExtensionsMindStop(mindName) {
1293
+ for (const { manifest } of loaded) {
1294
+ try {
1295
+ manifest.onMindStop?.(mindName);
1296
+ } catch (err) {
1297
+ logger_default.error(`extension ${manifest.id}: onMindStop failed for ${mindName}`, logger_default.errorData(err));
1298
+ }
1299
+ }
1300
+ }
1301
+
1302
+ export {
1303
+ createUser,
1304
+ verifyUser,
1305
+ getUser,
1306
+ getUserByUsername,
1307
+ listUsers,
1308
+ listPendingUsers,
1309
+ listUsersByType,
1310
+ getOrCreateMindUser,
1311
+ deleteMindUser,
1312
+ changePassword,
1313
+ approveUser,
1314
+ countAdmins,
1315
+ setUserRole,
1316
+ deleteUser,
1317
+ updateUserProfile,
1318
+ syncMindProfile,
1319
+ migrateMindRoles,
1320
+ readSystemsConfig,
1321
+ writeSystemsConfig,
1322
+ deleteSystemsConfig,
1323
+ loadAllExtensions,
1324
+ getLoadedExtensions,
1325
+ getExtensionStandardSkills,
1326
+ notifyExtensionsDaemonStart,
1327
+ notifyExtensionsDaemonStop,
1328
+ notifyExtensionsMindStart,
1329
+ notifyExtensionsMindStop
1330
+ };