volute 0.20.0 → 0.21.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 (92) hide show
  1. package/README.md +7 -7
  2. package/dist/{activity-events-OMXKXD5N.js → activity-events-3WHHCOBB.js} +3 -4
  3. package/dist/{archive-ZCFOSTKB.js → archive-4ZQYK5MN.js} +4 -2
  4. package/dist/auth-HM2RSPY7.js +37 -0
  5. package/dist/{channel-PUQKGSQM.js → channel-BOOMFULW.js} +2 -2
  6. package/dist/{chunk-IKMY5X76.js → chunk-5462YKWP.js} +12 -9
  7. package/dist/{chunk-PUVXOZ6T.js → chunk-7LPTHFIL.js} +63 -64
  8. package/dist/{chunk-UU7A7KLB.js → chunk-A4S7H6G6.js} +5 -7
  9. package/dist/chunk-AKPFNL7L.js +148 -0
  10. package/dist/{chunk-EBGCNDMM.js → chunk-B2CPS4QU.js} +128 -114
  11. package/dist/{chunk-FCDU5BFX.js → chunk-HFCBO2GL.js} +2 -2
  12. package/dist/{chunk-GZ7DW4YL.js → chunk-HGCDWKSP.js} +2 -2
  13. package/dist/{chunk-DYZGP3EW.js → chunk-IPJXU366.js} +1 -1
  14. package/dist/{chunk-7UFKREVW.js → chunk-J5A3DF2U.js} +2 -2
  15. package/dist/{chunk-WC6ZHVRL.js → chunk-KFI7TQJ6.js} +2 -2
  16. package/dist/{chunk-AW7P4EVV.js → chunk-KTJGZ7M7.js} +55 -7
  17. package/dist/{chunk-TIWH32HP.js → chunk-L3LHXZD7.js} +3 -3
  18. package/dist/{chunk-OGXOMR65.js → chunk-NWPT4ASZ.js} +1 -1
  19. package/dist/{chunk-FGSYHIS3.js → chunk-OGZYB5GL.js} +252 -296
  20. package/dist/{chunk-SCUDS4US.js → chunk-ON3FF5JA.js} +1 -1
  21. package/dist/{chunk-O6ASDHFO.js → chunk-PC6R6UUW.js} +4 -4
  22. package/dist/{chunk-VDWCHYTS.js → chunk-PHU4DEAJ.js} +1 -1
  23. package/dist/{chunk-7NO7EV5Z.js → chunk-Q7AITQ44.js} +2 -2
  24. package/dist/{chunk-32VR2EOH.js → chunk-QUJUKM4U.js} +2 -2
  25. package/dist/{chunk-NSE7VJQA.js → chunk-SGPEZ32F.js} +29 -1
  26. package/dist/{chunk-RHEGSQFJ.js → chunk-WSLPZF72.js} +1 -1
  27. package/dist/cli.js +57 -119
  28. package/dist/{connector-JBVNZ7VK.js → connector-PYT5UOTZ.js} +6 -6
  29. package/dist/connectors/discord.js +2 -2
  30. package/dist/connectors/slack.js +2 -2
  31. package/dist/connectors/telegram.js +2 -2
  32. package/dist/{create-HP4OVVHF.js → create-WIDA3M4C.js} +1 -1
  33. package/dist/{daemon-client-ITWUCNFO.js → daemon-client-ZHCDL4RS.js} +2 -2
  34. package/dist/{daemon-restart-KPSWNYTH.js → daemon-restart-BH67ZOTE.js} +6 -6
  35. package/dist/daemon.js +1538 -687
  36. package/dist/{delete-BSU7K3RY.js → delete-LOIANQGD.js} +1 -1
  37. package/dist/down-LIOQ5JDH.js +14 -0
  38. package/dist/{env-A3LMO777.js → env-4PHIHTF4.js} +2 -2
  39. package/dist/{export-6QBUOQGC.js → export-XD6PJBQP.js} +19 -8
  40. package/dist/{file-C57SK5DK.js → file-X4L5TTOL.js} +2 -2
  41. package/dist/{history-WNK3DFUM.js → history-HTEKRNID.js} +2 -2
  42. package/dist/{import-XEC34Y4Z.js → import-E433B4KG.js} +3 -3
  43. package/dist/{log-PPPZDVEF.js → log-SRO5Q6AD.js} +2 -2
  44. package/dist/{login-HNH3EUQV.js → login-UO6AOVEA.js} +4 -4
  45. package/dist/{logout-I5CB5UZS.js → logout-UKD5LA37.js} +2 -2
  46. package/dist/{logs-SF2IMJN4.js → logs-HNTNNBDW.js} +2 -2
  47. package/dist/{merge-33C237A4.js → merge-B6SYTGI7.js} +2 -2
  48. package/dist/{mind-Z7CKD6DG.js → mind-BIDOF65R.js} +27 -11
  49. package/dist/{mind-activity-tracker-624QLQLC.js → mind-activity-tracker-PGC3DBJ7.js} +4 -5
  50. package/dist/{mind-manager-3DMYKZPB.js → mind-manager-3V2NXX4I.js} +5 -6
  51. package/dist/{package-4NHAVUUI.js → package-HQR52XSG.js} +1 -1
  52. package/dist/{pages-4DGQT7ZA.js → pages-KQBR5TAZ.js} +6 -6
  53. package/dist/{publish-TAJUET4I.js → publish-OJ4QMXVZ.js} +6 -6
  54. package/dist/{pull-XAEWQJ47.js → pull-GRQAXM2E.js} +2 -2
  55. package/dist/{register-VSPCMHKX.js → register-U2UO6TC4.js} +5 -5
  56. package/dist/registry-D2BSQ2X5.js +42 -0
  57. package/dist/{restart-IQKMCK5M.js → restart-CIDAKGG2.js} +3 -6
  58. package/dist/{schedule-FFZG23IW.js → schedule-NLR3LZLY.js} +2 -2
  59. package/dist/{seed-J43YDKXG.js → seed-3H2MRREW.js} +2 -2
  60. package/dist/{send-KVIZIGCE.js → send-RP2TA7SG.js} +132 -36
  61. package/dist/{service-LUR7WDO7.js → service-TVNEORO7.js} +31 -13
  62. package/dist/{setup-52YRV7VP.js → setup-OZDYCKDI.js} +9 -34
  63. package/dist/{shared-KO35ZM44.js → shared-DCQ2UXOM.js} +4 -4
  64. package/dist/{skill-BCVNI6TV.js → skill-Q2Y6PQ3L.js} +2 -2
  65. package/dist/skills/orientation/SKILL.md +2 -2
  66. package/dist/skills/volute-mind/SKILL.md +5 -5
  67. package/dist/{sprout-QN7Y4VVO.js → sprout-6Z6C42YM.js} +34 -30
  68. package/dist/{start-I5JYB65M.js → start-JR6CUUWF.js} +3 -6
  69. package/dist/{status-D7E5HHBV.js → status-5XDGYHKP.js} +2 -2
  70. package/dist/{status-FU2PFVVF.js → status-LV34BG6G.js} +3 -3
  71. package/dist/{status-4ESFLGH4.js → status-Z7NAFMBI.js} +5 -5
  72. package/dist/{stop-NBVKEFQQ.js → stop-VKPGK25U.js} +2 -5
  73. package/dist/template-hash-BIMA4ILT.js +8 -0
  74. package/dist/{up-FS7CKM6V.js → up-7BGDMFRT.js} +5 -5
  75. package/dist/{update-FJIHDJKM.js → update-4WT7VWHW.js} +5 -5
  76. package/dist/{update-check-MWE5AH4U.js → update-check-F5Z3ALXX.js} +2 -2
  77. package/dist/{upgrade-AIT24B5I.js → upgrade-ZEC2GGFO.js} +1 -1
  78. package/dist/{variant-63ZWO2W7.js → variant-A4I7PHXS.js} +16 -24
  79. package/dist/version-notify-TFS2U5CF.js +173 -0
  80. package/dist/web-assets/assets/index-BR3gtK3E.css +1 -0
  81. package/dist/web-assets/assets/index-CWmrZRQd.js +64 -0
  82. package/dist/web-assets/index.html +2 -2
  83. package/package.json +1 -1
  84. package/dist/chunk-5XNT2472.js +0 -36
  85. package/dist/chunk-UJ6GHNR7.js +0 -675
  86. package/dist/db-C2CJ46ZU.js +0 -10
  87. package/dist/delivery-manager-CSG7LXA4.js +0 -16
  88. package/dist/down-ZY35KMHR.js +0 -14
  89. package/dist/schema-GFH6RV3W.js +0 -26
  90. package/dist/variants-JAGWGBXG.js +0 -26
  91. package/dist/web-assets/assets/index-CUZTZzaW.js +0 -64
  92. package/dist/web-assets/assets/index-adVuCkqy.css +0 -1
package/dist/daemon.js CHANGED
@@ -1,20 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- SEED_SKILLS,
4
- STANDARD_SKILLS,
5
- getSharedSkill,
6
- importSkillFromDir,
7
- installSkill,
8
- listFilesRecursive,
9
- listMindSkills,
10
- listSharedSkills,
11
- publishSkill,
12
- removeSharedSkill,
13
- sharedSkillsDir,
14
- syncBuiltinSkills,
15
- uninstallSkill,
16
- updateSkill
17
- } from "./chunk-IKMY5X76.js";
18
2
  import {
19
3
  addSharedWorktree,
20
4
  ensureSharedRepo,
@@ -23,16 +7,10 @@ import {
23
7
  sharedMerge,
24
8
  sharedPull,
25
9
  sharedStatus
26
- } from "./chunk-TIWH32HP.js";
10
+ } from "./chunk-L3LHXZD7.js";
27
11
  import {
28
12
  readSystemsConfig
29
- } from "./chunk-FCDU5BFX.js";
30
- import {
31
- getActiveMinds,
32
- markIdle,
33
- onMindEvent,
34
- stopAll
35
- } from "./chunk-GZ7DW4YL.js";
13
+ } from "./chunk-HFCBO2GL.js";
36
14
  import {
37
15
  PROMPT_DEFAULTS,
38
16
  PROMPT_KEYS,
@@ -47,7 +25,7 @@ import {
47
25
  loadJsonMap,
48
26
  saveJsonMap,
49
27
  substitute
50
- } from "./chunk-PUVXOZ6T.js";
28
+ } from "./chunk-7LPTHFIL.js";
51
29
  import {
52
30
  deliverMessage,
53
31
  extractTextContent,
@@ -57,20 +35,26 @@ import {
57
35
  publish,
58
36
  publishTypingForChannels,
59
37
  subscribe
60
- } from "./chunk-FGSYHIS3.js";
38
+ } from "./chunk-OGZYB5GL.js";
39
+ import {
40
+ applyInitFiles,
41
+ composeTemplate,
42
+ computeTemplateHash,
43
+ copyTemplateToDir,
44
+ findTemplatesRoot,
45
+ listFiles
46
+ } from "./chunk-AKPFNL7L.js";
47
+ import {
48
+ getActiveMinds,
49
+ markIdle,
50
+ onMindEvent,
51
+ stopAll
52
+ } from "./chunk-HGCDWKSP.js";
61
53
  import {
62
54
  broadcast,
63
55
  publish as publish2,
64
56
  subscribe as subscribe2
65
- } from "./chunk-UU7A7KLB.js";
66
- import {
67
- logBuffer,
68
- logger_default
69
- } from "./chunk-YUIHSKR6.js";
70
- import {
71
- CHANNELS,
72
- getChannelDriver
73
- } from "./chunk-UJ6GHNR7.js";
57
+ } from "./chunk-A4S7H6G6.js";
74
58
  import {
75
59
  findOpenClawSession,
76
60
  importOpenClawConnectors,
@@ -78,33 +62,54 @@ import {
78
62
  parseNameFromIdentity,
79
63
  readVoluteConfig,
80
64
  writeVoluteConfig
81
- } from "./chunk-O6ASDHFO.js";
65
+ } from "./chunk-PC6R6UUW.js";
82
66
  import {
83
67
  loadMergedEnv,
84
68
  mindEnvPath,
85
69
  readEnv,
86
70
  sharedEnvPath,
87
71
  writeEnv
88
- } from "./chunk-VDWCHYTS.js";
72
+ } from "./chunk-PHU4DEAJ.js";
73
+ import {
74
+ isHomeOnlyArchive
75
+ } from "./chunk-KTJGZ7M7.js";
89
76
  import {
90
- getDb
91
- } from "./chunk-5XNT2472.js";
77
+ SEED_SKILLS,
78
+ STANDARD_SKILLS,
79
+ getSharedSkill,
80
+ importSkillFromDir,
81
+ installSkill,
82
+ listFilesRecursive,
83
+ listMindSkills,
84
+ listSharedSkills,
85
+ publishSkill,
86
+ removeSharedSkill,
87
+ sharedSkillsDir,
88
+ syncBuiltinSkills,
89
+ uninstallSkill,
90
+ updateSkill
91
+ } from "./chunk-5462YKWP.js";
92
92
  import {
93
93
  activity,
94
94
  conversationParticipants,
95
95
  conversations,
96
+ getDb,
96
97
  messages,
97
98
  mindHistory,
98
99
  sessions,
99
100
  systemPrompts,
100
101
  users
101
- } from "./chunk-NSE7VJQA.js";
102
+ } from "./chunk-SGPEZ32F.js";
103
+ import {
104
+ logBuffer,
105
+ logger_default
106
+ } from "./chunk-YUIHSKR6.js";
102
107
  import "./chunk-D424ZQGI.js";
103
108
  import {
104
109
  exec,
105
110
  gitExec,
106
111
  resolveVoluteBin
107
- } from "./chunk-DYZGP3EW.js";
112
+ } from "./chunk-IPJXU366.js";
108
113
  import {
109
114
  chownMindDir,
110
115
  createMindUser,
@@ -112,16 +117,19 @@ import {
112
117
  ensureVoluteGroup,
113
118
  isIsolationEnabled,
114
119
  wrapForIsolation
115
- } from "./chunk-OGXOMR65.js";
120
+ } from "./chunk-NWPT4ASZ.js";
116
121
  import {
117
122
  checkForUpdate,
118
123
  checkForUpdateCached,
119
124
  getCurrentVersion
120
- } from "./chunk-SCUDS4US.js";
125
+ } from "./chunk-ON3FF5JA.js";
121
126
  import {
122
127
  buildVoluteSlug,
128
+ resolveChannelId,
129
+ slugify,
130
+ splitMessage,
123
131
  writeChannelEntry
124
- } from "./chunk-RHEGSQFJ.js";
132
+ } from "./chunk-WSLPZF72.js";
125
133
  import {
126
134
  addMind,
127
135
  addVariant,
@@ -141,22 +149,26 @@ import {
141
149
  removeVariant,
142
150
  setMindRunning,
143
151
  setMindStage,
152
+ setMindTemplateHash,
144
153
  setVariantRunning,
145
154
  stateDir,
146
155
  validateBranchName,
147
156
  validateMindName,
148
- voluteHome
149
- } from "./chunk-EBGCNDMM.js";
150
- import "./chunk-K3NQKI34.js";
157
+ voluteHome,
158
+ writeVariants
159
+ } from "./chunk-B2CPS4QU.js";
160
+ import {
161
+ __export
162
+ } from "./chunk-K3NQKI34.js";
151
163
 
152
164
  // src/daemon.ts
153
165
  import { randomBytes as randomBytes2 } from "crypto";
154
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
166
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
155
167
  import { homedir as homedir2 } from "os";
156
168
  import { resolve as resolve22 } from "path";
157
169
  import { format } from "util";
158
170
 
159
- // src/lib/connector-manager.ts
171
+ // src/lib/daemon/connector-manager.ts
160
172
  import { spawn } from "child_process";
161
173
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
162
174
  import { dirname, resolve as resolve2 } from "path";
@@ -233,7 +245,7 @@ function checkMissingEnvVars(def, env) {
233
245
  return def.envVars.filter((v) => v.required && !env[v.name]);
234
246
  }
235
247
 
236
- // src/lib/connector-manager.ts
248
+ // src/lib/daemon/connector-manager.ts
237
249
  var clog = logger_default.child("connectors");
238
250
  function searchUpwards(...segments) {
239
251
  let searchDir = dirname(new URL(import.meta.url).pathname);
@@ -253,13 +265,13 @@ var ConnectorManager = class {
253
265
  async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
254
266
  const config = readVoluteConfig(mindDir2) ?? {};
255
267
  const types = config.connectors ?? [];
256
- for (const type of types) {
257
- try {
258
- await this.startConnector(mindName, mindDir2, mindPort, type, daemonPort);
259
- } catch (err) {
260
- clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
261
- }
262
- }
268
+ await Promise.all(
269
+ types.map(
270
+ (type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
271
+ clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
272
+ })
273
+ )
274
+ );
263
275
  }
264
276
  checkConnectorEnv(type, mindName, mindDir2) {
265
277
  const mindConnectorDir = resolve2(mindDir2, "connectors", type);
@@ -281,13 +293,21 @@ var ConnectorManager = class {
281
293
  await new Promise((res) => {
282
294
  existing.child.on("exit", () => res());
283
295
  try {
284
- existing.child.kill("SIGTERM");
296
+ if (existing.child.pid) {
297
+ process.kill(-existing.child.pid, "SIGTERM");
298
+ } else {
299
+ existing.child.kill("SIGTERM");
300
+ }
285
301
  } catch {
286
302
  res();
287
303
  }
288
304
  setTimeout(() => {
289
305
  try {
290
- existing.child.kill("SIGKILL");
306
+ if (existing.child.pid) {
307
+ process.kill(-existing.child.pid, "SIGKILL");
308
+ } else {
309
+ existing.child.kill("SIGKILL");
310
+ }
291
311
  } catch {
292
312
  }
293
313
  res();
@@ -334,6 +354,7 @@ var ConnectorManager = class {
334
354
  );
335
355
  const spawnOpts = {
336
356
  stdio: ["ignore", "pipe", "pipe"],
357
+ detached: true,
337
358
  env: {
338
359
  ...process.env,
339
360
  VOLUTE_MIND_PORT: String(mindPort),
@@ -400,13 +421,13 @@ var ConnectorManager = class {
400
421
  await new Promise((resolve23) => {
401
422
  tracked.child.on("exit", () => resolve23());
402
423
  try {
403
- tracked.child.kill("SIGTERM");
424
+ process.kill(-tracked.child.pid, "SIGTERM");
404
425
  } catch {
405
426
  resolve23();
406
427
  }
407
428
  setTimeout(() => {
408
429
  try {
409
- tracked.child.kill("SIGKILL");
430
+ process.kill(-tracked.child.pid, "SIGKILL");
410
431
  } catch {
411
432
  }
412
433
  resolve23();
@@ -461,7 +482,11 @@ var ConnectorManager = class {
461
482
  try {
462
483
  const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
463
484
  if (pid > 0) {
464
- process.kill(pid, "SIGTERM");
485
+ try {
486
+ process.kill(-pid, "SIGTERM");
487
+ } catch {
488
+ process.kill(pid, "SIGTERM");
489
+ }
465
490
  clog.warn(`killed orphan connector ${type} (pid ${pid})`);
466
491
  }
467
492
  } catch {
@@ -490,7 +515,7 @@ function getConnectorManager() {
490
515
  return instance;
491
516
  }
492
517
 
493
- // src/lib/mail-poller.ts
518
+ // src/lib/daemon/mail-poller.ts
494
519
  var mlog = logger_default.child("mail");
495
520
  function formatEmailContent(email) {
496
521
  if (email.body) {
@@ -516,13 +541,14 @@ var MailPoller = class {
516
541
  reconnectDelay = INITIAL_RECONNECT_MS;
517
542
  reconnectAttempts = 0;
518
543
  disconnectedAt = null;
544
+ config = null;
519
545
  start() {
520
546
  if (this.running) {
521
547
  mlog.warn("already running \u2014 ignoring duplicate start");
522
548
  return;
523
549
  }
524
- const config = readSystemsConfig();
525
- if (!config) {
550
+ this.config = readSystemsConfig();
551
+ if (!this.config) {
526
552
  mlog.info("no systems config \u2014 mail disabled");
527
553
  return;
528
554
  }
@@ -531,6 +557,7 @@ var MailPoller = class {
531
557
  }
532
558
  stop() {
533
559
  this.running = false;
560
+ this.config = null;
534
561
  if (this.pingTimer) clearInterval(this.pingTimer);
535
562
  this.pingTimer = null;
536
563
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
@@ -545,16 +572,16 @@ var MailPoller = class {
545
572
  }
546
573
  connect() {
547
574
  if (!this.running) return;
548
- const config = readSystemsConfig();
549
- if (!config) {
575
+ this.config = readSystemsConfig();
576
+ if (!this.config) {
550
577
  mlog.info("systems config removed \u2014 stopping");
551
578
  this.stop();
552
579
  return;
553
580
  }
554
- const wsUrl = `${config.apiUrl.replace(/^http/, "ws")}/api/ws`;
581
+ const wsUrl = `${this.config.apiUrl.replace(/^http/, "ws")}/api/ws`;
555
582
  try {
556
583
  this.ws = new WebSocket(wsUrl, {
557
- headers: { Authorization: `Bearer ${config.apiKey}` }
584
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
558
585
  });
559
586
  } catch (err) {
560
587
  mlog.warn("failed to create WebSocket", logger_default.errorData(err));
@@ -620,11 +647,10 @@ var MailPoller = class {
620
647
  }
621
648
  /** Fetch emails that arrived while disconnected */
622
649
  catchUp(since) {
623
- const config = readSystemsConfig();
624
- if (!config) return;
625
- const url = `${config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
650
+ if (!this.config) return;
651
+ const url = `${this.config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
626
652
  fetch(url, {
627
- headers: { Authorization: `Bearer ${config.apiKey}` }
653
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
628
654
  }).then(async (res) => {
629
655
  if (!res.ok) {
630
656
  mlog.warn(`catch-up poll failed: HTTP ${res.status}`);
@@ -659,14 +685,13 @@ var MailPoller = class {
659
685
  });
660
686
  }
661
687
  async fetchAndDeliver(mind, notification) {
662
- const config = readSystemsConfig();
663
- if (!config) {
688
+ if (!this.config) {
664
689
  mlog.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
665
690
  return;
666
691
  }
667
- const url = `${config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
692
+ const url = `${this.config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
668
693
  const res = await fetch(url, {
669
- headers: { Authorization: `Bearer ${config.apiKey}` }
694
+ headers: { Authorization: `Bearer ${this.config.apiKey}` }
670
695
  });
671
696
  if (!res.ok) {
672
697
  mlog.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
@@ -727,182 +752,9 @@ async function ensureMailAddress(mindName) {
727
752
  }
728
753
  }
729
754
 
730
- // src/lib/migrate-agents-to-minds.ts
731
- import { execFileSync } from "child_process";
732
- import { existsSync as existsSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
733
- import { resolve as resolve3 } from "path";
734
- var TAG = "[migrate]";
735
- function log(msg) {
736
- console.error(`${TAG} ${msg}`);
737
- }
738
- function migrateAgentsToMinds() {
739
- const home = voluteHome();
740
- bridgeEnvVar();
741
- const names = migrateRegistry(home);
742
- migrateMindsDirectory(home);
743
- migrateLogFiles(home, names);
744
- migrateLinuxUsers(names);
745
- migrateProfileScript();
746
- }
747
- function bridgeEnvVar() {
748
- if (process.env.VOLUTE_AGENTS_DIR && !process.env.VOLUTE_MINDS_DIR) {
749
- process.env.VOLUTE_MINDS_DIR = process.env.VOLUTE_AGENTS_DIR;
750
- log(`bridged VOLUTE_AGENTS_DIR=${process.env.VOLUTE_AGENTS_DIR} \u2192 VOLUTE_MINDS_DIR`);
751
- }
752
- }
753
- function migrateRegistry(home) {
754
- const oldPath = resolve3(home, "agents.json");
755
- const newPath = resolve3(home, "minds.json");
756
- if (!existsSync3(oldPath) || existsSync3(newPath)) {
757
- return readNamesFromRegistry(newPath);
758
- }
759
- const raw = readFileSync3(oldPath, "utf-8");
760
- const entries = JSON.parse(raw);
761
- for (const entry of entries) {
762
- if (entry.stage === "mind") {
763
- entry.stage = "sprouted";
764
- }
765
- }
766
- writeFileSync2(newPath, `${JSON.stringify(entries, null, 2)}
767
- `);
768
- try {
769
- renameSync(oldPath, `${oldPath}.bak`);
770
- } catch {
771
- }
772
- log("renamed agents.json \u2192 minds.json");
773
- return entries.map((e) => e.name);
774
- }
775
- function readNamesFromRegistry(path) {
776
- if (!existsSync3(path)) return [];
777
- try {
778
- const entries = JSON.parse(readFileSync3(path, "utf-8"));
779
- return entries.map((e) => e.name);
780
- } catch {
781
- return [];
782
- }
783
- }
784
- function migrateMindsDirectory(home) {
785
- if (process.env.VOLUTE_MINDS_DIR) return;
786
- const oldDir = resolve3(home, "agents");
787
- const newDir = resolve3(home, "minds");
788
- if (existsSync3(oldDir) && !existsSync3(newDir)) {
789
- try {
790
- renameSync(oldDir, newDir);
791
- log("renamed agents/ \u2192 minds/");
792
- } catch (err) {
793
- log(`failed to rename agents/ \u2192 minds/: ${err}`);
794
- }
795
- }
796
- }
797
- function migrateLogFiles(home, names) {
798
- for (const name of names) {
799
- const logsDir = resolve3(home, "state", name, "logs");
800
- const oldLog = resolve3(logsDir, "agent.log");
801
- const newLog = resolve3(logsDir, "mind.log");
802
- if (existsSync3(oldLog) && !existsSync3(newLog)) {
803
- try {
804
- renameSync(oldLog, newLog);
805
- log(`renamed ${name} agent.log \u2192 mind.log`);
806
- } catch (err) {
807
- log(`failed to rename ${name} log file: ${err}`);
808
- }
809
- }
810
- }
811
- }
812
- function migrateLinuxUsers(names) {
813
- if (process.env.VOLUTE_ISOLATION !== "user") return;
814
- const oldPrefix = "agent-";
815
- const newPrefix = process.env.VOLUTE_USER_PREFIX ?? "mind-";
816
- if (newPrefix !== "mind-") return;
817
- for (const name of names) {
818
- const oldUser = `${oldPrefix}${name}`;
819
- const newUser = `${newPrefix}${name}`;
820
- try {
821
- execFileSync("id", [oldUser], { stdio: "ignore" });
822
- } catch {
823
- continue;
824
- }
825
- try {
826
- execFileSync("id", [newUser], { stdio: "ignore" });
827
- continue;
828
- } catch {
829
- }
830
- try {
831
- execFileSync("usermod", ["-l", newUser, oldUser], {
832
- stdio: ["ignore", "ignore", "pipe"]
833
- });
834
- log(`renamed user ${oldUser} \u2192 ${newUser}`);
835
- } catch (err) {
836
- const stderr = err?.stderr?.toString().trim();
837
- log(`failed to rename user ${oldUser}: ${stderr || err}`);
838
- continue;
839
- }
840
- try {
841
- execFileSync("getent", ["group", oldUser], { stdio: "ignore" });
842
- execFileSync("groupmod", ["-n", newUser, oldUser], {
843
- stdio: ["ignore", "ignore", "pipe"]
844
- });
845
- log(`renamed group ${oldUser} \u2192 ${newUser}`);
846
- } catch {
847
- }
848
- }
849
- }
850
- function migrateProfileScript() {
851
- const profilePath = "/etc/profile.d/volute.sh";
852
- if (!existsSync3(profilePath)) return;
853
- try {
854
- const content = readFileSync3(profilePath, "utf-8");
855
- if (!content.includes("VOLUTE_AGENTS_DIR")) return;
856
- const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
857
- writeFileSync2(profilePath, updated);
858
- log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
859
- } catch (err) {
860
- log(`failed to update profile script: ${err}`);
861
- }
862
- }
863
-
864
- // src/lib/migrate-state.ts
865
- import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, renameSync as renameSync2 } from "fs";
866
- import { resolve as resolve4 } from "path";
867
- function migrateDotVoluteDir(name) {
868
- const dir = mindDir(name);
869
- const oldDir = resolve4(dir, ".volute");
870
- const newDir = resolve4(dir, ".mind");
871
- if (existsSync4(oldDir) && !existsSync4(newDir)) {
872
- renameSync2(oldDir, newDir);
873
- } else if (existsSync4(oldDir) && existsSync4(newDir)) {
874
- console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
875
- }
876
- }
877
- function migrateMindState(name) {
878
- const src = resolve4(mindDir(name), ".mind");
879
- if (!existsSync4(src)) return;
880
- const dest = stateDir(name);
881
- mkdirSync2(dest, { recursive: true });
882
- for (const file of ["env.json", "channels.json"]) {
883
- const srcPath = resolve4(src, file);
884
- const destPath = resolve4(dest, file);
885
- if (existsSync4(srcPath) && !existsSync4(destPath)) {
886
- copyFileSync(srcPath, destPath);
887
- }
888
- }
889
- const srcLogs = resolve4(src, "logs");
890
- const destLogs = resolve4(dest, "logs");
891
- if (existsSync4(srcLogs) && !existsSync4(destLogs)) {
892
- mkdirSync2(destLogs, { recursive: true });
893
- for (const file of readdirSync(srcLogs)) {
894
- try {
895
- copyFileSync(resolve4(srcLogs, file), resolve4(destLogs, file));
896
- } catch (err) {
897
- console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
898
- }
899
- }
900
- }
901
- }
902
-
903
755
  // src/lib/pages-watcher.ts
904
- import { existsSync as existsSync5, readdirSync as readdirSync2, statSync, watch } from "fs";
905
- import { join, resolve as resolve5 } from "path";
756
+ import { existsSync as existsSync3, readdirSync, statSync, watch } from "fs";
757
+ import { join, resolve as resolve3 } from "path";
906
758
  var watchers = /* @__PURE__ */ new Map();
907
759
  var homeWatchers = /* @__PURE__ */ new Map();
908
760
  var debounceTimers = /* @__PURE__ */ new Map();
@@ -938,18 +790,18 @@ function startPagesWatcher(mindName, pagesDir) {
938
790
  }
939
791
  function startWatcher(mindName) {
940
792
  if (watchers.has(mindName)) return;
941
- const pagesDir = resolve5(mindDir(mindName), "home", "pages");
942
- if (existsSync5(pagesDir)) {
793
+ const pagesDir = resolve3(mindDir(mindName), "home", "pages");
794
+ if (existsSync3(pagesDir)) {
943
795
  startPagesWatcher(mindName, pagesDir);
944
796
  return;
945
797
  }
946
798
  if (homeWatchers.has(mindName)) return;
947
- const homeDir = resolve5(mindDir(mindName), "home");
948
- if (!existsSync5(homeDir)) return;
799
+ const homeDir = resolve3(mindDir(mindName), "home");
800
+ if (!existsSync3(homeDir)) return;
949
801
  try {
950
802
  const hw = watch(homeDir, (_eventType, filename) => {
951
803
  if (filename !== "pages") return;
952
- if (!existsSync5(pagesDir)) return;
804
+ if (!existsSync3(pagesDir)) return;
953
805
  hw.close();
954
806
  homeWatchers.delete(mindName);
955
807
  invalidateCache();
@@ -1001,13 +853,13 @@ function scanPagesDir(dir, urlPrefix) {
1001
853
  const pages = [];
1002
854
  let items;
1003
855
  try {
1004
- items = readdirSync2(dir);
856
+ items = readdirSync(dir);
1005
857
  } catch {
1006
858
  return pages;
1007
859
  }
1008
860
  for (const item of items) {
1009
861
  if (item.startsWith(".")) continue;
1010
- const fullPath = resolve5(dir, item);
862
+ const fullPath = resolve3(dir, item);
1011
863
  try {
1012
864
  const s = statSync(fullPath);
1013
865
  if (s.isFile() && item.endsWith(".html")) {
@@ -1017,8 +869,8 @@ function scanPagesDir(dir, urlPrefix) {
1017
869
  url: `${urlPrefix}/${item}`
1018
870
  });
1019
871
  } else if (s.isDirectory()) {
1020
- const indexPath = resolve5(fullPath, "index.html");
1021
- if (existsSync5(indexPath)) {
872
+ const indexPath = resolve3(fullPath, "index.html");
873
+ if (existsSync3(indexPath)) {
1022
874
  const indexStat = statSync(indexPath);
1023
875
  pages.push({
1024
876
  file: join(item, "index.html"),
@@ -1035,8 +887,8 @@ function scanPagesDir(dir, urlPrefix) {
1035
887
  }
1036
888
  function buildSites() {
1037
889
  const sites = [];
1038
- const systemPagesDir = resolve5(voluteHome(), "shared", "pages");
1039
- if (existsSync5(systemPagesDir)) {
890
+ const systemPagesDir = resolve3(voluteHome(), "shared", "pages");
891
+ if (existsSync3(systemPagesDir)) {
1040
892
  const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
1041
893
  if (systemPages.length > 0) {
1042
894
  sites.push({ name: "_system", label: "System", pages: systemPages });
@@ -1044,8 +896,8 @@ function buildSites() {
1044
896
  }
1045
897
  const entries = readRegistry();
1046
898
  for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
1047
- const pagesDir = resolve5(mindDir(entry.name), "home", "pages");
1048
- if (!existsSync5(pagesDir)) continue;
899
+ const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
900
+ if (!existsSync3(pagesDir)) continue;
1049
901
  const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
1050
902
  if (mindPages.length > 0) {
1051
903
  sites.push({ name: entry.name, label: entry.name, pages: mindPages });
@@ -1057,17 +909,17 @@ function buildRecentPages() {
1057
909
  const entries = readRegistry();
1058
910
  const pages = [];
1059
911
  for (const entry of entries) {
1060
- const pagesDir = resolve5(mindDir(entry.name), "home", "pages");
1061
- if (!existsSync5(pagesDir)) continue;
912
+ const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
913
+ if (!existsSync3(pagesDir)) continue;
1062
914
  let items;
1063
915
  try {
1064
- items = readdirSync2(pagesDir);
916
+ items = readdirSync(pagesDir);
1065
917
  } catch {
1066
918
  continue;
1067
919
  }
1068
920
  for (const item of items) {
1069
921
  if (item.startsWith(".")) continue;
1070
- const fullPath = resolve5(pagesDir, item);
922
+ const fullPath = resolve3(pagesDir, item);
1071
923
  try {
1072
924
  const s = statSync(fullPath);
1073
925
  if (s.isFile() && item.endsWith(".html")) {
@@ -1078,8 +930,8 @@ function buildRecentPages() {
1078
930
  url: `/pages/${entry.name}/${item}`
1079
931
  });
1080
932
  } else if (s.isDirectory()) {
1081
- const indexPath = resolve5(fullPath, "index.html");
1082
- if (existsSync5(indexPath)) {
933
+ const indexPath = resolve3(fullPath, "index.html");
934
+ if (existsSync3(indexPath)) {
1083
935
  const indexStat = statSync(indexPath);
1084
936
  pages.push({
1085
937
  mind: entry.name,
@@ -1105,8 +957,8 @@ function getCachedRecentPages() {
1105
957
  return recentPagesCache;
1106
958
  }
1107
959
 
1108
- // src/lib/scheduler.ts
1109
- import { resolve as resolve6 } from "path";
960
+ // src/lib/daemon/scheduler.ts
961
+ import { resolve as resolve4 } from "path";
1110
962
  import { CronExpressionParser } from "cron-parser";
1111
963
  var slog = logger_default.child("scheduler");
1112
964
  var Scheduler = class {
@@ -1115,7 +967,7 @@ var Scheduler = class {
1115
967
  lastFired = /* @__PURE__ */ new Map();
1116
968
  // "mind:scheduleId" → epoch minute
1117
969
  get statePath() {
1118
- return resolve6(voluteHome(), "scheduler-state.json");
970
+ return resolve4(voluteHome(), "scheduler-state.json");
1119
971
  }
1120
972
  start() {
1121
973
  this.loadState();
@@ -1149,39 +1001,46 @@ var Scheduler = class {
1149
1001
  }
1150
1002
  tick() {
1151
1003
  const now = /* @__PURE__ */ new Date();
1004
+ const epochMinute = Math.floor(now.getTime() / 6e4);
1005
+ const cronCache = /* @__PURE__ */ new Map();
1006
+ let anyFired = false;
1152
1007
  for (const [mind, schedules] of this.schedules) {
1153
1008
  for (const schedule of schedules) {
1154
1009
  if (!schedule.enabled) continue;
1155
- if (this.shouldFire(schedule, now, mind)) {
1010
+ if (this.shouldFire(schedule, epochMinute, mind, cronCache)) {
1011
+ anyFired = true;
1156
1012
  this.fire(mind, schedule);
1157
1013
  }
1158
1014
  }
1159
1015
  }
1016
+ if (anyFired) this.saveState();
1160
1017
  }
1161
- shouldFire(schedule, now, mind) {
1162
- try {
1163
- const interval = CronExpressionParser.parse(schedule.cron);
1164
- const prev = interval.prev().toDate();
1165
- const epochMinute = Math.floor(now.getTime() / 6e4);
1166
- const key = `${mind}:${schedule.id}`;
1167
- if (this.lastFired.get(key) === epochMinute) return false;
1168
- const prevMinute = Math.floor(prev.getTime() / 6e4);
1169
- if (prevMinute === epochMinute) {
1170
- this.lastFired.set(key, epochMinute);
1171
- this.saveState();
1172
- return true;
1018
+ shouldFire(schedule, epochMinute, mind, cronCache) {
1019
+ const key = `${mind}:${schedule.id}`;
1020
+ if (this.lastFired.get(key) === epochMinute) return false;
1021
+ let prevMinute = cronCache.get(schedule.cron);
1022
+ if (prevMinute === void 0) {
1023
+ try {
1024
+ const interval = CronExpressionParser.parse(schedule.cron);
1025
+ const prev = interval.prev().toDate();
1026
+ prevMinute = Math.floor(prev.getTime() / 6e4);
1027
+ cronCache.set(schedule.cron, prevMinute);
1028
+ } catch (err) {
1029
+ slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
1030
+ return false;
1173
1031
  }
1174
- return false;
1175
- } catch (err) {
1176
- slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
1177
- return false;
1178
1032
  }
1033
+ if (prevMinute === epochMinute) {
1034
+ this.lastFired.set(key, epochMinute);
1035
+ return true;
1036
+ }
1037
+ return false;
1179
1038
  }
1180
1039
  async fire(mindName, schedule) {
1181
1040
  try {
1182
1041
  let text;
1183
1042
  if (schedule.script) {
1184
- const homeDir = resolve6(mindDir(mindName), "home");
1043
+ const homeDir = resolve4(mindDir(mindName), "home");
1185
1044
  try {
1186
1045
  const output = await this.runScript(schedule.script, homeDir, mindName);
1187
1046
  if (!output.trim()) {
@@ -1229,19 +1088,21 @@ function getScheduler() {
1229
1088
  return instance3;
1230
1089
  }
1231
1090
 
1232
- // src/lib/token-budget.ts
1233
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1234
- import { resolve as resolve7 } from "path";
1091
+ // src/lib/daemon/token-budget.ts
1092
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1093
+ import { resolve as resolve5 } from "path";
1235
1094
  var tlog = logger_default.child("token-budget");
1236
1095
  var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
1237
1096
  var MAX_QUEUE_SIZE = 100;
1238
1097
  var TokenBudget = class {
1239
1098
  budgets = /* @__PURE__ */ new Map();
1240
1099
  interval = null;
1100
+ dirty = /* @__PURE__ */ new Set();
1241
1101
  start() {
1242
1102
  this.interval = setInterval(() => this.tick(), 6e4);
1243
1103
  }
1244
1104
  stop() {
1105
+ this.flush();
1245
1106
  if (this.interval) clearInterval(this.interval);
1246
1107
  this.interval = null;
1247
1108
  }
@@ -1276,7 +1137,7 @@ var TokenBudget = class {
1276
1137
  const state = this.budgets.get(mind);
1277
1138
  if (!state) return;
1278
1139
  state.tokensUsed += inputTokens + outputTokens;
1279
- this.saveBudgetState(mind, state);
1140
+ this.dirty.add(mind);
1280
1141
  }
1281
1142
  /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
1282
1143
  checkBudget(mind) {
@@ -1327,7 +1188,7 @@ var TokenBudget = class {
1327
1188
  state.tokensUsed = 0;
1328
1189
  state.periodStart = now;
1329
1190
  state.warningInjected = false;
1330
- this.saveBudgetState(mind, state);
1191
+ this.dirty.add(mind);
1331
1192
  const queued = this.drain(mind);
1332
1193
  if (queued.length > 0) {
1333
1194
  this.replay(mind, queued).catch((err) => {
@@ -1336,21 +1197,30 @@ var TokenBudget = class {
1336
1197
  }
1337
1198
  }
1338
1199
  }
1200
+ this.flush();
1201
+ }
1202
+ /** Flush all dirty budget states to disk. */
1203
+ flush() {
1204
+ for (const mind of this.dirty) {
1205
+ const state = this.budgets.get(mind);
1206
+ if (state) this.saveBudgetState(mind, state);
1207
+ }
1208
+ this.dirty.clear();
1339
1209
  }
1340
1210
  budgetStatePath(mind) {
1341
- return resolve7(stateDir(mind), "budget.json");
1211
+ return resolve5(stateDir(mind), "budget.json");
1342
1212
  }
1343
1213
  saveBudgetState(mind, state) {
1344
1214
  try {
1345
1215
  const dir = stateDir(mind);
1346
- mkdirSync3(dir, { recursive: true });
1216
+ mkdirSync2(dir, { recursive: true });
1347
1217
  const data = {
1348
1218
  periodStart: state.periodStart,
1349
1219
  tokensUsed: state.tokensUsed,
1350
1220
  warningInjected: state.warningInjected,
1351
1221
  queue: state.queue
1352
1222
  };
1353
- writeFileSync3(this.budgetStatePath(mind), `${JSON.stringify(data)}
1223
+ writeFileSync2(this.budgetStatePath(mind), `${JSON.stringify(data)}
1354
1224
  `);
1355
1225
  } catch (err) {
1356
1226
  tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
@@ -1359,8 +1229,8 @@ var TokenBudget = class {
1359
1229
  loadBudgetState(mind) {
1360
1230
  try {
1361
1231
  const path = this.budgetStatePath(mind);
1362
- if (!existsSync6(path)) return null;
1363
- const data = JSON.parse(readFileSync4(path, "utf-8"));
1232
+ if (!existsSync4(path)) return null;
1233
+ const data = JSON.parse(readFileSync3(path, "utf-8"));
1364
1234
  if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
1365
1235
  return {
1366
1236
  periodStart: data.periodStart,
@@ -1415,7 +1285,7 @@ function getTokenBudget() {
1415
1285
  return instance4;
1416
1286
  }
1417
1287
 
1418
- // src/lib/mind-service.ts
1288
+ // src/lib/daemon/mind-service.ts
1419
1289
  async function startMindFull(name) {
1420
1290
  const [baseName, variantName] = name.split("@", 2);
1421
1291
  await getMindManager().startMind(name);
@@ -1461,42 +1331,215 @@ async function stopMindFull(name) {
1461
1331
  }).catch((err) => logger_default.error("failed to publish mind_stopped activity", logger_default.errorData(err)));
1462
1332
  }
1463
1333
 
1464
- // src/web/middleware/auth.ts
1465
- import { timingSafeEqual } from "crypto";
1466
- import { eq as eq2, lt } from "drizzle-orm";
1467
- import { getCookie } from "hono/cookie";
1468
- import { createMiddleware } from "hono/factory";
1469
-
1470
- // src/lib/auth.ts
1471
- import { compareSync, hashSync } from "bcryptjs";
1472
- import { and, count, eq } from "drizzle-orm";
1473
- async function createUser(username, password) {
1474
- const db = await getDb();
1475
- const hash = hashSync(password, 10);
1476
- const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
1477
- const role = value === 0 ? "admin" : "pending";
1478
- const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
1479
- id: users.id,
1480
- username: users.username,
1481
- role: users.role,
1482
- user_type: users.user_type,
1483
- created_at: users.created_at
1484
- });
1485
- return result;
1486
- }
1487
- async function verifyUser(username, password) {
1488
- const db = await getDb();
1489
- const row = await db.select().from(users).where(eq(users.username, username)).get();
1490
- if (!row) return null;
1491
- if (row.user_type === "mind") return null;
1492
- if (!compareSync(password, row.password_hash)) return null;
1493
- const { password_hash: _, ...user } = row;
1494
- return user;
1334
+ // src/lib/migrate-agents-to-minds.ts
1335
+ import { execFileSync } from "child_process";
1336
+ import { existsSync as existsSync5, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "fs";
1337
+ import { resolve as resolve6 } from "path";
1338
+ var TAG = "[migrate]";
1339
+ function log(msg) {
1340
+ console.error(`${TAG} ${msg}`);
1495
1341
  }
1496
- async function getUser(id) {
1497
- const db = await getDb();
1498
- const row = await db.select({
1499
- id: users.id,
1342
+ function migrateAgentsToMinds() {
1343
+ const home = voluteHome();
1344
+ bridgeEnvVar();
1345
+ const names = migrateRegistry(home);
1346
+ migrateMindsDirectory(home);
1347
+ migrateLogFiles(home, names);
1348
+ migrateLinuxUsers(names);
1349
+ migrateProfileScript();
1350
+ }
1351
+ function bridgeEnvVar() {
1352
+ if (process.env.VOLUTE_AGENTS_DIR && !process.env.VOLUTE_MINDS_DIR) {
1353
+ process.env.VOLUTE_MINDS_DIR = process.env.VOLUTE_AGENTS_DIR;
1354
+ log(`bridged VOLUTE_AGENTS_DIR=${process.env.VOLUTE_AGENTS_DIR} \u2192 VOLUTE_MINDS_DIR`);
1355
+ }
1356
+ }
1357
+ function migrateRegistry(home) {
1358
+ const oldPath = resolve6(home, "agents.json");
1359
+ const newPath = resolve6(home, "minds.json");
1360
+ if (!existsSync5(oldPath) || existsSync5(newPath)) {
1361
+ return readNamesFromRegistry(newPath);
1362
+ }
1363
+ const raw = readFileSync4(oldPath, "utf-8");
1364
+ const entries = JSON.parse(raw);
1365
+ for (const entry of entries) {
1366
+ if (entry.stage === "mind") {
1367
+ entry.stage = "sprouted";
1368
+ }
1369
+ }
1370
+ writeFileSync3(newPath, `${JSON.stringify(entries, null, 2)}
1371
+ `);
1372
+ try {
1373
+ renameSync(oldPath, `${oldPath}.bak`);
1374
+ } catch {
1375
+ }
1376
+ log("renamed agents.json \u2192 minds.json");
1377
+ return entries.map((e) => e.name);
1378
+ }
1379
+ function readNamesFromRegistry(path) {
1380
+ if (!existsSync5(path)) return [];
1381
+ try {
1382
+ const entries = JSON.parse(readFileSync4(path, "utf-8"));
1383
+ return entries.map((e) => e.name);
1384
+ } catch {
1385
+ return [];
1386
+ }
1387
+ }
1388
+ function migrateMindsDirectory(home) {
1389
+ if (process.env.VOLUTE_MINDS_DIR) return;
1390
+ const oldDir = resolve6(home, "agents");
1391
+ const newDir = resolve6(home, "minds");
1392
+ if (existsSync5(oldDir) && !existsSync5(newDir)) {
1393
+ try {
1394
+ renameSync(oldDir, newDir);
1395
+ log("renamed agents/ \u2192 minds/");
1396
+ } catch (err) {
1397
+ log(`failed to rename agents/ \u2192 minds/: ${err}`);
1398
+ }
1399
+ }
1400
+ }
1401
+ function migrateLogFiles(home, names) {
1402
+ for (const name of names) {
1403
+ const logsDir = resolve6(home, "state", name, "logs");
1404
+ const oldLog = resolve6(logsDir, "agent.log");
1405
+ const newLog = resolve6(logsDir, "mind.log");
1406
+ if (existsSync5(oldLog) && !existsSync5(newLog)) {
1407
+ try {
1408
+ renameSync(oldLog, newLog);
1409
+ log(`renamed ${name} agent.log \u2192 mind.log`);
1410
+ } catch (err) {
1411
+ log(`failed to rename ${name} log file: ${err}`);
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1416
+ function migrateLinuxUsers(names) {
1417
+ if (process.env.VOLUTE_ISOLATION !== "user") return;
1418
+ const oldPrefix = "agent-";
1419
+ const newPrefix = process.env.VOLUTE_USER_PREFIX ?? "mind-";
1420
+ if (newPrefix !== "mind-") return;
1421
+ for (const name of names) {
1422
+ const oldUser = `${oldPrefix}${name}`;
1423
+ const newUser = `${newPrefix}${name}`;
1424
+ try {
1425
+ execFileSync("id", [oldUser], { stdio: "ignore" });
1426
+ } catch {
1427
+ continue;
1428
+ }
1429
+ try {
1430
+ execFileSync("id", [newUser], { stdio: "ignore" });
1431
+ continue;
1432
+ } catch {
1433
+ }
1434
+ try {
1435
+ execFileSync("usermod", ["-l", newUser, oldUser], {
1436
+ stdio: ["ignore", "ignore", "pipe"]
1437
+ });
1438
+ log(`renamed user ${oldUser} \u2192 ${newUser}`);
1439
+ } catch (err) {
1440
+ const stderr = err?.stderr?.toString().trim();
1441
+ log(`failed to rename user ${oldUser}: ${stderr || err}`);
1442
+ continue;
1443
+ }
1444
+ try {
1445
+ execFileSync("getent", ["group", oldUser], { stdio: "ignore" });
1446
+ execFileSync("groupmod", ["-n", newUser, oldUser], {
1447
+ stdio: ["ignore", "ignore", "pipe"]
1448
+ });
1449
+ log(`renamed group ${oldUser} \u2192 ${newUser}`);
1450
+ } catch {
1451
+ }
1452
+ }
1453
+ }
1454
+ function migrateProfileScript() {
1455
+ const profilePath = "/etc/profile.d/volute.sh";
1456
+ if (!existsSync5(profilePath)) return;
1457
+ try {
1458
+ const content = readFileSync4(profilePath, "utf-8");
1459
+ if (!content.includes("VOLUTE_AGENTS_DIR")) return;
1460
+ const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
1461
+ writeFileSync3(profilePath, updated);
1462
+ log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
1463
+ } catch (err) {
1464
+ log(`failed to update profile script: ${err}`);
1465
+ }
1466
+ }
1467
+
1468
+ // src/lib/migrate-state.ts
1469
+ import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync2, renameSync as renameSync2 } from "fs";
1470
+ import { resolve as resolve7 } from "path";
1471
+ function migrateDotVoluteDir(name) {
1472
+ const dir = mindDir(name);
1473
+ const oldDir = resolve7(dir, ".volute");
1474
+ const newDir = resolve7(dir, ".mind");
1475
+ if (existsSync6(oldDir) && !existsSync6(newDir)) {
1476
+ renameSync2(oldDir, newDir);
1477
+ } else if (existsSync6(oldDir) && existsSync6(newDir)) {
1478
+ console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
1479
+ }
1480
+ }
1481
+ function migrateMindState(name) {
1482
+ const src = resolve7(mindDir(name), ".mind");
1483
+ if (!existsSync6(src)) return;
1484
+ const dest = stateDir(name);
1485
+ mkdirSync3(dest, { recursive: true });
1486
+ for (const file of ["env.json", "channels.json"]) {
1487
+ const srcPath = resolve7(src, file);
1488
+ const destPath = resolve7(dest, file);
1489
+ if (existsSync6(srcPath) && !existsSync6(destPath)) {
1490
+ copyFileSync(srcPath, destPath);
1491
+ }
1492
+ }
1493
+ const srcLogs = resolve7(src, "logs");
1494
+ const destLogs = resolve7(dest, "logs");
1495
+ if (existsSync6(srcLogs) && !existsSync6(destLogs)) {
1496
+ mkdirSync3(destLogs, { recursive: true });
1497
+ for (const file of readdirSync2(srcLogs)) {
1498
+ try {
1499
+ copyFileSync(resolve7(srcLogs, file), resolve7(destLogs, file));
1500
+ } catch (err) {
1501
+ console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
1502
+ }
1503
+ }
1504
+ }
1505
+ }
1506
+
1507
+ // src/web/middleware/auth.ts
1508
+ import { timingSafeEqual } from "crypto";
1509
+ import { eq as eq2, lt } from "drizzle-orm";
1510
+ import { getCookie } from "hono/cookie";
1511
+ import { createMiddleware } from "hono/factory";
1512
+
1513
+ // src/lib/auth.ts
1514
+ import { compareSync, hashSync } from "bcryptjs";
1515
+ import { and, count, eq } from "drizzle-orm";
1516
+ async function createUser(username, password) {
1517
+ const db = await getDb();
1518
+ const hash = hashSync(password, 10);
1519
+ const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
1520
+ const role = value === 0 ? "admin" : "pending";
1521
+ const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
1522
+ id: users.id,
1523
+ username: users.username,
1524
+ role: users.role,
1525
+ user_type: users.user_type,
1526
+ created_at: users.created_at
1527
+ });
1528
+ return result;
1529
+ }
1530
+ async function verifyUser(username, password) {
1531
+ const db = await getDb();
1532
+ const row = await db.select().from(users).where(eq(users.username, username)).get();
1533
+ if (!row) return null;
1534
+ if (row.user_type === "mind") return null;
1535
+ if (!compareSync(password, row.password_hash)) return null;
1536
+ const { password_hash: _, ...user } = row;
1537
+ return user;
1538
+ }
1539
+ async function getUser(id) {
1540
+ const db = await getDb();
1541
+ const row = await db.select({
1542
+ id: users.id,
1500
1543
  username: users.username,
1501
1544
  role: users.role,
1502
1545
  user_type: users.user_type,
@@ -1680,8 +1723,8 @@ var authMiddleware = createMiddleware(async (c, next) => {
1680
1723
 
1681
1724
  // src/web/server.ts
1682
1725
  import { existsSync as existsSync15 } from "fs";
1683
- import { readFile as readFile3, stat as stat2 } from "fs/promises";
1684
- import { dirname as dirname3, extname as extname2, resolve as resolve21 } from "path";
1726
+ import { readFile as readFile3, stat as stat3 } from "fs/promises";
1727
+ import { dirname as dirname2, extname as extname3, resolve as resolve21 } from "path";
1685
1728
  import { serve } from "@hono/node-server";
1686
1729
 
1687
1730
  // src/web/app.ts
@@ -1695,7 +1738,7 @@ import { desc as desc2 } from "drizzle-orm";
1695
1738
  import { Hono } from "hono";
1696
1739
  import { streamSSE } from "hono/streaming";
1697
1740
 
1698
- // src/lib/conversations.ts
1741
+ // src/lib/events/conversations.ts
1699
1742
  import { randomUUID } from "crypto";
1700
1743
  import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
1701
1744
  async function createConversation(mindName, channel, opts) {
@@ -2067,51 +2110,710 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
2067
2110
  if (existing) {
2068
2111
  return c.json({ error: "Username already taken" }, 409);
2069
2112
  }
2070
- const user = await createUser(username, password);
2071
- if (user.role === "admin") {
2072
- const sessionId = await createSession(user.id);
2073
- setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
2113
+ const user = await createUser(username, password);
2114
+ if (user.role === "admin") {
2115
+ const sessionId = await createSession(user.id);
2116
+ setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
2117
+ }
2118
+ return c.json({ id: user.id, username: user.username, role: user.role });
2119
+ }).post("/login", zValidator("json", credentialsSchema), async (c) => {
2120
+ const { username, password } = c.req.valid("json");
2121
+ const user = await verifyUser(username, password);
2122
+ if (!user) {
2123
+ return c.json({ error: "Invalid credentials" }, 401);
2124
+ }
2125
+ const sessionId = await createSession(user.id);
2126
+ setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
2127
+ return c.json({ id: user.id, username: user.username, role: user.role });
2128
+ }).post("/logout", async (c) => {
2129
+ const sessionId = getCookie2(c, "volute_session");
2130
+ if (sessionId) {
2131
+ await deleteSession(sessionId);
2132
+ deleteCookie(c, "volute_session", { path: "/" });
2133
+ }
2134
+ return c.json({ ok: true });
2135
+ }).get("/me", async (c) => {
2136
+ const sessionId = getCookie2(c, "volute_session");
2137
+ if (!sessionId) return c.json({ error: "Not logged in" }, 401);
2138
+ const userId = await getSessionUserId(sessionId);
2139
+ if (userId == null) return c.json({ error: "Not logged in" }, 401);
2140
+ const user = await getUser(userId);
2141
+ if (!user) return c.json({ error: "Not logged in" }, 401);
2142
+ return c.json({ id: user.id, username: user.username, role: user.role });
2143
+ }).route("/", admin).route("/", authenticated);
2144
+ var auth_default = app2;
2145
+
2146
+ // src/web/api/channels.ts
2147
+ import { Hono as Hono3 } from "hono";
2148
+
2149
+ // src/lib/channels/discord.ts
2150
+ var discord_exports = {};
2151
+ __export(discord_exports, {
2152
+ createConversation: () => createConversation2,
2153
+ listConversations: () => listConversations,
2154
+ listUsers: () => listUsers2,
2155
+ read: () => read,
2156
+ send: () => send
2157
+ });
2158
+ var DISCORD_MAX_LENGTH = 2e3;
2159
+ var API_BASE = "https://discord.com/api/v10";
2160
+ function requireToken(env) {
2161
+ const token = env.DISCORD_TOKEN;
2162
+ if (!token) throw new Error("DISCORD_TOKEN not set");
2163
+ return token;
2164
+ }
2165
+ async function discordGet(token, path) {
2166
+ const res = await fetch(`${API_BASE}${path}`, {
2167
+ headers: { Authorization: `Bot ${token}` }
2168
+ });
2169
+ if (!res.ok) {
2170
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
2171
+ }
2172
+ return res.json();
2173
+ }
2174
+ async function read(env, channelSlug, limit) {
2175
+ const token = requireToken(env);
2176
+ const channelId = resolveChannelId2(env, channelSlug);
2177
+ const res = await fetch(`${API_BASE}/channels/${channelId}/messages?limit=${limit}`, {
2178
+ headers: { Authorization: `Bot ${token}` }
2179
+ });
2180
+ if (!res.ok) {
2181
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
2182
+ }
2183
+ const messages2 = await res.json();
2184
+ return messages2.reverse().map((m) => `${m.author.username}: ${m.content}`).join("\n");
2185
+ }
2186
+ async function send(env, channelSlug, message, images) {
2187
+ const token = requireToken(env);
2188
+ const channelId = resolveChannelId2(env, channelSlug);
2189
+ if (images?.length) {
2190
+ for (let i = 0; i < images.length; i++) {
2191
+ const img = images[i];
2192
+ const ext = img.media_type.split("/")[1] || "png";
2193
+ const form = new FormData();
2194
+ const content = i === 0 ? message.slice(0, DISCORD_MAX_LENGTH) : "";
2195
+ form.append("payload_json", JSON.stringify({ content }));
2196
+ form.append(
2197
+ "files[0]",
2198
+ new Blob([Buffer.from(img.data, "base64")], { type: img.media_type }),
2199
+ `image.${ext}`
2200
+ );
2201
+ const res = await fetch(`${API_BASE}/channels/${channelId}/messages`, {
2202
+ method: "POST",
2203
+ headers: { Authorization: `Bot ${token}` },
2204
+ body: form
2205
+ });
2206
+ if (!res.ok) {
2207
+ const body = await res.text().catch(() => "");
2208
+ const partial = i > 0 ? ` (${i}/${images.length} images were already sent)` : "";
2209
+ throw new Error(`Discord API error: ${res.status} ${body || res.statusText}${partial}`);
2210
+ }
2211
+ }
2212
+ return;
2213
+ }
2214
+ const chunks = splitMessage(message, DISCORD_MAX_LENGTH);
2215
+ for (let i = 0; i < chunks.length; i++) {
2216
+ const res = await fetch(`${API_BASE}/channels/${channelId}/messages`, {
2217
+ method: "POST",
2218
+ headers: {
2219
+ Authorization: `Bot ${token}`,
2220
+ "Content-Type": "application/json"
2221
+ },
2222
+ body: JSON.stringify({ content: chunks[i] })
2223
+ });
2224
+ if (!res.ok) {
2225
+ const partial = i > 0 ? ` (${i}/${chunks.length} chunks were already sent)` : "";
2226
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}${partial}`);
2227
+ }
2228
+ }
2229
+ }
2230
+ async function listConversations(env) {
2231
+ const token = requireToken(env);
2232
+ const results = [];
2233
+ const guilds = await discordGet(token, "/users/@me/guilds");
2234
+ for (const guild of guilds) {
2235
+ const channels = await discordGet(token, `/guilds/${guild.id}/channels`);
2236
+ for (const ch of channels) {
2237
+ if (ch.type !== 0) continue;
2238
+ results.push({
2239
+ id: `discord:${slugify(guild.name)}/${slugify(ch.name)}`,
2240
+ platformId: ch.id,
2241
+ name: `#${ch.name}`,
2242
+ type: "channel"
2243
+ });
2244
+ }
2245
+ }
2246
+ const dms = await discordGet(token, "/users/@me/channels");
2247
+ for (const dm of dms) {
2248
+ const recipients = dm.recipients?.map((r) => r.username) ?? [];
2249
+ const slug = recipients.length === 0 ? `discord:${dm.id}` : recipients.length === 1 ? `discord:@${slugify(recipients[0])}` : `discord:@${recipients.map(slugify).sort().join(",")}`;
2250
+ results.push({
2251
+ id: slug,
2252
+ platformId: dm.id,
2253
+ name: recipients.join(", ") || "DM",
2254
+ type: dm.type === 1 ? "dm" : "group"
2255
+ });
2256
+ }
2257
+ return results;
2258
+ }
2259
+ async function listUsers2(env) {
2260
+ const token = requireToken(env);
2261
+ const seen = /* @__PURE__ */ new Map();
2262
+ const guilds = await discordGet(token, "/users/@me/guilds");
2263
+ for (const guild of guilds) {
2264
+ const members = await discordGet(token, `/guilds/${guild.id}/members?limit=1000`);
2265
+ for (const m of members) {
2266
+ if (!seen.has(m.user.id)) {
2267
+ seen.set(m.user.id, {
2268
+ id: m.user.id,
2269
+ username: m.user.username,
2270
+ type: m.user.bot ? "bot" : "human"
2271
+ });
2272
+ }
2273
+ }
2274
+ }
2275
+ return [...seen.values()];
2276
+ }
2277
+ async function createConversation2(env, participants, _name) {
2278
+ const token = requireToken(env);
2279
+ if (participants.length !== 1) {
2280
+ throw new Error(
2281
+ "Discord group creation not supported via bot \u2014 use threads in an existing channel"
2282
+ );
2283
+ }
2284
+ const allUsers = await listUsers2(env);
2285
+ const target = allUsers.find((u) => u.username.toLowerCase() === participants[0].toLowerCase());
2286
+ if (!target) {
2287
+ throw new Error(`User not found: ${participants[0]}`);
2288
+ }
2289
+ const res = await fetch(`${API_BASE}/users/@me/channels`, {
2290
+ method: "POST",
2291
+ headers: {
2292
+ Authorization: `Bot ${token}`,
2293
+ "Content-Type": "application/json"
2294
+ },
2295
+ body: JSON.stringify({ recipient_id: target.id })
2296
+ });
2297
+ if (!res.ok) {
2298
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
2299
+ }
2300
+ const dm = await res.json();
2301
+ const slug = `discord:@${slugify(participants[0])}`;
2302
+ const mindName = env.VOLUTE_MIND;
2303
+ if (mindName) {
2304
+ writeChannelEntry(mindName, slug, {
2305
+ platformId: dm.id,
2306
+ platform: "discord",
2307
+ name: participants[0],
2308
+ type: "dm"
2309
+ });
2310
+ }
2311
+ return slug;
2312
+ }
2313
+
2314
+ // src/lib/channels/slack.ts
2315
+ var slack_exports = {};
2316
+ __export(slack_exports, {
2317
+ createConversation: () => createConversation3,
2318
+ listConversations: () => listConversations2,
2319
+ listUsers: () => listUsers3,
2320
+ read: () => read2,
2321
+ send: () => send2
2322
+ });
2323
+ var SLACK_MAX_LENGTH = 4e3;
2324
+ var API_BASE2 = "https://slack.com/api";
2325
+ function requireToken2(env) {
2326
+ const token = env.SLACK_BOT_TOKEN;
2327
+ if (!token) throw new Error("SLACK_BOT_TOKEN not set");
2328
+ return token;
2329
+ }
2330
+ async function slackApi(token, method, body) {
2331
+ const res = await fetch(`${API_BASE2}/${method}`, {
2332
+ method: "POST",
2333
+ headers: {
2334
+ Authorization: `Bearer ${token}`,
2335
+ "Content-Type": "application/json"
2336
+ },
2337
+ body: JSON.stringify(body)
2338
+ });
2339
+ if (!res.ok) {
2340
+ throw new Error(`Slack API HTTP error: ${res.status} ${res.statusText}`);
2341
+ }
2342
+ const data = await res.json();
2343
+ if (!data.ok) {
2344
+ throw new Error(`Slack API error: ${data.error}`);
2345
+ }
2346
+ return data;
2347
+ }
2348
+ async function read2(env, channelSlug, limit) {
2349
+ const token = requireToken2(env);
2350
+ const channelId = resolveChannelId2(env, channelSlug);
2351
+ const data = await slackApi(token, "conversations.history", {
2352
+ channel: channelId,
2353
+ limit
2354
+ });
2355
+ return data.messages.reverse().map((m) => `${m.user ?? m.bot_id ?? "unknown"}: ${m.text}`).join("\n");
2356
+ }
2357
+ async function send2(env, channelSlug, message, images) {
2358
+ const token = requireToken2(env);
2359
+ const channelId = resolveChannelId2(env, channelSlug);
2360
+ if (images?.length) {
2361
+ for (const img of images) {
2362
+ const ext = img.media_type.split("/")[1] || "png";
2363
+ const filename = `image.${ext}`;
2364
+ const binary = Buffer.from(img.data, "base64");
2365
+ const uploadData = await slackApi(token, "files.getUploadURLExternal", {
2366
+ filename,
2367
+ length: binary.length
2368
+ });
2369
+ const uploadRes = await fetch(uploadData.upload_url, {
2370
+ method: "POST",
2371
+ body: binary
2372
+ });
2373
+ if (!uploadRes.ok) {
2374
+ throw new Error(`Slack file upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
2375
+ }
2376
+ await slackApi(token, "files.completeUploadExternal", {
2377
+ files: [{ id: uploadData.file_id }],
2378
+ channel_id: channelId
2379
+ });
2380
+ }
2381
+ if (message) {
2382
+ const chunks2 = splitMessage(message, SLACK_MAX_LENGTH);
2383
+ for (const chunk of chunks2) {
2384
+ await slackApi(token, "chat.postMessage", {
2385
+ channel: channelId,
2386
+ text: chunk
2387
+ });
2388
+ }
2389
+ }
2390
+ return;
2391
+ }
2392
+ const chunks = splitMessage(message, SLACK_MAX_LENGTH);
2393
+ for (let i = 0; i < chunks.length; i++) {
2394
+ try {
2395
+ await slackApi(token, "chat.postMessage", {
2396
+ channel: channelId,
2397
+ text: chunks[i]
2398
+ });
2399
+ } catch (err) {
2400
+ const partial = i > 0 ? ` (${i}/${chunks.length} chunks were already sent)` : "";
2401
+ throw new Error(`${err instanceof Error ? err.message : err}${partial}`);
2402
+ }
2403
+ }
2404
+ }
2405
+ async function listConversations2(env) {
2406
+ const token = requireToken2(env);
2407
+ const authData = await slackApi(token, "auth.test", {});
2408
+ const teamName = authData.team ?? "workspace";
2409
+ const data = await slackApi(token, "conversations.list", {
2410
+ types: "public_channel,private_channel,mpim,im",
2411
+ limit: 1e3
2412
+ });
2413
+ const userMap = /* @__PURE__ */ new Map();
2414
+ const imChannels = data.channels.filter((ch) => ch.is_im && ch.user);
2415
+ if (imChannels.length > 0) {
2416
+ const users2 = await listUsers3(env);
2417
+ for (const u of users2) {
2418
+ userMap.set(u.id, u.username);
2419
+ }
2420
+ }
2421
+ return data.channels.map((ch) => {
2422
+ let type = "channel";
2423
+ if (ch.is_im) type = "dm";
2424
+ else if (ch.is_mpim) type = "group";
2425
+ let slug;
2426
+ let name;
2427
+ if (ch.is_im && ch.user) {
2428
+ const username = userMap.get(ch.user) ?? ch.user;
2429
+ slug = `slack:@${slugify(username)}`;
2430
+ name = username;
2431
+ } else if (ch.name) {
2432
+ slug = `slack:${slugify(teamName)}/${slugify(ch.name)}`;
2433
+ name = ch.name;
2434
+ } else {
2435
+ slug = `slack:${ch.id}`;
2436
+ name = ch.id;
2437
+ }
2438
+ return {
2439
+ id: slug,
2440
+ platformId: ch.id,
2441
+ name,
2442
+ type,
2443
+ participantCount: ch.num_members
2444
+ };
2445
+ });
2446
+ }
2447
+ async function listUsers3(env) {
2448
+ const token = requireToken2(env);
2449
+ const data = await slackApi(token, "users.list", {});
2450
+ return data.members.filter((m) => !m.deleted).map((m) => ({
2451
+ id: m.id,
2452
+ username: m.name,
2453
+ type: m.is_bot ? "bot" : "human"
2454
+ }));
2455
+ }
2456
+ async function createConversation3(env, participants, name) {
2457
+ const token = requireToken2(env);
2458
+ const allUsers = await listUsers3(env);
2459
+ const ids = [];
2460
+ for (const p of participants) {
2461
+ const user = allUsers.find((u) => u.username.toLowerCase() === p.toLowerCase());
2462
+ if (!user) throw new Error(`User not found: ${p}`);
2463
+ ids.push(user.id);
2464
+ }
2465
+ const mindName = env.VOLUTE_MIND;
2466
+ if (name) {
2467
+ const createData = await slackApi(token, "conversations.create", {
2468
+ name,
2469
+ is_private: true
2470
+ });
2471
+ const channelId = createData.channel.id;
2472
+ for (const userId of ids) {
2473
+ await slackApi(token, "conversations.invite", {
2474
+ channel: channelId,
2475
+ users: userId
2476
+ });
2477
+ }
2478
+ const authData = await slackApi(token, "auth.test", {});
2479
+ const teamName = authData.team ?? "workspace";
2480
+ const slug2 = `slack:${slugify(teamName)}/${slugify(name)}`;
2481
+ if (mindName) {
2482
+ writeChannelEntry(mindName, slug2, {
2483
+ platformId: channelId,
2484
+ platform: "slack",
2485
+ name,
2486
+ type: "channel"
2487
+ });
2488
+ }
2489
+ return slug2;
2490
+ }
2491
+ const openData = await slackApi(token, "conversations.open", {
2492
+ users: ids.join(",")
2493
+ });
2494
+ const platformId = openData.channel.id;
2495
+ const slug = participants.length === 1 ? `slack:@${slugify(participants[0])}` : `slack:@${participants.map(slugify).sort().join(",")}`;
2496
+ if (mindName) {
2497
+ writeChannelEntry(mindName, slug, {
2498
+ platformId,
2499
+ platform: "slack",
2500
+ name: participants.join(", "),
2501
+ type: participants.length === 1 ? "dm" : "group"
2502
+ });
2503
+ }
2504
+ return slug;
2505
+ }
2506
+
2507
+ // src/lib/channels/telegram.ts
2508
+ var telegram_exports = {};
2509
+ __export(telegram_exports, {
2510
+ createConversation: () => createConversation4,
2511
+ listConversations: () => listConversations3,
2512
+ listUsers: () => listUsers4,
2513
+ read: () => read3,
2514
+ send: () => send3
2515
+ });
2516
+ var TELEGRAM_MAX_LENGTH = 4096;
2517
+ var API_BASE3 = "https://api.telegram.org";
2518
+ function requireToken3(env) {
2519
+ const token = env.TELEGRAM_BOT_TOKEN;
2520
+ if (!token) throw new Error("TELEGRAM_BOT_TOKEN not set");
2521
+ return token;
2522
+ }
2523
+ async function read3(_env, _channelSlug, _limit) {
2524
+ throw new Error(
2525
+ "Telegram Bot API does not support reading chat history. Use volute send instead."
2526
+ );
2527
+ }
2528
+ async function send3(env, channelSlug, message, images) {
2529
+ const token = requireToken3(env);
2530
+ const chatId = resolveChannelId2(env, channelSlug);
2531
+ if (images?.length) {
2532
+ const CAPTION_MAX = 1024;
2533
+ for (let i = 0; i < images.length; i++) {
2534
+ const img = images[i];
2535
+ const ext = img.media_type.split("/")[1] || "png";
2536
+ const form = new FormData();
2537
+ form.append("chat_id", chatId);
2538
+ form.append(
2539
+ "photo",
2540
+ new Blob([Buffer.from(img.data, "base64")], { type: img.media_type }),
2541
+ `image.${ext}`
2542
+ );
2543
+ if (i === 0 && message) {
2544
+ form.append("caption", message.slice(0, CAPTION_MAX));
2545
+ }
2546
+ const res = await fetch(`${API_BASE3}/bot${token}/sendPhoto`, {
2547
+ method: "POST",
2548
+ body: form
2549
+ });
2550
+ if (!res.ok) {
2551
+ const body = await res.text().catch(() => "");
2552
+ const partial = i > 0 ? ` (${i}/${images.length} images were already sent)` : "";
2553
+ throw new Error(`Telegram API error: ${res.status} ${body}${partial}`);
2554
+ }
2555
+ }
2556
+ if (message && message.length > CAPTION_MAX) {
2557
+ const remaining = message.slice(CAPTION_MAX);
2558
+ const chunks2 = splitMessage(remaining, TELEGRAM_MAX_LENGTH);
2559
+ for (const chunk of chunks2) {
2560
+ const res = await fetch(`${API_BASE3}/bot${token}/sendMessage`, {
2561
+ method: "POST",
2562
+ headers: { "Content-Type": "application/json" },
2563
+ body: JSON.stringify({ chat_id: chatId, text: chunk })
2564
+ });
2565
+ if (!res.ok) {
2566
+ const body = await res.text().catch(() => "");
2567
+ throw new Error(`Telegram API error: ${res.status} ${body}`);
2568
+ }
2569
+ }
2570
+ }
2571
+ return;
2572
+ }
2573
+ const chunks = splitMessage(message, TELEGRAM_MAX_LENGTH);
2574
+ for (let i = 0; i < chunks.length; i++) {
2575
+ const res = await fetch(`${API_BASE3}/bot${token}/sendMessage`, {
2576
+ method: "POST",
2577
+ headers: { "Content-Type": "application/json" },
2578
+ body: JSON.stringify({ chat_id: chatId, text: chunks[i] })
2579
+ });
2580
+ if (!res.ok) {
2581
+ const body = await res.text().catch(() => "");
2582
+ const partial = i > 0 ? ` (${i}/${chunks.length} chunks were already sent)` : "";
2583
+ throw new Error(`Telegram API error: ${res.status} ${body}${partial}`);
2584
+ }
2585
+ }
2586
+ }
2587
+ async function listConversations3() {
2588
+ throw new Error(
2589
+ "Telegram Bot API does not support listing conversations. Users must message the bot first."
2590
+ );
2591
+ }
2592
+ async function listUsers4() {
2593
+ throw new Error(
2594
+ "Telegram Bot API does not support listing users. Users must message the bot first."
2595
+ );
2596
+ }
2597
+ async function createConversation4() {
2598
+ throw new Error(
2599
+ "Telegram Bot API does not support creating conversations. Users must message the bot first."
2600
+ );
2601
+ }
2602
+
2603
+ // src/lib/channels/volute.ts
2604
+ var volute_exports = {};
2605
+ __export(volute_exports, {
2606
+ createConversation: () => createConversation5,
2607
+ listConversations: () => listConversations4,
2608
+ listUsers: () => listUsers5,
2609
+ read: () => read4,
2610
+ send: () => send4
2611
+ });
2612
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
2613
+ import { resolve as resolve8 } from "path";
2614
+ function getDaemonConfig() {
2615
+ const configPath2 = resolve8(voluteHome(), "daemon.json");
2616
+ if (!existsSync7(configPath2)) {
2617
+ throw new Error("Volute daemon is not running");
2618
+ }
2619
+ let config;
2620
+ try {
2621
+ config = JSON.parse(readFileSync5(configPath2, "utf-8"));
2622
+ } catch (err) {
2623
+ throw new Error(`Failed to parse ${configPath2}: ${err}`);
2624
+ }
2625
+ if (typeof config.port !== "number") {
2626
+ throw new Error(`Invalid or missing port in ${configPath2}`);
2627
+ }
2628
+ const url = new URL("http://localhost");
2629
+ url.hostname = config.hostname || "localhost";
2630
+ url.port = String(config.port);
2631
+ return { url: url.origin, token: config.token };
2632
+ }
2633
+ async function read4(env, channelSlug, limit) {
2634
+ const mindName = env.VOLUTE_MIND;
2635
+ if (!mindName) throw new Error("VOLUTE_MIND not set");
2636
+ const conversationId = resolveChannelId2(env, channelSlug);
2637
+ const { url, token } = getDaemonConfig();
2638
+ const headers = { Origin: url };
2639
+ if (token) headers.Authorization = `Bearer ${token}`;
2640
+ const res = await fetch(
2641
+ `${url}/api/minds/${encodeURIComponent(mindName)}/conversations/${encodeURIComponent(conversationId)}/messages`,
2642
+ { headers }
2643
+ );
2644
+ if (!res.ok) {
2645
+ throw new Error(`Failed to read conversation: ${res.status} ${res.statusText}`);
2646
+ }
2647
+ const messages2 = await res.json();
2648
+ return messages2.slice(-limit).map((m) => {
2649
+ const text = Array.isArray(m.content) ? m.content.filter((b) => b.type === "text").map((b) => b.text).join("") : m.content;
2650
+ return `${m.sender_name ?? m.role}: ${text}`;
2651
+ }).join("\n");
2652
+ }
2653
+ async function send4(env, channelSlug, message, images) {
2654
+ const mindName = env.VOLUTE_MIND;
2655
+ if (!mindName) throw new Error("VOLUTE_MIND not set");
2656
+ const conversationId = resolveChannelId2(env, channelSlug);
2657
+ const { url, token } = getDaemonConfig();
2658
+ const headers = {
2659
+ "Content-Type": "application/json",
2660
+ Origin: url
2661
+ };
2662
+ if (token) headers.Authorization = `Bearer ${token}`;
2663
+ const res = await fetch(`${url}/api/minds/${encodeURIComponent(mindName)}/chat`, {
2664
+ method: "POST",
2665
+ headers,
2666
+ body: JSON.stringify({
2667
+ message,
2668
+ conversationId,
2669
+ sender: env.VOLUTE_SENDER ?? mindName,
2670
+ images
2671
+ })
2672
+ });
2673
+ if (!res.ok) {
2674
+ const data = await res.json().catch(() => ({}));
2675
+ throw new Error(data.error ?? `Failed to send: ${res.status}`);
2676
+ }
2677
+ }
2678
+ async function listConversations4(env) {
2679
+ const mindName = env.VOLUTE_MIND;
2680
+ if (!mindName) throw new Error("VOLUTE_MIND not set");
2681
+ const { url, token } = getDaemonConfig();
2682
+ const headers = { Origin: url };
2683
+ if (token) headers.Authorization = `Bearer ${token}`;
2684
+ const res = await fetch(`${url}/api/minds/${encodeURIComponent(mindName)}/conversations`, {
2685
+ headers
2686
+ });
2687
+ if (!res.ok) {
2688
+ throw new Error(`Failed to list conversations: ${res.status} ${res.statusText}`);
2689
+ }
2690
+ const convs = await res.json();
2691
+ const results = [];
2692
+ for (const conv of convs) {
2693
+ let participants = [];
2694
+ try {
2695
+ const pRes = await fetch(
2696
+ `${url}/api/minds/${encodeURIComponent(mindName)}/conversations/${encodeURIComponent(conv.id)}/participants`,
2697
+ { headers }
2698
+ );
2699
+ if (pRes.ok) {
2700
+ participants = await pRes.json();
2701
+ } else {
2702
+ console.error(`[volute] failed to fetch participants for ${conv.id}: HTTP ${pRes.status}`);
2703
+ }
2704
+ } catch (err) {
2705
+ console.error(`[volute] failed to fetch participants for ${conv.id}:`, err);
2706
+ }
2707
+ const slug = buildVoluteSlug({
2708
+ participants,
2709
+ mindUsername: mindName,
2710
+ convTitle: conv.title,
2711
+ conversationId: conv.id,
2712
+ convType: conv.type,
2713
+ convName: conv.name
2714
+ });
2715
+ const convType = conv.type === "channel" ? "channel" : participants.length === 2 ? "dm" : "group";
2716
+ results.push({
2717
+ id: slug,
2718
+ platformId: conv.id,
2719
+ name: conv.type === "channel" ? `#${conv.name}` : conv.title ?? "(untitled)",
2720
+ type: convType,
2721
+ participantCount: participants.length
2722
+ });
2723
+ }
2724
+ return results;
2725
+ }
2726
+ async function listUsers5(_env) {
2727
+ const { url, token } = getDaemonConfig();
2728
+ const headers = { Origin: url };
2729
+ if (token) headers.Authorization = `Bearer ${token}`;
2730
+ const res = await fetch(`${url}/api/auth/users`, { headers });
2731
+ if (!res.ok) {
2732
+ throw new Error(`Failed to list users: ${res.status} ${res.statusText}`);
2074
2733
  }
2075
- return c.json({ id: user.id, username: user.username, role: user.role });
2076
- }).post("/login", zValidator("json", credentialsSchema), async (c) => {
2077
- const { username, password } = c.req.valid("json");
2078
- const user = await verifyUser(username, password);
2079
- if (!user) {
2080
- return c.json({ error: "Invalid credentials" }, 401);
2734
+ const data = await res.json();
2735
+ return data.map((u) => ({
2736
+ id: String(u.id),
2737
+ username: u.username,
2738
+ type: u.user_type
2739
+ }));
2740
+ }
2741
+ async function createConversation5(env, participants, name) {
2742
+ const mindName = env.VOLUTE_MIND;
2743
+ if (!mindName) throw new Error("VOLUTE_MIND not set");
2744
+ const { url, token } = getDaemonConfig();
2745
+ const headers = {
2746
+ "Content-Type": "application/json",
2747
+ Origin: url
2748
+ };
2749
+ if (token) headers.Authorization = `Bearer ${token}`;
2750
+ const res = await fetch(`${url}/api/minds/${encodeURIComponent(mindName)}/conversations`, {
2751
+ method: "POST",
2752
+ headers,
2753
+ body: JSON.stringify({ participantNames: participants, title: name })
2754
+ });
2755
+ if (!res.ok) {
2756
+ const data = await res.json().catch(() => ({}));
2757
+ throw new Error(data.error ?? `Failed to create conversation: ${res.status}`);
2081
2758
  }
2082
- const sessionId = await createSession(user.id);
2083
- setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
2084
- return c.json({ id: user.id, username: user.username, role: user.role });
2085
- }).post("/logout", async (c) => {
2086
- const sessionId = getCookie2(c, "volute_session");
2087
- if (sessionId) {
2088
- await deleteSession(sessionId);
2089
- deleteCookie(c, "volute_session", { path: "/" });
2759
+ const conv = await res.json();
2760
+ return `volute:${conv.id}`;
2761
+ }
2762
+
2763
+ // src/lib/channels.ts
2764
+ var CHANNELS = {
2765
+ volute: {
2766
+ name: "volute",
2767
+ displayName: "Volute",
2768
+ showToolCalls: true,
2769
+ builtIn: true,
2770
+ driver: volute_exports
2771
+ },
2772
+ discord: {
2773
+ name: "discord",
2774
+ displayName: "Discord",
2775
+ showToolCalls: false,
2776
+ driver: discord_exports
2777
+ },
2778
+ slack: {
2779
+ name: "slack",
2780
+ displayName: "Slack",
2781
+ showToolCalls: false,
2782
+ driver: slack_exports
2783
+ },
2784
+ telegram: {
2785
+ name: "telegram",
2786
+ displayName: "Telegram",
2787
+ showToolCalls: false,
2788
+ driver: telegram_exports
2789
+ },
2790
+ mail: { name: "mail", displayName: "Email", showToolCalls: false },
2791
+ system: { name: "system", displayName: "System", showToolCalls: false }
2792
+ };
2793
+ function getChannelDriver(platform) {
2794
+ return CHANNELS[platform]?.driver ?? null;
2795
+ }
2796
+ function resolveChannelId2(env, slug) {
2797
+ const mindName = env.VOLUTE_MIND;
2798
+ if (!mindName) {
2799
+ const colonIdx = slug.indexOf(":");
2800
+ return colonIdx !== -1 ? slug.slice(colonIdx + 1) : slug;
2090
2801
  }
2091
- return c.json({ ok: true });
2092
- }).get("/me", async (c) => {
2093
- const sessionId = getCookie2(c, "volute_session");
2094
- if (!sessionId) return c.json({ error: "Not logged in" }, 401);
2095
- const userId = await getSessionUserId(sessionId);
2096
- if (userId == null) return c.json({ error: "Not logged in" }, 401);
2097
- const user = await getUser(userId);
2098
- if (!user) return c.json({ error: "Not logged in" }, 401);
2099
- return c.json({ id: user.id, username: user.username, role: user.role });
2100
- }).route("/", admin).route("/", authenticated);
2101
- var auth_default = app2;
2802
+ return resolveChannelId(mindName, slug);
2803
+ }
2102
2804
 
2103
2805
  // src/web/api/channels.ts
2104
- import { Hono as Hono3 } from "hono";
2105
2806
  function buildEnv(name) {
2106
2807
  return { ...loadMergedEnv(name), VOLUTE_MIND: name, VOLUTE_MIND_DIR: mindDir(name) };
2107
2808
  }
2108
2809
  var app3 = new Hono3().post("/:name/channels/send", requireAdmin, async (c) => {
2109
2810
  const name = c.req.param("name");
2110
2811
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
2111
- const { platform, uri, message, images } = await c.req.json();
2812
+ const { platform, uri, message, images, sender } = await c.req.json();
2112
2813
  const driver = getChannelDriver(platform);
2113
2814
  if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
2114
2815
  const env = buildEnv(name);
2816
+ if (sender) env.VOLUTE_SENDER = sender;
2115
2817
  try {
2116
2818
  await driver.send(env, uri, message, images);
2117
2819
  return c.json({ ok: true });
@@ -2181,13 +2883,15 @@ var app3 = new Hono3().post("/:name/channels/send", requireAdmin, async (c) => {
2181
2883
  const {
2182
2884
  platform,
2183
2885
  participants,
2184
- name: convName
2886
+ name: convName,
2887
+ sender
2185
2888
  } = await c.req.json();
2186
2889
  const driver = getChannelDriver(platform);
2187
2890
  if (!driver?.createConversation) {
2188
2891
  return c.json({ error: `Platform ${platform} does not support creating conversations` }, 400);
2189
2892
  }
2190
2893
  const env = buildEnv(name);
2894
+ if (sender) env.VOLUTE_SENDER = sender;
2191
2895
  try {
2192
2896
  const slug = await driver.createConversation(env, participants, convName);
2193
2897
  return c.json({ slug });
@@ -2351,14 +3055,14 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
2351
3055
  var env_default = app5;
2352
3056
 
2353
3057
  // src/web/api/file-sharing.ts
2354
- import { readFileSync as readFileSync6, statSync as statSync2 } from "fs";
2355
- import { resolve as resolve9 } from "path";
3058
+ import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3059
+ import { resolve as resolve10 } from "path";
2356
3060
  import { Hono as Hono6 } from "hono";
2357
3061
 
2358
3062
  // src/lib/file-sharing.ts
2359
3063
  import { randomBytes } from "crypto";
2360
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
2361
- import { basename, join as join2, normalize, resolve as resolve8 } from "path";
3064
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
3065
+ import { basename, join as join2, normalize, resolve as resolve9 } from "path";
2362
3066
  function validateFilePath(filePath) {
2363
3067
  if (!filePath) return "File path is required";
2364
3068
  const normalized = normalize(filePath);
@@ -2371,13 +3075,13 @@ function validateFilePath(filePath) {
2371
3075
  return null;
2372
3076
  }
2373
3077
  function configPath(dir) {
2374
- return resolve8(dir, "home", ".config", "file-sharing.json");
3078
+ return resolve9(dir, "home", ".config", "file-sharing.json");
2375
3079
  }
2376
3080
  function readFileSharingConfig(dir) {
2377
3081
  const p = configPath(dir);
2378
- if (!existsSync7(p)) return {};
3082
+ if (!existsSync8(p)) return {};
2379
3083
  try {
2380
- return JSON.parse(readFileSync5(p, "utf-8"));
3084
+ return JSON.parse(readFileSync6(p, "utf-8"));
2381
3085
  } catch (err) {
2382
3086
  console.warn(`[file-sharing] failed to parse config at ${p}:`, err);
2383
3087
  return {};
@@ -2385,7 +3089,7 @@ function readFileSharingConfig(dir) {
2385
3089
  }
2386
3090
  function writeFileSharingConfig(dir, config) {
2387
3091
  const p = configPath(dir);
2388
- mkdirSync4(resolve8(p, ".."), { recursive: true });
3092
+ mkdirSync4(resolve9(p, ".."), { recursive: true });
2389
3093
  writeFileSync4(p, `${JSON.stringify(config, null, 2)}
2390
3094
  `);
2391
3095
  }
@@ -2409,7 +3113,7 @@ function removeTrust(dir, sender) {
2409
3113
  writeFileSharingConfig(dir, config);
2410
3114
  }
2411
3115
  function pendingDir(receiver) {
2412
- return resolve8(stateDir(receiver), "pending-files");
3116
+ return resolve9(stateDir(receiver), "pending-files");
2413
3117
  }
2414
3118
  function validateId(id) {
2415
3119
  if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
@@ -2428,7 +3132,7 @@ function stageFile(receiver, sender, filename, content, originalPath) {
2428
3132
  throw new Error("Invalid sender name");
2429
3133
  }
2430
3134
  const id = generateId(sender);
2431
- const dir = resolve8(pendingDir(receiver), id);
3135
+ const dir = resolve9(pendingDir(receiver), id);
2432
3136
  mkdirSync4(dir, { recursive: true });
2433
3137
  const metadata = {
2434
3138
  id,
@@ -2438,22 +3142,22 @@ function stageFile(receiver, sender, filename, content, originalPath) {
2438
3142
  size: content.length,
2439
3143
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2440
3144
  };
2441
- writeFileSync4(resolve8(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
3145
+ writeFileSync4(resolve9(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
2442
3146
  `);
2443
- writeFileSync4(resolve8(dir, "data"), content);
3147
+ writeFileSync4(resolve9(dir, "data"), content);
2444
3148
  return { id };
2445
3149
  }
2446
3150
  function listPending(receiver) {
2447
3151
  const dir = pendingDir(receiver);
2448
- if (!existsSync7(dir)) return [];
3152
+ if (!existsSync8(dir)) return [];
2449
3153
  const entries = readdirSync3(dir, { withFileTypes: true });
2450
3154
  const result = [];
2451
3155
  for (const entry of entries) {
2452
3156
  if (!entry.isDirectory()) continue;
2453
- const metaPath = resolve8(dir, entry.name, "metadata.json");
2454
- if (!existsSync7(metaPath)) continue;
3157
+ const metaPath = resolve9(dir, entry.name, "metadata.json");
3158
+ if (!existsSync8(metaPath)) continue;
2455
3159
  try {
2456
- result.push(JSON.parse(readFileSync5(metaPath, "utf-8")));
3160
+ result.push(JSON.parse(readFileSync6(metaPath, "utf-8")));
2457
3161
  } catch (err) {
2458
3162
  console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
2459
3163
  }
@@ -2462,10 +3166,10 @@ function listPending(receiver) {
2462
3166
  }
2463
3167
  function getPending(receiver, id) {
2464
3168
  validateId(id);
2465
- const metaPath = resolve8(pendingDir(receiver), id, "metadata.json");
2466
- if (!existsSync7(metaPath)) return null;
3169
+ const metaPath = resolve9(pendingDir(receiver), id, "metadata.json");
3170
+ if (!existsSync8(metaPath)) return null;
2467
3171
  try {
2468
- return JSON.parse(readFileSync5(metaPath, "utf-8"));
3172
+ return JSON.parse(readFileSync6(metaPath, "utf-8"));
2469
3173
  } catch (err) {
2470
3174
  console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
2471
3175
  return null;
@@ -2480,27 +3184,27 @@ function deliverFile(receiverDir, sender, filename, content, inboxPath) {
2480
3184
  if (sender.includes("/") || sender.includes("\\")) {
2481
3185
  throw new Error("Invalid sender name");
2482
3186
  }
2483
- const destDir = resolve8(receiverDir, "home", inbox, sender);
3187
+ const destDir = resolve9(receiverDir, "home", inbox, sender);
2484
3188
  mkdirSync4(destDir, { recursive: true });
2485
- const destPath = resolve8(destDir, basename(filename));
3189
+ const destPath = resolve9(destDir, basename(filename));
2486
3190
  writeFileSync4(destPath, content);
2487
3191
  return join2(inbox, sender, basename(filename));
2488
3192
  }
2489
3193
  function acceptPending(receiver, id, receiverDir) {
2490
3194
  const meta = getPending(receiver, id);
2491
3195
  if (!meta) throw new Error(`Pending file not found: ${id}`);
2492
- const dataPath = resolve8(pendingDir(receiver), id, "data");
2493
- const content = readFileSync5(dataPath);
3196
+ const dataPath = resolve9(pendingDir(receiver), id, "data");
3197
+ const content = readFileSync6(dataPath);
2494
3198
  const config = readFileSharingConfig(receiverDir);
2495
3199
  const inboxPath = config.inboxPath ?? "inbox";
2496
3200
  const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
2497
- rmSync(resolve8(pendingDir(receiver), id), { recursive: true });
3201
+ rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
2498
3202
  return { sender: meta.sender, filename: meta.filename, destPath };
2499
3203
  }
2500
3204
  function rejectPending(receiver, id) {
2501
3205
  const meta = getPending(receiver, id);
2502
3206
  if (!meta) throw new Error(`Pending file not found: ${id}`);
2503
- rmSync(resolve8(pendingDir(receiver), id), { recursive: true });
3207
+ rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
2504
3208
  return { sender: meta.sender, filename: meta.filename };
2505
3209
  }
2506
3210
  function formatFileSize(bytes) {
@@ -2541,21 +3245,21 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
2541
3245
  const pathErr = validateFilePath(body.filePath);
2542
3246
  if (pathErr) return c.json({ error: pathErr }, 400);
2543
3247
  const senderDir = mindDir(senderName);
2544
- const filePath = resolve9(senderDir, "home", body.filePath);
3248
+ const filePath = resolve10(senderDir, "home", body.filePath);
2545
3249
  const MAX_FILE_SIZE = 50 * 1024 * 1024;
2546
- const stat3 = statSync2(filePath, { throwIfNoEntry: false });
2547
- if (!stat3) return c.json({ error: `File not found: ${body.filePath}` }, 404);
2548
- if (stat3.size > MAX_FILE_SIZE) {
3250
+ const stat4 = statSync2(filePath, { throwIfNoEntry: false });
3251
+ if (!stat4) return c.json({ error: `File not found: ${body.filePath}` }, 404);
3252
+ if (stat4.size > MAX_FILE_SIZE) {
2549
3253
  return c.json(
2550
3254
  {
2551
- error: `File too large (${formatFileSize(stat3.size)}, max ${formatFileSize(MAX_FILE_SIZE)})`
3255
+ error: `File too large (${formatFileSize(stat4.size)}, max ${formatFileSize(MAX_FILE_SIZE)})`
2552
3256
  },
2553
3257
  413
2554
3258
  );
2555
3259
  }
2556
3260
  let content;
2557
3261
  try {
2558
- content = readFileSync6(filePath);
3262
+ content = readFileSync7(filePath);
2559
3263
  } catch {
2560
3264
  return c.json({ error: `File not found: ${body.filePath}` }, 404);
2561
3265
  }
@@ -2657,18 +3361,61 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
2657
3361
  var file_sharing_default = app6;
2658
3362
 
2659
3363
  // src/web/api/files.ts
2660
- import { existsSync as existsSync8 } from "fs";
2661
- import { readdir, readFile } from "fs/promises";
2662
- import { resolve as resolve10 } from "path";
3364
+ import { existsSync as existsSync9 } from "fs";
3365
+ import { readdir, readFile, realpath, stat } from "fs/promises";
3366
+ import { extname, resolve as resolve11 } from "path";
2663
3367
  import { Hono as Hono7 } from "hono";
2664
3368
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
2665
- var app7 = new Hono7().get("/:name/files", async (c) => {
3369
+ var AVATAR_MIME = {
3370
+ ".png": "image/png",
3371
+ ".jpg": "image/jpeg",
3372
+ ".jpeg": "image/jpeg",
3373
+ ".gif": "image/gif",
3374
+ ".webp": "image/webp"
3375
+ };
3376
+ var MAX_AVATAR_SIZE = 2 * 1024 * 1024;
3377
+ var app7 = new Hono7().get("/:name/avatar", async (c) => {
3378
+ const name = c.req.param("name");
3379
+ const entry = findMind(name);
3380
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3381
+ const dir = mindDir(name);
3382
+ const config = readVoluteConfig(dir);
3383
+ if (!config?.avatar) return c.json({ error: "No avatar configured" }, 404);
3384
+ const ext = extname(config.avatar).toLowerCase();
3385
+ const mime = AVATAR_MIME[ext];
3386
+ if (!mime) return c.json({ error: "Invalid avatar extension" }, 400);
3387
+ const homeDir = resolve11(dir, "home");
3388
+ const avatarPath = resolve11(homeDir, config.avatar);
3389
+ if (!avatarPath.startsWith(`${homeDir}/`)) return c.json({ error: "Invalid avatar path" }, 400);
3390
+ let realAvatarPath;
3391
+ try {
3392
+ const realHome = await realpath(homeDir);
3393
+ realAvatarPath = await realpath(avatarPath);
3394
+ if (!realAvatarPath.startsWith(`${realHome}/`))
3395
+ return c.json({ error: "Invalid avatar path" }, 400);
3396
+ } catch (err) {
3397
+ if (err.code === "ENOENT")
3398
+ return c.json({ error: "Avatar file not found" }, 404);
3399
+ return c.json({ error: "Failed to resolve avatar path" }, 500);
3400
+ }
3401
+ try {
3402
+ const fileStat = await stat(realAvatarPath);
3403
+ if (fileStat.size > MAX_AVATAR_SIZE) return c.json({ error: "Avatar file too large" }, 400);
3404
+ const body = await readFile(realAvatarPath);
3405
+ return c.body(body, 200, {
3406
+ "Content-Type": mime,
3407
+ "Cache-Control": "public, max-age=300"
3408
+ });
3409
+ } catch {
3410
+ return c.json({ error: "Failed to read avatar file" }, 500);
3411
+ }
3412
+ }).get("/:name/files", async (c) => {
2666
3413
  const name = c.req.param("name");
2667
3414
  const entry = findMind(name);
2668
3415
  if (!entry) return c.json({ error: "Mind not found" }, 404);
2669
3416
  const dir = mindDir(name);
2670
- const homeDir = resolve10(dir, "home");
2671
- if (!existsSync8(homeDir)) return c.json({ error: "Home directory missing" }, 404);
3417
+ const homeDir = resolve11(dir, "home");
3418
+ if (!existsSync9(homeDir)) return c.json({ error: "Home directory missing" }, 404);
2672
3419
  const allFiles = await readdir(homeDir);
2673
3420
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
2674
3421
  return c.json(files);
@@ -2681,8 +3428,8 @@ var app7 = new Hono7().get("/:name/files", async (c) => {
2681
3428
  const entry = findMind(name);
2682
3429
  if (!entry) return c.json({ error: "Mind not found" }, 404);
2683
3430
  const dir = mindDir(name);
2684
- const filePath = resolve10(dir, "home", filename);
2685
- if (!existsSync8(filePath)) {
3431
+ const filePath = resolve11(dir, "home", filename);
3432
+ if (!existsSync9(filePath)) {
2686
3433
  return c.json({ error: "File not found" }, 404);
2687
3434
  }
2688
3435
  const content = await readFile(filePath, "utf-8");
@@ -2695,17 +3442,17 @@ import { Hono as Hono8 } from "hono";
2695
3442
 
2696
3443
  // src/lib/identity.ts
2697
3444
  import { createHash, generateKeyPairSync, sign, verify } from "crypto";
2698
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2699
- import { resolve as resolve11 } from "path";
3445
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3446
+ import { resolve as resolve12 } from "path";
2700
3447
  function generateIdentity(mindDir2) {
2701
- const identityDir = resolve11(mindDir2, ".mind/identity");
3448
+ const identityDir = resolve12(mindDir2, ".mind/identity");
2702
3449
  mkdirSync5(identityDir, { recursive: true });
2703
3450
  const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
2704
3451
  publicKeyEncoding: { type: "spki", format: "pem" },
2705
3452
  privateKeyEncoding: { type: "pkcs8", format: "pem" }
2706
3453
  });
2707
- const privatePath = resolve11(identityDir, "private.pem");
2708
- const publicPath = resolve11(identityDir, "public.pem");
3454
+ const privatePath = resolve12(identityDir, "private.pem");
3455
+ const publicPath = resolve12(identityDir, "public.pem");
2709
3456
  writeFileSync5(privatePath, privateKey, { mode: 384 });
2710
3457
  writeFileSync5(publicPath, publicKey, { mode: 420 });
2711
3458
  const config = readVoluteConfig(mindDir2) ?? {};
@@ -2720,17 +3467,17 @@ function getPrivateKey(mindDir2) {
2720
3467
  const config = readVoluteConfig(mindDir2);
2721
3468
  const relPath = config?.identity?.privateKey;
2722
3469
  if (!relPath) return null;
2723
- const fullPath = resolve11(mindDir2, relPath);
2724
- if (!existsSync9(fullPath)) return null;
2725
- return readFileSync7(fullPath, "utf-8");
3470
+ const fullPath = resolve12(mindDir2, relPath);
3471
+ if (!existsSync10(fullPath)) return null;
3472
+ return readFileSync8(fullPath, "utf-8");
2726
3473
  }
2727
3474
  function getPublicKey(mindDir2) {
2728
3475
  const config = readVoluteConfig(mindDir2);
2729
3476
  const relPath = config?.identity?.publicKey;
2730
3477
  if (!relPath) return null;
2731
- const fullPath = resolve11(mindDir2, relPath);
2732
- if (!existsSync9(fullPath)) return null;
2733
- return readFileSync7(fullPath, "utf-8");
3478
+ const fullPath = resolve12(mindDir2, relPath);
3479
+ if (!existsSync10(fullPath)) return null;
3480
+ return readFileSync8(fullPath, "utf-8");
2734
3481
  }
2735
3482
  function getFingerprint(publicKeyPem) {
2736
3483
  return createHash("sha256").update(publicKeyPem).digest("hex");
@@ -2783,16 +3530,16 @@ var keys_default = app8;
2783
3530
 
2784
3531
  // src/web/api/logs.ts
2785
3532
  import { spawn as spawn2 } from "child_process";
2786
- import { existsSync as existsSync10 } from "fs";
2787
- import { resolve as resolve12 } from "path";
3533
+ import { existsSync as existsSync11 } from "fs";
3534
+ import { resolve as resolve13 } from "path";
2788
3535
  import { Hono as Hono9 } from "hono";
2789
3536
  import { streamSSE as streamSSE2 } from "hono/streaming";
2790
3537
  var app9 = new Hono9().get("/:name/logs", async (c) => {
2791
3538
  const name = c.req.param("name");
2792
3539
  const entry = findMind(name);
2793
3540
  if (!entry) return c.json({ error: "Mind not found" }, 404);
2794
- const logFile = resolve12(stateDir(name), "logs", "mind.log");
2795
- if (!existsSync10(logFile)) {
3541
+ const logFile = resolve13(stateDir(name), "logs", "mind.log");
3542
+ if (!existsSync11(logFile)) {
2796
3543
  return c.json({ error: "No log file found" }, 404);
2797
3544
  }
2798
3545
  return streamSSE2(c, async (stream) => {
@@ -2819,8 +3566,8 @@ var app9 = new Hono9().get("/:name/logs", async (c) => {
2819
3566
  const name = c.req.param("name");
2820
3567
  const entry = findMind(name);
2821
3568
  if (!entry) return c.json({ error: "Mind not found" }, 404);
2822
- const logFile = resolve12(stateDir(name), "logs", "mind.log");
2823
- if (!existsSync10(logFile)) {
3569
+ const logFile = resolve13(stateDir(name), "logs", "mind.log");
3570
+ if (!existsSync11(logFile)) {
2824
3571
  return c.json({ error: "No log file found" }, 404);
2825
3572
  }
2826
3573
  const nParam = parseInt(c.req.query("n") ?? "50", 10);
@@ -2920,13 +3667,13 @@ var mind_skills_default = app10;
2920
3667
 
2921
3668
  // src/web/api/minds.ts
2922
3669
  import {
2923
- cpSync as cpSync2,
3670
+ cpSync,
2924
3671
  existsSync as existsSync12,
2925
- mkdirSync as mkdirSync8,
2926
- readdirSync as readdirSync6,
3672
+ mkdirSync as mkdirSync7,
3673
+ readdirSync as readdirSync5,
2927
3674
  readFileSync as readFileSync11,
2928
- rmSync as rmSync3,
2929
- writeFileSync as writeFileSync9
3675
+ rmSync as rmSync2,
3676
+ writeFileSync as writeFileSync8
2930
3677
  } from "fs";
2931
3678
  import { resolve as resolve16 } from "path";
2932
3679
  import { zValidator as zValidator3 } from "@hono/zod-validator";
@@ -2935,19 +3682,19 @@ import { Hono as Hono11 } from "hono";
2935
3682
  import { z as z3 } from "zod";
2936
3683
 
2937
3684
  // src/lib/consolidate.ts
2938
- import { readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
2939
- import { resolve as resolve13 } from "path";
3685
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
3686
+ import { resolve as resolve14 } from "path";
2940
3687
  async function consolidateMemory(mindDir2) {
2941
- const soulPath = resolve13(mindDir2, "home/SOUL.md");
2942
- const memoryPath = resolve13(mindDir2, "home/MEMORY.md");
2943
- const memoryDir = resolve13(mindDir2, "home/memory");
2944
- const soul = readFileSync8(soulPath, "utf-8");
3688
+ const soulPath = resolve14(mindDir2, "home/SOUL.md");
3689
+ const memoryPath = resolve14(mindDir2, "home/MEMORY.md");
3690
+ const memoryDir = resolve14(mindDir2, "home/memory");
3691
+ const soul = readFileSync9(soulPath, "utf-8");
2945
3692
  const logs = [];
2946
3693
  try {
2947
3694
  const files = readdirSync4(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2948
3695
  for (const filename of files) {
2949
3696
  const date = filename.replace(".md", "");
2950
- const content2 = readFileSync8(resolve13(memoryDir, filename), "utf-8").trim();
3697
+ const content2 = readFileSync9(resolve14(memoryDir, filename), "utf-8").trim();
2951
3698
  if (content2) {
2952
3699
  logs.push(`### ${date}
2953
3700
 
@@ -3007,11 +3754,11 @@ ${content2}`);
3007
3754
 
3008
3755
  // src/lib/convert-session.ts
3009
3756
  import { randomUUID as randomUUID2 } from "crypto";
3010
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
3757
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
3011
3758
  import { homedir } from "os";
3012
- import { resolve as resolve14 } from "path";
3759
+ import { resolve as resolve15 } from "path";
3013
3760
  function convertSession(opts) {
3014
- const lines = readFileSync9(opts.sessionPath, "utf-8").trim().split("\n");
3761
+ const lines = readFileSync10(opts.sessionPath, "utf-8").trim().split("\n");
3015
3762
  const sessionId = randomUUID2();
3016
3763
  const idMap = /* @__PURE__ */ new Map();
3017
3764
  const messages2 = [];
@@ -3125,9 +3872,9 @@ function convertSession(opts) {
3125
3872
  }
3126
3873
  }
3127
3874
  const projectId = opts.projectDir.replace(/\//g, "-");
3128
- const sdkDir = resolve14(homedir(), ".claude", "projects", projectId);
3875
+ const sdkDir = resolve15(homedir(), ".claude", "projects", projectId);
3129
3876
  mkdirSync6(sdkDir, { recursive: true });
3130
- const sdkPath = resolve14(sdkDir, `${sessionId}.jsonl`);
3877
+ const sdkPath = resolve15(sdkDir, `${sessionId}.jsonl`);
3131
3878
  writeFileSync7(sdkPath, `${sdkEvents.join("\n")}
3132
3879
  `);
3133
3880
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
@@ -3179,7 +3926,7 @@ function convertAssistantContent(content) {
3179
3926
  return result;
3180
3927
  }
3181
3928
 
3182
- // src/lib/mind-events.ts
3929
+ // src/lib/events/mind-events.ts
3183
3930
  var subscribers = /* @__PURE__ */ new Map();
3184
3931
  function subscribe3(mind, callback) {
3185
3932
  let set = subscribers.get(mind);
@@ -3207,111 +3954,6 @@ function publish3(mind, event) {
3207
3954
  }
3208
3955
  }
3209
3956
 
3210
- // src/lib/template.ts
3211
- import {
3212
- cpSync,
3213
- existsSync as existsSync11,
3214
- mkdirSync as mkdirSync7,
3215
- readdirSync as readdirSync5,
3216
- readFileSync as readFileSync10,
3217
- renameSync as renameSync3,
3218
- rmSync as rmSync2,
3219
- statSync as statSync3,
3220
- writeFileSync as writeFileSync8
3221
- } from "fs";
3222
- import { tmpdir } from "os";
3223
- import { dirname as dirname2, join as join3, relative, resolve as resolve15 } from "path";
3224
- function findTemplatesRoot() {
3225
- let dir = dirname2(new URL(import.meta.url).pathname);
3226
- for (let i = 0; i < 5; i++) {
3227
- const candidate = resolve15(dir, "templates");
3228
- if (existsSync11(resolve15(candidate, "_base"))) return candidate;
3229
- dir = dirname2(dir);
3230
- }
3231
- console.error(
3232
- "Templates directory not found. Searched up from:",
3233
- dirname2(new URL(import.meta.url).pathname)
3234
- );
3235
- process.exit(1);
3236
- }
3237
- function composeTemplate(templatesRoot, templateName) {
3238
- const baseDir = resolve15(templatesRoot, "_base");
3239
- const templateDir = resolve15(templatesRoot, templateName);
3240
- if (!existsSync11(baseDir)) {
3241
- console.error("Base template not found:", baseDir);
3242
- process.exit(1);
3243
- }
3244
- if (!existsSync11(templateDir)) {
3245
- console.error(`Template not found: ${templateName}`);
3246
- process.exit(1);
3247
- }
3248
- const composedDir = resolve15(tmpdir(), `volute-template-${Date.now()}`);
3249
- mkdirSync7(composedDir, { recursive: true });
3250
- cpSync(baseDir, composedDir, { recursive: true });
3251
- for (const file of listFiles(templateDir)) {
3252
- const src = resolve15(templateDir, file);
3253
- const dest = resolve15(composedDir, file);
3254
- mkdirSync7(dirname2(dest), { recursive: true });
3255
- cpSync(src, dest);
3256
- }
3257
- const manifestPath = resolve15(composedDir, "volute-template.json");
3258
- if (!existsSync11(manifestPath)) {
3259
- rmSync2(composedDir, { recursive: true, force: true });
3260
- console.error(`Template manifest not found: ${templateName}/volute-template.json`);
3261
- process.exit(1);
3262
- }
3263
- const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
3264
- rmSync2(manifestPath);
3265
- return { composedDir, manifest };
3266
- }
3267
- function copyTemplateToDir(composedDir, destDir, mindName, manifest) {
3268
- cpSync(composedDir, destDir, { recursive: true });
3269
- for (const [from, to] of Object.entries(manifest.rename)) {
3270
- const fromPath = resolve15(destDir, from);
3271
- if (existsSync11(fromPath)) {
3272
- renameSync3(fromPath, resolve15(destDir, to));
3273
- }
3274
- }
3275
- for (const file of manifest.substitute) {
3276
- const path = resolve15(destDir, file);
3277
- if (existsSync11(path)) {
3278
- const content = readFileSync10(path, "utf-8");
3279
- writeFileSync8(path, content.replaceAll("{{name}}", mindName));
3280
- }
3281
- }
3282
- }
3283
- function applyInitFiles(destDir) {
3284
- const initDir = resolve15(destDir, ".init");
3285
- if (!existsSync11(initDir)) return;
3286
- const homeDir = resolve15(destDir, "home");
3287
- for (const file of listFiles(initDir)) {
3288
- const src = resolve15(initDir, file);
3289
- const dest = resolve15(homeDir, file);
3290
- const parent = dirname2(dest);
3291
- if (!existsSync11(parent)) {
3292
- mkdirSync7(parent, { recursive: true });
3293
- }
3294
- cpSync(src, dest);
3295
- }
3296
- rmSync2(initDir, { recursive: true, force: true });
3297
- }
3298
- function listFiles(dir) {
3299
- const results = [];
3300
- function walk(current) {
3301
- for (const entry of readdirSync5(current)) {
3302
- const full = join3(current, entry);
3303
- if (statSync3(full).isDirectory()) {
3304
- if (entry === ".git") continue;
3305
- walk(full);
3306
- } else {
3307
- results.push(relative(dir, full));
3308
- }
3309
- }
3310
- }
3311
- walk(dir);
3312
- return results;
3313
- }
3314
-
3315
3957
  // src/web/api/minds.ts
3316
3958
  async function getMindStatus(name, port) {
3317
3959
  const manager = getMindManager();
@@ -3320,7 +3962,8 @@ async function getMindStatus(name, port) {
3320
3962
  const health = await checkHealth(port);
3321
3963
  status = health.ok ? "running" : "starting";
3322
3964
  }
3323
- const channelConfig = readVoluteConfig(mindDir(name))?.channels;
3965
+ const config = readVoluteConfig(mindDir(name));
3966
+ const channelConfig = config?.channels;
3324
3967
  const channels = [];
3325
3968
  for (const [, provider] of Object.entries(CHANNELS)) {
3326
3969
  if (!provider.builtIn) continue;
@@ -3341,7 +3984,13 @@ async function getMindStatus(name, port) {
3341
3984
  showToolCalls: channelConfig?.[cs.type]?.showToolCalls ?? provider?.showToolCalls ?? false
3342
3985
  });
3343
3986
  }
3344
- return { status, channels };
3987
+ return {
3988
+ status,
3989
+ channels,
3990
+ displayName: config?.displayName,
3991
+ description: config?.description,
3992
+ avatar: config?.avatar
3993
+ };
3345
3994
  }
3346
3995
  var TEMPLATE_BRANCH = "volute/template";
3347
3996
  async function configureGitIdentity(mindName, opts) {
@@ -3373,7 +4022,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
3373
4022
  } catch {
3374
4023
  }
3375
4024
  if (existsSync12(tempWorktree)) {
3376
- rmSync3(tempWorktree, { recursive: true, force: true });
4025
+ rmSync2(tempWorktree, { recursive: true, force: true });
3377
4026
  }
3378
4027
  const templatesRoot = findTemplatesRoot();
3379
4028
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
@@ -3395,7 +4044,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
3395
4044
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
3396
4045
  const initDir = resolve16(tempWorktree, ".init");
3397
4046
  if (existsSync12(initDir)) {
3398
- rmSync3(initDir, { recursive: true, force: true });
4047
+ rmSync2(initDir, { recursive: true, force: true });
3399
4048
  }
3400
4049
  await gitExec(["add", "-A"], { cwd: tempWorktree });
3401
4050
  try {
@@ -3409,9 +4058,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
3409
4058
  } catch {
3410
4059
  }
3411
4060
  if (existsSync12(tempWorktree)) {
3412
- rmSync3(tempWorktree, { recursive: true, force: true });
4061
+ rmSync2(tempWorktree, { recursive: true, force: true });
3413
4062
  }
3414
- rmSync3(composedDir, { recursive: true, force: true });
4063
+ rmSync2(composedDir, { recursive: true, force: true });
3415
4064
  }
3416
4065
  }
3417
4066
  async function mergeTemplateBranch(worktreeDir) {
@@ -3447,6 +4096,12 @@ async function importFromArchive(c, tempDir, nameOverride, manifest) {
3447
4096
  if (!manifest?.includes || !manifest.name || !manifest.template) {
3448
4097
  return c.json({ error: "Invalid archive manifest" }, 400);
3449
4098
  }
4099
+ if (isHomeOnlyArchive(manifest)) {
4100
+ return importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverride, manifest);
4101
+ }
4102
+ return importFromFullArchive(c, tempDir, extractedMindDir, nameOverride, manifest);
4103
+ }
4104
+ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride, manifest) {
3450
4105
  const name = nameOverride ?? manifest.name;
3451
4106
  const nameErr = validateMindName(name);
3452
4107
  if (nameErr) return c.json({ error: nameErr }, 400);
@@ -3455,93 +4110,220 @@ async function importFromArchive(c, tempDir, nameOverride, manifest) {
3455
4110
  const dest = mindDir(name);
3456
4111
  if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3457
4112
  try {
3458
- cpSync2(extractedMindDir, dest, { recursive: true });
4113
+ cpSync(extractedMindDir, dest, { recursive: true });
3459
4114
  if (!manifest.includes.identity) {
3460
4115
  generateIdentity(dest);
3461
4116
  }
3462
4117
  const state = stateDir(name);
3463
- mkdirSync8(state, { recursive: true });
4118
+ mkdirSync7(state, { recursive: true });
3464
4119
  const channelsJson = resolve16(tempDir, "state/channels.json");
3465
4120
  if (existsSync12(channelsJson)) {
3466
- cpSync2(channelsJson, resolve16(state, "channels.json"));
4121
+ cpSync(channelsJson, resolve16(state, "channels.json"));
3467
4122
  }
3468
4123
  const envJson = resolve16(tempDir, "state/env.json");
3469
4124
  if (existsSync12(envJson)) {
3470
- cpSync2(envJson, resolve16(state, "env.json"));
4125
+ cpSync(envJson, resolve16(state, "env.json"));
3471
4126
  }
3472
4127
  const port = nextPort();
3473
- addMind(name, port, void 0, manifest.template);
4128
+ addMind(name, port, manifest.stage, manifest.template);
4129
+ try {
4130
+ setMindTemplateHash(name, computeTemplateHash(manifest.template));
4131
+ } catch (err) {
4132
+ logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4133
+ }
3474
4134
  const homeDir = resolve16(dest, "home");
3475
4135
  ensureVoluteGroup();
3476
4136
  createMindUser(name, homeDir);
3477
4137
  chownMindDir(dest, name);
3478
4138
  await npmInstallAsMind(dest, name);
3479
- const historyJsonl = resolve16(tempDir, "history.jsonl");
3480
- if (existsSync12(historyJsonl)) {
4139
+ await importHistoryFromArchive(name, tempDir);
4140
+ importSessionsFromArchive(dest, tempDir);
4141
+ if (!existsSync12(resolve16(dest, ".git"))) {
3481
4142
  try {
3482
- const db = await getDb();
3483
- const lines = readFileSync11(historyJsonl, "utf-8").trim().split("\n");
3484
- let imported = 0;
3485
- let failed = 0;
3486
- for (const line of lines) {
3487
- if (!line) continue;
3488
- try {
3489
- const row = JSON.parse(line);
3490
- if (!row.type) {
3491
- failed++;
3492
- continue;
3493
- }
3494
- await db.insert(mindHistory).values({
3495
- mind: name,
3496
- channel: row.channel ?? null,
3497
- session: row.session ?? null,
3498
- sender: row.sender ?? null,
3499
- message_id: row.message_id ?? null,
3500
- type: row.type,
3501
- content: row.content ?? null,
3502
- metadata: row.metadata ?? null,
3503
- created_at: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
3504
- });
3505
- imported++;
3506
- } catch (lineErr) {
3507
- logger_default.warn("Failed to import history line", logger_default.errorData(lineErr));
3508
- failed++;
3509
- }
3510
- }
3511
- if (failed > 0) {
3512
- logger_default.warn(`History import: ${imported} imported, ${failed} failed`);
3513
- }
4143
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
4144
+ await gitExec(["init"], { cwd: dest, mindName: name, env });
4145
+ await configureGitIdentity(name, { cwd: dest, mindName: name, env });
4146
+ await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
4147
+ await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
3514
4148
  } catch (err) {
3515
- logger_default.error("Failed to open database for history import", logger_default.errorData(err));
4149
+ logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
4150
+ rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3516
4151
  }
3517
4152
  }
3518
- const sessionsDir = resolve16(tempDir, "sessions");
3519
- if (existsSync12(sessionsDir)) {
3520
- const destSessions = resolve16(dest, ".mind/sessions");
3521
- mkdirSync8(destSessions, { recursive: true });
3522
- for (const file of readdirSync6(sessionsDir)) {
3523
- cpSync2(resolve16(sessionsDir, file), resolve16(destSessions, file));
3524
- }
4153
+ chownMindDir(dest, name);
4154
+ rmSync2(tempDir, { recursive: true, force: true });
4155
+ return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
4156
+ } catch (err) {
4157
+ if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
4158
+ try {
4159
+ removeMind(name);
4160
+ } catch (cleanupErr) {
4161
+ logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
3525
4162
  }
3526
- if (!existsSync12(resolve16(dest, ".git"))) {
3527
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
4163
+ rmSync2(tempDir, { recursive: true, force: true });
4164
+ return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4165
+ }
4166
+ }
4167
+ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverride, manifest) {
4168
+ const name = nameOverride ?? manifest.name;
4169
+ const nameErr = validateMindName(name);
4170
+ if (nameErr) return c.json({ error: nameErr }, 400);
4171
+ if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
4172
+ ensureVoluteHome();
4173
+ const dest = mindDir(name);
4174
+ if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4175
+ const templatesRoot = findTemplatesRoot();
4176
+ const { composedDir, manifest: templateManifest } = composeTemplate(
4177
+ templatesRoot,
4178
+ manifest.template
4179
+ );
4180
+ try {
4181
+ copyTemplateToDir(composedDir, dest, name, templateManifest);
4182
+ applyInitFiles(dest);
4183
+ const extractedHome = resolve16(extractedMindDir, "home");
4184
+ if (existsSync12(extractedHome)) {
4185
+ cpSync(extractedHome, resolve16(dest, "home"), { recursive: true });
4186
+ }
4187
+ const extractedMindInternal = resolve16(extractedMindDir, ".mind");
4188
+ if (existsSync12(extractedMindInternal)) {
4189
+ cpSync(extractedMindInternal, resolve16(dest, ".mind"), { recursive: true });
4190
+ }
4191
+ const identityDir = resolve16(dest, ".mind/identity");
4192
+ let publicKeyPem;
4193
+ if (!manifest.includes.identity || !existsSync12(resolve16(identityDir, "private.pem"))) {
4194
+ ({ publicKeyPem } = generateIdentity(dest));
4195
+ } else {
4196
+ publicKeyPem = readFileSync11(resolve16(identityDir, "public.pem"), "utf-8");
4197
+ }
4198
+ const promptsPath = resolve16(dest, "home/.config/prompts.json");
4199
+ if (!existsSync12(promptsPath)) {
4200
+ const mindPrompts = await getMindPromptDefaults();
4201
+ writeFileSync8(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
4202
+ `);
4203
+ }
4204
+ const state = stateDir(name);
4205
+ mkdirSync7(state, { recursive: true });
4206
+ const channelsJson = resolve16(tempDir, "state/channels.json");
4207
+ if (existsSync12(channelsJson)) {
4208
+ cpSync(channelsJson, resolve16(state, "channels.json"));
4209
+ }
4210
+ const envJson = resolve16(tempDir, "state/env.json");
4211
+ if (existsSync12(envJson)) {
4212
+ cpSync(envJson, resolve16(state, "env.json"));
4213
+ }
4214
+ const port = nextPort();
4215
+ addMind(name, port, manifest.stage, manifest.template);
4216
+ const homeDir = resolve16(dest, "home");
4217
+ ensureVoluteGroup();
4218
+ createMindUser(name, homeDir);
4219
+ chownMindDir(dest, name);
4220
+ await npmInstallAsMind(dest, name);
4221
+ let gitWarning;
4222
+ try {
4223
+ const env = isIsolationEnabled() ? { ...process.env, HOME: homeDir } : void 0;
3528
4224
  await gitExec(["init"], { cwd: dest, mindName: name, env });
3529
4225
  await configureGitIdentity(name, { cwd: dest, mindName: name, env });
3530
- await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
3531
- await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
4226
+ await initTemplateBranch(dest, composedDir, templateManifest, name, env);
4227
+ } catch (err) {
4228
+ logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
4229
+ rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
4230
+ gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
4231
+ }
4232
+ try {
4233
+ await addSharedWorktree(name, dest);
4234
+ } catch (err) {
4235
+ logger_default.warn(`failed to add shared worktree for ${name}`, logger_default.errorData(err));
4236
+ }
4237
+ const skillSet = manifest.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS;
4238
+ const skillWarnings = [];
4239
+ for (const skillId of skillSet) {
4240
+ try {
4241
+ await installSkill(name, dest, skillId);
4242
+ } catch (err) {
4243
+ logger_default.error(`failed to install skill ${skillId} for ${name}`, logger_default.errorData(err));
4244
+ skillWarnings.push(`Failed to install skill: ${skillId}`);
4245
+ }
3532
4246
  }
4247
+ await importHistoryFromArchive(name, tempDir);
4248
+ importSessionsFromArchive(dest, tempDir);
3533
4249
  chownMindDir(dest, name);
3534
- rmSync3(tempDir, { recursive: true, force: true });
3535
- return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
4250
+ publishPublicKey(name, publicKeyPem).catch(
4251
+ (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
4252
+ );
4253
+ rmSync2(tempDir, { recursive: true, force: true });
4254
+ return c.json({
4255
+ ok: true,
4256
+ name,
4257
+ port,
4258
+ stage: manifest.stage ?? "sprouted",
4259
+ message: `Imported mind: ${name} (port ${port})`,
4260
+ ...gitWarning && { warning: gitWarning },
4261
+ ...skillWarnings.length > 0 && { skillWarnings }
4262
+ });
3536
4263
  } catch (err) {
3537
- if (existsSync12(dest)) rmSync3(dest, { recursive: true, force: true });
4264
+ if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3538
4265
  try {
3539
4266
  removeMind(name);
3540
4267
  } catch (cleanupErr) {
3541
4268
  logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
3542
4269
  }
3543
- rmSync3(tempDir, { recursive: true, force: true });
4270
+ rmSync2(tempDir, { recursive: true, force: true });
3544
4271
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4272
+ } finally {
4273
+ rmSync2(composedDir, { recursive: true, force: true });
4274
+ }
4275
+ }
4276
+ async function importHistoryFromArchive(name, tempDir) {
4277
+ const historyJsonl = resolve16(tempDir, "history.jsonl");
4278
+ if (!existsSync12(historyJsonl)) return;
4279
+ try {
4280
+ const db = await getDb();
4281
+ const lines = readFileSync11(historyJsonl, "utf-8").trim().split("\n");
4282
+ let imported = 0;
4283
+ let failed = 0;
4284
+ for (const line of lines) {
4285
+ if (!line) continue;
4286
+ try {
4287
+ const row = JSON.parse(line);
4288
+ if (!row.type) {
4289
+ failed++;
4290
+ continue;
4291
+ }
4292
+ await db.insert(mindHistory).values({
4293
+ mind: name,
4294
+ channel: row.channel ?? null,
4295
+ session: row.session ?? null,
4296
+ sender: row.sender ?? null,
4297
+ message_id: row.message_id ?? null,
4298
+ type: row.type,
4299
+ content: row.content ?? null,
4300
+ metadata: row.metadata ?? null,
4301
+ created_at: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
4302
+ });
4303
+ imported++;
4304
+ } catch (lineErr) {
4305
+ logger_default.warn("Failed to import history line", logger_default.errorData(lineErr));
4306
+ failed++;
4307
+ }
4308
+ }
4309
+ if (failed > 0) {
4310
+ logger_default.warn(`History import: ${imported} imported, ${failed} failed`);
4311
+ }
4312
+ } catch (err) {
4313
+ logger_default.error("Failed to open database for history import", logger_default.errorData(err));
4314
+ }
4315
+ }
4316
+ function importSessionsFromArchive(dest, tempDir) {
4317
+ const sessionsDir = resolve16(tempDir, "sessions");
4318
+ if (!existsSync12(sessionsDir)) return;
4319
+ try {
4320
+ const destSessions = resolve16(dest, ".mind/sessions");
4321
+ mkdirSync7(destSessions, { recursive: true });
4322
+ for (const file of readdirSync5(sessionsDir)) {
4323
+ cpSync(resolve16(sessionsDir, file), resolve16(destSessions, file));
4324
+ }
4325
+ } catch (err) {
4326
+ logger_default.error("Failed to import sessions from archive", logger_default.errorData(err));
3545
4327
  }
3546
4328
  }
3547
4329
  var createMindSchema = z3.object({
@@ -3568,21 +4350,32 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
3568
4350
  copyTemplateToDir(composedDir, dest, name, manifest);
3569
4351
  applyInitFiles(dest);
3570
4352
  const { publicKeyPem } = generateIdentity(dest);
4353
+ if (body.description) {
4354
+ const seedConfig = readVoluteConfig(dest);
4355
+ if (!seedConfig) throw new Error("Failed to read volute.json after identity generation");
4356
+ seedConfig.description = body.description;
4357
+ writeVoluteConfig(dest, seedConfig);
4358
+ }
3571
4359
  if (body.model) {
3572
4360
  const configPath2 = resolve16(dest, "home/.config/config.json");
3573
4361
  const existing = existsSync12(configPath2) ? JSON.parse(readFileSync11(configPath2, "utf-8")) : {};
3574
4362
  existing.model = body.model;
3575
- writeFileSync9(configPath2, `${JSON.stringify(existing, null, 2)}
4363
+ writeFileSync8(configPath2, `${JSON.stringify(existing, null, 2)}
3576
4364
  `);
3577
4365
  }
3578
4366
  const mindPrompts = await getMindPromptDefaults();
3579
- writeFileSync9(
4367
+ writeFileSync8(
3580
4368
  resolve16(dest, "home/.config/prompts.json"),
3581
4369
  `${JSON.stringify(mindPrompts, null, 2)}
3582
4370
  `
3583
4371
  );
3584
4372
  const port = nextPort();
3585
4373
  addMind(name, port, body.stage, template);
4374
+ try {
4375
+ setMindTemplateHash(name, computeTemplateHash(template));
4376
+ } catch (err) {
4377
+ logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4378
+ }
3586
4379
  const homeDir = resolve16(dest, "home");
3587
4380
  ensureVoluteGroup();
3588
4381
  createMindUser(name, homeDir);
@@ -3596,7 +4389,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
3596
4389
  await initTemplateBranch(dest, composedDir, manifest, name, env);
3597
4390
  } catch (err) {
3598
4391
  logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
3599
- rmSync3(resolve16(dest, ".git"), { recursive: true, force: true });
4392
+ rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3600
4393
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
3601
4394
  }
3602
4395
  try {
@@ -3611,7 +4404,7 @@ The human who planted you described you as: "${body.description}"
3611
4404
  ` : "";
3612
4405
  const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
3613
4406
  const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
3614
- writeFileSync9(resolve16(dest, "home/SOUL.md"), seedSoul);
4407
+ writeFileSync8(resolve16(dest, "home/SOUL.md"), seedSoul);
3615
4408
  }
3616
4409
  const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
3617
4410
  const skillWarnings = [];
@@ -3626,11 +4419,11 @@ The human who planted you described you as: "${body.description}"
3626
4419
  if (body.stage !== "seed") {
3627
4420
  const customSoul = await getPromptIfCustom("default_soul");
3628
4421
  if (customSoul) {
3629
- writeFileSync9(resolve16(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
4422
+ writeFileSync8(resolve16(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3630
4423
  }
3631
4424
  const customMemory = await getPromptIfCustom("default_memory");
3632
4425
  if (customMemory) {
3633
- writeFileSync9(resolve16(dest, "home/MEMORY.md"), customMemory);
4426
+ writeFileSync8(resolve16(dest, "home/MEMORY.md"), customMemory);
3634
4427
  }
3635
4428
  }
3636
4429
  publishPublicKey(name, publicKeyPem).catch(
@@ -3646,14 +4439,14 @@ The human who planted you described you as: "${body.description}"
3646
4439
  ...skillWarnings.length > 0 && { skillWarnings }
3647
4440
  });
3648
4441
  } catch (err) {
3649
- if (existsSync12(dest)) rmSync3(dest, { recursive: true, force: true });
4442
+ if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3650
4443
  try {
3651
4444
  removeMind(name);
3652
4445
  } catch {
3653
4446
  }
3654
4447
  return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
3655
4448
  } finally {
3656
- rmSync3(composedDir, { recursive: true, force: true });
4449
+ rmSync2(composedDir, { recursive: true, force: true });
3657
4450
  }
3658
4451
  }).post("/import", requireAdmin, async (c) => {
3659
4452
  let body;
@@ -3699,31 +4492,36 @@ ${user.trimEnd()}
3699
4492
  copyTemplateToDir(composedDir, dest, name, manifest);
3700
4493
  applyInitFiles(dest);
3701
4494
  const { publicKeyPem: importPublicKey } = generateIdentity(dest);
3702
- writeFileSync9(resolve16(dest, "home/SOUL.md"), mergedSoul);
4495
+ writeFileSync8(resolve16(dest, "home/SOUL.md"), mergedSoul);
3703
4496
  const wsMemoryPath = resolve16(wsDir, "MEMORY.md");
3704
4497
  const hasMemory = existsSync12(wsMemoryPath);
3705
4498
  if (hasMemory) {
3706
4499
  const existingMemory = readFileSync11(wsMemoryPath, "utf-8");
3707
- writeFileSync9(
4500
+ writeFileSync8(
3708
4501
  resolve16(dest, "home/MEMORY.md"),
3709
4502
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
3710
4503
  );
3711
4504
  } else if (user) {
3712
- writeFileSync9(resolve16(dest, "home/MEMORY.md"), `${user.trimEnd()}
4505
+ writeFileSync8(resolve16(dest, "home/MEMORY.md"), `${user.trimEnd()}
3713
4506
  `);
3714
4507
  }
3715
4508
  const wsMemoryDir = resolve16(wsDir, "memory");
3716
4509
  let dailyLogCount = 0;
3717
4510
  if (existsSync12(wsMemoryDir)) {
3718
4511
  const destMemoryDir = resolve16(dest, "home/memory");
3719
- const files = readdirSync6(wsMemoryDir).filter((f) => f.endsWith(".md"));
4512
+ const files = readdirSync5(wsMemoryDir).filter((f) => f.endsWith(".md"));
3720
4513
  for (const file of files) {
3721
- cpSync2(resolve16(wsMemoryDir, file), resolve16(destMemoryDir, file));
4514
+ cpSync(resolve16(wsMemoryDir, file), resolve16(destMemoryDir, file));
3722
4515
  }
3723
4516
  dailyLogCount = files.length;
3724
4517
  }
3725
4518
  const port = nextPort();
3726
4519
  addMind(name, port, void 0, template);
4520
+ try {
4521
+ setMindTemplateHash(name, computeTemplateHash(template));
4522
+ } catch (err) {
4523
+ logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4524
+ }
3727
4525
  const homeDir = resolve16(dest, "home");
3728
4526
  ensureVoluteGroup();
3729
4527
  createMindUser(name, homeDir);
@@ -3744,8 +4542,8 @@ ${user.trimEnd()}
3744
4542
  } else if (template === "claude") {
3745
4543
  const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
3746
4544
  const mindRuntimeDir = resolve16(dest, ".mind");
3747
- mkdirSync8(mindRuntimeDir, { recursive: true });
3748
- writeFileSync9(resolve16(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
4545
+ mkdirSync7(mindRuntimeDir, { recursive: true });
4546
+ writeFileSync8(resolve16(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
3749
4547
  }
3750
4548
  }
3751
4549
  importOpenClawConnectors(name, dest);
@@ -3760,14 +4558,14 @@ ${user.trimEnd()}
3760
4558
  );
3761
4559
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
3762
4560
  } catch (err) {
3763
- if (existsSync12(dest)) rmSync3(dest, { recursive: true, force: true });
4561
+ if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3764
4562
  try {
3765
4563
  removeMind(name);
3766
4564
  } catch {
3767
4565
  }
3768
4566
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
3769
4567
  } finally {
3770
- rmSync3(composedDir, { recursive: true, force: true });
4568
+ rmSync2(composedDir, { recursive: true, force: true });
3771
4569
  }
3772
4570
  }).get("/", async (c) => {
3773
4571
  const entries = readRegistry();
@@ -3783,12 +4581,11 @@ ${user.trimEnd()}
3783
4581
  }
3784
4582
  const minds = await Promise.all(
3785
4583
  entries.map(async (entry) => {
3786
- const { status, channels } = await getMindStatus(entry.name, entry.port);
4584
+ const mindStatus = await getMindStatus(entry.name, entry.port);
3787
4585
  const hasPages = existsSync12(resolve16(mindDir(entry.name), "home", "pages"));
3788
4586
  return {
3789
4587
  ...entry,
3790
- status,
3791
- channels,
4588
+ ...mindStatus,
3792
4589
  hasPages,
3793
4590
  lastActiveAt: lastActiveMap.get(entry.name) ?? null
3794
4591
  };
@@ -3804,7 +4601,7 @@ ${user.trimEnd()}
3804
4601
  const entry = findMind(name);
3805
4602
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3806
4603
  if (!existsSync12(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
3807
- const { status, channels } = await getMindStatus(name, entry.port);
4604
+ const mindStatus = await getMindStatus(name, entry.port);
3808
4605
  const variants = readVariants(name);
3809
4606
  const manager = getMindManager();
3810
4607
  const variantStatuses = await Promise.all(
@@ -3819,15 +4616,17 @@ ${user.trimEnd()}
3819
4616
  })
3820
4617
  );
3821
4618
  const hasPages = existsSync12(resolve16(mindDir(name), "home", "pages"));
3822
- return c.json({ ...entry, status, channels, variants: variantStatuses, hasPages });
4619
+ return c.json({ ...entry, ...mindStatus, variants: variantStatuses, hasPages });
3823
4620
  }).post("/:name/start", requireAdmin, async (c) => {
3824
4621
  const name = c.req.param("name");
3825
4622
  const [baseName, variantName] = name.split("@", 2);
3826
4623
  const entry = findMind(baseName);
3827
4624
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4625
+ let targetPort = entry.port;
3828
4626
  if (variantName) {
3829
4627
  const variant = findVariant(baseName, variantName);
3830
4628
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
4629
+ targetPort = variant.port;
3831
4630
  } else {
3832
4631
  const dir = mindDir(baseName);
3833
4632
  if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
@@ -3837,7 +4636,7 @@ ${user.trimEnd()}
3837
4636
  }
3838
4637
  try {
3839
4638
  await startMindFull(name);
3840
- return c.json({ ok: true });
4639
+ return c.json({ ok: true, port: targetPort });
3841
4640
  } catch (err) {
3842
4641
  return c.json({ error: err instanceof Error ? err.message : "Failed to start mind" }, 500);
3843
4642
  }
@@ -3846,9 +4645,11 @@ ${user.trimEnd()}
3846
4645
  const [baseName, variantName] = name.split("@", 2);
3847
4646
  const entry = findMind(baseName);
3848
4647
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4648
+ let targetPort = entry.port;
3849
4649
  if (variantName) {
3850
4650
  const variant = findVariant(baseName, variantName);
3851
4651
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
4652
+ targetPort = variant.port;
3852
4653
  } else {
3853
4654
  const dir = mindDir(baseName);
3854
4655
  if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
@@ -3944,7 +4745,7 @@ ${user.trimEnd()}
3944
4745
  }
3945
4746
  }
3946
4747
  await startMindFull(name);
3947
- return c.json({ ok: true });
4748
+ return c.json({ ok: true, port: targetPort });
3948
4749
  } catch (err) {
3949
4750
  return c.json({ error: err instanceof Error ? err.message : "Failed to restart mind" }, 500);
3950
4751
  }
@@ -3996,10 +4797,10 @@ ${user.trimEnd()}
3996
4797
  await deleteMindUser2(name);
3997
4798
  const state = stateDir(name);
3998
4799
  if (existsSync12(state)) {
3999
- rmSync3(state, { recursive: true, force: true });
4800
+ rmSync2(state, { recursive: true, force: true });
4000
4801
  }
4001
4802
  if (force && existsSync12(dir)) {
4002
- rmSync3(dir, { recursive: true, force: true });
4803
+ rmSync2(dir, { recursive: true, force: true });
4003
4804
  deleteMindUser(name);
4004
4805
  }
4005
4806
  return c.json({ ok: true });
@@ -4094,7 +4895,7 @@ ${user.trimEnd()}
4094
4895
  await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
4095
4896
  chownMindDir(dir, mindName);
4096
4897
  } catch (err) {
4097
- rmSync3(resolve16(dir, ".git"), { recursive: true, force: true });
4898
+ rmSync2(resolve16(dir, ".git"), { recursive: true, force: true });
4098
4899
  return c.json(
4099
4900
  {
4100
4901
  error: `Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
@@ -4121,7 +4922,7 @@ ${user.trimEnd()}
4121
4922
  await updateTemplateBranch(dir, template, mindName);
4122
4923
  const parentDir = resolve16(dir, ".variants");
4123
4924
  if (!existsSync12(parentDir)) {
4124
- mkdirSync8(parentDir, { recursive: true });
4925
+ mkdirSync7(parentDir, { recursive: true });
4125
4926
  }
4126
4927
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
4127
4928
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -4266,7 +5067,7 @@ ${user.trimEnd()}
4266
5067
  const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
4267
5068
  const msgCount = countResult[0]?.count ?? 0;
4268
5069
  if (msgCount >= 10 && msgCount % 10 === 0) {
4269
- const nudge = "\n[You've been exploring for a while. Whenever you feel ready, write your SOUL.md and MEMORY.md, then run volute sprout.]";
5070
+ const nudge = "\n[You've been exploring for a while. Whenever you feel ready, write your SOUL.md and MEMORY.md, then run volute mind sprout.]";
4270
5071
  if (typeof parsed.content === "string") {
4271
5072
  parsed.content = parsed.content + nudge;
4272
5073
  } else if (Array.isArray(parsed.content)) {
@@ -4459,7 +5260,7 @@ ${user.trimEnd()}
4459
5260
  const stream = new ReadableStream({
4460
5261
  start(controller) {
4461
5262
  const encoder = new TextEncoder();
4462
- const send = (data) => {
5263
+ const send5 = (data) => {
4463
5264
  controller.enqueue(encoder.encode(`data: ${data}
4464
5265
 
4465
5266
  `));
@@ -4468,7 +5269,7 @@ ${user.trimEnd()}
4468
5269
  if (typeFilter && !typeFilter.includes(event.type)) return;
4469
5270
  if (sessionFilter && event.session !== sessionFilter) return;
4470
5271
  if (channelFilter && event.channel !== channelFilter) return;
4471
- send(JSON.stringify(event));
5272
+ send5(JSON.stringify(event));
4472
5273
  });
4473
5274
  c.req.raw.signal.addEventListener("abort", () => {
4474
5275
  unsubscribe();
@@ -4528,6 +5329,12 @@ ${user.trimEnd()}
4528
5329
  const db = await getDb();
4529
5330
  const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
4530
5331
  return c.json(rows.map((r) => r.channel));
5332
+ }).get("/:name/history/export", async (c) => {
5333
+ const name = c.req.param("name");
5334
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
5335
+ const db = await getDb();
5336
+ const rows = await db.select().from(mindHistory).where(eq4(mindHistory.mind, name));
5337
+ return c.json(rows);
4531
5338
  }).get("/:name/history", async (c) => {
4532
5339
  const name = c.req.param("name");
4533
5340
  const channel = c.req.query("channel");
@@ -4552,8 +5359,8 @@ ${user.trimEnd()}
4552
5359
  var minds_default = app11;
4553
5360
 
4554
5361
  // src/web/api/pages.ts
4555
- import { readFile as readFile2, stat } from "fs/promises";
4556
- import { extname, resolve as resolve17 } from "path";
5362
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
5363
+ import { extname as extname2, resolve as resolve17 } from "path";
4557
5364
  import { Hono as Hono12 } from "hono";
4558
5365
  var MIME_TYPES = {
4559
5366
  ".html": "text/html",
@@ -4583,10 +5390,10 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
4583
5390
  const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
4584
5391
  const requestedPath = resolve17(pagesRoot, wildcard.slice(1));
4585
5392
  if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
4586
- let fileStat = await stat(requestedPath).catch(() => null);
5393
+ let fileStat = await stat2(requestedPath).catch(() => null);
4587
5394
  if (fileStat?.isDirectory()) {
4588
5395
  const indexPath = resolve17(requestedPath, "index.html");
4589
- fileStat = await stat(indexPath).catch(() => null);
5396
+ fileStat = await stat2(indexPath).catch(() => null);
4590
5397
  if (fileStat?.isFile()) {
4591
5398
  const body = await readFile2(indexPath);
4592
5399
  return c.body(body, 200, { "Content-Type": "text/html" });
@@ -4594,7 +5401,7 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
4594
5401
  return c.text("Not found", 404);
4595
5402
  }
4596
5403
  if (fileStat?.isFile()) {
4597
- const ext = extname(requestedPath);
5404
+ const ext = extname2(requestedPath);
4598
5405
  const mime = MIME_TYPES[ext] || "application/octet-stream";
4599
5406
  const body = await readFile2(requestedPath);
4600
5407
  return c.body(body, 200, { "Content-Type": mime });
@@ -4827,9 +5634,9 @@ var app15 = new Hono15().post("/:name/shared/merge", requireAdmin, async (c) =>
4827
5634
  var shared_default = app15;
4828
5635
 
4829
5636
  // src/web/api/skills.ts
4830
- import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync4 } from "fs";
4831
- import { tmpdir as tmpdir2 } from "os";
4832
- import { join as join4, resolve as resolve18 } from "path";
5637
+ import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
5638
+ import { tmpdir } from "os";
5639
+ import { join as join3, resolve as resolve18 } from "path";
4833
5640
  import AdmZip from "adm-zip";
4834
5641
  import { Hono as Hono16 } from "hono";
4835
5642
  var app16 = new Hono16().get("/", async (c) => {
@@ -4839,7 +5646,7 @@ var app16 = new Hono16().get("/", async (c) => {
4839
5646
  const id = c.req.param("id");
4840
5647
  const skill = await getSharedSkill(id);
4841
5648
  if (!skill) return c.json({ error: "Skill not found" }, 404);
4842
- const dir = join4(sharedSkillsDir(), id);
5649
+ const dir = join3(sharedSkillsDir(), id);
4843
5650
  const files = listFilesRecursive(dir);
4844
5651
  return c.json({ ...skill, files });
4845
5652
  }).post("/upload", requireAdmin, async (c) => {
@@ -4852,7 +5659,7 @@ var app16 = new Hono16().get("/", async (c) => {
4852
5659
  return c.json({ error: "Only .zip files are accepted" }, 400);
4853
5660
  }
4854
5661
  const buffer = Buffer.from(await file.arrayBuffer());
4855
- const tmpDir = mkdtempSync(join4(tmpdir2(), "volute-skill-upload-"));
5662
+ const tmpDir = mkdtempSync(join3(tmpdir(), "volute-skill-upload-"));
4856
5663
  try {
4857
5664
  const zip = new AdmZip(buffer);
4858
5665
  for (const entry of zip.getEntries()) {
@@ -4863,13 +5670,13 @@ var app16 = new Hono16().get("/", async (c) => {
4863
5670
  }
4864
5671
  zip.extractAllTo(tmpDir, true);
4865
5672
  let skillDir = null;
4866
- if (existsSync13(join4(tmpDir, "SKILL.md"))) {
5673
+ if (existsSync13(join3(tmpDir, "SKILL.md"))) {
4867
5674
  skillDir = tmpDir;
4868
5675
  } else {
4869
- const entries = readdirSync7(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
5676
+ const entries = readdirSync6(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4870
5677
  for (const entry of entries) {
4871
- if (existsSync13(join4(tmpDir, entry.name, "SKILL.md"))) {
4872
- skillDir = join4(tmpDir, entry.name);
5678
+ if (existsSync13(join3(tmpDir, entry.name, "SKILL.md"))) {
5679
+ skillDir = join3(tmpDir, entry.name);
4873
5680
  break;
4874
5681
  }
4875
5682
  }
@@ -4885,7 +5692,7 @@ var app16 = new Hono16().get("/", async (c) => {
4885
5692
  }
4886
5693
  throw e;
4887
5694
  } finally {
4888
- rmSync4(tmpDir, { recursive: true, force: true });
5695
+ rmSync3(tmpDir, { recursive: true, force: true });
4889
5696
  }
4890
5697
  }).delete("/:id", requireAdmin, async (c) => {
4891
5698
  const id = c.req.param("id");
@@ -4987,13 +5794,13 @@ var app19 = new Hono19().get("/update", async (c) => {
4987
5794
  var update_default = app19;
4988
5795
 
4989
5796
  // src/web/api/variants.ts
4990
- import { existsSync as existsSync14, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
5797
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
4991
5798
  import { resolve as resolve20 } from "path";
4992
5799
  import { Hono as Hono20 } from "hono";
4993
5800
 
4994
5801
  // src/lib/spawn-server.ts
4995
5802
  import { spawn as spawn4 } from "child_process";
4996
- import { closeSync, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync12 } from "fs";
5803
+ import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync12 } from "fs";
4997
5804
  import { resolve as resolve19 } from "path";
4998
5805
  function tsxBin(cwd) {
4999
5806
  return resolve19(cwd, "node_modules", ".bin", "tsx");
@@ -5032,7 +5839,7 @@ function spawnAttached(cwd, port) {
5032
5839
  }
5033
5840
  function spawnDetached(cwd, port, logDir) {
5034
5841
  const logsDir = logDir ?? resolve19(cwd, ".mind", "logs");
5035
- mkdirSync9(logsDir, { recursive: true });
5842
+ mkdirSync8(logsDir, { recursive: true });
5036
5843
  const logPath = resolve19(logsDir, "mind.log");
5037
5844
  const logFd = openSync(logPath, "a");
5038
5845
  const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
@@ -5117,6 +5924,16 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5117
5924
  return { ...v, status: health.ok ? "running" : "dead" };
5118
5925
  })
5119
5926
  );
5927
+ try {
5928
+ const updated = results.map(({ status, ...v }) => ({
5929
+ ...v,
5930
+ running: status === "running"
5931
+ }));
5932
+ const changed = variants.some((v, i) => v.running !== updated[i].running);
5933
+ if (changed) writeVariants(name, updated);
5934
+ } catch (err) {
5935
+ logger_default.warn(`failed to sync variant status for ${name}`, logger_default.errorData(err));
5936
+ }
5120
5937
  return c.json(results);
5121
5938
  }).post("/:name/variants", requireAdmin, async (c) => {
5122
5939
  const mindName = c.req.param("name");
@@ -5139,7 +5956,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5139
5956
  if (existsSync14(variantDir)) {
5140
5957
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
5141
5958
  }
5142
- mkdirSync10(resolve20(projectRoot, ".variants"), { recursive: true });
5959
+ mkdirSync9(resolve20(projectRoot, ".variants"), { recursive: true });
5143
5960
  try {
5144
5961
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
5145
5962
  } catch (e) {
@@ -5162,7 +5979,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5162
5979
  return c.json({ error: `npm install failed: ${msg}` }, 500);
5163
5980
  }
5164
5981
  if (body.soul) {
5165
- writeFileSync10(resolve20(variantDir, "home/SOUL.md"), body.soul);
5982
+ writeFileSync9(resolve20(variantDir, "home/SOUL.md"), body.soul);
5166
5983
  }
5167
5984
  const variantPort = body.port ?? nextPort();
5168
5985
  const variant = {
@@ -5268,6 +6085,16 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5268
6085
  } catch {
5269
6086
  }
5270
6087
  removeVariant(mindName, variantName);
6088
+ if (variantName === "upgrade") {
6089
+ try {
6090
+ const { computeTemplateHash: computeTemplateHash2 } = await import("./template-hash-BIMA4ILT.js");
6091
+ const { setMindTemplateHash: setMindTemplateHash2 } = await import("./registry-D2BSQ2X5.js");
6092
+ const tmpl = entry.template ?? "claude";
6093
+ setMindTemplateHash2(mindName, computeTemplateHash2(tmpl));
6094
+ } catch (err) {
6095
+ console.error(`[daemon] failed to update template hash for ${mindName}:`, err);
6096
+ }
6097
+ }
5271
6098
  chownMindDir(projectRoot, mindName);
5272
6099
  try {
5273
6100
  if (isIsolationEnabled()) {
@@ -5420,7 +6247,7 @@ async function fanOutToMinds(opts) {
5420
6247
  const participantNames = participants.map((p) => p.username);
5421
6248
  const isDM = opts.isDM ?? participants.length === 2;
5422
6249
  const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
5423
- const { getMindManager: getMindManager2 } = await import("./mind-manager-3DMYKZPB.js");
6250
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-3V2NXX4I.js");
5424
6251
  const manager = getMindManager2();
5425
6252
  const runningMinds = mindParticipants.map((ap) => {
5426
6253
  const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
@@ -5865,14 +6692,14 @@ async function startServer({
5865
6692
  hostname = "127.0.0.1"
5866
6693
  }) {
5867
6694
  let assetsDir = "";
5868
- let searchDir = dirname3(new URL(import.meta.url).pathname);
6695
+ let searchDir = dirname2(new URL(import.meta.url).pathname);
5869
6696
  for (let i = 0; i < 5; i++) {
5870
6697
  const candidate = resolve21(searchDir, "dist", "web-assets");
5871
6698
  if (existsSync15(candidate)) {
5872
6699
  assetsDir = candidate;
5873
6700
  break;
5874
6701
  }
5875
- searchDir = dirname3(searchDir);
6702
+ searchDir = dirname2(searchDir);
5876
6703
  }
5877
6704
  if (assetsDir) {
5878
6705
  app_default.get("*", async (c) => {
@@ -5880,15 +6707,15 @@ async function startServer({
5880
6707
  if (urlPath.startsWith("/api/")) return c.notFound();
5881
6708
  const filePath = resolve21(assetsDir, urlPath.slice(1));
5882
6709
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
5883
- const s = await stat2(filePath).catch(() => null);
6710
+ const s = await stat3(filePath).catch(() => null);
5884
6711
  if (s?.isFile()) {
5885
- const ext = extname2(filePath);
6712
+ const ext = extname3(filePath);
5886
6713
  const mime = MIME_TYPES2[ext] || "application/octet-stream";
5887
6714
  const body = await readFile3(filePath);
5888
6715
  return c.body(body, 200, { "Content-Type": mime });
5889
6716
  }
5890
6717
  const indexPath = resolve21(assetsDir, "index.html");
5891
- const indexStat = await stat2(indexPath).catch(() => null);
6718
+ const indexStat = await stat3(indexPath).catch(() => null);
5892
6719
  if (indexStat?.isFile()) {
5893
6720
  const body = await readFile3(indexPath, "utf-8");
5894
6721
  return c.html(body);
@@ -5930,7 +6757,7 @@ async function startDaemon(opts) {
5930
6757
  }
5931
6758
  const DAEMON_PID_PATH = resolve22(home, "daemon.pid");
5932
6759
  const DAEMON_JSON_PATH = resolve22(home, "daemon.json");
5933
- mkdirSync11(home, { recursive: true });
6760
+ mkdirSync10(home, { recursive: true });
5934
6761
  migrateAgentsToMinds();
5935
6762
  try {
5936
6763
  await ensureSharedRepo();
@@ -5958,8 +6785,8 @@ async function startDaemon(opts) {
5958
6785
  }
5959
6786
  throw err;
5960
6787
  }
5961
- writeFileSync11(DAEMON_PID_PATH, myPid, { mode: 420 });
5962
- writeFileSync11(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
6788
+ writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
6789
+ writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
5963
6790
  `, {
5964
6791
  mode: 420
5965
6792
  });
@@ -6015,10 +6842,20 @@ async function startDaemon(opts) {
6015
6842
  });
6016
6843
  await Promise.all(workers);
6017
6844
  }
6845
+ try {
6846
+ const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-TFS2U5CF.js");
6847
+ backfillTemplateHashes();
6848
+ notifyVersionUpdate().catch((err) => {
6849
+ logger_default.warn("failed to send version update notifications", logger_default.errorData(err));
6850
+ });
6851
+ } catch (err) {
6852
+ logger_default.warn("failed to initialize version notifications", logger_default.errorData(err));
6853
+ }
6018
6854
  delivery.restoreFromDb().catch((err) => {
6019
6855
  logger_default.warn("failed to restore delivery queue", logger_default.errorData(err));
6020
6856
  });
6021
- cleanExpiredSessions().catch(() => {
6857
+ cleanExpiredSessions().catch((err) => {
6858
+ logger_default.warn("failed to clean expired sessions", logger_default.errorData(err));
6022
6859
  });
6023
6860
  logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
6024
6861
  function cleanup() {
@@ -6041,19 +6878,33 @@ async function startDaemon(opts) {
6041
6878
  if (shuttingDown) return;
6042
6879
  shuttingDown = true;
6043
6880
  logger_default.info("shutting down...");
6044
- stopAllWatchers();
6045
- stopAll();
6046
- scheduler.stop();
6047
- scheduler.saveState();
6048
- mailPoller.stop();
6049
- tokenBudget.stop();
6050
- delivery.dispose();
6051
- await connectors.stopAll();
6052
- await manager.stopAll();
6053
- manager.clearCrashAttempts();
6054
- server.close();
6055
- cleanup();
6056
- process.exit(0);
6881
+ const safe = (label, fn) => {
6882
+ try {
6883
+ const result = fn();
6884
+ if (result instanceof Promise)
6885
+ return result.catch((err) => logger_default.error(`shutdown: ${label} failed`, logger_default.errorData(err)));
6886
+ } catch (err) {
6887
+ logger_default.error(`shutdown: ${label} failed`, logger_default.errorData(err));
6888
+ }
6889
+ };
6890
+ try {
6891
+ safe("stopAllWatchers", stopAllWatchers);
6892
+ safe("stopAllActivityTrackers", stopAll);
6893
+ safe("scheduler.stop", () => scheduler.stop());
6894
+ safe("scheduler.saveState", () => scheduler.saveState());
6895
+ safe("mailPoller.stop", () => mailPoller.stop());
6896
+ safe("tokenBudget.stop", () => tokenBudget.stop());
6897
+ safe("delivery.dispose", () => delivery.dispose());
6898
+ await safe("connectors.stopAll", () => connectors.stopAll());
6899
+ await safe("manager.stopAll", () => manager.stopAll());
6900
+ safe("clearCrashAttempts", () => manager.clearCrashAttempts());
6901
+ safe("server.close", () => server.close());
6902
+ } catch (err) {
6903
+ logger_default.error("error during shutdown", logger_default.errorData(err));
6904
+ } finally {
6905
+ cleanup();
6906
+ process.exit(0);
6907
+ }
6057
6908
  }
6058
6909
  process.on("SIGINT", shutdown);
6059
6910
  process.on("SIGTERM", shutdown);