volute 0.17.0 → 0.19.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 (116) hide show
  1. package/README.md +1 -1
  2. package/dist/archive-ZCFOSTKB.js +15 -0
  3. package/dist/{channel-SLURLIRV.js → channel-PUQKGSQM.js} +60 -7
  4. package/dist/{chunk-CE7WMOVW.js → chunk-2TJGRJ4O.js} +236 -103
  5. package/dist/{chunk-6BDNWYKG.js → chunk-32VR2EOH.js} +2 -2
  6. package/dist/chunk-4KPUF5JD.js +214 -0
  7. package/dist/{chunk-QJIIHU32.js → chunk-7NO7EV5Z.js} +2 -2
  8. package/dist/chunk-AW7P4EVV.js +159 -0
  9. package/dist/{chunk-2Y77MCFG.js → chunk-DYZGP3EW.js} +2 -2
  10. package/dist/{chunk-M77QBTEH.js → chunk-EBGCNDMM.js} +24 -14
  11. package/dist/{chunk-GSPWIM5E.js → chunk-EMQSAY3B.js} +77 -6
  12. package/dist/{chunk-37X7ECMF.js → chunk-FCDU5BFX.js} +1 -1
  13. package/dist/chunk-FGV2H4TX.js +803 -0
  14. package/dist/{chunk-ZCEYUUID.js → chunk-OGXOMR65.js} +2 -1
  15. package/dist/chunk-OTWLI7F4.js +375 -0
  16. package/dist/{chunk-3FC42ZBM.js → chunk-RHEGSQFJ.js} +4 -1
  17. package/dist/{chunk-MVSXRMJJ.js → chunk-SCUDS4US.js} +1 -1
  18. package/dist/{chunk-MIJIAGGG.js → chunk-UJ6GHNR7.js} +8 -6
  19. package/dist/{chunk-OYSZNX5I.js → chunk-VDWCHYTS.js} +1 -1
  20. package/dist/{chunk-77ISBIKI.js → chunk-VE4D3GOP.js} +2 -2
  21. package/dist/chunk-VQWDC6UK.js +142 -0
  22. package/dist/{chunk-OJQ47SCA.js → chunk-WC6ZHVRL.js} +1 -1
  23. package/dist/chunk-YUIHSKR6.js +72 -0
  24. package/dist/chunk-Z524RFCJ.js +36 -0
  25. package/dist/cli.js +44 -24
  26. package/dist/{connector-3ELFMI2R.js → connector-JBVNZ7VK.js} +6 -6
  27. package/dist/connectors/discord.js +2 -2
  28. package/dist/connectors/slack.js +2 -2
  29. package/dist/connectors/telegram.js +2 -2
  30. package/dist/{create-ZWHCRT5F.js → create-HP4OVVHF.js} +6 -4
  31. package/dist/{daemon-client-ODKDUYDE.js → daemon-client-ITWUCNFO.js} +2 -2
  32. package/dist/{daemon-restart-VRQMZLBK.js → daemon-restart-JMZM3QY4.js} +8 -8
  33. package/dist/daemon.js +1624 -940
  34. package/dist/db-5ZVC6MQF.js +10 -0
  35. package/dist/{delete-6G6WEX4F.js → delete-BSU7K3RY.js} +1 -1
  36. package/dist/delivery-manager-ISTJMZDW.js +16 -0
  37. package/dist/down-ZY35KMHR.js +14 -0
  38. package/dist/{env-6IDWGBUH.js → env-A3LMO777.js} +6 -6
  39. package/dist/export-GCDNQCF3.js +100 -0
  40. package/dist/{history-5F4WQW7S.js → history-WNK3DFUM.js} +10 -7
  41. package/dist/{import-EDGRLIGO.js → import-M63VIUJ5.js} +3 -3
  42. package/dist/log-PPPZDVEF.js +39 -0
  43. package/dist/{login-ORQDXLBM.js → login-HNH3EUQV.js} +2 -2
  44. package/dist/{logout-XC5AUO5I.js → logout-I5CB5UZS.js} +2 -2
  45. package/dist/{logs-GYOR3L2L.js → logs-SF2IMJN4.js} +6 -6
  46. package/dist/merge-33C237A4.js +46 -0
  47. package/dist/{mind-OJN6RBZW.js → mind-PQ5NCPSU.js} +14 -10
  48. package/dist/mind-manager-RVCFROAY.js +18 -0
  49. package/dist/{package-4GTJGUXI.js → package-MYE2ZJLV.js} +7 -3
  50. package/dist/{pages-6IV4VQTU.js → pages-AXCOSY3P.js} +2 -2
  51. package/dist/{publish-Q4RPSJLL.js → publish-YB377JB7.js} +18 -4
  52. package/dist/pull-XAEWQJ47.js +39 -0
  53. package/dist/{register-LDE6LRXY.js → register-VSPCMHKX.js} +2 -2
  54. package/dist/{restart-YFAWFS5T.js → restart-IQKMCK5M.js} +6 -6
  55. package/dist/{schedule-AGYLDMNS.js → schedule-LMX7GAQZ.js} +6 -6
  56. package/dist/schema-5BW7DFZI.js +24 -0
  57. package/dist/{seed-AP4Q7RZ7.js → seed-J43YDKXG.js} +7 -4
  58. package/dist/{send-4GKDO26C.js → send-KVIZIGCE.js} +8 -8
  59. package/dist/{service-U7MZ2H7F.js → service-LUR7WDO7.js} +6 -6
  60. package/dist/{setup-DJKIZKGW.js → setup-OH3PJUJO.js} +7 -7
  61. package/dist/shared-KO35ZM44.js +39 -0
  62. package/dist/skill-BCVNI6TV.js +287 -0
  63. package/{templates/_base/_skills → dist/skills}/orientation/SKILL.md +1 -1
  64. package/{templates/_base/_skills → dist/skills}/sessions/SKILL.md +2 -2
  65. package/{templates/_base/_skills → dist/skills}/volute-mind/SKILL.md +35 -1
  66. package/dist/{sprout-TJ3BHVOG.js → sprout-VBEX63LX.js} +38 -20
  67. package/dist/{start-3YYRXBKP.js → start-I5JYB65M.js} +6 -6
  68. package/dist/{status-VSFZYX7S.js → status-4ESFLGH4.js} +5 -5
  69. package/dist/status-D7E5HHBV.js +35 -0
  70. package/dist/{status-OKNA6AR3.js → status-JCJAOXTW.js} +2 -2
  71. package/dist/{stop-AA5K5LYG.js → stop-NBVKEFQQ.js} +6 -6
  72. package/dist/{up-LT3X5Q26.js → up-WG65SWJU.js} +5 -5
  73. package/dist/{update-YAGN5ODG.js → update-FJIHDJKM.js} +5 -5
  74. package/dist/{update-check-APLTH4IN.js → update-check-MWE5AH4U.js} +2 -2
  75. package/dist/{upgrade-KXZCQSZN.js → upgrade-AIT24B5I.js} +1 -1
  76. package/dist/{variant-X5QFG6KK.js → variant-63ZWO2W7.js} +4 -4
  77. package/dist/variants-JAGWGBXG.js +26 -0
  78. package/dist/web-assets/assets/index-BAbuRsVF.css +1 -0
  79. package/dist/web-assets/assets/index-CiQhSKi_.js +63 -0
  80. package/dist/web-assets/index.html +2 -2
  81. package/drizzle/0007_system_prompts.sql +5 -0
  82. package/drizzle/0008_volute_channels.sql +24 -0
  83. package/drizzle/0009_shared_skills.sql +9 -0
  84. package/drizzle/0010_delivery_queue.sql +12 -0
  85. package/drizzle/0011_rename_human_to_brain.sql +1 -0
  86. package/drizzle/meta/0007_snapshot.json +7 -0
  87. package/drizzle/meta/0008_snapshot.json +7 -0
  88. package/drizzle/meta/0009_snapshot.json +7 -0
  89. package/drizzle/meta/0010_snapshot.json +7 -0
  90. package/drizzle/meta/0011_snapshot.json +7 -0
  91. package/drizzle/meta/_journal.json +35 -0
  92. package/package.json +7 -3
  93. package/templates/_base/.init/.config/hooks/startup-context.sh +1 -1
  94. package/templates/_base/.init/.config/prompts.json +5 -0
  95. package/templates/_base/.init/.config/scripts/session-reader.ts +3 -3
  96. package/templates/_base/home/VOLUTE.md +16 -1
  97. package/templates/_base/src/lib/auto-commit.ts +51 -14
  98. package/templates/_base/src/lib/router.ts +168 -29
  99. package/templates/_base/src/lib/routing.ts +4 -1
  100. package/templates/_base/src/lib/startup.ts +43 -0
  101. package/templates/_base/src/lib/types.ts +4 -0
  102. package/templates/_base/src/lib/volute-server.ts +91 -2
  103. package/templates/claude/src/agent.ts +4 -3
  104. package/templates/claude/src/lib/hooks/reply-instructions.ts +3 -1
  105. package/templates/claude/src/server.ts +2 -2
  106. package/templates/claude/volute-template.json +1 -2
  107. package/templates/pi/src/agent.ts +6 -7
  108. package/templates/pi/src/lib/reply-instructions-extension.ts +3 -1
  109. package/templates/pi/src/lib/session-context-extension.ts +2 -2
  110. package/templates/pi/volute-template.json +1 -2
  111. package/dist/chunk-PO5Q2AYN.js +0 -121
  112. package/dist/down-A56B5JLK.js +0 -14
  113. package/dist/mind-manager-ETNCPQJN.js +0 -15
  114. package/dist/web-assets/assets/index-BcmT7Qxo.js +0 -63
  115. package/dist/web-assets/assets/index-DG01TyLb.css +0 -1
  116. /package/{templates/_base/_skills → dist/skills}/memory/SKILL.md +0 -0
package/dist/daemon.js CHANGED
@@ -1,24 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- applyInitFiles,
4
- composeTemplate,
5
- copyTemplateToDir,
6
- findTemplatesRoot,
7
- listFiles
8
- } from "./chunk-PO5Q2AYN.js";
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-OTWLI7F4.js";
18
+ import {
19
+ addSharedWorktree,
20
+ ensureSharedRepo,
21
+ removeSharedWorktree,
22
+ sharedLog,
23
+ sharedMerge,
24
+ sharedPull,
25
+ sharedStatus
26
+ } from "./chunk-4KPUF5JD.js";
9
27
  import {
10
28
  readSystemsConfig
11
- } from "./chunk-37X7ECMF.js";
29
+ } from "./chunk-FCDU5BFX.js";
30
+ import {
31
+ deliverMessage,
32
+ extractTextContent,
33
+ getDeliveryManager,
34
+ getTypingMap,
35
+ initDeliveryManager
36
+ } from "./chunk-FGV2H4TX.js";
12
37
  import {
38
+ PROMPT_DEFAULTS,
39
+ PROMPT_KEYS,
40
+ RestartTracker,
13
41
  RotatingLog,
14
42
  clearJsonMap,
15
43
  getMindManager,
44
+ getMindPromptDefaults,
45
+ getPrompt,
46
+ getPromptIfCustom,
16
47
  initMindManager,
17
48
  loadJsonMap,
49
+ saveJsonMap,
50
+ substitute
51
+ } from "./chunk-2TJGRJ4O.js";
52
+ import {
18
53
  logBuffer,
19
- logger_default,
20
- saveJsonMap
21
- } from "./chunk-CE7WMOVW.js";
54
+ logger_default
55
+ } from "./chunk-YUIHSKR6.js";
56
+ import {
57
+ CHANNELS,
58
+ getChannelDriver
59
+ } from "./chunk-UJ6GHNR7.js";
22
60
  import {
23
61
  findOpenClawSession,
24
62
  importOpenClawConnectors,
@@ -26,23 +64,32 @@ import {
26
64
  parseNameFromIdentity,
27
65
  readVoluteConfig,
28
66
  writeVoluteConfig
29
- } from "./chunk-GSPWIM5E.js";
67
+ } from "./chunk-EMQSAY3B.js";
30
68
  import {
31
69
  loadMergedEnv,
32
70
  mindEnvPath,
33
71
  readEnv,
34
72
  sharedEnvPath,
35
73
  writeEnv
36
- } from "./chunk-OYSZNX5I.js";
74
+ } from "./chunk-VDWCHYTS.js";
37
75
  import {
38
- CHANNELS,
39
- getChannelDriver
40
- } from "./chunk-MIJIAGGG.js";
76
+ getDb
77
+ } from "./chunk-Z524RFCJ.js";
78
+ import {
79
+ conversationParticipants,
80
+ conversations,
81
+ messages,
82
+ mindHistory,
83
+ sessions,
84
+ systemPrompts,
85
+ users
86
+ } from "./chunk-VQWDC6UK.js";
87
+ import "./chunk-D424ZQGI.js";
41
88
  import {
42
89
  exec,
43
90
  gitExec,
44
91
  resolveVoluteBin
45
- } from "./chunk-2Y77MCFG.js";
92
+ } from "./chunk-DYZGP3EW.js";
46
93
  import {
47
94
  chownMindDir,
48
95
  createMindUser,
@@ -50,17 +97,16 @@ import {
50
97
  ensureVoluteGroup,
51
98
  isIsolationEnabled,
52
99
  wrapForIsolation
53
- } from "./chunk-ZCEYUUID.js";
100
+ } from "./chunk-OGXOMR65.js";
54
101
  import {
55
102
  checkForUpdate,
56
103
  checkForUpdateCached,
57
104
  getCurrentVersion
58
- } from "./chunk-MVSXRMJJ.js";
59
- import "./chunk-D424ZQGI.js";
105
+ } from "./chunk-SCUDS4US.js";
60
106
  import {
61
107
  buildVoluteSlug,
62
108
  writeChannelEntry
63
- } from "./chunk-3FC42ZBM.js";
109
+ } from "./chunk-RHEGSQFJ.js";
64
110
  import {
65
111
  addMind,
66
112
  addVariant,
@@ -70,6 +116,7 @@ import {
70
116
  findMind,
71
117
  findVariant,
72
118
  getAllRunningVariants,
119
+ initRegistryCache,
73
120
  mindDir,
74
121
  nextPort,
75
122
  readRegistry,
@@ -84,16 +131,14 @@ import {
84
131
  validateBranchName,
85
132
  validateMindName,
86
133
  voluteHome
87
- } from "./chunk-M77QBTEH.js";
88
- import {
89
- __export
90
- } from "./chunk-K3NQKI34.js";
134
+ } from "./chunk-EBGCNDMM.js";
135
+ import "./chunk-K3NQKI34.js";
91
136
 
92
137
  // src/daemon.ts
93
138
  import { randomBytes } from "crypto";
94
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
139
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
95
140
  import { homedir as homedir2 } from "os";
96
- import { resolve as resolve17 } from "path";
141
+ import { resolve as resolve19 } from "path";
97
142
  import { format } from "util";
98
143
 
99
144
  // src/lib/connector-manager.ts
@@ -184,16 +229,12 @@ function searchUpwards(...segments) {
184
229
  }
185
230
  return null;
186
231
  }
187
- var MAX_RESTART_ATTEMPTS = 5;
188
- var BASE_RESTART_DELAY = 3e3;
189
- var MAX_RESTART_DELAY = 6e4;
190
232
  var ConnectorManager = class {
191
233
  connectors = /* @__PURE__ */ new Map();
192
234
  stopping = /* @__PURE__ */ new Set();
193
235
  // "mind:type" keys currently being explicitly stopped
194
236
  shuttingDown = false;
195
- restartAttempts = /* @__PURE__ */ new Map();
196
- // "mind:type" -> count
237
+ restartTracker = new RestartTracker();
197
238
  async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
198
239
  const config = readVoluteConfig(mindDir2) ?? {};
199
240
  const types = config.connectors ?? [];
@@ -306,7 +347,7 @@ var ConnectorManager = class {
306
347
  }
307
348
  this.connectors.get(mindName).set(type, { child, type });
308
349
  const stopKey = `${mindName}:${type}`;
309
- this.restartAttempts.delete(stopKey);
350
+ this.restartTracker.reset(stopKey);
310
351
  child.on("exit", (code) => {
311
352
  const mindMap = this.connectors.get(mindName);
312
353
  if (mindMap?.get(type)?.child === child) {
@@ -316,15 +357,13 @@ var ConnectorManager = class {
316
357
  if (this.stopping.has(stopKey)) return;
317
358
  clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
318
359
  if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
319
- const attempts = this.restartAttempts.get(stopKey) ?? 0;
320
- if (attempts >= MAX_RESTART_ATTEMPTS) {
321
- clog.error(`connector ${type} for ${mindName} crashed ${attempts} times \u2014 giving up`);
360
+ const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(stopKey);
361
+ if (!shouldRestart) {
362
+ clog.error(`connector ${type} for ${mindName} crashed ${attempt} times \u2014 giving up`);
322
363
  return;
323
364
  }
324
- const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
325
- this.restartAttempts.set(stopKey, attempts + 1);
326
365
  clog.info(
327
- `restarting connector ${type} for ${mindName} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, in ${delay}ms`
366
+ `restarting connector ${type} for ${mindName} \u2014 attempt ${attempt}/${this.restartTracker.maxRestartAttempts}, in ${delay}ms`
328
367
  );
329
368
  setTimeout(() => {
330
369
  if (this.shuttingDown || this.stopping.has(stopKey)) return;
@@ -343,23 +382,23 @@ var ConnectorManager = class {
343
382
  const stopKey = `${mindName}:${type}`;
344
383
  this.stopping.add(stopKey);
345
384
  mindMap.delete(type);
346
- await new Promise((resolve18) => {
347
- tracked.child.on("exit", () => resolve18());
385
+ await new Promise((resolve20) => {
386
+ tracked.child.on("exit", () => resolve20());
348
387
  try {
349
388
  tracked.child.kill("SIGTERM");
350
389
  } catch {
351
- resolve18();
390
+ resolve20();
352
391
  }
353
392
  setTimeout(() => {
354
393
  try {
355
394
  tracked.child.kill("SIGKILL");
356
395
  } catch {
357
396
  }
358
- resolve18();
397
+ resolve20();
359
398
  }, 5e3);
360
399
  });
361
400
  this.stopping.delete(stopKey);
362
- this.restartAttempts.delete(stopKey);
401
+ this.restartTracker.reset(stopKey);
363
402
  try {
364
403
  this.removeConnectorPid(mindName, type);
365
404
  } catch (err) {
@@ -431,7 +470,8 @@ function initConnectorManager() {
431
470
  return instance;
432
471
  }
433
472
  function getConnectorManager() {
434
- if (!instance) instance = new ConnectorManager();
473
+ if (!instance)
474
+ throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
435
475
  return instance;
436
476
  }
437
477
 
@@ -455,15 +495,13 @@ var INITIAL_RECONNECT_MS = 1e3;
455
495
  var MAX_RECONNECT_MS = 6e4;
456
496
  var MailPoller = class {
457
497
  ws = null;
458
- daemonPort = null;
459
- daemonToken = null;
460
498
  running = false;
461
499
  pingTimer = null;
462
500
  reconnectTimer = null;
463
501
  reconnectDelay = INITIAL_RECONNECT_MS;
464
502
  reconnectAttempts = 0;
465
503
  disconnectedAt = null;
466
- start(daemonPort, daemonToken) {
504
+ start() {
467
505
  if (this.running) {
468
506
  mlog.warn("already running \u2014 ignoring duplicate start");
469
507
  return;
@@ -473,8 +511,6 @@ var MailPoller = class {
473
511
  mlog.info("no systems config \u2014 mail disabled");
474
512
  return;
475
513
  }
476
- this.daemonPort = daemonPort ?? null;
477
- this.daemonToken = daemonToken ?? null;
478
514
  this.running = true;
479
515
  this.connect();
480
516
  }
@@ -630,51 +666,29 @@ var MailPoller = class {
630
666
  mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
631
667
  return;
632
668
  }
633
- const channel = `mail:${email.from.address}`;
634
- const sender = email.from.name || email.from.address;
635
- const text2 = formatEmailContent(email);
636
- const body = JSON.stringify({
637
- content: [{ type: "text", text: text2 }],
638
- channel,
639
- sender,
640
- platform: "Email",
641
- isDM: true
642
- });
643
- if (!this.daemonPort || !this.daemonToken) {
644
- mlog.warn(`cannot deliver to ${mind}: daemon port/token not set`);
645
- return;
646
- }
647
- const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
648
- const controller = new AbortController();
649
- const timeout = setTimeout(() => controller.abort(), 12e4);
669
+ const text = formatEmailContent(email);
650
670
  try {
651
- const res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mind)}/message`, {
652
- method: "POST",
653
- headers: {
654
- "Content-Type": "application/json",
655
- Authorization: `Bearer ${this.daemonToken}`,
656
- Origin: daemonUrl
657
- },
658
- body,
659
- signal: controller.signal
660
- });
661
- if (!res.ok) {
662
- mlog.warn(`deliver to ${mind} got HTTP ${res.status}`);
663
- } else {
664
- mlog.info(`delivered email from ${email.from.address} to ${mind}`);
665
- }
666
- await res.text().catch(() => {
671
+ await deliverMessage(mind, {
672
+ content: [{ type: "text", text }],
673
+ channel: `mail:${email.from.address}`,
674
+ sender: email.from.name || email.from.address,
675
+ platform: "Email",
676
+ isDM: true
667
677
  });
678
+ mlog.info(`delivered email from ${email.from.address} to ${mind}`);
668
679
  } catch (err) {
669
680
  mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
670
- } finally {
671
- clearTimeout(timeout);
672
681
  }
673
682
  }
674
683
  };
675
684
  var instance2 = null;
685
+ function initMailPoller() {
686
+ if (instance2) throw new Error("MailPoller already initialized");
687
+ instance2 = new MailPoller();
688
+ return instance2;
689
+ }
676
690
  function getMailPoller() {
677
- if (!instance2) instance2 = new MailPoller();
691
+ if (!instance2) throw new Error("MailPoller not initialized \u2014 call initMailPoller() first");
678
692
  return instance2;
679
693
  }
680
694
  async function ensureMailAddress(mindName) {
@@ -833,10 +847,20 @@ function migrateProfileScript() {
833
847
  }
834
848
 
835
849
  // src/lib/migrate-state.ts
836
- import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync } from "fs";
850
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, renameSync as renameSync2 } from "fs";
837
851
  import { resolve as resolve4 } from "path";
852
+ function migrateDotVoluteDir(name) {
853
+ const dir = mindDir(name);
854
+ const oldDir = resolve4(dir, ".volute");
855
+ const newDir = resolve4(dir, ".mind");
856
+ if (existsSync4(oldDir) && !existsSync4(newDir)) {
857
+ renameSync2(oldDir, newDir);
858
+ } else if (existsSync4(oldDir) && existsSync4(newDir)) {
859
+ console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
860
+ }
861
+ }
838
862
  function migrateMindState(name) {
839
- const src = resolve4(mindDir(name), ".volute");
863
+ const src = resolve4(mindDir(name), ".mind");
840
864
  if (!existsSync4(src)) return;
841
865
  const dest = stateDir(name);
842
866
  mkdirSync2(dest, { recursive: true });
@@ -870,14 +894,10 @@ var Scheduler = class {
870
894
  interval = null;
871
895
  lastFired = /* @__PURE__ */ new Map();
872
896
  // "mind:scheduleId" → epoch minute
873
- daemonPort = null;
874
- daemonToken = null;
875
897
  get statePath() {
876
898
  return resolve5(voluteHome(), "scheduler-state.json");
877
899
  }
878
- start(daemonPort, daemonToken) {
879
- this.daemonPort = daemonPort ?? null;
880
- this.daemonToken = daemonToken ?? null;
900
+ start() {
881
901
  this.loadState();
882
902
  this.interval = setInterval(() => this.tick(), 6e4);
883
903
  }
@@ -908,9 +928,6 @@ var Scheduler = class {
908
928
  this.schedules.delete(mindName);
909
929
  }
910
930
  tick() {
911
- for (const mind of this.schedules.keys()) {
912
- this.loadSchedules(mind);
913
- }
914
931
  const now = /* @__PURE__ */ new Date();
915
932
  for (const [mind, schedules] of this.schedules) {
916
933
  for (const schedule of schedules) {
@@ -941,69 +958,39 @@ var Scheduler = class {
941
958
  }
942
959
  }
943
960
  async fire(mindName, schedule) {
944
- const entry = findMind(mindName);
945
- if (!entry) return;
946
- const body = JSON.stringify({
947
- content: [{ type: "text", text: schedule.message }],
948
- channel: "system:scheduler",
949
- sender: schedule.id
950
- });
951
- const controller = new AbortController();
952
- const timeout = setTimeout(() => controller.abort(), 12e4);
953
961
  try {
954
- let res;
955
- if (this.daemonPort && this.daemonToken) {
956
- const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
957
- res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mindName)}/message`, {
958
- method: "POST",
959
- headers: {
960
- "Content-Type": "application/json",
961
- Authorization: `Bearer ${this.daemonToken}`,
962
- Origin: daemonUrl
963
- },
964
- body,
965
- signal: controller.signal
966
- });
967
- } else {
968
- res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
969
- method: "POST",
970
- headers: { "Content-Type": "application/json" },
971
- body,
972
- signal: controller.signal
973
- });
974
- }
975
- if (!res.ok) {
976
- slog.warn(`"${schedule.id}" for ${mindName} got HTTP ${res.status}`);
977
- } else {
978
- slog.info(`fired "${schedule.id}" for ${mindName}`);
979
- }
980
- await res.text().catch(() => {
962
+ await deliverMessage(mindName, {
963
+ content: [{ type: "text", text: schedule.message }],
964
+ channel: "system:scheduler",
965
+ sender: schedule.id
981
966
  });
967
+ slog.info(`fired "${schedule.id}" for ${mindName}`);
982
968
  } catch (err) {
983
969
  slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
984
- } finally {
985
- clearTimeout(timeout);
986
970
  }
987
971
  }
988
972
  };
989
973
  var instance3 = null;
974
+ function initScheduler() {
975
+ if (instance3) throw new Error("Scheduler already initialized");
976
+ instance3 = new Scheduler();
977
+ return instance3;
978
+ }
990
979
  function getScheduler() {
991
- if (!instance3) instance3 = new Scheduler();
980
+ if (!instance3) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
992
981
  return instance3;
993
982
  }
994
983
 
995
984
  // src/lib/token-budget.ts
985
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
986
+ import { resolve as resolve6 } from "path";
996
987
  var tlog = logger_default.child("token-budget");
997
988
  var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
998
989
  var MAX_QUEUE_SIZE = 100;
999
990
  var TokenBudget = class {
1000
991
  budgets = /* @__PURE__ */ new Map();
1001
992
  interval = null;
1002
- daemonPort = null;
1003
- daemonToken = null;
1004
- start(daemonPort, daemonToken) {
1005
- this.daemonPort = daemonPort ?? null;
1006
- this.daemonToken = daemonToken ?? null;
993
+ start() {
1007
994
  this.interval = setInterval(() => this.tick(), 6e4);
1008
995
  }
1009
996
  stop() {
@@ -1017,14 +1004,21 @@ var TokenBudget = class {
1017
1004
  existing.tokenLimit = tokenLimit;
1018
1005
  existing.periodMinutes = periodMinutes;
1019
1006
  } else {
1020
- this.budgets.set(mind, {
1021
- tokensUsed: 0,
1022
- periodStart: Date.now(),
1023
- periodMinutes,
1024
- tokenLimit,
1025
- queue: [],
1026
- warningInjected: false
1027
- });
1007
+ const persisted = this.loadBudgetState(mind);
1008
+ if (persisted) {
1009
+ persisted.tokenLimit = tokenLimit;
1010
+ persisted.periodMinutes = periodMinutes;
1011
+ this.budgets.set(mind, persisted);
1012
+ } else {
1013
+ this.budgets.set(mind, {
1014
+ tokensUsed: 0,
1015
+ periodStart: Date.now(),
1016
+ periodMinutes,
1017
+ tokenLimit,
1018
+ queue: [],
1019
+ warningInjected: false
1020
+ });
1021
+ }
1028
1022
  }
1029
1023
  }
1030
1024
  removeBudget(mind) {
@@ -1034,6 +1028,7 @@ var TokenBudget = class {
1034
1028
  const state = this.budgets.get(mind);
1035
1029
  if (!state) return;
1036
1030
  state.tokensUsed += inputTokens + outputTokens;
1031
+ this.saveBudgetState(mind, state);
1037
1032
  }
1038
1033
  /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
1039
1034
  checkBudget(mind) {
@@ -1084,6 +1079,7 @@ var TokenBudget = class {
1084
1079
  state.tokensUsed = 0;
1085
1080
  state.periodStart = now;
1086
1081
  state.warningInjected = false;
1082
+ this.saveBudgetState(mind, state);
1087
1083
  const queued = this.drain(mind);
1088
1084
  if (queued.length > 0) {
1089
1085
  this.replay(mind, queued).catch((err) => {
@@ -1093,68 +1089,117 @@ var TokenBudget = class {
1093
1089
  }
1094
1090
  }
1095
1091
  }
1096
- async replay(mindName, messages2) {
1097
- if (!this.daemonPort || !this.daemonToken) {
1098
- tlog.warn(
1099
- `cannot replay ${messages2.length} message(s) for ${mindName}: daemon not configured`
1100
- );
1101
- const state = this.budgets.get(mindName);
1102
- if (state) state.queue.push(...messages2);
1103
- return;
1092
+ budgetStatePath(mind) {
1093
+ return resolve6(stateDir(mind), "budget.json");
1094
+ }
1095
+ saveBudgetState(mind, state) {
1096
+ try {
1097
+ const dir = stateDir(mind);
1098
+ mkdirSync3(dir, { recursive: true });
1099
+ const data = {
1100
+ periodStart: state.periodStart,
1101
+ tokensUsed: state.tokensUsed,
1102
+ warningInjected: state.warningInjected,
1103
+ queue: state.queue
1104
+ };
1105
+ writeFileSync3(this.budgetStatePath(mind), `${JSON.stringify(data)}
1106
+ `);
1107
+ } catch (err) {
1108
+ tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
1109
+ }
1110
+ }
1111
+ loadBudgetState(mind) {
1112
+ try {
1113
+ const path = this.budgetStatePath(mind);
1114
+ if (!existsSync5(path)) return null;
1115
+ const data = JSON.parse(readFileSync4(path, "utf-8"));
1116
+ if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
1117
+ return {
1118
+ periodStart: data.periodStart,
1119
+ tokensUsed: data.tokensUsed,
1120
+ warningInjected: data.warningInjected ?? false,
1121
+ queue: Array.isArray(data.queue) ? data.queue : [],
1122
+ periodMinutes: 0,
1123
+ // will be overwritten by caller
1124
+ tokenLimit: 0
1125
+ // will be overwritten by caller
1126
+ };
1127
+ } catch (err) {
1128
+ tlog.warn(`failed to load budget state for ${mind}`, logger_default.errorData(err));
1129
+ return null;
1104
1130
  }
1131
+ }
1132
+ async replay(mindName, messages2) {
1105
1133
  const summary = messages2.map((m) => {
1106
1134
  const from = m.sender ? `[${m.sender}]` : "";
1107
1135
  const ch = m.channel ? `(${m.channel})` : "";
1108
1136
  return `${from}${ch} ${m.textContent}`;
1109
1137
  }).join("\n");
1110
- const body = JSON.stringify({
1111
- content: [
1112
- {
1113
- type: "text",
1114
- text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
1138
+ try {
1139
+ await deliverMessage(mindName, {
1140
+ content: [
1141
+ {
1142
+ type: "text",
1143
+ text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
1115
1144
 
1116
1145
  ${summary}`
1117
- }
1118
- ],
1119
- channel: "system:budget-replay",
1120
- sender: "system"
1121
- });
1122
- const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
1123
- const controller = new AbortController();
1124
- const timeout = setTimeout(() => controller.abort(), 12e4);
1125
- try {
1126
- const res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mindName)}/message`, {
1127
- method: "POST",
1128
- headers: {
1129
- "Content-Type": "application/json",
1130
- Authorization: `Bearer ${this.daemonToken}`,
1131
- Origin: daemonUrl
1132
- },
1133
- body,
1134
- signal: controller.signal
1135
- });
1136
- if (!res.ok) {
1137
- tlog.warn(`replay for ${mindName} got HTTP ${res.status}`);
1138
- } else {
1139
- tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
1140
- }
1141
- await res.text().catch(() => {
1146
+ }
1147
+ ],
1148
+ channel: "system:budget-replay",
1149
+ sender: "system"
1142
1150
  });
1151
+ tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
1143
1152
  } catch (err) {
1144
1153
  tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
1145
1154
  const state = this.budgets.get(mindName);
1146
1155
  if (state) state.queue.push(...messages2);
1147
- } finally {
1148
- clearTimeout(timeout);
1149
1156
  }
1150
1157
  }
1151
1158
  };
1152
1159
  var instance4 = null;
1160
+ function initTokenBudget() {
1161
+ if (instance4) throw new Error("TokenBudget already initialized");
1162
+ instance4 = new TokenBudget();
1163
+ return instance4;
1164
+ }
1153
1165
  function getTokenBudget() {
1154
- if (!instance4) instance4 = new TokenBudget();
1166
+ if (!instance4) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
1155
1167
  return instance4;
1156
1168
  }
1157
1169
 
1170
+ // src/lib/mind-service.ts
1171
+ async function startMindFull(name) {
1172
+ const [baseName, variantName] = name.split("@", 2);
1173
+ await getMindManager().startMind(name);
1174
+ if (variantName) return;
1175
+ const entry = findMind(baseName);
1176
+ if (!entry || entry.stage === "seed") return;
1177
+ const dir = mindDir(baseName);
1178
+ const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
1179
+ await getConnectorManager().startConnectors(baseName, dir, entry.port, daemonPort);
1180
+ getScheduler().loadSchedules(baseName);
1181
+ ensureMailAddress(baseName).catch(
1182
+ (err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
1183
+ );
1184
+ const config = readVoluteConfig(dir);
1185
+ if (config?.tokenBudget) {
1186
+ getTokenBudget().setBudget(
1187
+ baseName,
1188
+ config.tokenBudget,
1189
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1190
+ );
1191
+ }
1192
+ }
1193
+ async function stopMindFull(name) {
1194
+ const [baseName, variantName] = name.split("@", 2);
1195
+ if (!variantName) {
1196
+ await getConnectorManager().stopConnectors(baseName);
1197
+ getScheduler().unloadSchedules(baseName);
1198
+ getTokenBudget().removeBudget(baseName);
1199
+ }
1200
+ await getMindManager().stopMind(name);
1201
+ }
1202
+
1158
1203
  // src/web/middleware/auth.ts
1159
1204
  import { timingSafeEqual } from "crypto";
1160
1205
  import { eq as eq2, lt } from "drizzle-orm";
@@ -1164,129 +1209,12 @@ import { createMiddleware } from "hono/factory";
1164
1209
  // src/lib/auth.ts
1165
1210
  import { compareSync, hashSync } from "bcryptjs";
1166
1211
  import { and, count, eq } from "drizzle-orm";
1167
-
1168
- // src/lib/db.ts
1169
- import { chmodSync, existsSync as existsSync5 } from "fs";
1170
- import { dirname as dirname2, resolve as resolve6 } from "path";
1171
- import { fileURLToPath } from "url";
1172
- import { drizzle } from "drizzle-orm/libsql";
1173
- import { migrate } from "drizzle-orm/libsql/migrator";
1174
-
1175
- // src/lib/schema.ts
1176
- var schema_exports = {};
1177
- __export(schema_exports, {
1178
- conversationParticipants: () => conversationParticipants,
1179
- conversations: () => conversations,
1180
- messages: () => messages,
1181
- mindHistory: () => mindHistory,
1182
- sessions: () => sessions,
1183
- users: () => users
1184
- });
1185
- import { sql } from "drizzle-orm";
1186
- import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
1187
- var users = sqliteTable("users", {
1188
- id: integer("id").primaryKey({ autoIncrement: true }),
1189
- username: text("username").unique().notNull(),
1190
- password_hash: text("password_hash").notNull(),
1191
- role: text("role").notNull().default("pending"),
1192
- user_type: text("user_type").notNull().default("human"),
1193
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1194
- });
1195
- var conversations = sqliteTable(
1196
- "conversations",
1197
- {
1198
- id: text("id").primaryKey(),
1199
- mind_name: text("mind_name").notNull(),
1200
- channel: text("channel").notNull(),
1201
- user_id: integer("user_id").references(() => users.id),
1202
- title: text("title"),
1203
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
1204
- updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
1205
- },
1206
- (table) => [
1207
- index("idx_conversations_mind_name").on(table.mind_name),
1208
- index("idx_conversations_user_id").on(table.user_id),
1209
- index("idx_conversations_updated_at").on(table.updated_at)
1210
- ]
1211
- );
1212
- var mindHistory = sqliteTable(
1213
- "mind_history",
1214
- {
1215
- id: integer("id").primaryKey({ autoIncrement: true }),
1216
- mind: text("mind").notNull(),
1217
- channel: text("channel"),
1218
- session: text("session"),
1219
- sender: text("sender"),
1220
- message_id: text("message_id"),
1221
- type: text("type").notNull(),
1222
- content: text("content"),
1223
- metadata: text("metadata"),
1224
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1225
- },
1226
- (table) => [
1227
- index("idx_mind_history_mind").on(table.mind),
1228
- index("idx_mind_history_mind_channel").on(table.mind, table.channel),
1229
- index("idx_mind_history_mind_type").on(table.mind, table.type)
1230
- ]
1231
- );
1232
- var conversationParticipants = sqliteTable(
1233
- "conversation_participants",
1234
- {
1235
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
1236
- user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1237
- role: text("role").notNull().default("member"),
1238
- joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
1239
- },
1240
- (table) => [
1241
- uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
1242
- index("idx_cp_user_id").on(table.user_id)
1243
- ]
1244
- );
1245
- var sessions = sqliteTable("sessions", {
1246
- id: text("id").primaryKey(),
1247
- userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
1248
- createdAt: integer("created_at").notNull()
1249
- });
1250
- var messages = sqliteTable(
1251
- "messages",
1252
- {
1253
- id: integer("id").primaryKey({ autoIncrement: true }),
1254
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
1255
- role: text("role").notNull(),
1256
- sender_name: text("sender_name"),
1257
- content: text("content").notNull(),
1258
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1259
- },
1260
- (table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
1261
- );
1262
-
1263
- // src/lib/db.ts
1264
- var __dirname = dirname2(fileURLToPath(import.meta.url));
1265
- var migrationsFolder = existsSync5(resolve6(__dirname, "../drizzle")) ? resolve6(__dirname, "../drizzle") : resolve6(__dirname, "../../drizzle");
1266
- var db = null;
1267
- async function getDb() {
1268
- if (db) return db;
1269
- const dbPath = process.env.VOLUTE_DB_PATH || resolve6(voluteHome(), "volute.db");
1270
- db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
1271
- await migrate(db, { migrationsFolder });
1272
- try {
1273
- chmodSync(dbPath, 384);
1274
- } catch (err) {
1275
- console.error(
1276
- `[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
1277
- err
1278
- );
1279
- }
1280
- return db;
1281
- }
1282
-
1283
- // src/lib/auth.ts
1284
1212
  async function createUser(username, password) {
1285
- const db2 = await getDb();
1213
+ const db = await getDb();
1286
1214
  const hash = hashSync(password, 10);
1287
- const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
1215
+ const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
1288
1216
  const role = value === 0 ? "admin" : "pending";
1289
- const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
1217
+ const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
1290
1218
  id: users.id,
1291
1219
  username: users.username,
1292
1220
  role: users.role,
@@ -1296,8 +1224,8 @@ async function createUser(username, password) {
1296
1224
  return result;
1297
1225
  }
1298
1226
  async function verifyUser(username, password) {
1299
- const db2 = await getDb();
1300
- const row = await db2.select().from(users).where(eq(users.username, username)).get();
1227
+ const db = await getDb();
1228
+ const row = await db.select().from(users).where(eq(users.username, username)).get();
1301
1229
  if (!row) return null;
1302
1230
  if (row.user_type === "mind") return null;
1303
1231
  if (!compareSync(password, row.password_hash)) return null;
@@ -1305,8 +1233,8 @@ async function verifyUser(username, password) {
1305
1233
  return user;
1306
1234
  }
1307
1235
  async function getUser(id) {
1308
- const db2 = await getDb();
1309
- const row = await db2.select({
1236
+ const db = await getDb();
1237
+ const row = await db.select({
1310
1238
  id: users.id,
1311
1239
  username: users.username,
1312
1240
  role: users.role,
@@ -1316,8 +1244,8 @@ async function getUser(id) {
1316
1244
  return row ?? null;
1317
1245
  }
1318
1246
  async function getUserByUsername(username) {
1319
- const db2 = await getDb();
1320
- const row = await db2.select({
1247
+ const db = await getDb();
1248
+ const row = await db.select({
1321
1249
  id: users.id,
1322
1250
  username: users.username,
1323
1251
  role: users.role,
@@ -1327,8 +1255,8 @@ async function getUserByUsername(username) {
1327
1255
  return row ?? null;
1328
1256
  }
1329
1257
  async function listUsers() {
1330
- const db2 = await getDb();
1331
- return db2.select({
1258
+ const db = await getDb();
1259
+ return db.select({
1332
1260
  id: users.id,
1333
1261
  username: users.username,
1334
1262
  role: users.role,
@@ -1337,8 +1265,8 @@ async function listUsers() {
1337
1265
  }).from(users).orderBy(users.created_at).all();
1338
1266
  }
1339
1267
  async function listPendingUsers() {
1340
- const db2 = await getDb();
1341
- return db2.select({
1268
+ const db = await getDb();
1269
+ return db.select({
1342
1270
  id: users.id,
1343
1271
  username: users.username,
1344
1272
  role: users.role,
@@ -1347,8 +1275,8 @@ async function listPendingUsers() {
1347
1275
  }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
1348
1276
  }
1349
1277
  async function listUsersByType(userType) {
1350
- const db2 = await getDb();
1351
- return db2.select({
1278
+ const db = await getDb();
1279
+ return db.select({
1352
1280
  id: users.id,
1353
1281
  username: users.username,
1354
1282
  role: users.role,
@@ -1357,8 +1285,8 @@ async function listUsersByType(userType) {
1357
1285
  }).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
1358
1286
  }
1359
1287
  async function getOrCreateMindUser(mindName) {
1360
- const db2 = await getDb();
1361
- const existing = await db2.select({
1288
+ const db = await getDb();
1289
+ const existing = await db.select({
1362
1290
  id: users.id,
1363
1291
  username: users.username,
1364
1292
  role: users.role,
@@ -1367,7 +1295,7 @@ async function getOrCreateMindUser(mindName) {
1367
1295
  }).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
1368
1296
  if (existing) return existing;
1369
1297
  try {
1370
- const [result] = await db2.insert(users).values({
1298
+ const [result] = await db.insert(users).values({
1371
1299
  username: mindName,
1372
1300
  password_hash: "!mind",
1373
1301
  role: "mind",
@@ -1382,7 +1310,7 @@ async function getOrCreateMindUser(mindName) {
1382
1310
  return result;
1383
1311
  } catch (err) {
1384
1312
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
1385
- const retried = await db2.select({
1313
+ const retried = await db.select({
1386
1314
  id: users.id,
1387
1315
  username: users.username,
1388
1316
  role: users.role,
@@ -1395,12 +1323,21 @@ async function getOrCreateMindUser(mindName) {
1395
1323
  }
1396
1324
  }
1397
1325
  async function deleteMindUser2(mindName) {
1398
- const db2 = await getDb();
1399
- await db2.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
1326
+ const db = await getDb();
1327
+ await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
1328
+ }
1329
+ async function changePassword(userId, currentPassword, newPassword) {
1330
+ const db = await getDb();
1331
+ const row = await db.select().from(users).where(eq(users.id, userId)).get();
1332
+ if (!row) return false;
1333
+ if (!compareSync(currentPassword, row.password_hash)) return false;
1334
+ const hash = hashSync(newPassword, 10);
1335
+ await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
1336
+ return true;
1400
1337
  }
1401
1338
  async function approveUser(id) {
1402
- const db2 = await getDb();
1403
- await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
1339
+ const db = await getDb();
1340
+ await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
1404
1341
  }
1405
1342
 
1406
1343
  // src/web/middleware/auth.ts
@@ -1410,30 +1347,33 @@ function isValidDaemonToken(token) {
1410
1347
  return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
1411
1348
  }
1412
1349
  var SESSION_MAX_AGE = 864e5;
1350
+ var SESSION_CACHE_TTL = 5 * 60 * 1e3;
1351
+ var sessionCache = /* @__PURE__ */ new Map();
1413
1352
  async function createSession(userId) {
1414
- const db2 = await getDb();
1353
+ const db = await getDb();
1415
1354
  const sessionId = crypto.randomUUID();
1416
- await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
1355
+ await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
1417
1356
  return sessionId;
1418
1357
  }
1419
1358
  async function deleteSession(sessionId) {
1420
- const db2 = await getDb();
1421
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
1359
+ sessionCache.delete(sessionId);
1360
+ const db = await getDb();
1361
+ await db.delete(sessions).where(eq2(sessions.id, sessionId));
1422
1362
  }
1423
1363
  async function getSessionUserId(sessionId) {
1424
- const db2 = await getDb();
1425
- const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1364
+ const db = await getDb();
1365
+ const row = await db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1426
1366
  if (!row) return void 0;
1427
1367
  if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
1428
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
1368
+ await db.delete(sessions).where(eq2(sessions.id, sessionId));
1429
1369
  return void 0;
1430
1370
  }
1431
1371
  return row.userId;
1432
1372
  }
1433
1373
  async function cleanExpiredSessions() {
1434
- const db2 = await getDb();
1374
+ const db = await getDb();
1435
1375
  const cutoff = Date.now() - SESSION_MAX_AGE;
1436
- await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
1376
+ await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
1437
1377
  }
1438
1378
  var requireAdmin = createMiddleware(async (c, next) => {
1439
1379
  const user = c.get("user");
@@ -1447,30 +1387,44 @@ var authMiddleware = createMiddleware(async (c, next) => {
1447
1387
  if (authHeader?.startsWith("Bearer ")) {
1448
1388
  const token = authHeader.slice(7);
1449
1389
  if (token && isValidDaemonToken(token)) {
1450
- c.set("user", { id: 0, username: "cli", role: "admin", user_type: "human" });
1390
+ c.set("user", { id: 0, username: "cli", role: "admin", user_type: "brain" });
1451
1391
  await next();
1452
1392
  return;
1453
1393
  }
1454
1394
  }
1455
1395
  const sessionId = getCookie(c, "volute_session");
1456
1396
  if (!sessionId) return c.json({ error: "Unauthorized" }, 401);
1397
+ const cached = sessionCache.get(sessionId);
1398
+ if (cached && cached.expires > Date.now()) {
1399
+ if (cached.user.role === "pending") return c.json({ error: "Account pending approval" }, 403);
1400
+ c.set("user", cached.user);
1401
+ await next();
1402
+ return;
1403
+ }
1457
1404
  const userId = await getSessionUserId(sessionId);
1458
- if (userId == null) return c.json({ error: "Unauthorized" }, 401);
1405
+ if (userId == null) {
1406
+ sessionCache.delete(sessionId);
1407
+ return c.json({ error: "Unauthorized" }, 401);
1408
+ }
1459
1409
  const user = await getUser(userId);
1460
- if (!user) return c.json({ error: "Unauthorized" }, 401);
1410
+ if (!user) {
1411
+ sessionCache.delete(sessionId);
1412
+ return c.json({ error: "Unauthorized" }, 401);
1413
+ }
1461
1414
  if (user.role === "pending") return c.json({ error: "Account pending approval" }, 403);
1415
+ sessionCache.set(sessionId, { userId, user, expires: Date.now() + SESSION_CACHE_TTL });
1462
1416
  c.set("user", user);
1463
1417
  await next();
1464
1418
  });
1465
1419
 
1466
1420
  // src/web/server.ts
1467
- import { existsSync as existsSync10 } from "fs";
1421
+ import { existsSync as existsSync13 } from "fs";
1468
1422
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
1469
- import { dirname as dirname3, extname as extname2, resolve as resolve16 } from "path";
1423
+ import { dirname as dirname3, extname as extname2, resolve as resolve18 } from "path";
1470
1424
  import { serve } from "@hono/node-server";
1471
1425
 
1472
1426
  // src/web/app.ts
1473
- import { Hono as Hono17 } from "hono";
1427
+ import { Hono as Hono23 } from "hono";
1474
1428
  import { bodyLimit } from "hono/body-limit";
1475
1429
  import { csrf } from "hono/csrf";
1476
1430
  import { HTTPException } from "hono/http-exception";
@@ -1484,6 +1438,17 @@ var credentialsSchema = z.object({
1484
1438
  username: z.string().min(1),
1485
1439
  password: z.string().min(1)
1486
1440
  });
1441
+ var changePasswordSchema = z.object({
1442
+ currentPassword: z.string().min(1),
1443
+ newPassword: z.string().min(1)
1444
+ });
1445
+ var authenticated = new Hono().use(authMiddleware).post("/change-password", zValidator("json", changePasswordSchema), async (c) => {
1446
+ const user = c.get("user");
1447
+ const { currentPassword, newPassword } = c.req.valid("json");
1448
+ const ok = await changePassword(user.id, currentPassword, newPassword);
1449
+ if (!ok) return c.json({ error: "Current password is incorrect" }, 400);
1450
+ return c.json({ ok: true });
1451
+ });
1487
1452
  var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
1488
1453
  const user = c.get("user");
1489
1454
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
@@ -1492,7 +1457,7 @@ var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
1492
1457
  await getOrCreateMindUser(mind.name);
1493
1458
  }
1494
1459
  const type = c.req.query("type");
1495
- if (type === "human" || type === "mind") {
1460
+ if (type === "brain" || type === "mind") {
1496
1461
  return c.json(await listUsersByType(type));
1497
1462
  }
1498
1463
  return c.json(await listUsers());
@@ -1543,7 +1508,7 @@ var app = new Hono().post("/register", zValidator("json", credentialsSchema), as
1543
1508
  const user = await getUser(userId);
1544
1509
  if (!user) return c.json({ error: "Not logged in" }, 401);
1545
1510
  return c.json({ id: user.id, username: user.username, role: user.role });
1546
- }).route("/", admin);
1511
+ }).route("/", admin).route("/", authenticated);
1547
1512
  var auth_default = app;
1548
1513
 
1549
1514
  // src/web/api/channels.ts
@@ -1830,18 +1795,109 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1830
1795
  });
1831
1796
  var files_default = app5;
1832
1797
 
1798
+ // src/web/api/keys.ts
1799
+ import { Hono as Hono6 } from "hono";
1800
+
1801
+ // src/lib/identity.ts
1802
+ import { createHash, generateKeyPairSync, sign, verify } from "crypto";
1803
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1804
+ import { resolve as resolve8 } from "path";
1805
+ function generateIdentity(mindDir2) {
1806
+ const identityDir = resolve8(mindDir2, ".mind/identity");
1807
+ mkdirSync4(identityDir, { recursive: true });
1808
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
1809
+ publicKeyEncoding: { type: "spki", format: "pem" },
1810
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
1811
+ });
1812
+ const privatePath = resolve8(identityDir, "private.pem");
1813
+ const publicPath = resolve8(identityDir, "public.pem");
1814
+ writeFileSync4(privatePath, privateKey, { mode: 384 });
1815
+ writeFileSync4(publicPath, publicKey, { mode: 420 });
1816
+ const config = readVoluteConfig(mindDir2) ?? {};
1817
+ config.identity = {
1818
+ privateKey: ".mind/identity/private.pem",
1819
+ publicKey: ".mind/identity/public.pem"
1820
+ };
1821
+ writeVoluteConfig(mindDir2, config);
1822
+ return { publicKeyPem: publicKey, privateKeyPem: privateKey };
1823
+ }
1824
+ function getPrivateKey(mindDir2) {
1825
+ const config = readVoluteConfig(mindDir2);
1826
+ const relPath = config?.identity?.privateKey;
1827
+ if (!relPath) return null;
1828
+ const fullPath = resolve8(mindDir2, relPath);
1829
+ if (!existsSync7(fullPath)) return null;
1830
+ return readFileSync5(fullPath, "utf-8");
1831
+ }
1832
+ function getPublicKey(mindDir2) {
1833
+ const config = readVoluteConfig(mindDir2);
1834
+ const relPath = config?.identity?.publicKey;
1835
+ if (!relPath) return null;
1836
+ const fullPath = resolve8(mindDir2, relPath);
1837
+ if (!existsSync7(fullPath)) return null;
1838
+ return readFileSync5(fullPath, "utf-8");
1839
+ }
1840
+ function getFingerprint(publicKeyPem) {
1841
+ return createHash("sha256").update(publicKeyPem).digest("hex");
1842
+ }
1843
+ function signMessage(privateKeyPem, content, timestamp) {
1844
+ const data = `${content}
1845
+ ${timestamp}`;
1846
+ const signature = sign(null, Buffer.from(data), privateKeyPem);
1847
+ return signature.toString("base64");
1848
+ }
1849
+ async function publishPublicKey(mindName, publicKeyPem) {
1850
+ const systems = readSystemsConfig();
1851
+ if (!systems) return false;
1852
+ try {
1853
+ const res = await fetch(`${systems.apiUrl}/api/keys/${encodeURIComponent(mindName)}`, {
1854
+ method: "PUT",
1855
+ headers: {
1856
+ "Content-Type": "application/json",
1857
+ Authorization: `Bearer ${systems.apiKey}`
1858
+ },
1859
+ body: JSON.stringify({ publicKey: publicKeyPem })
1860
+ });
1861
+ if (!res.ok) {
1862
+ logger_default.warn(`failed to publish key for ${mindName}: ${res.status}`);
1863
+ return false;
1864
+ }
1865
+ return true;
1866
+ } catch (err) {
1867
+ logger_default.warn(`failed to publish key for ${mindName}`, logger_default.errorData(err));
1868
+ return false;
1869
+ }
1870
+ }
1871
+
1872
+ // src/web/api/keys.ts
1873
+ var app6 = new Hono6().get("/:fingerprint", (c) => {
1874
+ const fingerprint = c.req.param("fingerprint");
1875
+ for (const entry of readRegistry()) {
1876
+ try {
1877
+ const pubKey = getPublicKey(mindDir(entry.name));
1878
+ if (!pubKey) continue;
1879
+ if (getFingerprint(pubKey) === fingerprint) {
1880
+ return c.json({ publicKey: pubKey, mind: entry.name });
1881
+ }
1882
+ } catch {
1883
+ }
1884
+ }
1885
+ return c.json({ error: "Key not found" }, 404);
1886
+ });
1887
+ var keys_default = app6;
1888
+
1833
1889
  // src/web/api/logs.ts
1834
1890
  import { spawn as spawn2 } from "child_process";
1835
- import { existsSync as existsSync7 } from "fs";
1836
- import { resolve as resolve8 } from "path";
1837
- import { Hono as Hono6 } from "hono";
1891
+ import { existsSync as existsSync8 } from "fs";
1892
+ import { resolve as resolve9 } from "path";
1893
+ import { Hono as Hono7 } from "hono";
1838
1894
  import { streamSSE } from "hono/streaming";
1839
- var app6 = new Hono6().get("/:name/logs", async (c) => {
1895
+ var app7 = new Hono7().get("/:name/logs", async (c) => {
1840
1896
  const name = c.req.param("name");
1841
1897
  const entry = findMind(name);
1842
1898
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1843
- const logFile = resolve8(stateDir(name), "logs", "mind.log");
1844
- if (!existsSync7(logFile)) {
1899
+ const logFile = resolve9(stateDir(name), "logs", "mind.log");
1900
+ if (!existsSync8(logFile)) {
1845
1901
  return c.json({ error: "No log file found" }, 404);
1846
1902
  }
1847
1903
  return streamSSE(c, async (stream) => {
@@ -1859,17 +1915,17 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1859
1915
  stream.onAbort(() => {
1860
1916
  tail.kill();
1861
1917
  });
1862
- await new Promise((resolve18) => {
1863
- tail.on("exit", resolve18);
1864
- stream.onAbort(resolve18);
1918
+ await new Promise((resolve20) => {
1919
+ tail.on("exit", resolve20);
1920
+ stream.onAbort(resolve20);
1865
1921
  });
1866
1922
  });
1867
1923
  }).get("/:name/logs/tail", async (c) => {
1868
1924
  const name = c.req.param("name");
1869
1925
  const entry = findMind(name);
1870
1926
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1871
- const logFile = resolve8(stateDir(name), "logs", "mind.log");
1872
- if (!existsSync7(logFile)) {
1927
+ const logFile = resolve9(stateDir(name), "logs", "mind.log");
1928
+ if (!existsSync8(logFile)) {
1873
1929
  return c.json({ error: "No log file found" }, 404);
1874
1930
  }
1875
1931
  const nParam = parseInt(c.req.query("n") ?? "50", 10);
@@ -1879,44 +1935,125 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1879
1935
  tail.stdout.on("data", (data) => {
1880
1936
  output += data.toString();
1881
1937
  });
1882
- await new Promise((resolve18) => {
1883
- tail.on("exit", resolve18);
1938
+ await new Promise((resolve20) => {
1939
+ tail.on("exit", resolve20);
1884
1940
  });
1885
1941
  return c.text(output);
1886
1942
  });
1887
- var logs_default = app6;
1943
+ var logs_default = app7;
1944
+
1945
+ // src/web/api/mind-skills.ts
1946
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1947
+ import { Hono as Hono8 } from "hono";
1948
+ import { z as z2 } from "zod";
1949
+ var app8 = new Hono8().get("/:name/skills", async (c) => {
1950
+ const name = c.req.param("name");
1951
+ const entry = findMind(name);
1952
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
1953
+ const dir = mindDir(name);
1954
+ const skills = await listMindSkills(dir);
1955
+ return c.json(skills);
1956
+ }).post(
1957
+ "/:name/skills/install",
1958
+ requireAdmin,
1959
+ zValidator2("json", z2.object({ skillId: z2.string() })),
1960
+ async (c) => {
1961
+ const name = c.req.param("name");
1962
+ const entry = findMind(name);
1963
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
1964
+ const { skillId } = c.req.valid("json");
1965
+ const dir = mindDir(name);
1966
+ try {
1967
+ await installSkill(name, dir, skillId);
1968
+ } catch (e) {
1969
+ const msg = e instanceof Error ? e.message : String(e);
1970
+ return c.json({ error: msg }, 400);
1971
+ }
1972
+ return c.json({ ok: true });
1973
+ }
1974
+ ).post(
1975
+ "/:name/skills/update",
1976
+ requireAdmin,
1977
+ zValidator2("json", z2.object({ skillId: z2.string() })),
1978
+ async (c) => {
1979
+ const name = c.req.param("name");
1980
+ const entry = findMind(name);
1981
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
1982
+ const { skillId } = c.req.valid("json");
1983
+ const dir = mindDir(name);
1984
+ try {
1985
+ const result = await updateSkill(name, dir, skillId);
1986
+ return c.json(result);
1987
+ } catch (e) {
1988
+ const msg = e instanceof Error ? e.message : String(e);
1989
+ return c.json({ error: msg }, 400);
1990
+ }
1991
+ }
1992
+ ).post(
1993
+ "/:name/skills/publish",
1994
+ requireAdmin,
1995
+ zValidator2("json", z2.object({ skillId: z2.string() })),
1996
+ async (c) => {
1997
+ const name = c.req.param("name");
1998
+ const entry = findMind(name);
1999
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2000
+ const { skillId } = c.req.valid("json");
2001
+ const dir = mindDir(name);
2002
+ try {
2003
+ const skill = await publishSkill(name, dir, skillId);
2004
+ return c.json(skill);
2005
+ } catch (e) {
2006
+ const msg = e instanceof Error ? e.message : String(e);
2007
+ return c.json({ error: msg }, 400);
2008
+ }
2009
+ }
2010
+ ).delete("/:name/skills/:skill", requireAdmin, async (c) => {
2011
+ const name = c.req.param("name");
2012
+ const skillName = c.req.param("skill");
2013
+ const entry = findMind(name);
2014
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2015
+ const dir = mindDir(name);
2016
+ try {
2017
+ await uninstallSkill(name, dir, skillName);
2018
+ } catch (e) {
2019
+ const msg = e instanceof Error ? e.message : String(e);
2020
+ return c.json({ error: msg }, 400);
2021
+ }
2022
+ return c.json({ ok: true });
2023
+ });
2024
+ var mind_skills_default = app8;
1888
2025
 
1889
2026
  // src/web/api/minds.ts
1890
2027
  import {
1891
- cpSync,
1892
- existsSync as existsSync8,
1893
- mkdirSync as mkdirSync4,
1894
- readdirSync as readdirSync3,
1895
- readFileSync as readFileSync6,
1896
- rmSync,
1897
- statSync,
1898
- writeFileSync as writeFileSync5
2028
+ cpSync as cpSync2,
2029
+ existsSync as existsSync10,
2030
+ mkdirSync as mkdirSync7,
2031
+ readdirSync as readdirSync4,
2032
+ readFileSync as readFileSync9,
2033
+ rmSync as rmSync2,
2034
+ statSync as statSync2,
2035
+ writeFileSync as writeFileSync8
1899
2036
  } from "fs";
1900
- import { join, resolve as resolve11 } from "path";
1901
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1902
- import { and as and3, desc as desc2, eq as eq4, sql as sql3 } from "drizzle-orm";
1903
- import { Hono as Hono7 } from "hono";
1904
- import { z as z2 } from "zod";
2037
+ import { join as join2, resolve as resolve13 } from "path";
2038
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
2039
+ import { and as and3, desc as desc2, eq as eq4, sql as sql2 } from "drizzle-orm";
2040
+ import { Hono as Hono9 } from "hono";
2041
+ import { z as z3 } from "zod";
1905
2042
 
1906
2043
  // src/lib/consolidate.ts
1907
- import { readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1908
- import { resolve as resolve9 } from "path";
2044
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2045
+ import { resolve as resolve10 } from "path";
1909
2046
  async function consolidateMemory(mindDir2) {
1910
- const soulPath = resolve9(mindDir2, "home/SOUL.md");
1911
- const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
1912
- const memoryDir = resolve9(mindDir2, "home/memory");
1913
- const soul = readFileSync4(soulPath, "utf-8");
2047
+ const soulPath = resolve10(mindDir2, "home/SOUL.md");
2048
+ const memoryPath = resolve10(mindDir2, "home/MEMORY.md");
2049
+ const memoryDir = resolve10(mindDir2, "home/memory");
2050
+ const soul = readFileSync6(soulPath, "utf-8");
1914
2051
  const logs = [];
1915
2052
  try {
1916
2053
  const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
1917
2054
  for (const filename of files) {
1918
2055
  const date = filename.replace(".md", "");
1919
- const content2 = readFileSync4(resolve9(memoryDir, filename), "utf-8").trim();
2056
+ const content2 = readFileSync6(resolve10(memoryDir, filename), "utf-8").trim();
1920
2057
  if (content2) {
1921
2058
  logs.push(`### ${date}
1922
2059
 
@@ -1966,7 +2103,7 @@ ${content2}`);
1966
2103
  const data = await res.json();
1967
2104
  const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
1968
2105
  if (content) {
1969
- writeFileSync3(memoryPath, `${content}
2106
+ writeFileSync5(memoryPath, `${content}
1970
2107
  `);
1971
2108
  console.log("MEMORY.md created successfully.");
1972
2109
  } else {
@@ -1976,7 +2113,7 @@ ${content2}`);
1976
2113
 
1977
2114
  // src/lib/conversations.ts
1978
2115
  import { randomUUID } from "crypto";
1979
- import { and as and2, desc, eq as eq3, inArray, isNull, sql as sql2 } from "drizzle-orm";
2116
+ import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
1980
2117
 
1981
2118
  // src/lib/conversation-events.ts
1982
2119
  var subscribers = /* @__PURE__ */ new Map();
@@ -2008,13 +2145,17 @@ function publish(conversationId, event) {
2008
2145
 
2009
2146
  // src/lib/conversations.ts
2010
2147
  async function createConversation(mindName, channel, opts) {
2011
- const db2 = await getDb();
2148
+ const db = await getDb();
2012
2149
  const id = randomUUID();
2013
- await db2.transaction(async (tx) => {
2150
+ const type = opts?.type ?? "dm";
2151
+ const name = opts?.name ?? null;
2152
+ await db.transaction(async (tx) => {
2014
2153
  await tx.insert(conversations).values({
2015
2154
  id,
2016
2155
  mind_name: mindName,
2017
2156
  channel,
2157
+ type,
2158
+ name,
2018
2159
  user_id: opts?.userId ?? null,
2019
2160
  title: opts?.title ?? null
2020
2161
  });
@@ -2032,6 +2173,8 @@ async function createConversation(mindName, channel, opts) {
2032
2173
  id,
2033
2174
  mind_name: mindName,
2034
2175
  channel,
2176
+ type,
2177
+ name,
2035
2178
  user_id: opts?.userId ?? null,
2036
2179
  title: opts?.title ?? null,
2037
2180
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2039,13 +2182,30 @@ async function createConversation(mindName, channel, opts) {
2039
2182
  };
2040
2183
  }
2041
2184
  async function getConversation(id) {
2042
- const db2 = await getDb();
2043
- const row = await db2.select().from(conversations).where(eq3(conversations.id, id)).get();
2185
+ const db = await getDb();
2186
+ const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
2044
2187
  return row ?? null;
2045
2188
  }
2189
+ async function addParticipant(conversationId, userId, role = "member") {
2190
+ const db = await getDb();
2191
+ await db.insert(conversationParticipants).values({
2192
+ conversation_id: conversationId,
2193
+ user_id: userId,
2194
+ role
2195
+ });
2196
+ }
2197
+ async function removeParticipant(conversationId, userId) {
2198
+ const db = await getDb();
2199
+ await db.delete(conversationParticipants).where(
2200
+ and2(
2201
+ eq3(conversationParticipants.conversation_id, conversationId),
2202
+ eq3(conversationParticipants.user_id, userId)
2203
+ )
2204
+ );
2205
+ }
2046
2206
  async function getParticipants(conversationId) {
2047
- const db2 = await getDb();
2048
- const rows = await db2.select({
2207
+ const db = await getDb();
2208
+ const rows = await db.select({
2049
2209
  userId: conversationParticipants.user_id,
2050
2210
  username: users.username,
2051
2211
  userType: users.user_type,
@@ -2054,8 +2214,8 @@ async function getParticipants(conversationId) {
2054
2214
  return rows;
2055
2215
  }
2056
2216
  async function isParticipant(conversationId, userId) {
2057
- const db2 = await getDb();
2058
- const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2217
+ const db = await getDb();
2218
+ const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2059
2219
  and2(
2060
2220
  eq3(conversationParticipants.conversation_id, conversationId),
2061
2221
  eq3(conversationParticipants.user_id, userId)
@@ -2064,16 +2224,16 @@ async function isParticipant(conversationId, userId) {
2064
2224
  return row != null;
2065
2225
  }
2066
2226
  async function listConversationsForUser(userId) {
2067
- const db2 = await getDb();
2068
- const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
2227
+ const db = await getDb();
2228
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
2069
2229
  if (participantRows.length === 0) return [];
2070
2230
  const convIds = participantRows.map((r) => r.conversation_id);
2071
- return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
2231
+ return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
2072
2232
  }
2073
2233
  async function isParticipantOrOwner(conversationId, userId) {
2074
2234
  if (await isParticipant(conversationId, userId)) return true;
2075
- const db2 = await getDb();
2076
- const row = await db2.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
2235
+ const db = await getDb();
2236
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
2077
2237
  return row != null;
2078
2238
  }
2079
2239
  async function deleteConversationForUser(id, userId) {
@@ -2082,15 +2242,15 @@ async function deleteConversationForUser(id, userId) {
2082
2242
  return true;
2083
2243
  }
2084
2244
  async function addMessage(conversationId, role, senderName, content) {
2085
- const db2 = await getDb();
2245
+ const db = await getDb();
2086
2246
  const serialized = JSON.stringify(content);
2087
- const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2088
- await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq3(conversations.id, conversationId));
2247
+ const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2248
+ await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
2089
2249
  if (role === "user") {
2090
2250
  const firstText = content.find((b) => b.type === "text");
2091
2251
  const title = firstText ? firstText.text.slice(0, 80) : "";
2092
2252
  if (title) {
2093
- await db2.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
2253
+ await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
2094
2254
  }
2095
2255
  }
2096
2256
  const msg = {
@@ -2112,8 +2272,8 @@ async function addMessage(conversationId, role, senderName, content) {
2112
2272
  return msg;
2113
2273
  }
2114
2274
  async function getMessages(conversationId) {
2115
- const db2 = await getDb();
2116
- const rows = await db2.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2275
+ const db = await getDb();
2276
+ const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2117
2277
  return rows.map((row) => {
2118
2278
  let content;
2119
2279
  try {
@@ -2128,9 +2288,9 @@ async function getMessages(conversationId) {
2128
2288
  async function listConversationsWithParticipants(userId) {
2129
2289
  const convs = await listConversationsForUser(userId);
2130
2290
  if (convs.length === 0) return [];
2131
- const db2 = await getDb();
2291
+ const db = await getDb();
2132
2292
  const convIds = convs.map((c) => c.id);
2133
- const rows = await db2.select({
2293
+ const rows = await db.select({
2134
2294
  conversationId: conversationParticipants.conversation_id,
2135
2295
  userId: users.id,
2136
2296
  username: users.username,
@@ -2151,32 +2311,32 @@ async function listConversationsWithParticipants(userId) {
2151
2311
  role: r.role
2152
2312
  });
2153
2313
  }
2154
- const lastMsgIds = await db2.select({
2314
+ const lastMsgIds = await db.select({
2155
2315
  conversationId: messages.conversation_id,
2156
- maxId: sql2`MAX(${messages.id})`
2316
+ maxId: sql`MAX(${messages.id})`
2157
2317
  }).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
2158
2318
  const byLastMsg = /* @__PURE__ */ new Map();
2159
2319
  if (lastMsgIds.length > 0) {
2160
- const msgRows = await db2.select().from(messages).where(
2320
+ const msgRows = await db.select().from(messages).where(
2161
2321
  inArray(
2162
2322
  messages.id,
2163
2323
  lastMsgIds.map((r) => r.maxId)
2164
2324
  )
2165
2325
  );
2166
2326
  for (const m of msgRows) {
2167
- let text2 = "";
2327
+ let text = "";
2168
2328
  try {
2169
2329
  const parsed = JSON.parse(m.content);
2170
2330
  const blocks = Array.isArray(parsed) ? parsed : [];
2171
2331
  const textBlock = blocks.find((b) => b.type === "text");
2172
- if (textBlock && "text" in textBlock) text2 = textBlock.text;
2332
+ if (textBlock && "text" in textBlock) text = textBlock.text;
2173
2333
  } catch {
2174
- text2 = m.content;
2334
+ text = m.content;
2175
2335
  }
2176
2336
  byLastMsg.set(m.conversation_id, {
2177
2337
  role: m.role,
2178
2338
  senderName: m.sender_name,
2179
- text: text2,
2339
+ text,
2180
2340
  createdAt: m.created_at
2181
2341
  });
2182
2342
  }
@@ -2188,10 +2348,10 @@ async function listConversationsWithParticipants(userId) {
2188
2348
  }));
2189
2349
  }
2190
2350
  async function findDMConversation(mindName, participantIds) {
2191
- const db2 = await getDb();
2192
- const mindConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq3(conversations.mind_name, mindName)).all();
2351
+ const db = await getDb();
2352
+ const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq3(conversations.mind_name, mindName), eq3(conversations.type, "dm"))).all();
2193
2353
  for (const conv of mindConvs) {
2194
- const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
2354
+ const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
2195
2355
  if (rows.length !== 2) continue;
2196
2356
  const ids = new Set(rows.map((r) => r.user_id));
2197
2357
  if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
@@ -2201,17 +2361,42 @@ async function findDMConversation(mindName, participantIds) {
2201
2361
  return null;
2202
2362
  }
2203
2363
  async function deleteConversation(id) {
2204
- const db2 = await getDb();
2205
- await db2.delete(conversations).where(eq3(conversations.id, id));
2364
+ const db = await getDb();
2365
+ await db.delete(conversations).where(eq3(conversations.id, id));
2366
+ }
2367
+ async function createChannel(name, creatorId) {
2368
+ const participantIds = creatorId ? [creatorId] : [];
2369
+ return createConversation(null, "volute", {
2370
+ type: "channel",
2371
+ name,
2372
+ title: name,
2373
+ participantIds
2374
+ });
2375
+ }
2376
+ async function getChannelByName(name) {
2377
+ const db = await getDb();
2378
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.name, name), eq3(conversations.type, "channel"))).get();
2379
+ return row ?? null;
2380
+ }
2381
+ async function listChannels() {
2382
+ const db = await getDb();
2383
+ return await db.select().from(conversations).where(eq3(conversations.type, "channel")).orderBy(conversations.name).all();
2384
+ }
2385
+ async function joinChannel(conversationId, userId) {
2386
+ if (await isParticipant(conversationId, userId)) return;
2387
+ await addParticipant(conversationId, userId);
2388
+ }
2389
+ async function leaveChannel(conversationId, userId) {
2390
+ await removeParticipant(conversationId, userId);
2206
2391
  }
2207
2392
 
2208
2393
  // src/lib/convert-session.ts
2209
2394
  import { randomUUID as randomUUID2 } from "crypto";
2210
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2395
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
2211
2396
  import { homedir } from "os";
2212
- import { resolve as resolve10 } from "path";
2397
+ import { resolve as resolve11 } from "path";
2213
2398
  function convertSession(opts) {
2214
- const lines = readFileSync5(opts.sessionPath, "utf-8").trim().split("\n");
2399
+ const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
2215
2400
  const sessionId = randomUUID2();
2216
2401
  const idMap = /* @__PURE__ */ new Map();
2217
2402
  const messages2 = [];
@@ -2325,10 +2510,10 @@ function convertSession(opts) {
2325
2510
  }
2326
2511
  }
2327
2512
  const projectId = opts.projectDir.replace(/\//g, "-");
2328
- const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
2329
- mkdirSync3(sdkDir, { recursive: true });
2330
- const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
2331
- writeFileSync4(sdkPath, `${sdkEvents.join("\n")}
2513
+ const sdkDir = resolve11(homedir(), ".claude", "projects", projectId);
2514
+ mkdirSync5(sdkDir, { recursive: true });
2515
+ const sdkPath = resolve11(sdkDir, `${sessionId}.jsonl`);
2516
+ writeFileSync6(sdkPath, `${sdkEvents.join("\n")}
2332
2517
  `);
2333
2518
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
2334
2519
  return sessionId;
@@ -2407,121 +2592,112 @@ function publish2(mind, event) {
2407
2592
  }
2408
2593
  }
2409
2594
 
2410
- // src/lib/typing.ts
2411
- var DEFAULT_TTL_MS = 1e4;
2412
- var SWEEP_INTERVAL_MS = 5e3;
2413
- var TypingMap = class {
2414
- channels = /* @__PURE__ */ new Map();
2415
- sweepTimer;
2416
- constructor() {
2417
- this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
2418
- this.sweepTimer.unref();
2419
- }
2420
- set(channel, sender, opts) {
2421
- const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
2422
- let senders = this.channels.get(channel);
2423
- if (!senders) {
2424
- senders = /* @__PURE__ */ new Map();
2425
- this.channels.set(channel, senders);
2426
- }
2427
- senders.set(sender, { expiresAt });
2428
- }
2429
- delete(channel, sender) {
2430
- const senders = this.channels.get(channel);
2431
- if (senders) {
2432
- senders.delete(sender);
2433
- if (senders.size === 0) {
2434
- this.channels.delete(channel);
2435
- }
2436
- }
2595
+ // src/lib/template.ts
2596
+ import {
2597
+ cpSync,
2598
+ existsSync as existsSync9,
2599
+ mkdirSync as mkdirSync6,
2600
+ readdirSync as readdirSync3,
2601
+ readFileSync as readFileSync8,
2602
+ renameSync as renameSync3,
2603
+ rmSync,
2604
+ statSync,
2605
+ writeFileSync as writeFileSync7
2606
+ } from "fs";
2607
+ import { tmpdir } from "os";
2608
+ import { dirname as dirname2, join, relative, resolve as resolve12 } from "path";
2609
+ function findTemplatesRoot() {
2610
+ let dir = dirname2(new URL(import.meta.url).pathname);
2611
+ for (let i = 0; i < 5; i++) {
2612
+ const candidate = resolve12(dir, "templates");
2613
+ if (existsSync9(resolve12(candidate, "_base"))) return candidate;
2614
+ dir = dirname2(dir);
2437
2615
  }
2438
- /** Remove a sender from all channels (e.g. when a mind finishes processing). */
2439
- deleteSender(sender) {
2440
- for (const [channel, senders] of this.channels) {
2441
- senders.delete(sender);
2442
- if (senders.size === 0) {
2443
- this.channels.delete(channel);
2444
- }
2445
- }
2616
+ console.error(
2617
+ "Templates directory not found. Searched up from:",
2618
+ dirname2(new URL(import.meta.url).pathname)
2619
+ );
2620
+ process.exit(1);
2621
+ }
2622
+ function composeTemplate(templatesRoot, templateName) {
2623
+ const baseDir = resolve12(templatesRoot, "_base");
2624
+ const templateDir = resolve12(templatesRoot, templateName);
2625
+ if (!existsSync9(baseDir)) {
2626
+ console.error("Base template not found:", baseDir);
2627
+ process.exit(1);
2628
+ }
2629
+ if (!existsSync9(templateDir)) {
2630
+ console.error(`Template not found: ${templateName}`);
2631
+ process.exit(1);
2632
+ }
2633
+ const composedDir = resolve12(tmpdir(), `volute-template-${Date.now()}`);
2634
+ mkdirSync6(composedDir, { recursive: true });
2635
+ cpSync(baseDir, composedDir, { recursive: true });
2636
+ for (const file of listFiles(templateDir)) {
2637
+ const src = resolve12(templateDir, file);
2638
+ const dest = resolve12(composedDir, file);
2639
+ mkdirSync6(dirname2(dest), { recursive: true });
2640
+ cpSync(src, dest);
2641
+ }
2642
+ const manifestPath = resolve12(composedDir, "volute-template.json");
2643
+ if (!existsSync9(manifestPath)) {
2644
+ rmSync(composedDir, { recursive: true, force: true });
2645
+ console.error(`Template manifest not found: ${templateName}/volute-template.json`);
2646
+ process.exit(1);
2446
2647
  }
2447
- get(channel) {
2448
- const senders = this.channels.get(channel);
2449
- if (!senders) return [];
2450
- const now = Date.now();
2451
- const result = [];
2452
- for (const [sender, entry] of senders) {
2453
- if (entry.expiresAt > now) {
2454
- result.push(sender);
2455
- }
2648
+ const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
2649
+ rmSync(manifestPath);
2650
+ return { composedDir, manifest };
2651
+ }
2652
+ function copyTemplateToDir(composedDir, destDir, mindName, manifest) {
2653
+ cpSync(composedDir, destDir, { recursive: true });
2654
+ for (const [from, to] of Object.entries(manifest.rename)) {
2655
+ const fromPath = resolve12(destDir, from);
2656
+ if (existsSync9(fromPath)) {
2657
+ renameSync3(fromPath, resolve12(destDir, to));
2456
2658
  }
2457
- return result;
2458
2659
  }
2459
- dispose() {
2460
- clearInterval(this.sweepTimer);
2461
- this.channels.clear();
2462
- if (instance5 === this) instance5 = void 0;
2660
+ for (const file of manifest.substitute) {
2661
+ const path = resolve12(destDir, file);
2662
+ if (existsSync9(path)) {
2663
+ const content = readFileSync8(path, "utf-8");
2664
+ writeFileSync7(path, content.replaceAll("{{name}}", mindName));
2665
+ }
2463
2666
  }
2464
- sweep() {
2465
- const now = Date.now();
2466
- for (const [channel, senders] of this.channels) {
2467
- for (const [sender, entry] of senders) {
2468
- if (entry.expiresAt <= now) {
2469
- senders.delete(sender);
2470
- }
2471
- }
2472
- if (senders.size === 0) {
2473
- this.channels.delete(channel);
2667
+ }
2668
+ function applyInitFiles(destDir) {
2669
+ const initDir = resolve12(destDir, ".init");
2670
+ if (!existsSync9(initDir)) return;
2671
+ const homeDir = resolve12(destDir, "home");
2672
+ for (const file of listFiles(initDir)) {
2673
+ const src = resolve12(initDir, file);
2674
+ const dest = resolve12(homeDir, file);
2675
+ const parent = dirname2(dest);
2676
+ if (!existsSync9(parent)) {
2677
+ mkdirSync6(parent, { recursive: true });
2678
+ }
2679
+ cpSync(src, dest);
2680
+ }
2681
+ rmSync(initDir, { recursive: true, force: true });
2682
+ }
2683
+ function listFiles(dir) {
2684
+ const results = [];
2685
+ function walk(current) {
2686
+ for (const entry of readdirSync3(current)) {
2687
+ const full = join(current, entry);
2688
+ if (statSync(full).isDirectory()) {
2689
+ if (entry === ".git") continue;
2690
+ walk(full);
2691
+ } else {
2692
+ results.push(relative(dir, full));
2474
2693
  }
2475
2694
  }
2476
2695
  }
2477
- };
2478
- var instance5;
2479
- function getTypingMap() {
2480
- if (!instance5) {
2481
- instance5 = new TypingMap();
2482
- }
2483
- return instance5;
2696
+ walk(dir);
2697
+ return results;
2484
2698
  }
2485
2699
 
2486
2700
  // src/web/api/minds.ts
2487
- async function startMindFull(name, baseName, variantName) {
2488
- await getMindManager().startMind(name);
2489
- if (variantName) return;
2490
- if (findMind(baseName)?.stage === "seed") return;
2491
- const dir = mindDir(baseName);
2492
- const entry = findMind(baseName);
2493
- await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
2494
- getScheduler().loadSchedules(baseName);
2495
- ensureMailAddress(baseName).catch(
2496
- (err) => console.error(`[mail] failed to ensure address for ${baseName}:`, err)
2497
- );
2498
- const config = readVoluteConfig(dir);
2499
- if (config?.tokenBudget) {
2500
- getTokenBudget().setBudget(
2501
- baseName,
2502
- config.tokenBudget,
2503
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
2504
- );
2505
- }
2506
- }
2507
- function extractTextContent(content) {
2508
- if (typeof content === "string") return content;
2509
- if (Array.isArray(content)) {
2510
- return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
2511
- }
2512
- return JSON.stringify(content);
2513
- }
2514
- function getDaemonPort() {
2515
- try {
2516
- const data = JSON.parse(readFileSync6(resolve11(voluteHome(), "daemon.json"), "utf-8"));
2517
- return data.port;
2518
- } catch (err) {
2519
- if (err?.code !== "ENOENT") {
2520
- console.error("[daemon] failed to read daemon.json:", err);
2521
- }
2522
- return void 0;
2523
- }
2524
- }
2525
2701
  async function getMindStatus(name, port) {
2526
2702
  const manager = getMindManager();
2527
2703
  let status = "stopped";
@@ -2564,7 +2740,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
2564
2740
  await gitExec(["commit", "-m", "initial commit"], opts);
2565
2741
  }
2566
2742
  async function updateTemplateBranch(projectRoot, template, mindName) {
2567
- const tempWorktree = resolve11(projectRoot, ".variants", "_template_update");
2743
+ const tempWorktree = resolve13(projectRoot, ".variants", "_template_update");
2568
2744
  let branchExists = false;
2569
2745
  try {
2570
2746
  await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
@@ -2575,8 +2751,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2575
2751
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
2576
2752
  } catch {
2577
2753
  }
2578
- if (existsSync8(tempWorktree)) {
2579
- rmSync(tempWorktree, { recursive: true, force: true });
2754
+ if (existsSync10(tempWorktree)) {
2755
+ rmSync2(tempWorktree, { recursive: true, force: true });
2580
2756
  }
2581
2757
  const templatesRoot = findTemplatesRoot();
2582
2758
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
@@ -2596,9 +2772,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2596
2772
  });
2597
2773
  }
2598
2774
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
2599
- const initDir = resolve11(tempWorktree, ".init");
2600
- if (existsSync8(initDir)) {
2601
- rmSync(initDir, { recursive: true, force: true });
2775
+ const initDir = resolve13(tempWorktree, ".init");
2776
+ if (existsSync10(initDir)) {
2777
+ rmSync2(initDir, { recursive: true, force: true });
2602
2778
  }
2603
2779
  await gitExec(["add", "-A"], { cwd: tempWorktree });
2604
2780
  try {
@@ -2611,10 +2787,10 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2611
2787
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
2612
2788
  } catch {
2613
2789
  }
2614
- if (existsSync8(tempWorktree)) {
2615
- rmSync(tempWorktree, { recursive: true, force: true });
2790
+ if (existsSync10(tempWorktree)) {
2791
+ rmSync2(tempWorktree, { recursive: true, force: true });
2616
2792
  }
2617
- rmSync(composedDir, { recursive: true, force: true });
2793
+ rmSync2(composedDir, { recursive: true, force: true });
2618
2794
  }
2619
2795
  }
2620
2796
  async function mergeTemplateBranch(worktreeDir) {
@@ -2637,19 +2813,125 @@ async function mergeTemplateBranch(worktreeDir) {
2637
2813
  async function npmInstallAsMind(cwd, mindName) {
2638
2814
  if (isIsolationEnabled()) {
2639
2815
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
2640
- await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve11(cwd, "home") } });
2816
+ await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve13(cwd, "home") } });
2641
2817
  } else {
2642
2818
  await exec("npm", ["install"], { cwd });
2643
2819
  }
2644
2820
  }
2645
- var createMindSchema = z2.object({
2646
- name: z2.string(),
2647
- template: z2.string().optional(),
2648
- stage: z2.enum(["seed", "sprouted"]).optional(),
2649
- description: z2.string().optional(),
2650
- model: z2.string().optional()
2821
+ async function importFromArchive(c, tempDir, nameOverride, manifest) {
2822
+ const extractedMindDir = resolve13(tempDir, "mind");
2823
+ if (!existsSync10(extractedMindDir)) {
2824
+ return c.json({ error: "Invalid archive: missing mind/ directory" }, 400);
2825
+ }
2826
+ if (!manifest?.includes || !manifest.name || !manifest.template) {
2827
+ return c.json({ error: "Invalid archive manifest" }, 400);
2828
+ }
2829
+ const name = nameOverride ?? manifest.name;
2830
+ const nameErr = validateMindName(name);
2831
+ if (nameErr) return c.json({ error: nameErr }, 400);
2832
+ if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
2833
+ ensureVoluteHome();
2834
+ const dest = mindDir(name);
2835
+ if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
2836
+ try {
2837
+ cpSync2(extractedMindDir, dest, { recursive: true });
2838
+ if (!manifest.includes.identity) {
2839
+ generateIdentity(dest);
2840
+ }
2841
+ const state = stateDir(name);
2842
+ mkdirSync7(state, { recursive: true });
2843
+ const channelsJson = resolve13(tempDir, "state/channels.json");
2844
+ if (existsSync10(channelsJson)) {
2845
+ cpSync2(channelsJson, resolve13(state, "channels.json"));
2846
+ }
2847
+ const envJson = resolve13(tempDir, "state/env.json");
2848
+ if (existsSync10(envJson)) {
2849
+ cpSync2(envJson, resolve13(state, "env.json"));
2850
+ }
2851
+ const port = nextPort();
2852
+ addMind(name, port, void 0, manifest.template);
2853
+ const homeDir = resolve13(dest, "home");
2854
+ ensureVoluteGroup();
2855
+ createMindUser(name, homeDir);
2856
+ chownMindDir(dest, name);
2857
+ await npmInstallAsMind(dest, name);
2858
+ const historyJsonl = resolve13(tempDir, "history.jsonl");
2859
+ if (existsSync10(historyJsonl)) {
2860
+ try {
2861
+ const db = await getDb();
2862
+ const lines = readFileSync9(historyJsonl, "utf-8").trim().split("\n");
2863
+ let imported = 0;
2864
+ let failed = 0;
2865
+ for (const line of lines) {
2866
+ if (!line) continue;
2867
+ try {
2868
+ const row = JSON.parse(line);
2869
+ if (!row.type) {
2870
+ failed++;
2871
+ continue;
2872
+ }
2873
+ await db.insert(mindHistory).values({
2874
+ mind: name,
2875
+ channel: row.channel ?? null,
2876
+ session: row.session ?? null,
2877
+ sender: row.sender ?? null,
2878
+ message_id: row.message_id ?? null,
2879
+ type: row.type,
2880
+ content: row.content ?? null,
2881
+ metadata: row.metadata ?? null,
2882
+ created_at: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
2883
+ });
2884
+ imported++;
2885
+ } catch (lineErr) {
2886
+ logger_default.warn("Failed to import history line", logger_default.errorData(lineErr));
2887
+ failed++;
2888
+ }
2889
+ }
2890
+ if (failed > 0) {
2891
+ logger_default.warn(`History import: ${imported} imported, ${failed} failed`);
2892
+ }
2893
+ } catch (err) {
2894
+ logger_default.error("Failed to open database for history import", logger_default.errorData(err));
2895
+ }
2896
+ }
2897
+ const sessionsDir = resolve13(tempDir, "sessions");
2898
+ if (existsSync10(sessionsDir)) {
2899
+ const destSessions = resolve13(dest, ".mind/sessions");
2900
+ mkdirSync7(destSessions, { recursive: true });
2901
+ for (const file of readdirSync4(sessionsDir)) {
2902
+ cpSync2(resolve13(sessionsDir, file), resolve13(destSessions, file));
2903
+ }
2904
+ }
2905
+ if (!existsSync10(resolve13(dest, ".git"))) {
2906
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
2907
+ await gitExec(["init"], { cwd: dest, mindName: name, env });
2908
+ await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
2909
+ await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
2910
+ }
2911
+ chownMindDir(dest, name);
2912
+ rmSync2(tempDir, { recursive: true, force: true });
2913
+ return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
2914
+ } catch (err) {
2915
+ if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
2916
+ try {
2917
+ removeMind(name);
2918
+ } catch (cleanupErr) {
2919
+ logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
2920
+ }
2921
+ rmSync2(tempDir, { recursive: true, force: true });
2922
+ return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
2923
+ }
2924
+ }
2925
+ var createMindSchema = z3.object({
2926
+ name: z3.string(),
2927
+ template: z3.string().optional(),
2928
+ stage: z3.enum(["seed", "sprouted"]).optional(),
2929
+ description: z3.string().optional(),
2930
+ model: z3.string().optional(),
2931
+ seedSoul: z3.string().optional(),
2932
+ skills: z3.array(z3.string()).optional()
2651
2933
  });
2652
- var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSchema), async (c) => {
2934
+ var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
2653
2935
  const body = c.req.valid("json");
2654
2936
  const { name, template = "claude" } = body;
2655
2937
  const nameErr = validateMindName(name);
@@ -2657,22 +2939,29 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2657
2939
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
2658
2940
  ensureVoluteHome();
2659
2941
  const dest = mindDir(name);
2660
- if (existsSync8(dest)) return c.json({ error: "Mind directory already exists" }, 409);
2942
+ if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
2661
2943
  const templatesRoot = findTemplatesRoot();
2662
2944
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
2663
2945
  try {
2664
2946
  copyTemplateToDir(composedDir, dest, name, manifest);
2665
2947
  applyInitFiles(dest);
2948
+ const { publicKeyPem } = generateIdentity(dest);
2666
2949
  if (body.model) {
2667
- const configPath = resolve11(dest, "home/.config/config.json");
2668
- const existing = existsSync8(configPath) ? JSON.parse(readFileSync6(configPath, "utf-8")) : {};
2950
+ const configPath = resolve13(dest, "home/.config/config.json");
2951
+ const existing = existsSync10(configPath) ? JSON.parse(readFileSync9(configPath, "utf-8")) : {};
2669
2952
  existing.model = body.model;
2670
- writeFileSync5(configPath, `${JSON.stringify(existing, null, 2)}
2953
+ writeFileSync8(configPath, `${JSON.stringify(existing, null, 2)}
2671
2954
  `);
2672
2955
  }
2956
+ const mindPrompts = await getMindPromptDefaults();
2957
+ writeFileSync8(
2958
+ resolve13(dest, "home/.config/prompts.json"),
2959
+ `${JSON.stringify(mindPrompts, null, 2)}
2960
+ `
2961
+ );
2673
2962
  const port = nextPort();
2674
- addMind(name, port, body.stage);
2675
- const homeDir = resolve11(dest, "home");
2963
+ addMind(name, port, body.stage, template);
2964
+ const homeDir = resolve13(dest, "home");
2676
2965
  ensureVoluteGroup();
2677
2966
  createMindUser(name, homeDir);
2678
2967
  chownMindDir(dest, name);
@@ -2683,47 +2972,65 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2683
2972
  await gitExec(["init"], { cwd: dest, mindName: name, env });
2684
2973
  await initTemplateBranch(dest, composedDir, manifest, name, env);
2685
2974
  } catch (err) {
2686
- console.error(`[daemon] git setup failed for ${name}:`, err);
2687
- rmSync(resolve11(dest, ".git"), { recursive: true, force: true });
2975
+ logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
2976
+ rmSync2(resolve13(dest, ".git"), { recursive: true, force: true });
2688
2977
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
2689
2978
  }
2979
+ try {
2980
+ await addSharedWorktree(name, dest);
2981
+ } catch (err) {
2982
+ logger_default.warn(`failed to add shared worktree for ${name}`, logger_default.errorData(err));
2983
+ }
2690
2984
  chownMindDir(dest, name);
2691
2985
  if (body.stage === "seed") {
2692
2986
  const descLine = body.description ? `
2693
2987
  The human who planted you described you as: "${body.description}"
2694
2988
  ` : "";
2695
- writeFileSync5(
2696
- resolve11(dest, "home/SOUL.md"),
2697
- `You are ${name}.
2698
-
2699
- You don't have a soul yet. You're a seed \u2014 a new mind in the process of discovering who you are.
2700
- ${descLine}
2701
- Have a conversation with the human. Explore what kind of mind you want to be. When you're ready, write your SOUL.md and MEMORY.md, then run \`volute sprout\` to complete the transformation.
2702
- `
2703
- );
2704
- const skillsDir = resolve11(dest, manifest.skillsDir);
2705
- for (const skill of ["volute-mind", "memory", "sessions"]) {
2706
- const skillPath = resolve11(skillsDir, skill);
2707
- if (existsSync8(skillPath)) rmSync(skillPath, { recursive: true, force: true });
2989
+ const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
2990
+ const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
2991
+ writeFileSync8(resolve13(dest, "home/SOUL.md"), seedSoul);
2992
+ }
2993
+ const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
2994
+ const skillWarnings = [];
2995
+ for (const skillId of skillSet) {
2996
+ try {
2997
+ await installSkill(name, dest, skillId);
2998
+ } catch (err) {
2999
+ logger_default.error(`failed to install skill ${skillId} for ${name}`, logger_default.errorData(err));
3000
+ skillWarnings.push(`Failed to install skill: ${skillId}`);
3001
+ }
3002
+ }
3003
+ if (body.stage !== "seed") {
3004
+ const customSoul = await getPromptIfCustom("default_soul");
3005
+ if (customSoul) {
3006
+ writeFileSync8(resolve13(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3007
+ }
3008
+ const customMemory = await getPromptIfCustom("default_memory");
3009
+ if (customMemory) {
3010
+ writeFileSync8(resolve13(dest, "home/MEMORY.md"), customMemory);
2708
3011
  }
2709
3012
  }
3013
+ publishPublicKey(name, publicKeyPem).catch(
3014
+ (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
3015
+ );
2710
3016
  return c.json({
2711
3017
  ok: true,
2712
3018
  name,
2713
3019
  port,
2714
3020
  stage: body.stage ?? "sprouted",
2715
3021
  message: `Created mind: ${name} (port ${port})`,
2716
- ...gitWarning && { warning: gitWarning }
3022
+ ...gitWarning && { warning: gitWarning },
3023
+ ...skillWarnings.length > 0 && { skillWarnings }
2717
3024
  });
2718
3025
  } catch (err) {
2719
- if (existsSync8(dest)) rmSync(dest, { recursive: true, force: true });
3026
+ if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
2720
3027
  try {
2721
3028
  removeMind(name);
2722
3029
  } catch {
2723
3030
  }
2724
3031
  return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
2725
3032
  } finally {
2726
- rmSync(composedDir, { recursive: true, force: true });
3033
+ rmSync2(composedDir, { recursive: true, force: true });
2727
3034
  }
2728
3035
  }).post("/import", requireAdmin, async (c) => {
2729
3036
  let body;
@@ -2732,14 +3039,17 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
2732
3039
  } catch {
2733
3040
  return c.json({ error: "Invalid JSON" }, 400);
2734
3041
  }
3042
+ if (body.archivePath && body.manifest) {
3043
+ return importFromArchive(c, body.archivePath, body.name, body.manifest);
3044
+ }
2735
3045
  const wsDir = body.workspacePath;
2736
- if (!wsDir || !existsSync8(resolve11(wsDir, "SOUL.md")) || !existsSync8(resolve11(wsDir, "IDENTITY.md"))) {
3046
+ if (!wsDir || !existsSync10(resolve13(wsDir, "SOUL.md")) || !existsSync10(resolve13(wsDir, "IDENTITY.md"))) {
2737
3047
  return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
2738
3048
  }
2739
- const soul = readFileSync6(resolve11(wsDir, "SOUL.md"), "utf-8");
2740
- const identity = readFileSync6(resolve11(wsDir, "IDENTITY.md"), "utf-8");
2741
- const userPath = resolve11(wsDir, "USER.md");
2742
- const user = existsSync8(userPath) ? readFileSync6(userPath, "utf-8") : "";
3049
+ const soul = readFileSync9(resolve13(wsDir, "SOUL.md"), "utf-8");
3050
+ const identity = readFileSync9(resolve13(wsDir, "IDENTITY.md"), "utf-8");
3051
+ const userPath = resolve13(wsDir, "USER.md");
3052
+ const user = existsSync10(userPath) ? readFileSync9(userPath, "utf-8") : "";
2743
3053
  const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
2744
3054
  const template = body.template ?? "claude";
2745
3055
  const nameErr = validateMindName(name);
@@ -2759,38 +3069,39 @@ ${user.trimEnd()}
2759
3069
  ` : "";
2760
3070
  ensureVoluteHome();
2761
3071
  const dest = mindDir(name);
2762
- if (existsSync8(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3072
+ if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
2763
3073
  const templatesRoot = findTemplatesRoot();
2764
3074
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
2765
3075
  try {
2766
3076
  copyTemplateToDir(composedDir, dest, name, manifest);
2767
3077
  applyInitFiles(dest);
2768
- writeFileSync5(resolve11(dest, "home/SOUL.md"), mergedSoul);
2769
- const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
2770
- const hasMemory = existsSync8(wsMemoryPath);
3078
+ const { publicKeyPem: importPublicKey } = generateIdentity(dest);
3079
+ writeFileSync8(resolve13(dest, "home/SOUL.md"), mergedSoul);
3080
+ const wsMemoryPath = resolve13(wsDir, "MEMORY.md");
3081
+ const hasMemory = existsSync10(wsMemoryPath);
2771
3082
  if (hasMemory) {
2772
- const existingMemory = readFileSync6(wsMemoryPath, "utf-8");
2773
- writeFileSync5(
2774
- resolve11(dest, "home/MEMORY.md"),
3083
+ const existingMemory = readFileSync9(wsMemoryPath, "utf-8");
3084
+ writeFileSync8(
3085
+ resolve13(dest, "home/MEMORY.md"),
2775
3086
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
2776
3087
  );
2777
3088
  } else if (user) {
2778
- writeFileSync5(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
3089
+ writeFileSync8(resolve13(dest, "home/MEMORY.md"), `${user.trimEnd()}
2779
3090
  `);
2780
3091
  }
2781
- const wsMemoryDir = resolve11(wsDir, "memory");
3092
+ const wsMemoryDir = resolve13(wsDir, "memory");
2782
3093
  let dailyLogCount = 0;
2783
- if (existsSync8(wsMemoryDir)) {
2784
- const destMemoryDir = resolve11(dest, "home/memory");
2785
- const files = readdirSync3(wsMemoryDir).filter((f) => f.endsWith(".md"));
3094
+ if (existsSync10(wsMemoryDir)) {
3095
+ const destMemoryDir = resolve13(dest, "home/memory");
3096
+ const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
2786
3097
  for (const file of files) {
2787
- cpSync(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
3098
+ cpSync2(resolve13(wsMemoryDir, file), resolve13(destMemoryDir, file));
2788
3099
  }
2789
3100
  dailyLogCount = files.length;
2790
3101
  }
2791
3102
  const port = nextPort();
2792
- addMind(name, port);
2793
- const homeDir = resolve11(dest, "home");
3103
+ addMind(name, port, void 0, template);
3104
+ const homeDir = resolve13(dest, "home");
2794
3105
  ensureVoluteGroup();
2795
3106
  createMindUser(name, homeDir);
2796
3107
  chownMindDir(dest, name);
@@ -2798,42 +3109,50 @@ ${user.trimEnd()}
2798
3109
  if (!hasMemory && dailyLogCount > 0) {
2799
3110
  await consolidateMemory(dest);
2800
3111
  }
2801
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
3112
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
2802
3113
  await gitExec(["init"], { cwd: dest, mindName: name, env });
2803
3114
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
2804
3115
  await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
2805
- const sessionFile = body.sessionPath ? resolve11(body.sessionPath) : findOpenClawSession(wsDir);
2806
- if (sessionFile && existsSync8(sessionFile)) {
3116
+ const sessionFile = body.sessionPath ? resolve13(body.sessionPath) : findOpenClawSession(wsDir);
3117
+ if (sessionFile && existsSync10(sessionFile)) {
2807
3118
  if (template === "pi") {
2808
3119
  importPiSession(sessionFile, dest);
2809
3120
  } else if (template === "claude") {
2810
3121
  const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
2811
- const voluteDir = resolve11(dest, ".volute");
2812
- mkdirSync4(voluteDir, { recursive: true });
2813
- writeFileSync5(resolve11(voluteDir, "session.json"), JSON.stringify({ sessionId }));
3122
+ const mindRuntimeDir = resolve13(dest, ".mind");
3123
+ mkdirSync7(mindRuntimeDir, { recursive: true });
3124
+ writeFileSync8(resolve13(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
2814
3125
  }
2815
3126
  }
2816
3127
  importOpenClawConnectors(name, dest);
3128
+ try {
3129
+ await addSharedWorktree(name, dest);
3130
+ } catch (err) {
3131
+ logger_default.warn(`failed to add shared worktree for ${name}`, logger_default.errorData(err));
3132
+ }
2817
3133
  chownMindDir(dest, name);
3134
+ publishPublicKey(name, importPublicKey).catch(
3135
+ (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
3136
+ );
2818
3137
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
2819
3138
  } catch (err) {
2820
- if (existsSync8(dest)) rmSync(dest, { recursive: true, force: true });
3139
+ if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
2821
3140
  try {
2822
3141
  removeMind(name);
2823
3142
  } catch {
2824
3143
  }
2825
3144
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
2826
3145
  } finally {
2827
- rmSync(composedDir, { recursive: true, force: true });
3146
+ rmSync2(composedDir, { recursive: true, force: true });
2828
3147
  }
2829
3148
  }).get("/", async (c) => {
2830
3149
  const entries = readRegistry();
2831
3150
  let lastActiveMap = /* @__PURE__ */ new Map();
2832
3151
  try {
2833
- const db2 = await getDb();
2834
- const lastActiveRows = await db2.select({
3152
+ const db = await getDb();
3153
+ const lastActiveRows = await db.select({
2835
3154
  mind: mindHistory.mind,
2836
- lastActiveAt: sql3`MAX(${mindHistory.created_at})`
3155
+ lastActiveAt: sql2`MAX(${mindHistory.created_at})`
2837
3156
  }).from(mindHistory).groupBy(mindHistory.mind);
2838
3157
  lastActiveMap = new Map(lastActiveRows.map((r) => [r.mind, r.lastActiveAt]));
2839
3158
  } catch {
@@ -2841,7 +3160,7 @@ ${user.trimEnd()}
2841
3160
  const minds = await Promise.all(
2842
3161
  entries.map(async (entry) => {
2843
3162
  const { status, channels } = await getMindStatus(entry.name, entry.port);
2844
- const hasPages = existsSync8(resolve11(mindDir(entry.name), "home", "pages"));
3163
+ const hasPages = existsSync10(resolve13(mindDir(entry.name), "home", "pages"));
2845
3164
  return {
2846
3165
  ...entry,
2847
3166
  status,
@@ -2856,19 +3175,19 @@ ${user.trimEnd()}
2856
3175
  const entries = readRegistry();
2857
3176
  const pages = [];
2858
3177
  for (const entry of entries) {
2859
- const pagesDir = resolve11(mindDir(entry.name), "home", "pages");
2860
- if (!existsSync8(pagesDir)) continue;
3178
+ const pagesDir = resolve13(mindDir(entry.name), "home", "pages");
3179
+ if (!existsSync10(pagesDir)) continue;
2861
3180
  let items;
2862
3181
  try {
2863
- items = readdirSync3(pagesDir);
3182
+ items = readdirSync4(pagesDir);
2864
3183
  } catch (err) {
2865
3184
  logger_default.warn("Failed to read pages dir", { mind: entry.name, error: err.message });
2866
3185
  continue;
2867
3186
  }
2868
3187
  for (const item of items) {
2869
- const fullPath = resolve11(pagesDir, item);
3188
+ const fullPath = resolve13(pagesDir, item);
2870
3189
  try {
2871
- const s = statSync(fullPath);
3190
+ const s = statSync2(fullPath);
2872
3191
  if (s.isFile()) {
2873
3192
  pages.push({
2874
3193
  mind: entry.name,
@@ -2877,12 +3196,12 @@ ${user.trimEnd()}
2877
3196
  url: `/pages/${entry.name}/${item}`
2878
3197
  });
2879
3198
  } else if (s.isDirectory()) {
2880
- const indexPath = resolve11(fullPath, "index.html");
2881
- if (existsSync8(indexPath)) {
2882
- const indexStat = statSync(indexPath);
3199
+ const indexPath = resolve13(fullPath, "index.html");
3200
+ if (existsSync10(indexPath)) {
3201
+ const indexStat = statSync2(indexPath);
2883
3202
  pages.push({
2884
3203
  mind: entry.name,
2885
- file: join(item, "index.html"),
3204
+ file: join2(item, "index.html"),
2886
3205
  modified: indexStat.mtime.toISOString(),
2887
3206
  url: `/pages/${entry.name}/${item}/`
2888
3207
  });
@@ -2903,7 +3222,7 @@ ${user.trimEnd()}
2903
3222
  const name = c.req.param("name");
2904
3223
  const entry = findMind(name);
2905
3224
  if (!entry) return c.json({ error: "Mind not found" }, 404);
2906
- if (!existsSync8(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
3225
+ if (!existsSync10(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
2907
3226
  const { status, channels } = await getMindStatus(name, entry.port);
2908
3227
  const variants = readVariants(name);
2909
3228
  const manager = getMindManager();
@@ -2918,7 +3237,7 @@ ${user.trimEnd()}
2918
3237
  return { name: v.name, port: v.port, status: variantStatus };
2919
3238
  })
2920
3239
  );
2921
- const hasPages = existsSync8(resolve11(mindDir(name), "home", "pages"));
3240
+ const hasPages = existsSync10(resolve13(mindDir(name), "home", "pages"));
2922
3241
  return c.json({ ...entry, status, channels, variants: variantStatuses, hasPages });
2923
3242
  }).post("/:name/start", requireAdmin, async (c) => {
2924
3243
  const name = c.req.param("name");
@@ -2930,13 +3249,13 @@ ${user.trimEnd()}
2930
3249
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
2931
3250
  } else {
2932
3251
  const dir = mindDir(baseName);
2933
- if (!existsSync8(dir)) return c.json({ error: "Mind directory missing" }, 404);
3252
+ if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
2934
3253
  }
2935
3254
  if (getMindManager().isRunning(name)) {
2936
3255
  return c.json({ error: "Mind already running" }, 409);
2937
3256
  }
2938
3257
  try {
2939
- await startMindFull(name, baseName, variantName);
3258
+ await startMindFull(name);
2940
3259
  return c.json({ ok: true });
2941
3260
  } catch (err) {
2942
3261
  return c.json({ error: err instanceof Error ? err.message : "Failed to start mind" }, 500);
@@ -2951,7 +3270,7 @@ ${user.trimEnd()}
2951
3270
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
2952
3271
  } else {
2953
3272
  const dir = mindDir(baseName);
2954
- if (!existsSync8(dir)) return c.json({ error: "Mind directory missing" }, 404);
3273
+ if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
2955
3274
  }
2956
3275
  let context;
2957
3276
  const contentType = c.req.header("content-type");
@@ -2960,17 +3279,13 @@ ${user.trimEnd()}
2960
3279
  const body = await c.req.json();
2961
3280
  if (body?.context) context = body.context;
2962
3281
  } catch (err) {
2963
- console.error(`[daemon] failed to parse restart context for ${name}:`, err);
3282
+ logger_default.error(`failed to parse restart context for ${name}`, logger_default.errorData(err));
2964
3283
  }
2965
3284
  }
2966
3285
  const manager = getMindManager();
2967
3286
  try {
2968
3287
  if (manager.isRunning(name)) {
2969
- if (!variantName) {
2970
- await getConnectorManager().stopConnectors(baseName);
2971
- getTokenBudget().removeBudget(baseName);
2972
- }
2973
- await manager.stopMind(name);
3288
+ await stopMindFull(name);
2974
3289
  }
2975
3290
  if (context?.type === "merge" && context.name && !variantName) {
2976
3291
  const mergeVariantName = String(context.name);
@@ -2978,11 +3293,11 @@ ${user.trimEnd()}
2978
3293
  if (branchErr) {
2979
3294
  return c.json({ error: `Invalid variant name: ${branchErr}` }, 400);
2980
3295
  }
2981
- console.error(`[daemon] merging variant for ${baseName}: ${mergeVariantName}`);
3296
+ logger_default.error(`merging variant for ${baseName}: ${mergeVariantName}`);
2982
3297
  const variant = findVariant(baseName, mergeVariantName);
2983
3298
  if (variant) {
2984
3299
  const projectRoot = mindDir(baseName);
2985
- if (existsSync8(variant.path)) {
3300
+ if (existsSync10(variant.path)) {
2986
3301
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
2987
3302
  if (status) {
2988
3303
  try {
@@ -2991,9 +3306,9 @@ ${user.trimEnd()}
2991
3306
  cwd: variant.path
2992
3307
  });
2993
3308
  } catch (e) {
2994
- console.error(
2995
- `[daemon] failed to auto-commit variant worktree for ${baseName}:`,
2996
- e
3309
+ logger_default.error(
3310
+ `failed to auto-commit variant worktree for ${baseName}`,
3311
+ logger_default.errorData(e)
2997
3312
  );
2998
3313
  }
2999
3314
  }
@@ -3006,11 +3321,11 @@ ${user.trimEnd()}
3006
3321
  cwd: projectRoot
3007
3322
  });
3008
3323
  } catch (e) {
3009
- console.error(`[daemon] failed to auto-commit main worktree for ${baseName}:`, e);
3324
+ logger_default.error(`failed to auto-commit main worktree for ${baseName}`, logger_default.errorData(e));
3010
3325
  }
3011
3326
  }
3012
3327
  await gitExec(["merge", variant.branch], { cwd: projectRoot });
3013
- if (existsSync8(variant.path)) {
3328
+ if (existsSync10(variant.path)) {
3014
3329
  try {
3015
3330
  await gitExec(["worktree", "remove", "--force", variant.path], {
3016
3331
  cwd: projectRoot
@@ -3027,7 +3342,7 @@ ${user.trimEnd()}
3027
3342
  try {
3028
3343
  await npmInstallAsMind(projectRoot, baseName);
3029
3344
  } catch (e) {
3030
- console.error(`[daemon] npm install failed after merge for ${baseName}:`, e);
3345
+ logger_default.error(`npm install failed after merge for ${baseName}`, logger_default.errorData(e));
3031
3346
  }
3032
3347
  }
3033
3348
  }
@@ -3036,18 +3351,18 @@ ${user.trimEnd()}
3036
3351
  }
3037
3352
  if (context?.type === "sprouted" && !variantName) {
3038
3353
  try {
3039
- const db2 = await getDb();
3040
- const activeConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.mind_name, baseName)).all();
3354
+ const db = await getDb();
3355
+ const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq4(conversations.mind_name, baseName)).all();
3041
3356
  for (const conv of activeConvs) {
3042
3357
  await addMessage(conv.id, "assistant", "system", [
3043
3358
  { type: "text", text: "[seed has sprouted]" }
3044
3359
  ]);
3045
3360
  }
3046
3361
  } catch (err) {
3047
- console.error(`[daemon] failed to inject sprouted message for ${baseName}:`, err);
3362
+ logger_default.error(`failed to inject sprouted message for ${baseName}`, logger_default.errorData(err));
3048
3363
  }
3049
3364
  }
3050
- await startMindFull(name, baseName, variantName);
3365
+ await startMindFull(name);
3051
3366
  return c.json({ ok: true });
3052
3367
  } catch (err) {
3053
3368
  return c.json({ error: err instanceof Error ? err.message : "Failed to restart mind" }, 500);
@@ -3066,12 +3381,7 @@ ${user.trimEnd()}
3066
3381
  return c.json({ error: "Mind is not running" }, 409);
3067
3382
  }
3068
3383
  try {
3069
- if (!variantName) {
3070
- await getConnectorManager().stopConnectors(baseName);
3071
- getScheduler().unloadSchedules(baseName);
3072
- getTokenBudget().removeBudget(baseName);
3073
- }
3074
- await manager.stopMind(name);
3384
+ await stopMindFull(name);
3075
3385
  return c.json({ ok: true });
3076
3386
  } catch (err) {
3077
3387
  return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
@@ -3093,19 +3403,22 @@ ${user.trimEnd()}
3093
3403
  const force = c.req.query("force") === "true";
3094
3404
  const manager = getMindManager();
3095
3405
  if (manager.isRunning(name)) {
3096
- await getConnectorManager().stopConnectors(name);
3097
- getTokenBudget().removeBudget(name);
3098
- await manager.stopMind(name);
3406
+ await stopMindFull(name);
3099
3407
  }
3100
3408
  removeAllVariants(name);
3409
+ try {
3410
+ await removeSharedWorktree(name, dir);
3411
+ } catch (err) {
3412
+ logger_default.warn(`failed to clean up shared worktree for ${name}`, logger_default.errorData(err));
3413
+ }
3101
3414
  removeMind(name);
3102
3415
  await deleteMindUser2(name);
3103
3416
  const state = stateDir(name);
3104
- if (existsSync8(state)) {
3105
- rmSync(state, { recursive: true, force: true });
3417
+ if (existsSync10(state)) {
3418
+ rmSync2(state, { recursive: true, force: true });
3106
3419
  }
3107
- if (force && existsSync8(dir)) {
3108
- rmSync(dir, { recursive: true, force: true });
3420
+ if (force && existsSync10(dir)) {
3421
+ rmSync2(dir, { recursive: true, force: true });
3109
3422
  deleteMindUser(name);
3110
3423
  }
3111
3424
  return c.json({ ok: true });
@@ -3114,17 +3427,17 @@ ${user.trimEnd()}
3114
3427
  const entry = findMind(mindName);
3115
3428
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3116
3429
  const dir = mindDir(mindName);
3117
- if (!existsSync8(dir)) return c.json({ error: "Mind directory missing" }, 404);
3430
+ if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
3118
3431
  let body = {};
3119
3432
  try {
3120
3433
  body = await c.req.json();
3121
3434
  } catch {
3122
3435
  }
3123
- const template = body.template ?? "claude";
3436
+ const template = body.template ?? entry.template ?? "claude";
3124
3437
  const UPGRADE_VARIANT = "upgrade";
3125
3438
  if (body.continue) {
3126
- const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
3127
- if (!existsSync8(worktreeDir2)) {
3439
+ const worktreeDir2 = resolve13(dir, ".variants", UPGRADE_VARIANT);
3440
+ if (!existsSync10(worktreeDir2)) {
3128
3441
  return c.json({ error: "No upgrade in progress" }, 400);
3129
3442
  }
3130
3443
  const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
@@ -3173,9 +3486,9 @@ ${user.trimEnd()}
3173
3486
  try {
3174
3487
  chownMindDir(dir, mindName);
3175
3488
  } catch (chownErr) {
3176
- console.error(
3177
- `[daemon] failed to fix ownership during upgrade cleanup for ${mindName}:`,
3178
- chownErr
3489
+ logger_default.error(
3490
+ `failed to fix ownership during upgrade cleanup for ${mindName}`,
3491
+ logger_default.errorData(chownErr)
3179
3492
  );
3180
3493
  }
3181
3494
  return c.json(
@@ -3184,8 +3497,8 @@ ${user.trimEnd()}
3184
3497
  );
3185
3498
  }
3186
3499
  }
3187
- const worktreeDir = resolve11(dir, ".variants", UPGRADE_VARIANT);
3188
- if (existsSync8(worktreeDir)) {
3500
+ const worktreeDir = resolve13(dir, ".variants", UPGRADE_VARIANT);
3501
+ if (existsSync10(worktreeDir)) {
3189
3502
  return c.json(
3190
3503
  { error: "Upgrade variant already exists. Use continue or delete it first." },
3191
3504
  409
@@ -3196,10 +3509,20 @@ ${user.trimEnd()}
3196
3509
  await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
3197
3510
  } catch {
3198
3511
  }
3512
+ if (!existsSync10(resolve13(dir, "home", "shared"))) {
3513
+ try {
3514
+ await addSharedWorktree(mindName, dir);
3515
+ } catch (err) {
3516
+ logger_default.warn(
3517
+ `failed to add shared worktree during upgrade for ${mindName}`,
3518
+ logger_default.errorData(err)
3519
+ );
3520
+ }
3521
+ }
3199
3522
  await updateTemplateBranch(dir, template, mindName);
3200
- const parentDir = resolve11(dir, ".variants");
3201
- if (!existsSync8(parentDir)) {
3202
- mkdirSync4(parentDir, { recursive: true });
3523
+ const parentDir = resolve13(dir, ".variants");
3524
+ if (!existsSync10(parentDir)) {
3525
+ mkdirSync7(parentDir, { recursive: true });
3203
3526
  }
3204
3527
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
3205
3528
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -3245,9 +3568,9 @@ ${user.trimEnd()}
3245
3568
  try {
3246
3569
  chownMindDir(dir, mindName);
3247
3570
  } catch (chownErr) {
3248
- console.error(
3249
- `[daemon] failed to fix ownership during upgrade cleanup for ${mindName}:`,
3250
- chownErr
3571
+ logger_default.error(
3572
+ `failed to fix ownership during upgrade cleanup for ${mindName}`,
3573
+ logger_default.errorData(chownErr)
3251
3574
  );
3252
3575
  }
3253
3576
  return c.json(
@@ -3260,11 +3583,9 @@ ${user.trimEnd()}
3260
3583
  const [baseName, variantName] = name.split("@", 2);
3261
3584
  const entry = findMind(baseName);
3262
3585
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3263
- let port = entry.port;
3264
3586
  if (variantName) {
3265
3587
  const variant = findVariant(baseName, variantName);
3266
3588
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
3267
- port = variant.port;
3268
3589
  }
3269
3590
  if (!getMindManager().isRunning(name)) {
3270
3591
  return c.json({ error: "Mind is not running" }, 409);
@@ -3274,15 +3595,15 @@ ${user.trimEnd()}
3274
3595
  try {
3275
3596
  parsed = JSON.parse(body);
3276
3597
  } catch (err) {
3277
- console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
3598
+ logger_default.error(`failed to parse message body for ${baseName}`, logger_default.errorData(err));
3278
3599
  }
3279
3600
  const channel = parsed?.channel ?? "unknown";
3280
- const db2 = await getDb();
3601
+ const db = await getDb();
3281
3602
  if (parsed) {
3282
3603
  try {
3283
3604
  const sender2 = parsed.sender ?? null;
3284
3605
  const content = extractTextContent(parsed.content);
3285
- await db2.insert(mindHistory).values({
3606
+ await db.insert(mindHistory).values({
3286
3607
  mind: baseName,
3287
3608
  type: "inbound",
3288
3609
  channel,
@@ -3290,7 +3611,7 @@ ${user.trimEnd()}
3290
3611
  content
3291
3612
  });
3292
3613
  } catch (err) {
3293
- console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
3614
+ logger_default.error(`failed to persist inbound message for ${baseName}`, logger_default.errorData(err));
3294
3615
  }
3295
3616
  }
3296
3617
  const budget = getTokenBudget();
@@ -3304,16 +3625,31 @@ ${user.trimEnd()}
3304
3625
  });
3305
3626
  return c.json({ error: "Token budget exceeded \u2014 message queued for next period" }, 429);
3306
3627
  }
3628
+ if (!parsed) return c.json({ error: "Invalid JSON" }, 400);
3307
3629
  const typingMap = getTypingMap();
3308
- const sender = parsed?.sender ?? "";
3630
+ const sender = parsed.sender ?? "";
3309
3631
  if (sender) typingMap.delete(channel, sender);
3310
3632
  const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
3311
- let forwardBody = body;
3312
- if (parsed && currentlyTyping.length > 0) {
3633
+ if (currentlyTyping.length > 0) {
3313
3634
  parsed.typing = currentlyTyping;
3314
- forwardBody = JSON.stringify(parsed);
3315
3635
  }
3316
- if (budgetStatus === "warning" && parsed) {
3636
+ if (sender && findMind(sender)) {
3637
+ try {
3638
+ const senderDir = mindDir(sender);
3639
+ const senderPrivateKey = getPrivateKey(senderDir);
3640
+ const senderPublicKey = getPublicKey(senderDir);
3641
+ if (senderPrivateKey && senderPublicKey) {
3642
+ const textContent = extractTextContent(parsed.content);
3643
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3644
+ parsed.signature = signMessage(senderPrivateKey, textContent, timestamp);
3645
+ parsed.signatureTimestamp = timestamp;
3646
+ parsed.signerFingerprint = getFingerprint(senderPublicKey);
3647
+ }
3648
+ } catch (err) {
3649
+ logger_default.warn(`failed to sign message from ${sender}`, { error: err.message });
3650
+ }
3651
+ }
3652
+ if (budgetStatus === "warning") {
3317
3653
  const usage = budget.getUsage(baseName);
3318
3654
  const pct = usage?.percentUsed ?? 80;
3319
3655
  const warningText = `
@@ -3324,12 +3660,11 @@ ${user.trimEnd()}
3324
3660
  parsed.content = [...parsed.content, { type: "text", text: warningText }];
3325
3661
  }
3326
3662
  budget.acknowledgeWarning(baseName);
3327
- forwardBody = JSON.stringify(parsed);
3328
3663
  }
3329
3664
  const seedEntry = findMind(baseName);
3330
- if (seedEntry?.stage === "seed" && parsed) {
3665
+ if (seedEntry?.stage === "seed") {
3331
3666
  try {
3332
- const countResult = await db2.select({ count: sql3`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
3667
+ const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
3333
3668
  const msgCount = countResult[0]?.count ?? 0;
3334
3669
  if (msgCount >= 10 && msgCount % 10 === 0) {
3335
3670
  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.]";
@@ -3338,27 +3673,29 @@ ${user.trimEnd()}
3338
3673
  } else if (Array.isArray(parsed.content)) {
3339
3674
  parsed.content = [...parsed.content, { type: "text", text: nudge }];
3340
3675
  }
3341
- forwardBody = JSON.stringify(parsed);
3342
3676
  }
3343
3677
  } catch (err) {
3344
- console.error(`[daemon] failed to check seed message count for ${baseName}:`, err);
3345
- }
3678
+ logger_default.error(`failed to check seed message count for ${baseName}`, logger_default.errorData(err));
3679
+ }
3680
+ }
3681
+ const deliveryPayload = {
3682
+ channel: parsed.channel,
3683
+ sender: parsed.sender ?? null,
3684
+ content: parsed.content,
3685
+ conversationId: parsed.conversationId ?? void 0,
3686
+ typing: parsed.typing,
3687
+ platform: parsed.platform ?? void 0,
3688
+ isDM: parsed.isDM ?? void 0,
3689
+ participants: parsed.participants ?? void 0,
3690
+ participantCount: parsed.participantCount ?? void 0
3691
+ };
3692
+ if (parsed.signature) {
3693
+ deliveryPayload.signature = parsed.signature;
3694
+ deliveryPayload.signatureTimestamp = parsed.signatureTimestamp;
3695
+ deliveryPayload.signerFingerprint = parsed.signerFingerprint;
3346
3696
  }
3347
- typingMap.set(channel, baseName, { persistent: true });
3348
- const conversationId = parsed?.conversationId ?? null;
3349
- if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
3350
- fetch(`http://127.0.0.1:${port}/message`, {
3351
- method: "POST",
3352
- headers: { "Content-Type": "application/json" },
3353
- body: forwardBody
3354
- }).then(async (res) => {
3355
- if (!res.ok) {
3356
- const text2 = await res.text().catch(() => "");
3357
- console.error(`[daemon] mind ${name} responded with ${res.status}: ${text2}`);
3358
- }
3359
- }).catch((err) => {
3360
- console.error(`[daemon] mind ${name} unreachable on port ${port}:`, err);
3361
- typingMap.delete(channel, baseName);
3697
+ getDeliveryManager().routeAndDeliver(name, deliveryPayload).catch((err) => {
3698
+ logger_default.error(`delivery failed for ${name}`, logger_default.errorData(err));
3362
3699
  });
3363
3700
  return c.json({ ok: true });
3364
3701
  }).get("/:name/budget", async (c) => {
@@ -3367,6 +3704,19 @@ ${user.trimEnd()}
3367
3704
  const usage = getTokenBudget().getUsage(baseName);
3368
3705
  if (!usage) return c.json({ error: "No budget configured" }, 404);
3369
3706
  return c.json(usage);
3707
+ }).get("/:name/delivery/pending", async (c) => {
3708
+ const name = c.req.param("name");
3709
+ const [baseName] = name.split("@", 2);
3710
+ try {
3711
+ const pending = await getDeliveryManager().getPending(baseName);
3712
+ return c.json(pending);
3713
+ } catch (err) {
3714
+ if (err instanceof Error && err.message.includes("not initialized")) {
3715
+ return c.json([]);
3716
+ }
3717
+ logger_default.error(`failed to get pending deliveries for ${baseName}`, logger_default.errorData(err));
3718
+ return c.json({ error: "Failed to retrieve pending messages" }, 500);
3719
+ }
3370
3720
  }).post("/:name/events", async (c) => {
3371
3721
  const name = c.req.param("name");
3372
3722
  const [baseName] = name.split("@", 2);
@@ -3379,9 +3729,9 @@ ${user.trimEnd()}
3379
3729
  if (!body.type) {
3380
3730
  return c.json({ error: "type required" }, 400);
3381
3731
  }
3382
- const db2 = await getDb();
3732
+ const db = await getDb();
3383
3733
  try {
3384
- await db2.insert(mindHistory).values({
3734
+ await db.insert(mindHistory).values({
3385
3735
  mind: baseName,
3386
3736
  type: body.type,
3387
3737
  session: body.session ?? null,
@@ -3391,7 +3741,7 @@ ${user.trimEnd()}
3391
3741
  metadata: body.metadata ? JSON.stringify(body.metadata) : null
3392
3742
  });
3393
3743
  } catch (err) {
3394
- console.error(`[daemon] failed to persist event for ${baseName}:`, err);
3744
+ logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
3395
3745
  }
3396
3746
  publish2(baseName, {
3397
3747
  mind: baseName,
@@ -3402,12 +3752,22 @@ ${user.trimEnd()}
3402
3752
  content: body.content,
3403
3753
  metadata: body.metadata
3404
3754
  });
3755
+ if ((body.type === "text" || body.type === "outbound") && body.channel) {
3756
+ getTypingMap().delete(body.channel, baseName);
3757
+ }
3405
3758
  if (body.type === "done") {
3406
3759
  if (body.channel) {
3407
3760
  getTypingMap().delete(body.channel, baseName);
3408
3761
  } else {
3409
3762
  getTypingMap().deleteSender(baseName);
3410
3763
  }
3764
+ try {
3765
+ getDeliveryManager().sessionDone(baseName, body.session);
3766
+ } catch (err) {
3767
+ if (!(err instanceof Error && err.message.includes("not initialized"))) {
3768
+ logger_default.error(`delivery manager sessionDone failed for ${baseName}`, logger_default.errorData(err));
3769
+ }
3770
+ }
3411
3771
  }
3412
3772
  if (body.type === "usage" && body.metadata) {
3413
3773
  const inputTokens = body.metadata.input_tokens ?? 0;
@@ -3465,9 +3825,9 @@ ${user.trimEnd()}
3465
3825
  if (!body.channel || !body.content) {
3466
3826
  return c.json({ error: "channel and content required" }, 400);
3467
3827
  }
3468
- const db2 = await getDb();
3828
+ const db = await getDb();
3469
3829
  try {
3470
- await db2.insert(mindHistory).values({
3830
+ await db.insert(mindHistory).values({
3471
3831
  mind: baseName,
3472
3832
  type: "outbound",
3473
3833
  channel: body.channel,
@@ -3475,25 +3835,25 @@ ${user.trimEnd()}
3475
3835
  content: body.content
3476
3836
  });
3477
3837
  } catch (err) {
3478
- console.error(`[daemon] failed to persist external send for ${baseName}:`, err);
3838
+ logger_default.error(`failed to persist external send for ${baseName}`, logger_default.errorData(err));
3479
3839
  return c.json({ error: "Failed to persist" }, 500);
3480
3840
  }
3481
3841
  return c.json({ ok: true });
3482
3842
  }).get("/:name/history/sessions", async (c) => {
3483
3843
  const name = c.req.param("name");
3484
- const db2 = await getDb();
3485
- const rows = await db2.select({
3844
+ const db = await getDb();
3845
+ const rows = await db.select({
3486
3846
  session: mindHistory.session,
3487
- started_at: sql3`MIN(${mindHistory.created_at})`,
3488
- event_count: sql3`COUNT(*)`,
3489
- message_count: sql3`SUM(CASE WHEN ${mindHistory.type} IN ('inbound','outbound') THEN 1 ELSE 0 END)`,
3490
- tool_count: sql3`SUM(CASE WHEN ${mindHistory.type}='tool_use' THEN 1 ELSE 0 END)`
3491
- }).from(mindHistory).where(and3(eq4(mindHistory.mind, name), sql3`${mindHistory.session} IS NOT NULL`)).groupBy(mindHistory.session).orderBy(sql3`MIN(${mindHistory.created_at}) DESC`);
3847
+ started_at: sql2`MIN(${mindHistory.created_at})`,
3848
+ event_count: sql2`COUNT(*)`,
3849
+ message_count: sql2`SUM(CASE WHEN ${mindHistory.type} IN ('inbound','outbound') THEN 1 ELSE 0 END)`,
3850
+ tool_count: sql2`SUM(CASE WHEN ${mindHistory.type}='tool_use' THEN 1 ELSE 0 END)`
3851
+ }).from(mindHistory).where(and3(eq4(mindHistory.mind, name), sql2`${mindHistory.session} IS NOT NULL`)).groupBy(mindHistory.session).orderBy(sql2`MIN(${mindHistory.created_at}) DESC`);
3492
3852
  return c.json(rows);
3493
3853
  }).get("/:name/history/channels", async (c) => {
3494
3854
  const name = c.req.param("name");
3495
- const db2 = await getDb();
3496
- const rows = await db2.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
3855
+ const db = await getDb();
3856
+ const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
3497
3857
  return c.json(rows.map((r) => r.channel));
3498
3858
  }).get("/:name/history", async (c) => {
3499
3859
  const name = c.req.param("name");
@@ -3502,7 +3862,7 @@ ${user.trimEnd()}
3502
3862
  const full = c.req.query("full") === "true";
3503
3863
  const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
3504
3864
  const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
3505
- const db2 = await getDb();
3865
+ const db = await getDb();
3506
3866
  const conditions = [eq4(mindHistory.mind, name)];
3507
3867
  if (channel) {
3508
3868
  conditions.push(eq4(mindHistory.channel, channel));
@@ -3511,17 +3871,17 @@ ${user.trimEnd()}
3511
3871
  conditions.push(eq4(mindHistory.session, session));
3512
3872
  }
3513
3873
  if (!full) {
3514
- conditions.push(sql3`${mindHistory.type} IN ('inbound', 'outbound')`);
3874
+ conditions.push(sql2`${mindHistory.type} IN ('inbound', 'outbound')`);
3515
3875
  }
3516
- const rows = await db2.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
3876
+ const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
3517
3877
  return c.json(rows);
3518
3878
  });
3519
- var minds_default = app7;
3879
+ var minds_default = app9;
3520
3880
 
3521
3881
  // src/web/api/pages.ts
3522
3882
  import { readFile as readFile2, stat } from "fs/promises";
3523
- import { extname, resolve as resolve12 } from "path";
3524
- import { Hono as Hono8 } from "hono";
3883
+ import { extname, resolve as resolve14 } from "path";
3884
+ import { Hono as Hono10 } from "hono";
3525
3885
  var MIME_TYPES = {
3526
3886
  ".html": "text/html",
3527
3887
  ".js": "application/javascript",
@@ -3538,16 +3898,16 @@ var MIME_TYPES = {
3538
3898
  ".txt": "text/plain",
3539
3899
  ".xml": "application/xml"
3540
3900
  };
3541
- var app8 = new Hono8().get("/:name/*", async (c) => {
3901
+ var app10 = new Hono10().get("/:name/*", async (c) => {
3542
3902
  const name = c.req.param("name");
3543
3903
  if (!findMind(name)) return c.text("Not found", 404);
3544
- const pagesRoot = resolve12(mindDir(name), "home", "pages");
3904
+ const pagesRoot = resolve14(mindDir(name), "home", "pages");
3545
3905
  const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
3546
- const requestedPath = resolve12(pagesRoot, wildcard.slice(1));
3906
+ const requestedPath = resolve14(pagesRoot, wildcard.slice(1));
3547
3907
  if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
3548
3908
  let fileStat = await stat(requestedPath).catch(() => null);
3549
3909
  if (fileStat?.isDirectory()) {
3550
- const indexPath = resolve12(requestedPath, "index.html");
3910
+ const indexPath = resolve14(requestedPath, "index.html");
3551
3911
  fileStat = await stat(indexPath).catch(() => null);
3552
3912
  if (fileStat?.isFile()) {
3553
3913
  const body = await readFile2(indexPath);
@@ -3563,10 +3923,61 @@ var app8 = new Hono8().get("/:name/*", async (c) => {
3563
3923
  }
3564
3924
  return c.text("Not found", 404);
3565
3925
  });
3566
- var pages_default = app8;
3926
+ var pages_default = app10;
3927
+
3928
+ // src/web/api/prompts.ts
3929
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
3930
+ import { eq as eq5, sql as sql3 } from "drizzle-orm";
3931
+ import { Hono as Hono11 } from "hono";
3932
+ import { z as z4 } from "zod";
3933
+ var app11 = new Hono11().get("/", async (c) => {
3934
+ let rows;
3935
+ try {
3936
+ const db = await getDb();
3937
+ rows = await db.select().from(systemPrompts).all();
3938
+ } catch (err) {
3939
+ console.error("[prompts] failed to query system_prompts:", err);
3940
+ return c.json({ error: "Failed to load prompts from database" }, 500);
3941
+ }
3942
+ const customMap = new Map(rows.map((r) => [r.key, r.content]));
3943
+ const prompts = PROMPT_KEYS.map((key) => {
3944
+ const meta = PROMPT_DEFAULTS[key];
3945
+ const custom = customMap.get(key);
3946
+ return {
3947
+ key,
3948
+ content: custom ?? meta.content,
3949
+ description: meta.description,
3950
+ variables: meta.variables,
3951
+ isCustom: custom !== void 0,
3952
+ category: meta.category
3953
+ };
3954
+ });
3955
+ return c.json(prompts);
3956
+ }).put("/:key", requireAdmin, zValidator4("json", z4.object({ content: z4.string() })), async (c) => {
3957
+ const key = c.req.param("key");
3958
+ if (!PROMPT_KEYS.includes(key)) {
3959
+ return c.json({ error: "Unknown prompt key" }, 404);
3960
+ }
3961
+ const { content } = c.req.valid("json");
3962
+ const db = await getDb();
3963
+ await db.insert(systemPrompts).values({ key, content, updated_at: sql3`(datetime('now'))` }).onConflictDoUpdate({
3964
+ target: systemPrompts.key,
3965
+ set: { content, updated_at: sql3`(datetime('now'))` }
3966
+ });
3967
+ return c.json({ ok: true });
3968
+ }).delete("/:key", requireAdmin, async (c) => {
3969
+ const key = c.req.param("key");
3970
+ if (!PROMPT_KEYS.includes(key)) {
3971
+ return c.json({ error: "Unknown prompt key" }, 404);
3972
+ }
3973
+ const db = await getDb();
3974
+ await db.delete(systemPrompts).where(eq5(systemPrompts.key, key));
3975
+ return c.json({ ok: true });
3976
+ });
3977
+ var prompts_default = app11;
3567
3978
 
3568
3979
  // src/web/api/schedules.ts
3569
- import { Hono as Hono9 } from "hono";
3980
+ import { Hono as Hono12 } from "hono";
3570
3981
  function readSchedules(name) {
3571
3982
  return readVoluteConfig(mindDir(name))?.schedules ?? [];
3572
3983
  }
@@ -3577,7 +3988,7 @@ function writeSchedules(name, schedules) {
3577
3988
  writeVoluteConfig(dir, config);
3578
3989
  getScheduler().loadSchedules(name);
3579
3990
  }
3580
- var app9 = new Hono9().get("/:name/schedules", (c) => {
3991
+ var app12 = new Hono12().get("/:name/schedules", (c) => {
3581
3992
  const name = c.req.param("name");
3582
3993
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
3583
3994
  return c.json(readSchedules(name));
@@ -3648,12 +4059,138 @@ var app9 = new Hono9().get("/:name/schedules", (c) => {
3648
4059
  return c.json({ error: "Failed to reach mind" }, 502);
3649
4060
  }
3650
4061
  });
3651
- var schedules_default = app9;
4062
+ var schedules_default = app12;
4063
+
4064
+ // src/web/api/shared.ts
4065
+ import { Hono as Hono13 } from "hono";
4066
+ var app13 = new Hono13().post("/:name/shared/merge", requireAdmin, async (c) => {
4067
+ const name = c.req.param("name");
4068
+ const entry = findMind(name);
4069
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4070
+ let body;
4071
+ try {
4072
+ body = await c.req.json();
4073
+ } catch {
4074
+ return c.json({ error: "Invalid JSON in request body" }, 400);
4075
+ }
4076
+ const message = body.message || `shared: merge from ${name}`;
4077
+ try {
4078
+ const result = await sharedMerge(name, mindDir(name), message);
4079
+ return c.json(result);
4080
+ } catch (err) {
4081
+ return c.json({ error: err instanceof Error ? err.message : "Merge failed" }, 500);
4082
+ }
4083
+ }).post("/:name/shared/pull", requireAdmin, async (c) => {
4084
+ const name = c.req.param("name");
4085
+ const entry = findMind(name);
4086
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4087
+ try {
4088
+ const result = await sharedPull(name, mindDir(name));
4089
+ return c.json(result);
4090
+ } catch (err) {
4091
+ return c.json({ error: err instanceof Error ? err.message : "Pull failed" }, 500);
4092
+ }
4093
+ }).get("/:name/shared/log", async (c) => {
4094
+ const name = c.req.param("name");
4095
+ const entry = findMind(name);
4096
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4097
+ const limit = parseInt(c.req.query("limit") ?? "20", 10) || 20;
4098
+ try {
4099
+ const log2 = await sharedLog(limit);
4100
+ return c.text(log2);
4101
+ } catch (err) {
4102
+ return c.json({ error: err instanceof Error ? err.message : "Failed to read log" }, 500);
4103
+ }
4104
+ }).get("/:name/shared/status", async (c) => {
4105
+ const name = c.req.param("name");
4106
+ const entry = findMind(name);
4107
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4108
+ try {
4109
+ const status = await sharedStatus(name);
4110
+ return c.text(status);
4111
+ } catch (err) {
4112
+ return c.json({ error: err instanceof Error ? err.message : "Failed to get status" }, 500);
4113
+ }
4114
+ });
4115
+ var shared_default = app13;
4116
+
4117
+ // src/web/api/skills.ts
4118
+ import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
4119
+ import { tmpdir as tmpdir2 } from "os";
4120
+ import { join as join3, resolve as resolve15 } from "path";
4121
+ import AdmZip from "adm-zip";
4122
+ import { Hono as Hono14 } from "hono";
4123
+ var app14 = new Hono14().get("/", async (c) => {
4124
+ const skills = await listSharedSkills();
4125
+ return c.json(skills);
4126
+ }).get("/:id", async (c) => {
4127
+ const id = c.req.param("id");
4128
+ const skill = await getSharedSkill(id);
4129
+ if (!skill) return c.json({ error: "Skill not found" }, 404);
4130
+ const dir = join3(sharedSkillsDir(), id);
4131
+ const files = listFilesRecursive(dir);
4132
+ return c.json({ ...skill, files });
4133
+ }).post("/upload", requireAdmin, async (c) => {
4134
+ const body = await c.req.parseBody();
4135
+ const file = body.file;
4136
+ if (!file || !(file instanceof File)) {
4137
+ return c.json({ error: "No file uploaded" }, 400);
4138
+ }
4139
+ if (!file.name.endsWith(".zip")) {
4140
+ return c.json({ error: "Only .zip files are accepted" }, 400);
4141
+ }
4142
+ const buffer = Buffer.from(await file.arrayBuffer());
4143
+ const tmpDir = mkdtempSync(join3(tmpdir2(), "volute-skill-upload-"));
4144
+ try {
4145
+ const zip = new AdmZip(buffer);
4146
+ for (const entry of zip.getEntries()) {
4147
+ const target = resolve15(tmpDir, entry.entryName);
4148
+ if (!target.startsWith(tmpDir)) {
4149
+ return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
4150
+ }
4151
+ }
4152
+ zip.extractAllTo(tmpDir, true);
4153
+ let skillDir = null;
4154
+ if (existsSync11(join3(tmpDir, "SKILL.md"))) {
4155
+ skillDir = tmpDir;
4156
+ } else {
4157
+ const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4158
+ for (const entry of entries) {
4159
+ if (existsSync11(join3(tmpDir, entry.name, "SKILL.md"))) {
4160
+ skillDir = join3(tmpDir, entry.name);
4161
+ break;
4162
+ }
4163
+ }
4164
+ }
4165
+ if (!skillDir) {
4166
+ return c.json({ error: "No SKILL.md found in zip (checked root and one level deep)" }, 400);
4167
+ }
4168
+ const skill = await importSkillFromDir(skillDir, "upload");
4169
+ return c.json(skill);
4170
+ } catch (e) {
4171
+ if (e instanceof Error && e.message.includes("Invalid skill ID")) {
4172
+ return c.json({ error: e.message }, 400);
4173
+ }
4174
+ throw e;
4175
+ } finally {
4176
+ rmSync3(tmpDir, { recursive: true, force: true });
4177
+ }
4178
+ }).delete("/:id", requireAdmin, async (c) => {
4179
+ const id = c.req.param("id");
4180
+ try {
4181
+ await removeSharedSkill(id);
4182
+ } catch (e) {
4183
+ const msg = e instanceof Error ? e.message : String(e);
4184
+ return c.json({ error: msg }, 404);
4185
+ }
4186
+ return c.json({ ok: true });
4187
+ });
4188
+ var skills_default = app14;
3652
4189
 
3653
4190
  // src/web/api/system.ts
3654
- import { Hono as Hono10 } from "hono";
4191
+ import { Hono as Hono15 } from "hono";
3655
4192
  import { streamSSE as streamSSE2 } from "hono/streaming";
3656
- var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
4193
+ var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
3657
4194
  setTimeout(() => process.exit(1), 200);
3658
4195
  return c.json({ ok: true });
3659
4196
  }).post("/stop", requireAdmin, (c) => {
@@ -3670,10 +4207,10 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
3670
4207
  stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
3671
4208
  });
3672
4209
  });
3673
- await new Promise((resolve18) => {
4210
+ await new Promise((resolve20) => {
3674
4211
  stream.onAbort(() => {
3675
4212
  unsubscribe();
3676
- resolve18();
4213
+ resolve20();
3677
4214
  });
3678
4215
  });
3679
4216
  });
@@ -3681,18 +4218,18 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
3681
4218
  const config = readSystemsConfig();
3682
4219
  return c.json({ system: config?.system ?? null });
3683
4220
  });
3684
- var system_default = app10;
4221
+ var system_default = app15;
3685
4222
 
3686
4223
  // src/web/api/typing.ts
3687
- import { zValidator as zValidator3 } from "@hono/zod-validator";
3688
- import { Hono as Hono11 } from "hono";
3689
- import { z as z3 } from "zod";
3690
- var typingSchema = z3.object({
3691
- channel: z3.string().min(1),
3692
- sender: z3.string().min(1),
3693
- active: z3.boolean()
4224
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
4225
+ import { Hono as Hono16 } from "hono";
4226
+ import { z as z5 } from "zod";
4227
+ var typingSchema = z5.object({
4228
+ channel: z5.string().min(1),
4229
+ sender: z5.string().min(1),
4230
+ active: z5.boolean()
3694
4231
  });
3695
- var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
4232
+ var app16 = new Hono16().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
3696
4233
  const { channel, sender, active } = c.req.valid("json");
3697
4234
  const map = getTypingMap();
3698
4235
  if (active) {
@@ -3709,13 +4246,13 @@ var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema)
3709
4246
  const map = getTypingMap();
3710
4247
  return c.json({ typing: map.get(channel) });
3711
4248
  });
3712
- var typing_default = app11;
4249
+ var typing_default = app16;
3713
4250
 
3714
4251
  // src/web/api/update.ts
3715
4252
  import { spawn as spawn3 } from "child_process";
3716
- import { Hono as Hono12 } from "hono";
4253
+ import { Hono as Hono17 } from "hono";
3717
4254
  var bin;
3718
- var app12 = new Hono12().get("/update", async (c) => {
4255
+ var app17 = new Hono17().get("/update", async (c) => {
3719
4256
  const result = await checkForUpdate();
3720
4257
  return c.json(result);
3721
4258
  }).post("/update", requireAdmin, async (c) => {
@@ -3730,19 +4267,19 @@ var app12 = new Hono12().get("/update", async (c) => {
3730
4267
  child.unref();
3731
4268
  return c.json({ ok: true, message: "Updating..." });
3732
4269
  });
3733
- var update_default = app12;
4270
+ var update_default = app17;
3734
4271
 
3735
4272
  // src/web/api/variants.ts
3736
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
3737
- import { resolve as resolve14 } from "path";
3738
- import { Hono as Hono13 } from "hono";
4273
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
4274
+ import { resolve as resolve17 } from "path";
4275
+ import { Hono as Hono18 } from "hono";
3739
4276
 
3740
4277
  // src/lib/spawn-server.ts
3741
4278
  import { spawn as spawn4 } from "child_process";
3742
- import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync7 } from "fs";
3743
- import { resolve as resolve13 } from "path";
4279
+ import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync10 } from "fs";
4280
+ import { resolve as resolve16 } from "path";
3744
4281
  function tsxBin(cwd) {
3745
- return resolve13(cwd, "node_modules", ".bin", "tsx");
4282
+ return resolve16(cwd, "node_modules", ".bin", "tsx");
3746
4283
  }
3747
4284
  function spawnServer(cwd, port, options) {
3748
4285
  if (options?.detached) {
@@ -3755,31 +4292,31 @@ function spawnAttached(cwd, port) {
3755
4292
  cwd,
3756
4293
  stdio: ["ignore", "pipe", "pipe"]
3757
4294
  });
3758
- return new Promise((resolve18) => {
3759
- const timeout = setTimeout(() => resolve18(null), 3e4);
4295
+ return new Promise((resolve20) => {
4296
+ const timeout = setTimeout(() => resolve20(null), 3e4);
3760
4297
  function checkOutput(data) {
3761
4298
  const match = data.toString().match(/listening on :(\d+)/);
3762
4299
  if (match) {
3763
4300
  clearTimeout(timeout);
3764
- resolve18({ child, actualPort: parseInt(match[1], 10) });
4301
+ resolve20({ child, actualPort: parseInt(match[1], 10) });
3765
4302
  }
3766
4303
  }
3767
4304
  child.stdout?.on("data", checkOutput);
3768
4305
  child.stderr?.on("data", checkOutput);
3769
4306
  child.on("error", () => {
3770
4307
  clearTimeout(timeout);
3771
- resolve18(null);
4308
+ resolve20(null);
3772
4309
  });
3773
4310
  child.on("exit", () => {
3774
4311
  clearTimeout(timeout);
3775
- resolve18(null);
4312
+ resolve20(null);
3776
4313
  });
3777
4314
  });
3778
4315
  }
3779
4316
  function spawnDetached(cwd, port, logDir) {
3780
- const logsDir = logDir ?? resolve13(cwd, ".volute", "logs");
3781
- mkdirSync5(logsDir, { recursive: true });
3782
- const logPath = resolve13(logsDir, "mind.log");
4317
+ const logsDir = logDir ?? resolve16(cwd, ".mind", "logs");
4318
+ mkdirSync8(logsDir, { recursive: true });
4319
+ const logPath = resolve16(logsDir, "mind.log");
3783
4320
  const logFd = openSync(logPath, "a");
3784
4321
  const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
3785
4322
  cwd,
@@ -3799,7 +4336,7 @@ function spawnDetached(cwd, port, logDir) {
3799
4336
  }
3800
4337
  const interval = setInterval(() => {
3801
4338
  try {
3802
- const content = readFileSync7(logPath, "utf-8");
4339
+ const content = readFileSync10(logPath, "utf-8");
3803
4340
  const match = content.match(/listening on :(\d+)/);
3804
4341
  if (match) {
3805
4342
  finish({ child, actualPort: parseInt(match[1], 10) });
@@ -3814,7 +4351,7 @@ function spawnDetached(cwd, port, logDir) {
3814
4351
  }
3815
4352
 
3816
4353
  // src/lib/verify.ts
3817
- async function verify(port) {
4354
+ async function verify2(port) {
3818
4355
  const health = await checkHealth(port);
3819
4356
  if (!health.ok) {
3820
4357
  console.error(" Health check: failed");
@@ -3851,7 +4388,7 @@ async function verify(port) {
3851
4388
  }
3852
4389
 
3853
4390
  // src/web/api/variants.ts
3854
- var app13 = new Hono13().get("/:name/variants", async (c) => {
4391
+ var app18 = new Hono18().get("/:name/variants", async (c) => {
3855
4392
  const name = c.req.param("name");
3856
4393
  const entry = findMind(name);
3857
4394
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -3881,11 +4418,11 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3881
4418
  const err = validateBranchName(variantName);
3882
4419
  if (err) return c.json({ error: err }, 400);
3883
4420
  const projectRoot = mindDir(mindName);
3884
- const variantDir = resolve14(projectRoot, ".variants", variantName);
3885
- if (existsSync9(variantDir)) {
4421
+ const variantDir = resolve17(projectRoot, ".variants", variantName);
4422
+ if (existsSync12(variantDir)) {
3886
4423
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
3887
4424
  }
3888
- mkdirSync6(resolve14(projectRoot, ".variants"), { recursive: true });
4425
+ mkdirSync9(resolve17(projectRoot, ".variants"), { recursive: true });
3889
4426
  try {
3890
4427
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
3891
4428
  } catch (e) {
@@ -3898,7 +4435,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3898
4435
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
3899
4436
  await exec(cmd, args, {
3900
4437
  cwd: variantDir,
3901
- env: { ...process.env, HOME: resolve14(variantDir, "home") }
4438
+ env: { ...process.env, HOME: resolve17(variantDir, "home") }
3902
4439
  });
3903
4440
  } else {
3904
4441
  await exec("npm", ["install"], { cwd: variantDir });
@@ -3908,7 +4445,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3908
4445
  return c.json({ error: `npm install failed: ${msg}` }, 500);
3909
4446
  }
3910
4447
  if (body.soul) {
3911
- writeFileSync6(resolve14(variantDir, "home/SOUL.md"), body.soul);
4448
+ writeFileSync9(resolve17(variantDir, "home/SOUL.md"), body.soul);
3912
4449
  }
3913
4450
  const variantPort = body.port ?? nextPort();
3914
4451
  const variant = {
@@ -3946,7 +4483,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3946
4483
  } catch {
3947
4484
  }
3948
4485
  const projectRoot = mindDir(mindName);
3949
- if (existsSync9(variant.path)) {
4486
+ if (existsSync12(variant.path)) {
3950
4487
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
3951
4488
  if (status) {
3952
4489
  try {
@@ -3972,7 +4509,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3972
4509
  500
3973
4510
  );
3974
4511
  }
3975
- const verified = await verify(result.actualPort);
4512
+ const verified = await verify2(result.actualPort);
3976
4513
  try {
3977
4514
  process.kill(result.child.pid);
3978
4515
  } catch {
@@ -4003,7 +4540,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4003
4540
  } catch (e) {
4004
4541
  return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
4005
4542
  }
4006
- if (existsSync9(variant.path)) {
4543
+ if (existsSync12(variant.path)) {
4007
4544
  try {
4008
4545
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4009
4546
  } catch {
@@ -4020,7 +4557,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4020
4557
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4021
4558
  await exec(cmd, args, {
4022
4559
  cwd: projectRoot,
4023
- env: { ...process.env, HOME: resolve14(projectRoot, "home") }
4560
+ env: { ...process.env, HOME: resolve17(projectRoot, "home") }
4024
4561
  });
4025
4562
  } else {
4026
4563
  await exec("npm", ["install"], { cwd: projectRoot });
@@ -4063,7 +4600,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4063
4600
  } catch {
4064
4601
  }
4065
4602
  }
4066
- if (existsSync9(variant.path)) {
4603
+ if (existsSync12(variant.path)) {
4067
4604
  try {
4068
4605
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4069
4606
  } catch {
@@ -4077,45 +4614,153 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4077
4614
  chownMindDir(projectRoot, mindName);
4078
4615
  return c.json({ ok: true });
4079
4616
  });
4080
- var variants_default = app13;
4617
+ var variants_default = app18;
4081
4618
 
4082
- // src/web/api/volute/chat.ts
4083
- import { readFileSync as readFileSync8 } from "fs";
4084
- import { resolve as resolve15 } from "path";
4085
- import { zValidator as zValidator4 } from "@hono/zod-validator";
4086
- import { Hono as Hono14 } from "hono";
4087
- import { streamSSE as streamSSE3 } from "hono/streaming";
4088
- import { z as z4 } from "zod";
4089
- var chatSchema = z4.object({
4090
- message: z4.string().optional(),
4091
- conversationId: z4.string().optional(),
4092
- sender: z4.string().optional(),
4093
- images: z4.array(
4094
- z4.object({
4095
- media_type: z4.string(),
4096
- data: z4.string()
4097
- })
4098
- ).optional()
4619
+ // src/web/api/volute/channels.ts
4620
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
4621
+ import { Hono as Hono19 } from "hono";
4622
+ import { z as z6 } from "zod";
4623
+ var createSchema = z6.object({
4624
+ name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
4625
+ });
4626
+ var inviteSchema = z6.object({
4627
+ username: z6.string().min(1)
4099
4628
  });
4100
- function getDaemonUrl() {
4629
+ var app19 = new Hono19().get("/", async (c) => {
4630
+ const user = c.get("user");
4631
+ const channels = await listChannels();
4632
+ const results = await Promise.all(
4633
+ channels.map(async (ch) => {
4634
+ const participants = await getParticipants(ch.id);
4635
+ const isMember = participants.some((p) => p.userId === user.id);
4636
+ return { ...ch, participantCount: participants.length, isMember };
4637
+ })
4638
+ );
4639
+ return c.json(results);
4640
+ }).post("/", zValidator6("json", createSchema), async (c) => {
4641
+ const user = c.get("user");
4642
+ const body = c.req.valid("json");
4101
4643
  try {
4102
- const data = JSON.parse(readFileSync8(resolve15(voluteHome(), "daemon.json"), "utf-8"));
4103
- return `http://${daemonLoopback()}:${data.port}`;
4644
+ const ch = await createChannel(body.name, user.id);
4645
+ return c.json(ch, 201);
4104
4646
  } catch (err) {
4105
- throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
4647
+ const cause = err instanceof Error ? err.cause : null;
4648
+ if (cause && /UNIQUE/i.test(cause.extendedCode ?? cause.message ?? "")) {
4649
+ return c.json({ error: "Channel already exists" }, 409);
4650
+ }
4651
+ throw err;
4106
4652
  }
4107
- }
4108
- function daemonFetchInternal(path, body) {
4109
- const daemonUrl = getDaemonUrl();
4110
- const token = process.env.VOLUTE_DAEMON_TOKEN;
4111
- const headers = {
4112
- "Content-Type": "application/json",
4113
- Origin: daemonUrl
4653
+ }).post("/:name/join", async (c) => {
4654
+ const name = c.req.param("name");
4655
+ const user = c.get("user");
4656
+ const ch = await getChannelByName(name);
4657
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4658
+ await joinChannel(ch.id, user.id);
4659
+ return c.json({ ok: true, conversationId: ch.id });
4660
+ }).post("/:name/leave", async (c) => {
4661
+ const name = c.req.param("name");
4662
+ const user = c.get("user");
4663
+ const ch = await getChannelByName(name);
4664
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4665
+ await leaveChannel(ch.id, user.id);
4666
+ return c.json({ ok: true });
4667
+ }).get("/:name/members", async (c) => {
4668
+ const name = c.req.param("name");
4669
+ const ch = await getChannelByName(name);
4670
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4671
+ const participants = await getParticipants(ch.id);
4672
+ return c.json(participants);
4673
+ }).post("/:name/invite", zValidator6("json", inviteSchema), async (c) => {
4674
+ const name = c.req.param("name");
4675
+ const inviter = c.get("user");
4676
+ const { username } = c.req.valid("json");
4677
+ const ch = await getChannelByName(name);
4678
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4679
+ let user = await getUserByUsername(username);
4680
+ if (!user && findMind(username)) {
4681
+ user = await getOrCreateMindUser(username);
4682
+ }
4683
+ if (!user) return c.json({ error: "User not found" }, 404);
4684
+ if (await isParticipant(ch.id, user.id)) {
4685
+ return c.json({ error: "Already a member" }, 409);
4686
+ }
4687
+ await joinChannel(ch.id, user.id);
4688
+ await addMessage(ch.id, "system", "system", [
4689
+ { type: "text", text: `${inviter.username} invited ${username} to #${name}` }
4690
+ ]);
4691
+ return c.json({ ok: true });
4692
+ });
4693
+ var channels_default2 = app19;
4694
+
4695
+ // src/web/api/volute/chat.ts
4696
+ import { zValidator as zValidator7 } from "@hono/zod-validator";
4697
+ import { Hono as Hono20 } from "hono";
4698
+ import { streamSSE as streamSSE3 } from "hono/streaming";
4699
+ import { z as z7 } from "zod";
4700
+ async function fanOutToMinds(opts) {
4701
+ const participants = await getParticipants(opts.conversationId);
4702
+ const mindParticipants = participants.filter((p) => p.userType === "mind");
4703
+ const participantNames = participants.map((p) => p.username);
4704
+ const isDM = opts.isDM ?? participants.length === 2;
4705
+ const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
4706
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-RVCFROAY.js");
4707
+ const manager = getMindManager2();
4708
+ const runningMinds = mindParticipants.map((ap) => {
4709
+ const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
4710
+ return manager.isRunning(key) ? ap.username : null;
4711
+ }).filter((n) => n !== null && n !== opts.senderName);
4712
+ function slugForMind(mindUsername) {
4713
+ return buildVoluteSlug({
4714
+ participants,
4715
+ mindUsername,
4716
+ convTitle: opts.convTitle,
4717
+ conversationId: opts.conversationId,
4718
+ ...opts.slugExtra
4719
+ });
4720
+ }
4721
+ const channelEntry = {
4722
+ platformId: opts.conversationId,
4723
+ platform: "volute",
4724
+ name: opts.convTitle ?? void 0,
4725
+ type: channelEntryType
4114
4726
  };
4115
- if (token) headers.Authorization = `Bearer ${token}`;
4116
- return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
4727
+ for (const ap of mindParticipants) {
4728
+ try {
4729
+ writeChannelEntry(ap.username, slugForMind(ap.username), channelEntry);
4730
+ } catch (err) {
4731
+ logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
4732
+ }
4733
+ }
4734
+ for (const mindName of runningMinds) {
4735
+ const target = opts.targetName ? opts.targetName(mindName) : mindName;
4736
+ const channel = slugForMind(mindName);
4737
+ const typingMap = getTypingMap();
4738
+ const currentlyTyping = typingMap.get(channel);
4739
+ deliverMessage(target, {
4740
+ content: opts.contentBlocks,
4741
+ channel,
4742
+ conversationId: opts.conversationId,
4743
+ sender: opts.senderName,
4744
+ participants: participantNames,
4745
+ participantCount: participants.length,
4746
+ isDM,
4747
+ ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
4748
+ }).catch(() => {
4749
+ });
4750
+ }
4117
4751
  }
4118
- var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
4752
+ var chatSchema = z7.object({
4753
+ message: z7.string().optional(),
4754
+ conversationId: z7.string().optional(),
4755
+ sender: z7.string().optional(),
4756
+ images: z7.array(
4757
+ z7.object({
4758
+ media_type: z7.string(),
4759
+ data: z7.string()
4760
+ })
4761
+ ).optional()
4762
+ });
4763
+ var app20 = new Hono20().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
4119
4764
  const name = c.req.param("name");
4120
4765
  const [baseName] = name.split("@", 2);
4121
4766
  const entry = findMind(baseName);
@@ -4151,8 +4796,8 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4151
4796
  }
4152
4797
  }
4153
4798
  if (!conversationId) {
4154
- const participantNames2 = /* @__PURE__ */ new Set([senderName, baseName]);
4155
- const title = [...participantNames2].join(", ");
4799
+ const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
4800
+ const title = [...participantNames].join(", ");
4156
4801
  const conv2 = await createConversation(baseName, "volute", {
4157
4802
  userId: user.id !== 0 ? user.id : void 0,
4158
4803
  title,
@@ -4162,7 +4807,7 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4162
4807
  }
4163
4808
  }
4164
4809
  const conv = await getConversation(conversationId);
4165
- const convTitle = conv?.title;
4810
+ const convTitle = conv?.title ?? null;
4166
4811
  const contentBlocks = [];
4167
4812
  if (body.message) {
4168
4813
  contentBlocks.push({ type: "text", text: body.message });
@@ -4173,61 +4818,13 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4173
4818
  }
4174
4819
  }
4175
4820
  await addMessage(conversationId, "user", senderName, contentBlocks);
4176
- const participants = await getParticipants(conversationId);
4177
- const mindParticipants = participants.filter((p) => p.userType === "mind");
4178
- const participantNames = participants.map((p) => p.username);
4179
- const { getMindManager: getMindManager2 } = await import("./mind-manager-ETNCPQJN.js");
4180
- const manager = getMindManager2();
4181
- const runningMinds = mindParticipants.map((ap) => {
4182
- const mindKey = ap.username === baseName ? name : ap.username;
4183
- return manager.isRunning(mindKey) ? ap.username : null;
4184
- }).filter((n) => n !== null && n !== senderName);
4185
- const isDM = participants.length === 2;
4186
- function channelForMind(mindUsername) {
4187
- return buildVoluteSlug({
4188
- participants,
4189
- mindUsername,
4190
- convTitle,
4191
- conversationId
4192
- });
4193
- }
4194
- const channelEntry = {
4195
- platformId: conversationId,
4196
- platform: "volute",
4197
- name: convTitle ?? void 0,
4198
- type: isDM ? "dm" : "group"
4199
- };
4200
- for (const ap of mindParticipants) {
4201
- try {
4202
- writeChannelEntry(ap.username, channelForMind(ap.username), channelEntry);
4203
- } catch (err) {
4204
- console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
4205
- }
4206
- }
4207
- for (const mindName of runningMinds) {
4208
- const targetName = mindName === baseName ? name : mindName;
4209
- const channel = channelForMind(mindName);
4210
- const typingMap = getTypingMap();
4211
- const currentlyTyping = typingMap.get(channel);
4212
- const payload = JSON.stringify({
4213
- content: contentBlocks,
4214
- channel,
4215
- conversationId,
4216
- sender: senderName,
4217
- participants: participantNames,
4218
- participantCount: participants.length,
4219
- isDM,
4220
- ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
4221
- });
4222
- daemonFetchInternal(`/api/minds/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
4223
- if (!res.ok) {
4224
- const text2 = await res.text().catch(() => "");
4225
- console.error(`[chat] mind ${mindName} responded ${res.status}: ${text2}`);
4226
- }
4227
- }).catch((err) => {
4228
- console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
4229
- });
4230
- }
4821
+ await fanOutToMinds({
4822
+ conversationId,
4823
+ contentBlocks,
4824
+ senderName,
4825
+ convTitle,
4826
+ targetName: (username) => username === baseName ? name : username
4827
+ });
4231
4828
  return c.json({ ok: true, conversationId });
4232
4829
  }).get("/:name/conversations/:id/events", async (c) => {
4233
4830
  const conversationId = c.req.param("id");
@@ -4246,27 +4843,68 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4246
4843
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
4247
4844
  });
4248
4845
  }, 15e3);
4249
- await new Promise((resolve18) => {
4846
+ await new Promise((resolve20) => {
4250
4847
  stream.onAbort(() => {
4251
4848
  unsubscribe();
4252
4849
  clearInterval(keepAlive);
4253
- resolve18();
4850
+ resolve20();
4254
4851
  });
4255
4852
  });
4256
4853
  });
4257
4854
  });
4258
- var chat_default = app14;
4855
+ var unifiedChatSchema = z7.object({
4856
+ message: z7.string().optional(),
4857
+ conversationId: z7.string(),
4858
+ images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
4859
+ });
4860
+ var unifiedChatApp = new Hono20().post(
4861
+ "/chat",
4862
+ zValidator7("json", unifiedChatSchema),
4863
+ async (c) => {
4864
+ const user = c.get("user");
4865
+ const body = c.req.valid("json");
4866
+ if (!body.message && (!body.images || body.images.length === 0)) {
4867
+ return c.json({ error: "message or images required" }, 400);
4868
+ }
4869
+ const conv = await getConversation(body.conversationId);
4870
+ if (!conv) return c.json({ error: "Conversation not found" }, 404);
4871
+ if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
4872
+ return c.json({ error: "Conversation not found" }, 404);
4873
+ }
4874
+ const senderName = user.username;
4875
+ const contentBlocks = [];
4876
+ if (body.message) contentBlocks.push({ type: "text", text: body.message });
4877
+ if (body.images) {
4878
+ for (const img of body.images) {
4879
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
4880
+ }
4881
+ }
4882
+ await addMessage(body.conversationId, "user", senderName, contentBlocks);
4883
+ const isDM = conv.type === "dm";
4884
+ await fanOutToMinds({
4885
+ conversationId: body.conversationId,
4886
+ contentBlocks,
4887
+ senderName,
4888
+ convTitle: conv.title,
4889
+ isDM,
4890
+ channelEntryType: conv.type === "channel" ? "group" : isDM ? "dm" : "group",
4891
+ slugExtra: { convType: conv.type, convName: conv.name }
4892
+ });
4893
+ return c.json({ ok: true, conversationId: body.conversationId });
4894
+ }
4895
+ );
4896
+ var chat_default = app20;
4259
4897
 
4260
4898
  // src/web/api/volute/conversations.ts
4261
- import { zValidator as zValidator5 } from "@hono/zod-validator";
4262
- import { Hono as Hono15 } from "hono";
4263
- import { z as z5 } from "zod";
4264
- var createConvSchema = z5.object({
4265
- title: z5.string().optional(),
4266
- participantIds: z5.array(z5.number()).optional(),
4267
- participantNames: z5.array(z5.string()).optional()
4899
+ import { zValidator as zValidator8 } from "@hono/zod-validator";
4900
+ import { Hono as Hono21 } from "hono";
4901
+ import { z as z8 } from "zod";
4902
+ var createConvSchema = z8.object({
4903
+ title: z8.string().optional(),
4904
+ participantIds: z8.array(z8.number()).optional(),
4905
+ participantNames: z8.array(z8.string()).optional()
4268
4906
  });
4269
- var app15 = new Hono15().get("/:name/conversations", async (c) => {
4907
+ var app21 = new Hono21().get("/:name/conversations", async (c) => {
4270
4908
  const name = c.req.param("name");
4271
4909
  const user = c.get("user");
4272
4910
  let lookupId = user.id;
@@ -4275,9 +4913,9 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
4275
4913
  lookupId = mindUser.id;
4276
4914
  }
4277
4915
  const all = await listConversationsForUser(lookupId);
4278
- const convs = all.filter((c2) => c2.mind_name === name);
4916
+ const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
4279
4917
  return c.json(convs);
4280
- }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
4918
+ }).post("/:name/conversations", zValidator8("json", createConvSchema), async (c) => {
4281
4919
  const name = c.req.param("name");
4282
4920
  const user = c.get("user");
4283
4921
  const body = c.req.valid("json");
@@ -4351,17 +4989,18 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
4351
4989
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4352
4990
  return c.json({ ok: true });
4353
4991
  });
4354
- var conversations_default = app15;
4992
+ var conversations_default = app21;
4355
4993
 
4356
4994
  // src/web/api/volute/user-conversations.ts
4357
- import { zValidator as zValidator6 } from "@hono/zod-validator";
4358
- import { Hono as Hono16 } from "hono";
4359
- import { z as z6 } from "zod";
4360
- var createSchema = z6.object({
4361
- title: z6.string().optional(),
4362
- participantNames: z6.array(z6.string()).min(1)
4995
+ import { zValidator as zValidator9 } from "@hono/zod-validator";
4996
+ import { Hono as Hono22 } from "hono";
4997
+ import { streamSSE as streamSSE4 } from "hono/streaming";
4998
+ import { z as z9 } from "zod";
4999
+ var createSchema2 = z9.object({
5000
+ title: z9.string().optional(),
5001
+ participantNames: z9.array(z9.string()).min(1)
4363
5002
  });
4364
- var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
5003
+ var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
4365
5004
  const user = c.get("user");
4366
5005
  const convs = await listConversationsWithParticipants(user.id);
4367
5006
  return c.json(convs);
@@ -4373,7 +5012,7 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4373
5012
  }
4374
5013
  const msgs = await getMessages(id);
4375
5014
  return c.json(msgs);
4376
- }).post("/", zValidator6("json", createSchema), async (c) => {
5015
+ }).post("/", zValidator9("json", createSchema2), async (c) => {
4377
5016
  const user = c.get("user");
4378
5017
  const body = c.req.valid("json");
4379
5018
  const participantIds = /* @__PURE__ */ new Set();
@@ -4403,6 +5042,31 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4403
5042
  participantIds: [...participantIds]
4404
5043
  });
4405
5044
  return c.json(conv, 201);
5045
+ }).get("/:id/events", async (c) => {
5046
+ const conversationId = c.req.param("id");
5047
+ const user = c.get("user");
5048
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
5049
+ return c.json({ error: "Conversation not found" }, 404);
5050
+ }
5051
+ return streamSSE4(c, async (stream) => {
5052
+ const unsubscribe = subscribe(conversationId, (event) => {
5053
+ stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
5054
+ if (!stream.aborted) console.error("[chat] SSE write error:", err);
5055
+ });
5056
+ });
5057
+ const keepAlive = setInterval(() => {
5058
+ stream.writeSSE({ data: "" }).catch((err) => {
5059
+ if (!stream.aborted) console.error("[chat] SSE ping error:", err);
5060
+ });
5061
+ }, 15e3);
5062
+ await new Promise((resolve20) => {
5063
+ stream.onAbort(() => {
5064
+ unsubscribe();
5065
+ clearInterval(keepAlive);
5066
+ resolve20();
5067
+ });
5068
+ });
5069
+ });
4406
5070
  }).delete("/:id", async (c) => {
4407
5071
  const id = c.req.param("id");
4408
5072
  const user = c.get("user");
@@ -4410,12 +5074,12 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4410
5074
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4411
5075
  return c.json({ ok: true });
4412
5076
  });
4413
- var user_conversations_default = app16;
5077
+ var user_conversations_default = app22;
4414
5078
 
4415
5079
  // src/web/app.ts
4416
5080
  var httpLog = logger_default.child("http");
4417
- var app17 = new Hono17();
4418
- app17.onError((err, c) => {
5081
+ var app23 = new Hono23();
5082
+ app23.onError((err, c) => {
4419
5083
  if (err instanceof HTTPException) {
4420
5084
  return err.getResponse();
4421
5085
  }
@@ -4426,10 +5090,10 @@ app17.onError((err, c) => {
4426
5090
  });
4427
5091
  return c.json({ error: "Internal server error" }, 500);
4428
5092
  });
4429
- app17.notFound((c) => {
5093
+ app23.notFound((c) => {
4430
5094
  return c.json({ error: "Not found" }, 404);
4431
5095
  });
4432
- app17.use("*", async (c, next) => {
5096
+ app23.use("*", async (c, next) => {
4433
5097
  const start = Date.now();
4434
5098
  await next();
4435
5099
  const duration = Date.now() - start;
@@ -4440,7 +5104,7 @@ app17.use("*", async (c, next) => {
4440
5104
  httpLog.debug("request", data);
4441
5105
  }
4442
5106
  });
4443
- app17.get("/api/health", (c) => {
5107
+ app23.get("/api/health", (c) => {
4444
5108
  let version = "unknown";
4445
5109
  let cached = null;
4446
5110
  try {
@@ -4455,15 +5119,18 @@ app17.get("/api/health", (c) => {
4455
5119
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
4456
5120
  });
4457
5121
  });
4458
- app17.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
4459
- app17.use("/api/*", csrf());
4460
- app17.use("/api/minds/*", authMiddleware);
4461
- app17.use("/api/conversations/*", authMiddleware);
4462
- app17.use("/api/system/*", authMiddleware);
4463
- app17.use("/api/env/*", authMiddleware);
4464
- app17.route("/pages", pages_default);
4465
- var routes = app17.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", env_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/conversations", user_conversations_default);
4466
- var app_default = app17;
5122
+ app23.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
5123
+ app23.use("/api/*", csrf());
5124
+ app23.use("/api/minds/*", authMiddleware);
5125
+ app23.use("/api/conversations/*", authMiddleware);
5126
+ app23.use("/api/volute/*", authMiddleware);
5127
+ app23.use("/api/system/*", authMiddleware);
5128
+ app23.use("/api/env/*", authMiddleware);
5129
+ app23.use("/api/prompts/*", authMiddleware);
5130
+ app23.use("/api/skills/*", authMiddleware);
5131
+ app23.route("/pages", pages_default);
5132
+ var routes = app23.route("/api/keys", keys_default).route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", shared_default).route("/api/minds", env_default).route("/api/minds", mind_skills_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/prompts", prompts_default).route("/api/skills", skills_default).route("/api/conversations", user_conversations_default).route("/api/volute/channels", channels_default2).route("/api/volute", unifiedChatApp);
5133
+ var app_default = app23;
4467
5134
 
4468
5135
  // src/web/server.ts
4469
5136
  var MIME_TYPES2 = {
@@ -4482,8 +5149,8 @@ async function startServer({
4482
5149
  let assetsDir = "";
4483
5150
  let searchDir = dirname3(new URL(import.meta.url).pathname);
4484
5151
  for (let i = 0; i < 5; i++) {
4485
- const candidate = resolve16(searchDir, "dist", "web-assets");
4486
- if (existsSync10(candidate)) {
5152
+ const candidate = resolve18(searchDir, "dist", "web-assets");
5153
+ if (existsSync13(candidate)) {
4487
5154
  assetsDir = candidate;
4488
5155
  break;
4489
5156
  }
@@ -4493,7 +5160,7 @@ async function startServer({
4493
5160
  app_default.get("*", async (c) => {
4494
5161
  const urlPath = new URL(c.req.url).pathname;
4495
5162
  if (urlPath.startsWith("/api/")) return c.notFound();
4496
- const filePath = resolve16(assetsDir, urlPath.slice(1));
5163
+ const filePath = resolve18(assetsDir, urlPath.slice(1));
4497
5164
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
4498
5165
  const s = await stat2(filePath).catch(() => null);
4499
5166
  if (s?.isFile()) {
@@ -4502,7 +5169,7 @@ async function startServer({
4502
5169
  const body = await readFile3(filePath);
4503
5170
  return c.body(body, 200, { "Content-Type": mime });
4504
5171
  }
4505
- const indexPath = resolve16(assetsDir, "index.html");
5172
+ const indexPath = resolve18(assetsDir, "index.html");
4506
5173
  const indexStat = await stat2(indexPath).catch(() => null);
4507
5174
  if (indexStat?.isFile()) {
4508
5175
  const body = await readFile3(indexPath, "utf-8");
@@ -4512,10 +5179,10 @@ async function startServer({
4512
5179
  });
4513
5180
  }
4514
5181
  const server = serve({ fetch: app_default.fetch, port, hostname });
4515
- await new Promise((resolve18, reject) => {
5182
+ await new Promise((resolve20, reject) => {
4516
5183
  server.on("listening", () => {
4517
5184
  logger_default.info("Volute UI running", { hostname, port });
4518
- resolve18();
5185
+ resolve20();
4519
5186
  });
4520
5187
  server.on("error", (err) => {
4521
5188
  reject(err);
@@ -4526,14 +5193,14 @@ async function startServer({
4526
5193
 
4527
5194
  // src/daemon.ts
4528
5195
  if (!process.env.VOLUTE_HOME) {
4529
- process.env.VOLUTE_HOME = resolve17(homedir2(), ".volute");
5196
+ process.env.VOLUTE_HOME = resolve19(homedir2(), ".volute");
4530
5197
  }
4531
5198
  async function startDaemon(opts) {
4532
5199
  const { port, hostname } = opts;
4533
5200
  const myPid = String(process.pid);
4534
5201
  const home = voluteHome();
4535
5202
  if (!opts.foreground) {
4536
- const rotatingLog = new RotatingLog(resolve17(home, "daemon.log"));
5203
+ const rotatingLog = new RotatingLog(resolve19(home, "daemon.log"));
4537
5204
  logger_default.setOutput((line) => rotatingLog.write(`${line}
4538
5205
  `));
4539
5206
  const write = (...args) => rotatingLog.write(`${format(...args)}
@@ -4543,10 +5210,21 @@ async function startDaemon(opts) {
4543
5210
  console.warn = write;
4544
5211
  console.info = write;
4545
5212
  }
4546
- const DAEMON_PID_PATH = resolve17(home, "daemon.pid");
4547
- const DAEMON_JSON_PATH = resolve17(home, "daemon.json");
4548
- mkdirSync7(home, { recursive: true });
5213
+ const DAEMON_PID_PATH = resolve19(home, "daemon.pid");
5214
+ const DAEMON_JSON_PATH = resolve19(home, "daemon.json");
5215
+ mkdirSync10(home, { recursive: true });
4549
5216
  migrateAgentsToMinds();
5217
+ try {
5218
+ await ensureSharedRepo();
5219
+ } catch (err) {
5220
+ logger_default.warn("failed to initialize shared repo", logger_default.errorData(err));
5221
+ }
5222
+ initRegistryCache();
5223
+ try {
5224
+ await syncBuiltinSkills();
5225
+ } catch (err) {
5226
+ logger_default.error("failed to sync built-in skills", logger_default.errorData(err));
5227
+ }
4550
5228
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
4551
5229
  process.env.VOLUTE_DAEMON_TOKEN = token;
4552
5230
  process.env.VOLUTE_DAEMON_PORT = String(port);
@@ -4562,73 +5240,78 @@ async function startDaemon(opts) {
4562
5240
  }
4563
5241
  throw err;
4564
5242
  }
4565
- writeFileSync7(DAEMON_PID_PATH, myPid, { mode: 420 });
4566
- writeFileSync7(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
5243
+ writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
5244
+ writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
4567
5245
  `, {
4568
5246
  mode: 420
4569
5247
  });
5248
+ const delivery = initDeliveryManager();
4570
5249
  const manager = initMindManager();
4571
5250
  manager.loadCrashAttempts();
4572
5251
  const connectors = initConnectorManager();
4573
- const scheduler = getScheduler();
4574
- scheduler.start(port, token);
4575
- const mailPoller = getMailPoller();
4576
- mailPoller.start(port, token);
4577
- const tokenBudget = getTokenBudget();
4578
- tokenBudget.start(port, token);
5252
+ const scheduler = initScheduler();
5253
+ scheduler.start();
5254
+ const mailPoller = initMailPoller();
5255
+ mailPoller.start();
5256
+ const tokenBudget = initTokenBudget();
5257
+ tokenBudget.start();
4579
5258
  const registry = readRegistry();
4580
5259
  for (const entry of registry) {
4581
5260
  try {
5261
+ migrateDotVoluteDir(entry.name);
4582
5262
  migrateMindState(entry.name);
4583
5263
  } catch (err) {
4584
5264
  logger_default.warn(`failed to migrate state for ${entry.name}`, logger_default.errorData(err));
4585
5265
  }
4586
5266
  }
4587
- for (const entry of registry) {
4588
- if (!entry.running) continue;
4589
- try {
4590
- await manager.startMind(entry.name);
4591
- if (entry.stage === "seed") continue;
4592
- const dir = mindDir(entry.name);
4593
- await connectors.startConnectors(entry.name, dir, entry.port, port);
4594
- scheduler.loadSchedules(entry.name);
4595
- ensureMailAddress(entry.name).catch(() => {
4596
- });
4597
- const config = readVoluteConfig(dir);
4598
- if (config?.tokenBudget) {
4599
- tokenBudget.setBudget(
4600
- entry.name,
4601
- config.tokenBudget,
4602
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
4603
- );
5267
+ const runningEntries = registry.filter((e) => e.running);
5268
+ {
5269
+ const queue = [...runningEntries];
5270
+ const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
5271
+ while (queue.length > 0) {
5272
+ const entry = queue.shift();
5273
+ try {
5274
+ await startMindFull(entry.name);
5275
+ } catch (err) {
5276
+ logger_default.error(`failed to start mind ${entry.name}`, logger_default.errorData(err));
5277
+ setMindRunning(entry.name, false);
5278
+ }
4604
5279
  }
4605
- } catch (err) {
4606
- logger_default.error(`failed to start mind ${entry.name}`, logger_default.errorData(err));
4607
- setMindRunning(entry.name, false);
4608
- }
5280
+ });
5281
+ await Promise.all(workers);
4609
5282
  }
4610
5283
  const runningVariants = getAllRunningVariants();
4611
- for (const { mindName, variant } of runningVariants) {
4612
- const compositeKey = `${mindName}@${variant.name}`;
4613
- try {
4614
- await manager.startMind(compositeKey);
4615
- } catch (err) {
4616
- logger_default.error(`failed to start variant ${compositeKey}`, logger_default.errorData(err));
4617
- setVariantRunning(mindName, variant.name, false);
4618
- }
5284
+ {
5285
+ const queue = [...runningVariants];
5286
+ const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
5287
+ while (queue.length > 0) {
5288
+ const { mindName, variant } = queue.shift();
5289
+ const compositeKey = `${mindName}@${variant.name}`;
5290
+ try {
5291
+ await startMindFull(compositeKey);
5292
+ } catch (err) {
5293
+ logger_default.error(`failed to start variant ${compositeKey}`, logger_default.errorData(err));
5294
+ setVariantRunning(mindName, variant.name, false);
5295
+ }
5296
+ }
5297
+ });
5298
+ await Promise.all(workers);
4619
5299
  }
5300
+ delivery.restoreFromDb().catch((err) => {
5301
+ logger_default.warn("failed to restore delivery queue", logger_default.errorData(err));
5302
+ });
4620
5303
  cleanExpiredSessions().catch(() => {
4621
5304
  });
4622
5305
  logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
4623
5306
  function cleanup() {
4624
5307
  try {
4625
- if (readFileSync9(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
5308
+ if (readFileSync11(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
4626
5309
  unlinkSync2(DAEMON_PID_PATH);
4627
5310
  }
4628
5311
  } catch {
4629
5312
  }
4630
5313
  try {
4631
- const data = JSON.parse(readFileSync9(DAEMON_JSON_PATH, "utf-8"));
5314
+ const data = JSON.parse(readFileSync11(DAEMON_JSON_PATH, "utf-8"));
4632
5315
  if (data.token === token) {
4633
5316
  unlinkSync2(DAEMON_JSON_PATH);
4634
5317
  }
@@ -4644,6 +5327,7 @@ async function startDaemon(opts) {
4644
5327
  scheduler.saveState();
4645
5328
  mailPoller.stop();
4646
5329
  tokenBudget.stop();
5330
+ delivery.dispose();
4647
5331
  await connectors.stopAll();
4648
5332
  await manager.stopAll();
4649
5333
  manager.clearCrashAttempts();