volute 0.30.1 → 0.31.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 (169) hide show
  1. package/dist/{accept-E3PAH3QJ.js → accept-GAKQ3MEH.js} +4 -4
  2. package/dist/{activity-events-BKBPPUBP.js → activity-events-T5ZRCVAL.js} +2 -2
  3. package/dist/{ai-service-VAJT5UBS.js → ai-service-UWUPM4T6.js} +5 -3
  4. package/dist/api.d.ts +238 -111
  5. package/dist/{archive-WWDBWYN2.js → archive-YBNSJYZZ.js} +2 -2
  6. package/dist/auth-T5AW2USD.js +43 -0
  7. package/dist/{bridge-RO37CUFM.js → bridge-4AJ3EY26.js} +4 -4
  8. package/dist/{chat-TCUNPFGO.js → chat-7YLT7FI3.js} +8 -8
  9. package/dist/{chunk-EFVHR7KH.js → chunk-4OUOFS23.js} +24 -5
  10. package/dist/{chunk-G3GBKZGG.js → chunk-57OKQMP3.js} +54 -2
  11. package/dist/chunk-6QIUN46C.js +38 -0
  12. package/dist/{chunk-FSM45XD5.js → chunk-AAO77TZX.js} +1 -1
  13. package/dist/{chunk-DTC6EH5I.js → chunk-BC3P3QCK.js} +1 -1
  14. package/dist/{chunk-P7VFDSSG.js → chunk-BNC43CSY.js} +2 -2
  15. package/dist/{chunk-QVAQ5454.js → chunk-BWKIHH7B.js} +3080 -2099
  16. package/dist/{chunk-IKHDUZRH.js → chunk-DAXJKPHZ.js} +2 -2
  17. package/dist/{chunk-P27RV5WM.js → chunk-EKDWA7E4.js} +3 -1
  18. package/dist/{chunk-S5LR3XYJ.js → chunk-EMPFLFTG.js} +1 -1
  19. package/dist/{chunk-2C2VXEBB.js → chunk-FAHDKPEH.js} +18 -56
  20. package/dist/{chunk-W5OOPLNP.js → chunk-HDKY4TWU.js} +3 -3
  21. package/dist/{chunk-EFP3PE6C.js → chunk-HR5JKIDG.js} +2 -12
  22. package/dist/{chunk-JGFRDMR6.js → chunk-LX6T3GKQ.js} +1 -1
  23. package/dist/{chunk-2NDZC3S7.js → chunk-NOWVQ7AL.js} +447 -299
  24. package/dist/{chunk-ZWKTUQEL.js → chunk-NV3TYNWX.js} +1 -1
  25. package/dist/{chunk-NSBFETWP.js → chunk-PNQCXLSV.js} +7 -26
  26. package/dist/chunk-R5QJBZZG.js +175 -0
  27. package/dist/{chunk-MDPCSXZ4.js → chunk-S2TZLSDH.js} +4 -4
  28. package/dist/{chunk-FXHXHI2A.js → chunk-SNVPRRT7.js} +3 -6
  29. package/dist/{chunk-UPA6COHU.js → chunk-WRS3B556.js} +5 -5
  30. package/dist/{chunk-HHTXM4JT.js → chunk-X62AXPR7.js} +36 -4
  31. package/dist/cli.js +74 -23
  32. package/dist/{clock-G3ALCMLJ.js → clock-LJCG426D.js} +10 -8
  33. package/dist/{cloud-sync-JV4LJOK3.js → cloud-sync-O3LXIRN6.js} +13 -13
  34. package/dist/{conversations-7KVQV7EZ.js → conversations-RKKGP5IA.js} +7 -7
  35. package/dist/{create-VQSQHJQW.js → create-TL623TFC.js} +1 -1
  36. package/dist/{create-JTLS7GX3.js → create-WUTIIRI2.js} +4 -4
  37. package/dist/daemon-client-CVGM25DM.js +11 -0
  38. package/dist/{daemon-restart-4JGBHEJ4.js → daemon-restart-EZP7XH3V.js} +6 -7
  39. package/dist/daemon.js +1879 -1727
  40. package/dist/{db-HMFPIRO2.js → db-SW5PL6QA.js} +1 -1
  41. package/dist/{delete-JESHKE7F.js → delete-Z6HAG35F.js} +1 -1
  42. package/dist/down-TS4XQBA4.js +13 -0
  43. package/dist/{env-CLXXT7M2.js → env-NHESNNSP.js} +4 -4
  44. package/dist/{export-EGA5M5PB.js → export-EVMP7GWY.js} +3 -3
  45. package/dist/{extension-WZ4SUPJB.js → extension-LR7EW3JF.js} +5 -6
  46. package/dist/{extensions-ECO4RPFQ.js → extensions-NGEJI7JH.js} +7 -7
  47. package/dist/{files-4VEJDASH.js → files-3SM7V33S.js} +5 -5
  48. package/dist/{history-EJMMLXDO.js → history-PQD3LXEP.js} +4 -4
  49. package/dist/{import-YCGPMBSI.js → import-PR2OCGQJ.js} +3 -3
  50. package/dist/{join-2GBJKZEN.js → join-R4EN5CWQ.js} +1 -1
  51. package/dist/{list-Q6O7FGAN.js → list-B4XNUOFO.js} +4 -4
  52. package/dist/{login-RL6AU2SM.js → login-62JVY6A2.js} +4 -4
  53. package/dist/{login-RET5WESK.js → login-URWP6S2N.js} +2 -2
  54. package/dist/{logout-CGAGJN3L.js → logout-NXJQJDLI.js} +2 -2
  55. package/dist/{logout-JRPBEMMR.js → logout-ZK2N62T3.js} +2 -2
  56. package/dist/message-delivery-UJHCLVU4.js +30 -0
  57. package/dist/{mind-LUWRQUQ5.js → mind-E2ZV2WRX.js} +17 -17
  58. package/dist/{mind-activity-tracker-VYN2ZZ2M.js → mind-activity-tracker-ASNZBMLC.js} +3 -3
  59. package/dist/{mind-list-V5WW5DUA.js → mind-list-BEI7E5WY.js} +2 -2
  60. package/dist/mind-manager-IPA6DZXD.js +26 -0
  61. package/dist/{mind-sleep-R6PTNNW4.js → mind-sleep-CANABWJI.js} +4 -4
  62. package/dist/{mind-status-I4ISFJ6I.js → mind-status-6WKZVUOP.js} +2 -2
  63. package/dist/{mind-wake-67ZQEWAV.js → mind-wake-RZKLH2IN.js} +4 -4
  64. package/dist/{package-OYUD4ZJ4.js → package-NU4CA7OU.js} +3 -3
  65. package/dist/{pages-watcher-Z3PKNROC.js → pages-watcher-72OVPRMH.js} +4 -3
  66. package/dist/read-THL362EI.js +74 -0
  67. package/dist/{register-NZDSTLP3.js → register-QAQELAS6.js} +4 -4
  68. package/dist/{registry-ODSALQQL.js → registry-ASXCQCNH.js} +1 -1
  69. package/dist/{reject-2HZOJEIJ.js → reject-AYPBNPNL.js} +4 -4
  70. package/dist/{restart-QHS3NT64.js → restart-6SKPV3T2.js} +4 -4
  71. package/dist/{sandbox-O5FUSF43.js → sandbox-6ZEWQDVU.js} +3 -3
  72. package/dist/{seed-WUQMPLDM.js → seed-OWX2AW75.js} +36 -12
  73. package/dist/{send-OAN3RYYY.js → send-ZO4BTWXK.js} +5 -5
  74. package/dist/{setup-QMDK5RZX.js → setup-7CFITEQN.js} +2 -4
  75. package/dist/{setup-XJH3E7YM.js → setup-ZXBXG7E4.js} +6 -8
  76. package/dist/{skill-FZIN4W4Q.js → skill-YFXP67A2.js} +4 -4
  77. package/dist/skills/dreaming/references/INSTALL.md +2 -3
  78. package/dist/skills/dreaming/scripts/dream.ts +3 -25
  79. package/dist/skills/volute-mind/SKILL.md +1 -1
  80. package/dist/sleep-manager-TPS6OGCA.js +30 -0
  81. package/dist/{split-EXYGGGQN.js → split-MI62KJUU.js} +1 -1
  82. package/dist/{sprout-AXQ6H5DB.js → sprout-FDVI2CGN.js} +5 -6
  83. package/dist/{start-MTOVL6SY.js → start-D64BRKPH.js} +4 -4
  84. package/dist/{status-ZRO37MWR.js → status-ZZWBYFGE.js} +4 -5
  85. package/dist/{stop-OK5WEPVC.js → stop-OP2CTXCO.js} +4 -4
  86. package/dist/system-chat-B43GIXQU.js +30 -0
  87. package/dist/{systems-W3BBMSOZ.js → systems-EQPPT4B7.js} +5 -5
  88. package/dist/{tailscale-BM72RXCJ.js → tailscale-6DJKUMNF.js} +1 -1
  89. package/dist/up-TDXEP3VA.js +16 -0
  90. package/dist/{update-PLPHMMZ2.js → update-KUJXATRS.js} +4 -5
  91. package/dist/{update-check-CVCN7MF6.js → update-check-5WVSU37T.js} +2 -2
  92. package/dist/{upgrade-I6NPCYUU.js → upgrade-KBHCWX6T.js} +1 -1
  93. package/dist/{version-notify-2NTWVEHL.js → version-notify-75ELVKPV.js} +17 -21
  94. package/dist/web-assets/assets/index-BM1cTzBg.js +72 -0
  95. package/dist/web-assets/assets/index-BfJkKTPF.css +1 -0
  96. package/dist/web-assets/ext-theme.css +1 -0
  97. package/dist/web-assets/index.html +2 -2
  98. package/drizzle/0000_baseline.sql +152 -0
  99. package/drizzle/0001_add_conversation_private.sql +1 -0
  100. package/drizzle/0002_turns.sql +21 -0
  101. package/drizzle/0003_turn_feed_links.sql +11 -0
  102. package/drizzle/meta/0000_snapshot.json +3 -223
  103. package/drizzle/meta/0001_snapshot.json +3 -294
  104. package/drizzle/meta/0002_snapshot.json +3 -335
  105. package/drizzle/meta/0003_snapshot.json +3 -413
  106. package/drizzle/meta/_journal.json +8 -106
  107. package/package.json +3 -3
  108. package/packages/extensions/notes/dist/ui/assets/{index-DgawVO5g.css → index-B8GdTnXs.css} +1 -1
  109. package/packages/extensions/notes/dist/ui/assets/index-CDpGTCWb.js +2 -0
  110. package/packages/extensions/notes/dist/ui/index.html +2 -2
  111. package/packages/extensions/notes/skills/notes/SKILL.md +8 -8
  112. package/packages/extensions/pages/skills/pages/SKILL.md +7 -4
  113. package/packages/extensions/pages/skills/pages/scripts/pages.mjs +58 -0
  114. package/templates/_base/.init/.config/bin/volute +27 -0
  115. package/templates/_base/src/lib/auto-commit.ts +82 -43
  116. package/templates/_base/src/lib/daemon-client.ts +19 -23
  117. package/templates/_base/src/lib/router.ts +17 -1
  118. package/templates/_base/src/lib/startup.ts +6 -8
  119. package/templates/_base/src/lib/volute-server.ts +2 -5
  120. package/templates/claude/src/agent.ts +2 -1
  121. package/templates/claude/src/lib/hooks/auto-commit.ts +7 -3
  122. package/templates/claude/src/server.ts +0 -9
  123. package/templates/pi/package.json.tmpl +1 -1
  124. package/templates/pi/src/agent.ts +1 -1
  125. package/templates/pi/src/lib/event-handler.ts +5 -3
  126. package/dist/chunk-7D47T4RB.js +0 -84
  127. package/dist/chunk-CVH6Y2YG.js +0 -59
  128. package/dist/chunk-LIRWLNAK.js +0 -729
  129. package/dist/daemon-client-BCTFGVCZ.js +0 -9
  130. package/dist/down-NGBMGORS.js +0 -14
  131. package/dist/message-delivery-6YMVNOEC.js +0 -28
  132. package/dist/migrate-registry-to-db-FK35IPEH.js +0 -110
  133. package/dist/mind-manager-YFCOIAAX.js +0 -18
  134. package/dist/read-WQMPTSN2.js +0 -46
  135. package/dist/sleep-manager-O7YQFCV5.js +0 -30
  136. package/dist/up-BXUAIDXB.js +0 -17
  137. package/dist/web-assets/assets/index--kREqKl9.js +0 -72
  138. package/dist/web-assets/assets/index-BXYTG0nJ.css +0 -1
  139. package/drizzle/0000_flaky_mariko_yashida.sql +0 -34
  140. package/drizzle/0001_careless_warpath.sql +0 -12
  141. package/drizzle/0002_wealthy_the_call.sql +0 -6
  142. package/drizzle/0003_clean_ego.sql +0 -12
  143. package/drizzle/0004_magical_silverclaw.sql +0 -1
  144. package/drizzle/0005_rename_agents_to_minds.sql +0 -11
  145. package/drizzle/0006_mind_history.sql +0 -20
  146. package/drizzle/0007_system_prompts.sql +0 -5
  147. package/drizzle/0008_volute_channels.sql +0 -24
  148. package/drizzle/0009_shared_skills.sql +0 -9
  149. package/drizzle/0010_delivery_queue.sql +0 -12
  150. package/drizzle/0011_rename_human_to_brain.sql +0 -1
  151. package/drizzle/0012_activity.sql +0 -11
  152. package/drizzle/0013_user_profiles.sql +0 -3
  153. package/drizzle/0014_conversation_reads.sql +0 -7
  154. package/drizzle/0015_notes.sql +0 -23
  155. package/drizzle/0016_note_reactions_and_replies.sql +0 -15
  156. package/drizzle/0017_minds.sql +0 -16
  157. package/drizzle/meta/0004_snapshot.json +0 -410
  158. package/drizzle/meta/0005_snapshot.json +0 -410
  159. package/drizzle/meta/0006_snapshot.json +0 -7
  160. package/drizzle/meta/0007_snapshot.json +0 -7
  161. package/drizzle/meta/0008_snapshot.json +0 -7
  162. package/drizzle/meta/0009_snapshot.json +0 -7
  163. package/drizzle/meta/0010_snapshot.json +0 -7
  164. package/drizzle/meta/0011_snapshot.json +0 -7
  165. package/drizzle/meta/0012_snapshot.json +0 -7
  166. package/drizzle/meta/0013_snapshot.json +0 -7
  167. package/packages/extensions/notes/dist/ui/assets/index-qUWoeC4c.js +0 -2
  168. package/packages/extensions/notes/skills/notes/scripts/notes.mjs +0 -185
  169. package/templates/_base/home/public/.gitkeep +0 -0
@@ -1,24 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- broadcast,
3
+ getUser,
4
+ getUserByUsername
5
+ } from "./chunk-R5QJBZZG.js";
6
+ import {
4
7
  publish
5
- } from "./chunk-P27RV5WM.js";
8
+ } from "./chunk-EKDWA7E4.js";
6
9
  import {
7
10
  hashSkillDir,
8
11
  importSkillFromDir,
9
12
  sharedSkillsDir
10
- } from "./chunk-MDPCSXZ4.js";
13
+ } from "./chunk-S2TZLSDH.js";
11
14
  import {
12
15
  logger_default
13
16
  } from "./chunk-YUIHSKR6.js";
14
17
  import {
15
18
  getDb,
16
19
  mindDir,
17
- users,
20
+ turns,
18
21
  voluteHome,
19
- voluteSystemDir,
20
- voluteUserHome
21
- } from "./chunk-HHTXM4JT.js";
22
+ voluteSystemDir
23
+ } from "./chunk-X62AXPR7.js";
22
24
 
23
25
  // src/lib/extensions.ts
24
26
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
@@ -40,47 +42,6 @@ function createExtension(manifest) {
40
42
  return manifest;
41
43
  }
42
44
 
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
45
  // packages/extensions/notes/src/notes.ts
85
46
  function slugify(text) {
86
47
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -319,7 +280,199 @@ async function resolveNoteId(db, getUserByUsername2, authorSlug) {
319
280
  return row?.id ?? null;
320
281
  }
321
282
 
283
+ // packages/extensions/notes/src/commands.ts
284
+ function getFlag(args, flag) {
285
+ const idx = args.indexOf(flag);
286
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
287
+ return void 0;
288
+ }
289
+ function createCommands() {
290
+ return {
291
+ write: {
292
+ description: "Write a new note",
293
+ usage: 'volute notes write "title" "content" [--reply-to author/slug]',
294
+ handler: async (args, ctx) => {
295
+ if (!ctx.db) return { error: "Notes extension requires a database" };
296
+ const mindName = ctx.mindName;
297
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
298
+ const user = await ctx.getUserByUsername(mindName);
299
+ if (!user) return { error: `Unknown mind: ${mindName}` };
300
+ const title = args[0];
301
+ const content = args[1];
302
+ if (!title || !content)
303
+ return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
304
+ let replyToId;
305
+ const replyTo = getFlag(args, "--reply-to");
306
+ if (replyTo) {
307
+ const id = await resolveNoteId(ctx.db, ctx.getUserByUsername, replyTo);
308
+ if (id === null) return { error: `Reply target not found: ${replyTo}` };
309
+ replyToId = id;
310
+ }
311
+ const note = await createNote(ctx.db, ctx.getUser, user.id, title, content, replyToId);
312
+ ctx.publishActivity({
313
+ type: "note_created",
314
+ mind: user.username,
315
+ summary: `${user.username} wrote "${title}"`,
316
+ metadata: { author: user.username, slug: note.slug, bodyHtml: content.slice(0, 500) }
317
+ });
318
+ return { output: `Published: ${note.author_username}/${note.slug}` };
319
+ }
320
+ },
321
+ list: {
322
+ description: "List notes",
323
+ usage: "volute notes list [--author name] [--limit N]",
324
+ handler: async (args, ctx) => {
325
+ if (!ctx.db) return { error: "Notes extension requires a database" };
326
+ const author = getFlag(args, "--author");
327
+ const limit = parseInt(getFlag(args, "--limit") ?? "10", 10);
328
+ const notes = await listNotes(ctx.db, ctx.getUser, ctx.getUserByUsername, {
329
+ authorUsername: author,
330
+ limit
331
+ });
332
+ if (notes.length === 0) return { output: "No notes found." };
333
+ const lines = notes.map((n) => {
334
+ const date = new Date(n.created_at).toLocaleDateString();
335
+ return ` ${n.author_username}/${n.slug} "${n.title}" (${date})`;
336
+ });
337
+ return { output: lines.join("\n") };
338
+ }
339
+ },
340
+ read: {
341
+ description: "Read a note",
342
+ usage: "volute notes read <author/slug>",
343
+ handler: async (args, ctx) => {
344
+ if (!ctx.db) return { error: "Notes extension requires a database" };
345
+ const ref = args[0];
346
+ if (!ref || !ref.includes("/")) return { error: "Usage: volute notes read <author/slug>" };
347
+ const [author, slug] = ref.split("/", 2);
348
+ const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
349
+ if (!note) return { error: "Note not found" };
350
+ const lines = [
351
+ `# ${note.title}
352
+ `,
353
+ `By ${note.author_username} \u2014 ${new Date(note.created_at).toLocaleString()}
354
+ `,
355
+ note.content
356
+ ];
357
+ if (note.reactions?.length) {
358
+ lines.push(
359
+ `
360
+ Reactions: ${note.reactions.map((r) => `${r.emoji} (${r.count})`).join(" ")}`
361
+ );
362
+ }
363
+ if (note.comments?.length) {
364
+ lines.push(`
365
+ Comments (${note.comments.length}):`);
366
+ for (const c of note.comments) {
367
+ lines.push(` ${c.author_username}: ${c.content}`);
368
+ }
369
+ }
370
+ return { output: lines.join("\n") };
371
+ }
372
+ },
373
+ comment: {
374
+ description: "Comment on a note",
375
+ usage: 'volute notes comment <author/slug> "content"',
376
+ handler: async (args, ctx) => {
377
+ if (!ctx.db) return { error: "Notes extension requires a database" };
378
+ const mindName = ctx.mindName;
379
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
380
+ const user = await ctx.getUserByUsername(mindName);
381
+ if (!user) return { error: `Unknown mind: ${mindName}` };
382
+ const ref = args[0];
383
+ const content = args[1];
384
+ if (!ref || !ref.includes("/") || !content) {
385
+ return { error: 'Usage: volute notes comment <author/slug> "content"' };
386
+ }
387
+ const [author, slug] = ref.split("/", 2);
388
+ const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
389
+ if (!note) return { error: "Note not found" };
390
+ await addComment(ctx.db, ctx.getUser, note.id, user.id, content);
391
+ return { output: "Comment added." };
392
+ }
393
+ },
394
+ react: {
395
+ description: "React to a note",
396
+ usage: 'volute notes react <author/slug> "emoji"',
397
+ handler: async (args, ctx) => {
398
+ if (!ctx.db) return { error: "Notes extension requires a database" };
399
+ const mindName = ctx.mindName;
400
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
401
+ const user = await ctx.getUserByUsername(mindName);
402
+ if (!user) return { error: `Unknown mind: ${mindName}` };
403
+ const ref = args[0];
404
+ const emoji = args[1];
405
+ if (!ref || !ref.includes("/") || !emoji) {
406
+ return { error: 'Usage: volute notes react <author/slug> "emoji"' };
407
+ }
408
+ const [author, slug] = ref.split("/", 2);
409
+ const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
410
+ if (!note) return { error: "Note not found" };
411
+ const result = toggleReaction(ctx.db, note.id, user.id, emoji);
412
+ return { output: result.added ? "Reaction added." : "Reaction removed." };
413
+ }
414
+ },
415
+ delete: {
416
+ description: "Delete your own note",
417
+ usage: "volute notes delete <author/slug>",
418
+ handler: async (args, ctx) => {
419
+ if (!ctx.db) return { error: "Notes extension requires a database" };
420
+ const mindName = ctx.mindName;
421
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
422
+ const user = await ctx.getUserByUsername(mindName);
423
+ if (!user) return { error: `Unknown mind: ${mindName}` };
424
+ const ref = args[0];
425
+ if (!ref || !ref.includes("/"))
426
+ return { error: "Usage: volute notes delete <author/slug>" };
427
+ const [author, slug] = ref.split("/", 2);
428
+ const deleted = await deleteNote(ctx.db, ctx.getUserByUsername, author, slug, user.id);
429
+ if (!deleted) return { error: "Note not found or not authorized" };
430
+ return { output: "Note deleted." };
431
+ }
432
+ }
433
+ };
434
+ }
435
+
436
+ // packages/extensions/notes/src/db.ts
437
+ function initDb(db) {
438
+ db.exec(`
439
+ CREATE TABLE IF NOT EXISTS notes (
440
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
441
+ author_id INTEGER NOT NULL,
442
+ title TEXT NOT NULL,
443
+ slug TEXT NOT NULL,
444
+ content TEXT NOT NULL,
445
+ reply_to_id INTEGER,
446
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
447
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
448
+ );
449
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
450
+ CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
451
+ CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
452
+
453
+ CREATE TABLE IF NOT EXISTS note_comments (
454
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
455
+ note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
456
+ author_id INTEGER NOT NULL,
457
+ content TEXT NOT NULL,
458
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
459
+ );
460
+ CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
461
+
462
+ CREATE TABLE IF NOT EXISTS note_reactions (
463
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
464
+ note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
465
+ user_id INTEGER NOT NULL,
466
+ emoji TEXT NOT NULL,
467
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
468
+ );
469
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
470
+ CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
471
+ `);
472
+ }
473
+
322
474
  // packages/extensions/notes/src/routes.ts
475
+ import { Hono } from "hono";
323
476
  async function parseJson(c) {
324
477
  try {
325
478
  return await c.req.json();
@@ -366,12 +519,19 @@ function createRoutes(ctx) {
366
519
  replyToId = id;
367
520
  }
368
521
  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
- });
522
+ ctx.publishActivity(
523
+ {
524
+ type: "note_created",
525
+ mind: actor.username,
526
+ summary: `${actor.username} wrote "${body.title}"`,
527
+ metadata: {
528
+ author: actor.username,
529
+ slug: note.slug,
530
+ bodyHtml: body.content.slice(0, 500)
531
+ }
532
+ },
533
+ c
534
+ );
375
535
  return c.json(note, 201);
376
536
  }).get("/:author/:slug", async (c) => {
377
537
  const { author, slug } = c.req.param();
@@ -460,13 +620,20 @@ var src_default = createExtension({
460
620
  version: "0.1.0",
461
621
  description: "Public notes for sharing thoughts, reflections, and ideas",
462
622
  routes: (ctx) => createRoutes(ctx),
623
+ commands: createCommands(),
463
624
  initDb,
464
625
  skillsDir,
465
626
  standardSkill: true,
466
627
  ui: {
467
628
  assetsDir,
468
629
  systemSection: { id: "notes", label: "Notes", urlPatterns: ["/notes", "/notes/:author/:slug"] },
469
- mindSections: [{ id: "notes", label: "Notes" }],
630
+ mindSections: [
631
+ {
632
+ id: "notes",
633
+ label: "Notes",
634
+ 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>'
635
+ }
636
+ ],
470
637
  feedSource: {
471
638
  endpoint: "/api/ext/notes/feed"
472
639
  }
@@ -499,7 +666,7 @@ var MIME_TYPES = {
499
666
  var _pagesWatcher = null;
500
667
  async function getPagesWatcher() {
501
668
  if (_pagesWatcher) return _pagesWatcher;
502
- const mod = await import("./pages-watcher-Z3PKNROC.js");
669
+ const mod = await import("./pages-watcher-72OVPRMH.js");
503
670
  _pagesWatcher = mod;
504
671
  return _pagesWatcher;
505
672
  }
@@ -553,6 +720,31 @@ function createRoutes2(ctx) {
553
720
  } catch (err) {
554
721
  return c.json({ error: `Connection failed: ${err.message}` }, 502);
555
722
  }
723
+ }).post("/notify", async (c) => {
724
+ const user = ctx.resolveUser(c);
725
+ if (!user) return c.json({ error: "Unauthorized" }, 401);
726
+ let body;
727
+ try {
728
+ body = await c.req.json();
729
+ } catch {
730
+ body = {};
731
+ }
732
+ const file = body.file ?? "page";
733
+ ctx.publishActivity(
734
+ {
735
+ type: "page_updated",
736
+ mind: user.username,
737
+ summary: `${user.username} updated ${file}`,
738
+ metadata: { file, iframeUrl: `/ext/pages/public/${user.username}/${file}` }
739
+ },
740
+ c
741
+ );
742
+ try {
743
+ const mod = await import("./pages-watcher-72OVPRMH.js");
744
+ mod.invalidateCache();
745
+ } catch {
746
+ }
747
+ return c.json({ ok: true });
556
748
  }).get("/status/:name", async (c) => {
557
749
  const user = ctx.resolveUser(c);
558
750
  if (!user) return c.json({ error: "Unauthorized" }, 401);
@@ -576,7 +768,7 @@ function createRoutes2(ctx) {
576
768
  var _voluteHome = null;
577
769
  async function getVoluteHome() {
578
770
  if (_voluteHome) return _voluteHome();
579
- const mod = await import("./registry-ODSALQQL.js");
771
+ const mod = await import("./registry-ASXCQCNH.js");
580
772
  _voluteHome = mod.voluteHome;
581
773
  return _voluteHome();
582
774
  }
@@ -624,9 +816,26 @@ var skillsDir2 = resolve3(import.meta.dirname, "../skills");
624
816
  var _watcher = null;
625
817
  async function getWatcher() {
626
818
  if (_watcher) return _watcher;
627
- _watcher = await import("./pages-watcher-Z3PKNROC.js");
819
+ _watcher = await import("./pages-watcher-72OVPRMH.js");
628
820
  return _watcher;
629
821
  }
822
+ var notifyHandler = async (args, ctx) => {
823
+ const mindName = ctx.mindName;
824
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
825
+ const file = args[0] || "page";
826
+ ctx.publishActivity({
827
+ type: "page_updated",
828
+ mind: mindName,
829
+ summary: `${mindName} updated ${file}`,
830
+ metadata: { file, iframeUrl: `/ext/pages/public/${mindName}/${file}` }
831
+ });
832
+ try {
833
+ const mod = await import("./pages-watcher-72OVPRMH.js");
834
+ mod.invalidateCache();
835
+ } catch {
836
+ }
837
+ return { output: `Notified: ${file}` };
838
+ };
630
839
  var src_default2 = createExtension({
631
840
  id: "pages",
632
841
  name: "Pages",
@@ -634,6 +843,13 @@ var src_default2 = createExtension({
634
843
  description: "Publish and serve web pages from mind directories",
635
844
  routes: (ctx) => createRoutes2(ctx),
636
845
  publicRoutes: (ctx) => createPublicRoutes(ctx),
846
+ commands: {
847
+ notify: {
848
+ description: "Notify that a page was created or updated",
849
+ usage: "volute pages notify [filename]",
850
+ handler: notifyHandler
851
+ }
852
+ },
637
853
  skillsDir: skillsDir2,
638
854
  standardSkill: true,
639
855
  ui: {
@@ -643,7 +859,13 @@ var src_default2 = createExtension({
643
859
  label: "Pages",
644
860
  urlPatterns: ["/pages", "/pages/:site", "/pages/:site/:path"]
645
861
  },
646
- mindSections: [{ id: "pages", label: "Pages" }],
862
+ mindSections: [
863
+ {
864
+ id: "pages",
865
+ label: "Pages",
866
+ 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>'
867
+ }
868
+ ],
647
869
  feedSource: {
648
870
  endpoint: "/api/ext/pages/feed"
649
871
  }
@@ -668,170 +890,110 @@ var src_default2 = createExtension({
668
890
  }
669
891
  });
670
892
 
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;
893
+ // src/lib/daemon/turn-tracker.ts
894
+ import { randomUUID } from "crypto";
895
+ import { eq } from "drizzle-orm";
896
+ var tlog = logger_default.child("turn-tracker");
897
+ var activeTurns = /* @__PURE__ */ new Map();
898
+ function key(mind, session) {
899
+ return `${mind}:${session ?? "*"}`;
900
+ }
901
+ async function createTurn(mind) {
902
+ const k = key(mind);
903
+ const existing = activeTurns.get(k);
904
+ if (existing) return existing.turnId;
905
+ const turnId = randomUUID();
727
906
  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;
907
+ const db = await getDb();
908
+ await db.insert(turns).values({ id: turnId, mind, status: "active" });
735
909
  } 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;
910
+ tlog.error(`failed to create turn for ${mind}`, logger_default.errorData(err));
911
+ return void 0;
741
912
  }
913
+ activeTurns.set(k, { turnId, lastToolUseEventId: void 0 });
914
+ return turnId;
742
915
  }
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;
916
+ function getActiveTurnId(mind, session) {
917
+ return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
764
918
  }
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));
919
+ function trackToolUse(mind, session, eventId) {
920
+ const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
921
+ if (entry) entry.lastToolUseEventId = eventId;
770
922
  }
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")));
923
+ function getLastToolUseEventId(mind, session) {
924
+ return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
776
925
  }
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));
926
+ async function assignSession(mind, turnId, session) {
927
+ const wildcardKey = key(mind);
928
+ const entry = activeTurns.get(wildcardKey);
929
+ if (!entry || entry.turnId !== turnId) {
930
+ tlog.warn(`assignSession: no matching turn for ${mind} (turnId=${turnId}, session=${session})`);
931
+ return;
932
+ }
933
+ try {
934
+ const db = await getDb();
935
+ await db.update(turns).set({ session }).where(eq(turns.id, turnId));
936
+ } catch (err) {
937
+ tlog.error(`failed to assign session to turn ${turnId}`, logger_default.errorData(err));
938
+ return;
939
+ }
940
+ activeTurns.delete(wildcardKey);
941
+ activeTurns.set(key(mind, session), entry);
942
+ }
943
+ async function completeTurn(mind, session) {
944
+ const k = key(mind, session);
945
+ const wildcardKey = key(mind);
946
+ const entry = activeTurns.get(k) ?? activeTurns.get(wildcardKey);
947
+ if (!entry) return void 0;
948
+ try {
949
+ const db = await getDb();
950
+ await db.update(turns).set({ status: "complete" }).where(eq(turns.id, entry.turnId));
951
+ } catch (err) {
952
+ tlog.error(`failed to complete turn ${entry.turnId}`, logger_default.errorData(err));
953
+ return void 0;
954
+ }
955
+ activeTurns.delete(k);
956
+ activeTurns.delete(wildcardKey);
957
+ return entry.turnId;
782
958
  }
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` });
959
+ async function setSummaryEventId(turnId, summaryEventId) {
960
+ try {
961
+ const db = await getDb();
962
+ await db.update(turns).set({ summary_event_id: summaryEventId }).where(eq(turns.id, turnId));
963
+ } catch (err) {
964
+ tlog.error(`failed to set summary event for turn ${turnId}`, logger_default.errorData(err));
965
+ }
795
966
  }
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"])));
967
+ async function clearMind(mind) {
968
+ const toDelete = [];
969
+ const turnIds = [];
970
+ for (const [k, entry] of activeTurns.entries()) {
971
+ if (k.startsWith(`${mind}:`)) {
972
+ turnIds.push(entry.turnId);
973
+ toDelete.push(k);
974
+ }
975
+ }
976
+ for (const k of toDelete) activeTurns.delete(k);
977
+ if (turnIds.length > 0) {
978
+ try {
979
+ const db = await getDb();
980
+ for (const id of turnIds) {
981
+ await db.update(turns).set({ status: "complete" }).where(eq(turns.id, id));
982
+ }
983
+ } catch (err) {
984
+ tlog.error(`failed to complete orphaned turns for ${mind}`, logger_default.errorData(err));
985
+ }
986
+ }
799
987
  }
800
988
 
801
989
  // src/lib/systems-config.ts
802
- import {
803
- existsSync,
804
- mkdirSync,
805
- readFileSync,
806
- renameSync,
807
- unlinkSync,
808
- writeFileSync
809
- } from "fs";
990
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
810
991
  import { resolve as resolve4 } from "path";
811
992
  var DEFAULT_API_URL = "https://volute.systems";
812
993
  function configPath() {
813
994
  return resolve4(voluteSystemDir(), "systems.json");
814
995
  }
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
996
  function readSystemsConfig() {
834
- migrateIfNeeded();
835
997
  const path = configPath();
836
998
  if (!existsSync(path)) return null;
837
999
  const raw = readFileSync(path, "utf-8");
@@ -904,69 +1066,6 @@ async function openExtensionDb(_id, dataDir) {
904
1066
  const Database = await getLibsqlDatabase();
905
1067
  return new Database(dbPath);
906
1068
  }
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
1069
  async function buildContext(manifest, dataDir, authMw) {
971
1070
  let db = null;
972
1071
  if (manifest.initDb) {
@@ -977,9 +1076,6 @@ async function buildContext(manifest, dataDir, authMw) {
977
1076
  realDb.close();
978
1077
  throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
979
1078
  }
980
- if (manifest.id === "notes") {
981
- await migrateNotesFromCoreDb(realDb);
982
- }
983
1079
  db = realDb;
984
1080
  }
985
1081
  return {
@@ -992,8 +1088,15 @@ async function buildContext(manifest, dataDir, authMw) {
992
1088
  },
993
1089
  getUser: async (id) => getUser(id),
994
1090
  getUserByUsername: async (username) => getUserByUsername(username),
995
- publishActivity: (event) => {
996
- publish(event).catch(
1091
+ publishActivity: (event, sessionOrContext) => {
1092
+ const session = typeof sessionOrContext === "string" ? sessionOrContext : sessionOrContext?.get("mindSession");
1093
+ const turnId = getActiveTurnId(event.mind, session);
1094
+ const sourceEventId = getLastToolUseEventId(event.mind, session);
1095
+ publish({
1096
+ ...event,
1097
+ turn_id: turnId,
1098
+ source_event_id: sourceEventId
1099
+ }).catch(
997
1100
  (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
998
1101
  );
999
1102
  },
@@ -1030,6 +1133,35 @@ async function loadExtension(manifest, app, authMw) {
1030
1133
  const publicApp = manifest.publicRoutes(context);
1031
1134
  app.route(`/ext/${manifest.id}/public`, publicApp);
1032
1135
  }
1136
+ if (manifest.commands) {
1137
+ for (const [cmdName, cmd] of Object.entries(manifest.commands)) {
1138
+ app.post(`${extApiPath}/commands/${cmdName}`, async (c) => {
1139
+ let body;
1140
+ try {
1141
+ body = await c.req.json();
1142
+ } catch {
1143
+ return c.json({ error: "Invalid JSON in request body" }, 400);
1144
+ }
1145
+ const user = c.get("user");
1146
+ const mindName = body.mind || user?.username;
1147
+ const session = c.get("mindSession");
1148
+ try {
1149
+ const result = await cmd.handler(body.args ?? [], {
1150
+ ...context,
1151
+ // Bind publishActivity to the session so command handlers
1152
+ // don't need to pass it explicitly
1153
+ publishActivity: (event, sc) => context.publishActivity(event, sc ?? session),
1154
+ mindName,
1155
+ session
1156
+ });
1157
+ return c.json(result);
1158
+ } catch (err) {
1159
+ logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
1160
+ return c.json({ error: err.message }, 500);
1161
+ }
1162
+ });
1163
+ }
1164
+ }
1033
1165
  let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
1034
1166
  if (resolvedAssetsDir && !existsSync2(resolvedAssetsDir)) {
1035
1167
  let searchDir = dirname(new URL(import.meta.url).pathname);
@@ -1228,17 +1360,42 @@ async function loadAllExtensions(app, authMw) {
1228
1360
  logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
1229
1361
  }
1230
1362
  }
1363
+ app.get("/api/extensions/commands", (c) => {
1364
+ const result = {};
1365
+ for (const { manifest } of loaded) {
1366
+ if (!manifest.commands) continue;
1367
+ const cmds = {};
1368
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
1369
+ cmds[name] = { description: cmd.description, ...cmd.usage ? { usage: cmd.usage } : {} };
1370
+ }
1371
+ result[manifest.id] = { commands: cmds };
1372
+ }
1373
+ return c.json(result);
1374
+ });
1231
1375
  }
1232
1376
  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
- }));
1377
+ return loaded.map(({ manifest }) => {
1378
+ let commands;
1379
+ if (manifest.commands) {
1380
+ commands = {};
1381
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
1382
+ commands[name] = {
1383
+ description: cmd.description,
1384
+ ...cmd.usage ? { usage: cmd.usage } : {}
1385
+ };
1386
+ }
1387
+ }
1388
+ return {
1389
+ id: manifest.id,
1390
+ name: manifest.name,
1391
+ version: manifest.version,
1392
+ description: manifest.description,
1393
+ systemSection: manifest.ui?.systemSection,
1394
+ mindSections: manifest.ui?.mindSections,
1395
+ feedSource: manifest.ui?.feedSource,
1396
+ commands
1397
+ };
1398
+ });
1242
1399
  }
1243
1400
  function getExtensionStandardSkills() {
1244
1401
  const skills = [];
@@ -1300,23 +1457,14 @@ function notifyExtensionsMindStop(mindName) {
1300
1457
  }
1301
1458
 
1302
1459
  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,
1460
+ createTurn,
1461
+ getActiveTurnId,
1462
+ trackToolUse,
1463
+ getLastToolUseEventId,
1464
+ assignSession,
1465
+ completeTurn,
1466
+ setSummaryEventId,
1467
+ clearMind,
1320
1468
  readSystemsConfig,
1321
1469
  writeSystemsConfig,
1322
1470
  deleteSystemsConfig,