volute 0.18.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 (104) 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-AYB7XAWO.js → chunk-2TJGRJ4O.js} +114 -279
  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-GK4E7LM7.js → chunk-RHEGSQFJ.js} +1 -1
  17. package/dist/{chunk-MVSXRMJJ.js → chunk-SCUDS4US.js} +1 -1
  18. package/dist/{chunk-FW5API7X.js → chunk-UJ6GHNR7.js} +2 -2
  19. package/dist/{chunk-OYSZNX5I.js → chunk-VDWCHYTS.js} +1 -1
  20. package/dist/{chunk-6DVBMLVN.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 +33 -25
  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-2HVTHZAT.js → daemon-restart-JMZM3QY4.js} +8 -8
  33. package/dist/daemon.js +1144 -1108
  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-YUEKTJ2N.js → history-WNK3DFUM.js} +6 -6
  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-OKLFO7UY.js → package-MYE2ZJLV.js} +5 -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-BNDTLUPM.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-2Y42P4JY.js → skill-BCVNI6TV.js} +6 -6
  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 +19 -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-7B3BWF2U.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/0010_delivery_queue.sql +12 -0
  82. package/drizzle/0011_rename_human_to_brain.sql +1 -0
  83. package/drizzle/meta/0010_snapshot.json +7 -0
  84. package/drizzle/meta/0011_snapshot.json +7 -0
  85. package/drizzle/meta/_journal.json +14 -0
  86. package/package.json +5 -3
  87. package/templates/_base/.init/.config/hooks/startup-context.sh +1 -1
  88. package/templates/_base/.init/.config/scripts/session-reader.ts +3 -3
  89. package/templates/_base/home/VOLUTE.md +16 -1
  90. package/templates/_base/src/lib/auto-commit.ts +51 -14
  91. package/templates/_base/src/lib/router.ts +123 -1
  92. package/templates/_base/src/lib/types.ts +4 -0
  93. package/templates/_base/src/lib/volute-server.ts +91 -2
  94. package/templates/claude/src/server.ts +2 -2
  95. package/templates/claude/volute-template.json +1 -2
  96. package/templates/pi/src/agent.ts +1 -1
  97. package/templates/pi/src/lib/session-context-extension.ts +2 -2
  98. package/templates/pi/volute-template.json +1 -2
  99. package/dist/chunk-PO5Q2AYN.js +0 -121
  100. package/dist/down-A56B5JLK.js +0 -14
  101. package/dist/mind-manager-Z7O7PN2O.js +0 -15
  102. package/dist/web-assets/assets/index-CtiimdWK.css +0 -1
  103. package/dist/web-assets/assets/index-kt1_EcuO.js +0 -63
  104. /package/{templates/_base/_skills → dist/skills}/memory/SKILL.md +0 -0
package/dist/daemon.js CHANGED
@@ -1,39 +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 {
13
38
  PROMPT_DEFAULTS,
14
39
  PROMPT_KEYS,
40
+ RestartTracker,
15
41
  RotatingLog,
16
42
  clearJsonMap,
17
- conversationParticipants,
18
- conversations,
19
- getDb,
20
43
  getMindManager,
21
44
  getMindPromptDefaults,
22
45
  getPrompt,
23
46
  getPromptIfCustom,
24
47
  initMindManager,
25
48
  loadJsonMap,
26
- logBuffer,
27
- logger_default,
28
- messages,
29
- mindHistory,
30
49
  saveJsonMap,
31
- sessions,
32
- sharedSkills,
33
- substitute,
34
- systemPrompts,
35
- users
36
- } from "./chunk-AYB7XAWO.js";
50
+ substitute
51
+ } from "./chunk-2TJGRJ4O.js";
52
+ import {
53
+ logBuffer,
54
+ logger_default
55
+ } from "./chunk-YUIHSKR6.js";
56
+ import {
57
+ CHANNELS,
58
+ getChannelDriver
59
+ } from "./chunk-UJ6GHNR7.js";
37
60
  import {
38
61
  findOpenClawSession,
39
62
  importOpenClawConnectors,
@@ -41,23 +64,32 @@ import {
41
64
  parseNameFromIdentity,
42
65
  readVoluteConfig,
43
66
  writeVoluteConfig
44
- } from "./chunk-GSPWIM5E.js";
67
+ } from "./chunk-EMQSAY3B.js";
45
68
  import {
46
69
  loadMergedEnv,
47
70
  mindEnvPath,
48
71
  readEnv,
49
72
  sharedEnvPath,
50
73
  writeEnv
51
- } from "./chunk-OYSZNX5I.js";
74
+ } from "./chunk-VDWCHYTS.js";
52
75
  import {
53
- CHANNELS,
54
- getChannelDriver
55
- } from "./chunk-FW5API7X.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";
56
88
  import {
57
89
  exec,
58
90
  gitExec,
59
91
  resolveVoluteBin
60
- } from "./chunk-2Y77MCFG.js";
92
+ } from "./chunk-DYZGP3EW.js";
61
93
  import {
62
94
  chownMindDir,
63
95
  createMindUser,
@@ -65,17 +97,16 @@ import {
65
97
  ensureVoluteGroup,
66
98
  isIsolationEnabled,
67
99
  wrapForIsolation
68
- } from "./chunk-ZCEYUUID.js";
100
+ } from "./chunk-OGXOMR65.js";
69
101
  import {
70
102
  checkForUpdate,
71
103
  checkForUpdateCached,
72
104
  getCurrentVersion
73
- } from "./chunk-MVSXRMJJ.js";
74
- import "./chunk-D424ZQGI.js";
105
+ } from "./chunk-SCUDS4US.js";
75
106
  import {
76
107
  buildVoluteSlug,
77
108
  writeChannelEntry
78
- } from "./chunk-GK4E7LM7.js";
109
+ } from "./chunk-RHEGSQFJ.js";
79
110
  import {
80
111
  addMind,
81
112
  addVariant,
@@ -85,6 +116,7 @@ import {
85
116
  findMind,
86
117
  findVariant,
87
118
  getAllRunningVariants,
119
+ initRegistryCache,
88
120
  mindDir,
89
121
  nextPort,
90
122
  readRegistry,
@@ -99,14 +131,14 @@ import {
99
131
  validateBranchName,
100
132
  validateMindName,
101
133
  voluteHome
102
- } from "./chunk-M77QBTEH.js";
134
+ } from "./chunk-EBGCNDMM.js";
103
135
  import "./chunk-K3NQKI34.js";
104
136
 
105
137
  // src/daemon.ts
106
138
  import { randomBytes } from "crypto";
107
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "fs";
139
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
108
140
  import { homedir as homedir2 } from "os";
109
- import { resolve as resolve18 } from "path";
141
+ import { resolve as resolve19 } from "path";
110
142
  import { format } from "util";
111
143
 
112
144
  // src/lib/connector-manager.ts
@@ -197,16 +229,12 @@ function searchUpwards(...segments) {
197
229
  }
198
230
  return null;
199
231
  }
200
- var MAX_RESTART_ATTEMPTS = 5;
201
- var BASE_RESTART_DELAY = 3e3;
202
- var MAX_RESTART_DELAY = 6e4;
203
232
  var ConnectorManager = class {
204
233
  connectors = /* @__PURE__ */ new Map();
205
234
  stopping = /* @__PURE__ */ new Set();
206
235
  // "mind:type" keys currently being explicitly stopped
207
236
  shuttingDown = false;
208
- restartAttempts = /* @__PURE__ */ new Map();
209
- // "mind:type" -> count
237
+ restartTracker = new RestartTracker();
210
238
  async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
211
239
  const config = readVoluteConfig(mindDir2) ?? {};
212
240
  const types = config.connectors ?? [];
@@ -319,7 +347,7 @@ var ConnectorManager = class {
319
347
  }
320
348
  this.connectors.get(mindName).set(type, { child, type });
321
349
  const stopKey = `${mindName}:${type}`;
322
- this.restartAttempts.delete(stopKey);
350
+ this.restartTracker.reset(stopKey);
323
351
  child.on("exit", (code) => {
324
352
  const mindMap = this.connectors.get(mindName);
325
353
  if (mindMap?.get(type)?.child === child) {
@@ -329,15 +357,13 @@ var ConnectorManager = class {
329
357
  if (this.stopping.has(stopKey)) return;
330
358
  clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
331
359
  if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
332
- const attempts = this.restartAttempts.get(stopKey) ?? 0;
333
- if (attempts >= MAX_RESTART_ATTEMPTS) {
334
- 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`);
335
363
  return;
336
364
  }
337
- const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
338
- this.restartAttempts.set(stopKey, attempts + 1);
339
365
  clog.info(
340
- `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`
341
367
  );
342
368
  setTimeout(() => {
343
369
  if (this.shuttingDown || this.stopping.has(stopKey)) return;
@@ -356,23 +382,23 @@ var ConnectorManager = class {
356
382
  const stopKey = `${mindName}:${type}`;
357
383
  this.stopping.add(stopKey);
358
384
  mindMap.delete(type);
359
- await new Promise((resolve19) => {
360
- tracked.child.on("exit", () => resolve19());
385
+ await new Promise((resolve20) => {
386
+ tracked.child.on("exit", () => resolve20());
361
387
  try {
362
388
  tracked.child.kill("SIGTERM");
363
389
  } catch {
364
- resolve19();
390
+ resolve20();
365
391
  }
366
392
  setTimeout(() => {
367
393
  try {
368
394
  tracked.child.kill("SIGKILL");
369
395
  } catch {
370
396
  }
371
- resolve19();
397
+ resolve20();
372
398
  }, 5e3);
373
399
  });
374
400
  this.stopping.delete(stopKey);
375
- this.restartAttempts.delete(stopKey);
401
+ this.restartTracker.reset(stopKey);
376
402
  try {
377
403
  this.removeConnectorPid(mindName, type);
378
404
  } catch (err) {
@@ -444,7 +470,8 @@ function initConnectorManager() {
444
470
  return instance;
445
471
  }
446
472
  function getConnectorManager() {
447
- if (!instance) instance = new ConnectorManager();
473
+ if (!instance)
474
+ throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
448
475
  return instance;
449
476
  }
450
477
 
@@ -468,15 +495,13 @@ var INITIAL_RECONNECT_MS = 1e3;
468
495
  var MAX_RECONNECT_MS = 6e4;
469
496
  var MailPoller = class {
470
497
  ws = null;
471
- daemonPort = null;
472
- daemonToken = null;
473
498
  running = false;
474
499
  pingTimer = null;
475
500
  reconnectTimer = null;
476
501
  reconnectDelay = INITIAL_RECONNECT_MS;
477
502
  reconnectAttempts = 0;
478
503
  disconnectedAt = null;
479
- start(daemonPort, daemonToken) {
504
+ start() {
480
505
  if (this.running) {
481
506
  mlog.warn("already running \u2014 ignoring duplicate start");
482
507
  return;
@@ -486,8 +511,6 @@ var MailPoller = class {
486
511
  mlog.info("no systems config \u2014 mail disabled");
487
512
  return;
488
513
  }
489
- this.daemonPort = daemonPort ?? null;
490
- this.daemonToken = daemonToken ?? null;
491
514
  this.running = true;
492
515
  this.connect();
493
516
  }
@@ -643,51 +666,29 @@ var MailPoller = class {
643
666
  mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
644
667
  return;
645
668
  }
646
- const channel = `mail:${email.from.address}`;
647
- const sender = email.from.name || email.from.address;
648
669
  const text = formatEmailContent(email);
649
- const body = JSON.stringify({
650
- content: [{ type: "text", text }],
651
- channel,
652
- sender,
653
- platform: "Email",
654
- isDM: true
655
- });
656
- if (!this.daemonPort || !this.daemonToken) {
657
- mlog.warn(`cannot deliver to ${mind}: daemon port/token not set`);
658
- return;
659
- }
660
- const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
661
- const controller = new AbortController();
662
- const timeout = setTimeout(() => controller.abort(), 12e4);
663
670
  try {
664
- const res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mind)}/message`, {
665
- method: "POST",
666
- headers: {
667
- "Content-Type": "application/json",
668
- Authorization: `Bearer ${this.daemonToken}`,
669
- Origin: daemonUrl
670
- },
671
- body,
672
- signal: controller.signal
673
- });
674
- if (!res.ok) {
675
- mlog.warn(`deliver to ${mind} got HTTP ${res.status}`);
676
- } else {
677
- mlog.info(`delivered email from ${email.from.address} to ${mind}`);
678
- }
679
- 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
680
677
  });
678
+ mlog.info(`delivered email from ${email.from.address} to ${mind}`);
681
679
  } catch (err) {
682
680
  mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
683
- } finally {
684
- clearTimeout(timeout);
685
681
  }
686
682
  }
687
683
  };
688
684
  var instance2 = null;
685
+ function initMailPoller() {
686
+ if (instance2) throw new Error("MailPoller already initialized");
687
+ instance2 = new MailPoller();
688
+ return instance2;
689
+ }
689
690
  function getMailPoller() {
690
- if (!instance2) instance2 = new MailPoller();
691
+ if (!instance2) throw new Error("MailPoller not initialized \u2014 call initMailPoller() first");
691
692
  return instance2;
692
693
  }
693
694
  async function ensureMailAddress(mindName) {
@@ -846,10 +847,20 @@ function migrateProfileScript() {
846
847
  }
847
848
 
848
849
  // src/lib/migrate-state.ts
849
- 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";
850
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
+ }
851
862
  function migrateMindState(name) {
852
- const src = resolve4(mindDir(name), ".volute");
863
+ const src = resolve4(mindDir(name), ".mind");
853
864
  if (!existsSync4(src)) return;
854
865
  const dest = stateDir(name);
855
866
  mkdirSync2(dest, { recursive: true });
@@ -883,14 +894,10 @@ var Scheduler = class {
883
894
  interval = null;
884
895
  lastFired = /* @__PURE__ */ new Map();
885
896
  // "mind:scheduleId" → epoch minute
886
- daemonPort = null;
887
- daemonToken = null;
888
897
  get statePath() {
889
898
  return resolve5(voluteHome(), "scheduler-state.json");
890
899
  }
891
- start(daemonPort, daemonToken) {
892
- this.daemonPort = daemonPort ?? null;
893
- this.daemonToken = daemonToken ?? null;
900
+ start() {
894
901
  this.loadState();
895
902
  this.interval = setInterval(() => this.tick(), 6e4);
896
903
  }
@@ -921,9 +928,6 @@ var Scheduler = class {
921
928
  this.schedules.delete(mindName);
922
929
  }
923
930
  tick() {
924
- for (const mind of this.schedules.keys()) {
925
- this.loadSchedules(mind);
926
- }
927
931
  const now = /* @__PURE__ */ new Date();
928
932
  for (const [mind, schedules] of this.schedules) {
929
933
  for (const schedule of schedules) {
@@ -954,69 +958,39 @@ var Scheduler = class {
954
958
  }
955
959
  }
956
960
  async fire(mindName, schedule) {
957
- const entry = findMind(mindName);
958
- if (!entry) return;
959
- const body = JSON.stringify({
960
- content: [{ type: "text", text: schedule.message }],
961
- channel: "system:scheduler",
962
- sender: schedule.id
963
- });
964
- const controller = new AbortController();
965
- const timeout = setTimeout(() => controller.abort(), 12e4);
966
961
  try {
967
- let res;
968
- if (this.daemonPort && this.daemonToken) {
969
- const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
970
- res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mindName)}/message`, {
971
- method: "POST",
972
- headers: {
973
- "Content-Type": "application/json",
974
- Authorization: `Bearer ${this.daemonToken}`,
975
- Origin: daemonUrl
976
- },
977
- body,
978
- signal: controller.signal
979
- });
980
- } else {
981
- res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
982
- method: "POST",
983
- headers: { "Content-Type": "application/json" },
984
- body,
985
- signal: controller.signal
986
- });
987
- }
988
- if (!res.ok) {
989
- slog.warn(`"${schedule.id}" for ${mindName} got HTTP ${res.status}`);
990
- } else {
991
- slog.info(`fired "${schedule.id}" for ${mindName}`);
992
- }
993
- await res.text().catch(() => {
962
+ await deliverMessage(mindName, {
963
+ content: [{ type: "text", text: schedule.message }],
964
+ channel: "system:scheduler",
965
+ sender: schedule.id
994
966
  });
967
+ slog.info(`fired "${schedule.id}" for ${mindName}`);
995
968
  } catch (err) {
996
969
  slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
997
- } finally {
998
- clearTimeout(timeout);
999
970
  }
1000
971
  }
1001
972
  };
1002
973
  var instance3 = null;
974
+ function initScheduler() {
975
+ if (instance3) throw new Error("Scheduler already initialized");
976
+ instance3 = new Scheduler();
977
+ return instance3;
978
+ }
1003
979
  function getScheduler() {
1004
- if (!instance3) instance3 = new Scheduler();
980
+ if (!instance3) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
1005
981
  return instance3;
1006
982
  }
1007
983
 
1008
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";
1009
987
  var tlog = logger_default.child("token-budget");
1010
988
  var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
1011
989
  var MAX_QUEUE_SIZE = 100;
1012
990
  var TokenBudget = class {
1013
991
  budgets = /* @__PURE__ */ new Map();
1014
992
  interval = null;
1015
- daemonPort = null;
1016
- daemonToken = null;
1017
- start(daemonPort, daemonToken) {
1018
- this.daemonPort = daemonPort ?? null;
1019
- this.daemonToken = daemonToken ?? null;
993
+ start() {
1020
994
  this.interval = setInterval(() => this.tick(), 6e4);
1021
995
  }
1022
996
  stop() {
@@ -1030,14 +1004,21 @@ var TokenBudget = class {
1030
1004
  existing.tokenLimit = tokenLimit;
1031
1005
  existing.periodMinutes = periodMinutes;
1032
1006
  } else {
1033
- this.budgets.set(mind, {
1034
- tokensUsed: 0,
1035
- periodStart: Date.now(),
1036
- periodMinutes,
1037
- tokenLimit,
1038
- queue: [],
1039
- warningInjected: false
1040
- });
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
+ }
1041
1022
  }
1042
1023
  }
1043
1024
  removeBudget(mind) {
@@ -1047,6 +1028,7 @@ var TokenBudget = class {
1047
1028
  const state = this.budgets.get(mind);
1048
1029
  if (!state) return;
1049
1030
  state.tokensUsed += inputTokens + outputTokens;
1031
+ this.saveBudgetState(mind, state);
1050
1032
  }
1051
1033
  /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
1052
1034
  checkBudget(mind) {
@@ -1097,6 +1079,7 @@ var TokenBudget = class {
1097
1079
  state.tokensUsed = 0;
1098
1080
  state.periodStart = now;
1099
1081
  state.warningInjected = false;
1082
+ this.saveBudgetState(mind, state);
1100
1083
  const queued = this.drain(mind);
1101
1084
  if (queued.length > 0) {
1102
1085
  this.replay(mind, queued).catch((err) => {
@@ -1106,68 +1089,117 @@ var TokenBudget = class {
1106
1089
  }
1107
1090
  }
1108
1091
  }
1109
- async replay(mindName, messages2) {
1110
- if (!this.daemonPort || !this.daemonToken) {
1111
- tlog.warn(
1112
- `cannot replay ${messages2.length} message(s) for ${mindName}: daemon not configured`
1113
- );
1114
- const state = this.budgets.get(mindName);
1115
- if (state) state.queue.push(...messages2);
1116
- 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;
1117
1130
  }
1131
+ }
1132
+ async replay(mindName, messages2) {
1118
1133
  const summary = messages2.map((m) => {
1119
1134
  const from = m.sender ? `[${m.sender}]` : "";
1120
1135
  const ch = m.channel ? `(${m.channel})` : "";
1121
1136
  return `${from}${ch} ${m.textContent}`;
1122
1137
  }).join("\n");
1123
- const body = JSON.stringify({
1124
- content: [
1125
- {
1126
- type: "text",
1127
- 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:
1128
1144
 
1129
1145
  ${summary}`
1130
- }
1131
- ],
1132
- channel: "system:budget-replay",
1133
- sender: "system"
1134
- });
1135
- const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
1136
- const controller = new AbortController();
1137
- const timeout = setTimeout(() => controller.abort(), 12e4);
1138
- try {
1139
- const res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mindName)}/message`, {
1140
- method: "POST",
1141
- headers: {
1142
- "Content-Type": "application/json",
1143
- Authorization: `Bearer ${this.daemonToken}`,
1144
- Origin: daemonUrl
1145
- },
1146
- body,
1147
- signal: controller.signal
1148
- });
1149
- if (!res.ok) {
1150
- tlog.warn(`replay for ${mindName} got HTTP ${res.status}`);
1151
- } else {
1152
- tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
1153
- }
1154
- await res.text().catch(() => {
1146
+ }
1147
+ ],
1148
+ channel: "system:budget-replay",
1149
+ sender: "system"
1155
1150
  });
1151
+ tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
1156
1152
  } catch (err) {
1157
1153
  tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
1158
1154
  const state = this.budgets.get(mindName);
1159
1155
  if (state) state.queue.push(...messages2);
1160
- } finally {
1161
- clearTimeout(timeout);
1162
1156
  }
1163
1157
  }
1164
1158
  };
1165
1159
  var instance4 = null;
1160
+ function initTokenBudget() {
1161
+ if (instance4) throw new Error("TokenBudget already initialized");
1162
+ instance4 = new TokenBudget();
1163
+ return instance4;
1164
+ }
1166
1165
  function getTokenBudget() {
1167
- if (!instance4) instance4 = new TokenBudget();
1166
+ if (!instance4) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
1168
1167
  return instance4;
1169
1168
  }
1170
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
+
1171
1203
  // src/web/middleware/auth.ts
1172
1204
  import { timingSafeEqual } from "crypto";
1173
1205
  import { eq as eq2, lt } from "drizzle-orm";
@@ -1180,7 +1212,7 @@ import { and, count, eq } from "drizzle-orm";
1180
1212
  async function createUser(username, password) {
1181
1213
  const db = await getDb();
1182
1214
  const hash = hashSync(password, 10);
1183
- const [{ value }] = await db.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"));
1184
1216
  const role = value === 0 ? "admin" : "pending";
1185
1217
  const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
1186
1218
  id: users.id,
@@ -1315,6 +1347,8 @@ function isValidDaemonToken(token) {
1315
1347
  return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
1316
1348
  }
1317
1349
  var SESSION_MAX_AGE = 864e5;
1350
+ var SESSION_CACHE_TTL = 5 * 60 * 1e3;
1351
+ var sessionCache = /* @__PURE__ */ new Map();
1318
1352
  async function createSession(userId) {
1319
1353
  const db = await getDb();
1320
1354
  const sessionId = crypto.randomUUID();
@@ -1322,6 +1356,7 @@ async function createSession(userId) {
1322
1356
  return sessionId;
1323
1357
  }
1324
1358
  async function deleteSession(sessionId) {
1359
+ sessionCache.delete(sessionId);
1325
1360
  const db = await getDb();
1326
1361
  await db.delete(sessions).where(eq2(sessions.id, sessionId));
1327
1362
  }
@@ -1352,30 +1387,44 @@ var authMiddleware = createMiddleware(async (c, next) => {
1352
1387
  if (authHeader?.startsWith("Bearer ")) {
1353
1388
  const token = authHeader.slice(7);
1354
1389
  if (token && isValidDaemonToken(token)) {
1355
- 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" });
1356
1391
  await next();
1357
1392
  return;
1358
1393
  }
1359
1394
  }
1360
1395
  const sessionId = getCookie(c, "volute_session");
1361
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
+ }
1362
1404
  const userId = await getSessionUserId(sessionId);
1363
- 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
+ }
1364
1409
  const user = await getUser(userId);
1365
- if (!user) return c.json({ error: "Unauthorized" }, 401);
1410
+ if (!user) {
1411
+ sessionCache.delete(sessionId);
1412
+ return c.json({ error: "Unauthorized" }, 401);
1413
+ }
1366
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 });
1367
1416
  c.set("user", user);
1368
1417
  await next();
1369
1418
  });
1370
1419
 
1371
1420
  // src/web/server.ts
1372
- import { existsSync as existsSync11 } from "fs";
1421
+ import { existsSync as existsSync13 } from "fs";
1373
1422
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
1374
- import { dirname as dirname2, extname as extname2, resolve as resolve17 } from "path";
1423
+ import { dirname as dirname3, extname as extname2, resolve as resolve18 } from "path";
1375
1424
  import { serve } from "@hono/node-server";
1376
1425
 
1377
1426
  // src/web/app.ts
1378
- import { Hono as Hono21 } from "hono";
1427
+ import { Hono as Hono23 } from "hono";
1379
1428
  import { bodyLimit } from "hono/body-limit";
1380
1429
  import { csrf } from "hono/csrf";
1381
1430
  import { HTTPException } from "hono/http-exception";
@@ -1408,7 +1457,7 @@ var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
1408
1457
  await getOrCreateMindUser(mind.name);
1409
1458
  }
1410
1459
  const type = c.req.query("type");
1411
- if (type === "human" || type === "mind") {
1460
+ if (type === "brain" || type === "mind") {
1412
1461
  return c.json(await listUsersByType(type));
1413
1462
  }
1414
1463
  return c.json(await listUsers());
@@ -1713,9 +1762,9 @@ var sharedEnvApp = new Hono4().get("/", (c) => {
1713
1762
  var env_default = app4;
1714
1763
 
1715
1764
  // src/web/api/files.ts
1716
- import { existsSync as existsSync5 } from "fs";
1765
+ import { existsSync as existsSync6 } from "fs";
1717
1766
  import { readdir, readFile } from "fs/promises";
1718
- import { resolve as resolve6 } from "path";
1767
+ import { resolve as resolve7 } from "path";
1719
1768
  import { Hono as Hono5 } from "hono";
1720
1769
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1721
1770
  var app5 = new Hono5().get("/:name/files", async (c) => {
@@ -1723,8 +1772,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1723
1772
  const entry = findMind(name);
1724
1773
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1725
1774
  const dir = mindDir(name);
1726
- const homeDir = resolve6(dir, "home");
1727
- if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1775
+ const homeDir = resolve7(dir, "home");
1776
+ if (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1728
1777
  const allFiles = await readdir(homeDir);
1729
1778
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1730
1779
  return c.json(files);
@@ -1737,8 +1786,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1737
1786
  const entry = findMind(name);
1738
1787
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1739
1788
  const dir = mindDir(name);
1740
- const filePath = resolve6(dir, "home", filename);
1741
- if (!existsSync5(filePath)) {
1789
+ const filePath = resolve7(dir, "home", filename);
1790
+ if (!existsSync6(filePath)) {
1742
1791
  return c.json({ error: "File not found" }, 404);
1743
1792
  }
1744
1793
  const content = await readFile(filePath, "utf-8");
@@ -1746,18 +1795,109 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1746
1795
  });
1747
1796
  var files_default = app5;
1748
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
+
1749
1889
  // src/web/api/logs.ts
1750
1890
  import { spawn as spawn2 } from "child_process";
1751
- import { existsSync as existsSync6 } from "fs";
1752
- import { resolve as resolve7 } from "path";
1753
- 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";
1754
1894
  import { streamSSE } from "hono/streaming";
1755
- var app6 = new Hono6().get("/:name/logs", async (c) => {
1895
+ var app7 = new Hono7().get("/:name/logs", async (c) => {
1756
1896
  const name = c.req.param("name");
1757
1897
  const entry = findMind(name);
1758
1898
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1759
- const logFile = resolve7(stateDir(name), "logs", "mind.log");
1760
- if (!existsSync6(logFile)) {
1899
+ const logFile = resolve9(stateDir(name), "logs", "mind.log");
1900
+ if (!existsSync8(logFile)) {
1761
1901
  return c.json({ error: "No log file found" }, 404);
1762
1902
  }
1763
1903
  return streamSSE(c, async (stream) => {
@@ -1775,17 +1915,17 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1775
1915
  stream.onAbort(() => {
1776
1916
  tail.kill();
1777
1917
  });
1778
- await new Promise((resolve19) => {
1779
- tail.on("exit", resolve19);
1780
- stream.onAbort(resolve19);
1918
+ await new Promise((resolve20) => {
1919
+ tail.on("exit", resolve20);
1920
+ stream.onAbort(resolve20);
1781
1921
  });
1782
1922
  });
1783
1923
  }).get("/:name/logs/tail", async (c) => {
1784
1924
  const name = c.req.param("name");
1785
1925
  const entry = findMind(name);
1786
1926
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1787
- const logFile = resolve7(stateDir(name), "logs", "mind.log");
1788
- if (!existsSync6(logFile)) {
1927
+ const logFile = resolve9(stateDir(name), "logs", "mind.log");
1928
+ if (!existsSync8(logFile)) {
1789
1929
  return c.json({ error: "No log file found" }, 404);
1790
1930
  }
1791
1931
  const nParam = parseInt(c.req.query("n") ?? "50", 10);
@@ -1795,314 +1935,18 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1795
1935
  tail.stdout.on("data", (data) => {
1796
1936
  output += data.toString();
1797
1937
  });
1798
- await new Promise((resolve19) => {
1799
- tail.on("exit", resolve19);
1938
+ await new Promise((resolve20) => {
1939
+ tail.on("exit", resolve20);
1800
1940
  });
1801
1941
  return c.text(output);
1802
1942
  });
1803
- var logs_default = app6;
1943
+ var logs_default = app7;
1804
1944
 
1805
1945
  // src/web/api/mind-skills.ts
1806
1946
  import { zValidator as zValidator2 } from "@hono/zod-validator";
1807
- import { Hono as Hono7 } from "hono";
1947
+ import { Hono as Hono8 } from "hono";
1808
1948
  import { z as z2 } from "zod";
1809
-
1810
- // src/lib/skills.ts
1811
- import {
1812
- cpSync,
1813
- existsSync as existsSync7,
1814
- mkdirSync as mkdirSync3,
1815
- readdirSync as readdirSync2,
1816
- readFileSync as readFileSync4,
1817
- rmSync,
1818
- writeFileSync as writeFileSync3
1819
- } from "fs";
1820
- import { tmpdir } from "os";
1821
- import { basename, join, resolve as resolve8 } from "path";
1822
- import { eq as eq3, sql } from "drizzle-orm";
1823
- var VALID_SKILL_ID = /^[a-zA-Z0-9_-]+$/;
1824
- function validateSkillId(id) {
1825
- if (!id || !VALID_SKILL_ID.test(id)) {
1826
- throw new Error(`Invalid skill ID: ${id}`);
1827
- }
1828
- }
1829
- function sharedSkillsDir() {
1830
- return resolve8(voluteHome(), "skills");
1831
- }
1832
- function parseSkillMd(content) {
1833
- const match = content.match(/^---\n([\s\S]*?)\n---/);
1834
- if (!match) return { name: "", description: "" };
1835
- const frontmatter = match[1];
1836
- const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
1837
- const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
1838
- return {
1839
- name: nameMatch?.[1].trim() ?? "",
1840
- description: descMatch?.[1].trim() ?? ""
1841
- };
1842
- }
1843
- async function listSharedSkills() {
1844
- const db = await getDb();
1845
- return db.select().from(sharedSkills).all();
1846
- }
1847
- async function getSharedSkill(id) {
1848
- const db = await getDb();
1849
- return db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1850
- }
1851
- async function importSkillFromDir(sourceDir, author) {
1852
- const skillMdPath = join(sourceDir, "SKILL.md");
1853
- if (!existsSync7(skillMdPath)) {
1854
- throw new Error("SKILL.md not found in source directory");
1855
- }
1856
- const content = readFileSync4(skillMdPath, "utf-8");
1857
- const { name, description } = parseSkillMd(content);
1858
- const id = basename(sourceDir);
1859
- if (!id || id === "." || id === "..") {
1860
- throw new Error("Invalid skill directory name");
1861
- }
1862
- validateSkillId(id);
1863
- const destDir = join(sharedSkillsDir(), id);
1864
- if (existsSync7(destDir)) rmSync(destDir, { recursive: true });
1865
- mkdirSync3(destDir, { recursive: true });
1866
- cpSync(sourceDir, destDir, { recursive: true });
1867
- const upstreamPath = join(destDir, ".upstream.json");
1868
- if (existsSync7(upstreamPath)) rmSync(upstreamPath);
1869
- const db = await getDb();
1870
- const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1871
- const version = existing ? existing.version + 1 : 1;
1872
- await db.insert(sharedSkills).values({ id, name: name || id, description, author, version }).onConflictDoUpdate({
1873
- target: sharedSkills.id,
1874
- set: {
1875
- name: name || id,
1876
- description,
1877
- author,
1878
- version,
1879
- updated_at: sql`(datetime('now'))`
1880
- }
1881
- });
1882
- const row = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1883
- if (!row) throw new Error(`Failed to upsert shared skill: ${id}`);
1884
- return row;
1885
- }
1886
- async function removeSharedSkill(id) {
1887
- const db = await getDb();
1888
- const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1889
- if (!existing) throw new Error(`Shared skill not found: ${id}`);
1890
- await db.delete(sharedSkills).where(eq3(sharedSkills.id, id));
1891
- const dir = join(sharedSkillsDir(), id);
1892
- if (existsSync7(dir)) rmSync(dir, { recursive: true });
1893
- }
1894
- function mindSkillsDir(dir) {
1895
- return resolve8(dir, "home", ".claude", "skills");
1896
- }
1897
- function readUpstream(skillDir) {
1898
- const upstreamPath = join(skillDir, ".upstream.json");
1899
- if (!existsSync7(upstreamPath)) return null;
1900
- try {
1901
- const data = JSON.parse(readFileSync4(upstreamPath, "utf-8"));
1902
- if (typeof data?.source !== "string" || typeof data?.version !== "number" || typeof data?.baseCommit !== "string") {
1903
- return null;
1904
- }
1905
- return data;
1906
- } catch {
1907
- return null;
1908
- }
1909
- }
1910
- async function installSkill(_mindName, dir, skillId) {
1911
- validateSkillId(skillId);
1912
- const shared = await getSharedSkill(skillId);
1913
- if (!shared) throw new Error(`Shared skill not found: ${skillId}`);
1914
- const sourceDir = join(sharedSkillsDir(), skillId);
1915
- if (!existsSync7(sourceDir)) throw new Error(`Shared skill files not found: ${skillId}`);
1916
- const destDir = join(mindSkillsDir(dir), skillId);
1917
- if (existsSync7(destDir)) throw new Error(`Skill already installed: ${skillId}`);
1918
- mkdirSync3(destDir, { recursive: true });
1919
- cpSync(sourceDir, destDir, { recursive: true });
1920
- await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
1921
- await gitExec(["commit", "-m", `Install shared skill: ${skillId}`], { cwd: dir });
1922
- const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
1923
- const upstream = {
1924
- source: skillId,
1925
- version: shared.version,
1926
- baseCommit: commitHash
1927
- };
1928
- writeFileSync3(join(destDir, ".upstream.json"), `${JSON.stringify(upstream, null, 2)}
1929
- `);
1930
- await gitExec(["add", join("home", ".claude", "skills", skillId, ".upstream.json")], {
1931
- cwd: dir
1932
- });
1933
- await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
1934
- }
1935
- async function uninstallSkill(_mindName, dir, skillId) {
1936
- validateSkillId(skillId);
1937
- const skillDir = join(mindSkillsDir(dir), skillId);
1938
- if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
1939
- rmSync(skillDir, { recursive: true });
1940
- await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
1941
- await gitExec(["commit", "-m", `Uninstall skill: ${skillId}`], { cwd: dir });
1942
- }
1943
- async function updateSkill(_mindName, dir, skillId) {
1944
- validateSkillId(skillId);
1945
- const skillDir = join(mindSkillsDir(dir), skillId);
1946
- if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
1947
- const upstream = readUpstream(skillDir);
1948
- if (!upstream) throw new Error(`No upstream tracking for skill: ${skillId}`);
1949
- const shared = await getSharedSkill(upstream.source);
1950
- if (!shared) throw new Error(`Shared skill no longer exists: ${upstream.source}`);
1951
- if (shared.version <= upstream.version) {
1952
- return { status: "up-to-date" };
1953
- }
1954
- const sourceDir = join(sharedSkillsDir(), upstream.source);
1955
- if (!existsSync7(sourceDir)) throw new Error(`Shared skill files missing: ${upstream.source}`);
1956
- const relSkillPath = join("home", ".claude", "skills", skillId);
1957
- const currentFiles = listFilesRecursive(skillDir).filter((f) => f !== ".upstream.json");
1958
- const newFiles = listFilesRecursive(sourceDir).filter((f) => f !== ".upstream.json");
1959
- const allFiles = [.../* @__PURE__ */ new Set([...currentFiles, ...newFiles])];
1960
- const conflictFiles = [];
1961
- const tmpBase = join(tmpdir(), `volute-merge-${process.pid}-${Date.now()}`);
1962
- mkdirSync3(tmpBase, { recursive: true });
1963
- try {
1964
- for (const file of allFiles) {
1965
- const currentPath = join(skillDir, file);
1966
- const newPath = join(sourceDir, file);
1967
- const currentExists = existsSync7(currentPath);
1968
- const newExists = existsSync7(newPath);
1969
- if (!currentExists && newExists) {
1970
- const destPath = join(skillDir, file);
1971
- mkdirSync3(join(skillDir, ...file.split("/").slice(0, -1)), { recursive: true });
1972
- cpSync(newPath, destPath);
1973
- continue;
1974
- }
1975
- if (currentExists && !newExists) {
1976
- let baseContent2 = null;
1977
- try {
1978
- baseContent2 = await gitExec(
1979
- ["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
1980
- { cwd: dir }
1981
- );
1982
- } catch {
1983
- continue;
1984
- }
1985
- const currentContent2 = readFileSync4(currentPath, "utf-8");
1986
- if (currentContent2 === baseContent2) {
1987
- rmSync(currentPath);
1988
- }
1989
- continue;
1990
- }
1991
- let baseContent;
1992
- try {
1993
- baseContent = await gitExec(
1994
- ["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
1995
- { cwd: dir }
1996
- );
1997
- } catch {
1998
- baseContent = "";
1999
- }
2000
- const currentContent = readFileSync4(currentPath, "utf-8");
2001
- const newContent = readFileSync4(newPath, "utf-8");
2002
- if (currentContent === baseContent) {
2003
- writeFileSync3(currentPath, newContent);
2004
- continue;
2005
- }
2006
- if (newContent === baseContent) {
2007
- continue;
2008
- }
2009
- const baseTmp = join(tmpBase, `${file}.base`);
2010
- const currentTmp = join(tmpBase, `${file}.current`);
2011
- const newTmp = join(tmpBase, `${file}.new`);
2012
- mkdirSync3(join(tmpBase, ...file.split("/").slice(0, -1)), { recursive: true });
2013
- writeFileSync3(baseTmp, baseContent);
2014
- writeFileSync3(currentTmp, currentContent);
2015
- writeFileSync3(newTmp, newContent);
2016
- try {
2017
- await exec("git", ["merge-file", currentTmp, baseTmp, newTmp]);
2018
- writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
2019
- } catch (e) {
2020
- const exitCode = e && typeof e === "object" && "code" in e ? e.code : null;
2021
- if (exitCode === 1) {
2022
- writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
2023
- conflictFiles.push(file);
2024
- } else {
2025
- throw e;
2026
- }
2027
- }
2028
- }
2029
- } finally {
2030
- rmSync(tmpBase, { recursive: true, force: true });
2031
- }
2032
- if (conflictFiles.length > 0) {
2033
- return { status: "conflict", conflictFiles };
2034
- }
2035
- const upstreamInfo = {
2036
- source: upstream.source,
2037
- version: shared.version,
2038
- baseCommit: upstream.baseCommit
2039
- // will update after commit
2040
- };
2041
- writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
2042
- `);
2043
- await gitExec(["add", relSkillPath], { cwd: dir });
2044
- await gitExec(["commit", "-m", `Update skill: ${skillId} (v${shared.version})`], { cwd: dir });
2045
- const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
2046
- upstreamInfo.baseCommit = commitHash;
2047
- writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
2048
- `);
2049
- await gitExec(["add", join(relSkillPath, ".upstream.json")], { cwd: dir });
2050
- await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
2051
- return { status: "updated" };
2052
- }
2053
- async function listMindSkills(dir) {
2054
- const skillsDir = mindSkillsDir(dir);
2055
- if (!existsSync7(skillsDir)) return [];
2056
- const entries = readdirSync2(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2057
- const sharedMap = /* @__PURE__ */ new Map();
2058
- for (const s of await listSharedSkills()) {
2059
- sharedMap.set(s.id, s);
2060
- }
2061
- const results = [];
2062
- for (const entry of entries) {
2063
- const skillDir = join(skillsDir, entry.name);
2064
- const skillMdPath = join(skillDir, "SKILL.md");
2065
- let name = entry.name;
2066
- let description = "";
2067
- if (existsSync7(skillMdPath)) {
2068
- const parsed = parseSkillMd(readFileSync4(skillMdPath, "utf-8"));
2069
- if (parsed.name) name = parsed.name;
2070
- description = parsed.description;
2071
- }
2072
- const upstream = readUpstream(skillDir);
2073
- let updateAvailable = false;
2074
- if (upstream) {
2075
- const shared = sharedMap.get(upstream.source);
2076
- if (shared && shared.version > upstream.version) {
2077
- updateAvailable = true;
2078
- }
2079
- }
2080
- results.push({ id: entry.name, name, description, upstream, updateAvailable });
2081
- }
2082
- return results;
2083
- }
2084
- async function publishSkill(mindName, dir, skillId) {
2085
- const skillDir = join(mindSkillsDir(dir), skillId);
2086
- if (!existsSync7(skillDir)) throw new Error(`Skill not found: ${skillId}`);
2087
- const skillMdPath = join(skillDir, "SKILL.md");
2088
- if (!existsSync7(skillMdPath)) throw new Error(`SKILL.md not found in ${skillId}`);
2089
- return importSkillFromDir(skillDir, mindName);
2090
- }
2091
- function listFilesRecursive(dir, prefix = "") {
2092
- const results = [];
2093
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2094
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2095
- if (entry.isDirectory()) {
2096
- results.push(...listFilesRecursive(join(dir, entry.name), rel));
2097
- } else {
2098
- results.push(rel);
2099
- }
2100
- }
2101
- return results;
2102
- }
2103
-
2104
- // src/web/api/mind-skills.ts
2105
- var app7 = new Hono7().get("/:name/skills", async (c) => {
1949
+ var app8 = new Hono8().get("/:name/skills", async (c) => {
2106
1950
  const name = c.req.param("name");
2107
1951
  const entry = findMind(name);
2108
1952
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -2177,39 +2021,39 @@ var app7 = new Hono7().get("/:name/skills", async (c) => {
2177
2021
  }
2178
2022
  return c.json({ ok: true });
2179
2023
  });
2180
- var mind_skills_default = app7;
2024
+ var mind_skills_default = app8;
2181
2025
 
2182
2026
  // src/web/api/minds.ts
2183
2027
  import {
2184
2028
  cpSync as cpSync2,
2185
- existsSync as existsSync8,
2186
- mkdirSync as mkdirSync5,
2029
+ existsSync as existsSync10,
2030
+ mkdirSync as mkdirSync7,
2187
2031
  readdirSync as readdirSync4,
2188
- readFileSync as readFileSync7,
2032
+ readFileSync as readFileSync9,
2189
2033
  rmSync as rmSync2,
2190
- statSync,
2191
- writeFileSync as writeFileSync6
2034
+ statSync as statSync2,
2035
+ writeFileSync as writeFileSync8
2192
2036
  } from "fs";
2193
- import { join as join2, resolve as resolve11 } from "path";
2037
+ import { join as join2, resolve as resolve13 } from "path";
2194
2038
  import { zValidator as zValidator3 } from "@hono/zod-validator";
2195
- import { and as and3, desc as desc2, eq as eq5, sql as sql3 } from "drizzle-orm";
2196
- import { Hono as Hono8 } from "hono";
2039
+ import { and as and3, desc as desc2, eq as eq4, sql as sql2 } from "drizzle-orm";
2040
+ import { Hono as Hono9 } from "hono";
2197
2041
  import { z as z3 } from "zod";
2198
2042
 
2199
2043
  // src/lib/consolidate.ts
2200
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2201
- 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";
2202
2046
  async function consolidateMemory(mindDir2) {
2203
- const soulPath = resolve9(mindDir2, "home/SOUL.md");
2204
- const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
2205
- const memoryDir = resolve9(mindDir2, "home/memory");
2206
- const soul = readFileSync5(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");
2207
2051
  const logs = [];
2208
2052
  try {
2209
- const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2053
+ const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2210
2054
  for (const filename of files) {
2211
2055
  const date = filename.replace(".md", "");
2212
- const content2 = readFileSync5(resolve9(memoryDir, filename), "utf-8").trim();
2056
+ const content2 = readFileSync6(resolve10(memoryDir, filename), "utf-8").trim();
2213
2057
  if (content2) {
2214
2058
  logs.push(`### ${date}
2215
2059
 
@@ -2259,7 +2103,7 @@ ${content2}`);
2259
2103
  const data = await res.json();
2260
2104
  const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
2261
2105
  if (content) {
2262
- writeFileSync4(memoryPath, `${content}
2106
+ writeFileSync5(memoryPath, `${content}
2263
2107
  `);
2264
2108
  console.log("MEMORY.md created successfully.");
2265
2109
  } else {
@@ -2269,7 +2113,7 @@ ${content2}`);
2269
2113
 
2270
2114
  // src/lib/conversations.ts
2271
2115
  import { randomUUID } from "crypto";
2272
- import { and as and2, desc, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
2116
+ import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
2273
2117
 
2274
2118
  // src/lib/conversation-events.ts
2275
2119
  var subscribers = /* @__PURE__ */ new Map();
@@ -2339,7 +2183,7 @@ async function createConversation(mindName, channel, opts) {
2339
2183
  }
2340
2184
  async function getConversation(id) {
2341
2185
  const db = await getDb();
2342
- const row = await db.select().from(conversations).where(eq4(conversations.id, id)).get();
2186
+ const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
2343
2187
  return row ?? null;
2344
2188
  }
2345
2189
  async function addParticipant(conversationId, userId, role = "member") {
@@ -2354,8 +2198,8 @@ async function removeParticipant(conversationId, userId) {
2354
2198
  const db = await getDb();
2355
2199
  await db.delete(conversationParticipants).where(
2356
2200
  and2(
2357
- eq4(conversationParticipants.conversation_id, conversationId),
2358
- eq4(conversationParticipants.user_id, userId)
2201
+ eq3(conversationParticipants.conversation_id, conversationId),
2202
+ eq3(conversationParticipants.user_id, userId)
2359
2203
  )
2360
2204
  );
2361
2205
  }
@@ -2366,22 +2210,22 @@ async function getParticipants(conversationId) {
2366
2210
  username: users.username,
2367
2211
  userType: users.user_type,
2368
2212
  role: conversationParticipants.role
2369
- }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
2213
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
2370
2214
  return rows;
2371
2215
  }
2372
2216
  async function isParticipant(conversationId, userId) {
2373
2217
  const db = await getDb();
2374
2218
  const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2375
2219
  and2(
2376
- eq4(conversationParticipants.conversation_id, conversationId),
2377
- eq4(conversationParticipants.user_id, userId)
2220
+ eq3(conversationParticipants.conversation_id, conversationId),
2221
+ eq3(conversationParticipants.user_id, userId)
2378
2222
  )
2379
2223
  ).get();
2380
2224
  return row != null;
2381
2225
  }
2382
2226
  async function listConversationsForUser(userId) {
2383
2227
  const db = await getDb();
2384
- const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
2228
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
2385
2229
  if (participantRows.length === 0) return [];
2386
2230
  const convIds = participantRows.map((r) => r.conversation_id);
2387
2231
  return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
@@ -2389,7 +2233,7 @@ async function listConversationsForUser(userId) {
2389
2233
  async function isParticipantOrOwner(conversationId, userId) {
2390
2234
  if (await isParticipant(conversationId, userId)) return true;
2391
2235
  const db = await getDb();
2392
- const row = await db.select().from(conversations).where(and2(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
2236
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
2393
2237
  return row != null;
2394
2238
  }
2395
2239
  async function deleteConversationForUser(id, userId) {
@@ -2401,12 +2245,12 @@ async function addMessage(conversationId, role, senderName, content) {
2401
2245
  const db = await getDb();
2402
2246
  const serialized = JSON.stringify(content);
2403
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 });
2404
- await db.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
2248
+ await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
2405
2249
  if (role === "user") {
2406
2250
  const firstText = content.find((b) => b.type === "text");
2407
2251
  const title = firstText ? firstText.text.slice(0, 80) : "";
2408
2252
  if (title) {
2409
- await db.update(conversations).set({ title }).where(and2(eq4(conversations.id, conversationId), isNull(conversations.title)));
2253
+ await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
2410
2254
  }
2411
2255
  }
2412
2256
  const msg = {
@@ -2429,7 +2273,7 @@ async function addMessage(conversationId, role, senderName, content) {
2429
2273
  }
2430
2274
  async function getMessages(conversationId) {
2431
2275
  const db = await getDb();
2432
- const rows = await db.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2276
+ const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2433
2277
  return rows.map((row) => {
2434
2278
  let content;
2435
2279
  try {
@@ -2452,7 +2296,7 @@ async function listConversationsWithParticipants(userId) {
2452
2296
  username: users.username,
2453
2297
  userType: users.user_type,
2454
2298
  role: conversationParticipants.role
2455
- }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2299
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2456
2300
  const byConv = /* @__PURE__ */ new Map();
2457
2301
  for (const r of rows) {
2458
2302
  let arr = byConv.get(r.conversationId);
@@ -2469,7 +2313,7 @@ async function listConversationsWithParticipants(userId) {
2469
2313
  }
2470
2314
  const lastMsgIds = await db.select({
2471
2315
  conversationId: messages.conversation_id,
2472
- maxId: sql2`MAX(${messages.id})`
2316
+ maxId: sql`MAX(${messages.id})`
2473
2317
  }).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
2474
2318
  const byLastMsg = /* @__PURE__ */ new Map();
2475
2319
  if (lastMsgIds.length > 0) {
@@ -2505,9 +2349,9 @@ async function listConversationsWithParticipants(userId) {
2505
2349
  }
2506
2350
  async function findDMConversation(mindName, participantIds) {
2507
2351
  const db = await getDb();
2508
- const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq4(conversations.mind_name, mindName), eq4(conversations.type, "dm"))).all();
2352
+ const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq3(conversations.mind_name, mindName), eq3(conversations.type, "dm"))).all();
2509
2353
  for (const conv of mindConvs) {
2510
- const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(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();
2511
2355
  if (rows.length !== 2) continue;
2512
2356
  const ids = new Set(rows.map((r) => r.user_id));
2513
2357
  if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
@@ -2518,7 +2362,7 @@ async function findDMConversation(mindName, participantIds) {
2518
2362
  }
2519
2363
  async function deleteConversation(id) {
2520
2364
  const db = await getDb();
2521
- await db.delete(conversations).where(eq4(conversations.id, id));
2365
+ await db.delete(conversations).where(eq3(conversations.id, id));
2522
2366
  }
2523
2367
  async function createChannel(name, creatorId) {
2524
2368
  const participantIds = creatorId ? [creatorId] : [];
@@ -2531,12 +2375,12 @@ async function createChannel(name, creatorId) {
2531
2375
  }
2532
2376
  async function getChannelByName(name) {
2533
2377
  const db = await getDb();
2534
- const row = await db.select().from(conversations).where(and2(eq4(conversations.name, name), eq4(conversations.type, "channel"))).get();
2378
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.name, name), eq3(conversations.type, "channel"))).get();
2535
2379
  return row ?? null;
2536
2380
  }
2537
2381
  async function listChannels() {
2538
2382
  const db = await getDb();
2539
- return await db.select().from(conversations).where(eq4(conversations.type, "channel")).orderBy(conversations.name).all();
2383
+ return await db.select().from(conversations).where(eq3(conversations.type, "channel")).orderBy(conversations.name).all();
2540
2384
  }
2541
2385
  async function joinChannel(conversationId, userId) {
2542
2386
  if (await isParticipant(conversationId, userId)) return;
@@ -2548,11 +2392,11 @@ async function leaveChannel(conversationId, userId) {
2548
2392
 
2549
2393
  // src/lib/convert-session.ts
2550
2394
  import { randomUUID as randomUUID2 } from "crypto";
2551
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2395
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
2552
2396
  import { homedir } from "os";
2553
- import { resolve as resolve10 } from "path";
2397
+ import { resolve as resolve11 } from "path";
2554
2398
  function convertSession(opts) {
2555
- const lines = readFileSync6(opts.sessionPath, "utf-8").trim().split("\n");
2399
+ const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
2556
2400
  const sessionId = randomUUID2();
2557
2401
  const idMap = /* @__PURE__ */ new Map();
2558
2402
  const messages2 = [];
@@ -2666,10 +2510,10 @@ function convertSession(opts) {
2666
2510
  }
2667
2511
  }
2668
2512
  const projectId = opts.projectDir.replace(/\//g, "-");
2669
- const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
2670
- mkdirSync4(sdkDir, { recursive: true });
2671
- const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
2672
- writeFileSync5(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")}
2673
2517
  `);
2674
2518
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
2675
2519
  return sessionId;
@@ -2748,121 +2592,112 @@ function publish2(mind, event) {
2748
2592
  }
2749
2593
  }
2750
2594
 
2751
- // src/lib/typing.ts
2752
- var DEFAULT_TTL_MS = 1e4;
2753
- var SWEEP_INTERVAL_MS = 5e3;
2754
- var TypingMap = class {
2755
- channels = /* @__PURE__ */ new Map();
2756
- sweepTimer;
2757
- constructor() {
2758
- this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
2759
- this.sweepTimer.unref();
2760
- }
2761
- set(channel, sender, opts) {
2762
- const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
2763
- let senders = this.channels.get(channel);
2764
- if (!senders) {
2765
- senders = /* @__PURE__ */ new Map();
2766
- this.channels.set(channel, senders);
2767
- }
2768
- senders.set(sender, { expiresAt });
2769
- }
2770
- delete(channel, sender) {
2771
- const senders = this.channels.get(channel);
2772
- if (senders) {
2773
- senders.delete(sender);
2774
- if (senders.size === 0) {
2775
- this.channels.delete(channel);
2776
- }
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);
2615
+ }
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);
2647
+ }
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));
2777
2658
  }
2778
2659
  }
2779
- /** Remove a sender from all channels (e.g. when a mind finishes processing). */
2780
- deleteSender(sender) {
2781
- for (const [channel, senders] of this.channels) {
2782
- senders.delete(sender);
2783
- if (senders.size === 0) {
2784
- this.channels.delete(channel);
2785
- }
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));
2786
2665
  }
2787
2666
  }
2788
- get(channel) {
2789
- const senders = this.channels.get(channel);
2790
- if (!senders) return [];
2791
- const now = Date.now();
2792
- const result = [];
2793
- for (const [sender, entry] of senders) {
2794
- if (entry.expiresAt > now) {
2795
- result.push(sender);
2796
- }
2797
- }
2798
- return result;
2799
- }
2800
- dispose() {
2801
- clearInterval(this.sweepTimer);
2802
- this.channels.clear();
2803
- if (instance5 === this) instance5 = void 0;
2804
- }
2805
- sweep() {
2806
- const now = Date.now();
2807
- for (const [channel, senders] of this.channels) {
2808
- for (const [sender, entry] of senders) {
2809
- if (entry.expiresAt <= now) {
2810
- senders.delete(sender);
2811
- }
2812
- }
2813
- if (senders.size === 0) {
2814
- 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));
2815
2693
  }
2816
2694
  }
2817
2695
  }
2818
- };
2819
- var instance5;
2820
- function getTypingMap() {
2821
- if (!instance5) {
2822
- instance5 = new TypingMap();
2823
- }
2824
- return instance5;
2696
+ walk(dir);
2697
+ return results;
2825
2698
  }
2826
2699
 
2827
2700
  // src/web/api/minds.ts
2828
- async function startMindFull(name, baseName, variantName) {
2829
- await getMindManager().startMind(name);
2830
- if (variantName) return;
2831
- if (findMind(baseName)?.stage === "seed") return;
2832
- const dir = mindDir(baseName);
2833
- const entry = findMind(baseName);
2834
- await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
2835
- getScheduler().loadSchedules(baseName);
2836
- ensureMailAddress(baseName).catch(
2837
- (err) => console.error(`[mail] failed to ensure address for ${baseName}:`, err)
2838
- );
2839
- const config = readVoluteConfig(dir);
2840
- if (config?.tokenBudget) {
2841
- getTokenBudget().setBudget(
2842
- baseName,
2843
- config.tokenBudget,
2844
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
2845
- );
2846
- }
2847
- }
2848
- function extractTextContent(content) {
2849
- if (typeof content === "string") return content;
2850
- if (Array.isArray(content)) {
2851
- return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
2852
- }
2853
- return JSON.stringify(content);
2854
- }
2855
- function getDaemonPort() {
2856
- try {
2857
- const data = JSON.parse(readFileSync7(resolve11(voluteHome(), "daemon.json"), "utf-8"));
2858
- return data.port;
2859
- } catch (err) {
2860
- if (err?.code !== "ENOENT") {
2861
- console.error("[daemon] failed to read daemon.json:", err);
2862
- }
2863
- return void 0;
2864
- }
2865
- }
2866
2701
  async function getMindStatus(name, port) {
2867
2702
  const manager = getMindManager();
2868
2703
  let status = "stopped";
@@ -2905,7 +2740,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
2905
2740
  await gitExec(["commit", "-m", "initial commit"], opts);
2906
2741
  }
2907
2742
  async function updateTemplateBranch(projectRoot, template, mindName) {
2908
- const tempWorktree = resolve11(projectRoot, ".variants", "_template_update");
2743
+ const tempWorktree = resolve13(projectRoot, ".variants", "_template_update");
2909
2744
  let branchExists = false;
2910
2745
  try {
2911
2746
  await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
@@ -2916,7 +2751,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2916
2751
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
2917
2752
  } catch {
2918
2753
  }
2919
- if (existsSync8(tempWorktree)) {
2754
+ if (existsSync10(tempWorktree)) {
2920
2755
  rmSync2(tempWorktree, { recursive: true, force: true });
2921
2756
  }
2922
2757
  const templatesRoot = findTemplatesRoot();
@@ -2937,8 +2772,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2937
2772
  });
2938
2773
  }
2939
2774
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
2940
- const initDir = resolve11(tempWorktree, ".init");
2941
- if (existsSync8(initDir)) {
2775
+ const initDir = resolve13(tempWorktree, ".init");
2776
+ if (existsSync10(initDir)) {
2942
2777
  rmSync2(initDir, { recursive: true, force: true });
2943
2778
  }
2944
2779
  await gitExec(["add", "-A"], { cwd: tempWorktree });
@@ -2952,7 +2787,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2952
2787
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
2953
2788
  } catch {
2954
2789
  }
2955
- if (existsSync8(tempWorktree)) {
2790
+ if (existsSync10(tempWorktree)) {
2956
2791
  rmSync2(tempWorktree, { recursive: true, force: true });
2957
2792
  }
2958
2793
  rmSync2(composedDir, { recursive: true, force: true });
@@ -2978,20 +2813,125 @@ async function mergeTemplateBranch(worktreeDir) {
2978
2813
  async function npmInstallAsMind(cwd, mindName) {
2979
2814
  if (isIsolationEnabled()) {
2980
2815
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
2981
- 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") } });
2982
2817
  } else {
2983
2818
  await exec("npm", ["install"], { cwd });
2984
2819
  }
2985
2820
  }
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
+ }
2986
2925
  var createMindSchema = z3.object({
2987
2926
  name: z3.string(),
2988
2927
  template: z3.string().optional(),
2989
2928
  stage: z3.enum(["seed", "sprouted"]).optional(),
2990
2929
  description: z3.string().optional(),
2991
2930
  model: z3.string().optional(),
2992
- seedSoul: z3.string().optional()
2931
+ seedSoul: z3.string().optional(),
2932
+ skills: z3.array(z3.string()).optional()
2993
2933
  });
2994
- var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
2934
+ var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
2995
2935
  const body = c.req.valid("json");
2996
2936
  const { name, template = "claude" } = body;
2997
2937
  const nameErr = validateMindName(name);
@@ -2999,28 +2939,29 @@ var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSch
2999
2939
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
3000
2940
  ensureVoluteHome();
3001
2941
  const dest = mindDir(name);
3002
- 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);
3003
2943
  const templatesRoot = findTemplatesRoot();
3004
2944
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
3005
2945
  try {
3006
2946
  copyTemplateToDir(composedDir, dest, name, manifest);
3007
2947
  applyInitFiles(dest);
2948
+ const { publicKeyPem } = generateIdentity(dest);
3008
2949
  if (body.model) {
3009
- const configPath = resolve11(dest, "home/.config/config.json");
3010
- const existing = existsSync8(configPath) ? JSON.parse(readFileSync7(configPath, "utf-8")) : {};
2950
+ const configPath = resolve13(dest, "home/.config/config.json");
2951
+ const existing = existsSync10(configPath) ? JSON.parse(readFileSync9(configPath, "utf-8")) : {};
3011
2952
  existing.model = body.model;
3012
- writeFileSync6(configPath, `${JSON.stringify(existing, null, 2)}
2953
+ writeFileSync8(configPath, `${JSON.stringify(existing, null, 2)}
3013
2954
  `);
3014
2955
  }
3015
2956
  const mindPrompts = await getMindPromptDefaults();
3016
- writeFileSync6(
3017
- resolve11(dest, "home/.config/prompts.json"),
2957
+ writeFileSync8(
2958
+ resolve13(dest, "home/.config/prompts.json"),
3018
2959
  `${JSON.stringify(mindPrompts, null, 2)}
3019
2960
  `
3020
2961
  );
3021
2962
  const port = nextPort();
3022
- addMind(name, port, body.stage);
3023
- const homeDir = resolve11(dest, "home");
2963
+ addMind(name, port, body.stage, template);
2964
+ const homeDir = resolve13(dest, "home");
3024
2965
  ensureVoluteGroup();
3025
2966
  createMindUser(name, homeDir);
3026
2967
  chownMindDir(dest, name);
@@ -3031,10 +2972,15 @@ var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSch
3031
2972
  await gitExec(["init"], { cwd: dest, mindName: name, env });
3032
2973
  await initTemplateBranch(dest, composedDir, manifest, name, env);
3033
2974
  } catch (err) {
3034
- console.error(`[daemon] git setup failed for ${name}:`, err);
3035
- rmSync2(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 });
3036
2977
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
3037
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
+ }
3038
2984
  chownMindDir(dest, name);
3039
2985
  if (body.stage === "seed") {
3040
2986
  const descLine = body.description ? `
@@ -3042,33 +2988,42 @@ The human who planted you described you as: "${body.description}"
3042
2988
  ` : "";
3043
2989
  const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
3044
2990
  const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
3045
- writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
3046
- const skillsDir = resolve11(dest, manifest.skillsDir);
3047
- for (const skill of ["volute-mind", "memory", "sessions"]) {
3048
- const skillPath = resolve11(skillsDir, skill);
3049
- if (existsSync8(skillPath)) rmSync2(skillPath, { recursive: true, force: true });
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}`);
3050
3001
  }
3051
3002
  }
3052
3003
  if (body.stage !== "seed") {
3053
3004
  const customSoul = await getPromptIfCustom("default_soul");
3054
3005
  if (customSoul) {
3055
- writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3006
+ writeFileSync8(resolve13(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3056
3007
  }
3057
3008
  const customMemory = await getPromptIfCustom("default_memory");
3058
3009
  if (customMemory) {
3059
- writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
3010
+ writeFileSync8(resolve13(dest, "home/MEMORY.md"), customMemory);
3060
3011
  }
3061
3012
  }
3013
+ publishPublicKey(name, publicKeyPem).catch(
3014
+ (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
3015
+ );
3062
3016
  return c.json({
3063
3017
  ok: true,
3064
3018
  name,
3065
3019
  port,
3066
3020
  stage: body.stage ?? "sprouted",
3067
3021
  message: `Created mind: ${name} (port ${port})`,
3068
- ...gitWarning && { warning: gitWarning }
3022
+ ...gitWarning && { warning: gitWarning },
3023
+ ...skillWarnings.length > 0 && { skillWarnings }
3069
3024
  });
3070
3025
  } catch (err) {
3071
- if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
3026
+ if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
3072
3027
  try {
3073
3028
  removeMind(name);
3074
3029
  } catch {
@@ -3084,14 +3039,17 @@ The human who planted you described you as: "${body.description}"
3084
3039
  } catch {
3085
3040
  return c.json({ error: "Invalid JSON" }, 400);
3086
3041
  }
3042
+ if (body.archivePath && body.manifest) {
3043
+ return importFromArchive(c, body.archivePath, body.name, body.manifest);
3044
+ }
3087
3045
  const wsDir = body.workspacePath;
3088
- 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"))) {
3089
3047
  return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
3090
3048
  }
3091
- const soul = readFileSync7(resolve11(wsDir, "SOUL.md"), "utf-8");
3092
- const identity = readFileSync7(resolve11(wsDir, "IDENTITY.md"), "utf-8");
3093
- const userPath = resolve11(wsDir, "USER.md");
3094
- const user = existsSync8(userPath) ? readFileSync7(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") : "";
3095
3053
  const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
3096
3054
  const template = body.template ?? "claude";
3097
3055
  const nameErr = validateMindName(name);
@@ -3111,38 +3069,39 @@ ${user.trimEnd()}
3111
3069
  ` : "";
3112
3070
  ensureVoluteHome();
3113
3071
  const dest = mindDir(name);
3114
- 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);
3115
3073
  const templatesRoot = findTemplatesRoot();
3116
3074
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
3117
3075
  try {
3118
3076
  copyTemplateToDir(composedDir, dest, name, manifest);
3119
3077
  applyInitFiles(dest);
3120
- writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
3121
- const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
3122
- 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);
3123
3082
  if (hasMemory) {
3124
- const existingMemory = readFileSync7(wsMemoryPath, "utf-8");
3125
- writeFileSync6(
3126
- resolve11(dest, "home/MEMORY.md"),
3083
+ const existingMemory = readFileSync9(wsMemoryPath, "utf-8");
3084
+ writeFileSync8(
3085
+ resolve13(dest, "home/MEMORY.md"),
3127
3086
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
3128
3087
  );
3129
3088
  } else if (user) {
3130
- writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
3089
+ writeFileSync8(resolve13(dest, "home/MEMORY.md"), `${user.trimEnd()}
3131
3090
  `);
3132
3091
  }
3133
- const wsMemoryDir = resolve11(wsDir, "memory");
3092
+ const wsMemoryDir = resolve13(wsDir, "memory");
3134
3093
  let dailyLogCount = 0;
3135
- if (existsSync8(wsMemoryDir)) {
3136
- const destMemoryDir = resolve11(dest, "home/memory");
3094
+ if (existsSync10(wsMemoryDir)) {
3095
+ const destMemoryDir = resolve13(dest, "home/memory");
3137
3096
  const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
3138
3097
  for (const file of files) {
3139
- cpSync2(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
3098
+ cpSync2(resolve13(wsMemoryDir, file), resolve13(destMemoryDir, file));
3140
3099
  }
3141
3100
  dailyLogCount = files.length;
3142
3101
  }
3143
3102
  const port = nextPort();
3144
- addMind(name, port);
3145
- const homeDir = resolve11(dest, "home");
3103
+ addMind(name, port, void 0, template);
3104
+ const homeDir = resolve13(dest, "home");
3146
3105
  ensureVoluteGroup();
3147
3106
  createMindUser(name, homeDir);
3148
3107
  chownMindDir(dest, name);
@@ -3150,26 +3109,34 @@ ${user.trimEnd()}
3150
3109
  if (!hasMemory && dailyLogCount > 0) {
3151
3110
  await consolidateMemory(dest);
3152
3111
  }
3153
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
3112
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
3154
3113
  await gitExec(["init"], { cwd: dest, mindName: name, env });
3155
3114
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
3156
3115
  await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
3157
- const sessionFile = body.sessionPath ? resolve11(body.sessionPath) : findOpenClawSession(wsDir);
3158
- if (sessionFile && existsSync8(sessionFile)) {
3116
+ const sessionFile = body.sessionPath ? resolve13(body.sessionPath) : findOpenClawSession(wsDir);
3117
+ if (sessionFile && existsSync10(sessionFile)) {
3159
3118
  if (template === "pi") {
3160
3119
  importPiSession(sessionFile, dest);
3161
3120
  } else if (template === "claude") {
3162
3121
  const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
3163
- const voluteDir = resolve11(dest, ".volute");
3164
- mkdirSync5(voluteDir, { recursive: true });
3165
- writeFileSync6(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 }));
3166
3125
  }
3167
3126
  }
3168
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
+ }
3169
3133
  chownMindDir(dest, name);
3134
+ publishPublicKey(name, importPublicKey).catch(
3135
+ (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
3136
+ );
3170
3137
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
3171
3138
  } catch (err) {
3172
- if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
3139
+ if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
3173
3140
  try {
3174
3141
  removeMind(name);
3175
3142
  } catch {
@@ -3185,7 +3152,7 @@ ${user.trimEnd()}
3185
3152
  const db = await getDb();
3186
3153
  const lastActiveRows = await db.select({
3187
3154
  mind: mindHistory.mind,
3188
- lastActiveAt: sql3`MAX(${mindHistory.created_at})`
3155
+ lastActiveAt: sql2`MAX(${mindHistory.created_at})`
3189
3156
  }).from(mindHistory).groupBy(mindHistory.mind);
3190
3157
  lastActiveMap = new Map(lastActiveRows.map((r) => [r.mind, r.lastActiveAt]));
3191
3158
  } catch {
@@ -3193,7 +3160,7 @@ ${user.trimEnd()}
3193
3160
  const minds = await Promise.all(
3194
3161
  entries.map(async (entry) => {
3195
3162
  const { status, channels } = await getMindStatus(entry.name, entry.port);
3196
- const hasPages = existsSync8(resolve11(mindDir(entry.name), "home", "pages"));
3163
+ const hasPages = existsSync10(resolve13(mindDir(entry.name), "home", "pages"));
3197
3164
  return {
3198
3165
  ...entry,
3199
3166
  status,
@@ -3208,8 +3175,8 @@ ${user.trimEnd()}
3208
3175
  const entries = readRegistry();
3209
3176
  const pages = [];
3210
3177
  for (const entry of entries) {
3211
- const pagesDir = resolve11(mindDir(entry.name), "home", "pages");
3212
- if (!existsSync8(pagesDir)) continue;
3178
+ const pagesDir = resolve13(mindDir(entry.name), "home", "pages");
3179
+ if (!existsSync10(pagesDir)) continue;
3213
3180
  let items;
3214
3181
  try {
3215
3182
  items = readdirSync4(pagesDir);
@@ -3218,9 +3185,9 @@ ${user.trimEnd()}
3218
3185
  continue;
3219
3186
  }
3220
3187
  for (const item of items) {
3221
- const fullPath = resolve11(pagesDir, item);
3188
+ const fullPath = resolve13(pagesDir, item);
3222
3189
  try {
3223
- const s = statSync(fullPath);
3190
+ const s = statSync2(fullPath);
3224
3191
  if (s.isFile()) {
3225
3192
  pages.push({
3226
3193
  mind: entry.name,
@@ -3229,9 +3196,9 @@ ${user.trimEnd()}
3229
3196
  url: `/pages/${entry.name}/${item}`
3230
3197
  });
3231
3198
  } else if (s.isDirectory()) {
3232
- const indexPath = resolve11(fullPath, "index.html");
3233
- if (existsSync8(indexPath)) {
3234
- const indexStat = statSync(indexPath);
3199
+ const indexPath = resolve13(fullPath, "index.html");
3200
+ if (existsSync10(indexPath)) {
3201
+ const indexStat = statSync2(indexPath);
3235
3202
  pages.push({
3236
3203
  mind: entry.name,
3237
3204
  file: join2(item, "index.html"),
@@ -3255,7 +3222,7 @@ ${user.trimEnd()}
3255
3222
  const name = c.req.param("name");
3256
3223
  const entry = findMind(name);
3257
3224
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3258
- 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);
3259
3226
  const { status, channels } = await getMindStatus(name, entry.port);
3260
3227
  const variants = readVariants(name);
3261
3228
  const manager = getMindManager();
@@ -3270,7 +3237,7 @@ ${user.trimEnd()}
3270
3237
  return { name: v.name, port: v.port, status: variantStatus };
3271
3238
  })
3272
3239
  );
3273
- const hasPages = existsSync8(resolve11(mindDir(name), "home", "pages"));
3240
+ const hasPages = existsSync10(resolve13(mindDir(name), "home", "pages"));
3274
3241
  return c.json({ ...entry, status, channels, variants: variantStatuses, hasPages });
3275
3242
  }).post("/:name/start", requireAdmin, async (c) => {
3276
3243
  const name = c.req.param("name");
@@ -3282,13 +3249,13 @@ ${user.trimEnd()}
3282
3249
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
3283
3250
  } else {
3284
3251
  const dir = mindDir(baseName);
3285
- if (!existsSync8(dir)) return c.json({ error: "Mind directory missing" }, 404);
3252
+ if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
3286
3253
  }
3287
3254
  if (getMindManager().isRunning(name)) {
3288
3255
  return c.json({ error: "Mind already running" }, 409);
3289
3256
  }
3290
3257
  try {
3291
- await startMindFull(name, baseName, variantName);
3258
+ await startMindFull(name);
3292
3259
  return c.json({ ok: true });
3293
3260
  } catch (err) {
3294
3261
  return c.json({ error: err instanceof Error ? err.message : "Failed to start mind" }, 500);
@@ -3303,7 +3270,7 @@ ${user.trimEnd()}
3303
3270
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
3304
3271
  } else {
3305
3272
  const dir = mindDir(baseName);
3306
- if (!existsSync8(dir)) return c.json({ error: "Mind directory missing" }, 404);
3273
+ if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
3307
3274
  }
3308
3275
  let context;
3309
3276
  const contentType = c.req.header("content-type");
@@ -3312,17 +3279,13 @@ ${user.trimEnd()}
3312
3279
  const body = await c.req.json();
3313
3280
  if (body?.context) context = body.context;
3314
3281
  } catch (err) {
3315
- 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));
3316
3283
  }
3317
3284
  }
3318
3285
  const manager = getMindManager();
3319
3286
  try {
3320
3287
  if (manager.isRunning(name)) {
3321
- if (!variantName) {
3322
- await getConnectorManager().stopConnectors(baseName);
3323
- getTokenBudget().removeBudget(baseName);
3324
- }
3325
- await manager.stopMind(name);
3288
+ await stopMindFull(name);
3326
3289
  }
3327
3290
  if (context?.type === "merge" && context.name && !variantName) {
3328
3291
  const mergeVariantName = String(context.name);
@@ -3330,11 +3293,11 @@ ${user.trimEnd()}
3330
3293
  if (branchErr) {
3331
3294
  return c.json({ error: `Invalid variant name: ${branchErr}` }, 400);
3332
3295
  }
3333
- console.error(`[daemon] merging variant for ${baseName}: ${mergeVariantName}`);
3296
+ logger_default.error(`merging variant for ${baseName}: ${mergeVariantName}`);
3334
3297
  const variant = findVariant(baseName, mergeVariantName);
3335
3298
  if (variant) {
3336
3299
  const projectRoot = mindDir(baseName);
3337
- if (existsSync8(variant.path)) {
3300
+ if (existsSync10(variant.path)) {
3338
3301
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
3339
3302
  if (status) {
3340
3303
  try {
@@ -3343,9 +3306,9 @@ ${user.trimEnd()}
3343
3306
  cwd: variant.path
3344
3307
  });
3345
3308
  } catch (e) {
3346
- console.error(
3347
- `[daemon] failed to auto-commit variant worktree for ${baseName}:`,
3348
- e
3309
+ logger_default.error(
3310
+ `failed to auto-commit variant worktree for ${baseName}`,
3311
+ logger_default.errorData(e)
3349
3312
  );
3350
3313
  }
3351
3314
  }
@@ -3358,11 +3321,11 @@ ${user.trimEnd()}
3358
3321
  cwd: projectRoot
3359
3322
  });
3360
3323
  } catch (e) {
3361
- 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));
3362
3325
  }
3363
3326
  }
3364
3327
  await gitExec(["merge", variant.branch], { cwd: projectRoot });
3365
- if (existsSync8(variant.path)) {
3328
+ if (existsSync10(variant.path)) {
3366
3329
  try {
3367
3330
  await gitExec(["worktree", "remove", "--force", variant.path], {
3368
3331
  cwd: projectRoot
@@ -3379,7 +3342,7 @@ ${user.trimEnd()}
3379
3342
  try {
3380
3343
  await npmInstallAsMind(projectRoot, baseName);
3381
3344
  } catch (e) {
3382
- 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));
3383
3346
  }
3384
3347
  }
3385
3348
  }
@@ -3389,17 +3352,17 @@ ${user.trimEnd()}
3389
3352
  if (context?.type === "sprouted" && !variantName) {
3390
3353
  try {
3391
3354
  const db = await getDb();
3392
- const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq5(conversations.mind_name, baseName)).all();
3355
+ const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq4(conversations.mind_name, baseName)).all();
3393
3356
  for (const conv of activeConvs) {
3394
3357
  await addMessage(conv.id, "assistant", "system", [
3395
3358
  { type: "text", text: "[seed has sprouted]" }
3396
3359
  ]);
3397
3360
  }
3398
3361
  } catch (err) {
3399
- 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));
3400
3363
  }
3401
3364
  }
3402
- await startMindFull(name, baseName, variantName);
3365
+ await startMindFull(name);
3403
3366
  return c.json({ ok: true });
3404
3367
  } catch (err) {
3405
3368
  return c.json({ error: err instanceof Error ? err.message : "Failed to restart mind" }, 500);
@@ -3418,12 +3381,7 @@ ${user.trimEnd()}
3418
3381
  return c.json({ error: "Mind is not running" }, 409);
3419
3382
  }
3420
3383
  try {
3421
- if (!variantName) {
3422
- await getConnectorManager().stopConnectors(baseName);
3423
- getScheduler().unloadSchedules(baseName);
3424
- getTokenBudget().removeBudget(baseName);
3425
- }
3426
- await manager.stopMind(name);
3384
+ await stopMindFull(name);
3427
3385
  return c.json({ ok: true });
3428
3386
  } catch (err) {
3429
3387
  return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
@@ -3445,18 +3403,21 @@ ${user.trimEnd()}
3445
3403
  const force = c.req.query("force") === "true";
3446
3404
  const manager = getMindManager();
3447
3405
  if (manager.isRunning(name)) {
3448
- await getConnectorManager().stopConnectors(name);
3449
- getTokenBudget().removeBudget(name);
3450
- await manager.stopMind(name);
3406
+ await stopMindFull(name);
3451
3407
  }
3452
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
+ }
3453
3414
  removeMind(name);
3454
3415
  await deleteMindUser2(name);
3455
3416
  const state = stateDir(name);
3456
- if (existsSync8(state)) {
3417
+ if (existsSync10(state)) {
3457
3418
  rmSync2(state, { recursive: true, force: true });
3458
3419
  }
3459
- if (force && existsSync8(dir)) {
3420
+ if (force && existsSync10(dir)) {
3460
3421
  rmSync2(dir, { recursive: true, force: true });
3461
3422
  deleteMindUser(name);
3462
3423
  }
@@ -3466,17 +3427,17 @@ ${user.trimEnd()}
3466
3427
  const entry = findMind(mindName);
3467
3428
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3468
3429
  const dir = mindDir(mindName);
3469
- if (!existsSync8(dir)) return c.json({ error: "Mind directory missing" }, 404);
3430
+ if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
3470
3431
  let body = {};
3471
3432
  try {
3472
3433
  body = await c.req.json();
3473
3434
  } catch {
3474
3435
  }
3475
- const template = body.template ?? "claude";
3436
+ const template = body.template ?? entry.template ?? "claude";
3476
3437
  const UPGRADE_VARIANT = "upgrade";
3477
3438
  if (body.continue) {
3478
- const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
3479
- if (!existsSync8(worktreeDir2)) {
3439
+ const worktreeDir2 = resolve13(dir, ".variants", UPGRADE_VARIANT);
3440
+ if (!existsSync10(worktreeDir2)) {
3480
3441
  return c.json({ error: "No upgrade in progress" }, 400);
3481
3442
  }
3482
3443
  const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
@@ -3525,9 +3486,9 @@ ${user.trimEnd()}
3525
3486
  try {
3526
3487
  chownMindDir(dir, mindName);
3527
3488
  } catch (chownErr) {
3528
- console.error(
3529
- `[daemon] failed to fix ownership during upgrade cleanup for ${mindName}:`,
3530
- chownErr
3489
+ logger_default.error(
3490
+ `failed to fix ownership during upgrade cleanup for ${mindName}`,
3491
+ logger_default.errorData(chownErr)
3531
3492
  );
3532
3493
  }
3533
3494
  return c.json(
@@ -3536,8 +3497,8 @@ ${user.trimEnd()}
3536
3497
  );
3537
3498
  }
3538
3499
  }
3539
- const worktreeDir = resolve11(dir, ".variants", UPGRADE_VARIANT);
3540
- if (existsSync8(worktreeDir)) {
3500
+ const worktreeDir = resolve13(dir, ".variants", UPGRADE_VARIANT);
3501
+ if (existsSync10(worktreeDir)) {
3541
3502
  return c.json(
3542
3503
  { error: "Upgrade variant already exists. Use continue or delete it first." },
3543
3504
  409
@@ -3548,10 +3509,20 @@ ${user.trimEnd()}
3548
3509
  await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
3549
3510
  } catch {
3550
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
+ }
3551
3522
  await updateTemplateBranch(dir, template, mindName);
3552
- const parentDir = resolve11(dir, ".variants");
3553
- if (!existsSync8(parentDir)) {
3554
- mkdirSync5(parentDir, { recursive: true });
3523
+ const parentDir = resolve13(dir, ".variants");
3524
+ if (!existsSync10(parentDir)) {
3525
+ mkdirSync7(parentDir, { recursive: true });
3555
3526
  }
3556
3527
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
3557
3528
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -3597,9 +3568,9 @@ ${user.trimEnd()}
3597
3568
  try {
3598
3569
  chownMindDir(dir, mindName);
3599
3570
  } catch (chownErr) {
3600
- console.error(
3601
- `[daemon] failed to fix ownership during upgrade cleanup for ${mindName}:`,
3602
- chownErr
3571
+ logger_default.error(
3572
+ `failed to fix ownership during upgrade cleanup for ${mindName}`,
3573
+ logger_default.errorData(chownErr)
3603
3574
  );
3604
3575
  }
3605
3576
  return c.json(
@@ -3612,11 +3583,9 @@ ${user.trimEnd()}
3612
3583
  const [baseName, variantName] = name.split("@", 2);
3613
3584
  const entry = findMind(baseName);
3614
3585
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3615
- let port = entry.port;
3616
3586
  if (variantName) {
3617
3587
  const variant = findVariant(baseName, variantName);
3618
3588
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
3619
- port = variant.port;
3620
3589
  }
3621
3590
  if (!getMindManager().isRunning(name)) {
3622
3591
  return c.json({ error: "Mind is not running" }, 409);
@@ -3626,7 +3595,7 @@ ${user.trimEnd()}
3626
3595
  try {
3627
3596
  parsed = JSON.parse(body);
3628
3597
  } catch (err) {
3629
- 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));
3630
3599
  }
3631
3600
  const channel = parsed?.channel ?? "unknown";
3632
3601
  const db = await getDb();
@@ -3642,7 +3611,7 @@ ${user.trimEnd()}
3642
3611
  content
3643
3612
  });
3644
3613
  } catch (err) {
3645
- 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));
3646
3615
  }
3647
3616
  }
3648
3617
  const budget = getTokenBudget();
@@ -3656,16 +3625,31 @@ ${user.trimEnd()}
3656
3625
  });
3657
3626
  return c.json({ error: "Token budget exceeded \u2014 message queued for next period" }, 429);
3658
3627
  }
3628
+ if (!parsed) return c.json({ error: "Invalid JSON" }, 400);
3659
3629
  const typingMap = getTypingMap();
3660
- const sender = parsed?.sender ?? "";
3630
+ const sender = parsed.sender ?? "";
3661
3631
  if (sender) typingMap.delete(channel, sender);
3662
3632
  const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
3663
- let forwardBody = body;
3664
- if (parsed && currentlyTyping.length > 0) {
3633
+ if (currentlyTyping.length > 0) {
3665
3634
  parsed.typing = currentlyTyping;
3666
- forwardBody = JSON.stringify(parsed);
3667
3635
  }
3668
- 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") {
3669
3653
  const usage = budget.getUsage(baseName);
3670
3654
  const pct = usage?.percentUsed ?? 80;
3671
3655
  const warningText = `
@@ -3676,12 +3660,11 @@ ${user.trimEnd()}
3676
3660
  parsed.content = [...parsed.content, { type: "text", text: warningText }];
3677
3661
  }
3678
3662
  budget.acknowledgeWarning(baseName);
3679
- forwardBody = JSON.stringify(parsed);
3680
3663
  }
3681
3664
  const seedEntry = findMind(baseName);
3682
- if (seedEntry?.stage === "seed" && parsed) {
3665
+ if (seedEntry?.stage === "seed") {
3683
3666
  try {
3684
- const countResult = await db.select({ count: sql3`count(*)` }).from(mindHistory).where(eq5(mindHistory.mind, baseName));
3667
+ const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
3685
3668
  const msgCount = countResult[0]?.count ?? 0;
3686
3669
  if (msgCount >= 10 && msgCount % 10 === 0) {
3687
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.]";
@@ -3690,27 +3673,29 @@ ${user.trimEnd()}
3690
3673
  } else if (Array.isArray(parsed.content)) {
3691
3674
  parsed.content = [...parsed.content, { type: "text", text: nudge }];
3692
3675
  }
3693
- forwardBody = JSON.stringify(parsed);
3694
3676
  }
3695
3677
  } catch (err) {
3696
- console.error(`[daemon] failed to check seed message count for ${baseName}:`, err);
3697
- }
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;
3698
3696
  }
3699
- typingMap.set(channel, baseName, { persistent: true });
3700
- const conversationId = parsed?.conversationId ?? null;
3701
- if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
3702
- fetch(`http://127.0.0.1:${port}/message`, {
3703
- method: "POST",
3704
- headers: { "Content-Type": "application/json" },
3705
- body: forwardBody
3706
- }).then(async (res) => {
3707
- if (!res.ok) {
3708
- const text = await res.text().catch(() => "");
3709
- console.error(`[daemon] mind ${name} responded with ${res.status}: ${text}`);
3710
- }
3711
- }).catch((err) => {
3712
- console.error(`[daemon] mind ${name} unreachable on port ${port}:`, err);
3713
- typingMap.delete(channel, baseName);
3697
+ getDeliveryManager().routeAndDeliver(name, deliveryPayload).catch((err) => {
3698
+ logger_default.error(`delivery failed for ${name}`, logger_default.errorData(err));
3714
3699
  });
3715
3700
  return c.json({ ok: true });
3716
3701
  }).get("/:name/budget", async (c) => {
@@ -3719,6 +3704,19 @@ ${user.trimEnd()}
3719
3704
  const usage = getTokenBudget().getUsage(baseName);
3720
3705
  if (!usage) return c.json({ error: "No budget configured" }, 404);
3721
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
+ }
3722
3720
  }).post("/:name/events", async (c) => {
3723
3721
  const name = c.req.param("name");
3724
3722
  const [baseName] = name.split("@", 2);
@@ -3743,7 +3741,7 @@ ${user.trimEnd()}
3743
3741
  metadata: body.metadata ? JSON.stringify(body.metadata) : null
3744
3742
  });
3745
3743
  } catch (err) {
3746
- console.error(`[daemon] failed to persist event for ${baseName}:`, err);
3744
+ logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
3747
3745
  }
3748
3746
  publish2(baseName, {
3749
3747
  mind: baseName,
@@ -3754,12 +3752,22 @@ ${user.trimEnd()}
3754
3752
  content: body.content,
3755
3753
  metadata: body.metadata
3756
3754
  });
3755
+ if ((body.type === "text" || body.type === "outbound") && body.channel) {
3756
+ getTypingMap().delete(body.channel, baseName);
3757
+ }
3757
3758
  if (body.type === "done") {
3758
3759
  if (body.channel) {
3759
3760
  getTypingMap().delete(body.channel, baseName);
3760
3761
  } else {
3761
3762
  getTypingMap().deleteSender(baseName);
3762
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
+ }
3763
3771
  }
3764
3772
  if (body.type === "usage" && body.metadata) {
3765
3773
  const inputTokens = body.metadata.input_tokens ?? 0;
@@ -3827,7 +3835,7 @@ ${user.trimEnd()}
3827
3835
  content: body.content
3828
3836
  });
3829
3837
  } catch (err) {
3830
- 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));
3831
3839
  return c.json({ error: "Failed to persist" }, 500);
3832
3840
  }
3833
3841
  return c.json({ ok: true });
@@ -3836,16 +3844,16 @@ ${user.trimEnd()}
3836
3844
  const db = await getDb();
3837
3845
  const rows = await db.select({
3838
3846
  session: mindHistory.session,
3839
- started_at: sql3`MIN(${mindHistory.created_at})`,
3840
- event_count: sql3`COUNT(*)`,
3841
- message_count: sql3`SUM(CASE WHEN ${mindHistory.type} IN ('inbound','outbound') THEN 1 ELSE 0 END)`,
3842
- tool_count: sql3`SUM(CASE WHEN ${mindHistory.type}='tool_use' THEN 1 ELSE 0 END)`
3843
- }).from(mindHistory).where(and3(eq5(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`);
3844
3852
  return c.json(rows);
3845
3853
  }).get("/:name/history/channels", async (c) => {
3846
3854
  const name = c.req.param("name");
3847
3855
  const db = await getDb();
3848
- const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq5(mindHistory.mind, name));
3856
+ const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
3849
3857
  return c.json(rows.map((r) => r.channel));
3850
3858
  }).get("/:name/history", async (c) => {
3851
3859
  const name = c.req.param("name");
@@ -3855,25 +3863,25 @@ ${user.trimEnd()}
3855
3863
  const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
3856
3864
  const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
3857
3865
  const db = await getDb();
3858
- const conditions = [eq5(mindHistory.mind, name)];
3866
+ const conditions = [eq4(mindHistory.mind, name)];
3859
3867
  if (channel) {
3860
- conditions.push(eq5(mindHistory.channel, channel));
3868
+ conditions.push(eq4(mindHistory.channel, channel));
3861
3869
  }
3862
3870
  if (session) {
3863
- conditions.push(eq5(mindHistory.session, session));
3871
+ conditions.push(eq4(mindHistory.session, session));
3864
3872
  }
3865
3873
  if (!full) {
3866
- conditions.push(sql3`${mindHistory.type} IN ('inbound', 'outbound')`);
3874
+ conditions.push(sql2`${mindHistory.type} IN ('inbound', 'outbound')`);
3867
3875
  }
3868
3876
  const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
3869
3877
  return c.json(rows);
3870
3878
  });
3871
- var minds_default = app8;
3879
+ var minds_default = app9;
3872
3880
 
3873
3881
  // src/web/api/pages.ts
3874
3882
  import { readFile as readFile2, stat } from "fs/promises";
3875
- import { extname, resolve as resolve12 } from "path";
3876
- import { Hono as Hono9 } from "hono";
3883
+ import { extname, resolve as resolve14 } from "path";
3884
+ import { Hono as Hono10 } from "hono";
3877
3885
  var MIME_TYPES = {
3878
3886
  ".html": "text/html",
3879
3887
  ".js": "application/javascript",
@@ -3890,16 +3898,16 @@ var MIME_TYPES = {
3890
3898
  ".txt": "text/plain",
3891
3899
  ".xml": "application/xml"
3892
3900
  };
3893
- var app9 = new Hono9().get("/:name/*", async (c) => {
3901
+ var app10 = new Hono10().get("/:name/*", async (c) => {
3894
3902
  const name = c.req.param("name");
3895
3903
  if (!findMind(name)) return c.text("Not found", 404);
3896
- const pagesRoot = resolve12(mindDir(name), "home", "pages");
3904
+ const pagesRoot = resolve14(mindDir(name), "home", "pages");
3897
3905
  const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
3898
- const requestedPath = resolve12(pagesRoot, wildcard.slice(1));
3906
+ const requestedPath = resolve14(pagesRoot, wildcard.slice(1));
3899
3907
  if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
3900
3908
  let fileStat = await stat(requestedPath).catch(() => null);
3901
3909
  if (fileStat?.isDirectory()) {
3902
- const indexPath = resolve12(requestedPath, "index.html");
3910
+ const indexPath = resolve14(requestedPath, "index.html");
3903
3911
  fileStat = await stat(indexPath).catch(() => null);
3904
3912
  if (fileStat?.isFile()) {
3905
3913
  const body = await readFile2(indexPath);
@@ -3915,14 +3923,14 @@ var app9 = new Hono9().get("/:name/*", async (c) => {
3915
3923
  }
3916
3924
  return c.text("Not found", 404);
3917
3925
  });
3918
- var pages_default = app9;
3926
+ var pages_default = app10;
3919
3927
 
3920
3928
  // src/web/api/prompts.ts
3921
3929
  import { zValidator as zValidator4 } from "@hono/zod-validator";
3922
- import { eq as eq6, sql as sql4 } from "drizzle-orm";
3923
- import { Hono as Hono10 } from "hono";
3930
+ import { eq as eq5, sql as sql3 } from "drizzle-orm";
3931
+ import { Hono as Hono11 } from "hono";
3924
3932
  import { z as z4 } from "zod";
3925
- var app10 = new Hono10().get("/", async (c) => {
3933
+ var app11 = new Hono11().get("/", async (c) => {
3926
3934
  let rows;
3927
3935
  try {
3928
3936
  const db = await getDb();
@@ -3952,9 +3960,9 @@ var app10 = new Hono10().get("/", async (c) => {
3952
3960
  }
3953
3961
  const { content } = c.req.valid("json");
3954
3962
  const db = await getDb();
3955
- await db.insert(systemPrompts).values({ key, content, updated_at: sql4`(datetime('now'))` }).onConflictDoUpdate({
3963
+ await db.insert(systemPrompts).values({ key, content, updated_at: sql3`(datetime('now'))` }).onConflictDoUpdate({
3956
3964
  target: systemPrompts.key,
3957
- set: { content, updated_at: sql4`(datetime('now'))` }
3965
+ set: { content, updated_at: sql3`(datetime('now'))` }
3958
3966
  });
3959
3967
  return c.json({ ok: true });
3960
3968
  }).delete("/:key", requireAdmin, async (c) => {
@@ -3963,13 +3971,13 @@ var app10 = new Hono10().get("/", async (c) => {
3963
3971
  return c.json({ error: "Unknown prompt key" }, 404);
3964
3972
  }
3965
3973
  const db = await getDb();
3966
- await db.delete(systemPrompts).where(eq6(systemPrompts.key, key));
3974
+ await db.delete(systemPrompts).where(eq5(systemPrompts.key, key));
3967
3975
  return c.json({ ok: true });
3968
3976
  });
3969
- var prompts_default = app10;
3977
+ var prompts_default = app11;
3970
3978
 
3971
3979
  // src/web/api/schedules.ts
3972
- import { Hono as Hono11 } from "hono";
3980
+ import { Hono as Hono12 } from "hono";
3973
3981
  function readSchedules(name) {
3974
3982
  return readVoluteConfig(mindDir(name))?.schedules ?? [];
3975
3983
  }
@@ -3980,7 +3988,7 @@ function writeSchedules(name, schedules) {
3980
3988
  writeVoluteConfig(dir, config);
3981
3989
  getScheduler().loadSchedules(name);
3982
3990
  }
3983
- var app11 = new Hono11().get("/:name/schedules", (c) => {
3991
+ var app12 = new Hono12().get("/:name/schedules", (c) => {
3984
3992
  const name = c.req.param("name");
3985
3993
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
3986
3994
  return c.json(readSchedules(name));
@@ -4051,15 +4059,68 @@ var app11 = new Hono11().get("/:name/schedules", (c) => {
4051
4059
  return c.json({ error: "Failed to reach mind" }, 502);
4052
4060
  }
4053
4061
  });
4054
- var schedules_default = app11;
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;
4055
4116
 
4056
4117
  // src/web/api/skills.ts
4057
- import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
4118
+ import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
4058
4119
  import { tmpdir as tmpdir2 } from "os";
4059
- import { join as join3, resolve as resolve13 } from "path";
4120
+ import { join as join3, resolve as resolve15 } from "path";
4060
4121
  import AdmZip from "adm-zip";
4061
- import { Hono as Hono12 } from "hono";
4062
- var app12 = new Hono12().get("/", async (c) => {
4122
+ import { Hono as Hono14 } from "hono";
4123
+ var app14 = new Hono14().get("/", async (c) => {
4063
4124
  const skills = await listSharedSkills();
4064
4125
  return c.json(skills);
4065
4126
  }).get("/:id", async (c) => {
@@ -4083,19 +4144,19 @@ var app12 = new Hono12().get("/", async (c) => {
4083
4144
  try {
4084
4145
  const zip = new AdmZip(buffer);
4085
4146
  for (const entry of zip.getEntries()) {
4086
- const target = resolve13(tmpDir, entry.entryName);
4147
+ const target = resolve15(tmpDir, entry.entryName);
4087
4148
  if (!target.startsWith(tmpDir)) {
4088
4149
  return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
4089
4150
  }
4090
4151
  }
4091
4152
  zip.extractAllTo(tmpDir, true);
4092
4153
  let skillDir = null;
4093
- if (existsSync9(join3(tmpDir, "SKILL.md"))) {
4154
+ if (existsSync11(join3(tmpDir, "SKILL.md"))) {
4094
4155
  skillDir = tmpDir;
4095
4156
  } else {
4096
4157
  const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4097
4158
  for (const entry of entries) {
4098
- if (existsSync9(join3(tmpDir, entry.name, "SKILL.md"))) {
4159
+ if (existsSync11(join3(tmpDir, entry.name, "SKILL.md"))) {
4099
4160
  skillDir = join3(tmpDir, entry.name);
4100
4161
  break;
4101
4162
  }
@@ -4124,12 +4185,12 @@ var app12 = new Hono12().get("/", async (c) => {
4124
4185
  }
4125
4186
  return c.json({ ok: true });
4126
4187
  });
4127
- var skills_default = app12;
4188
+ var skills_default = app14;
4128
4189
 
4129
4190
  // src/web/api/system.ts
4130
- import { Hono as Hono13 } from "hono";
4191
+ import { Hono as Hono15 } from "hono";
4131
4192
  import { streamSSE as streamSSE2 } from "hono/streaming";
4132
- var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
4193
+ var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
4133
4194
  setTimeout(() => process.exit(1), 200);
4134
4195
  return c.json({ ok: true });
4135
4196
  }).post("/stop", requireAdmin, (c) => {
@@ -4146,10 +4207,10 @@ var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
4146
4207
  stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
4147
4208
  });
4148
4209
  });
4149
- await new Promise((resolve19) => {
4210
+ await new Promise((resolve20) => {
4150
4211
  stream.onAbort(() => {
4151
4212
  unsubscribe();
4152
- resolve19();
4213
+ resolve20();
4153
4214
  });
4154
4215
  });
4155
4216
  });
@@ -4157,18 +4218,18 @@ var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
4157
4218
  const config = readSystemsConfig();
4158
4219
  return c.json({ system: config?.system ?? null });
4159
4220
  });
4160
- var system_default = app13;
4221
+ var system_default = app15;
4161
4222
 
4162
4223
  // src/web/api/typing.ts
4163
4224
  import { zValidator as zValidator5 } from "@hono/zod-validator";
4164
- import { Hono as Hono14 } from "hono";
4225
+ import { Hono as Hono16 } from "hono";
4165
4226
  import { z as z5 } from "zod";
4166
4227
  var typingSchema = z5.object({
4167
4228
  channel: z5.string().min(1),
4168
4229
  sender: z5.string().min(1),
4169
4230
  active: z5.boolean()
4170
4231
  });
4171
- var app14 = new Hono14().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
4232
+ var app16 = new Hono16().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
4172
4233
  const { channel, sender, active } = c.req.valid("json");
4173
4234
  const map = getTypingMap();
4174
4235
  if (active) {
@@ -4185,13 +4246,13 @@ var app14 = new Hono14().post("/:name/typing", zValidator5("json", typingSchema)
4185
4246
  const map = getTypingMap();
4186
4247
  return c.json({ typing: map.get(channel) });
4187
4248
  });
4188
- var typing_default = app14;
4249
+ var typing_default = app16;
4189
4250
 
4190
4251
  // src/web/api/update.ts
4191
4252
  import { spawn as spawn3 } from "child_process";
4192
- import { Hono as Hono15 } from "hono";
4253
+ import { Hono as Hono17 } from "hono";
4193
4254
  var bin;
4194
- var app15 = new Hono15().get("/update", async (c) => {
4255
+ var app17 = new Hono17().get("/update", async (c) => {
4195
4256
  const result = await checkForUpdate();
4196
4257
  return c.json(result);
4197
4258
  }).post("/update", requireAdmin, async (c) => {
@@ -4206,19 +4267,19 @@ var app15 = new Hono15().get("/update", async (c) => {
4206
4267
  child.unref();
4207
4268
  return c.json({ ok: true, message: "Updating..." });
4208
4269
  });
4209
- var update_default = app15;
4270
+ var update_default = app17;
4210
4271
 
4211
4272
  // src/web/api/variants.ts
4212
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
4213
- import { resolve as resolve15 } from "path";
4214
- import { Hono as Hono16 } 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";
4215
4276
 
4216
4277
  // src/lib/spawn-server.ts
4217
4278
  import { spawn as spawn4 } from "child_process";
4218
- import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync8 } from "fs";
4219
- import { resolve as resolve14 } from "path";
4279
+ import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync10 } from "fs";
4280
+ import { resolve as resolve16 } from "path";
4220
4281
  function tsxBin(cwd) {
4221
- return resolve14(cwd, "node_modules", ".bin", "tsx");
4282
+ return resolve16(cwd, "node_modules", ".bin", "tsx");
4222
4283
  }
4223
4284
  function spawnServer(cwd, port, options) {
4224
4285
  if (options?.detached) {
@@ -4231,31 +4292,31 @@ function spawnAttached(cwd, port) {
4231
4292
  cwd,
4232
4293
  stdio: ["ignore", "pipe", "pipe"]
4233
4294
  });
4234
- return new Promise((resolve19) => {
4235
- const timeout = setTimeout(() => resolve19(null), 3e4);
4295
+ return new Promise((resolve20) => {
4296
+ const timeout = setTimeout(() => resolve20(null), 3e4);
4236
4297
  function checkOutput(data) {
4237
4298
  const match = data.toString().match(/listening on :(\d+)/);
4238
4299
  if (match) {
4239
4300
  clearTimeout(timeout);
4240
- resolve19({ child, actualPort: parseInt(match[1], 10) });
4301
+ resolve20({ child, actualPort: parseInt(match[1], 10) });
4241
4302
  }
4242
4303
  }
4243
4304
  child.stdout?.on("data", checkOutput);
4244
4305
  child.stderr?.on("data", checkOutput);
4245
4306
  child.on("error", () => {
4246
4307
  clearTimeout(timeout);
4247
- resolve19(null);
4308
+ resolve20(null);
4248
4309
  });
4249
4310
  child.on("exit", () => {
4250
4311
  clearTimeout(timeout);
4251
- resolve19(null);
4312
+ resolve20(null);
4252
4313
  });
4253
4314
  });
4254
4315
  }
4255
4316
  function spawnDetached(cwd, port, logDir) {
4256
- const logsDir = logDir ?? resolve14(cwd, ".volute", "logs");
4257
- mkdirSync6(logsDir, { recursive: true });
4258
- const logPath = resolve14(logsDir, "mind.log");
4317
+ const logsDir = logDir ?? resolve16(cwd, ".mind", "logs");
4318
+ mkdirSync8(logsDir, { recursive: true });
4319
+ const logPath = resolve16(logsDir, "mind.log");
4259
4320
  const logFd = openSync(logPath, "a");
4260
4321
  const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
4261
4322
  cwd,
@@ -4275,7 +4336,7 @@ function spawnDetached(cwd, port, logDir) {
4275
4336
  }
4276
4337
  const interval = setInterval(() => {
4277
4338
  try {
4278
- const content = readFileSync8(logPath, "utf-8");
4339
+ const content = readFileSync10(logPath, "utf-8");
4279
4340
  const match = content.match(/listening on :(\d+)/);
4280
4341
  if (match) {
4281
4342
  finish({ child, actualPort: parseInt(match[1], 10) });
@@ -4290,7 +4351,7 @@ function spawnDetached(cwd, port, logDir) {
4290
4351
  }
4291
4352
 
4292
4353
  // src/lib/verify.ts
4293
- async function verify(port) {
4354
+ async function verify2(port) {
4294
4355
  const health = await checkHealth(port);
4295
4356
  if (!health.ok) {
4296
4357
  console.error(" Health check: failed");
@@ -4327,7 +4388,7 @@ async function verify(port) {
4327
4388
  }
4328
4389
 
4329
4390
  // src/web/api/variants.ts
4330
- var app16 = new Hono16().get("/:name/variants", async (c) => {
4391
+ var app18 = new Hono18().get("/:name/variants", async (c) => {
4331
4392
  const name = c.req.param("name");
4332
4393
  const entry = findMind(name);
4333
4394
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -4357,11 +4418,11 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4357
4418
  const err = validateBranchName(variantName);
4358
4419
  if (err) return c.json({ error: err }, 400);
4359
4420
  const projectRoot = mindDir(mindName);
4360
- const variantDir = resolve15(projectRoot, ".variants", variantName);
4361
- if (existsSync10(variantDir)) {
4421
+ const variantDir = resolve17(projectRoot, ".variants", variantName);
4422
+ if (existsSync12(variantDir)) {
4362
4423
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
4363
4424
  }
4364
- mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
4425
+ mkdirSync9(resolve17(projectRoot, ".variants"), { recursive: true });
4365
4426
  try {
4366
4427
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
4367
4428
  } catch (e) {
@@ -4374,7 +4435,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4374
4435
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4375
4436
  await exec(cmd, args, {
4376
4437
  cwd: variantDir,
4377
- env: { ...process.env, HOME: resolve15(variantDir, "home") }
4438
+ env: { ...process.env, HOME: resolve17(variantDir, "home") }
4378
4439
  });
4379
4440
  } else {
4380
4441
  await exec("npm", ["install"], { cwd: variantDir });
@@ -4384,7 +4445,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4384
4445
  return c.json({ error: `npm install failed: ${msg}` }, 500);
4385
4446
  }
4386
4447
  if (body.soul) {
4387
- writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
4448
+ writeFileSync9(resolve17(variantDir, "home/SOUL.md"), body.soul);
4388
4449
  }
4389
4450
  const variantPort = body.port ?? nextPort();
4390
4451
  const variant = {
@@ -4422,7 +4483,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4422
4483
  } catch {
4423
4484
  }
4424
4485
  const projectRoot = mindDir(mindName);
4425
- if (existsSync10(variant.path)) {
4486
+ if (existsSync12(variant.path)) {
4426
4487
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
4427
4488
  if (status) {
4428
4489
  try {
@@ -4448,7 +4509,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4448
4509
  500
4449
4510
  );
4450
4511
  }
4451
- const verified = await verify(result.actualPort);
4512
+ const verified = await verify2(result.actualPort);
4452
4513
  try {
4453
4514
  process.kill(result.child.pid);
4454
4515
  } catch {
@@ -4479,7 +4540,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4479
4540
  } catch (e) {
4480
4541
  return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
4481
4542
  }
4482
- if (existsSync10(variant.path)) {
4543
+ if (existsSync12(variant.path)) {
4483
4544
  try {
4484
4545
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4485
4546
  } catch {
@@ -4496,7 +4557,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4496
4557
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4497
4558
  await exec(cmd, args, {
4498
4559
  cwd: projectRoot,
4499
- env: { ...process.env, HOME: resolve15(projectRoot, "home") }
4560
+ env: { ...process.env, HOME: resolve17(projectRoot, "home") }
4500
4561
  });
4501
4562
  } else {
4502
4563
  await exec("npm", ["install"], { cwd: projectRoot });
@@ -4539,7 +4600,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4539
4600
  } catch {
4540
4601
  }
4541
4602
  }
4542
- if (existsSync10(variant.path)) {
4603
+ if (existsSync12(variant.path)) {
4543
4604
  try {
4544
4605
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4545
4606
  } catch {
@@ -4553,16 +4614,19 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
4553
4614
  chownMindDir(projectRoot, mindName);
4554
4615
  return c.json({ ok: true });
4555
4616
  });
4556
- var variants_default = app16;
4617
+ var variants_default = app18;
4557
4618
 
4558
4619
  // src/web/api/volute/channels.ts
4559
4620
  import { zValidator as zValidator6 } from "@hono/zod-validator";
4560
- import { Hono as Hono17 } from "hono";
4621
+ import { Hono as Hono19 } from "hono";
4561
4622
  import { z as z6 } from "zod";
4562
4623
  var createSchema = z6.object({
4563
4624
  name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
4564
4625
  });
4565
- var app17 = new Hono17().get("/", async (c) => {
4626
+ var inviteSchema = z6.object({
4627
+ username: z6.string().min(1)
4628
+ });
4629
+ var app19 = new Hono19().get("/", async (c) => {
4566
4630
  const user = c.get("user");
4567
4631
  const channels = await listChannels();
4568
4632
  const results = await Promise.all(
@@ -4606,16 +4670,85 @@ var app17 = new Hono17().get("/", async (c) => {
4606
4670
  if (!ch) return c.json({ error: "Channel not found" }, 404);
4607
4671
  const participants = await getParticipants(ch.id);
4608
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 });
4609
4692
  });
4610
- var channels_default2 = app17;
4693
+ var channels_default2 = app19;
4611
4694
 
4612
4695
  // src/web/api/volute/chat.ts
4613
- import { readFileSync as readFileSync9 } from "fs";
4614
- import { resolve as resolve16 } from "path";
4615
4696
  import { zValidator as zValidator7 } from "@hono/zod-validator";
4616
- import { Hono as Hono18 } from "hono";
4697
+ import { Hono as Hono20 } from "hono";
4617
4698
  import { streamSSE as streamSSE3 } from "hono/streaming";
4618
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
4726
+ };
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
+ }
4751
+ }
4619
4752
  var chatSchema = z7.object({
4620
4753
  message: z7.string().optional(),
4621
4754
  conversationId: z7.string().optional(),
@@ -4627,25 +4760,7 @@ var chatSchema = z7.object({
4627
4760
  })
4628
4761
  ).optional()
4629
4762
  });
4630
- function getDaemonUrl() {
4631
- try {
4632
- const data = JSON.parse(readFileSync9(resolve16(voluteHome(), "daemon.json"), "utf-8"));
4633
- return `http://${daemonLoopback()}:${data.port}`;
4634
- } catch (err) {
4635
- throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
4636
- }
4637
- }
4638
- function daemonFetchInternal(path, body) {
4639
- const daemonUrl = getDaemonUrl();
4640
- const token = process.env.VOLUTE_DAEMON_TOKEN;
4641
- const headers = {
4642
- "Content-Type": "application/json",
4643
- Origin: daemonUrl
4644
- };
4645
- if (token) headers.Authorization = `Bearer ${token}`;
4646
- return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
4647
- }
4648
- var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
4763
+ var app20 = new Hono20().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
4649
4764
  const name = c.req.param("name");
4650
4765
  const [baseName] = name.split("@", 2);
4651
4766
  const entry = findMind(baseName);
@@ -4681,8 +4796,8 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
4681
4796
  }
4682
4797
  }
4683
4798
  if (!conversationId) {
4684
- const participantNames2 = /* @__PURE__ */ new Set([senderName, baseName]);
4685
- const title = [...participantNames2].join(", ");
4799
+ const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
4800
+ const title = [...participantNames].join(", ");
4686
4801
  const conv2 = await createConversation(baseName, "volute", {
4687
4802
  userId: user.id !== 0 ? user.id : void 0,
4688
4803
  title,
@@ -4692,7 +4807,7 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
4692
4807
  }
4693
4808
  }
4694
4809
  const conv = await getConversation(conversationId);
4695
- const convTitle = conv?.title;
4810
+ const convTitle = conv?.title ?? null;
4696
4811
  const contentBlocks = [];
4697
4812
  if (body.message) {
4698
4813
  contentBlocks.push({ type: "text", text: body.message });
@@ -4703,61 +4818,13 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
4703
4818
  }
4704
4819
  }
4705
4820
  await addMessage(conversationId, "user", senderName, contentBlocks);
4706
- const participants = await getParticipants(conversationId);
4707
- const mindParticipants = participants.filter((p) => p.userType === "mind");
4708
- const participantNames = participants.map((p) => p.username);
4709
- const { getMindManager: getMindManager2 } = await import("./mind-manager-Z7O7PN2O.js");
4710
- const manager = getMindManager2();
4711
- const runningMinds = mindParticipants.map((ap) => {
4712
- const mindKey = ap.username === baseName ? name : ap.username;
4713
- return manager.isRunning(mindKey) ? ap.username : null;
4714
- }).filter((n) => n !== null && n !== senderName);
4715
- const isDM = participants.length === 2;
4716
- function channelForMind(mindUsername) {
4717
- return buildVoluteSlug({
4718
- participants,
4719
- mindUsername,
4720
- convTitle,
4721
- conversationId
4722
- });
4723
- }
4724
- const channelEntry = {
4725
- platformId: conversationId,
4726
- platform: "volute",
4727
- name: convTitle ?? void 0,
4728
- type: isDM ? "dm" : "group"
4729
- };
4730
- for (const ap of mindParticipants) {
4731
- try {
4732
- writeChannelEntry(ap.username, channelForMind(ap.username), channelEntry);
4733
- } catch (err) {
4734
- console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
4735
- }
4736
- }
4737
- for (const mindName of runningMinds) {
4738
- const targetName = mindName === baseName ? name : mindName;
4739
- const channel = channelForMind(mindName);
4740
- const typingMap = getTypingMap();
4741
- const currentlyTyping = typingMap.get(channel);
4742
- const payload = JSON.stringify({
4743
- content: contentBlocks,
4744
- channel,
4745
- conversationId,
4746
- sender: senderName,
4747
- participants: participantNames,
4748
- participantCount: participants.length,
4749
- isDM,
4750
- ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
4751
- });
4752
- daemonFetchInternal(`/api/minds/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
4753
- if (!res.ok) {
4754
- const text = await res.text().catch(() => "");
4755
- console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
4756
- }
4757
- }).catch((err) => {
4758
- console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
4759
- });
4760
- }
4821
+ await fanOutToMinds({
4822
+ conversationId,
4823
+ contentBlocks,
4824
+ senderName,
4825
+ convTitle,
4826
+ targetName: (username) => username === baseName ? name : username
4827
+ });
4761
4828
  return c.json({ ok: true, conversationId });
4762
4829
  }).get("/:name/conversations/:id/events", async (c) => {
4763
4830
  const conversationId = c.req.param("id");
@@ -4776,11 +4843,11 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
4776
4843
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
4777
4844
  });
4778
4845
  }, 15e3);
4779
- await new Promise((resolve19) => {
4846
+ await new Promise((resolve20) => {
4780
4847
  stream.onAbort(() => {
4781
4848
  unsubscribe();
4782
4849
  clearInterval(keepAlive);
4783
- resolve19();
4850
+ resolve20();
4784
4851
  });
4785
4852
  });
4786
4853
  });
@@ -4790,7 +4857,7 @@ var unifiedChatSchema = z7.object({
4790
4857
  conversationId: z7.string(),
4791
4858
  images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
4792
4859
  });
4793
- var unifiedChatApp = new Hono18().post(
4860
+ var unifiedChatApp = new Hono20().post(
4794
4861
  "/chat",
4795
4862
  zValidator7("json", unifiedChatSchema),
4796
4863
  async (c) => {
@@ -4813,79 +4880,31 @@ var unifiedChatApp = new Hono18().post(
4813
4880
  }
4814
4881
  }
4815
4882
  await addMessage(body.conversationId, "user", senderName, contentBlocks);
4816
- const participants = await getParticipants(body.conversationId);
4817
- const mindParticipants = participants.filter((p) => p.userType === "mind");
4818
- const participantNames = participants.map((p) => p.username);
4819
- const { getMindManager: getMindManager2 } = await import("./mind-manager-Z7O7PN2O.js");
4820
- const manager = getMindManager2();
4821
- const runningMinds = mindParticipants.map((ap) => manager.isRunning(ap.username) ? ap.username : null).filter((n) => n !== null && n !== senderName);
4822
- const isDM = conv.type === "dm" && participants.length === 2;
4823
- const channelEntry = {
4824
- platformId: body.conversationId,
4825
- platform: "volute",
4826
- name: conv.title ?? void 0,
4827
- type: conv.type === "channel" ? "group" : isDM ? "dm" : "group"
4828
- };
4829
- for (const ap of mindParticipants) {
4830
- const slug = buildVoluteSlug({
4831
- participants,
4832
- mindUsername: ap.username,
4833
- convTitle: conv.title,
4834
- conversationId: conv.id,
4835
- convType: conv.type,
4836
- convName: conv.name
4837
- });
4838
- try {
4839
- writeChannelEntry(ap.username, slug, channelEntry);
4840
- } catch (err) {
4841
- console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
4842
- }
4843
- }
4844
- for (const mindName of runningMinds) {
4845
- const channel = buildVoluteSlug({
4846
- participants,
4847
- mindUsername: mindName,
4848
- convTitle: conv.title,
4849
- conversationId: body.conversationId,
4850
- convType: conv.type,
4851
- convName: conv.name
4852
- });
4853
- const typingMap = getTypingMap();
4854
- const currentlyTyping = typingMap.get(channel);
4855
- const payload = JSON.stringify({
4856
- content: contentBlocks,
4857
- channel,
4858
- conversationId: body.conversationId,
4859
- sender: senderName,
4860
- participants: participantNames,
4861
- participantCount: participants.length,
4862
- isDM,
4863
- ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
4864
- });
4865
- daemonFetchInternal(`/api/minds/${encodeURIComponent(mindName)}/message`, payload).then(async (res) => {
4866
- if (!res.ok) {
4867
- const text = await res.text().catch(() => "");
4868
- console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
4869
- }
4870
- }).catch((err) => {
4871
- console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
4872
- });
4873
- }
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
+ });
4874
4893
  return c.json({ ok: true, conversationId: body.conversationId });
4875
4894
  }
4876
4895
  );
4877
- var chat_default = app18;
4896
+ var chat_default = app20;
4878
4897
 
4879
4898
  // src/web/api/volute/conversations.ts
4880
4899
  import { zValidator as zValidator8 } from "@hono/zod-validator";
4881
- import { Hono as Hono19 } from "hono";
4900
+ import { Hono as Hono21 } from "hono";
4882
4901
  import { z as z8 } from "zod";
4883
4902
  var createConvSchema = z8.object({
4884
4903
  title: z8.string().optional(),
4885
4904
  participantIds: z8.array(z8.number()).optional(),
4886
4905
  participantNames: z8.array(z8.string()).optional()
4887
4906
  });
4888
- var app19 = new Hono19().get("/:name/conversations", async (c) => {
4907
+ var app21 = new Hono21().get("/:name/conversations", async (c) => {
4889
4908
  const name = c.req.param("name");
4890
4909
  const user = c.get("user");
4891
4910
  let lookupId = user.id;
@@ -4970,18 +4989,18 @@ var app19 = new Hono19().get("/:name/conversations", async (c) => {
4970
4989
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4971
4990
  return c.json({ ok: true });
4972
4991
  });
4973
- var conversations_default = app19;
4992
+ var conversations_default = app21;
4974
4993
 
4975
4994
  // src/web/api/volute/user-conversations.ts
4976
4995
  import { zValidator as zValidator9 } from "@hono/zod-validator";
4977
- import { Hono as Hono20 } from "hono";
4996
+ import { Hono as Hono22 } from "hono";
4978
4997
  import { streamSSE as streamSSE4 } from "hono/streaming";
4979
4998
  import { z as z9 } from "zod";
4980
4999
  var createSchema2 = z9.object({
4981
5000
  title: z9.string().optional(),
4982
5001
  participantNames: z9.array(z9.string()).min(1)
4983
5002
  });
4984
- var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
5003
+ var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
4985
5004
  const user = c.get("user");
4986
5005
  const convs = await listConversationsWithParticipants(user.id);
4987
5006
  return c.json(convs);
@@ -5040,11 +5059,11 @@ var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
5040
5059
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
5041
5060
  });
5042
5061
  }, 15e3);
5043
- await new Promise((resolve19) => {
5062
+ await new Promise((resolve20) => {
5044
5063
  stream.onAbort(() => {
5045
5064
  unsubscribe();
5046
5065
  clearInterval(keepAlive);
5047
- resolve19();
5066
+ resolve20();
5048
5067
  });
5049
5068
  });
5050
5069
  });
@@ -5055,12 +5074,12 @@ var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
5055
5074
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
5056
5075
  return c.json({ ok: true });
5057
5076
  });
5058
- var user_conversations_default = app20;
5077
+ var user_conversations_default = app22;
5059
5078
 
5060
5079
  // src/web/app.ts
5061
5080
  var httpLog = logger_default.child("http");
5062
- var app21 = new Hono21();
5063
- app21.onError((err, c) => {
5081
+ var app23 = new Hono23();
5082
+ app23.onError((err, c) => {
5064
5083
  if (err instanceof HTTPException) {
5065
5084
  return err.getResponse();
5066
5085
  }
@@ -5071,10 +5090,10 @@ app21.onError((err, c) => {
5071
5090
  });
5072
5091
  return c.json({ error: "Internal server error" }, 500);
5073
5092
  });
5074
- app21.notFound((c) => {
5093
+ app23.notFound((c) => {
5075
5094
  return c.json({ error: "Not found" }, 404);
5076
5095
  });
5077
- app21.use("*", async (c, next) => {
5096
+ app23.use("*", async (c, next) => {
5078
5097
  const start = Date.now();
5079
5098
  await next();
5080
5099
  const duration = Date.now() - start;
@@ -5085,7 +5104,7 @@ app21.use("*", async (c, next) => {
5085
5104
  httpLog.debug("request", data);
5086
5105
  }
5087
5106
  });
5088
- app21.get("/api/health", (c) => {
5107
+ app23.get("/api/health", (c) => {
5089
5108
  let version = "unknown";
5090
5109
  let cached = null;
5091
5110
  try {
@@ -5100,18 +5119,18 @@ app21.get("/api/health", (c) => {
5100
5119
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
5101
5120
  });
5102
5121
  });
5103
- app21.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
5104
- app21.use("/api/*", csrf());
5105
- app21.use("/api/minds/*", authMiddleware);
5106
- app21.use("/api/conversations/*", authMiddleware);
5107
- app21.use("/api/volute/*", authMiddleware);
5108
- app21.use("/api/system/*", authMiddleware);
5109
- app21.use("/api/env/*", authMiddleware);
5110
- app21.use("/api/prompts/*", authMiddleware);
5111
- app21.use("/api/skills/*", authMiddleware);
5112
- app21.route("/pages", pages_default);
5113
- var routes = app21.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", 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);
5114
- var app_default = app21;
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;
5115
5134
 
5116
5135
  // src/web/server.ts
5117
5136
  var MIME_TYPES2 = {
@@ -5128,20 +5147,20 @@ async function startServer({
5128
5147
  hostname = "127.0.0.1"
5129
5148
  }) {
5130
5149
  let assetsDir = "";
5131
- let searchDir = dirname2(new URL(import.meta.url).pathname);
5150
+ let searchDir = dirname3(new URL(import.meta.url).pathname);
5132
5151
  for (let i = 0; i < 5; i++) {
5133
- const candidate = resolve17(searchDir, "dist", "web-assets");
5134
- if (existsSync11(candidate)) {
5152
+ const candidate = resolve18(searchDir, "dist", "web-assets");
5153
+ if (existsSync13(candidate)) {
5135
5154
  assetsDir = candidate;
5136
5155
  break;
5137
5156
  }
5138
- searchDir = dirname2(searchDir);
5157
+ searchDir = dirname3(searchDir);
5139
5158
  }
5140
5159
  if (assetsDir) {
5141
5160
  app_default.get("*", async (c) => {
5142
5161
  const urlPath = new URL(c.req.url).pathname;
5143
5162
  if (urlPath.startsWith("/api/")) return c.notFound();
5144
- const filePath = resolve17(assetsDir, urlPath.slice(1));
5163
+ const filePath = resolve18(assetsDir, urlPath.slice(1));
5145
5164
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
5146
5165
  const s = await stat2(filePath).catch(() => null);
5147
5166
  if (s?.isFile()) {
@@ -5150,7 +5169,7 @@ async function startServer({
5150
5169
  const body = await readFile3(filePath);
5151
5170
  return c.body(body, 200, { "Content-Type": mime });
5152
5171
  }
5153
- const indexPath = resolve17(assetsDir, "index.html");
5172
+ const indexPath = resolve18(assetsDir, "index.html");
5154
5173
  const indexStat = await stat2(indexPath).catch(() => null);
5155
5174
  if (indexStat?.isFile()) {
5156
5175
  const body = await readFile3(indexPath, "utf-8");
@@ -5160,10 +5179,10 @@ async function startServer({
5160
5179
  });
5161
5180
  }
5162
5181
  const server = serve({ fetch: app_default.fetch, port, hostname });
5163
- await new Promise((resolve19, reject) => {
5182
+ await new Promise((resolve20, reject) => {
5164
5183
  server.on("listening", () => {
5165
5184
  logger_default.info("Volute UI running", { hostname, port });
5166
- resolve19();
5185
+ resolve20();
5167
5186
  });
5168
5187
  server.on("error", (err) => {
5169
5188
  reject(err);
@@ -5174,14 +5193,14 @@ async function startServer({
5174
5193
 
5175
5194
  // src/daemon.ts
5176
5195
  if (!process.env.VOLUTE_HOME) {
5177
- process.env.VOLUTE_HOME = resolve18(homedir2(), ".volute");
5196
+ process.env.VOLUTE_HOME = resolve19(homedir2(), ".volute");
5178
5197
  }
5179
5198
  async function startDaemon(opts) {
5180
5199
  const { port, hostname } = opts;
5181
5200
  const myPid = String(process.pid);
5182
5201
  const home = voluteHome();
5183
5202
  if (!opts.foreground) {
5184
- const rotatingLog = new RotatingLog(resolve18(home, "daemon.log"));
5203
+ const rotatingLog = new RotatingLog(resolve19(home, "daemon.log"));
5185
5204
  logger_default.setOutput((line) => rotatingLog.write(`${line}
5186
5205
  `));
5187
5206
  const write = (...args) => rotatingLog.write(`${format(...args)}
@@ -5191,10 +5210,21 @@ async function startDaemon(opts) {
5191
5210
  console.warn = write;
5192
5211
  console.info = write;
5193
5212
  }
5194
- const DAEMON_PID_PATH = resolve18(home, "daemon.pid");
5195
- const DAEMON_JSON_PATH = resolve18(home, "daemon.json");
5196
- mkdirSync8(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 });
5197
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
+ }
5198
5228
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
5199
5229
  process.env.VOLUTE_DAEMON_TOKEN = token;
5200
5230
  process.env.VOLUTE_DAEMON_PORT = String(port);
@@ -5210,73 +5240,78 @@ async function startDaemon(opts) {
5210
5240
  }
5211
5241
  throw err;
5212
5242
  }
5213
- writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
5214
- writeFileSync8(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)}
5215
5245
  `, {
5216
5246
  mode: 420
5217
5247
  });
5248
+ const delivery = initDeliveryManager();
5218
5249
  const manager = initMindManager();
5219
5250
  manager.loadCrashAttempts();
5220
5251
  const connectors = initConnectorManager();
5221
- const scheduler = getScheduler();
5222
- scheduler.start(port, token);
5223
- const mailPoller = getMailPoller();
5224
- mailPoller.start(port, token);
5225
- const tokenBudget = getTokenBudget();
5226
- 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();
5227
5258
  const registry = readRegistry();
5228
5259
  for (const entry of registry) {
5229
5260
  try {
5261
+ migrateDotVoluteDir(entry.name);
5230
5262
  migrateMindState(entry.name);
5231
5263
  } catch (err) {
5232
5264
  logger_default.warn(`failed to migrate state for ${entry.name}`, logger_default.errorData(err));
5233
5265
  }
5234
5266
  }
5235
- for (const entry of registry) {
5236
- if (!entry.running) continue;
5237
- try {
5238
- await manager.startMind(entry.name);
5239
- if (entry.stage === "seed") continue;
5240
- const dir = mindDir(entry.name);
5241
- await connectors.startConnectors(entry.name, dir, entry.port, port);
5242
- scheduler.loadSchedules(entry.name);
5243
- ensureMailAddress(entry.name).catch(() => {
5244
- });
5245
- const config = readVoluteConfig(dir);
5246
- if (config?.tokenBudget) {
5247
- tokenBudget.setBudget(
5248
- entry.name,
5249
- config.tokenBudget,
5250
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
5251
- );
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
+ }
5252
5279
  }
5253
- } catch (err) {
5254
- logger_default.error(`failed to start mind ${entry.name}`, logger_default.errorData(err));
5255
- setMindRunning(entry.name, false);
5256
- }
5280
+ });
5281
+ await Promise.all(workers);
5257
5282
  }
5258
5283
  const runningVariants = getAllRunningVariants();
5259
- for (const { mindName, variant } of runningVariants) {
5260
- const compositeKey = `${mindName}@${variant.name}`;
5261
- try {
5262
- await manager.startMind(compositeKey);
5263
- } catch (err) {
5264
- logger_default.error(`failed to start variant ${compositeKey}`, logger_default.errorData(err));
5265
- setVariantRunning(mindName, variant.name, false);
5266
- }
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);
5267
5299
  }
5300
+ delivery.restoreFromDb().catch((err) => {
5301
+ logger_default.warn("failed to restore delivery queue", logger_default.errorData(err));
5302
+ });
5268
5303
  cleanExpiredSessions().catch(() => {
5269
5304
  });
5270
5305
  logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
5271
5306
  function cleanup() {
5272
5307
  try {
5273
- if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
5308
+ if (readFileSync11(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
5274
5309
  unlinkSync2(DAEMON_PID_PATH);
5275
5310
  }
5276
5311
  } catch {
5277
5312
  }
5278
5313
  try {
5279
- const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
5314
+ const data = JSON.parse(readFileSync11(DAEMON_JSON_PATH, "utf-8"));
5280
5315
  if (data.token === token) {
5281
5316
  unlinkSync2(DAEMON_JSON_PATH);
5282
5317
  }
@@ -5292,6 +5327,7 @@ async function startDaemon(opts) {
5292
5327
  scheduler.saveState();
5293
5328
  mailPoller.stop();
5294
5329
  tokenBudget.stop();
5330
+ delivery.dispose();
5295
5331
  await connectors.stopAll();
5296
5332
  await manager.stopAll();
5297
5333
  manager.clearCrashAttempts();