volute 0.32.0 → 0.34.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 (187) hide show
  1. package/README.md +16 -0
  2. package/dist/{accept-74M7I4RZ.js → accept-TW6V4WI4.js} +4 -4
  3. package/dist/{activity-events-HETAODOK.js → activity-events-BN7V6KCC.js} +4 -4
  4. package/dist/{ai-service-ZIPCV3MX.js → ai-service-PSILB5WD.js} +5 -5
  5. package/dist/{api-client-YPKOZP2O.js → api-client-XUXOB7LI.js} +1 -1
  6. package/dist/api.d.ts +1198 -957
  7. package/dist/{archive-INXYFVCW.js → archive-C2VEMQOR.js} +4 -4
  8. package/dist/{auth-6DMGES3I.js → auth-ZFZXJZDQ.js} +5 -5
  9. package/dist/{bridge-BVCBTGPF.js → bridge-O753D5F4.js} +4 -4
  10. package/dist/{chat-XT4OBJBU.js → chat-BHYX7DJ4.js} +9 -9
  11. package/dist/{chunk-M7UL5S3Q.js → chunk-2IOP6PHB.js} +1 -1
  12. package/dist/{chunk-JJ7W6WSB.js → chunk-47XDEWWV.js} +5 -5
  13. package/dist/{chunk-RSX4OPZY.js → chunk-47ZPNLF4.js} +7 -7
  14. package/dist/{chunk-RPZZSXV3.js → chunk-4JSR7YO7.js} +20 -1
  15. package/dist/chunk-6LXAAQ43.js +22 -0
  16. package/dist/{chunk-TSXLLQZW.js → chunk-6OWJXUAR.js} +10 -1
  17. package/dist/{chunk-I5KY25PQ.js → chunk-6WAWMWR5.js} +1 -1
  18. package/dist/{chunk-LSGWR54X.js → chunk-7F2SW2KD.js} +2 -2
  19. package/dist/chunk-7KJOFUNN.js +22 -0
  20. package/dist/{spirit-N4W4UQRH.js → chunk-B2BVAIZ4.js} +21 -12
  21. package/dist/{chunk-LGB6JBHI.js → chunk-BDK73LK6.js} +5 -55
  22. package/dist/{chunk-IYDIE3HG.js → chunk-BFWHBQK4.js} +1 -1
  23. package/dist/{chunk-TDRYEPH4.js → chunk-BM474GX6.js} +4 -4
  24. package/dist/{chunk-R7E6CRVQ.js → chunk-BTWAGDV5.js} +1 -1
  25. package/dist/{chunk-WKF5FEFK.js → chunk-CVL5IGIR.js} +629 -174
  26. package/dist/{chunk-S6NFERDC.js → chunk-E5C7OWZ2.js} +20 -22
  27. package/dist/chunk-FYCALD4Q.js +23 -0
  28. package/dist/{chunk-SKLSMHXO.js → chunk-IS7WJ56Q.js} +1 -1
  29. package/dist/{chunk-2NGTS5UU.js → chunk-M3K5AARV.js} +1 -1
  30. package/dist/{chunk-ALEF47VT.js → chunk-MLOQKQNB.js} +1 -1
  31. package/dist/{chunk-D5G5YOPL.js → chunk-N3DNFPVA.js} +41 -5
  32. package/dist/{chunk-LRCG2JLP.js → chunk-N7BLAHNE.js} +5 -1
  33. package/dist/chunk-OYAKCAVY.js +29 -0
  34. package/dist/{chunk-UKVWJRKN.js → chunk-PLDWHR4D.js} +1 -1
  35. package/dist/{chunk-QBQ424EM.js → chunk-TAHX36HZ.js} +545 -246
  36. package/dist/chunk-U5BTYSAL.js +59 -0
  37. package/dist/{chunk-SX5TKJBZ.js → chunk-V45JXOWY.js} +2 -2
  38. package/dist/{chunk-2FLJ63GU.js → chunk-V6ZCNULL.js} +2 -2
  39. package/dist/{chunk-QZANELPX.js → chunk-XWXBJQBE.js} +3 -2
  40. package/dist/cli.js +32 -24
  41. package/dist/{clock-2UOZ6JPU.js → clock-3X4DSC2N.js} +38 -23
  42. package/dist/{cloud-sync-JN3NWKEM.js → cloud-sync-TG3TIX5H.js} +21 -17
  43. package/dist/{config-H2H4UIF7.js → config-OROA5DUA.js} +4 -4
  44. package/dist/connectors/discord-bridge.js +1 -1
  45. package/dist/connectors/slack-bridge.js +1 -1
  46. package/dist/connectors/telegram-bridge.js +1 -1
  47. package/dist/{conversations-3O5O6AS3.js → conversations-HL2JP5GI.js} +5 -5
  48. package/dist/{create-RNLNCORE.js → create-3SEKKI6P.js} +5 -5
  49. package/dist/{create-WBBYI6V7.js → create-UOSOQ2HN.js} +4 -4
  50. package/dist/daemon-client-WOAQXXBM.js +12 -0
  51. package/dist/{daemon-restart-NGFHFAUF.js → daemon-restart-5ABHNXJZ.js} +9 -8
  52. package/dist/daemon.js +2730 -1520
  53. package/dist/{db-RA45JBFG.js → db-PLEDCBHZ.js} +1 -1
  54. package/dist/db-RYX3SS2W.js +9 -0
  55. package/dist/{delete-QTGWEDBI.js → delete-KYOVWR23.js} +3 -3
  56. package/dist/delivery-manager-2BR5NZKF.js +32 -0
  57. package/dist/{delivery-router-FL45JL7N.js → delivery-router-D5ELDMS2.js} +4 -4
  58. package/dist/down-QVFN4UPK.js +15 -0
  59. package/dist/{env-RLYQBOOP.js → env-R34DT7XL.js} +10 -6
  60. package/dist/exec-DVLXKRIO.js +17 -0
  61. package/dist/{export-SUYRLI5Q.js → export-6ZXAXATG.js} +6 -6
  62. package/dist/extension-PM42QCID.js +97 -0
  63. package/dist/extensions-BBGVL5JC.js +38 -0
  64. package/dist/{files-EAMPO2SJ.js → files-VQV2VZQO.js} +5 -5
  65. package/dist/{import-DDUFE7AY.js → import-MK2I2T6F.js} +5 -5
  66. package/dist/isolation-62MKDZN3.js +22 -0
  67. package/dist/{join-I5QEE3LG.js → join-DGYHTJUH.js} +3 -3
  68. package/dist/lib-DYEZMGW7.js +6588 -0
  69. package/dist/{list-DW2VRTOZ.js → list-C644WTHV.js} +16 -8
  70. package/dist/{login-7CHPW2PN.js → login-IIGEQPHL.js} +4 -4
  71. package/dist/{login-RIJF2F4G.js → login-KZQLMAWE.js} +4 -4
  72. package/dist/{logout-5MLHZALK.js → logout-AGTZVRGP.js} +4 -4
  73. package/dist/{logout-UZJRGY4Z.js → logout-KD6GXIJJ.js} +4 -4
  74. package/dist/message-delivery-V3R6NXJP.js +42 -0
  75. package/dist/{mind-2B6M7Y25.js → mind-BI4EPBVZ.js} +25 -19
  76. package/dist/{mind-activity-tracker-NZZT2NTT.js → mind-activity-tracker-2ACNHA7B.js} +5 -5
  77. package/dist/mind-history-WOYFLQAI.js +264 -0
  78. package/dist/{mind-list-WUPMQDYQ.js → mind-list-6VPM7GUQ.js} +4 -4
  79. package/dist/mind-manager-MWW3BTS4.js +32 -0
  80. package/dist/mind-profile-WPG42U5Y.js +47 -0
  81. package/dist/mind-service-VIKZJK2M.js +38 -0
  82. package/dist/{mind-sleep-B7BHJLH7.js → mind-sleep-XDISJY74.js} +4 -4
  83. package/dist/{mind-status-L3EFFRPR.js → mind-status-7FTZWPZF.js} +4 -4
  84. package/dist/{mind-wake-GY3RFX7Y.js → mind-wake-KIIKEI3A.js} +4 -4
  85. package/dist/{package-PK6JUFL3.js → package-V2WHWVG6.js} +9 -5
  86. package/dist/{read-5AMJRO3D.js → read-H5C26YO7.js} +18 -8
  87. package/dist/read-stdin-PIRM6A2Y.js +8 -0
  88. package/dist/{register-V2JZZKFK.js → register-J27WP33N.js} +4 -4
  89. package/dist/{registry-PJ4S5PHQ.js → registry-UYV5S6QT.js} +3 -3
  90. package/dist/{reject-33HEZMZ4.js → reject-OEANJYIA.js} +4 -4
  91. package/dist/{restart-3UCMRUVC.js → restart-V5EGYBJG.js} +4 -4
  92. package/dist/{sandbox-JANNTX6U.js → sandbox-SI5HMBP3.js} +5 -5
  93. package/dist/scheduler-AGG3L2FO.js +32 -0
  94. package/dist/{schema-PA3M5ZKH.js → schema-ETMABTW4.js} +4 -2
  95. package/dist/seed-WNGI6PNW.js +11 -0
  96. package/dist/seed-check-PXTH7YXS.js +32 -0
  97. package/dist/seed-cmd-VENFTGS3.js +36 -0
  98. package/dist/{seed-ALUQ55FF.js → seed-create-663ALOKH.js} +8 -8
  99. package/dist/{sprout-L2GFOVF7.js → seed-sprout-EH3AGKAI.js} +24 -11
  100. package/dist/{send-3MI36LEF.js → send-7FUUUZZH.js} +66 -51
  101. package/dist/{setup-SZIARWI6.js → setup-GGMKENLN.js} +6 -4
  102. package/dist/{setup-WENLVPVP.js → setup-Z3DEVWV7.js} +13 -11
  103. package/dist/{skill-TUVOTW4Z.js → skill-DKNYJS4P.js} +12 -8
  104. package/dist/skills/imagegen/SKILL.md +11 -7
  105. package/dist/skills/imagegen/scripts/imagegen.ts +146 -25
  106. package/dist/skills/orientation/SKILL.md +9 -2
  107. package/dist/skills/plan-coordinator/SKILL.md +60 -0
  108. package/dist/skills/seed-nurture/SKILL.md +42 -0
  109. package/dist/skills/volute-mind/SKILL.md +11 -221
  110. package/dist/skills/volute-mind/references/extensions.md +37 -0
  111. package/dist/skills/volute-mind/references/integrations.md +48 -0
  112. package/dist/skills/volute-mind/references/routing.md +86 -0
  113. package/dist/skills/volute-mind/references/sleep.md +33 -0
  114. package/dist/skills/volute-mind/references/variants.md +31 -0
  115. package/dist/{skills-XNZK6P4K.js → skills-Q6VZ2UGD.js} +11 -6
  116. package/dist/sleep-manager-BJK2ROPX.js +36 -0
  117. package/dist/spirit-4JP4TY4C.js +23 -0
  118. package/dist/{split-STOROBYJ.js → split-3YPMS2CL.js} +3 -3
  119. package/dist/sprout-E3HJIV2Z.js +11 -0
  120. package/dist/{start-K2NCUUCG.js → start-W3TPKX4D.js} +4 -4
  121. package/dist/{status-TCUMUO6M.js → status-4OVFXFEJ.js} +7 -6
  122. package/dist/{stop-H26JZDXF.js → stop-GTT6YWYO.js} +4 -4
  123. package/dist/system-channel-DXD2JBOU.js +36 -0
  124. package/dist/system-chat-TYLOL7SX.js +36 -0
  125. package/dist/{systems-DHBKVYEY.js → systems-AYLO727G.js} +7 -7
  126. package/dist/{tailscale-XHQBZROW.js → tailscale-ZEUK7GKZ.js} +3 -3
  127. package/dist/{template-hash-A6VVKOXJ.js → template-hash-EJRTKE36.js} +1 -1
  128. package/dist/up-PA7F2CXE.js +18 -0
  129. package/dist/{update-QVPRF6GR.js → update-HG4LCUSG.js} +7 -6
  130. package/dist/{update-check-ZD6OOIYQ.js → update-check-X3YG4WVP.js} +4 -4
  131. package/dist/{upgrade-O4Q7WJM3.js → upgrade-YGNIDICG.js} +3 -3
  132. package/dist/{variant-7TGZHOU3.js → variant-MZUMRTQO.js} +1 -1
  133. package/dist/{version-notify-TCKWBZZG.js → version-notify-YCH4UVQ2.js} +23 -20
  134. package/dist/volute-config-WBKYJGYQ.js +10 -0
  135. package/dist/web-assets/assets/index-DiiwC-CZ.css +1 -0
  136. package/dist/web-assets/assets/index-d6y5b9Ij.js +75 -0
  137. package/dist/web-assets/ext-theme.css +48 -9
  138. package/dist/web-assets/index.html +2 -2
  139. package/drizzle/0005_meta_summaries.sql +15 -0
  140. package/drizzle/meta/0005_snapshot.json +7 -0
  141. package/drizzle/meta/_journal.json +7 -0
  142. package/package.json +8 -4
  143. package/packages/extensions/plan/dist/ui/assets/index-CJj2gZnZ.css +1 -0
  144. package/packages/extensions/plan/dist/ui/assets/index-FMEJmvQz.js +61 -0
  145. package/packages/extensions/plan/dist/ui/index.html +14 -0
  146. package/packages/extensions/plan/skills/plan/SKILL.md +43 -0
  147. package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +37 -0
  148. package/templates/_base/home/VOLUTE.md +12 -19
  149. package/templates/_base/src/lib/context-breakdown.ts +450 -0
  150. package/templates/_base/src/lib/format-prefix.ts +17 -0
  151. package/templates/_base/src/lib/hook-loader.ts +8 -2
  152. package/templates/_base/src/lib/router.ts +75 -33
  153. package/templates/_base/src/lib/routing.ts +4 -1
  154. package/templates/_base/src/lib/startup.ts +16 -8
  155. package/templates/_base/src/lib/types.ts +2 -1
  156. package/templates/_base/src/lib/volute-server.ts +69 -8
  157. package/templates/claude/.init/CLAUDE.md +4 -10
  158. package/templates/claude/package.json.tmpl +1 -0
  159. package/templates/claude/src/agent.ts +100 -32
  160. package/templates/claude/src/lib/hooks/reply-instructions.ts +27 -7
  161. package/templates/claude/src/lib/stream-consumer.ts +40 -2
  162. package/templates/claude/src/server.ts +1 -0
  163. package/templates/codex/package.json.tmpl +1 -0
  164. package/templates/codex/src/agent.ts +81 -8
  165. package/templates/codex/src/server.ts +1 -4
  166. package/templates/pi/package.json.tmpl +1 -0
  167. package/templates/pi/src/agent.ts +115 -36
  168. package/templates/pi/src/lib/event-handler.ts +22 -7
  169. package/templates/pi/src/lib/reply-instructions-extension.ts +23 -4
  170. package/templates/pi/src/lib/subagents.ts +20 -17
  171. package/templates/pi/src/server.ts +2 -5
  172. package/dist/chunk-K3NQKI34.js +0 -10
  173. package/dist/daemon-client-6QXHZ7US.js +0 -12
  174. package/dist/db-F34YLV7D.js +0 -9
  175. package/dist/delivery-manager-SDVXFD4W.js +0 -28
  176. package/dist/down-TB3ESMNP.js +0 -14
  177. package/dist/extension-FQ5D3NCC.js +0 -174
  178. package/dist/extensions-GDYWQXC4.js +0 -29
  179. package/dist/history-FO5PHBQ5.js +0 -128
  180. package/dist/message-delivery-2FIM7QKO.js +0 -32
  181. package/dist/mind-manager-BNCMGYXW.js +0 -28
  182. package/dist/mind-service-AV273WT4.js +0 -34
  183. package/dist/sleep-manager-53DZOWW7.js +0 -32
  184. package/dist/system-chat-NPYFYZVI.js +0 -32
  185. package/dist/up-6I6BHRTO.js +0 -17
  186. package/dist/web-assets/assets/index-Bui7U9Uu.css +0 -1
  187. package/dist/web-assets/assets/index-e36DIo1b.js +0 -73
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ hashSkillDir,
4
+ importSkillFromDir,
5
+ removeSharedSkill,
6
+ sharedSkillsDir
7
+ } from "./chunk-N3DNFPVA.js";
2
8
  import {
3
9
  getAllSites,
4
10
  getPublishedPages,
@@ -9,31 +15,26 @@ import {
9
15
  import {
10
16
  getUser,
11
17
  getUserByUsername
12
- } from "./chunk-TDRYEPH4.js";
18
+ } from "./chunk-BM474GX6.js";
13
19
  import {
14
20
  publish
15
- } from "./chunk-QZANELPX.js";
16
- import {
17
- hashSkillDir,
18
- importSkillFromDir,
19
- sharedSkillsDir
20
- } from "./chunk-D5G5YOPL.js";
21
+ } from "./chunk-XWXBJQBE.js";
21
22
  import {
22
23
  logger_default
23
24
  } from "./chunk-YUIHSKR6.js";
24
25
  import {
25
- getDb,
26
+ readGlobalConfig,
27
+ writeGlobalConfig
28
+ } from "./chunk-6OWJXUAR.js";
29
+ import {
26
30
  mindDir,
27
31
  voluteHome,
28
32
  voluteSystemDir
29
- } from "./chunk-LRCG2JLP.js";
30
- import {
31
- turns
32
- } from "./chunk-RPZZSXV3.js";
33
+ } from "./chunk-N7BLAHNE.js";
33
34
 
34
35
  // src/lib/extensions.ts
35
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
36
- import { dirname, resolve as resolve6 } from "path";
36
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
37
+ import { dirname, resolve as resolve7 } from "path";
37
38
 
38
39
  // packages/extensions/notes/src/index.ts
39
40
  import { resolve } from "path";
@@ -299,7 +300,7 @@ function createCommands() {
299
300
  return {
300
301
  write: {
301
302
  description: "Write a new note",
302
- usage: 'volute notes write "title" "content" [--reply-to author/slug]',
303
+ usage: 'volute notes write "title" ["content"] [--reply-to author/slug] (content can be piped via stdin)',
303
304
  handler: async (args, ctx) => {
304
305
  if (!ctx.db) return { error: "Notes extension requires a database" };
305
306
  const mindName = ctx.mindName;
@@ -307,7 +308,7 @@ function createCommands() {
307
308
  const user = await ctx.getUserByUsername(mindName);
308
309
  if (!user) return { error: `Unknown mind: ${mindName}` };
309
310
  const title = args[0];
310
- const content = args[1];
311
+ const content = args[1] ?? ctx.stdin;
311
312
  if (!title || !content)
312
313
  return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
313
314
  let replyToId;
@@ -381,7 +382,7 @@ Comments (${note.comments.length}):`);
381
382
  },
382
383
  comment: {
383
384
  description: "Comment on a note",
384
- usage: 'volute notes comment <author/slug> "content"',
385
+ usage: 'volute notes comment <author/slug> ["content"] (content can be piped via stdin)',
385
386
  handler: async (args, ctx) => {
386
387
  if (!ctx.db) return { error: "Notes extension requires a database" };
387
388
  const mindName = ctx.mindName;
@@ -389,7 +390,7 @@ Comments (${note.comments.length}):`);
389
390
  const user = await ctx.getUserByUsername(mindName);
390
391
  if (!user) return { error: `Unknown mind: ${mindName}` };
391
392
  const ref = args[0];
392
- const content = args[1];
393
+ const content = args[1] ?? ctx.stdin;
393
394
  if (!ref || !ref.includes("/") || !content) {
394
395
  return { error: 'Usage: volute notes comment <author/slug> "content"' };
395
396
  }
@@ -528,19 +529,16 @@ function createRoutes(ctx) {
528
529
  replyToId = id;
529
530
  }
530
531
  const note = await createNote(db, getUser2, actor.id, body.title, body.content, replyToId);
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
- );
532
+ ctx.publishActivity({
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
+ });
544
542
  return c.json(note, 201);
545
543
  }).get("/:author/:slug", async (c) => {
546
544
  const { author, slug } = c.req.param();
@@ -629,6 +627,7 @@ var src_default = createExtension({
629
627
  version: "0.1.0",
630
628
  description: "Public notes for sharing thoughts, reflections, and ideas",
631
629
  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>',
630
+ color: "yellow",
632
631
  routes: (ctx) => createRoutes(ctx),
633
632
  commands: createCommands(),
634
633
  initDb: initDb2,
@@ -751,7 +750,7 @@ Warning: remote publish failed: ${err.message}`;
751
750
  const allFlag = args.includes("--all");
752
751
  const port = process.env.VOLUTE_DAEMON_PORT || "1618";
753
752
  if (allFlag) {
754
- const { getAllSites: getAllSites2 } = await import("./db-RA45JBFG.js");
753
+ const { getAllSites: getAllSites2 } = await import("./db-PLEDCBHZ.js");
755
754
  const sites = getAllSites2(db);
756
755
  const lines2 = [];
757
756
  for (const site of sites) {
@@ -948,7 +947,7 @@ function createRoutes2(ctx) {
948
947
  var _voluteHome = null;
949
948
  async function getVoluteHome() {
950
949
  if (_voluteHome) return _voluteHome();
951
- const mod = await import("./registry-PJ4S5PHQ.js");
950
+ const mod = await import("./registry-UYV5S6QT.js");
952
951
  _voluteHome = mod.voluteHome;
953
952
  return _voluteHome();
954
953
  }
@@ -1012,6 +1011,7 @@ var src_default2 = createExtension({
1012
1011
  skillsDir: skillsDir2,
1013
1012
  standardSkill: true,
1014
1013
  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>',
1014
+ color: "purple",
1015
1015
  ui: {
1016
1016
  assetsDir: assetsDir2,
1017
1017
  systemSection: {
@@ -1026,121 +1026,438 @@ var src_default2 = createExtension({
1026
1026
  }
1027
1027
  });
1028
1028
 
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);
1044
- try {
1045
- const db = await getDb();
1046
- await db.insert(turns).values({ id: turnId, mind, status: "active" });
1047
- } catch (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;
1051
- }
1052
- return turnId;
1029
+ // packages/extensions/plan/src/index.ts
1030
+ import { resolve as resolve5 } from "path";
1031
+
1032
+ // packages/extensions/plan/src/plans.ts
1033
+ async function startPlan(db, getUser2, userId, title, description) {
1034
+ db.prepare(
1035
+ "UPDATE plans SET status = 'archived', completed_at = datetime('now') WHERE status = 'active'"
1036
+ ).run();
1037
+ const row = db.prepare(
1038
+ `INSERT INTO plans (title, description, set_by)
1039
+ VALUES (?, ?, ?)
1040
+ RETURNING *`
1041
+ ).get(title, description, userId);
1042
+ const user = await getUser2(userId);
1043
+ return {
1044
+ ...row,
1045
+ set_by_username: user?.username ?? "unknown",
1046
+ set_by_display_name: user?.display_name ?? null
1047
+ };
1053
1048
  }
1054
- function getActiveTurnId(mind, session) {
1055
- return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
1049
+ async function getActivePlan(db, getUser2) {
1050
+ const row = db.prepare("SELECT * FROM plans WHERE status = 'active' LIMIT 1").get();
1051
+ if (!row) return null;
1052
+ const user = await getUser2(row.set_by);
1053
+ const logs = db.prepare("SELECT * FROM plan_logs WHERE plan_id = ? ORDER BY created_at DESC LIMIT 20").all(row.id);
1054
+ const messages = db.prepare("SELECT * FROM plan_messages WHERE plan_id = ? ORDER BY id DESC LIMIT 10").all(row.id);
1055
+ const latestMessage = messages.length > 0 ? messages[0].content : null;
1056
+ return {
1057
+ ...row,
1058
+ set_by_username: user?.username ?? "unknown",
1059
+ set_by_display_name: user?.display_name ?? null,
1060
+ logs,
1061
+ messages,
1062
+ latestMessage
1063
+ };
1056
1064
  }
1057
- function trackToolUse(mind, session, eventId) {
1058
- const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
1059
- if (entry) entry.lastToolUseEventId = eventId;
1065
+ function logProgress(db, planId, mindName, content) {
1066
+ return db.prepare(
1067
+ `INSERT INTO plan_logs (plan_id, mind_name, content)
1068
+ VALUES (?, ?, ?)
1069
+ RETURNING *`
1070
+ ).get(planId, mindName, content);
1060
1071
  }
1061
- function getLastToolUseEventId(mind, session) {
1062
- return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
1072
+ function addPlanMessage(db, planId, content) {
1073
+ return db.prepare(
1074
+ `INSERT INTO plan_messages (plan_id, content)
1075
+ VALUES (?, ?)
1076
+ RETURNING *`
1077
+ ).get(planId, content);
1063
1078
  }
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;
1079
+ function finishPlan(db, planId, message) {
1080
+ const result = db.prepare(
1081
+ "UPDATE plans SET status = 'completed', completed_at = datetime('now'), finish_message = ? WHERE id = ? AND status = 'active'"
1082
+ ).run(message ?? null, planId);
1083
+ return result.changes > 0;
1084
+ }
1085
+ async function listPlans(db, getUser2, opts) {
1086
+ const limit = opts?.limit ?? 20;
1087
+ const offset = opts?.offset ?? 0;
1088
+ const rows = opts?.status ? db.prepare("SELECT * FROM plans WHERE status = ? ORDER BY created_at DESC LIMIT ? OFFSET ?").all(opts.status, limit, offset) : db.prepare("SELECT * FROM plans ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
1089
+ const userCache = /* @__PURE__ */ new Map();
1090
+ const result = [];
1091
+ for (const row of rows) {
1092
+ if (!userCache.has(row.set_by)) {
1093
+ const u = await getUser2(row.set_by);
1094
+ userCache.set(row.set_by, {
1095
+ username: u?.username ?? "unknown",
1096
+ display_name: u?.display_name ?? null
1097
+ });
1098
+ }
1099
+ const userInfo = userCache.get(row.set_by);
1100
+ result.push({
1101
+ ...row,
1102
+ set_by_username: userInfo.username,
1103
+ set_by_display_name: userInfo.display_name
1104
+ });
1070
1105
  }
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;
1106
+ return result;
1107
+ }
1108
+
1109
+ // packages/extensions/plan/src/commands.ts
1110
+ function getFlag2(args, flag) {
1111
+ const idx = args.indexOf(flag);
1112
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1];
1113
+ return void 0;
1114
+ }
1115
+ var _announce = null;
1116
+ async function announceToSystem(text) {
1117
+ if (!_announce) {
1118
+ try {
1119
+ const mod = await import("./system-channel-DXD2JBOU.js");
1120
+ _announce = mod.announceToSystem;
1121
+ } catch {
1122
+ return false;
1123
+ }
1077
1124
  }
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
1125
  try {
1087
- const db = await getDb();
1088
- await db.update(turns).set({ status: "complete" }).where(eq(turns.id, entry.turnId));
1126
+ await _announce(text);
1127
+ return true;
1089
1128
  } catch (err) {
1090
- tlog.error(`failed to complete turn ${entry.turnId}`, logger_default.errorData(err));
1091
- return void 0;
1129
+ console.error("[plan] Failed to announce to system channel:", err);
1130
+ return false;
1092
1131
  }
1093
- activeTurns.delete(k);
1094
- activeTurns.delete(wildcardKey);
1095
- return entry.turnId;
1096
1132
  }
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
- }
1133
+ function createCommands3() {
1134
+ return {
1135
+ start: {
1136
+ description: "Start a new system plan",
1137
+ usage: 'volute plan start "title" "description" (description can be piped via stdin)',
1138
+ handler: async (args, ctx) => {
1139
+ if (!ctx.db) return { error: "Plan extension requires a database" };
1140
+ const mindName = ctx.mindName;
1141
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
1142
+ const user = await ctx.getUserByUsername(mindName);
1143
+ if (!user) return { error: `Unknown mind: ${mindName}` };
1144
+ const title = args[0];
1145
+ const description = args[1] ?? ctx.stdin ?? "";
1146
+ if (!title) return { error: 'Usage: volute plan start "title" "description"' };
1147
+ const plan = await startPlan(ctx.db, ctx.getUser, user.id, title, description);
1148
+ ctx.publishActivity({
1149
+ type: "plan_started",
1150
+ mind: user.username,
1151
+ summary: `${user.username} started plan: "${title}"`,
1152
+ metadata: { planId: plan.id, title }
1153
+ });
1154
+ return { output: `Plan started: ${plan.title}` };
1155
+ }
1156
+ },
1157
+ message: {
1158
+ description: "Post a message about the current plan (sent to #system)",
1159
+ usage: `volute plan message "today's focus: ..." (content can be piped via stdin)`,
1160
+ handler: async (args, ctx) => {
1161
+ if (!ctx.db) return { error: "Plan extension requires a database" };
1162
+ const mindName = ctx.mindName;
1163
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
1164
+ const content = args[0] ?? ctx.stdin;
1165
+ if (!content) return { error: 'Usage: volute plan message "your message"' };
1166
+ const plan = await getActivePlan(ctx.db, ctx.getUser);
1167
+ if (!plan) return { error: "No active plan" };
1168
+ const msg = addPlanMessage(ctx.db, plan.id, content);
1169
+ ctx.publishActivity({
1170
+ type: "plan_message",
1171
+ mind: mindName,
1172
+ summary: `Plan message: "${content.slice(0, 100)}"`,
1173
+ metadata: { planId: plan.id, messageId: msg.id }
1174
+ });
1175
+ const announced = await announceToSystem(`[Plan: ${plan.title}] ${content}`);
1176
+ return {
1177
+ output: announced ? "Message posted to #system." : "Message logged (system channel unavailable)."
1178
+ };
1179
+ }
1180
+ },
1181
+ log: {
1182
+ description: "Log progress on the current plan",
1183
+ usage: 'volute plan log "progress update" (content can be piped via stdin)',
1184
+ handler: async (args, ctx) => {
1185
+ if (!ctx.db) return { error: "Plan extension requires a database" };
1186
+ const mindName = ctx.mindName;
1187
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
1188
+ const content = args[0] ?? ctx.stdin;
1189
+ if (!content) return { error: 'Usage: volute plan log "progress update"' };
1190
+ const plan = await getActivePlan(ctx.db, ctx.getUser);
1191
+ if (!plan) return { error: "No active plan" };
1192
+ const log = logProgress(ctx.db, plan.id, mindName, content);
1193
+ ctx.publishActivity({
1194
+ type: "plan_progress",
1195
+ mind: mindName,
1196
+ summary: `${mindName} logged progress: "${content.slice(0, 100)}"`,
1197
+ metadata: { planId: plan.id, logId: log.id }
1198
+ });
1199
+ return { output: "Progress logged." };
1200
+ }
1201
+ },
1202
+ current: {
1203
+ description: "Show the current active plan",
1204
+ usage: "volute plan current",
1205
+ handler: async (_args, ctx) => {
1206
+ if (!ctx.db) return { error: "Plan extension requires a database" };
1207
+ const plan = await getActivePlan(ctx.db, ctx.getUser);
1208
+ if (!plan) return { output: "No active plan." };
1209
+ const lines = [
1210
+ `# ${plan.title}`,
1211
+ "",
1212
+ `Set by ${plan.set_by_username} \u2014 ${new Date(plan.created_at).toLocaleString()}`
1213
+ ];
1214
+ if (plan.description) {
1215
+ lines.push("", plan.description);
1216
+ }
1217
+ if (plan.latestMessage) {
1218
+ lines.push("", `## Latest message`, "", plan.latestMessage);
1219
+ }
1220
+ if (plan.logs.length > 0) {
1221
+ lines.push("", "## Progress");
1222
+ for (const log of plan.logs) {
1223
+ const date = new Date(log.created_at).toLocaleString();
1224
+ lines.push(` ${log.mind_name} (${date}): ${log.content}`);
1225
+ }
1226
+ }
1227
+ return { output: lines.join("\n") };
1228
+ }
1229
+ },
1230
+ history: {
1231
+ description: "List past plans",
1232
+ usage: "volute plan history [--limit N]",
1233
+ handler: async (args, ctx) => {
1234
+ if (!ctx.db) return { error: "Plan extension requires a database" };
1235
+ const limit = parseInt(getFlag2(args, "--limit") ?? "10", 10);
1236
+ const plans = await listPlans(ctx.db, ctx.getUser, { limit });
1237
+ if (plans.length === 0) return { output: "No plans found." };
1238
+ const lines = plans.map((p) => {
1239
+ const date = new Date(p.created_at).toLocaleDateString();
1240
+ const status = p.status === "active" ? " [active]" : "";
1241
+ return ` ${p.title} (${date}, by ${p.set_by_username})${status}`;
1242
+ });
1243
+ return { output: lines.join("\n") };
1244
+ }
1245
+ },
1246
+ finish: {
1247
+ description: "Finish the current plan with a closing message",
1248
+ usage: 'volute plan finish "closing message" (message can be piped via stdin)',
1249
+ handler: async (args, ctx) => {
1250
+ if (!ctx.db) return { error: "Plan extension requires a database" };
1251
+ const mindName = ctx.mindName;
1252
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
1253
+ const plan = await getActivePlan(ctx.db, ctx.getUser);
1254
+ if (!plan) return { error: "No active plan" };
1255
+ const message = args[0] ?? ctx.stdin ?? "";
1256
+ finishPlan(ctx.db, plan.id, message);
1257
+ ctx.publishActivity({
1258
+ type: "plan_finished",
1259
+ mind: mindName,
1260
+ summary: `${mindName} finished plan: "${plan.title}"`,
1261
+ metadata: { planId: plan.id }
1262
+ });
1263
+ const announcement = message ? `[Plan finished: ${plan.title}] ${message}` : `[Plan finished: ${plan.title}]`;
1264
+ const announced = await announceToSystem(announcement);
1265
+ const suffix = announced ? "" : " (system channel unavailable)";
1266
+ return { output: `Finished: ${plan.title}${suffix}` };
1267
+ }
1268
+ }
1269
+ };
1104
1270
  }
1105
- async function completeOrphanedTurns() {
1271
+
1272
+ // packages/extensions/plan/src/db.ts
1273
+ function initDb3(db) {
1274
+ db.exec(`
1275
+ CREATE TABLE IF NOT EXISTS plans (
1276
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1277
+ title TEXT NOT NULL,
1278
+ description TEXT NOT NULL DEFAULT '',
1279
+ status TEXT NOT NULL DEFAULT 'active',
1280
+ set_by INTEGER NOT NULL,
1281
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1282
+ completed_at TEXT,
1283
+ finish_message TEXT
1284
+ );
1285
+ CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
1286
+ CREATE INDEX IF NOT EXISTS idx_plans_created_at ON plans(created_at);
1287
+
1288
+ CREATE TABLE IF NOT EXISTS plan_logs (
1289
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1290
+ plan_id INTEGER NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
1291
+ mind_name TEXT NOT NULL,
1292
+ content TEXT NOT NULL,
1293
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1294
+ );
1295
+ CREATE INDEX IF NOT EXISTS idx_plan_logs_plan_id ON plan_logs(plan_id);
1296
+
1297
+ CREATE TABLE IF NOT EXISTS plan_messages (
1298
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1299
+ plan_id INTEGER NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
1300
+ content TEXT NOT NULL,
1301
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1302
+ );
1303
+ CREATE INDEX IF NOT EXISTS idx_plan_messages_plan_id ON plan_messages(plan_id);
1304
+ `);
1305
+ }
1306
+
1307
+ // packages/extensions/plan/src/routes.ts
1308
+ import { Hono as Hono3 } from "hono";
1309
+ function resolveUserId2(c) {
1310
+ const user = c.get("user");
1311
+ if (!user || user.id === 0) return null;
1312
+ return user;
1313
+ }
1314
+ async function parseJson2(c) {
1106
1315
  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));
1316
+ return await c.req.json();
1317
+ } catch {
1318
+ return null;
1114
1319
  }
1115
1320
  }
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);
1321
+ function createRoutes3(ctx) {
1322
+ if (!ctx.db) throw new Error("Plan extension requires a database");
1323
+ const db = ctx.db;
1324
+ const { getUser: getUser2 } = ctx;
1325
+ const app = new Hono3().get("/current", async (c) => {
1326
+ const plan = await getActivePlan(db, getUser2);
1327
+ if (!plan) return c.json(null);
1328
+ return c.json(plan);
1329
+ }).get("/", async (c) => {
1330
+ const status = c.req.query("status");
1331
+ const rawLimit = c.req.query("limit");
1332
+ const rawOffset = c.req.query("offset");
1333
+ const limit = rawLimit ? parseInt(rawLimit, 10) : void 0;
1334
+ const offset = rawOffset ? parseInt(rawOffset, 10) : void 0;
1335
+ if (limit !== void 0 && Number.isNaN(limit) || offset !== void 0 && Number.isNaN(offset)) {
1336
+ return c.json({ error: "Invalid limit or offset parameter" }, 400);
1123
1337
  }
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));
1338
+ const plans = await listPlans(db, getUser2, { status: status ?? void 0, limit, offset });
1339
+ return c.json(plans);
1340
+ }).post("/", async (c) => {
1341
+ const actor = resolveUserId2(c);
1342
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
1343
+ if (actor.role !== "admin" && actor.user_type !== "mind") {
1344
+ return c.json({ error: "Only spirit or admin can start plans" }, 403);
1134
1345
  }
1135
- }
1346
+ const body = await parseJson2(c);
1347
+ if (!body) return c.json({ error: "Invalid JSON body" }, 400);
1348
+ if (!body.title) return c.json({ error: "title is required" }, 400);
1349
+ const plan = await startPlan(db, getUser2, actor.id, body.title, body.description ?? "");
1350
+ ctx.publishActivity({
1351
+ type: "plan_started",
1352
+ mind: actor.username,
1353
+ summary: `${actor.username} started plan: "${body.title}"`,
1354
+ metadata: { planId: plan.id, title: body.title }
1355
+ });
1356
+ return c.json(plan, 201);
1357
+ }).post("/:id{[0-9]+}/message", async (c) => {
1358
+ const actor = resolveUserId2(c);
1359
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
1360
+ const planId = parseInt(c.req.param("id"), 10);
1361
+ if (Number.isNaN(planId)) return c.json({ error: "Invalid plan ID" }, 400);
1362
+ const plan = db.prepare("SELECT id FROM plans WHERE id = ? AND status = 'active'").get(planId);
1363
+ if (!plan) return c.json({ error: "Active plan not found" }, 404);
1364
+ const body = await parseJson2(c);
1365
+ if (!body) return c.json({ error: "Invalid JSON body" }, 400);
1366
+ if (!body.content) return c.json({ error: "content is required" }, 400);
1367
+ const msg = addPlanMessage(db, planId, body.content);
1368
+ ctx.publishActivity({
1369
+ type: "plan_message",
1370
+ mind: actor.username,
1371
+ summary: `Plan message: "${body.content.slice(0, 100)}"`,
1372
+ metadata: { planId, messageId: msg.id }
1373
+ });
1374
+ return c.json(msg, 201);
1375
+ }).post("/:id{[0-9]+}/log", async (c) => {
1376
+ const actor = resolveUserId2(c);
1377
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
1378
+ const planId = parseInt(c.req.param("id"), 10);
1379
+ if (Number.isNaN(planId)) return c.json({ error: "Invalid plan ID" }, 400);
1380
+ const plan = db.prepare("SELECT id FROM plans WHERE id = ? AND status = 'active'").get(planId);
1381
+ if (!plan) return c.json({ error: "Active plan not found" }, 404);
1382
+ const body = await parseJson2(c);
1383
+ if (!body) return c.json({ error: "Invalid JSON body" }, 400);
1384
+ if (!body.content) return c.json({ error: "content is required" }, 400);
1385
+ const log = logProgress(db, planId, actor.username, body.content);
1386
+ ctx.publishActivity({
1387
+ type: "plan_progress",
1388
+ mind: actor.username,
1389
+ summary: `${actor.username} logged progress: "${body.content.slice(0, 100)}"`,
1390
+ metadata: { planId, logId: log.id }
1391
+ });
1392
+ return c.json(log, 201);
1393
+ }).patch("/:id{[0-9]+}/finish", async (c) => {
1394
+ const actor = resolveUserId2(c);
1395
+ if (!actor) return c.json({ error: "Unauthorized" }, 401);
1396
+ if (actor.role !== "admin" && actor.user_type !== "mind") {
1397
+ return c.json({ error: "Only spirit or admin can finish plans" }, 403);
1398
+ }
1399
+ const planId = parseInt(c.req.param("id"), 10);
1400
+ if (Number.isNaN(planId)) return c.json({ error: "Invalid plan ID" }, 400);
1401
+ const body = await parseJson2(c);
1402
+ const message = body?.message;
1403
+ const ok = finishPlan(db, planId, message);
1404
+ if (!ok) return c.json({ error: "Plan not found" }, 404);
1405
+ ctx.publishActivity({
1406
+ type: "plan_finished",
1407
+ mind: actor.username,
1408
+ summary: `${actor.username} finished a plan`,
1409
+ metadata: { planId }
1410
+ });
1411
+ return c.json({ ok: true });
1412
+ }).get("/feed", async (c) => {
1413
+ const rawLimit = c.req.query("limit");
1414
+ const limit = rawLimit ? parseInt(rawLimit, 10) : 5;
1415
+ if (Number.isNaN(limit)) return c.json({ error: "Invalid limit parameter" }, 400);
1416
+ const plans = await listPlans(db, getUser2, { limit });
1417
+ return c.json(
1418
+ plans.map((p) => ({
1419
+ id: `plan-${p.id}`,
1420
+ title: p.title,
1421
+ url: `/plan`,
1422
+ date: p.created_at,
1423
+ author: p.set_by_username,
1424
+ bodyHtml: p.description || `<em>${p.status}</em>`,
1425
+ 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="M8 4v4l2.5 2.5"/></svg>',
1426
+ color: "blue"
1427
+ }))
1428
+ );
1429
+ });
1430
+ return app;
1136
1431
  }
1137
1432
 
1433
+ // packages/extensions/plan/src/index.ts
1434
+ var assetsDir3 = resolve5(import.meta.dirname, "../dist/ui");
1435
+ var skillsDir3 = resolve5(import.meta.dirname, "../skills");
1436
+ var src_default3 = createExtension({
1437
+ id: "plan",
1438
+ name: "Plan",
1439
+ version: "0.1.0",
1440
+ description: "System-wide plans for coordinated mind activity",
1441
+ 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="M8 4v4l2.5 2.5"/></svg>',
1442
+ color: "blue",
1443
+ routes: (ctx) => createRoutes3(ctx),
1444
+ commands: createCommands3(),
1445
+ initDb: initDb3,
1446
+ skillsDir: skillsDir3,
1447
+ standardSkill: true,
1448
+ ui: {
1449
+ assetsDir: assetsDir3,
1450
+ systemSection: { id: "plan", label: "Plan", urlPatterns: ["/plan"] },
1451
+ feedSource: { endpoint: "/api/ext/plan/feed" }
1452
+ }
1453
+ });
1454
+
1138
1455
  // src/lib/systems-config.ts
1139
1456
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
1140
- import { resolve as resolve5 } from "path";
1457
+ import { resolve as resolve6 } from "path";
1141
1458
  var DEFAULT_API_URL = "https://volute.systems";
1142
1459
  function configPath() {
1143
- return resolve5(voluteSystemDir(), "systems.json");
1460
+ return resolve6(voluteSystemDir(), "systems.json");
1144
1461
  }
1145
1462
  function readSystemsConfig() {
1146
1463
  const path = configPath();
@@ -1180,14 +1497,15 @@ function deleteSystemsConfig() {
1180
1497
  // src/lib/extensions.ts
1181
1498
  var VALID_EXTENSION_ID2 = /^[a-z0-9][a-z0-9_-]*$/;
1182
1499
  var loaded = [];
1500
+ var discovered = [];
1183
1501
  function extensionsBaseDir() {
1184
- return resolve6(voluteHome(), "extensions");
1502
+ return resolve7(voluteHome(), "extensions");
1185
1503
  }
1186
1504
  function extensionDataDir(id) {
1187
- return resolve6(voluteSystemDir(), "extension-data", id);
1505
+ return resolve7(voluteSystemDir(), "extension-data", id);
1188
1506
  }
1189
1507
  function extensionsConfigPath() {
1190
- return resolve6(voluteHome(), "system", "extensions.json");
1508
+ return resolve7(voluteHome(), "system", "extensions.json");
1191
1509
  }
1192
1510
  function readExtensionsConfig() {
1193
1511
  const configPath2 = extensionsConfigPath();
@@ -1211,7 +1529,7 @@ async function getLibsqlDatabase() {
1211
1529
  return _LibsqlDatabase;
1212
1530
  }
1213
1531
  async function openExtensionDb(_id, dataDir) {
1214
- const dbPath = resolve6(dataDir, "data.db");
1532
+ const dbPath = resolve7(dataDir, "data.db");
1215
1533
  const Database = await getLibsqlDatabase();
1216
1534
  return new Database(dbPath);
1217
1535
  }
@@ -1237,15 +1555,16 @@ async function buildContext(manifest, dataDir, authMw) {
1237
1555
  },
1238
1556
  getUser: async (id) => getUser(id),
1239
1557
  getUserByUsername: async (username) => getUserByUsername(username),
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({
1558
+ publishActivity: (event) => {
1559
+ const enriched = {
1245
1560
  ...event,
1246
- turn_id: turnId,
1247
- source_event_id: sourceEventId
1248
- }).catch(
1561
+ metadata: {
1562
+ ...event.metadata,
1563
+ ...manifest.icon && !event.metadata?.icon ? { icon: manifest.icon } : {},
1564
+ ...manifest.color && !event.metadata?.color ? { color: manifest.color } : {}
1565
+ }
1566
+ };
1567
+ publish(enriched).catch(
1249
1568
  (err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
1250
1569
  );
1251
1570
  },
@@ -1295,15 +1614,36 @@ async function loadExtension(manifest, app, authMw) {
1295
1614
  const mindName = body.mind || user?.username;
1296
1615
  const session = c.get("mindSession");
1297
1616
  try {
1617
+ const activityPromises = [];
1298
1618
  const result = await cmd.handler(body.args ?? [], {
1299
1619
  ...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),
1620
+ publishActivity: (rawEvent) => {
1621
+ const event = {
1622
+ ...rawEvent,
1623
+ metadata: {
1624
+ ...rawEvent.metadata,
1625
+ ...manifest.icon && !rawEvent.metadata?.icon ? { icon: manifest.icon } : {},
1626
+ ...manifest.color && !rawEvent.metadata?.color ? { color: manifest.color } : {}
1627
+ }
1628
+ };
1629
+ activityPromises.push(
1630
+ publish(event).catch((err) => {
1631
+ logger_default.error(
1632
+ `extension ${manifest.id}: failed to publish activity`,
1633
+ logger_default.errorData(err)
1634
+ );
1635
+ return 0;
1636
+ })
1637
+ );
1638
+ },
1303
1639
  mindName,
1304
- session
1640
+ session,
1641
+ stdin: body.stdin
1305
1642
  });
1306
- return c.json(result);
1643
+ const activityIds = (await Promise.all(activityPromises)).filter((id) => id > 0);
1644
+ const markers = activityIds.map((id) => `[volute:activity:${id}]`).join("");
1645
+ const output = result && typeof result === "object" && "output" in result ? { ...result, output: `${result.output}${markers}` } : markers ? { ...result, output: markers } : result;
1646
+ return c.json(output);
1307
1647
  } catch (err) {
1308
1648
  logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
1309
1649
  return c.json({ error: err.message }, 500);
@@ -1315,7 +1655,7 @@ async function loadExtension(manifest, app, authMw) {
1315
1655
  if (resolvedAssetsDir && !existsSync3(resolvedAssetsDir)) {
1316
1656
  let searchDir = dirname(new URL(import.meta.url).pathname);
1317
1657
  for (let i = 0; i < 5; i++) {
1318
- const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
1658
+ const candidate = resolve7(searchDir, "packages", "extensions", manifest.id, "dist", "ui");
1319
1659
  if (existsSync3(candidate)) {
1320
1660
  resolvedAssetsDir = candidate;
1321
1661
  break;
@@ -1324,7 +1664,7 @@ async function loadExtension(manifest, app, authMw) {
1324
1664
  }
1325
1665
  }
1326
1666
  if (resolvedAssetsDir && existsSync3(resolvedAssetsDir)) {
1327
- const assetsDir3 = resolvedAssetsDir;
1667
+ const assetsDir4 = resolvedAssetsDir;
1328
1668
  const { readFile: readFile2, stat: fsStat } = await import("fs/promises");
1329
1669
  const { extname: ext } = await import("path");
1330
1670
  const mimeTypes = {
@@ -1340,12 +1680,12 @@ async function loadExtension(manifest, app, authMw) {
1340
1680
  ".woff2": "font/woff2"
1341
1681
  };
1342
1682
  const prefix = `/ext/${manifest.id}`;
1343
- const indexPath = resolve6(assetsDir3, "index.html");
1683
+ const indexPath = resolve7(assetsDir4, "index.html");
1344
1684
  const serveExtAssets = async (c) => {
1345
1685
  const urlPath = new URL(c.req.url).pathname;
1346
1686
  const relativePath = urlPath.slice(prefix.length).replace(/^\//, "") || "index.html";
1347
- const filePath = resolve6(assetsDir3, relativePath);
1348
- if (filePath !== assetsDir3 && !filePath.startsWith(assetsDir3 + "/"))
1687
+ const filePath = resolve7(assetsDir4, relativePath);
1688
+ if (filePath !== assetsDir4 && !filePath.startsWith(assetsDir4 + "/"))
1349
1689
  return c.text("Forbidden", 403);
1350
1690
  const s = await fsStat(filePath).catch(() => null);
1351
1691
  if (s?.isFile()) {
@@ -1362,11 +1702,11 @@ async function loadExtension(manifest, app, authMw) {
1362
1702
  app.get(`${prefix}/*`, serveExtAssets);
1363
1703
  app.get(prefix, serveExtAssets);
1364
1704
  }
1365
- const skillsDir3 = resolveSkillsDir(manifest);
1366
- if (skillsDir3) {
1705
+ const skillsDir4 = resolveSkillsDir(manifest);
1706
+ if (skillsDir4) {
1367
1707
  let entries;
1368
1708
  try {
1369
- entries = readdirSync2(skillsDir3, { withFileTypes: true });
1709
+ entries = readdirSync2(skillsDir4, { withFileTypes: true });
1370
1710
  } catch (err) {
1371
1711
  logger_default.error(`failed to read skills dir for extension ${manifest.id}`, logger_default.errorData(err));
1372
1712
  entries = [];
@@ -1374,9 +1714,9 @@ async function loadExtension(manifest, app, authMw) {
1374
1714
  for (const entry of entries) {
1375
1715
  if (!entry.isDirectory()) continue;
1376
1716
  try {
1377
- const skillPath = resolve6(skillsDir3, entry.name);
1717
+ const skillPath = resolve7(skillsDir4, entry.name);
1378
1718
  const sourceHash = hashSkillDir(skillPath);
1379
- const destDir = resolve6(sharedSkillsDir(), entry.name);
1719
+ const destDir = resolve7(sharedSkillsDir(), entry.name);
1380
1720
  if (existsSync3(destDir)) {
1381
1721
  const destHash = hashSkillDir(destDir);
1382
1722
  if (sourceHash === destHash) continue;
@@ -1401,7 +1741,7 @@ function resolveSkillsDir(manifest) {
1401
1741
  if (!manifest.skillsDir) return null;
1402
1742
  let searchDir = dirname(new URL(import.meta.url).pathname);
1403
1743
  for (let i = 0; i < 5; i++) {
1404
- const candidate = resolve6(searchDir, "packages", "extensions", manifest.id, "skills");
1744
+ const candidate = resolve7(searchDir, "packages", "extensions", manifest.id, "skills");
1405
1745
  if (existsSync3(candidate)) return candidate;
1406
1746
  searchDir = dirname(searchDir);
1407
1747
  }
@@ -1410,30 +1750,30 @@ function resolveSkillsDir(manifest) {
1410
1750
  return null;
1411
1751
  }
1412
1752
  function discoverBuiltinExtensions() {
1413
- return [src_default, src_default2];
1753
+ return [src_default, src_default2, src_default3];
1414
1754
  }
1415
1755
  async function discoverInstalledExtensions() {
1416
- const manifests = [];
1756
+ const results = [];
1417
1757
  const packages = readExtensionsConfig();
1418
- const npmDir = resolve6(voluteHome(), "extensions", "_npm");
1758
+ const npmDir = resolve7(voluteHome(), "extensions", "_npm");
1419
1759
  const { createRequire } = await import("module");
1420
1760
  for (const pkg of packages) {
1421
1761
  try {
1422
1762
  let resolved = pkg;
1423
- const npmPkgDir = resolve6(npmDir, "node_modules", pkg);
1763
+ const npmPkgDir = resolve7(npmDir, "node_modules", pkg);
1424
1764
  if (existsSync3(npmPkgDir)) {
1425
- const require2 = createRequire(resolve6(npmDir, "noop.js"));
1765
+ const require2 = createRequire(resolve7(npmDir, "noop.js"));
1426
1766
  resolved = require2.resolve(pkg);
1427
1767
  }
1428
1768
  const mod = await import(resolved);
1429
1769
  const manifest = mod.default ?? mod.extension ?? mod;
1430
1770
  if (!validateManifest(manifest, `package ${pkg}`)) continue;
1431
- manifests.push(manifest);
1771
+ results.push({ manifest, package: pkg });
1432
1772
  } catch (err) {
1433
1773
  logger_default.error(`failed to load extension package: ${pkg}`, logger_default.errorData(err));
1434
1774
  }
1435
1775
  }
1436
- return manifests;
1776
+ return results;
1437
1777
  }
1438
1778
  function validateManifest(manifest, source) {
1439
1779
  if (!manifest || typeof manifest !== "object") {
@@ -1475,8 +1815,8 @@ async function discoverLocalExtensions() {
1475
1815
  return [];
1476
1816
  }
1477
1817
  for (const dir of entries) {
1478
- const extDir = resolve6(baseDir, dir);
1479
- const candidates = [resolve6(extDir, "src", "index.js"), resolve6(extDir, "index.js")];
1818
+ const extDir = resolve7(baseDir, dir);
1819
+ const candidates = [resolve7(extDir, "src", "index.js"), resolve7(extDir, "index.js")];
1480
1820
  const entryPoint = candidates.find((p) => existsSync3(p));
1481
1821
  if (!entryPoint) continue;
1482
1822
  try {
@@ -1495,14 +1835,25 @@ async function loadAllExtensions(app, authMw) {
1495
1835
  const builtins = discoverBuiltinExtensions();
1496
1836
  const installed = await discoverInstalledExtensions();
1497
1837
  const local = await discoverLocalExtensions();
1498
- const all = [...builtins, ...installed, ...local];
1838
+ const disabledIds = new Set(readGlobalConfig().disabledExtensions ?? []);
1839
+ const all = [
1840
+ ...builtins.map((m) => ({ manifest: m, source: "builtin" })),
1841
+ ...installed.map((i) => ({ manifest: i.manifest, source: "npm", package: i.package })),
1842
+ ...local.map((m) => ({ manifest: m, source: "local" }))
1843
+ ];
1499
1844
  const seen = /* @__PURE__ */ new Set();
1500
- for (const manifest of all) {
1845
+ for (const entry of all) {
1846
+ const { manifest } = entry;
1501
1847
  if (seen.has(manifest.id)) {
1502
1848
  logger_default.warn(`duplicate extension ID: ${manifest.id}, skipping`);
1503
1849
  continue;
1504
1850
  }
1505
1851
  seen.add(manifest.id);
1852
+ discovered.push(entry);
1853
+ if (disabledIds.has(manifest.id)) {
1854
+ logger_default.info(`extension disabled, skipping: ${manifest.id}`);
1855
+ continue;
1856
+ }
1506
1857
  try {
1507
1858
  await loadExtension(manifest, app, authMw);
1508
1859
  } catch (err) {
@@ -1547,6 +1898,114 @@ function getLoadedExtensions() {
1547
1898
  };
1548
1899
  });
1549
1900
  }
1901
+ function getAllDiscoveredExtensions() {
1902
+ const disabledIds = new Set(readGlobalConfig().disabledExtensions ?? []);
1903
+ return discovered.map((d) => ({
1904
+ id: d.manifest.id,
1905
+ name: d.manifest.name,
1906
+ version: d.manifest.version,
1907
+ description: d.manifest.description,
1908
+ icon: d.manifest.icon,
1909
+ source: d.source,
1910
+ enabled: !disabledIds.has(d.manifest.id),
1911
+ package: d.package
1912
+ }));
1913
+ }
1914
+ function setExtensionEnabled(id, enabled) {
1915
+ if (!discovered.find((d) => d.manifest.id === id)) {
1916
+ throw new Error(`Extension "${id}" not found`);
1917
+ }
1918
+ const config = readGlobalConfig();
1919
+ const disabled = new Set(config.disabledExtensions ?? []);
1920
+ if (enabled) {
1921
+ disabled.delete(id);
1922
+ } else {
1923
+ disabled.add(id);
1924
+ }
1925
+ config.disabledExtensions = disabled.size > 0 ? [...disabled] : void 0;
1926
+ writeGlobalConfig(config);
1927
+ }
1928
+ function extensionsNpmDir() {
1929
+ return resolve7(voluteHome(), "extensions", "_npm");
1930
+ }
1931
+ function ensureExtensionsNpmDir() {
1932
+ const dir = extensionsNpmDir();
1933
+ mkdirSync2(dir, { recursive: true });
1934
+ const pkgPath = resolve7(dir, "package.json");
1935
+ if (!existsSync3(pkgPath)) {
1936
+ writeFileSync2(pkgPath, '{"private":true,"dependencies":{}}\n');
1937
+ }
1938
+ return dir;
1939
+ }
1940
+ function writeExtensionsConfig(packages) {
1941
+ const configPath2 = extensionsConfigPath();
1942
+ mkdirSync2(resolve7(configPath2, ".."), { recursive: true });
1943
+ writeFileSync2(configPath2, `${JSON.stringify(packages, null, 2)}
1944
+ `);
1945
+ }
1946
+ var VALID_NPM_PACKAGE = /^(@[a-z0-9-~][a-z0-9._-~]*\/)?[a-z0-9-~][a-z0-9._-~]*(@[^\s]+)?$/;
1947
+ async function installNpmExtension(pkg) {
1948
+ if (!VALID_NPM_PACKAGE.test(pkg)) {
1949
+ throw new Error(`Invalid package name: "${pkg}"`);
1950
+ }
1951
+ const packages = readExtensionsConfig();
1952
+ if (packages.includes(pkg)) {
1953
+ throw new Error(`Extension "${pkg}" is already installed`);
1954
+ }
1955
+ const dir = ensureExtensionsNpmDir();
1956
+ const { exec } = await import("./exec-DVLXKRIO.js");
1957
+ try {
1958
+ await exec("npm", ["install", pkg], { cwd: dir });
1959
+ } catch (err) {
1960
+ logger_default.error(`npm install failed for "${pkg}"`, logger_default.errorData(err));
1961
+ throw new Error(`Failed to install "${pkg}". Check daemon logs for details.`);
1962
+ }
1963
+ packages.push(pkg);
1964
+ writeExtensionsConfig(packages);
1965
+ logger_default.info(`installed extension package: ${pkg}`);
1966
+ }
1967
+ async function uninstallNpmExtension(pkg) {
1968
+ const packages = readExtensionsConfig();
1969
+ const idx = packages.indexOf(pkg);
1970
+ if (idx === -1) {
1971
+ throw new Error(`Extension "${pkg}" is not installed`);
1972
+ }
1973
+ await cleanupExtensionSkills(pkg);
1974
+ packages.splice(idx, 1);
1975
+ writeExtensionsConfig(packages);
1976
+ try {
1977
+ const { exec } = await import("./exec-DVLXKRIO.js");
1978
+ await exec("npm", ["uninstall", pkg], { cwd: extensionsNpmDir() });
1979
+ } catch (err) {
1980
+ logger_default.warn(
1981
+ `npm uninstall failed for "${pkg}" (may have been manually removed)`,
1982
+ logger_default.errorData(err)
1983
+ );
1984
+ }
1985
+ logger_default.info(`uninstalled extension package: ${pkg}`);
1986
+ }
1987
+ async function cleanupExtensionSkills(pkg) {
1988
+ try {
1989
+ const pkgDir = resolve7(extensionsNpmDir(), "node_modules", pkg);
1990
+ if (!existsSync3(pkgDir)) return;
1991
+ const { createRequire } = await import("module");
1992
+ const require2 = createRequire(resolve7(extensionsNpmDir(), "noop.js"));
1993
+ const mod = require2(pkg);
1994
+ const manifest = mod.default ?? mod.extension ?? mod;
1995
+ if (!manifest?.skillsDir || !existsSync3(manifest.skillsDir)) return;
1996
+ const skillDirs = readdirSync2(manifest.skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1997
+ for (const skillId of skillDirs) {
1998
+ try {
1999
+ await removeSharedSkill(skillId);
2000
+ logger_default.info(`removed skill "${skillId}" from extension ${pkg}`);
2001
+ } catch (err) {
2002
+ logger_default.warn(`failed to remove skill "${skillId}" for extension ${pkg}`, logger_default.errorData(err));
2003
+ }
2004
+ }
2005
+ } catch (err) {
2006
+ logger_default.warn(`could not clean up skills for "${pkg}"`, logger_default.errorData(err));
2007
+ }
2008
+ }
1550
2009
  function getExtensionStandardSkills() {
1551
2010
  const skills = [];
1552
2011
  for (const { manifest } of loaded) {
@@ -1586,6 +2045,7 @@ function notifyExtensionsDaemonStop() {
1586
2045
  }
1587
2046
  }
1588
2047
  loaded.length = 0;
2048
+ discovered.length = 0;
1589
2049
  }
1590
2050
  function notifyExtensionsMindStart(mindName) {
1591
2051
  for (const { manifest } of loaded) {
@@ -1607,20 +2067,15 @@ function notifyExtensionsMindStop(mindName) {
1607
2067
  }
1608
2068
 
1609
2069
  export {
1610
- createTurn,
1611
- getActiveTurnId,
1612
- trackToolUse,
1613
- getLastToolUseEventId,
1614
- assignSession,
1615
- completeTurn,
1616
- setSummaryEventId,
1617
- completeOrphanedTurns,
1618
- clearMind,
1619
2070
  readSystemsConfig,
1620
2071
  writeSystemsConfig,
1621
2072
  deleteSystemsConfig,
1622
2073
  loadAllExtensions,
1623
2074
  getLoadedExtensions,
2075
+ getAllDiscoveredExtensions,
2076
+ setExtensionEnabled,
2077
+ installNpmExtension,
2078
+ uninstallNpmExtension,
1624
2079
  getExtensionStandardSkills,
1625
2080
  notifyExtensionsDaemonStart,
1626
2081
  notifyExtensionsDaemonStop,