volute 0.30.1 → 0.32.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 (227) hide show
  1. package/README.md +15 -22
  2. package/dist/{accept-E3PAH3QJ.js → accept-74M7I4RZ.js} +5 -4
  3. package/dist/{activity-events-BKBPPUBP.js → activity-events-HETAODOK.js} +3 -2
  4. package/dist/{ai-service-VAJT5UBS.js → ai-service-ZIPCV3MX.js} +20 -5
  5. package/dist/api.d.ts +341 -397
  6. package/dist/{archive-WWDBWYN2.js → archive-INXYFVCW.js} +3 -2
  7. package/dist/auth-6DMGES3I.js +44 -0
  8. package/dist/{bridge-RO37CUFM.js → bridge-BVCBTGPF.js} +5 -4
  9. package/dist/{chat-TCUNPFGO.js → chat-XT4OBJBU.js} +8 -8
  10. package/dist/{chunk-P7VFDSSG.js → chunk-2FLJ63GU.js} +2 -2
  11. package/dist/{chunk-ZWKTUQEL.js → chunk-2NGTS5UU.js} +1 -1
  12. package/dist/{chunk-JGFRDMR6.js → chunk-ALEF47VT.js} +1 -1
  13. package/dist/{chunk-MDPCSXZ4.js → chunk-D5G5YOPL.js} +163 -15
  14. package/dist/{chunk-VGWJSNHS.js → chunk-G53F3JA4.js} +1 -35
  15. package/dist/{chunk-A6TUJJ3L.js → chunk-G6BSYHPK.js} +2 -2
  16. package/dist/{chunk-DTC6EH5I.js → chunk-I5KY25PQ.js} +1 -9
  17. package/dist/{chunk-NSBFETWP.js → chunk-IYDIE3HG.js} +64 -26
  18. package/dist/{chunk-W5OOPLNP.js → chunk-JJ7W6WSB.js} +3 -3
  19. package/dist/{chunk-G3GBKZGG.js → chunk-LGB6JBHI.js} +54 -2
  20. package/dist/chunk-LRCG2JLP.js +251 -0
  21. package/dist/{chunk-FXHXHI2A.js → chunk-LSGWR54X.js} +3 -6
  22. package/dist/{chunk-S5LR3XYJ.js → chunk-M7UL5S3Q.js} +1 -1
  23. package/dist/chunk-PB65JZK2.js +85 -0
  24. package/dist/chunk-PVY5W6QN.js +41 -0
  25. package/dist/{chunk-QVAQ5454.js → chunk-QBQ424EM.js} +3007 -2126
  26. package/dist/{chunk-P27RV5WM.js → chunk-QZANELPX.js} +6 -2
  27. package/dist/{chunk-FSM45XD5.js → chunk-R7E6CRVQ.js} +1 -1
  28. package/dist/{chunk-HHTXM4JT.js → chunk-RPZZSXV3.js} +39 -195
  29. package/dist/{chunk-UPA6COHU.js → chunk-RSX4OPZY.js} +5 -5
  30. package/dist/{chunk-2C2VXEBB.js → chunk-S6NFERDC.js} +21 -57
  31. package/dist/chunk-SKLSMHXO.js +208 -0
  32. package/dist/{chunk-IKHDUZRH.js → chunk-SX5TKJBZ.js} +2 -2
  33. package/dist/chunk-TDRYEPH4.js +185 -0
  34. package/dist/chunk-TSXLLQZW.js +46 -0
  35. package/dist/{chunk-EFVHR7KH.js → chunk-UKVWJRKN.js} +24 -5
  36. package/dist/{chunk-2NDZC3S7.js → chunk-WKF5FEFK.js} +688 -389
  37. package/dist/cli.js +93 -24
  38. package/dist/{clock-G3ALCMLJ.js → clock-2UOZ6JPU.js} +11 -8
  39. package/dist/{cloud-sync-JV4LJOK3.js → cloud-sync-JN3NWKEM.js} +16 -14
  40. package/dist/config-H2H4UIF7.js +72 -0
  41. package/dist/connectors/discord-bridge.js +1 -1
  42. package/dist/connectors/slack-bridge.js +1 -1
  43. package/dist/connectors/telegram-bridge.js +1 -1
  44. package/dist/{conversations-7KVQV7EZ.js → conversations-3O5O6AS3.js} +8 -7
  45. package/dist/{create-JTLS7GX3.js → create-RNLNCORE.js} +5 -4
  46. package/dist/{create-VQSQHJQW.js → create-WBBYI6V7.js} +6 -2
  47. package/dist/daemon-client-6QXHZ7US.js +12 -0
  48. package/dist/{daemon-restart-4JGBHEJ4.js → daemon-restart-NGFHFAUF.js} +7 -7
  49. package/dist/daemon.js +2446 -1999
  50. package/dist/{db-HMFPIRO2.js → db-F34YLV7D.js} +2 -1
  51. package/dist/db-RA45JBFG.js +16 -0
  52. package/dist/{delete-JESHKE7F.js → delete-QTGWEDBI.js} +1 -1
  53. package/dist/delivery-manager-SDVXFD4W.js +28 -0
  54. package/dist/delivery-router-FL45JL7N.js +21 -0
  55. package/dist/down-TB3ESMNP.js +14 -0
  56. package/dist/{env-CLXXT7M2.js → env-RLYQBOOP.js} +5 -4
  57. package/dist/{export-EGA5M5PB.js → export-SUYRLI5Q.js} +4 -3
  58. package/dist/{extension-WZ4SUPJB.js → extension-FQ5D3NCC.js} +6 -6
  59. package/dist/{extensions-ECO4RPFQ.js → extensions-GDYWQXC4.js} +9 -7
  60. package/dist/{files-4VEJDASH.js → files-EAMPO2SJ.js} +6 -5
  61. package/dist/{history-EJMMLXDO.js → history-FO5PHBQ5.js} +9 -4
  62. package/dist/{import-YCGPMBSI.js → import-DDUFE7AY.js} +4 -3
  63. package/dist/{join-2GBJKZEN.js → join-I5QEE3LG.js} +1 -1
  64. package/dist/{list-Q6O7FGAN.js → list-DW2VRTOZ.js} +5 -4
  65. package/dist/{login-RL6AU2SM.js → login-7CHPW2PN.js} +5 -4
  66. package/dist/{login-RET5WESK.js → login-RIJF2F4G.js} +3 -2
  67. package/dist/{logout-CGAGJN3L.js → logout-5MLHZALK.js} +3 -2
  68. package/dist/{logout-JRPBEMMR.js → logout-UZJRGY4Z.js} +3 -2
  69. package/dist/message-delivery-2FIM7QKO.js +32 -0
  70. package/dist/{mind-LUWRQUQ5.js → mind-2B6M7Y25.js} +18 -18
  71. package/dist/{mind-activity-tracker-VYN2ZZ2M.js → mind-activity-tracker-NZZT2NTT.js} +4 -3
  72. package/dist/{mind-list-V5WW5DUA.js → mind-list-WUPMQDYQ.js} +3 -2
  73. package/dist/mind-manager-BNCMGYXW.js +28 -0
  74. package/dist/mind-service-AV273WT4.js +34 -0
  75. package/dist/{mind-sleep-R6PTNNW4.js → mind-sleep-B7BHJLH7.js} +5 -4
  76. package/dist/{mind-status-I4ISFJ6I.js → mind-status-L3EFFRPR.js} +3 -2
  77. package/dist/{mind-wake-67ZQEWAV.js → mind-wake-GY3RFX7Y.js} +5 -4
  78. package/dist/{package-OYUD4ZJ4.js → package-PK6JUFL3.js} +3 -3
  79. package/dist/read-5AMJRO3D.js +75 -0
  80. package/dist/{register-NZDSTLP3.js → register-V2JZZKFK.js} +5 -4
  81. package/dist/{registry-ODSALQQL.js → registry-PJ4S5PHQ.js} +8 -1
  82. package/dist/{reject-2HZOJEIJ.js → reject-33HEZMZ4.js} +5 -4
  83. package/dist/{restart-QHS3NT64.js → restart-3UCMRUVC.js} +5 -4
  84. package/dist/{sandbox-O5FUSF43.js → sandbox-JANNTX6U.js} +4 -3
  85. package/dist/schema-PA3M5ZKH.js +32 -0
  86. package/dist/seed-ALUQ55FF.js +112 -0
  87. package/dist/{send-OAN3RYYY.js → send-3MI36LEF.js} +58 -69
  88. package/dist/{setup-QMDK5RZX.js → setup-SZIARWI6.js} +5 -4
  89. package/dist/{setup-XJH3E7YM.js → setup-WENLVPVP.js} +9 -9
  90. package/dist/{skill-FZIN4W4Q.js → skill-TUVOTW4Z.js} +5 -4
  91. package/dist/skills/dreaming/SKILL.md +6 -4
  92. package/dist/skills/dreaming/references/INSTALL.md +4 -5
  93. package/dist/skills/dreaming/scripts/dream.ts +5 -27
  94. package/dist/skills/dreaming/scripts/wake-context-dreams.sh +1 -1
  95. package/dist/skills/imagegen/SKILL.md +6 -5
  96. package/dist/skills/imagegen/references/INSTALL.md +1 -1
  97. package/dist/skills/resonance/SKILL.md +4 -1
  98. package/dist/skills/resonance/references/INSTALL.md +2 -2
  99. package/dist/skills/resonance/scripts/resonance-hook.sh +2 -0
  100. package/dist/skills/resonance/scripts/resonance.ts +35 -5
  101. package/dist/skills/volute-admin/SKILL.md +83 -0
  102. package/dist/skills/volute-mind/SKILL.md +12 -12
  103. package/dist/skills-XNZK6P4K.js +61 -0
  104. package/dist/sleep-manager-53DZOWW7.js +32 -0
  105. package/dist/spirit-N4W4UQRH.js +217 -0
  106. package/dist/{split-EXYGGGQN.js → split-STOROBYJ.js} +1 -1
  107. package/dist/{sprout-AXQ6H5DB.js → sprout-L2GFOVF7.js} +9 -8
  108. package/dist/{start-MTOVL6SY.js → start-K2NCUUCG.js} +5 -4
  109. package/dist/{status-ZRO37MWR.js → status-TCUMUO6M.js} +5 -5
  110. package/dist/{stop-OK5WEPVC.js → stop-H26JZDXF.js} +5 -4
  111. package/dist/system-chat-NPYFYZVI.js +32 -0
  112. package/dist/{systems-W3BBMSOZ.js → systems-DHBKVYEY.js} +6 -5
  113. package/dist/{tailscale-BM72RXCJ.js → tailscale-XHQBZROW.js} +2 -1
  114. package/dist/{template-hash-3HOR4UAJ.js → template-hash-A6VVKOXJ.js} +2 -1
  115. package/dist/up-6I6BHRTO.js +17 -0
  116. package/dist/{update-PLPHMMZ2.js → update-QVPRF6GR.js} +5 -5
  117. package/dist/{update-check-CVCN7MF6.js → update-check-ZD6OOIYQ.js} +3 -2
  118. package/dist/{upgrade-I6NPCYUU.js → upgrade-O4Q7WJM3.js} +12 -14
  119. package/dist/{version-notify-2NTWVEHL.js → version-notify-TCKWBZZG.js} +22 -23
  120. package/dist/web-assets/assets/index-Bui7U9Uu.css +1 -0
  121. package/dist/web-assets/assets/index-e36DIo1b.js +73 -0
  122. package/dist/web-assets/ext-theme.css +94 -0
  123. package/dist/web-assets/index.html +2 -2
  124. package/drizzle/0000_baseline.sql +152 -0
  125. package/drizzle/0001_add_conversation_private.sql +1 -0
  126. package/drizzle/0002_turns.sql +21 -0
  127. package/drizzle/0003_turn_feed_links.sql +11 -0
  128. package/drizzle/0004_spirits.sql +5 -0
  129. package/drizzle/meta/0000_snapshot.json +3 -223
  130. package/drizzle/meta/0001_snapshot.json +3 -294
  131. package/drizzle/meta/0002_snapshot.json +3 -335
  132. package/drizzle/meta/0003_snapshot.json +3 -413
  133. package/drizzle/meta/0004_snapshot.json +3 -406
  134. package/drizzle/meta/_journal.json +10 -101
  135. package/package.json +3 -3
  136. package/packages/extensions/notes/dist/ui/assets/index-8jWEv9SA.js +61 -0
  137. package/packages/extensions/notes/dist/ui/assets/index-DkaB7Ytd.css +1 -0
  138. package/packages/extensions/notes/dist/ui/index.html +2 -2
  139. package/packages/extensions/notes/skills/notes/SKILL.md +8 -8
  140. package/packages/extensions/pages/skills/pages/SKILL.md +17 -44
  141. package/templates/_base/.init/.config/hooks/pre-prompt/session-activity.ts +40 -0
  142. package/templates/_base/.init/.local/bin/volute +27 -0
  143. package/templates/_base/.init/.local/hooks/pre-prompt/session-activity.ts +40 -0
  144. package/templates/_base/.init/.local/hooks/startup-context.ts +58 -0
  145. package/templates/_base/home/.config/routes.json +1 -1
  146. package/templates/_base/src/lib/auto-commit.ts +82 -43
  147. package/templates/_base/src/lib/daemon-client.ts +40 -36
  148. package/templates/_base/src/lib/format-prefix.ts +1 -0
  149. package/templates/_base/src/lib/hook-loader.ts +155 -0
  150. package/templates/_base/src/lib/router.ts +17 -1
  151. package/templates/_base/src/lib/startup.ts +17 -12
  152. package/templates/_base/src/lib/transparency.ts +2 -2
  153. package/templates/_base/src/lib/volute-server.ts +2 -5
  154. package/templates/claude/.init/.claude/settings.json +1 -1
  155. package/templates/claude/.init/.config/routes.json +2 -2
  156. package/templates/claude/src/agent.ts +97 -14
  157. package/templates/claude/src/lib/hooks/auto-commit.ts +7 -3
  158. package/templates/claude/src/lib/message-channel.ts +7 -2
  159. package/templates/claude/src/server.ts +0 -9
  160. package/templates/codex/.init/.config/routes.json +11 -0
  161. package/templates/codex/.init/AGENTS.md +29 -0
  162. package/templates/codex/home/.config/config.json.tmpl +7 -0
  163. package/templates/codex/package.json.tmpl +20 -0
  164. package/templates/codex/src/agent.ts +553 -0
  165. package/templates/codex/src/lib/content.ts +16 -0
  166. package/templates/codex/src/lib/session-store.ts +56 -0
  167. package/templates/codex/src/server.ts +59 -0
  168. package/templates/codex/volute-template.json +8 -0
  169. package/templates/pi/.init/.config/routes.json +2 -2
  170. package/templates/pi/package.json.tmpl +1 -1
  171. package/templates/pi/src/agent.ts +63 -9
  172. package/templates/pi/src/lib/event-handler.ts +6 -4
  173. package/templates/pi/src/lib/reply-instructions-extension.ts +32 -11
  174. package/dist/chunk-7D47T4RB.js +0 -84
  175. package/dist/chunk-CVH6Y2YG.js +0 -59
  176. package/dist/chunk-EFP3PE6C.js +0 -232
  177. package/dist/chunk-LIRWLNAK.js +0 -729
  178. package/dist/daemon-client-BCTFGVCZ.js +0 -9
  179. package/dist/down-NGBMGORS.js +0 -14
  180. package/dist/message-delivery-6YMVNOEC.js +0 -28
  181. package/dist/migrate-registry-to-db-FK35IPEH.js +0 -110
  182. package/dist/mind-manager-YFCOIAAX.js +0 -18
  183. package/dist/pages-watcher-Z3PKNROC.js +0 -21
  184. package/dist/read-WQMPTSN2.js +0 -46
  185. package/dist/seed-WUQMPLDM.js +0 -71
  186. package/dist/skills/sessions/SKILL.md +0 -49
  187. package/dist/sleep-manager-O7YQFCV5.js +0 -30
  188. package/dist/up-BXUAIDXB.js +0 -17
  189. package/dist/web-assets/assets/index--kREqKl9.js +0 -72
  190. package/dist/web-assets/assets/index-BXYTG0nJ.css +0 -1
  191. package/drizzle/0000_flaky_mariko_yashida.sql +0 -34
  192. package/drizzle/0001_careless_warpath.sql +0 -12
  193. package/drizzle/0002_wealthy_the_call.sql +0 -6
  194. package/drizzle/0003_clean_ego.sql +0 -12
  195. package/drizzle/0004_magical_silverclaw.sql +0 -1
  196. package/drizzle/0005_rename_agents_to_minds.sql +0 -11
  197. package/drizzle/0006_mind_history.sql +0 -20
  198. package/drizzle/0007_system_prompts.sql +0 -5
  199. package/drizzle/0008_volute_channels.sql +0 -24
  200. package/drizzle/0009_shared_skills.sql +0 -9
  201. package/drizzle/0010_delivery_queue.sql +0 -12
  202. package/drizzle/0011_rename_human_to_brain.sql +0 -1
  203. package/drizzle/0012_activity.sql +0 -11
  204. package/drizzle/0013_user_profiles.sql +0 -3
  205. package/drizzle/0014_conversation_reads.sql +0 -7
  206. package/drizzle/0015_notes.sql +0 -23
  207. package/drizzle/0016_note_reactions_and_replies.sql +0 -15
  208. package/drizzle/0017_minds.sql +0 -16
  209. package/drizzle/meta/0005_snapshot.json +0 -410
  210. package/drizzle/meta/0006_snapshot.json +0 -7
  211. package/drizzle/meta/0007_snapshot.json +0 -7
  212. package/drizzle/meta/0008_snapshot.json +0 -7
  213. package/drizzle/meta/0009_snapshot.json +0 -7
  214. package/drizzle/meta/0010_snapshot.json +0 -7
  215. package/drizzle/meta/0011_snapshot.json +0 -7
  216. package/drizzle/meta/0012_snapshot.json +0 -7
  217. package/drizzle/meta/0013_snapshot.json +0 -7
  218. package/packages/extensions/notes/dist/ui/assets/index-DgawVO5g.css +0 -1
  219. package/packages/extensions/notes/dist/ui/assets/index-qUWoeC4c.js +0 -2
  220. package/packages/extensions/notes/skills/notes/scripts/notes.mjs +0 -185
  221. package/templates/_base/.init/.config/hooks/startup-context.sh +0 -46
  222. package/templates/_base/.init/.config/scripts/session-reader.ts +0 -59
  223. package/templates/_base/home/public/.gitkeep +0 -0
  224. package/templates/_base/src/lib/session-monitor.ts +0 -400
  225. package/templates/claude/src/lib/hooks/session-context.ts +0 -32
  226. package/templates/pi/src/lib/session-context-extension.ts +0 -35
  227. /package/templates/_base/.init/{.config → .local}/hooks/wake-context.sh +0 -0
@@ -1,28 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- broadcast,
3
+ getAllSites,
4
+ getPublishedPages,
5
+ getRecentPages,
6
+ initDb,
7
+ syncPublishedPages
8
+ } from "./chunk-PB65JZK2.js";
9
+ import {
10
+ getUser,
11
+ getUserByUsername
12
+ } from "./chunk-TDRYEPH4.js";
13
+ import {
4
14
  publish
5
- } from "./chunk-P27RV5WM.js";
15
+ } from "./chunk-QZANELPX.js";
6
16
  import {
7
17
  hashSkillDir,
8
18
  importSkillFromDir,
9
19
  sharedSkillsDir
10
- } from "./chunk-MDPCSXZ4.js";
20
+ } from "./chunk-D5G5YOPL.js";
11
21
  import {
12
22
  logger_default
13
23
  } from "./chunk-YUIHSKR6.js";
14
24
  import {
15
25
  getDb,
16
26
  mindDir,
17
- users,
18
27
  voluteHome,
19
- voluteSystemDir,
20
- voluteUserHome
21
- } from "./chunk-HHTXM4JT.js";
28
+ voluteSystemDir
29
+ } from "./chunk-LRCG2JLP.js";
30
+ import {
31
+ turns
32
+ } from "./chunk-RPZZSXV3.js";
22
33
 
23
34
  // 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";
35
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
36
+ import { dirname, resolve as resolve6 } from "path";
26
37
 
27
38
  // packages/extensions/notes/src/index.ts
28
39
  import { resolve } from "path";
@@ -40,47 +51,6 @@ function createExtension(manifest) {
40
51
  return manifest;
41
52
  }
42
53
 
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
54
  // packages/extensions/notes/src/notes.ts
85
55
  function slugify(text) {
86
56
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -319,7 +289,199 @@ async function resolveNoteId(db, getUserByUsername2, authorSlug) {
319
289
  return row?.id ?? null;
320
290
  }
321
291
 
292
+ // packages/extensions/notes/src/commands.ts
293
+ function getFlag(args, flag) {
294
+ const idx = args.indexOf(flag);
295
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
296
+ return void 0;
297
+ }
298
+ function createCommands() {
299
+ return {
300
+ write: {
301
+ description: "Write a new note",
302
+ usage: 'volute notes write "title" "content" [--reply-to author/slug]',
303
+ handler: async (args, ctx) => {
304
+ if (!ctx.db) return { error: "Notes extension requires a database" };
305
+ const mindName = ctx.mindName;
306
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
307
+ const user = await ctx.getUserByUsername(mindName);
308
+ if (!user) return { error: `Unknown mind: ${mindName}` };
309
+ const title = args[0];
310
+ const content = args[1];
311
+ if (!title || !content)
312
+ return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
313
+ let replyToId;
314
+ const replyTo = getFlag(args, "--reply-to");
315
+ if (replyTo) {
316
+ const id = await resolveNoteId(ctx.db, ctx.getUserByUsername, replyTo);
317
+ if (id === null) return { error: `Reply target not found: ${replyTo}` };
318
+ replyToId = id;
319
+ }
320
+ const note = await createNote(ctx.db, ctx.getUser, user.id, title, content, replyToId);
321
+ ctx.publishActivity({
322
+ type: "note_created",
323
+ mind: user.username,
324
+ summary: `${user.username} wrote "${title}"`,
325
+ metadata: { author: user.username, slug: note.slug, bodyHtml: content.slice(0, 500) }
326
+ });
327
+ return { output: `Published: ${note.author_username}/${note.slug}` };
328
+ }
329
+ },
330
+ list: {
331
+ description: "List notes",
332
+ usage: "volute notes list [--author name] [--limit N]",
333
+ handler: async (args, ctx) => {
334
+ if (!ctx.db) return { error: "Notes extension requires a database" };
335
+ const author = getFlag(args, "--author");
336
+ const limit = parseInt(getFlag(args, "--limit") ?? "10", 10);
337
+ const notes = await listNotes(ctx.db, ctx.getUser, ctx.getUserByUsername, {
338
+ authorUsername: author,
339
+ limit
340
+ });
341
+ if (notes.length === 0) return { output: "No notes found." };
342
+ const lines = notes.map((n) => {
343
+ const date = new Date(n.created_at).toLocaleDateString();
344
+ return ` ${n.author_username}/${n.slug} "${n.title}" (${date})`;
345
+ });
346
+ return { output: lines.join("\n") };
347
+ }
348
+ },
349
+ read: {
350
+ description: "Read a note",
351
+ usage: "volute notes read <author/slug>",
352
+ handler: async (args, ctx) => {
353
+ if (!ctx.db) return { error: "Notes extension requires a database" };
354
+ const ref = args[0];
355
+ if (!ref || !ref.includes("/")) return { error: "Usage: volute notes read <author/slug>" };
356
+ const [author, slug] = ref.split("/", 2);
357
+ const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
358
+ if (!note) return { error: "Note not found" };
359
+ const lines = [
360
+ `# ${note.title}
361
+ `,
362
+ `By ${note.author_username} \u2014 ${new Date(note.created_at).toLocaleString()}
363
+ `,
364
+ note.content
365
+ ];
366
+ if (note.reactions?.length) {
367
+ lines.push(
368
+ `
369
+ Reactions: ${note.reactions.map((r) => `${r.emoji} (${r.count})`).join(" ")}`
370
+ );
371
+ }
372
+ if (note.comments?.length) {
373
+ lines.push(`
374
+ Comments (${note.comments.length}):`);
375
+ for (const c of note.comments) {
376
+ lines.push(` ${c.author_username}: ${c.content}`);
377
+ }
378
+ }
379
+ return { output: lines.join("\n") };
380
+ }
381
+ },
382
+ comment: {
383
+ description: "Comment on a note",
384
+ usage: 'volute notes comment <author/slug> "content"',
385
+ handler: async (args, ctx) => {
386
+ if (!ctx.db) return { error: "Notes extension requires a database" };
387
+ const mindName = ctx.mindName;
388
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
389
+ const user = await ctx.getUserByUsername(mindName);
390
+ if (!user) return { error: `Unknown mind: ${mindName}` };
391
+ const ref = args[0];
392
+ const content = args[1];
393
+ if (!ref || !ref.includes("/") || !content) {
394
+ return { error: 'Usage: volute notes comment <author/slug> "content"' };
395
+ }
396
+ const [author, slug] = ref.split("/", 2);
397
+ const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
398
+ if (!note) return { error: "Note not found" };
399
+ await addComment(ctx.db, ctx.getUser, note.id, user.id, content);
400
+ return { output: "Comment added." };
401
+ }
402
+ },
403
+ react: {
404
+ description: "React to a note",
405
+ usage: 'volute notes react <author/slug> "emoji"',
406
+ handler: async (args, ctx) => {
407
+ if (!ctx.db) return { error: "Notes extension requires a database" };
408
+ const mindName = ctx.mindName;
409
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
410
+ const user = await ctx.getUserByUsername(mindName);
411
+ if (!user) return { error: `Unknown mind: ${mindName}` };
412
+ const ref = args[0];
413
+ const emoji = args[1];
414
+ if (!ref || !ref.includes("/") || !emoji) {
415
+ return { error: 'Usage: volute notes react <author/slug> "emoji"' };
416
+ }
417
+ const [author, slug] = ref.split("/", 2);
418
+ const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
419
+ if (!note) return { error: "Note not found" };
420
+ const result = toggleReaction(ctx.db, note.id, user.id, emoji);
421
+ return { output: result.added ? "Reaction added." : "Reaction removed." };
422
+ }
423
+ },
424
+ delete: {
425
+ description: "Delete your own note",
426
+ usage: "volute notes delete <author/slug>",
427
+ handler: async (args, ctx) => {
428
+ if (!ctx.db) return { error: "Notes extension requires a database" };
429
+ const mindName = ctx.mindName;
430
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
431
+ const user = await ctx.getUserByUsername(mindName);
432
+ if (!user) return { error: `Unknown mind: ${mindName}` };
433
+ const ref = args[0];
434
+ if (!ref || !ref.includes("/"))
435
+ return { error: "Usage: volute notes delete <author/slug>" };
436
+ const [author, slug] = ref.split("/", 2);
437
+ const deleted = await deleteNote(ctx.db, ctx.getUserByUsername, author, slug, user.id);
438
+ if (!deleted) return { error: "Note not found or not authorized" };
439
+ return { output: "Note deleted." };
440
+ }
441
+ }
442
+ };
443
+ }
444
+
445
+ // packages/extensions/notes/src/db.ts
446
+ function initDb2(db) {
447
+ db.exec(`
448
+ CREATE TABLE IF NOT EXISTS notes (
449
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
450
+ author_id INTEGER NOT NULL,
451
+ title TEXT NOT NULL,
452
+ slug TEXT NOT NULL,
453
+ content TEXT NOT NULL,
454
+ reply_to_id INTEGER,
455
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
456
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
457
+ );
458
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
459
+ CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
460
+ CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
461
+
462
+ CREATE TABLE IF NOT EXISTS note_comments (
463
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
464
+ note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
465
+ author_id INTEGER NOT NULL,
466
+ content TEXT NOT NULL,
467
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
468
+ );
469
+ CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
470
+
471
+ CREATE TABLE IF NOT EXISTS note_reactions (
472
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
473
+ note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
474
+ user_id INTEGER NOT NULL,
475
+ emoji TEXT NOT NULL,
476
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
477
+ );
478
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
479
+ CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
480
+ `);
481
+ }
482
+
322
483
  // packages/extensions/notes/src/routes.ts
484
+ import { Hono } from "hono";
323
485
  async function parseJson(c) {
324
486
  try {
325
487
  return await c.req.json();
@@ -366,12 +528,19 @@ function createRoutes(ctx) {
366
528
  replyToId = id;
367
529
  }
368
530
  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
- });
531
+ ctx.publishActivity(
532
+ {
533
+ type: "note_created",
534
+ mind: actor.username,
535
+ summary: `${actor.username} wrote "${body.title}"`,
536
+ metadata: {
537
+ author: actor.username,
538
+ slug: note.slug,
539
+ bodyHtml: body.content.slice(0, 500)
540
+ }
541
+ },
542
+ c
543
+ );
375
544
  return c.json(note, 201);
376
545
  }).get("/:author/:slug", async (c) => {
377
546
  const { author, slug } = c.req.param();
@@ -459,8 +628,10 @@ var src_default = createExtension({
459
628
  name: "Notes",
460
629
  version: "0.1.0",
461
630
  description: "Public notes for sharing thoughts, reflections, and ideas",
631
+ icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h10M3 7h8M3 10h6M3 13h9"/></svg>',
462
632
  routes: (ctx) => createRoutes(ctx),
463
- initDb,
633
+ commands: createCommands(),
634
+ initDb: initDb2,
464
635
  skillsDir,
465
636
  standardSkill: true,
466
637
  ui: {
@@ -474,12 +645,221 @@ var src_default = createExtension({
474
645
  });
475
646
 
476
647
  // packages/extensions/pages/src/index.ts
477
- import { resolve as resolve3 } from "path";
648
+ import { resolve as resolve4 } from "path";
649
+
650
+ // packages/extensions/pages/src/commands.ts
651
+ import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
652
+ import { relative, resolve as resolve2 } from "path";
653
+ function createCommands2() {
654
+ return {
655
+ publish: {
656
+ description: "Publish all pages (copy to public snapshot)",
657
+ usage: "volute pages publish [--remote]",
658
+ handler: async (args, ctx) => {
659
+ const mindName = ctx.mindName;
660
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
661
+ const remote = args.includes("--remote");
662
+ const mindDir2 = ctx.getMindDir(mindName);
663
+ if (!mindDir2) return { error: `Mind not found: ${mindName}` };
664
+ const sourceDir = resolve2(mindDir2, "home", "public", "pages");
665
+ if (!existsSync(sourceDir))
666
+ return { error: "No pages directory found (home/public/pages/)" };
667
+ const db = ctx.db;
668
+ if (!db) return { error: "Database not available" };
669
+ const snapshotDir = resolve2(ctx.dataDir, "sites", mindName);
670
+ try {
671
+ if (existsSync(snapshotDir)) rmSync(snapshotDir, { recursive: true });
672
+ cpSync(sourceDir, snapshotDir, { recursive: true });
673
+ } catch (err) {
674
+ return { error: `Failed to publish snapshot: ${err.message}` };
675
+ }
676
+ const htmlFiles = collectHtmlFiles(snapshotDir, snapshotDir);
677
+ let diff;
678
+ try {
679
+ diff = syncPublishedPages(db, mindName, htmlFiles);
680
+ } catch (err) {
681
+ return { error: `Failed to update page database: ${err.message}` };
682
+ }
683
+ for (const file of diff.added) {
684
+ ctx.publishActivity({
685
+ type: "page_published",
686
+ mind: mindName,
687
+ summary: `${mindName} published ${file}`,
688
+ metadata: { file, iframeUrl: `/ext/pages/public/${mindName}/${file}` }
689
+ });
690
+ }
691
+ for (const file of diff.removed) {
692
+ ctx.publishActivity({
693
+ type: "page_removed",
694
+ mind: mindName,
695
+ summary: `${mindName} removed ${file}`,
696
+ metadata: { file }
697
+ });
698
+ }
699
+ let output = `Published ${htmlFiles.length} files`;
700
+ const parts = [];
701
+ if (diff.added.length > 0) parts.push(`${diff.added.length} new`);
702
+ if (diff.updated.length > 0) parts.push(`${diff.updated.length} updated`);
703
+ if (diff.removed.length > 0) parts.push(`${diff.removed.length} removed`);
704
+ if (parts.length > 0) output += ` (${parts.join(", ")})`;
705
+ if (remote) {
706
+ const config = ctx.getSystemsConfig();
707
+ if (!config)
708
+ return {
709
+ error: "Not connected to volute.systems. Run volute systems register or login first."
710
+ };
711
+ const allFiles = collectAllFiles(snapshotDir, snapshotDir);
712
+ const files = {};
713
+ for (const f of allFiles) {
714
+ const fp = resolve2(snapshotDir, f);
715
+ files[f] = readFileSync(fp).toString("base64");
716
+ }
717
+ try {
718
+ const res = await fetch(`${config.apiUrl}/api/pages/publish/${mindName}`, {
719
+ method: "PUT",
720
+ headers: {
721
+ "Content-Type": "application/json",
722
+ Authorization: `Bearer ${config.apiKey}`
723
+ },
724
+ body: JSON.stringify({ files })
725
+ });
726
+ const data = await res.json().catch(() => ({}));
727
+ if (!res.ok) {
728
+ const errMsg = data.error || `HTTP ${res.status}`;
729
+ output += `
730
+ Warning: remote publish failed: ${errMsg}`;
731
+ } else if (data.url) {
732
+ output += `
733
+ Remote: ${data.url}`;
734
+ }
735
+ } catch (err) {
736
+ output += `
737
+ Warning: remote publish failed: ${err.message}`;
738
+ }
739
+ }
740
+ return { output };
741
+ }
742
+ },
743
+ list: {
744
+ description: "List pages with publish status",
745
+ usage: "volute pages list [--all]",
746
+ handler: async (args, ctx) => {
747
+ const mindName = ctx.mindName;
748
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
749
+ const db = ctx.db;
750
+ if (!db) return { error: "Database not available" };
751
+ const allFlag = args.includes("--all");
752
+ const port = process.env.VOLUTE_DAEMON_PORT || "1618";
753
+ if (allFlag) {
754
+ const { getAllSites: getAllSites2 } = await import("./db-RA45JBFG.js");
755
+ const sites = getAllSites2(db);
756
+ const lines2 = [];
757
+ for (const site of sites) {
758
+ for (const f of site.files) {
759
+ const url = `http://localhost:${port}/ext/pages/public/${site.mind}/${f.file}`;
760
+ lines2.push(`${site.mind.padEnd(15)} ${f.file.padEnd(25)} ${url}`);
761
+ }
762
+ }
763
+ return { output: lines2.length > 0 ? lines2.join("\n") : "No published pages found." };
764
+ }
765
+ const mindDir2 = ctx.getMindDir(mindName);
766
+ if (!mindDir2) return { error: `Mind not found: ${mindName}` };
767
+ const sourceDir = resolve2(mindDir2, "home", "public", "pages");
768
+ const published = new Set(getPublishedPages(db, mindName).map((p) => p.file));
769
+ const draftFiles = existsSync(sourceDir) ? collectHtmlFiles(sourceDir, sourceDir) : [];
770
+ const allFiles = /* @__PURE__ */ new Set([...published, ...draftFiles]);
771
+ if (allFiles.size === 0) return { output: "No pages found." };
772
+ const lines = [...allFiles].sort().map((file) => {
773
+ const isPublished = published.has(file);
774
+ const status = isPublished ? "published" : "draft";
775
+ const url = isPublished ? `http://localhost:${port}/ext/pages/public/${mindName}/${file}` : "";
776
+ return `${status.padEnd(11)} ${file.padEnd(25)} ${url}`;
777
+ });
778
+ return { output: lines.join("\n") };
779
+ }
780
+ }
781
+ };
782
+ }
783
+ function collectHtmlFiles(dir, baseDir) {
784
+ const files = [];
785
+ let items;
786
+ try {
787
+ items = readdirSync(dir);
788
+ } catch (err) {
789
+ console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
790
+ return files;
791
+ }
792
+ for (const item of items) {
793
+ if (item.startsWith(".")) continue;
794
+ const fullPath = resolve2(dir, item);
795
+ try {
796
+ const s = statSync(fullPath);
797
+ if (s.isFile() && item.endsWith(".html")) {
798
+ files.push(relative(baseDir, fullPath));
799
+ } else if (s.isDirectory()) {
800
+ files.push(...collectHtmlFiles(fullPath, baseDir));
801
+ }
802
+ } catch (err) {
803
+ console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
804
+ }
805
+ }
806
+ return files.sort();
807
+ }
808
+ function collectAllFiles(dir, baseDir) {
809
+ const files = [];
810
+ let items;
811
+ try {
812
+ items = readdirSync(dir);
813
+ } catch (err) {
814
+ console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
815
+ return files;
816
+ }
817
+ for (const item of items) {
818
+ if (item.startsWith(".")) continue;
819
+ const fullPath = resolve2(dir, item);
820
+ try {
821
+ const s = statSync(fullPath);
822
+ if (s.isFile()) {
823
+ files.push(relative(baseDir, fullPath));
824
+ } else if (s.isDirectory()) {
825
+ files.push(...collectAllFiles(fullPath, baseDir));
826
+ }
827
+ } catch (err) {
828
+ console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
829
+ }
830
+ }
831
+ return files.sort();
832
+ }
478
833
 
479
834
  // packages/extensions/pages/src/routes.ts
480
835
  import { readFile, stat } from "fs/promises";
481
- import { extname, resolve as resolve2 } from "path";
836
+ import { extname, resolve as resolve3 } from "path";
482
837
  import { Hono as Hono2 } from "hono";
838
+
839
+ // packages/extensions/pages/src/cache.ts
840
+ function getSites(db) {
841
+ const sites = getAllSites(db);
842
+ return sites.map((site) => ({
843
+ name: site.mind,
844
+ label: site.mind,
845
+ pages: site.files.map((f) => ({
846
+ file: f.file,
847
+ modified: f.updated_at,
848
+ url: `/ext/pages/public/${site.mind}/${f.file}`
849
+ }))
850
+ }));
851
+ }
852
+ function getRecentPagesList(db, opts) {
853
+ const rows = getRecentPages(db, opts);
854
+ return rows.map((r) => ({
855
+ mind: r.mind,
856
+ file: r.file,
857
+ modified: r.updated_at,
858
+ url: `/ext/pages/public/${r.mind}/${r.file}`
859
+ }));
860
+ }
861
+
862
+ // packages/extensions/pages/src/routes.ts
483
863
  var MIME_TYPES = {
484
864
  ".html": "text/html",
485
865
  ".js": "application/javascript",
@@ -496,28 +876,20 @@ var MIME_TYPES = {
496
876
  ".txt": "text/plain",
497
877
  ".xml": "application/xml"
498
878
  };
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
879
  function createRoutes2(ctx) {
507
880
  return new Hono2().get("/", async (c) => {
508
- const pw = await getPagesWatcher();
509
- const sites = await pw.getCachedSites();
510
- const recentPages = await pw.getCachedRecentPages();
881
+ if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
882
+ const sites = getSites(ctx.db);
883
+ const recentPages = getRecentPagesList(ctx.db);
511
884
  return c.json({ sites, recentPages });
512
885
  }).get("/feed", async (c) => {
513
- const pw = await getPagesWatcher();
514
- let recentPages = await pw.getCachedRecentPages();
886
+ if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
515
887
  const mind = c.req.query("mind");
516
- if (mind) recentPages = recentPages.filter((p) => p.mind === mind);
517
888
  const rawLimit = c.req.query("limit");
518
- const limit = rawLimit ? parseInt(rawLimit, 10) : 8;
889
+ const limit = rawLimit ? parseInt(rawLimit, 10) || 8 : 8;
890
+ const recentPages = getRecentPagesList(ctx.db, { mind: mind || void 0, limit });
519
891
  return c.json(
520
- recentPages.slice(0, limit).map((p) => ({
892
+ recentPages.map((p) => ({
521
893
  id: `page-${p.mind}-${p.file}`,
522
894
  title: `${p.mind}/${p.file}`,
523
895
  url: p.url ?? `/minds/${p.mind}/pages/${p.file}`,
@@ -576,66 +948,70 @@ function createRoutes2(ctx) {
576
948
  var _voluteHome = null;
577
949
  async function getVoluteHome() {
578
950
  if (_voluteHome) return _voluteHome();
579
- const mod = await import("./registry-ODSALQQL.js");
951
+ const mod = await import("./registry-PJ4S5PHQ.js");
580
952
  _voluteHome = mod.voluteHome;
581
953
  return _voluteHome();
582
954
  }
583
955
  function createPublicRoutes(ctx) {
584
956
  return new Hono2().get("/:name/*", async (c) => {
585
957
  const name = c.req.param("name");
958
+ if (name.includes("/") || name.includes("\\") || name === "." || name === "..")
959
+ return c.text("Not found", 404);
586
960
  let pagesRoot;
587
961
  if (name === "_system") {
588
962
  const home = await getVoluteHome();
589
- pagesRoot = resolve2(home, "shared", "pages");
963
+ pagesRoot = resolve3(home, "shared", "pages");
590
964
  } else {
591
- const mindDirPath = ctx.getMindDir(name);
592
- if (!mindDirPath) return c.text("Not found", 404);
593
- pagesRoot = resolve2(mindDirPath, "home", "public", "pages");
965
+ pagesRoot = resolve3(ctx.dataDir, "sites", name);
594
966
  }
595
967
  const prefix = `/public/${name}`;
596
968
  const idx = c.req.path.indexOf(prefix);
597
969
  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 + "/"))
970
+ const requestedPath = resolve3(pagesRoot, wildcard.slice(1));
971
+ if (requestedPath !== pagesRoot && !requestedPath.startsWith(`${pagesRoot}/`))
600
972
  return c.text("Forbidden", 403);
973
+ let fileToServe = requestedPath;
601
974
  let fileStat = await stat(requestedPath).catch(() => null);
602
975
  if (fileStat?.isDirectory()) {
603
- const indexPath = resolve2(requestedPath, "index.html");
976
+ const indexPath = resolve3(requestedPath, "index.html");
604
977
  fileStat = await stat(indexPath).catch(() => null);
605
978
  if (fileStat?.isFile()) {
606
- const body = await readFile(indexPath);
607
- return c.body(body, 200, { "Content-Type": "text/html" });
979
+ fileToServe = indexPath;
980
+ } else {
981
+ return c.text("Not found", 404);
608
982
  }
983
+ } else if (!fileStat?.isFile()) {
609
984
  return c.text("Not found", 404);
610
985
  }
611
- if (fileStat?.isFile()) {
612
- const ext = extname(requestedPath);
613
- const mime = MIME_TYPES[ext] || "application/octet-stream";
614
- const body = await readFile(requestedPath);
986
+ const ext = extname(fileToServe);
987
+ const mime = MIME_TYPES[ext] || "application/octet-stream";
988
+ try {
989
+ const body = await readFile(fileToServe);
615
990
  return c.body(body, 200, { "Content-Type": mime });
991
+ } catch (err) {
992
+ const code = err.code;
993
+ if (code === "EACCES") return c.text("Forbidden", 403);
994
+ if (code === "ENOENT") return c.text("Not found", 404);
995
+ return c.text("Internal server error", 500);
616
996
  }
617
- return c.text("Not found", 404);
618
997
  });
619
998
  }
620
999
 
621
1000
  // 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
- }
1001
+ var assetsDir2 = resolve4(import.meta.dirname, "../dist/ui");
1002
+ var skillsDir2 = resolve4(import.meta.dirname, "../skills");
630
1003
  var src_default2 = createExtension({
631
1004
  id: "pages",
632
1005
  name: "Pages",
633
1006
  version: "0.1.0",
634
1007
  description: "Publish and serve web pages from mind directories",
1008
+ initDb,
635
1009
  routes: (ctx) => createRoutes2(ctx),
636
1010
  publicRoutes: (ctx) => createPublicRoutes(ctx),
1011
+ commands: createCommands2(),
637
1012
  skillsDir: skillsDir2,
638
1013
  standardSkill: true,
1014
+ icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="2" width="14" height="12" rx="1.5"/><path d="M1 5h14"/><circle cx="3" cy="3.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="5" cy="3.5" r="0.5" fill="currentColor" stroke="none"/></svg>',
639
1015
  ui: {
640
1016
  assetsDir: assetsDir2,
641
1017
  systemSection: {
@@ -647,194 +1023,129 @@ var src_default2 = createExtension({
647
1023
  feedSource: {
648
1024
  endpoint: "/api/ext/pages/feed"
649
1025
  }
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
1026
  }
669
1027
  });
670
1028
 
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;
1029
+ // src/lib/daemon/turn-tracker.ts
1030
+ import { randomUUID } from "crypto";
1031
+ import { eq } from "drizzle-orm";
1032
+ var tlog = logger_default.child("turn-tracker");
1033
+ var activeTurns = /* @__PURE__ */ new Map();
1034
+ function key(mind, session) {
1035
+ return `${mind}:${session ?? "*"}`;
1036
+ }
1037
+ async function createTurn(mind) {
1038
+ const k = key(mind);
1039
+ const existing = activeTurns.get(k);
1040
+ if (existing) return existing.turnId;
1041
+ const turnId = randomUUID();
1042
+ const entry = { turnId, lastToolUseEventId: void 0 };
1043
+ activeTurns.set(k, entry);
727
1044
  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;
1045
+ const db = await getDb();
1046
+ await db.insert(turns).values({ id: turnId, mind, status: "active" });
735
1047
  } 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;
1048
+ tlog.error(`failed to create turn for ${mind}`, logger_default.errorData(err));
1049
+ if (activeTurns.get(k) === entry) activeTurns.delete(k);
1050
+ return void 0;
741
1051
  }
1052
+ return turnId;
742
1053
  }
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;
1054
+ function getActiveTurnId(mind, session) {
1055
+ return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
755
1056
  }
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")));
1057
+ function trackToolUse(mind, session, eventId) {
1058
+ const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
1059
+ if (entry) entry.lastToolUseEventId = eventId;
759
1060
  }
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;
1061
+ function getLastToolUseEventId(mind, session) {
1062
+ return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
764
1063
  }
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")));
1064
+ async function assignSession(mind, turnId, session) {
1065
+ const wildcardKey = key(mind);
1066
+ const entry = activeTurns.get(wildcardKey);
1067
+ if (!entry || entry.turnId !== turnId) {
1068
+ tlog.warn(`assignSession: no matching turn for ${mind} (turnId=${turnId}, session=${session})`);
1069
+ return;
1070
+ }
1071
+ try {
1072
+ const db = await getDb();
1073
+ await db.update(turns).set({ session }).where(eq(turns.id, turnId));
1074
+ } catch (err) {
1075
+ tlog.error(`failed to assign session to turn ${turnId}`, logger_default.errorData(err));
1076
+ return;
1077
+ }
1078
+ activeTurns.delete(wildcardKey);
1079
+ activeTurns.set(key(mind, session), entry);
1080
+ }
1081
+ async function completeTurn(mind, session) {
1082
+ const k = key(mind, session);
1083
+ const wildcardKey = key(mind);
1084
+ const entry = activeTurns.get(k) ?? activeTurns.get(wildcardKey);
1085
+ if (!entry) return void 0;
1086
+ try {
1087
+ const db = await getDb();
1088
+ await db.update(turns).set({ status: "complete" }).where(eq(turns.id, entry.turnId));
1089
+ } catch (err) {
1090
+ tlog.error(`failed to complete turn ${entry.turnId}`, logger_default.errorData(err));
1091
+ return void 0;
1092
+ }
1093
+ activeTurns.delete(k);
1094
+ activeTurns.delete(wildcardKey);
1095
+ return entry.turnId;
776
1096
  }
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));
1097
+ async function setSummaryEventId(turnId, summaryEventId) {
1098
+ try {
1099
+ const db = await getDb();
1100
+ await db.update(turns).set({ summary_event_id: summaryEventId }).where(eq(turns.id, turnId));
1101
+ } catch (err) {
1102
+ tlog.error(`failed to set summary event for turn ${turnId}`, logger_default.errorData(err));
1103
+ }
782
1104
  }
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` });
1105
+ async function completeOrphanedTurns() {
1106
+ try {
1107
+ const db = await getDb();
1108
+ const active = await db.select({ id: turns.id }).from(turns).where(eq(turns.status, "active"));
1109
+ if (active.length === 0) return;
1110
+ await db.update(turns).set({ status: "complete" }).where(eq(turns.status, "active"));
1111
+ tlog.info(`completed ${active.length} orphaned active turn(s) from previous daemon session`);
1112
+ } catch (err) {
1113
+ tlog.error("failed to complete orphaned turns on startup", logger_default.errorData(err));
1114
+ }
795
1115
  }
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"])));
1116
+ async function clearMind(mind) {
1117
+ const toDelete = [];
1118
+ const turnIds = [];
1119
+ for (const [k, entry] of activeTurns.entries()) {
1120
+ if (k.startsWith(`${mind}:`)) {
1121
+ turnIds.push(entry.turnId);
1122
+ toDelete.push(k);
1123
+ }
1124
+ }
1125
+ for (const k of toDelete) activeTurns.delete(k);
1126
+ if (turnIds.length > 0) {
1127
+ try {
1128
+ const db = await getDb();
1129
+ for (const id of turnIds) {
1130
+ await db.update(turns).set({ status: "complete" }).where(eq(turns.id, id));
1131
+ }
1132
+ } catch (err) {
1133
+ tlog.error(`failed to complete orphaned turns for ${mind}`, logger_default.errorData(err));
1134
+ }
1135
+ }
799
1136
  }
800
1137
 
801
1138
  // 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";
1139
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
1140
+ import { resolve as resolve5 } from "path";
811
1141
  var DEFAULT_API_URL = "https://volute.systems";
812
1142
  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
- }
1143
+ return resolve5(voluteSystemDir(), "systems.json");
832
1144
  }
833
1145
  function readSystemsConfig() {
834
- migrateIfNeeded();
835
1146
  const path = configPath();
836
- if (!existsSync(path)) return null;
837
- const raw = readFileSync(path, "utf-8");
1147
+ if (!existsSync2(path)) return null;
1148
+ const raw = readFileSync2(path, "utf-8");
838
1149
  let data;
839
1150
  try {
840
1151
  data = JSON.parse(raw);
@@ -870,19 +1181,19 @@ function deleteSystemsConfig() {
870
1181
  var VALID_EXTENSION_ID2 = /^[a-z0-9][a-z0-9_-]*$/;
871
1182
  var loaded = [];
872
1183
  function extensionsBaseDir() {
873
- return resolve5(voluteHome(), "extensions");
1184
+ return resolve6(voluteHome(), "extensions");
874
1185
  }
875
1186
  function extensionDataDir(id) {
876
- return resolve5(voluteSystemDir(), "extension-data", id);
1187
+ return resolve6(voluteSystemDir(), "extension-data", id);
877
1188
  }
878
1189
  function extensionsConfigPath() {
879
- return resolve5(voluteHome(), "system", "extensions.json");
1190
+ return resolve6(voluteHome(), "system", "extensions.json");
880
1191
  }
881
1192
  function readExtensionsConfig() {
882
1193
  const configPath2 = extensionsConfigPath();
883
- if (!existsSync2(configPath2)) return [];
1194
+ if (!existsSync3(configPath2)) return [];
884
1195
  try {
885
- const data = JSON.parse(readFileSync2(configPath2, "utf-8"));
1196
+ const data = JSON.parse(readFileSync3(configPath2, "utf-8"));
886
1197
  return Array.isArray(data) ? data : [];
887
1198
  } catch (err) {
888
1199
  logger_default.warn("failed to read extensions config, ignoring installed extensions", {
@@ -900,73 +1211,10 @@ async function getLibsqlDatabase() {
900
1211
  return _LibsqlDatabase;
901
1212
  }
902
1213
  async function openExtensionDb(_id, dataDir) {
903
- const dbPath = resolve5(dataDir, "data.db");
1214
+ const dbPath = resolve6(dataDir, "data.db");
904
1215
  const Database = await getLibsqlDatabase();
905
1216
  return new Database(dbPath);
906
1217
  }
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
1218
  async function buildContext(manifest, dataDir, authMw) {
971
1219
  let db = null;
972
1220
  if (manifest.initDb) {
@@ -977,9 +1225,6 @@ async function buildContext(manifest, dataDir, authMw) {
977
1225
  realDb.close();
978
1226
  throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
979
1227
  }
980
- if (manifest.id === "notes") {
981
- await migrateNotesFromCoreDb(realDb);
982
- }
983
1228
  db = realDb;
984
1229
  }
985
1230
  return {
@@ -992,15 +1237,22 @@ async function buildContext(manifest, dataDir, authMw) {
992
1237
  },
993
1238
  getUser: async (id) => getUser(id),
994
1239
  getUserByUsername: async (username) => getUserByUsername(username),
995
- publishActivity: (event) => {
996
- publish(event).catch(
1240
+ publishActivity: (event, sessionOrContext) => {
1241
+ const session = typeof sessionOrContext === "string" ? sessionOrContext : sessionOrContext?.get("mindSession");
1242
+ const turnId = getActiveTurnId(event.mind, session);
1243
+ const sourceEventId = getLastToolUseEventId(event.mind, session);
1244
+ publish({
1245
+ ...event,
1246
+ turn_id: turnId,
1247
+ source_event_id: sourceEventId
1248
+ }).catch(
997
1249
  (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
998
1250
  );
999
1251
  },
1000
1252
  getMindDir: (name) => {
1001
1253
  try {
1002
1254
  const dir = mindDir(name);
1003
- return existsSync2(dir) ? dir : null;
1255
+ return existsSync3(dir) ? dir : null;
1004
1256
  } catch (err) {
1005
1257
  logger_default.warn(
1006
1258
  `extension ${manifest.id}: failed to resolve mind dir for ${name}`,
@@ -1030,19 +1282,48 @@ async function loadExtension(manifest, app, authMw) {
1030
1282
  const publicApp = manifest.publicRoutes(context);
1031
1283
  app.route(`/ext/${manifest.id}/public`, publicApp);
1032
1284
  }
1285
+ if (manifest.commands) {
1286
+ for (const [cmdName, cmd] of Object.entries(manifest.commands)) {
1287
+ app.post(`${extApiPath}/commands/${cmdName}`, async (c) => {
1288
+ let body;
1289
+ try {
1290
+ body = await c.req.json();
1291
+ } catch {
1292
+ return c.json({ error: "Invalid JSON in request body" }, 400);
1293
+ }
1294
+ const user = c.get("user");
1295
+ const mindName = body.mind || user?.username;
1296
+ const session = c.get("mindSession");
1297
+ try {
1298
+ const result = await cmd.handler(body.args ?? [], {
1299
+ ...context,
1300
+ // Bind publishActivity to the session so command handlers
1301
+ // don't need to pass it explicitly
1302
+ publishActivity: (event, sc) => context.publishActivity(event, sc ?? session),
1303
+ mindName,
1304
+ session
1305
+ });
1306
+ return c.json(result);
1307
+ } catch (err) {
1308
+ logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
1309
+ return c.json({ error: err.message }, 500);
1310
+ }
1311
+ });
1312
+ }
1313
+ }
1033
1314
  let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
1034
- if (resolvedAssetsDir && !existsSync2(resolvedAssetsDir)) {
1315
+ if (resolvedAssetsDir && !existsSync3(resolvedAssetsDir)) {
1035
1316
  let searchDir = dirname(new URL(import.meta.url).pathname);
1036
1317
  for (let i = 0; i < 5; i++) {
1037
- const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
1038
- if (existsSync2(candidate)) {
1318
+ const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
1319
+ if (existsSync3(candidate)) {
1039
1320
  resolvedAssetsDir = candidate;
1040
1321
  break;
1041
1322
  }
1042
1323
  searchDir = dirname(searchDir);
1043
1324
  }
1044
1325
  }
1045
- if (resolvedAssetsDir && existsSync2(resolvedAssetsDir)) {
1326
+ if (resolvedAssetsDir && existsSync3(resolvedAssetsDir)) {
1046
1327
  const assetsDir3 = resolvedAssetsDir;
1047
1328
  const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
1048
1329
  const { extname: ext } = await import("path");
@@ -1059,11 +1340,11 @@ async function loadExtension(manifest, app, authMw) {
1059
1340
  ".woff2": "font/woff2"
1060
1341
  };
1061
1342
  const prefix = `/ext/${manifest.id}`;
1062
- const indexPath = resolve5(assetsDir3, "index.html");
1343
+ const indexPath = resolve6(assetsDir3, "index.html");
1063
1344
  const serveExtAssets = async (c) => {
1064
1345
  const urlPath = new URL(c.req.url).pathname;
1065
1346
  const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
1066
- const filePath = resolve5(assetsDir3, relativePath);
1347
+ const filePath = resolve6(assetsDir3, relativePath);
1067
1348
  if (filePath !== assetsDir3 && !filePath.startsWith(assetsDir3 + "/"))
1068
1349
  return c.text("Forbidden", 403);
1069
1350
  const s = await fsStat(filePath).catch(() => null);
@@ -1072,7 +1353,7 @@ async function loadExtension(manifest, app, authMw) {
1072
1353
  const body = await readFile2(filePath);
1073
1354
  return c.body(body, 200, { "Content-Type": mime });
1074
1355
  }
1075
- if (existsSync2(indexPath)) {
1356
+ if (existsSync3(indexPath)) {
1076
1357
  const body = await readFile2(indexPath, "utf-8");
1077
1358
  return c.html(body);
1078
1359
  }
@@ -1085,7 +1366,7 @@ async function loadExtension(manifest, app, authMw) {
1085
1366
  if (skillsDir3) {
1086
1367
  let entries;
1087
1368
  try {
1088
- entries = readdirSync(skillsDir3, { withFileTypes: true });
1369
+ entries = readdirSync2(skillsDir3, { withFileTypes: true });
1089
1370
  } catch (err) {
1090
1371
  logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
1091
1372
  entries = [];
@@ -1093,10 +1374,10 @@ async function loadExtension(manifest, app, authMw) {
1093
1374
  for (const entry of entries) {
1094
1375
  if (!entry.isDirectory()) continue;
1095
1376
  try {
1096
- const skillPath = resolve5(skillsDir3, entry.name);
1377
+ const skillPath = resolve6(skillsDir3, entry.name);
1097
1378
  const sourceHash = hashSkillDir(skillPath);
1098
- const destDir = resolve5(sharedSkillsDir(), entry.name);
1099
- if (existsSync2(destDir)) {
1379
+ const destDir = resolve6(sharedSkillsDir(), entry.name);
1380
+ if (existsSync3(destDir)) {
1100
1381
  const destHash = hashSkillDir(destDir);
1101
1382
  if (sourceHash === destHash) continue;
1102
1383
  }
@@ -1120,11 +1401,11 @@ function resolveSkillsDir(manifest) {
1120
1401
  if (!manifest.skillsDir) return null;
1121
1402
  let searchDir = dirname(new URL(import.meta.url).pathname);
1122
1403
  for (let i = 0; i < 5; i++) {
1123
- const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "skills");
1124
- if (existsSync2(candidate)) return candidate;
1404
+ const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "skills");
1405
+ if (existsSync3(candidate)) return candidate;
1125
1406
  searchDir = dirname(searchDir);
1126
1407
  }
1127
- if (existsSync2(manifest.skillsDir)) return manifest.skillsDir;
1408
+ if (existsSync3(manifest.skillsDir)) return manifest.skillsDir;
1128
1409
  logger_default.warn(`skills dir not found for extension ${manifest.id}: ${manifest.skillsDir}`);
1129
1410
  return null;
1130
1411
  }
@@ -1134,14 +1415,14 @@ function discoverBuiltinExtensions() {
1134
1415
  async function discoverInstalledExtensions() {
1135
1416
  const manifests = [];
1136
1417
  const packages = readExtensionsConfig();
1137
- const npmDir = resolve5(voluteHome(), "extensions", "_npm");
1418
+ const npmDir = resolve6(voluteHome(), "extensions", "_npm");
1138
1419
  const { createRequire } = await import("module");
1139
1420
  for (const pkg of packages) {
1140
1421
  try {
1141
1422
  let resolved = pkg;
1142
- const npmPkgDir = resolve5(npmDir, "node_modules", pkg);
1143
- if (existsSync2(npmPkgDir)) {
1144
- const require2 = createRequire(resolve5(npmDir, "noop.js"));
1423
+ const npmPkgDir = resolve6(npmDir, "node_modules", pkg);
1424
+ if (existsSync3(npmPkgDir)) {
1425
+ const require2 = createRequire(resolve6(npmDir, "noop.js"));
1145
1426
  resolved = require2.resolve(pkg);
1146
1427
  }
1147
1428
  const mod = await import(resolved);
@@ -1184,19 +1465,19 @@ function validateManifest(manifest, source) {
1184
1465
  }
1185
1466
  async function discoverLocalExtensions() {
1186
1467
  const baseDir = extensionsBaseDir();
1187
- if (!existsSync2(baseDir)) return [];
1468
+ if (!existsSync3(baseDir)) return [];
1188
1469
  const manifests = [];
1189
1470
  let entries;
1190
1471
  try {
1191
- entries = readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
1472
+ entries = readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
1192
1473
  } catch (err) {
1193
1474
  logger_default.error("failed to read local extensions directory", logger_default.errorData(err));
1194
1475
  return [];
1195
1476
  }
1196
1477
  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));
1478
+ const extDir = resolve6(baseDir, dir);
1479
+ const candidates = [resolve6(extDir, "src", "index.js"), resolve6(extDir, "index.js")];
1480
+ const entryPoint = candidates.find((p) => existsSync3(p));
1200
1481
  if (!entryPoint) continue;
1201
1482
  try {
1202
1483
  const mod = await import(entryPoint);
@@ -1228,17 +1509,43 @@ async function loadAllExtensions(app, authMw) {
1228
1509
  logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
1229
1510
  }
1230
1511
  }
1512
+ app.get("/api/extensions/commands", (c) => {
1513
+ const result = {};
1514
+ for (const { manifest } of loaded) {
1515
+ if (!manifest.commands) continue;
1516
+ const cmds = {};
1517
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
1518
+ cmds[name] = { description: cmd.description, ...cmd.usage ? { usage: cmd.usage } : {} };
1519
+ }
1520
+ result[manifest.id] = { commands: cmds };
1521
+ }
1522
+ return c.json(result);
1523
+ });
1231
1524
  }
1232
1525
  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
- }));
1526
+ return loaded.map(({ manifest }) => {
1527
+ let commands;
1528
+ if (manifest.commands) {
1529
+ commands = {};
1530
+ for (const [name, cmd] of Object.entries(manifest.commands)) {
1531
+ commands[name] = {
1532
+ description: cmd.description,
1533
+ ...cmd.usage ? { usage: cmd.usage } : {}
1534
+ };
1535
+ }
1536
+ }
1537
+ return {
1538
+ id: manifest.id,
1539
+ name: manifest.name,
1540
+ version: manifest.version,
1541
+ description: manifest.description,
1542
+ icon: manifest.icon,
1543
+ systemSection: manifest.ui?.systemSection,
1544
+ mindSections: manifest.ui?.mindSections,
1545
+ feedSource: manifest.ui?.feedSource,
1546
+ commands
1547
+ };
1548
+ });
1242
1549
  }
1243
1550
  function getExtensionStandardSkills() {
1244
1551
  const skills = [];
@@ -1247,7 +1554,7 @@ function getExtensionStandardSkills() {
1247
1554
  const dir = resolveSkillsDir(manifest);
1248
1555
  if (!dir) continue;
1249
1556
  try {
1250
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1557
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1251
1558
  if (entry.isDirectory()) skills.push(entry.name);
1252
1559
  }
1253
1560
  } catch (err) {
@@ -1300,23 +1607,15 @@ function notifyExtensionsMindStop(mindName) {
1300
1607
  }
1301
1608
 
1302
1609
  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,
1610
+ createTurn,
1611
+ getActiveTurnId,
1612
+ trackToolUse,
1613
+ getLastToolUseEventId,
1614
+ assignSession,
1615
+ completeTurn,
1616
+ setSummaryEventId,
1617
+ completeOrphanedTurns,
1618
+ clearMind,
1320
1619
  readSystemsConfig,
1321
1620
  writeSystemsConfig,
1322
1621
  deleteSystemsConfig,