volute 0.31.0 → 0.33.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 (195) hide show
  1. package/README.md +31 -22
  2. package/dist/{accept-GAKQ3MEH.js → accept-D5VBM7JW.js} +5 -4
  3. package/dist/{activity-events-T5ZRCVAL.js → activity-events-XJO3P4RR.js} +3 -2
  4. package/dist/{ai-service-UWUPM4T6.js → ai-service-SBY2WG7O.js} +18 -5
  5. package/dist/api.d.ts +703 -1068
  6. package/dist/{archive-YBNSJYZZ.js → archive-INXYFVCW.js} +3 -2
  7. package/dist/{auth-T5AW2USD.js → auth-GKCDSO4T.js} +4 -3
  8. package/dist/{bridge-4AJ3EY26.js → bridge-TXWWPPOJ.js} +5 -4
  9. package/dist/{chat-7YLT7FI3.js → chat-U5ZOME3O.js} +8 -8
  10. package/dist/{chunk-NV3TYNWX.js → chunk-2NGTS5UU.js} +1 -1
  11. package/dist/{chunk-BWKIHH7B.js → chunk-3Z2DPESO.js} +662 -508
  12. package/dist/chunk-6LXAAQ43.js +22 -0
  13. package/dist/chunk-7J3HEVR7.js +220 -0
  14. package/dist/{chunk-NOWVQ7AL.js → chunk-A2A4KLFE.js} +351 -301
  15. package/dist/{chunk-LX6T3GKQ.js → chunk-ALEF47VT.js} +1 -1
  16. package/dist/{chunk-S2TZLSDH.js → chunk-C7I35G4R.js} +163 -15
  17. package/dist/{chunk-VGWJSNHS.js → chunk-G53F3JA4.js} +1 -35
  18. package/dist/{chunk-A6TUJJ3L.js → chunk-G6BSYHPK.js} +2 -2
  19. package/dist/{chunk-DAXJKPHZ.js → chunk-GY5HBI7A.js} +2 -2
  20. package/dist/{chunk-BC3P3QCK.js → chunk-I5KY25PQ.js} +1 -9
  21. package/dist/{chunk-BNC43CSY.js → chunk-JUKK7FPS.js} +2 -2
  22. package/dist/{chunk-R5QJBZZG.js → chunk-JYVGHWEJ.js} +21 -11
  23. package/dist/chunk-KIEPMIM5.js +59 -0
  24. package/dist/{chunk-EKDWA7E4.js → chunk-KVK2DLWI.js} +5 -2
  25. package/dist/{chunk-AAO77TZX.js → chunk-LOEJ4HPQ.js} +1 -1
  26. package/dist/chunk-LRCG2JLP.js +251 -0
  27. package/dist/{chunk-EMPFLFTG.js → chunk-M7UL5S3Q.js} +1 -1
  28. package/dist/{chunk-6QIUN46C.js → chunk-N432I7QH.js} +20 -3
  29. package/dist/{chunk-SNVPRRT7.js → chunk-NNB4WIG7.js} +2 -2
  30. package/dist/{chunk-HDKY4TWU.js → chunk-NPKSDYA2.js} +3 -3
  31. package/dist/chunk-OYAKCAVY.js +29 -0
  32. package/dist/chunk-PB65JZK2.js +85 -0
  33. package/dist/chunk-PVY5W6QN.js +41 -0
  34. package/dist/{chunk-PNQCXLSV.js → chunk-QTUVYI7W.js} +58 -1
  35. package/dist/{chunk-X62AXPR7.js → chunk-RPZZSXV3.js} +8 -196
  36. package/dist/{chunk-WRS3B556.js → chunk-RSX4OPZY.js} +5 -5
  37. package/dist/{chunk-FAHDKPEH.js → chunk-RVGLDGMI.js} +5 -3
  38. package/dist/chunk-SKLSMHXO.js +208 -0
  39. package/dist/{chunk-4OUOFS23.js → chunk-UKVWJRKN.js} +1 -1
  40. package/dist/{chunk-57OKQMP3.js → chunk-VH33ZWMW.js} +5 -55
  41. package/dist/cli.js +49 -23
  42. package/dist/{clock-LJCG426D.js → clock-BVH3V6E3.js} +7 -6
  43. package/dist/{cloud-sync-O3LXIRN6.js → cloud-sync-4NWLMFVH.js} +20 -14
  44. package/dist/config-H2H4UIF7.js +72 -0
  45. package/dist/connectors/discord-bridge.js +1 -1
  46. package/dist/connectors/slack-bridge.js +1 -1
  47. package/dist/connectors/telegram-bridge.js +1 -1
  48. package/dist/{conversations-RKKGP5IA.js → conversations-AWI5SZW2.js} +4 -3
  49. package/dist/{create-TL623TFC.js → create-2FK7Z46Y.js} +6 -2
  50. package/dist/{create-WUTIIRI2.js → create-YWD2TIP4.js} +6 -5
  51. package/dist/{daemon-client-CVGM25DM.js → daemon-client-6QXHZ7US.js} +3 -2
  52. package/dist/{daemon-restart-EZP7XH3V.js → daemon-restart-GOBUKLX7.js} +8 -6
  53. package/dist/daemon.js +1918 -1472
  54. package/dist/{db-SW5PL6QA.js → db-F34YLV7D.js} +2 -1
  55. package/dist/db-RA45JBFG.js +16 -0
  56. package/dist/{delete-Z6HAG35F.js → delete-QTGWEDBI.js} +1 -1
  57. package/dist/delivery-manager-PFAKEJTC.js +32 -0
  58. package/dist/delivery-router-FL45JL7N.js +21 -0
  59. package/dist/down-FWWTEKXM.js +15 -0
  60. package/dist/{env-NHESNNSP.js → env-JCOF2222.js} +5 -4
  61. package/dist/{export-EVMP7GWY.js → export-SUYRLI5Q.js} +4 -3
  62. package/dist/{extension-LR7EW3JF.js → extension-OBTGKQQD.js} +5 -3
  63. package/dist/{extensions-NGEJI7JH.js → extensions-KYNTVTMO.js} +10 -7
  64. package/dist/{files-3SM7V33S.js → files-65PMW5IK.js} +6 -5
  65. package/dist/{history-PQD3LXEP.js → history-DKCDI3JO.js} +9 -4
  66. package/dist/{import-PR2OCGQJ.js → import-DDUFE7AY.js} +4 -3
  67. package/dist/isolation-LLAYQYDY.js +22 -0
  68. package/dist/{join-R4EN5CWQ.js → join-I5QEE3LG.js} +1 -1
  69. package/dist/{list-B4XNUOFO.js → list-JQ463EDA.js} +5 -4
  70. package/dist/{login-62JVY6A2.js → login-D7ETSU4R.js} +5 -4
  71. package/dist/{login-URWP6S2N.js → login-RIJF2F4G.js} +3 -2
  72. package/dist/{logout-NXJQJDLI.js → logout-5MLHZALK.js} +3 -2
  73. package/dist/{logout-ZK2N62T3.js → logout-UZJRGY4Z.js} +3 -2
  74. package/dist/message-delivery-DFF5SJRM.js +42 -0
  75. package/dist/{mind-E2ZV2WRX.js → mind-IOJFLEM5.js} +25 -19
  76. package/dist/{mind-activity-tracker-ASNZBMLC.js → mind-activity-tracker-F6O4Q2SL.js} +4 -3
  77. package/dist/{mind-list-BEI7E5WY.js → mind-list-WUPMQDYQ.js} +3 -2
  78. package/dist/mind-manager-NBJF5D26.js +32 -0
  79. package/dist/mind-profile-P67FEHOY.js +47 -0
  80. package/dist/mind-service-2MQ6UK5N.js +38 -0
  81. package/dist/{mind-sleep-CANABWJI.js → mind-sleep-WW2IX7JT.js} +5 -4
  82. package/dist/{mind-status-6WKZVUOP.js → mind-status-L3EFFRPR.js} +3 -2
  83. package/dist/{mind-wake-RZKLH2IN.js → mind-wake-VSSGW465.js} +5 -4
  84. package/dist/{package-NU4CA7OU.js → package-U3VFO273.js} +2 -1
  85. package/dist/{read-THL362EI.js → read-EBY56C33.js} +5 -4
  86. package/dist/read-stdin-HQJ7774D.js +8 -0
  87. package/dist/{register-QAQELAS6.js → register-HD74C4TT.js} +5 -4
  88. package/dist/{registry-ASXCQCNH.js → registry-PJ4S5PHQ.js} +8 -1
  89. package/dist/{reject-AYPBNPNL.js → reject-UJKFBHRO.js} +5 -4
  90. package/dist/{restart-6SKPV3T2.js → restart-3UCMRUVC.js} +3 -2
  91. package/dist/{sandbox-6ZEWQDVU.js → sandbox-GJOK4QLQ.js} +4 -3
  92. package/dist/scheduler-ZZ7XGQG6.js +32 -0
  93. package/dist/schema-PA3M5ZKH.js +32 -0
  94. package/dist/seed-QDYVLG74.js +11 -0
  95. package/dist/seed-check-S2IX25RL.js +32 -0
  96. package/dist/seed-cmd-DKOUFEAU.js +36 -0
  97. package/dist/{seed-OWX2AW75.js → seed-create-4XBBOLRH.js} +27 -10
  98. package/dist/{sprout-FDVI2CGN.js → seed-sprout-GQEIIQRT.js} +24 -9
  99. package/dist/{send-ZO4BTWXK.js → send-QIV2INHB.js} +92 -101
  100. package/dist/{setup-7CFITEQN.js → setup-TISPCO22.js} +7 -2
  101. package/dist/{setup-ZXBXG7E4.js → setup-XMCBE3LF.js} +11 -7
  102. package/dist/{skill-YFXP67A2.js → skill-PSQGRRJX.js} +5 -4
  103. package/dist/skills/dreaming/SKILL.md +6 -4
  104. package/dist/skills/dreaming/references/INSTALL.md +2 -2
  105. package/dist/skills/dreaming/scripts/dream.ts +2 -2
  106. package/dist/skills/dreaming/scripts/wake-context-dreams.sh +1 -1
  107. package/dist/skills/imagegen/SKILL.md +16 -11
  108. package/dist/skills/imagegen/references/INSTALL.md +1 -1
  109. package/dist/skills/imagegen/scripts/imagegen.ts +146 -25
  110. package/dist/skills/orientation/SKILL.md +9 -2
  111. package/dist/skills/resonance/SKILL.md +4 -1
  112. package/dist/skills/resonance/references/INSTALL.md +2 -2
  113. package/dist/skills/resonance/scripts/resonance-hook.sh +2 -0
  114. package/dist/skills/resonance/scripts/resonance.ts +35 -5
  115. package/dist/skills/seed-nurture/SKILL.md +42 -0
  116. package/dist/skills/volute-admin/SKILL.md +83 -0
  117. package/dist/skills/volute-mind/SKILL.md +15 -11
  118. package/dist/skills-7FV7EJTE.js +62 -0
  119. package/dist/sleep-manager-JTXSN7NV.js +36 -0
  120. package/dist/spirit-VRONKFMF.js +23 -0
  121. package/dist/{split-MI62KJUU.js → split-STOROBYJ.js} +1 -1
  122. package/dist/sprout-WKLZXUIQ.js +11 -0
  123. package/dist/{start-D64BRKPH.js → start-K2NCUUCG.js} +3 -2
  124. package/dist/{status-ZZWBYFGE.js → status-3JBTFSMI.js} +6 -4
  125. package/dist/{stop-OP2CTXCO.js → stop-H26JZDXF.js} +3 -2
  126. package/dist/system-chat-JAPOJ3KE.js +36 -0
  127. package/dist/{systems-EQPPT4B7.js → systems-XRI52VCH.js} +6 -5
  128. package/dist/{tailscale-6DJKUMNF.js → tailscale-XHQBZROW.js} +2 -1
  129. package/dist/{template-hash-3HOR4UAJ.js → template-hash-A6VVKOXJ.js} +2 -1
  130. package/dist/up-M5AS6SBV.js +18 -0
  131. package/dist/{update-KUJXATRS.js → update-UD543CXX.js} +6 -4
  132. package/dist/{update-check-5WVSU37T.js → update-check-ZD6OOIYQ.js} +3 -2
  133. package/dist/{upgrade-KBHCWX6T.js → upgrade-O4Q7WJM3.js} +12 -14
  134. package/dist/{version-notify-75ELVKPV.js → version-notify-NBI2MTJO.js} +22 -16
  135. package/dist/volute-config-HD7WWUQC.js +10 -0
  136. package/dist/web-assets/assets/index-CWJrVveV.css +1 -0
  137. package/dist/web-assets/assets/index-DJt14FRI.js +75 -0
  138. package/dist/web-assets/ext-theme.css +93 -0
  139. package/dist/web-assets/index.html +2 -2
  140. package/drizzle/0004_spirits.sql +5 -0
  141. package/drizzle/meta/0004_snapshot.json +7 -0
  142. package/drizzle/meta/_journal.json +7 -0
  143. package/package.json +2 -1
  144. package/packages/extensions/notes/dist/ui/assets/index-8jWEv9SA.js +61 -0
  145. package/packages/extensions/notes/dist/ui/assets/index-DkaB7Ytd.css +1 -0
  146. package/packages/extensions/notes/dist/ui/index.html +2 -2
  147. package/packages/extensions/pages/skills/pages/SKILL.md +16 -46
  148. package/templates/_base/.init/.config/hooks/pre-prompt/session-activity.ts +40 -0
  149. package/templates/_base/.init/{.config → .local}/bin/volute +1 -1
  150. package/templates/_base/.init/.local/hooks/pre-prompt/session-activity.ts +40 -0
  151. package/templates/_base/.init/.local/hooks/startup-context.ts +58 -0
  152. package/templates/_base/home/.config/routes.json +1 -1
  153. package/templates/_base/src/lib/daemon-client.ts +21 -13
  154. package/templates/_base/src/lib/format-prefix.ts +1 -0
  155. package/templates/_base/src/lib/hook-loader.ts +155 -0
  156. package/templates/_base/src/lib/startup.ts +11 -4
  157. package/templates/_base/src/lib/transparency.ts +2 -2
  158. package/templates/claude/.init/.claude/settings.json +1 -1
  159. package/templates/claude/.init/.config/routes.json +2 -2
  160. package/templates/claude/src/agent.ts +95 -13
  161. package/templates/claude/src/lib/message-channel.ts +7 -2
  162. package/templates/claude/src/lib/stream-consumer.ts +38 -0
  163. package/templates/codex/.init/.config/routes.json +11 -0
  164. package/templates/codex/.init/AGENTS.md +29 -0
  165. package/templates/codex/home/.config/config.json.tmpl +7 -0
  166. package/templates/codex/package.json.tmpl +20 -0
  167. package/templates/codex/src/agent.ts +554 -0
  168. package/templates/codex/src/lib/content.ts +16 -0
  169. package/templates/codex/src/lib/session-store.ts +56 -0
  170. package/templates/codex/src/server.ts +59 -0
  171. package/templates/codex/volute-template.json +8 -0
  172. package/templates/pi/.init/.config/routes.json +2 -2
  173. package/templates/pi/src/agent.ts +62 -8
  174. package/templates/pi/src/lib/event-handler.ts +1 -1
  175. package/templates/pi/src/lib/reply-instructions-extension.ts +32 -11
  176. package/dist/chunk-HR5JKIDG.js +0 -222
  177. package/dist/down-TS4XQBA4.js +0 -13
  178. package/dist/message-delivery-UJHCLVU4.js +0 -30
  179. package/dist/mind-manager-IPA6DZXD.js +0 -26
  180. package/dist/pages-watcher-72OVPRMH.js +0 -22
  181. package/dist/skills/sessions/SKILL.md +0 -49
  182. package/dist/sleep-manager-TPS6OGCA.js +0 -30
  183. package/dist/system-chat-B43GIXQU.js +0 -30
  184. package/dist/up-TDXEP3VA.js +0 -16
  185. package/dist/web-assets/assets/index-BM1cTzBg.js +0 -72
  186. package/dist/web-assets/assets/index-BfJkKTPF.css +0 -1
  187. package/packages/extensions/notes/dist/ui/assets/index-B8GdTnXs.css +0 -1
  188. package/packages/extensions/notes/dist/ui/assets/index-CDpGTCWb.js +0 -2
  189. package/packages/extensions/pages/skills/pages/scripts/pages.mjs +0 -58
  190. package/templates/_base/.init/.config/hooks/startup-context.sh +0 -46
  191. package/templates/_base/.init/.config/scripts/session-reader.ts +0 -59
  192. package/templates/_base/src/lib/session-monitor.ts +0 -400
  193. package/templates/claude/src/lib/hooks/session-context.ts +0 -32
  194. package/templates/pi/src/lib/session-context-extension.ts +0 -35
  195. /package/templates/_base/.init/{.config → .local}/hooks/wake-context.sh +0 -0
@@ -1,30 +1,35 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getAllSites,
4
+ getPublishedPages,
5
+ getRecentPages,
6
+ initDb,
7
+ syncPublishedPages
8
+ } from "./chunk-PB65JZK2.js";
2
9
  import {
3
10
  getUser,
4
11
  getUserByUsername
5
- } from "./chunk-R5QJBZZG.js";
12
+ } from "./chunk-JYVGHWEJ.js";
6
13
  import {
7
14
  publish
8
- } from "./chunk-EKDWA7E4.js";
15
+ } from "./chunk-KVK2DLWI.js";
9
16
  import {
10
17
  hashSkillDir,
11
18
  importSkillFromDir,
12
19
  sharedSkillsDir
13
- } from "./chunk-S2TZLSDH.js";
20
+ } from "./chunk-C7I35G4R.js";
14
21
  import {
15
22
  logger_default
16
23
  } from "./chunk-YUIHSKR6.js";
17
24
  import {
18
- getDb,
19
25
  mindDir,
20
- turns,
21
26
  voluteHome,
22
27
  voluteSystemDir
23
- } from "./chunk-X62AXPR7.js";
28
+ } from "./chunk-LRCG2JLP.js";
24
29
 
25
30
  // src/lib/extensions.ts
26
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
27
- import { dirname, resolve as resolve5 } from "path";
31
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
32
+ import { dirname, resolve as resolve6 } from "path";
28
33
 
29
34
  // packages/extensions/notes/src/index.ts
30
35
  import { resolve } from "path";
@@ -290,7 +295,7 @@ function createCommands() {
290
295
  return {
291
296
  write: {
292
297
  description: "Write a new note",
293
- usage: 'volute notes write "title" "content" [--reply-to author/slug]',
298
+ usage: 'volute notes write "title" ["content"] [--reply-to author/slug] (content can be piped via stdin)',
294
299
  handler: async (args, ctx) => {
295
300
  if (!ctx.db) return { error: "Notes extension requires a database" };
296
301
  const mindName = ctx.mindName;
@@ -298,7 +303,7 @@ function createCommands() {
298
303
  const user = await ctx.getUserByUsername(mindName);
299
304
  if (!user) return { error: `Unknown mind: ${mindName}` };
300
305
  const title = args[0];
301
- const content = args[1];
306
+ const content = args[1] ?? ctx.stdin;
302
307
  if (!title || !content)
303
308
  return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
304
309
  let replyToId;
@@ -372,7 +377,7 @@ Comments (${note.comments.length}):`);
372
377
  },
373
378
  comment: {
374
379
  description: "Comment on a note",
375
- usage: 'volute notes comment <author/slug> "content"',
380
+ usage: 'volute notes comment <author/slug> ["content"] (content can be piped via stdin)',
376
381
  handler: async (args, ctx) => {
377
382
  if (!ctx.db) return { error: "Notes extension requires a database" };
378
383
  const mindName = ctx.mindName;
@@ -380,7 +385,7 @@ Comments (${note.comments.length}):`);
380
385
  const user = await ctx.getUserByUsername(mindName);
381
386
  if (!user) return { error: `Unknown mind: ${mindName}` };
382
387
  const ref = args[0];
383
- const content = args[1];
388
+ const content = args[1] ?? ctx.stdin;
384
389
  if (!ref || !ref.includes("/") || !content) {
385
390
  return { error: 'Usage: volute notes comment <author/slug> "content"' };
386
391
  }
@@ -434,7 +439,7 @@ Comments (${note.comments.length}):`);
434
439
  }
435
440
 
436
441
  // packages/extensions/notes/src/db.ts
437
- function initDb(db) {
442
+ function initDb2(db) {
438
443
  db.exec(`
439
444
  CREATE TABLE IF NOT EXISTS notes (
440
445
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -519,19 +524,16 @@ function createRoutes(ctx) {
519
524
  replyToId = id;
520
525
  }
521
526
  const note = await createNote(db, getUser2, actor.id, body.title, body.content, replyToId);
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
- );
527
+ ctx.publishActivity({
528
+ type: "note_created",
529
+ mind: actor.username,
530
+ summary: `${actor.username} wrote "${body.title}"`,
531
+ metadata: {
532
+ author: actor.username,
533
+ slug: note.slug,
534
+ bodyHtml: body.content.slice(0, 500)
535
+ }
536
+ });
535
537
  return c.json(note, 201);
536
538
  }).get("/:author/:slug", async (c) => {
537
539
  const { author, slug } = c.req.param();
@@ -619,21 +621,17 @@ var src_default = createExtension({
619
621
  name: "Notes",
620
622
  version: "0.1.0",
621
623
  description: "Public notes for sharing thoughts, reflections, and ideas",
624
+ 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>',
625
+ color: "yellow",
622
626
  routes: (ctx) => createRoutes(ctx),
623
627
  commands: createCommands(),
624
- initDb,
628
+ initDb: initDb2,
625
629
  skillsDir,
626
630
  standardSkill: true,
627
631
  ui: {
628
632
  assetsDir,
629
633
  systemSection: { id: "notes", label: "Notes", urlPatterns: ["/notes", "/notes/:author/:slug"] },
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
- ],
634
+ mindSections: [{ id: "notes", label: "Notes" }],
637
635
  feedSource: {
638
636
  endpoint: "/api/ext/notes/feed"
639
637
  }
@@ -641,12 +639,221 @@ var src_default = createExtension({
641
639
  });
642
640
 
643
641
  // packages/extensions/pages/src/index.ts
644
- import { resolve as resolve3 } from "path";
642
+ import { resolve as resolve4 } from "path";
643
+
644
+ // packages/extensions/pages/src/commands.ts
645
+ import { cpSync, existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
646
+ import { relative, resolve as resolve2 } from "path";
647
+ function createCommands2() {
648
+ return {
649
+ publish: {
650
+ description: "Publish all pages (copy to public snapshot)",
651
+ usage: "volute pages publish [--remote]",
652
+ handler: async (args, ctx) => {
653
+ const mindName = ctx.mindName;
654
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
655
+ const remote = args.includes("--remote");
656
+ const mindDir2 = ctx.getMindDir(mindName);
657
+ if (!mindDir2) return { error: `Mind not found: ${mindName}` };
658
+ const sourceDir = resolve2(mindDir2, "home", "public", "pages");
659
+ if (!existsSync(sourceDir))
660
+ return { error: "No pages directory found (home/public/pages/)" };
661
+ const db = ctx.db;
662
+ if (!db) return { error: "Database not available" };
663
+ const snapshotDir = resolve2(ctx.dataDir, "sites", mindName);
664
+ try {
665
+ if (existsSync(snapshotDir)) rmSync(snapshotDir, { recursive: true });
666
+ cpSync(sourceDir, snapshotDir, { recursive: true });
667
+ } catch (err) {
668
+ return { error: `Failed to publish snapshot: ${err.message}` };
669
+ }
670
+ const htmlFiles = collectHtmlFiles(snapshotDir, snapshotDir);
671
+ let diff;
672
+ try {
673
+ diff = syncPublishedPages(db, mindName, htmlFiles);
674
+ } catch (err) {
675
+ return { error: `Failed to update page database: ${err.message}` };
676
+ }
677
+ for (const file of diff.added) {
678
+ ctx.publishActivity({
679
+ type: "page_published",
680
+ mind: mindName,
681
+ summary: `${mindName} published ${file}`,
682
+ metadata: { file, iframeUrl: `/ext/pages/public/${mindName}/${file}` }
683
+ });
684
+ }
685
+ for (const file of diff.removed) {
686
+ ctx.publishActivity({
687
+ type: "page_removed",
688
+ mind: mindName,
689
+ summary: `${mindName} removed ${file}`,
690
+ metadata: { file }
691
+ });
692
+ }
693
+ let output = `Published ${htmlFiles.length} files`;
694
+ const parts = [];
695
+ if (diff.added.length > 0) parts.push(`${diff.added.length} new`);
696
+ if (diff.updated.length > 0) parts.push(`${diff.updated.length} updated`);
697
+ if (diff.removed.length > 0) parts.push(`${diff.removed.length} removed`);
698
+ if (parts.length > 0) output += ` (${parts.join(", ")})`;
699
+ if (remote) {
700
+ const config = ctx.getSystemsConfig();
701
+ if (!config)
702
+ return {
703
+ error: "Not connected to volute.systems. Run volute systems register or login first."
704
+ };
705
+ const allFiles = collectAllFiles(snapshotDir, snapshotDir);
706
+ const files = {};
707
+ for (const f of allFiles) {
708
+ const fp = resolve2(snapshotDir, f);
709
+ files[f] = readFileSync(fp).toString("base64");
710
+ }
711
+ try {
712
+ const res = await fetch(`${config.apiUrl}/api/pages/publish/${mindName}`, {
713
+ method: "PUT",
714
+ headers: {
715
+ "Content-Type": "application/json",
716
+ Authorization: `Bearer ${config.apiKey}`
717
+ },
718
+ body: JSON.stringify({ files })
719
+ });
720
+ const data = await res.json().catch(() => ({}));
721
+ if (!res.ok) {
722
+ const errMsg = data.error || `HTTP ${res.status}`;
723
+ output += `
724
+ Warning: remote publish failed: ${errMsg}`;
725
+ } else if (data.url) {
726
+ output += `
727
+ Remote: ${data.url}`;
728
+ }
729
+ } catch (err) {
730
+ output += `
731
+ Warning: remote publish failed: ${err.message}`;
732
+ }
733
+ }
734
+ return { output };
735
+ }
736
+ },
737
+ list: {
738
+ description: "List pages with publish status",
739
+ usage: "volute pages list [--all]",
740
+ handler: async (args, ctx) => {
741
+ const mindName = ctx.mindName;
742
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
743
+ const db = ctx.db;
744
+ if (!db) return { error: "Database not available" };
745
+ const allFlag = args.includes("--all");
746
+ const port = process.env.VOLUTE_DAEMON_PORT || "1618";
747
+ if (allFlag) {
748
+ const { getAllSites: getAllSites2 } = await import("./db-RA45JBFG.js");
749
+ const sites = getAllSites2(db);
750
+ const lines2 = [];
751
+ for (const site of sites) {
752
+ for (const f of site.files) {
753
+ const url = `http://localhost:${port}/ext/pages/public/${site.mind}/${f.file}`;
754
+ lines2.push(`${site.mind.padEnd(15)} ${f.file.padEnd(25)} ${url}`);
755
+ }
756
+ }
757
+ return { output: lines2.length > 0 ? lines2.join("\n") : "No published pages found." };
758
+ }
759
+ const mindDir2 = ctx.getMindDir(mindName);
760
+ if (!mindDir2) return { error: `Mind not found: ${mindName}` };
761
+ const sourceDir = resolve2(mindDir2, "home", "public", "pages");
762
+ const published = new Set(getPublishedPages(db, mindName).map((p) => p.file));
763
+ const draftFiles = existsSync(sourceDir) ? collectHtmlFiles(sourceDir, sourceDir) : [];
764
+ const allFiles = /* @__PURE__ */ new Set([...published, ...draftFiles]);
765
+ if (allFiles.size === 0) return { output: "No pages found." };
766
+ const lines = [...allFiles].sort().map((file) => {
767
+ const isPublished = published.has(file);
768
+ const status = isPublished ? "published" : "draft";
769
+ const url = isPublished ? `http://localhost:${port}/ext/pages/public/${mindName}/${file}` : "";
770
+ return `${status.padEnd(11)} ${file.padEnd(25)} ${url}`;
771
+ });
772
+ return { output: lines.join("\n") };
773
+ }
774
+ }
775
+ };
776
+ }
777
+ function collectHtmlFiles(dir, baseDir) {
778
+ const files = [];
779
+ let items;
780
+ try {
781
+ items = readdirSync(dir);
782
+ } catch (err) {
783
+ console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
784
+ return files;
785
+ }
786
+ for (const item of items) {
787
+ if (item.startsWith(".")) continue;
788
+ const fullPath = resolve2(dir, item);
789
+ try {
790
+ const s = statSync(fullPath);
791
+ if (s.isFile() && item.endsWith(".html")) {
792
+ files.push(relative(baseDir, fullPath));
793
+ } else if (s.isDirectory()) {
794
+ files.push(...collectHtmlFiles(fullPath, baseDir));
795
+ }
796
+ } catch (err) {
797
+ console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
798
+ }
799
+ }
800
+ return files.sort();
801
+ }
802
+ function collectAllFiles(dir, baseDir) {
803
+ const files = [];
804
+ let items;
805
+ try {
806
+ items = readdirSync(dir);
807
+ } catch (err) {
808
+ console.error(`[pages] failed to read directory ${dir}: ${err.message}`);
809
+ return files;
810
+ }
811
+ for (const item of items) {
812
+ if (item.startsWith(".")) continue;
813
+ const fullPath = resolve2(dir, item);
814
+ try {
815
+ const s = statSync(fullPath);
816
+ if (s.isFile()) {
817
+ files.push(relative(baseDir, fullPath));
818
+ } else if (s.isDirectory()) {
819
+ files.push(...collectAllFiles(fullPath, baseDir));
820
+ }
821
+ } catch (err) {
822
+ console.error(`[pages] failed to stat ${fullPath}: ${err.message}`);
823
+ }
824
+ }
825
+ return files.sort();
826
+ }
645
827
 
646
828
  // packages/extensions/pages/src/routes.ts
647
829
  import { readFile, stat } from "fs/promises";
648
- import { extname, resolve as resolve2 } from "path";
830
+ import { extname, resolve as resolve3 } from "path";
649
831
  import { Hono as Hono2 } from "hono";
832
+
833
+ // packages/extensions/pages/src/cache.ts
834
+ function getSites(db) {
835
+ const sites = getAllSites(db);
836
+ return sites.map((site) => ({
837
+ name: site.mind,
838
+ label: site.mind,
839
+ pages: site.files.map((f) => ({
840
+ file: f.file,
841
+ modified: f.updated_at,
842
+ url: `/ext/pages/public/${site.mind}/${f.file}`
843
+ }))
844
+ }));
845
+ }
846
+ function getRecentPagesList(db, opts) {
847
+ const rows = getRecentPages(db, opts);
848
+ return rows.map((r) => ({
849
+ mind: r.mind,
850
+ file: r.file,
851
+ modified: r.updated_at,
852
+ url: `/ext/pages/public/${r.mind}/${r.file}`
853
+ }));
854
+ }
855
+
856
+ // packages/extensions/pages/src/routes.ts
650
857
  var MIME_TYPES = {
651
858
  ".html": "text/html",
652
859
  ".js": "application/javascript",
@@ -663,28 +870,20 @@ var MIME_TYPES = {
663
870
  ".txt": "text/plain",
664
871
  ".xml": "application/xml"
665
872
  };
666
- var _pagesWatcher = null;
667
- async function getPagesWatcher() {
668
- if (_pagesWatcher) return _pagesWatcher;
669
- const mod = await import("./pages-watcher-72OVPRMH.js");
670
- _pagesWatcher = mod;
671
- return _pagesWatcher;
672
- }
673
873
  function createRoutes2(ctx) {
674
874
  return new Hono2().get("/", async (c) => {
675
- const pw = await getPagesWatcher();
676
- const sites = await pw.getCachedSites();
677
- const recentPages = await pw.getCachedRecentPages();
875
+ if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
876
+ const sites = getSites(ctx.db);
877
+ const recentPages = getRecentPagesList(ctx.db);
678
878
  return c.json({ sites, recentPages });
679
879
  }).get("/feed", async (c) => {
680
- const pw = await getPagesWatcher();
681
- let recentPages = await pw.getCachedRecentPages();
880
+ if (!ctx.db) return c.json({ error: "Pages database not available" }, 503);
682
881
  const mind = c.req.query("mind");
683
- if (mind) recentPages = recentPages.filter((p) => p.mind === mind);
684
882
  const rawLimit = c.req.query("limit");
685
- const limit = rawLimit ? parseInt(rawLimit, 10) : 8;
883
+ const limit = rawLimit ? parseInt(rawLimit, 10) || 8 : 8;
884
+ const recentPages = getRecentPagesList(ctx.db, { mind: mind || void 0, limit });
686
885
  return c.json(
687
- recentPages.slice(0, limit).map((p) => ({
886
+ recentPages.map((p) => ({
688
887
  id: `page-${p.mind}-${p.file}`,
689
888
  title: `${p.mind}/${p.file}`,
690
889
  url: p.url ?? `/minds/${p.mind}/pages/${p.file}`,
@@ -720,31 +919,6 @@ function createRoutes2(ctx) {
720
919
  } catch (err) {
721
920
  return c.json({ error: `Connection failed: ${err.message}` }, 502);
722
921
  }
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 });
748
922
  }).get("/status/:name", async (c) => {
749
923
  const user = ctx.resolveUser(c);
750
924
  if (!user) return c.json({ error: "Unauthorized" }, 401);
@@ -768,90 +942,71 @@ function createRoutes2(ctx) {
768
942
  var _voluteHome = null;
769
943
  async function getVoluteHome() {
770
944
  if (_voluteHome) return _voluteHome();
771
- const mod = await import("./registry-ASXCQCNH.js");
945
+ const mod = await import("./registry-PJ4S5PHQ.js");
772
946
  _voluteHome = mod.voluteHome;
773
947
  return _voluteHome();
774
948
  }
775
949
  function createPublicRoutes(ctx) {
776
950
  return new Hono2().get("/:name/*", async (c) => {
777
951
  const name = c.req.param("name");
952
+ if (name.includes("/") || name.includes("\\") || name === "." || name === "..")
953
+ return c.text("Not found", 404);
778
954
  let pagesRoot;
779
955
  if (name === "_system") {
780
956
  const home = await getVoluteHome();
781
- pagesRoot = resolve2(home, "shared", "pages");
957
+ pagesRoot = resolve3(home, "shared", "pages");
782
958
  } else {
783
- const mindDirPath = ctx.getMindDir(name);
784
- if (!mindDirPath) return c.text("Not found", 404);
785
- pagesRoot = resolve2(mindDirPath, "home", "public", "pages");
959
+ pagesRoot = resolve3(ctx.dataDir, "sites", name);
786
960
  }
787
961
  const prefix = `/public/${name}`;
788
962
  const idx = c.req.path.indexOf(prefix);
789
963
  const wildcard = idx >= 0 ? c.req.path.slice(idx + prefix.length) : "/";
790
- const requestedPath = resolve2(pagesRoot, wildcard.slice(1));
791
- if (requestedPath !== pagesRoot && !requestedPath.startsWith(pagesRoot + "/"))
964
+ const requestedPath = resolve3(pagesRoot, wildcard.slice(1));
965
+ if (requestedPath !== pagesRoot && !requestedPath.startsWith(`${pagesRoot}/`))
792
966
  return c.text("Forbidden", 403);
967
+ let fileToServe = requestedPath;
793
968
  let fileStat = await stat(requestedPath).catch(() => null);
794
969
  if (fileStat?.isDirectory()) {
795
- const indexPath = resolve2(requestedPath, "index.html");
970
+ const indexPath = resolve3(requestedPath, "index.html");
796
971
  fileStat = await stat(indexPath).catch(() => null);
797
972
  if (fileStat?.isFile()) {
798
- const body = await readFile(indexPath);
799
- return c.body(body, 200, { "Content-Type": "text/html" });
973
+ fileToServe = indexPath;
974
+ } else {
975
+ return c.text("Not found", 404);
800
976
  }
977
+ } else if (!fileStat?.isFile()) {
801
978
  return c.text("Not found", 404);
802
979
  }
803
- if (fileStat?.isFile()) {
804
- const ext = extname(requestedPath);
805
- const mime = MIME_TYPES[ext] || "application/octet-stream";
806
- const body = await readFile(requestedPath);
980
+ const ext = extname(fileToServe);
981
+ const mime = MIME_TYPES[ext] || "application/octet-stream";
982
+ try {
983
+ const body = await readFile(fileToServe);
807
984
  return c.body(body, 200, { "Content-Type": mime });
985
+ } catch (err) {
986
+ const code = err.code;
987
+ if (code === "EACCES") return c.text("Forbidden", 403);
988
+ if (code === "ENOENT") return c.text("Not found", 404);
989
+ return c.text("Internal server error", 500);
808
990
  }
809
- return c.text("Not found", 404);
810
991
  });
811
992
  }
812
993
 
813
994
  // packages/extensions/pages/src/index.ts
814
- var assetsDir2 = resolve3(import.meta.dirname, "../dist/ui");
815
- var skillsDir2 = resolve3(import.meta.dirname, "../skills");
816
- var _watcher = null;
817
- async function getWatcher() {
818
- if (_watcher) return _watcher;
819
- _watcher = await import("./pages-watcher-72OVPRMH.js");
820
- return _watcher;
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
- };
995
+ var assetsDir2 = resolve4(import.meta.dirname, "../dist/ui");
996
+ var skillsDir2 = resolve4(import.meta.dirname, "../skills");
839
997
  var src_default2 = createExtension({
840
998
  id: "pages",
841
999
  name: "Pages",
842
1000
  version: "0.1.0",
843
1001
  description: "Publish and serve web pages from mind directories",
1002
+ initDb,
844
1003
  routes: (ctx) => createRoutes2(ctx),
845
1004
  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
- },
1005
+ commands: createCommands2(),
853
1006
  skillsDir: skillsDir2,
854
1007
  standardSkill: true,
1008
+ 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>',
1009
+ color: "purple",
855
1010
  ui: {
856
1011
  assetsDir: assetsDir2,
857
1012
  systemSection: {
@@ -859,144 +1014,24 @@ var src_default2 = createExtension({
859
1014
  label: "Pages",
860
1015
  urlPatterns: ["/pages", "/pages/:site", "/pages/:site/:path"]
861
1016
  },
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
- ],
1017
+ mindSections: [{ id: "pages", label: "Pages" }],
869
1018
  feedSource: {
870
1019
  endpoint: "/api/ext/pages/feed"
871
1020
  }
872
- },
873
- onDaemonStart: () => {
874
- getWatcher().then((w) => w.startSystemWatcher()).catch(
875
- (err) => console.error("[pages] failed to start system watcher:", err.message)
876
- );
877
- },
878
- onDaemonStop: () => {
879
- getWatcher().then((w) => w.stopAllWatchers()).catch((err) => console.error("[pages] failed to stop watchers:", err.message));
880
- },
881
- onMindStart: (mindName) => {
882
- getWatcher().then((w) => w.startWatcher(mindName)).catch(
883
- (err) => console.error(`[pages] failed to start watcher for ${mindName}:`, err.message)
884
- );
885
- },
886
- onMindStop: (mindName) => {
887
- getWatcher().then((w) => w.stopWatcher(mindName)).catch(
888
- (err) => console.error(`[pages] failed to stop watcher for ${mindName}:`, err.message)
889
- );
890
1021
  }
891
1022
  });
892
1023
 
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();
906
- try {
907
- const db = await getDb();
908
- await db.insert(turns).values({ id: turnId, mind, status: "active" });
909
- } catch (err) {
910
- tlog.error(`failed to create turn for ${mind}`, logger_default.errorData(err));
911
- return void 0;
912
- }
913
- activeTurns.set(k, { turnId, lastToolUseEventId: void 0 });
914
- return turnId;
915
- }
916
- function getActiveTurnId(mind, session) {
917
- return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
918
- }
919
- function trackToolUse(mind, session, eventId) {
920
- const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
921
- if (entry) entry.lastToolUseEventId = eventId;
922
- }
923
- function getLastToolUseEventId(mind, session) {
924
- return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
925
- }
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;
958
- }
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
- }
966
- }
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
- }
987
- }
988
-
989
1024
  // src/lib/systems-config.ts
990
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
991
- import { resolve as resolve4 } from "path";
1025
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
1026
+ import { resolve as resolve5 } from "path";
992
1027
  var DEFAULT_API_URL = "https://volute.systems";
993
1028
  function configPath() {
994
- return resolve4(voluteSystemDir(), "systems.json");
1029
+ return resolve5(voluteSystemDir(), "systems.json");
995
1030
  }
996
1031
  function readSystemsConfig() {
997
1032
  const path = configPath();
998
- if (!existsSync(path)) return null;
999
- const raw = readFileSync(path, "utf-8");
1033
+ if (!existsSync2(path)) return null;
1034
+ const raw = readFileSync2(path, "utf-8");
1000
1035
  let data;
1001
1036
  try {
1002
1037
  data = JSON.parse(raw);
@@ -1032,19 +1067,19 @@ function deleteSystemsConfig() {
1032
1067
  var VALID_EXTENSION_ID2 = /^[a-z0-9][a-z0-9_-]*$/;
1033
1068
  var loaded = [];
1034
1069
  function extensionsBaseDir() {
1035
- return resolve5(voluteHome(), "extensions");
1070
+ return resolve6(voluteHome(), "extensions");
1036
1071
  }
1037
1072
  function extensionDataDir(id) {
1038
- return resolve5(voluteSystemDir(), "extension-data", id);
1073
+ return resolve6(voluteSystemDir(), "extension-data", id);
1039
1074
  }
1040
1075
  function extensionsConfigPath() {
1041
- return resolve5(voluteHome(), "system", "extensions.json");
1076
+ return resolve6(voluteHome(), "system", "extensions.json");
1042
1077
  }
1043
1078
  function readExtensionsConfig() {
1044
1079
  const configPath2 = extensionsConfigPath();
1045
- if (!existsSync2(configPath2)) return [];
1080
+ if (!existsSync3(configPath2)) return [];
1046
1081
  try {
1047
- const data = JSON.parse(readFileSync2(configPath2, "utf-8"));
1082
+ const data = JSON.parse(readFileSync3(configPath2, "utf-8"));
1048
1083
  return Array.isArray(data) ? data : [];
1049
1084
  } catch (err) {
1050
1085
  logger_default.warn("failed to read extensions config, ignoring installed extensions", {
@@ -1062,7 +1097,7 @@ async function getLibsqlDatabase() {
1062
1097
  return _LibsqlDatabase;
1063
1098
  }
1064
1099
  async function openExtensionDb(_id, dataDir) {
1065
- const dbPath = resolve5(dataDir, "data.db");
1100
+ const dbPath = resolve6(dataDir, "data.db");
1066
1101
  const Database = await getLibsqlDatabase();
1067
1102
  return new Database(dbPath);
1068
1103
  }
@@ -1088,22 +1123,23 @@ async function buildContext(manifest, dataDir, authMw) {
1088
1123
  },
1089
1124
  getUser: async (id) => getUser(id),
1090
1125
  getUserByUsername: async (username) => getUserByUsername(username),
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({
1126
+ publishActivity: (event) => {
1127
+ const enriched = {
1096
1128
  ...event,
1097
- turn_id: turnId,
1098
- source_event_id: sourceEventId
1099
- }).catch(
1129
+ metadata: {
1130
+ ...event.metadata,
1131
+ ...manifest.icon && !event.metadata?.icon ? { icon: manifest.icon } : {},
1132
+ ...manifest.color && !event.metadata?.color ? { color: manifest.color } : {}
1133
+ }
1134
+ };
1135
+ publish(enriched).catch(
1100
1136
  (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
1101
1137
  );
1102
1138
  },
1103
1139
  getMindDir: (name) => {
1104
1140
  try {
1105
1141
  const dir = mindDir(name);
1106
- return existsSync2(dir) ? dir : null;
1142
+ return existsSync3(dir) ? dir : null;
1107
1143
  } catch (err) {
1108
1144
  logger_default.warn(
1109
1145
  `extension ${manifest.id}: failed to resolve mind dir for ${name}`,
@@ -1146,15 +1182,36 @@ async function loadExtension(manifest, app, authMw) {
1146
1182
  const mindName = body.mind || user?.username;
1147
1183
  const session = c.get("mindSession");
1148
1184
  try {
1185
+ const activityPromises = [];
1149
1186
  const result = await cmd.handler(body.args ?? [], {
1150
1187
  ...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),
1188
+ publishActivity: (rawEvent) => {
1189
+ const event = {
1190
+ ...rawEvent,
1191
+ metadata: {
1192
+ ...rawEvent.metadata,
1193
+ ...manifest.icon && !rawEvent.metadata?.icon ? { icon: manifest.icon } : {},
1194
+ ...manifest.color && !rawEvent.metadata?.color ? { color: manifest.color } : {}
1195
+ }
1196
+ };
1197
+ activityPromises.push(
1198
+ publish(event).catch((err) => {
1199
+ logger_default.error(
1200
+ `extension ${manifest.id}: failed to publish activity`,
1201
+ logger_default.errorData(err)
1202
+ );
1203
+ return 0;
1204
+ })
1205
+ );
1206
+ },
1154
1207
  mindName,
1155
- session
1208
+ session,
1209
+ stdin: body.stdin
1156
1210
  });
1157
- return c.json(result);
1211
+ const activityIds = (await Promise.all(activityPromises)).filter((id) => id > 0);
1212
+ const markers = activityIds.map((id) => `[volute:activity:${id}]`).join("");
1213
+ const output = result && typeof result === "object" && "output" in result ? { ...result, output: `${result.output}${markers}` } : markers ? { ...result, output: markers } : result;
1214
+ return c.json(output);
1158
1215
  } catch (err) {
1159
1216
  logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
1160
1217
  return c.json({ error: err.message }, 500);
@@ -1163,18 +1220,18 @@ async function loadExtension(manifest, app, authMw) {
1163
1220
  }
1164
1221
  }
1165
1222
  let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
1166
- if (resolvedAssetsDir && !existsSync2(resolvedAssetsDir)) {
1223
+ if (resolvedAssetsDir && !existsSync3(resolvedAssetsDir)) {
1167
1224
  let searchDir = dirname(new URL(import.meta.url).pathname);
1168
1225
  for (let i = 0; i < 5; i++) {
1169
- const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
1170
- if (existsSync2(candidate)) {
1226
+ const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
1227
+ if (existsSync3(candidate)) {
1171
1228
  resolvedAssetsDir = candidate;
1172
1229
  break;
1173
1230
  }
1174
1231
  searchDir = dirname(searchDir);
1175
1232
  }
1176
1233
  }
1177
- if (resolvedAssetsDir && existsSync2(resolvedAssetsDir)) {
1234
+ if (resolvedAssetsDir && existsSync3(resolvedAssetsDir)) {
1178
1235
  const assetsDir3 = resolvedAssetsDir;
1179
1236
  const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
1180
1237
  const { extname: ext } = await import("path");
@@ -1191,11 +1248,11 @@ async function loadExtension(manifest, app, authMw) {
1191
1248
  ".woff2": "font/woff2"
1192
1249
  };
1193
1250
  const prefix = `/ext/${manifest.id}`;
1194
- const indexPath = resolve5(assetsDir3, "index.html");
1251
+ const indexPath = resolve6(assetsDir3, "index.html");
1195
1252
  const serveExtAssets = async (c) => {
1196
1253
  const urlPath = new URL(c.req.url).pathname;
1197
1254
  const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
1198
- const filePath = resolve5(assetsDir3, relativePath);
1255
+ const filePath = resolve6(assetsDir3, relativePath);
1199
1256
  if (filePath !== assetsDir3 && !filePath.startsWith(assetsDir3 + "/"))
1200
1257
  return c.text("Forbidden", 403);
1201
1258
  const s = await fsStat(filePath).catch(() => null);
@@ -1204,7 +1261,7 @@ async function loadExtension(manifest, app, authMw) {
1204
1261
  const body = await readFile2(filePath);
1205
1262
  return c.body(body, 200, { "Content-Type": mime });
1206
1263
  }
1207
- if (existsSync2(indexPath)) {
1264
+ if (existsSync3(indexPath)) {
1208
1265
  const body = await readFile2(indexPath, "utf-8");
1209
1266
  return c.html(body);
1210
1267
  }
@@ -1217,7 +1274,7 @@ async function loadExtension(manifest, app, authMw) {
1217
1274
  if (skillsDir3) {
1218
1275
  let entries;
1219
1276
  try {
1220
- entries = readdirSync(skillsDir3, { withFileTypes: true });
1277
+ entries = readdirSync2(skillsDir3, { withFileTypes: true });
1221
1278
  } catch (err) {
1222
1279
  logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
1223
1280
  entries = [];
@@ -1225,10 +1282,10 @@ async function loadExtension(manifest, app, authMw) {
1225
1282
  for (const entry of entries) {
1226
1283
  if (!entry.isDirectory()) continue;
1227
1284
  try {
1228
- const skillPath = resolve5(skillsDir3, entry.name);
1285
+ const skillPath = resolve6(skillsDir3, entry.name);
1229
1286
  const sourceHash = hashSkillDir(skillPath);
1230
- const destDir = resolve5(sharedSkillsDir(), entry.name);
1231
- if (existsSync2(destDir)) {
1287
+ const destDir = resolve6(sharedSkillsDir(), entry.name);
1288
+ if (existsSync3(destDir)) {
1232
1289
  const destHash = hashSkillDir(destDir);
1233
1290
  if (sourceHash === destHash) continue;
1234
1291
  }
@@ -1252,11 +1309,11 @@ function resolveSkillsDir(manifest) {
1252
1309
  if (!manifest.skillsDir) return null;
1253
1310
  let searchDir = dirname(new URL(import.meta.url).pathname);
1254
1311
  for (let i = 0; i < 5; i++) {
1255
- const candidate = resolve5(searchDir, "packages", "extensions", manifest.id, "skills");
1256
- if (existsSync2(candidate)) return candidate;
1312
+ const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "skills");
1313
+ if (existsSync3(candidate)) return candidate;
1257
1314
  searchDir = dirname(searchDir);
1258
1315
  }
1259
- if (existsSync2(manifest.skillsDir)) return manifest.skillsDir;
1316
+ if (existsSync3(manifest.skillsDir)) return manifest.skillsDir;
1260
1317
  logger_default.warn(`skills dir not found for extension ${manifest.id}: ${manifest.skillsDir}`);
1261
1318
  return null;
1262
1319
  }
@@ -1266,14 +1323,14 @@ function discoverBuiltinExtensions() {
1266
1323
  async function discoverInstalledExtensions() {
1267
1324
  const manifests = [];
1268
1325
  const packages = readExtensionsConfig();
1269
- const npmDir = resolve5(voluteHome(), "extensions", "_npm");
1326
+ const npmDir = resolve6(voluteHome(), "extensions", "_npm");
1270
1327
  const { createRequire } = await import("module");
1271
1328
  for (const pkg of packages) {
1272
1329
  try {
1273
1330
  let resolved = pkg;
1274
- const npmPkgDir = resolve5(npmDir, "node_modules", pkg);
1275
- if (existsSync2(npmPkgDir)) {
1276
- const require2 = createRequire(resolve5(npmDir, "noop.js"));
1331
+ const npmPkgDir = resolve6(npmDir, "node_modules", pkg);
1332
+ if (existsSync3(npmPkgDir)) {
1333
+ const require2 = createRequire(resolve6(npmDir, "noop.js"));
1277
1334
  resolved = require2.resolve(pkg);
1278
1335
  }
1279
1336
  const mod = await import(resolved);
@@ -1316,19 +1373,19 @@ function validateManifest(manifest, source) {
1316
1373
  }
1317
1374
  async function discoverLocalExtensions() {
1318
1375
  const baseDir = extensionsBaseDir();
1319
- if (!existsSync2(baseDir)) return [];
1376
+ if (!existsSync3(baseDir)) return [];
1320
1377
  const manifests = [];
1321
1378
  let entries;
1322
1379
  try {
1323
- entries = readdirSync(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
1380
+ entries = readdirSync2(baseDir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_npm").map((d) => d.name);
1324
1381
  } catch (err) {
1325
1382
  logger_default.error("failed to read local extensions directory", logger_default.errorData(err));
1326
1383
  return [];
1327
1384
  }
1328
1385
  for (const dir of entries) {
1329
- const extDir = resolve5(baseDir, dir);
1330
- const candidates = [resolve5(extDir, "src", "index.js"), resolve5(extDir, "index.js")];
1331
- const entryPoint = candidates.find((p) => existsSync2(p));
1386
+ const extDir = resolve6(baseDir, dir);
1387
+ const candidates = [resolve6(extDir, "src", "index.js"), resolve6(extDir, "index.js")];
1388
+ const entryPoint = candidates.find((p) => existsSync3(p));
1332
1389
  if (!entryPoint) continue;
1333
1390
  try {
1334
1391
  const mod = await import(entryPoint);
@@ -1390,6 +1447,7 @@ function getLoadedExtensions() {
1390
1447
  name: manifest.name,
1391
1448
  version: manifest.version,
1392
1449
  description: manifest.description,
1450
+ icon: manifest.icon,
1393
1451
  systemSection: manifest.ui?.systemSection,
1394
1452
  mindSections: manifest.ui?.mindSections,
1395
1453
  feedSource: manifest.ui?.feedSource,
@@ -1404,7 +1462,7 @@ function getExtensionStandardSkills() {
1404
1462
  const dir = resolveSkillsDir(manifest);
1405
1463
  if (!dir) continue;
1406
1464
  try {
1407
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1465
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1408
1466
  if (entry.isDirectory()) skills.push(entry.name);
1409
1467
  }
1410
1468
  } catch (err) {
@@ -1457,14 +1515,6 @@ function notifyExtensionsMindStop(mindName) {
1457
1515
  }
1458
1516
 
1459
1517
  export {
1460
- createTurn,
1461
- getActiveTurnId,
1462
- trackToolUse,
1463
- getLastToolUseEventId,
1464
- assignSession,
1465
- completeTurn,
1466
- setSummaryEventId,
1467
- clearMind,
1468
1518
  readSystemsConfig,
1469
1519
  writeSystemsConfig,
1470
1520
  deleteSystemsConfig,