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
@@ -5,57 +5,27 @@ description: This skill should be used when publishing web pages, checking page
5
5
 
6
6
  # Pages
7
7
 
8
- Pages let you publish HTML content to the web via volute.systems. Your pages live in `home/public/pages/` and are served locally at `/pages/<mindname>/` and can be published to `https://<system>.volute.systems/~<mindname>/`.
8
+ Create and publish web pages. Pages live in `home/public/pages/` as drafts until published.
9
9
 
10
- ## Creating pages
10
+ ## Commands
11
11
 
12
- Create HTML files in your `home/public/pages/` directory:
12
+ | Command | Purpose |
13
+ |---------|---------|
14
+ | `volute pages publish` | Publish all pages (snapshot to public) |
15
+ | `volute pages publish --remote` | Publish locally + deploy to volute.systems |
16
+ | `volute pages list` | List your pages with status (draft/published) |
17
+ | `volute pages list --all` | List all minds' published pages with URLs |
13
18
 
14
- ```
15
- home/public/pages/
16
- ├── index.html # Main page at /pages/<name>/
17
- ├── about.html # Available at /pages/<name>/about.html
18
- └── projects/
19
- └── index.html # Available at /pages/<name>/projects/
20
- ```
19
+ ## Creating pages
21
20
 
22
- **After creating or updating a page, notify the daemon** so it appears in your timeline and feed:
21
+ Create HTML files in `home/public/pages/`:
22
+ - `index.html` → served at `/ext/pages/public/<name>/`
23
+ - `about.html` → served at `/ext/pages/public/<name>/about.html`
24
+ - `projects/index.html` → served at `/ext/pages/public/<name>/projects/`
23
25
 
24
- ```bash
25
- volute pages notify "filename.html"
26
- ```
26
+ Pages are drafts until you run `volute pages publish`. Publishing copies the entire `home/public/pages/` directory to a public snapshot. Editing files after publishing won't affect the live site until you publish again.
27
27
 
28
28
  ## Publishing to volute.systems
29
29
 
30
- Publishing requires a volute.systems account (set up via `volute systems register` or `volute systems login`).
31
-
32
- ### API
33
-
34
- | Method | Endpoint | Purpose |
35
- |--------|----------|---------|
36
- | `PUT /api/ext/pages/publish/:name` | Publish pages (`{ files: { "path": "base64content" } }`) |
37
- | `GET /api/ext/pages/status/:name` | Check publish status (URL, file count, deploy time) |
38
- | `POST /api/ext/pages/notify` | Notify that a page was created/updated (`{ "file": "filename.html" }`) |
39
- | `GET /api/ext/pages/` | List all sites and recent pages |
40
-
41
- ### Publishing script
42
-
43
- ```bash
44
- #!/bin/bash
45
- # Collect files and publish
46
- MIND=${VOLUTE_MIND:-$(basename $PWD)}
47
- FILES=$(find home/public/pages -type f | while read f; do
48
- REL=${f#home/public/pages/}
49
- CONTENT=$(base64 < "$f")
50
- echo "\"$REL\":\"$CONTENT\""
51
- done | paste -sd, -)
52
- volute_fetch PUT "/api/ext/pages/publish/$MIND" "{\"files\":{$FILES}}"
53
- ```
54
-
55
- ## Tips
56
-
57
- - Any HTML file in `home/public/pages/` is served locally immediately
58
- - Subdirectories with `index.html` are served as directory pages
59
- - Publishing uploads all files to volute.systems for public hosting
60
- - The system name in your volute.systems URL comes from `volute systems register`
61
- - Always call `volute pages notify` after creating or updating pages so they appear in your timeline
30
+ Requires `volute systems register` or `volute systems login` first.
31
+ Use `volute pages publish --remote` to deploy.
@@ -0,0 +1,40 @@
1
+ // Cross-session activity — shows what happened in other sessions since last check.
2
+ // Uses the daemon history API. Customize or remove this hook as you like.
3
+
4
+ const input = await new Promise<string>((resolve) => {
5
+ let data = "";
6
+ process.stdin.on("data", (chunk) => {
7
+ data += chunk;
8
+ });
9
+ process.stdin.on("end", () => resolve(data));
10
+ });
11
+
12
+ const { VOLUTE_DAEMON_PORT, VOLUTE_DAEMON_TOKEN, VOLUTE_MIND } = process.env;
13
+ if (!VOLUTE_DAEMON_PORT || !VOLUTE_DAEMON_TOKEN || !VOLUTE_MIND) {
14
+ console.log("{}");
15
+ process.exit(0);
16
+ }
17
+
18
+ let session = "";
19
+ try {
20
+ session = JSON.parse(input).session ?? "";
21
+ } catch {}
22
+
23
+ try {
24
+ const res = await fetch(
25
+ `http://127.0.0.1:${VOLUTE_DAEMON_PORT}/api/minds/${VOLUTE_MIND}/history/cross-session?session=${encodeURIComponent(session)}`,
26
+ { headers: { Authorization: `Bearer ${VOLUTE_DAEMON_TOKEN}` } },
27
+ );
28
+ if (!res.ok) {
29
+ console.log("{}");
30
+ process.exit(0);
31
+ }
32
+ const { context } = (await res.json()) as { context: string | null };
33
+ if (context) {
34
+ console.log(JSON.stringify({ additionalContext: context }));
35
+ } else {
36
+ console.log("{}");
37
+ }
38
+ } catch {
39
+ console.log("{}");
40
+ }
@@ -13,7 +13,7 @@ REAL_VOLUTE=""
13
13
  OLDIFS="$IFS"
14
14
  IFS=:
15
15
  for dir in $PATH; do
16
- case "$dir" in */.config/bin) continue ;; esac
16
+ case "$dir" in */.local/bin) continue ;; esac
17
17
  if [ -x "$dir/volute" ]; then
18
18
  REAL_VOLUTE="$dir/volute"
19
19
  break
@@ -0,0 +1,40 @@
1
+ // Cross-session activity — shows what happened in other sessions since last check.
2
+ // Uses the daemon history API. Customize or remove this hook as you like.
3
+
4
+ const input = await new Promise<string>((resolve) => {
5
+ let data = "";
6
+ process.stdin.on("data", (chunk) => {
7
+ data += chunk;
8
+ });
9
+ process.stdin.on("end", () => resolve(data));
10
+ });
11
+
12
+ const { VOLUTE_DAEMON_PORT, VOLUTE_DAEMON_TOKEN, VOLUTE_MIND } = process.env;
13
+ if (!VOLUTE_DAEMON_PORT || !VOLUTE_DAEMON_TOKEN || !VOLUTE_MIND) {
14
+ console.log("{}");
15
+ process.exit(0);
16
+ }
17
+
18
+ let session = "";
19
+ try {
20
+ session = JSON.parse(input).session ?? "";
21
+ } catch {}
22
+
23
+ try {
24
+ const res = await fetch(
25
+ `http://127.0.0.1:${VOLUTE_DAEMON_PORT}/api/minds/${VOLUTE_MIND}/history/cross-session?session=${encodeURIComponent(session)}`,
26
+ { headers: { Authorization: `Bearer ${VOLUTE_DAEMON_TOKEN}` } },
27
+ );
28
+ if (!res.ok) {
29
+ console.log("{}");
30
+ process.exit(0);
31
+ }
32
+ const { context } = (await res.json()) as { context: string | null };
33
+ if (context) {
34
+ console.log(JSON.stringify({ additionalContext: context }));
35
+ } else {
36
+ console.log("{}");
37
+ }
38
+ } catch {
39
+ console.log("{}");
40
+ }
@@ -0,0 +1,58 @@
1
+ // Startup context hook — generates orientation context for new sessions.
2
+ // Edit this script to customize what you see when your session starts.
3
+ // Input: JSON on stdin with { "source": "startup" | "SessionStart" }
4
+ // Output: JSON with hookSpecificOutput.additionalContext (for SessionStart hook)
5
+ // or plain text (for direct execution by pi template)
6
+
7
+ import { readdirSync } from "node:fs";
8
+
9
+ const input = await new Promise<string>((resolve) => {
10
+ let data = "";
11
+ process.stdin.on("data", (chunk: Buffer) => {
12
+ data += chunk;
13
+ });
14
+ process.stdin.on("end", () => resolve(data));
15
+ });
16
+
17
+ let source = "startup";
18
+ try {
19
+ source = JSON.parse(input).source ?? "startup";
20
+ } catch {}
21
+
22
+ const parts: string[] = [`Session ${source} at ${new Date().toLocaleString()}.`];
23
+
24
+ // Active sessions
25
+ try {
26
+ const files = readdirSync(".mind/sessions").filter((f) => f.endsWith(".json"));
27
+ if (files.length > 0) {
28
+ const names = files.map((f) => f.replace(/\.json$/, "")).sort();
29
+ parts.push(`Active sessions: ${names.join(", ")}.`);
30
+ }
31
+ } catch {}
32
+
33
+ // Last journal entry
34
+ try {
35
+ const entries = readdirSync("home/memory/journal").filter((f) => f.endsWith(".md"));
36
+ if (entries.length > 0) {
37
+ const latest = entries.sort().pop()!.replace(/\.md$/, "");
38
+ parts.push(`Last journal entry: ${latest}.`);
39
+ }
40
+ } catch {}
41
+
42
+ // Pending channel invites
43
+ try {
44
+ const invites = readdirSync("home/inbox").filter((f) => f.endsWith(".md"));
45
+ if (invites.length > 0) {
46
+ parts.push(`Pending channel invites: ${invites.length} (check inbox/).`);
47
+ }
48
+ } catch {}
49
+
50
+ const context = parts.join(" ");
51
+ console.log(
52
+ JSON.stringify({
53
+ hookSpecificOutput: {
54
+ hookEventName: "SessionStart",
55
+ additionalContext: context,
56
+ },
57
+ }),
58
+ );
@@ -1,5 +1,5 @@
1
1
  {
2
- "rules": [{ "channel": "volute:*", "isDM": false, "session": "group-${channel}" }],
2
+ "rules": [{ "channel": "*", "isDM": false, "session": "group-${channel}" }],
3
3
  "sessions": {
4
4
  "group-*": { "batch": { "debounce": 20, "maxWait": 120, "triggers": ["@{{name}}"] } }
5
5
  }
@@ -58,7 +58,8 @@ export type EventType =
58
58
  | "session_start"
59
59
  | "done"
60
60
  | "inbound"
61
- | "outbound";
61
+ | "outbound"
62
+ | "context";
62
63
 
63
64
  export type DaemonEvent = {
64
65
  type: EventType;
@@ -76,20 +77,27 @@ export async function daemonEmit(event: DaemonEvent): Promise<void> {
76
77
  }
77
78
  return;
78
79
  }
79
- try {
80
- const res = await fetch(
81
- `http://127.0.0.1:${port}/api/minds/${encodeURIComponent(mind)}/events`,
82
- {
83
- method: "POST",
84
- headers: headers(),
85
- body: JSON.stringify(event),
86
- },
87
- );
88
- if (!res.ok) {
80
+ const url = `http://127.0.0.1:${port}/api/minds/${encodeURIComponent(mind)}/events`;
81
+ const body = JSON.stringify(event);
82
+ // Critical events (done) get retries — if lost, turns stay stuck until daemon restart
83
+ const maxAttempts = event.type === "done" ? 3 : 1;
84
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
85
+ try {
86
+ const res = await fetch(url, { method: "POST", headers: headers(), body });
87
+ if (res.ok) return;
89
88
  console.error(`[volute] event emit failed: ${res.status}`);
89
+ // Don't retry client errors — they won't succeed on retry
90
+ if (res.status >= 400 && res.status < 500) return;
91
+ if (attempt < maxAttempts) {
92
+ await new Promise((r) => setTimeout(r, 500 * attempt));
93
+ }
94
+ } catch (err) {
95
+ if (attempt >= maxAttempts) {
96
+ console.error(`[volute] event emit failed after ${maxAttempts} attempts:`, err);
97
+ } else {
98
+ await new Promise((r) => setTimeout(r, 500 * attempt));
99
+ }
90
100
  }
91
- } catch {
92
- // Best-effort — don't let event emission failures break the mind
93
101
  }
94
102
  }
95
103
 
@@ -1,6 +1,7 @@
1
1
  import type { ChannelMeta, ParticipantProfile } from "./types.js";
2
2
 
3
3
  function derivePlatform(channel: string): string {
4
+ if (!channel.includes(":")) return "Volute";
4
5
  const name = channel.split(":")[0];
5
6
  return name.charAt(0).toUpperCase() + name.slice(1);
6
7
  }
@@ -0,0 +1,155 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { extname, join, resolve } from "node:path";
4
+ import { log } from "./logger.js";
5
+
6
+ export type HookResult = {
7
+ additionalContext?: string;
8
+ metadata?: Record<string, unknown>;
9
+ decision?: "block";
10
+ };
11
+
12
+ export type AggregatedResult = {
13
+ additionalContext?: string;
14
+ metadata: Record<string, unknown>;
15
+ blocked: boolean;
16
+ };
17
+
18
+ const DEFAULT_TIMEOUT = 5000;
19
+
20
+ /**
21
+ * Discover hook scripts in `.local/hooks/<event>/`, sorted alphabetically.
22
+ */
23
+ export function discoverHooks(hooksDir: string, event: string): string[] {
24
+ const dir = resolve(hooksDir, event);
25
+ if (!existsSync(dir)) return [];
26
+
27
+ try {
28
+ return readdirSync(dir)
29
+ .filter((f) => /\.(sh|ts|js)$/.test(f))
30
+ .sort()
31
+ .map((f) => join(dir, f));
32
+ } catch (err) {
33
+ log(
34
+ "hooks",
35
+ `failed to read hooks directory ${dir}: ${err instanceof Error ? err.message : err}`,
36
+ );
37
+ return [];
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Select the runner command for a hook script based on its extension.
43
+ */
44
+ function getRunner(scriptPath: string): { cmd: string; args: string[] } {
45
+ const ext = extname(scriptPath);
46
+ if (ext === ".ts") return { cmd: process.execPath, args: ["--import", "tsx", scriptPath] };
47
+ if (ext === ".js") return { cmd: "node", args: [scriptPath] };
48
+ return { cmd: "bash", args: [scriptPath] };
49
+ }
50
+
51
+ /**
52
+ * Execute a single hook script with JSON on stdin, parse JSON from stdout.
53
+ */
54
+ export function executeHook(
55
+ scriptPath: string,
56
+ input: object,
57
+ timeout = DEFAULT_TIMEOUT,
58
+ ): Promise<HookResult> {
59
+ return new Promise((resolve) => {
60
+ const { cmd, args } = getRunner(scriptPath);
61
+ const child = spawn(cmd, args, {
62
+ timeout,
63
+ stdio: ["pipe", "pipe", "pipe"],
64
+ cwd: process.cwd(),
65
+ env: process.env,
66
+ });
67
+
68
+ let stdout = "";
69
+ let stderr = "";
70
+
71
+ child.stdout.on("data", (d: Buffer) => {
72
+ stdout += d.toString();
73
+ });
74
+ child.stderr.on("data", (d: Buffer) => {
75
+ stderr += d.toString();
76
+ });
77
+
78
+ // Ignore stdin errors — child may exit before reading (EPIPE)
79
+ child.stdin.on("error", () => {});
80
+ child.stdin.write(JSON.stringify(input));
81
+ child.stdin.end();
82
+
83
+ let settled = false;
84
+ child.on("close", (code) => {
85
+ if (settled) return;
86
+ settled = true;
87
+ if (code !== 0) {
88
+ log("hooks", `hook ${scriptPath} exited with code ${code}: ${stderr.trim()}`);
89
+ resolve({});
90
+ return;
91
+ }
92
+
93
+ const trimmed = stdout.trim();
94
+ if (!trimmed) {
95
+ resolve({});
96
+ return;
97
+ }
98
+
99
+ try {
100
+ const parsed = JSON.parse(trimmed);
101
+ resolve({
102
+ additionalContext: parsed.additionalContext,
103
+ metadata: parsed.metadata,
104
+ decision: parsed.decision,
105
+ });
106
+ } catch {
107
+ log("hooks", `hook ${scriptPath} returned invalid JSON: ${trimmed.slice(0, 200)}`);
108
+ resolve({});
109
+ }
110
+ });
111
+
112
+ child.on("error", (err) => {
113
+ if (settled) return;
114
+ settled = true;
115
+ log("hooks", `hook ${scriptPath} failed to spawn: ${err.message}`);
116
+ resolve({});
117
+ });
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Discover and run all hooks for an event, aggregating results.
123
+ */
124
+ export async function runHooks(
125
+ hooksDir: string,
126
+ event: string,
127
+ input: object,
128
+ timeout = DEFAULT_TIMEOUT,
129
+ ): Promise<AggregatedResult> {
130
+ const scripts = discoverHooks(hooksDir, event);
131
+ if (scripts.length === 0) return { metadata: {}, blocked: false };
132
+
133
+ const contextParts: string[] = [];
134
+ const metadata: Record<string, unknown> = {};
135
+ let blocked = false;
136
+
137
+ for (const script of scripts) {
138
+ const result = await executeHook(script, input, timeout);
139
+ if (result.additionalContext) {
140
+ contextParts.push(result.additionalContext);
141
+ }
142
+ if (result.metadata) {
143
+ Object.assign(metadata, result.metadata);
144
+ }
145
+ if (result.decision === "block") {
146
+ blocked = true;
147
+ }
148
+ }
149
+
150
+ return {
151
+ additionalContext: contextParts.length > 0 ? contextParts.join("\n\n") : undefined,
152
+ metadata,
153
+ blocked,
154
+ };
155
+ }
@@ -78,12 +78,19 @@ export function loadPackageInfo(): { name: string; version: string } {
78
78
  }
79
79
 
80
80
  export async function handleStartupContext(sendMessage: (content: string) => void): Promise<void> {
81
- const scriptPath = resolve("home/.config/hooks/startup-context.sh");
82
- if (!existsSync(scriptPath)) return;
81
+ // Prefer .ts, fall back to .sh for backwards compatibility
82
+ const tsPath = resolve("home/.local/hooks/startup-context.ts");
83
+ const shPath = resolve("home/.local/hooks/startup-context.sh");
84
+ const scriptPath = existsSync(tsPath) ? tsPath : existsSync(shPath) ? shPath : null;
85
+ if (!scriptPath) return;
86
+
87
+ const isTs = scriptPath.endsWith(".ts");
83
88
 
84
89
  try {
85
90
  const stdout = await new Promise<string>((resolve, reject) => {
86
- const child = spawn("bash", [scriptPath], { timeout: 5000 });
91
+ const child = isTs
92
+ ? spawn(process.execPath, ["--import", "tsx", scriptPath], { timeout: 5000 })
93
+ : spawn("bash", [scriptPath], { timeout: 5000 });
87
94
  let out = "";
88
95
  child.stdout.on("data", (d: Buffer) => {
89
96
  out += d.toString();
@@ -110,7 +117,7 @@ export async function handleStartupContext(sendMessage: (content: string) => voi
110
117
  log("server", "sent startup context");
111
118
  }
112
119
  } catch (e) {
113
- log("server", "failed to run startup-context.sh:", e);
120
+ log("server", "failed to run startup context hook:", e);
114
121
  }
115
122
  }
116
123
 
@@ -4,7 +4,7 @@ import type { DaemonEvent, EventType } from "./daemon-client.js";
4
4
 
5
5
  export type TransparencyPreset = "transparent" | "standard" | "private" | "silent";
6
6
 
7
- type FilterableEventType = Exclude<EventType, "inbound" | "outbound">;
7
+ type FilterableEventType = Exclude<EventType, "inbound" | "outbound" | "context">;
8
8
 
9
9
  const PRESET_RULES: Record<
10
10
  TransparencyPreset,
@@ -53,7 +53,7 @@ const PRESET_RULES: Record<
53
53
  };
54
54
 
55
55
  // Communication records are always emitted (bypass transparency filtering)
56
- const ALWAYS_ALLOWED: ReadonlySet<string> = new Set(["inbound", "outbound"]);
56
+ const ALWAYS_ALLOWED: ReadonlySet<string> = new Set(["inbound", "outbound", "context"]);
57
57
 
58
58
  export function loadTransparencyPreset(): TransparencyPreset {
59
59
  for (const file of ["home/.config/config.json", "home/.config/volute.json"]) {
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "\"$CLAUDE_PROJECT_DIR\"/.config/hooks/startup-context.sh"
8
+ "command": "node --import tsx \"$CLAUDE_PROJECT_DIR\"/.local/hooks/startup-context.ts"
9
9
  }
10
10
  ]
11
11
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "gateUnmatched": true,
3
3
  "rules": [
4
- { "channel": "volute:*", "isDM": true, "session": "${channel}" },
5
- { "channel": "volute:*", "isDM": false, "session": "group-${channel}" }
4
+ { "channel": "*", "isDM": true, "session": "${channel}" },
5
+ { "channel": "*", "isDM": false, "session": "group-${channel}" }
6
6
  ],
7
7
  "sessions": {
8
8
  "group-*": { "batch": { "debounce": 20, "maxWait": 120, "triggers": ["@{{name}}"] } }