volute 0.16.0 → 0.18.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 (49) hide show
  1. package/dist/chunk-AYB7XAWO.js +812 -0
  2. package/dist/{chunk-3FD4ZZUL.js → chunk-FW5API7X.js} +116 -10
  3. package/dist/{chunk-3FC42ZBM.js → chunk-GK4E7LM7.js} +3 -0
  4. package/dist/cli.js +18 -6
  5. package/dist/connectors/discord.js +1 -1
  6. package/dist/connectors/slack.js +1 -1
  7. package/dist/connectors/telegram.js +1 -1
  8. package/dist/{daemon-restart-MS5FI44G.js → daemon-restart-2HVTHZAT.js} +1 -1
  9. package/dist/daemon.js +1443 -592
  10. package/dist/history-YUEKTJ2N.js +108 -0
  11. package/dist/{mind-manager-PN5SUDJ4.js → mind-manager-Z7O7PN2O.js} +1 -1
  12. package/dist/{package-3QGV3KX6.js → package-OKLFO7UY.js} +8 -9
  13. package/dist/{send-KBBZNYG6.js → send-BNDTLUPM.js} +41 -9
  14. package/dist/skill-2Y42P4JY.js +287 -0
  15. package/dist/{up-GZLWZAQE.js → up-7B3BWF2U.js} +1 -1
  16. package/dist/web-assets/assets/index-CtiimdWK.css +1 -0
  17. package/dist/web-assets/assets/index-kt1_EcuO.js +63 -0
  18. package/dist/web-assets/index.html +2 -1
  19. package/drizzle/0006_mind_history.sql +20 -0
  20. package/drizzle/0007_system_prompts.sql +5 -0
  21. package/drizzle/0008_volute_channels.sql +24 -0
  22. package/drizzle/0009_shared_skills.sql +9 -0
  23. package/drizzle/meta/0006_snapshot.json +7 -0
  24. package/drizzle/meta/0007_snapshot.json +7 -0
  25. package/drizzle/meta/0008_snapshot.json +7 -0
  26. package/drizzle/meta/0009_snapshot.json +7 -0
  27. package/drizzle/meta/_journal.json +28 -0
  28. package/package.json +8 -9
  29. package/templates/_base/.init/.config/prompts.json +5 -0
  30. package/templates/_base/_skills/volute-mind/SKILL.md +19 -5
  31. package/templates/_base/src/lib/daemon-client.ts +45 -0
  32. package/templates/_base/src/lib/logger.ts +19 -0
  33. package/templates/_base/src/lib/router.ts +48 -41
  34. package/templates/_base/src/lib/routing.ts +5 -8
  35. package/templates/_base/src/lib/startup.ts +43 -0
  36. package/templates/_base/src/lib/transparency.ts +89 -0
  37. package/templates/_base/src/lib/types.ts +0 -1
  38. package/templates/_base/src/lib/volute-server.ts +3 -35
  39. package/templates/claude/src/agent.ts +9 -22
  40. package/templates/claude/src/lib/hooks/reply-instructions.ts +6 -9
  41. package/templates/claude/src/lib/stream-consumer.ts +39 -12
  42. package/templates/pi/src/agent.ts +9 -22
  43. package/templates/pi/src/lib/event-handler.ts +58 -7
  44. package/templates/pi/src/lib/reply-instructions-extension.ts +6 -9
  45. package/dist/chunk-J52CJCVI.js +0 -447
  46. package/dist/history-LKCJJMUV.js +0 -50
  47. package/dist/web-assets/assets/index-B1XIIGCh.js +0 -307
  48. package/templates/_base/src/lib/auto-reply.ts +0 -38
  49. /package/dist/{chunk-LLBBVTEY.js → chunk-6DVBMLVN.js} +0 -0
package/dist/daemon.js CHANGED
@@ -10,13 +10,30 @@ import {
10
10
  readSystemsConfig
11
11
  } from "./chunk-37X7ECMF.js";
12
12
  import {
13
+ PROMPT_DEFAULTS,
14
+ PROMPT_KEYS,
13
15
  RotatingLog,
14
16
  clearJsonMap,
17
+ conversationParticipants,
18
+ conversations,
19
+ getDb,
15
20
  getMindManager,
21
+ getMindPromptDefaults,
22
+ getPrompt,
23
+ getPromptIfCustom,
16
24
  initMindManager,
17
25
  loadJsonMap,
18
- saveJsonMap
19
- } from "./chunk-J52CJCVI.js";
26
+ logBuffer,
27
+ logger_default,
28
+ messages,
29
+ mindHistory,
30
+ saveJsonMap,
31
+ sessions,
32
+ sharedSkills,
33
+ substitute,
34
+ systemPrompts,
35
+ users
36
+ } from "./chunk-AYB7XAWO.js";
20
37
  import {
21
38
  findOpenClawSession,
22
39
  importOpenClawConnectors,
@@ -35,7 +52,7 @@ import {
35
52
  import {
36
53
  CHANNELS,
37
54
  getChannelDriver
38
- } from "./chunk-3FD4ZZUL.js";
55
+ } from "./chunk-FW5API7X.js";
39
56
  import {
40
57
  exec,
41
58
  gitExec,
@@ -58,7 +75,7 @@ import "./chunk-D424ZQGI.js";
58
75
  import {
59
76
  buildVoluteSlug,
60
77
  writeChannelEntry
61
- } from "./chunk-3FC42ZBM.js";
78
+ } from "./chunk-GK4E7LM7.js";
62
79
  import {
63
80
  addMind,
64
81
  addVariant,
@@ -83,15 +100,13 @@ import {
83
100
  validateMindName,
84
101
  voluteHome
85
102
  } from "./chunk-M77QBTEH.js";
86
- import {
87
- __export
88
- } from "./chunk-K3NQKI34.js";
103
+ import "./chunk-K3NQKI34.js";
89
104
 
90
105
  // src/daemon.ts
91
106
  import { randomBytes } from "crypto";
92
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
107
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "fs";
93
108
  import { homedir as homedir2 } from "os";
94
- import { resolve as resolve17 } from "path";
109
+ import { resolve as resolve18 } from "path";
95
110
  import { format } from "util";
96
111
 
97
112
  // src/lib/connector-manager.ts
@@ -172,6 +187,7 @@ function checkMissingEnvVars(def, env) {
172
187
  }
173
188
 
174
189
  // src/lib/connector-manager.ts
190
+ var clog = logger_default.child("connectors");
175
191
  function searchUpwards(...segments) {
176
192
  let searchDir = dirname(new URL(import.meta.url).pathname);
177
193
  for (let i = 0; i < 5; i++) {
@@ -198,7 +214,7 @@ var ConnectorManager = class {
198
214
  try {
199
215
  await this.startConnector(mindName, mindDir2, mindPort, type, daemonPort);
200
216
  } catch (err) {
201
- console.error(`[daemon] failed to start connector ${type} for ${mindName}:`, err);
217
+ clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
202
218
  }
203
219
  }
204
220
  }
@@ -311,28 +327,26 @@ var ConnectorManager = class {
311
327
  }
312
328
  if (this.shuttingDown) return;
313
329
  if (this.stopping.has(stopKey)) return;
314
- console.error(`[daemon] connector ${type} for ${mindName} exited with code ${code}`);
315
- if (lastStderr) console.error(`[daemon] last output: ${lastStderr}`);
330
+ clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
331
+ if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
316
332
  const attempts = this.restartAttempts.get(stopKey) ?? 0;
317
333
  if (attempts >= MAX_RESTART_ATTEMPTS) {
318
- console.error(
319
- `[daemon] connector ${type} for ${mindName} crashed ${attempts} times \u2014 giving up`
320
- );
334
+ clog.error(`connector ${type} for ${mindName} crashed ${attempts} times \u2014 giving up`);
321
335
  return;
322
336
  }
323
337
  const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
324
338
  this.restartAttempts.set(stopKey, attempts + 1);
325
- console.error(
326
- `[daemon] restarting connector ${type} for ${mindName} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, in ${delay}ms`
339
+ clog.info(
340
+ `restarting connector ${type} for ${mindName} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, in ${delay}ms`
327
341
  );
328
342
  setTimeout(() => {
329
343
  if (this.shuttingDown || this.stopping.has(stopKey)) return;
330
344
  this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
331
- console.error(`[daemon] failed to restart connector ${type} for ${mindName}:`, err);
345
+ clog.error(`failed to restart connector ${type} for ${mindName}`, logger_default.errorData(err));
332
346
  });
333
347
  }, delay);
334
348
  });
335
- console.error(`[daemon] started connector ${type} for ${mindName}`);
349
+ clog.info(`started connector ${type} for ${mindName}`);
336
350
  }
337
351
  async stopConnector(mindName, type) {
338
352
  const mindMap = this.connectors.get(mindName);
@@ -342,19 +356,19 @@ var ConnectorManager = class {
342
356
  const stopKey = `${mindName}:${type}`;
343
357
  this.stopping.add(stopKey);
344
358
  mindMap.delete(type);
345
- await new Promise((resolve18) => {
346
- tracked.child.on("exit", () => resolve18());
359
+ await new Promise((resolve19) => {
360
+ tracked.child.on("exit", () => resolve19());
347
361
  try {
348
362
  tracked.child.kill("SIGTERM");
349
363
  } catch {
350
- resolve18();
364
+ resolve19();
351
365
  }
352
366
  setTimeout(() => {
353
367
  try {
354
368
  tracked.child.kill("SIGKILL");
355
369
  } catch {
356
370
  }
357
- resolve18();
371
+ resolve19();
358
372
  }, 5e3);
359
373
  });
360
374
  this.stopping.delete(stopKey);
@@ -362,9 +376,9 @@ var ConnectorManager = class {
362
376
  try {
363
377
  this.removeConnectorPid(mindName, type);
364
378
  } catch (err) {
365
- console.error(`[daemon] failed to remove PID file for ${type}/${mindName}:`, err);
379
+ clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
366
380
  }
367
- console.error(`[daemon] stopped connector ${type} for ${mindName}`);
381
+ clog.info(`stopped connector ${type} for ${mindName}`);
368
382
  }
369
383
  async stopConnectors(mindName) {
370
384
  const mindMap = this.connectors.get(mindName);
@@ -407,7 +421,7 @@ var ConnectorManager = class {
407
421
  const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
408
422
  if (pid > 0) {
409
423
  process.kill(pid, "SIGTERM");
410
- console.error(`[daemon] killed orphan connector ${type} (pid ${pid})`);
424
+ clog.warn(`killed orphan connector ${type} (pid ${pid})`);
411
425
  }
412
426
  } catch {
413
427
  }
@@ -435,6 +449,7 @@ function getConnectorManager() {
435
449
  }
436
450
 
437
451
  // src/lib/mail-poller.ts
452
+ var mlog = logger_default.child("mail");
438
453
  function formatEmailContent(email) {
439
454
  if (email.body) {
440
455
  return email.subject ? `Subject: ${email.subject}
@@ -448,89 +463,198 @@ ${email.body}` : email.body;
448
463
  }
449
464
  return email.subject ? `Subject: ${email.subject}` : "[Empty email]";
450
465
  }
466
+ var PING_INTERVAL_MS = 3e4;
467
+ var INITIAL_RECONNECT_MS = 1e3;
468
+ var MAX_RECONNECT_MS = 6e4;
451
469
  var MailPoller = class {
452
- interval = null;
470
+ ws = null;
453
471
  daemonPort = null;
454
472
  daemonToken = null;
455
- lastPoll = null;
456
473
  running = false;
474
+ pingTimer = null;
475
+ reconnectTimer = null;
476
+ reconnectDelay = INITIAL_RECONNECT_MS;
477
+ reconnectAttempts = 0;
478
+ disconnectedAt = null;
457
479
  start(daemonPort, daemonToken) {
458
480
  if (this.running) {
459
- console.error("[mail] already running \u2014 ignoring duplicate start");
481
+ mlog.warn("already running \u2014 ignoring duplicate start");
460
482
  return;
461
483
  }
462
484
  const config = readSystemsConfig();
463
485
  if (!config) {
464
- console.error("[mail] no systems config \u2014 mail polling disabled");
486
+ mlog.info("no systems config \u2014 mail disabled");
465
487
  return;
466
488
  }
467
489
  this.daemonPort = daemonPort ?? null;
468
490
  this.daemonToken = daemonToken ?? null;
469
- this.lastPoll = (/* @__PURE__ */ new Date()).toISOString();
470
491
  this.running = true;
471
- this.interval = setInterval(() => this.poll(), 3e4);
472
- console.error("[mail] polling started");
492
+ this.connect();
473
493
  }
474
494
  stop() {
475
- if (this.interval) clearInterval(this.interval);
476
- this.interval = null;
477
495
  this.running = false;
496
+ if (this.pingTimer) clearInterval(this.pingTimer);
497
+ this.pingTimer = null;
498
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
499
+ this.reconnectTimer = null;
500
+ if (this.ws) {
501
+ this.ws.close();
502
+ this.ws = null;
503
+ }
478
504
  }
479
505
  isRunning() {
480
506
  return this.running;
481
507
  }
482
- async poll() {
508
+ connect() {
509
+ if (!this.running) return;
483
510
  const config = readSystemsConfig();
484
511
  if (!config) {
485
- console.error("[mail] systems config removed \u2014 stopping mail polling");
512
+ mlog.info("systems config removed \u2014 stopping");
486
513
  this.stop();
487
514
  return;
488
515
  }
489
- const since = this.lastPoll ?? (/* @__PURE__ */ new Date()).toISOString();
490
- const url = `${config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
516
+ const wsUrl = `${config.apiUrl.replace(/^http/, "ws")}/api/ws`;
491
517
  try {
492
- const res = await fetch(url, {
518
+ this.ws = new WebSocket(wsUrl, {
493
519
  headers: { Authorization: `Bearer ${config.apiKey}` }
494
520
  });
521
+ } catch (err) {
522
+ mlog.warn("failed to create WebSocket", logger_default.errorData(err));
523
+ this.scheduleReconnect();
524
+ return;
525
+ }
526
+ this.ws.onopen = () => {
527
+ if (this.reconnectAttempts > 0) {
528
+ mlog.info(`reconnected after ${this.reconnectAttempts} attempts`);
529
+ }
530
+ mlog.info("connected");
531
+ this.reconnectAttempts = 0;
532
+ this.reconnectDelay = INITIAL_RECONNECT_MS;
533
+ if (this.disconnectedAt) {
534
+ this.catchUp(this.disconnectedAt);
535
+ this.disconnectedAt = null;
536
+ }
537
+ if (this.pingTimer) clearInterval(this.pingTimer);
538
+ this.pingTimer = setInterval(() => {
539
+ try {
540
+ if (this.ws?.readyState === WebSocket.OPEN) {
541
+ this.ws.send("ping");
542
+ }
543
+ } catch (err) {
544
+ mlog.warn("ping failed", logger_default.errorData(err));
545
+ }
546
+ }, PING_INTERVAL_MS);
547
+ };
548
+ this.ws.onmessage = (event) => {
549
+ this.handleMessage(String(event.data));
550
+ };
551
+ this.ws.onclose = () => {
552
+ mlog.warn("disconnected");
553
+ if (!this.disconnectedAt) {
554
+ this.disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
555
+ }
556
+ this.cleanup();
557
+ this.scheduleReconnect();
558
+ };
559
+ this.ws.onerror = (err) => {
560
+ mlog.warn("WebSocket error", logger_default.errorData(err));
561
+ };
562
+ }
563
+ cleanup() {
564
+ if (this.pingTimer) clearInterval(this.pingTimer);
565
+ this.pingTimer = null;
566
+ this.ws = null;
567
+ }
568
+ scheduleReconnect() {
569
+ if (!this.running) return;
570
+ this.reconnectAttempts++;
571
+ if (this.reconnectAttempts % 10 === 0) {
572
+ mlog.warn(
573
+ `failed to connect ${this.reconnectAttempts} times \u2014 check systems config and network`
574
+ );
575
+ }
576
+ mlog.info(`reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
577
+ this.reconnectTimer = setTimeout(() => {
578
+ this.reconnectTimer = null;
579
+ this.connect();
580
+ }, this.reconnectDelay);
581
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_MS);
582
+ }
583
+ /** Fetch emails that arrived while disconnected */
584
+ catchUp(since) {
585
+ const config = readSystemsConfig();
586
+ if (!config) return;
587
+ const url = `${config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
588
+ fetch(url, {
589
+ headers: { Authorization: `Bearer ${config.apiKey}` }
590
+ }).then(async (res) => {
495
591
  if (!res.ok) {
496
- console.error(`[mail] poll failed: HTTP ${res.status}`);
592
+ mlog.warn(`catch-up poll failed: HTTP ${res.status}`);
497
593
  return;
498
594
  }
499
595
  const data = await res.json();
500
- if (!Array.isArray(data.emails)) {
501
- console.error("[mail] poll response missing emails array");
502
- return;
503
- }
596
+ if (!Array.isArray(data.emails) || data.emails.length === 0) return;
597
+ mlog.info(`catching up on ${data.emails.length} missed emails`);
504
598
  for (const email of data.emails) {
505
599
  await this.deliver(email.mind, email);
506
600
  }
507
- if (data.emails.length > 0) {
508
- this.lastPoll = data.emails[data.emails.length - 1].receivedAt;
509
- } else {
510
- this.lastPoll = (/* @__PURE__ */ new Date()).toISOString();
511
- }
512
- } catch (err) {
513
- console.error("[mail] poll error:", err);
601
+ }).catch((err) => {
602
+ mlog.warn("catch-up error", logger_default.errorData(err));
603
+ });
604
+ }
605
+ handleMessage(data) {
606
+ if (data === "pong") return;
607
+ let msg;
608
+ try {
609
+ msg = JSON.parse(data);
610
+ } catch {
611
+ mlog.warn(`received unparseable message: ${data.slice(0, 200)}`);
612
+ return;
613
+ }
614
+ if (msg.type !== "email") return;
615
+ if (!msg.mind || !msg.email?.id) {
616
+ mlog.warn(`received malformed email notification: ${data.slice(0, 500)}`);
617
+ return;
618
+ }
619
+ this.fetchAndDeliver(msg.mind, msg.email).catch((err) => {
620
+ mlog.warn(`failed to process email for ${msg.mind}`, logger_default.errorData(err));
621
+ });
622
+ }
623
+ async fetchAndDeliver(mind, notification) {
624
+ const config = readSystemsConfig();
625
+ if (!config) {
626
+ mlog.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
627
+ return;
628
+ }
629
+ const url = `${config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
630
+ const res = await fetch(url, {
631
+ headers: { Authorization: `Bearer ${config.apiKey}` }
632
+ });
633
+ if (!res.ok) {
634
+ mlog.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
635
+ return;
514
636
  }
637
+ const email = await res.json();
638
+ await this.deliver(mind, { ...email, mind });
515
639
  }
516
640
  async deliver(mind, email) {
517
641
  const entry = findMind(mind);
518
642
  if (!entry || !entry.running) {
519
- console.error(`[mail] skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
643
+ mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
520
644
  return;
521
645
  }
522
646
  const channel = `mail:${email.from.address}`;
523
647
  const sender = email.from.name || email.from.address;
524
- const text2 = formatEmailContent(email);
648
+ const text = formatEmailContent(email);
525
649
  const body = JSON.stringify({
526
- content: [{ type: "text", text: text2 }],
650
+ content: [{ type: "text", text }],
527
651
  channel,
528
652
  sender,
529
653
  platform: "Email",
530
654
  isDM: true
531
655
  });
532
656
  if (!this.daemonPort || !this.daemonToken) {
533
- console.error(`[mail] cannot deliver to ${mind}: daemon port/token not set`);
657
+ mlog.warn(`cannot deliver to ${mind}: daemon port/token not set`);
534
658
  return;
535
659
  }
536
660
  const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
@@ -548,14 +672,14 @@ var MailPoller = class {
548
672
  signal: controller.signal
549
673
  });
550
674
  if (!res.ok) {
551
- console.error(`[mail] deliver to ${mind} got HTTP ${res.status}`);
675
+ mlog.warn(`deliver to ${mind} got HTTP ${res.status}`);
552
676
  } else {
553
- console.error(`[mail] delivered email from ${email.from.address} to ${mind}`);
677
+ mlog.info(`delivered email from ${email.from.address} to ${mind}`);
554
678
  }
555
679
  await res.text().catch(() => {
556
680
  });
557
681
  } catch (err) {
558
- console.error(`[mail] failed to deliver to ${mind}:`, err);
682
+ mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
559
683
  } finally {
560
684
  clearTimeout(timeout);
561
685
  }
@@ -578,12 +702,12 @@ async function ensureMailAddress(mindName) {
578
702
  }
579
703
  });
580
704
  if (!res.ok) {
581
- console.error(`[mail] failed to ensure address for ${mindName}: HTTP ${res.status}`);
705
+ mlog.warn(`failed to ensure address for ${mindName}: HTTP ${res.status}`);
582
706
  }
583
707
  await res.text().catch(() => {
584
708
  });
585
709
  } catch (err) {
586
- console.error(`[mail] failed to ensure address for ${mindName}:`, err);
710
+ mlog.warn(`failed to ensure address for ${mindName}`, logger_default.errorData(err));
587
711
  }
588
712
  }
589
713
 
@@ -753,6 +877,7 @@ function migrateMindState(name) {
753
877
  // src/lib/scheduler.ts
754
878
  import { resolve as resolve5 } from "path";
755
879
  import { CronExpressionParser } from "cron-parser";
880
+ var slog = logger_default.child("scheduler");
756
881
  var Scheduler = class {
757
882
  schedules = /* @__PURE__ */ new Map();
758
883
  interval = null;
@@ -824,7 +949,7 @@ var Scheduler = class {
824
949
  }
825
950
  return false;
826
951
  } catch (err) {
827
- console.error(`[scheduler] invalid cron "${schedule.cron}" for ${mind}:${schedule.id}:`, err);
952
+ slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
828
953
  return false;
829
954
  }
830
955
  }
@@ -861,14 +986,14 @@ var Scheduler = class {
861
986
  });
862
987
  }
863
988
  if (!res.ok) {
864
- console.error(`[scheduler] "${schedule.id}" for ${mindName} got HTTP ${res.status}`);
989
+ slog.warn(`"${schedule.id}" for ${mindName} got HTTP ${res.status}`);
865
990
  } else {
866
- console.error(`[scheduler] fired "${schedule.id}" for ${mindName}`);
991
+ slog.info(`fired "${schedule.id}" for ${mindName}`);
867
992
  }
868
993
  await res.text().catch(() => {
869
994
  });
870
995
  } catch (err) {
871
- console.error(`[scheduler] failed to fire "${schedule.id}" for ${mindName}:`, err);
996
+ slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
872
997
  } finally {
873
998
  clearTimeout(timeout);
874
999
  }
@@ -881,6 +1006,7 @@ function getScheduler() {
881
1006
  }
882
1007
 
883
1008
  // src/lib/token-budget.ts
1009
+ var tlog = logger_default.child("token-budget");
884
1010
  var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
885
1011
  var MAX_QUEUE_SIZE = 100;
886
1012
  var TokenBudget = class {
@@ -974,7 +1100,7 @@ var TokenBudget = class {
974
1100
  const queued = this.drain(mind);
975
1101
  if (queued.length > 0) {
976
1102
  this.replay(mind, queued).catch((err) => {
977
- console.error(`[token-budget] replay error for ${mind}:`, err);
1103
+ tlog.warn(`replay error for ${mind}`, logger_default.errorData(err));
978
1104
  });
979
1105
  }
980
1106
  }
@@ -982,8 +1108,8 @@ var TokenBudget = class {
982
1108
  }
983
1109
  async replay(mindName, messages2) {
984
1110
  if (!this.daemonPort || !this.daemonToken) {
985
- console.error(
986
- `[token-budget] cannot replay ${messages2.length} message(s) for ${mindName}: daemon not configured`
1111
+ tlog.warn(
1112
+ `cannot replay ${messages2.length} message(s) for ${mindName}: daemon not configured`
987
1113
  );
988
1114
  const state = this.budgets.get(mindName);
989
1115
  if (state) state.queue.push(...messages2);
@@ -1021,16 +1147,14 @@ ${summary}`
1021
1147
  signal: controller.signal
1022
1148
  });
1023
1149
  if (!res.ok) {
1024
- console.error(`[token-budget] replay for ${mindName} got HTTP ${res.status}`);
1150
+ tlog.warn(`replay for ${mindName} got HTTP ${res.status}`);
1025
1151
  } else {
1026
- console.error(
1027
- `[token-budget] replayed ${messages2.length} queued message(s) for ${mindName}`
1028
- );
1152
+ tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
1029
1153
  }
1030
1154
  await res.text().catch(() => {
1031
1155
  });
1032
1156
  } catch (err) {
1033
- console.error(`[token-budget] failed to replay for ${mindName}:`, err);
1157
+ tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
1034
1158
  const state = this.budgets.get(mindName);
1035
1159
  if (state) state.queue.push(...messages2);
1036
1160
  } finally {
@@ -1053,124 +1177,12 @@ import { createMiddleware } from "hono/factory";
1053
1177
  // src/lib/auth.ts
1054
1178
  import { compareSync, hashSync } from "bcryptjs";
1055
1179
  import { and, count, eq } from "drizzle-orm";
1056
-
1057
- // src/lib/db.ts
1058
- import { chmodSync, existsSync as existsSync5 } from "fs";
1059
- import { dirname as dirname2, resolve as resolve6 } from "path";
1060
- import { fileURLToPath } from "url";
1061
- import { drizzle } from "drizzle-orm/libsql";
1062
- import { migrate } from "drizzle-orm/libsql/migrator";
1063
-
1064
- // src/lib/schema.ts
1065
- var schema_exports = {};
1066
- __export(schema_exports, {
1067
- conversationParticipants: () => conversationParticipants,
1068
- conversations: () => conversations,
1069
- messages: () => messages,
1070
- mindMessages: () => mindMessages,
1071
- sessions: () => sessions,
1072
- users: () => users
1073
- });
1074
- import { sql } from "drizzle-orm";
1075
- import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
1076
- var users = sqliteTable("users", {
1077
- id: integer("id").primaryKey({ autoIncrement: true }),
1078
- username: text("username").unique().notNull(),
1079
- password_hash: text("password_hash").notNull(),
1080
- role: text("role").notNull().default("pending"),
1081
- user_type: text("user_type").notNull().default("human"),
1082
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1083
- });
1084
- var conversations = sqliteTable(
1085
- "conversations",
1086
- {
1087
- id: text("id").primaryKey(),
1088
- mind_name: text("mind_name").notNull(),
1089
- channel: text("channel").notNull(),
1090
- user_id: integer("user_id").references(() => users.id),
1091
- title: text("title"),
1092
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
1093
- updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
1094
- },
1095
- (table) => [
1096
- index("idx_conversations_mind_name").on(table.mind_name),
1097
- index("idx_conversations_user_id").on(table.user_id),
1098
- index("idx_conversations_updated_at").on(table.updated_at)
1099
- ]
1100
- );
1101
- var mindMessages = sqliteTable(
1102
- "mind_messages",
1103
- {
1104
- id: integer("id").primaryKey({ autoIncrement: true }),
1105
- mind: text("mind").notNull(),
1106
- channel: text("channel").notNull(),
1107
- sender: text("sender"),
1108
- content: text("content").notNull(),
1109
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1110
- },
1111
- (table) => [
1112
- index("idx_mind_messages_mind").on(table.mind),
1113
- index("idx_mind_messages_channel").on(table.mind, table.channel)
1114
- ]
1115
- );
1116
- var conversationParticipants = sqliteTable(
1117
- "conversation_participants",
1118
- {
1119
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
1120
- user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1121
- role: text("role").notNull().default("member"),
1122
- joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
1123
- },
1124
- (table) => [
1125
- uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
1126
- index("idx_cp_user_id").on(table.user_id)
1127
- ]
1128
- );
1129
- var sessions = sqliteTable("sessions", {
1130
- id: text("id").primaryKey(),
1131
- userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
1132
- createdAt: integer("created_at").notNull()
1133
- });
1134
- var messages = sqliteTable(
1135
- "messages",
1136
- {
1137
- id: integer("id").primaryKey({ autoIncrement: true }),
1138
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
1139
- role: text("role").notNull(),
1140
- sender_name: text("sender_name"),
1141
- content: text("content").notNull(),
1142
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1143
- },
1144
- (table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
1145
- );
1146
-
1147
- // src/lib/db.ts
1148
- var __dirname = dirname2(fileURLToPath(import.meta.url));
1149
- var migrationsFolder = existsSync5(resolve6(__dirname, "../drizzle")) ? resolve6(__dirname, "../drizzle") : resolve6(__dirname, "../../drizzle");
1150
- var db = null;
1151
- async function getDb() {
1152
- if (db) return db;
1153
- const dbPath = process.env.VOLUTE_DB_PATH || resolve6(voluteHome(), "volute.db");
1154
- db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
1155
- await migrate(db, { migrationsFolder });
1156
- try {
1157
- chmodSync(dbPath, 384);
1158
- } catch (err) {
1159
- console.error(
1160
- `[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
1161
- err
1162
- );
1163
- }
1164
- return db;
1165
- }
1166
-
1167
- // src/lib/auth.ts
1168
1180
  async function createUser(username, password) {
1169
- const db2 = await getDb();
1181
+ const db = await getDb();
1170
1182
  const hash = hashSync(password, 10);
1171
- const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
1183
+ const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
1172
1184
  const role = value === 0 ? "admin" : "pending";
1173
- const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
1185
+ const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
1174
1186
  id: users.id,
1175
1187
  username: users.username,
1176
1188
  role: users.role,
@@ -1180,8 +1192,8 @@ async function createUser(username, password) {
1180
1192
  return result;
1181
1193
  }
1182
1194
  async function verifyUser(username, password) {
1183
- const db2 = await getDb();
1184
- const row = await db2.select().from(users).where(eq(users.username, username)).get();
1195
+ const db = await getDb();
1196
+ const row = await db.select().from(users).where(eq(users.username, username)).get();
1185
1197
  if (!row) return null;
1186
1198
  if (row.user_type === "mind") return null;
1187
1199
  if (!compareSync(password, row.password_hash)) return null;
@@ -1189,8 +1201,8 @@ async function verifyUser(username, password) {
1189
1201
  return user;
1190
1202
  }
1191
1203
  async function getUser(id) {
1192
- const db2 = await getDb();
1193
- const row = await db2.select({
1204
+ const db = await getDb();
1205
+ const row = await db.select({
1194
1206
  id: users.id,
1195
1207
  username: users.username,
1196
1208
  role: users.role,
@@ -1200,8 +1212,8 @@ async function getUser(id) {
1200
1212
  return row ?? null;
1201
1213
  }
1202
1214
  async function getUserByUsername(username) {
1203
- const db2 = await getDb();
1204
- const row = await db2.select({
1215
+ const db = await getDb();
1216
+ const row = await db.select({
1205
1217
  id: users.id,
1206
1218
  username: users.username,
1207
1219
  role: users.role,
@@ -1211,8 +1223,8 @@ async function getUserByUsername(username) {
1211
1223
  return row ?? null;
1212
1224
  }
1213
1225
  async function listUsers() {
1214
- const db2 = await getDb();
1215
- return db2.select({
1226
+ const db = await getDb();
1227
+ return db.select({
1216
1228
  id: users.id,
1217
1229
  username: users.username,
1218
1230
  role: users.role,
@@ -1221,8 +1233,8 @@ async function listUsers() {
1221
1233
  }).from(users).orderBy(users.created_at).all();
1222
1234
  }
1223
1235
  async function listPendingUsers() {
1224
- const db2 = await getDb();
1225
- return db2.select({
1236
+ const db = await getDb();
1237
+ return db.select({
1226
1238
  id: users.id,
1227
1239
  username: users.username,
1228
1240
  role: users.role,
@@ -1231,8 +1243,8 @@ async function listPendingUsers() {
1231
1243
  }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
1232
1244
  }
1233
1245
  async function listUsersByType(userType) {
1234
- const db2 = await getDb();
1235
- return db2.select({
1246
+ const db = await getDb();
1247
+ return db.select({
1236
1248
  id: users.id,
1237
1249
  username: users.username,
1238
1250
  role: users.role,
@@ -1241,8 +1253,8 @@ async function listUsersByType(userType) {
1241
1253
  }).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
1242
1254
  }
1243
1255
  async function getOrCreateMindUser(mindName) {
1244
- const db2 = await getDb();
1245
- const existing = await db2.select({
1256
+ const db = await getDb();
1257
+ const existing = await db.select({
1246
1258
  id: users.id,
1247
1259
  username: users.username,
1248
1260
  role: users.role,
@@ -1251,7 +1263,7 @@ async function getOrCreateMindUser(mindName) {
1251
1263
  }).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
1252
1264
  if (existing) return existing;
1253
1265
  try {
1254
- const [result] = await db2.insert(users).values({
1266
+ const [result] = await db.insert(users).values({
1255
1267
  username: mindName,
1256
1268
  password_hash: "!mind",
1257
1269
  role: "mind",
@@ -1266,7 +1278,7 @@ async function getOrCreateMindUser(mindName) {
1266
1278
  return result;
1267
1279
  } catch (err) {
1268
1280
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
1269
- const retried = await db2.select({
1281
+ const retried = await db.select({
1270
1282
  id: users.id,
1271
1283
  username: users.username,
1272
1284
  role: users.role,
@@ -1279,12 +1291,21 @@ async function getOrCreateMindUser(mindName) {
1279
1291
  }
1280
1292
  }
1281
1293
  async function deleteMindUser2(mindName) {
1282
- const db2 = await getDb();
1283
- await db2.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
1294
+ const db = await getDb();
1295
+ await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
1296
+ }
1297
+ async function changePassword(userId, currentPassword, newPassword) {
1298
+ const db = await getDb();
1299
+ const row = await db.select().from(users).where(eq(users.id, userId)).get();
1300
+ if (!row) return false;
1301
+ if (!compareSync(currentPassword, row.password_hash)) return false;
1302
+ const hash = hashSync(newPassword, 10);
1303
+ await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
1304
+ return true;
1284
1305
  }
1285
1306
  async function approveUser(id) {
1286
- const db2 = await getDb();
1287
- await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
1307
+ const db = await getDb();
1308
+ await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
1288
1309
  }
1289
1310
 
1290
1311
  // src/web/middleware/auth.ts
@@ -1295,29 +1316,29 @@ function isValidDaemonToken(token) {
1295
1316
  }
1296
1317
  var SESSION_MAX_AGE = 864e5;
1297
1318
  async function createSession(userId) {
1298
- const db2 = await getDb();
1319
+ const db = await getDb();
1299
1320
  const sessionId = crypto.randomUUID();
1300
- await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
1321
+ await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
1301
1322
  return sessionId;
1302
1323
  }
1303
1324
  async function deleteSession(sessionId) {
1304
- const db2 = await getDb();
1305
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
1325
+ const db = await getDb();
1326
+ await db.delete(sessions).where(eq2(sessions.id, sessionId));
1306
1327
  }
1307
1328
  async function getSessionUserId(sessionId) {
1308
- const db2 = await getDb();
1309
- const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1329
+ const db = await getDb();
1330
+ const row = await db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1310
1331
  if (!row) return void 0;
1311
1332
  if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
1312
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
1333
+ await db.delete(sessions).where(eq2(sessions.id, sessionId));
1313
1334
  return void 0;
1314
1335
  }
1315
1336
  return row.userId;
1316
1337
  }
1317
1338
  async function cleanExpiredSessions() {
1318
- const db2 = await getDb();
1339
+ const db = await getDb();
1319
1340
  const cutoff = Date.now() - SESSION_MAX_AGE;
1320
- await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
1341
+ await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
1321
1342
  }
1322
1343
  var requireAdmin = createMiddleware(async (c, next) => {
1323
1344
  const user = c.get("user");
@@ -1348,62 +1369,18 @@ var authMiddleware = createMiddleware(async (c, next) => {
1348
1369
  });
1349
1370
 
1350
1371
  // src/web/server.ts
1351
- import { existsSync as existsSync10 } from "fs";
1372
+ import { existsSync as existsSync11 } from "fs";
1352
1373
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
1353
- import { dirname as dirname3, extname as extname2, resolve as resolve16 } from "path";
1374
+ import { dirname as dirname2, extname as extname2, resolve as resolve17 } from "path";
1354
1375
  import { serve } from "@hono/node-server";
1355
1376
 
1356
- // src/lib/log-buffer.ts
1357
- var LogBuffer = class {
1358
- entries = [];
1359
- maxSize = 1e3;
1360
- subscribers = /* @__PURE__ */ new Set();
1361
- append(entry) {
1362
- this.entries.push(entry);
1363
- if (this.entries.length > this.maxSize) {
1364
- this.entries.shift();
1365
- }
1366
- for (const sub of this.subscribers) {
1367
- sub(entry);
1368
- }
1369
- }
1370
- getEntries() {
1371
- return [...this.entries];
1372
- }
1373
- subscribe(fn) {
1374
- this.subscribers.add(fn);
1375
- return () => this.subscribers.delete(fn);
1376
- }
1377
- };
1378
- var logBuffer = new LogBuffer();
1379
-
1380
- // src/lib/logger.ts
1381
- function write(level, msg, data) {
1382
- const entry = {
1383
- level,
1384
- msg,
1385
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1386
- ...data ? { data } : {}
1387
- };
1388
- const line = JSON.stringify(entry);
1389
- process.stderr.write(`${line}
1390
- `);
1391
- logBuffer.append(entry);
1392
- }
1393
- var log2 = {
1394
- info: (msg, data) => write("info", msg, data),
1395
- warn: (msg, data) => write("warn", msg, data),
1396
- error: (msg, data) => write("error", msg, data)
1397
- };
1398
- var logger_default = log2;
1399
-
1400
1377
  // src/web/app.ts
1401
- import { Hono as Hono17 } from "hono";
1378
+ import { Hono as Hono21 } from "hono";
1402
1379
  import { bodyLimit } from "hono/body-limit";
1403
1380
  import { csrf } from "hono/csrf";
1404
1381
  import { HTTPException } from "hono/http-exception";
1405
1382
 
1406
- // src/web/routes/auth.ts
1383
+ // src/web/api/auth.ts
1407
1384
  import { zValidator } from "@hono/zod-validator";
1408
1385
  import { Hono } from "hono";
1409
1386
  import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
@@ -1412,6 +1389,17 @@ var credentialsSchema = z.object({
1412
1389
  username: z.string().min(1),
1413
1390
  password: z.string().min(1)
1414
1391
  });
1392
+ var changePasswordSchema = z.object({
1393
+ currentPassword: z.string().min(1),
1394
+ newPassword: z.string().min(1)
1395
+ });
1396
+ var authenticated = new Hono().use(authMiddleware).post("/change-password", zValidator("json", changePasswordSchema), async (c) => {
1397
+ const user = c.get("user");
1398
+ const { currentPassword, newPassword } = c.req.valid("json");
1399
+ const ok = await changePassword(user.id, currentPassword, newPassword);
1400
+ if (!ok) return c.json({ error: "Current password is incorrect" }, 400);
1401
+ return c.json({ ok: true });
1402
+ });
1415
1403
  var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
1416
1404
  const user = c.get("user");
1417
1405
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
@@ -1471,10 +1459,10 @@ var app = new Hono().post("/register", zValidator("json", credentialsSchema), as
1471
1459
  const user = await getUser(userId);
1472
1460
  if (!user) return c.json({ error: "Not logged in" }, 401);
1473
1461
  return c.json({ id: user.id, username: user.username, role: user.role });
1474
- }).route("/", admin);
1462
+ }).route("/", admin).route("/", authenticated);
1475
1463
  var auth_default = app;
1476
1464
 
1477
- // src/web/routes/channels.ts
1465
+ // src/web/api/channels.ts
1478
1466
  import { Hono as Hono2 } from "hono";
1479
1467
  function buildEnv(name) {
1480
1468
  return { ...loadMergedEnv(name), VOLUTE_MIND: name, VOLUTE_MIND_DIR: mindDir(name) };
@@ -1482,12 +1470,12 @@ function buildEnv(name) {
1482
1470
  var app2 = new Hono2().post("/:name/channels/send", requireAdmin, async (c) => {
1483
1471
  const name = c.req.param("name");
1484
1472
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
1485
- const { platform, uri, message } = await c.req.json();
1473
+ const { platform, uri, message, images } = await c.req.json();
1486
1474
  const driver = getChannelDriver(platform);
1487
1475
  if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
1488
1476
  const env = buildEnv(name);
1489
1477
  try {
1490
- await driver.send(env, uri, message);
1478
+ await driver.send(env, uri, message, images);
1491
1479
  return c.json({ ok: true });
1492
1480
  } catch (err) {
1493
1481
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
@@ -1571,7 +1559,7 @@ var app2 = new Hono2().post("/:name/channels/send", requireAdmin, async (c) => {
1571
1559
  });
1572
1560
  var channels_default = app2;
1573
1561
 
1574
- // src/web/routes/connectors.ts
1562
+ // src/web/api/connectors.ts
1575
1563
  import { Hono as Hono3 } from "hono";
1576
1564
  var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
1577
1565
  var app3 = new Hono3().get("/:name/connectors", (c) => {
@@ -1650,7 +1638,7 @@ var app3 = new Hono3().get("/:name/connectors", (c) => {
1650
1638
  });
1651
1639
  var connectors_default = app3;
1652
1640
 
1653
- // src/web/routes/env.ts
1641
+ // src/web/api/env.ts
1654
1642
  import { Hono as Hono4 } from "hono";
1655
1643
  var app4 = new Hono4().get("/:name/env", (c) => {
1656
1644
  const name = c.req.param("name");
@@ -1724,10 +1712,10 @@ var sharedEnvApp = new Hono4().get("/", (c) => {
1724
1712
  });
1725
1713
  var env_default = app4;
1726
1714
 
1727
- // src/web/routes/files.ts
1728
- import { existsSync as existsSync6 } from "fs";
1715
+ // src/web/api/files.ts
1716
+ import { existsSync as existsSync5 } from "fs";
1729
1717
  import { readdir, readFile } from "fs/promises";
1730
- import { resolve as resolve7 } from "path";
1718
+ import { resolve as resolve6 } from "path";
1731
1719
  import { Hono as Hono5 } from "hono";
1732
1720
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1733
1721
  var app5 = new Hono5().get("/:name/files", async (c) => {
@@ -1735,8 +1723,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1735
1723
  const entry = findMind(name);
1736
1724
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1737
1725
  const dir = mindDir(name);
1738
- const homeDir = resolve7(dir, "home");
1739
- if (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1726
+ const homeDir = resolve6(dir, "home");
1727
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1740
1728
  const allFiles = await readdir(homeDir);
1741
1729
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1742
1730
  return c.json(files);
@@ -1749,8 +1737,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1749
1737
  const entry = findMind(name);
1750
1738
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1751
1739
  const dir = mindDir(name);
1752
- const filePath = resolve7(dir, "home", filename);
1753
- if (!existsSync6(filePath)) {
1740
+ const filePath = resolve6(dir, "home", filename);
1741
+ if (!existsSync5(filePath)) {
1754
1742
  return c.json({ error: "File not found" }, 404);
1755
1743
  }
1756
1744
  const content = await readFile(filePath, "utf-8");
@@ -1758,18 +1746,18 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1758
1746
  });
1759
1747
  var files_default = app5;
1760
1748
 
1761
- // src/web/routes/logs.ts
1749
+ // src/web/api/logs.ts
1762
1750
  import { spawn as spawn2 } from "child_process";
1763
- import { existsSync as existsSync7 } from "fs";
1764
- import { resolve as resolve8 } from "path";
1751
+ import { existsSync as existsSync6 } from "fs";
1752
+ import { resolve as resolve7 } from "path";
1765
1753
  import { Hono as Hono6 } from "hono";
1766
1754
  import { streamSSE } from "hono/streaming";
1767
1755
  var app6 = new Hono6().get("/:name/logs", async (c) => {
1768
1756
  const name = c.req.param("name");
1769
1757
  const entry = findMind(name);
1770
1758
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1771
- const logFile = resolve8(stateDir(name), "logs", "mind.log");
1772
- if (!existsSync7(logFile)) {
1759
+ const logFile = resolve7(stateDir(name), "logs", "mind.log");
1760
+ if (!existsSync6(logFile)) {
1773
1761
  return c.json({ error: "No log file found" }, 404);
1774
1762
  }
1775
1763
  return streamSSE(c, async (stream) => {
@@ -1787,17 +1775,17 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1787
1775
  stream.onAbort(() => {
1788
1776
  tail.kill();
1789
1777
  });
1790
- await new Promise((resolve18) => {
1791
- tail.on("exit", resolve18);
1792
- stream.onAbort(resolve18);
1778
+ await new Promise((resolve19) => {
1779
+ tail.on("exit", resolve19);
1780
+ stream.onAbort(resolve19);
1793
1781
  });
1794
1782
  });
1795
1783
  }).get("/:name/logs/tail", async (c) => {
1796
1784
  const name = c.req.param("name");
1797
1785
  const entry = findMind(name);
1798
1786
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1799
- const logFile = resolve8(stateDir(name), "logs", "mind.log");
1800
- if (!existsSync7(logFile)) {
1787
+ const logFile = resolve7(stateDir(name), "logs", "mind.log");
1788
+ if (!existsSync6(logFile)) {
1801
1789
  return c.json({ error: "No log file found" }, 404);
1802
1790
  }
1803
1791
  const nParam = parseInt(c.req.query("n") ?? "50", 10);
@@ -1807,44 +1795,421 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1807
1795
  tail.stdout.on("data", (data) => {
1808
1796
  output += data.toString();
1809
1797
  });
1810
- await new Promise((resolve18) => {
1811
- tail.on("exit", resolve18);
1798
+ await new Promise((resolve19) => {
1799
+ tail.on("exit", resolve19);
1812
1800
  });
1813
1801
  return c.text(output);
1814
1802
  });
1815
1803
  var logs_default = app6;
1816
1804
 
1817
- // src/web/routes/minds.ts
1805
+ // src/web/api/mind-skills.ts
1806
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1807
+ import { Hono as Hono7 } from "hono";
1808
+ import { z as z2 } from "zod";
1809
+
1810
+ // src/lib/skills.ts
1818
1811
  import {
1819
1812
  cpSync,
1820
- existsSync as existsSync8,
1821
- mkdirSync as mkdirSync4,
1822
- readdirSync as readdirSync3,
1823
- readFileSync as readFileSync6,
1813
+ existsSync as existsSync7,
1814
+ mkdirSync as mkdirSync3,
1815
+ readdirSync as readdirSync2,
1816
+ readFileSync as readFileSync4,
1824
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) => {
2106
+ const name = c.req.param("name");
2107
+ const entry = findMind(name);
2108
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2109
+ const dir = mindDir(name);
2110
+ const skills = await listMindSkills(dir);
2111
+ return c.json(skills);
2112
+ }).post(
2113
+ "/:name/skills/install",
2114
+ requireAdmin,
2115
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2116
+ async (c) => {
2117
+ const name = c.req.param("name");
2118
+ const entry = findMind(name);
2119
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2120
+ const { skillId } = c.req.valid("json");
2121
+ const dir = mindDir(name);
2122
+ try {
2123
+ await installSkill(name, dir, skillId);
2124
+ } catch (e) {
2125
+ const msg = e instanceof Error ? e.message : String(e);
2126
+ return c.json({ error: msg }, 400);
2127
+ }
2128
+ return c.json({ ok: true });
2129
+ }
2130
+ ).post(
2131
+ "/:name/skills/update",
2132
+ requireAdmin,
2133
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2134
+ async (c) => {
2135
+ const name = c.req.param("name");
2136
+ const entry = findMind(name);
2137
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2138
+ const { skillId } = c.req.valid("json");
2139
+ const dir = mindDir(name);
2140
+ try {
2141
+ const result = await updateSkill(name, dir, skillId);
2142
+ return c.json(result);
2143
+ } catch (e) {
2144
+ const msg = e instanceof Error ? e.message : String(e);
2145
+ return c.json({ error: msg }, 400);
2146
+ }
2147
+ }
2148
+ ).post(
2149
+ "/:name/skills/publish",
2150
+ requireAdmin,
2151
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2152
+ async (c) => {
2153
+ const name = c.req.param("name");
2154
+ const entry = findMind(name);
2155
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2156
+ const { skillId } = c.req.valid("json");
2157
+ const dir = mindDir(name);
2158
+ try {
2159
+ const skill = await publishSkill(name, dir, skillId);
2160
+ return c.json(skill);
2161
+ } catch (e) {
2162
+ const msg = e instanceof Error ? e.message : String(e);
2163
+ return c.json({ error: msg }, 400);
2164
+ }
2165
+ }
2166
+ ).delete("/:name/skills/:skill", requireAdmin, async (c) => {
2167
+ const name = c.req.param("name");
2168
+ const skillName = c.req.param("skill");
2169
+ const entry = findMind(name);
2170
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2171
+ const dir = mindDir(name);
2172
+ try {
2173
+ await uninstallSkill(name, dir, skillName);
2174
+ } catch (e) {
2175
+ const msg = e instanceof Error ? e.message : String(e);
2176
+ return c.json({ error: msg }, 400);
2177
+ }
2178
+ return c.json({ ok: true });
2179
+ });
2180
+ var mind_skills_default = app7;
2181
+
2182
+ // src/web/api/minds.ts
2183
+ import {
2184
+ cpSync as cpSync2,
2185
+ existsSync as existsSync8,
2186
+ mkdirSync as mkdirSync5,
2187
+ readdirSync as readdirSync4,
2188
+ readFileSync as readFileSync7,
2189
+ rmSync as rmSync2,
1825
2190
  statSync,
1826
- writeFileSync as writeFileSync5
2191
+ writeFileSync as writeFileSync6
1827
2192
  } from "fs";
1828
- import { join, resolve as resolve11 } from "path";
1829
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1830
- import { and as and3, desc as desc2, eq as eq4, sql as sql3 } from "drizzle-orm";
1831
- import { Hono as Hono7 } from "hono";
1832
- import { z as z2 } from "zod";
2193
+ import { join as join2, resolve as resolve11 } from "path";
2194
+ 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";
2197
+ import { z as z3 } from "zod";
1833
2198
 
1834
2199
  // src/lib/consolidate.ts
1835
- import { readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2200
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1836
2201
  import { resolve as resolve9 } from "path";
1837
2202
  async function consolidateMemory(mindDir2) {
1838
2203
  const soulPath = resolve9(mindDir2, "home/SOUL.md");
1839
2204
  const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
1840
2205
  const memoryDir = resolve9(mindDir2, "home/memory");
1841
- const soul = readFileSync4(soulPath, "utf-8");
2206
+ const soul = readFileSync5(soulPath, "utf-8");
1842
2207
  const logs = [];
1843
2208
  try {
1844
- const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2209
+ const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
1845
2210
  for (const filename of files) {
1846
2211
  const date = filename.replace(".md", "");
1847
- const content2 = readFileSync4(resolve9(memoryDir, filename), "utf-8").trim();
2212
+ const content2 = readFileSync5(resolve9(memoryDir, filename), "utf-8").trim();
1848
2213
  if (content2) {
1849
2214
  logs.push(`### ${date}
1850
2215
 
@@ -1894,7 +2259,7 @@ ${content2}`);
1894
2259
  const data = await res.json();
1895
2260
  const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
1896
2261
  if (content) {
1897
- writeFileSync3(memoryPath, `${content}
2262
+ writeFileSync4(memoryPath, `${content}
1898
2263
  `);
1899
2264
  console.log("MEMORY.md created successfully.");
1900
2265
  } else {
@@ -1904,7 +2269,7 @@ ${content2}`);
1904
2269
 
1905
2270
  // src/lib/conversations.ts
1906
2271
  import { randomUUID } from "crypto";
1907
- import { and as and2, desc, eq as eq3, inArray, isNull, sql as sql2 } from "drizzle-orm";
2272
+ import { and as and2, desc, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
1908
2273
 
1909
2274
  // src/lib/conversation-events.ts
1910
2275
  var subscribers = /* @__PURE__ */ new Map();
@@ -1936,13 +2301,17 @@ function publish(conversationId, event) {
1936
2301
 
1937
2302
  // src/lib/conversations.ts
1938
2303
  async function createConversation(mindName, channel, opts) {
1939
- const db2 = await getDb();
2304
+ const db = await getDb();
1940
2305
  const id = randomUUID();
1941
- await db2.transaction(async (tx) => {
2306
+ const type = opts?.type ?? "dm";
2307
+ const name = opts?.name ?? null;
2308
+ await db.transaction(async (tx) => {
1942
2309
  await tx.insert(conversations).values({
1943
2310
  id,
1944
2311
  mind_name: mindName,
1945
2312
  channel,
2313
+ type,
2314
+ name,
1946
2315
  user_id: opts?.userId ?? null,
1947
2316
  title: opts?.title ?? null
1948
2317
  });
@@ -1960,6 +2329,8 @@ async function createConversation(mindName, channel, opts) {
1960
2329
  id,
1961
2330
  mind_name: mindName,
1962
2331
  channel,
2332
+ type,
2333
+ name,
1963
2334
  user_id: opts?.userId ?? null,
1964
2335
  title: opts?.title ?? null,
1965
2336
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1967,41 +2338,58 @@ async function createConversation(mindName, channel, opts) {
1967
2338
  };
1968
2339
  }
1969
2340
  async function getConversation(id) {
1970
- const db2 = await getDb();
1971
- const row = await db2.select().from(conversations).where(eq3(conversations.id, id)).get();
2341
+ const db = await getDb();
2342
+ const row = await db.select().from(conversations).where(eq4(conversations.id, id)).get();
1972
2343
  return row ?? null;
1973
2344
  }
2345
+ async function addParticipant(conversationId, userId, role = "member") {
2346
+ const db = await getDb();
2347
+ await db.insert(conversationParticipants).values({
2348
+ conversation_id: conversationId,
2349
+ user_id: userId,
2350
+ role
2351
+ });
2352
+ }
2353
+ async function removeParticipant(conversationId, userId) {
2354
+ const db = await getDb();
2355
+ await db.delete(conversationParticipants).where(
2356
+ and2(
2357
+ eq4(conversationParticipants.conversation_id, conversationId),
2358
+ eq4(conversationParticipants.user_id, userId)
2359
+ )
2360
+ );
2361
+ }
1974
2362
  async function getParticipants(conversationId) {
1975
- const db2 = await getDb();
1976
- const rows = await db2.select({
2363
+ const db = await getDb();
2364
+ const rows = await db.select({
1977
2365
  userId: conversationParticipants.user_id,
1978
2366
  username: users.username,
1979
2367
  userType: users.user_type,
1980
2368
  role: conversationParticipants.role
1981
- }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
2369
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
1982
2370
  return rows;
1983
2371
  }
1984
2372
  async function isParticipant(conversationId, userId) {
1985
- const db2 = await getDb();
1986
- const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2373
+ const db = await getDb();
2374
+ const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
1987
2375
  and2(
1988
- eq3(conversationParticipants.conversation_id, conversationId),
1989
- eq3(conversationParticipants.user_id, userId)
2376
+ eq4(conversationParticipants.conversation_id, conversationId),
2377
+ eq4(conversationParticipants.user_id, userId)
1990
2378
  )
1991
2379
  ).get();
1992
2380
  return row != null;
1993
2381
  }
1994
2382
  async function listConversationsForUser(userId) {
1995
- const db2 = await getDb();
1996
- const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
2383
+ const db = await getDb();
2384
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
1997
2385
  if (participantRows.length === 0) return [];
1998
2386
  const convIds = participantRows.map((r) => r.conversation_id);
1999
- return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
2387
+ return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
2000
2388
  }
2001
2389
  async function isParticipantOrOwner(conversationId, userId) {
2002
2390
  if (await isParticipant(conversationId, userId)) return true;
2003
- const db2 = await getDb();
2004
- const row = await db2.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
2391
+ const db = await getDb();
2392
+ const row = await db.select().from(conversations).where(and2(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
2005
2393
  return row != null;
2006
2394
  }
2007
2395
  async function deleteConversationForUser(id, userId) {
@@ -2010,15 +2398,15 @@ async function deleteConversationForUser(id, userId) {
2010
2398
  return true;
2011
2399
  }
2012
2400
  async function addMessage(conversationId, role, senderName, content) {
2013
- const db2 = await getDb();
2401
+ const db = await getDb();
2014
2402
  const serialized = JSON.stringify(content);
2015
- const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2016
- await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq3(conversations.id, conversationId));
2403
+ 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));
2017
2405
  if (role === "user") {
2018
2406
  const firstText = content.find((b) => b.type === "text");
2019
2407
  const title = firstText ? firstText.text.slice(0, 80) : "";
2020
2408
  if (title) {
2021
- await db2.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
2409
+ await db.update(conversations).set({ title }).where(and2(eq4(conversations.id, conversationId), isNull(conversations.title)));
2022
2410
  }
2023
2411
  }
2024
2412
  const msg = {
@@ -2040,8 +2428,8 @@ async function addMessage(conversationId, role, senderName, content) {
2040
2428
  return msg;
2041
2429
  }
2042
2430
  async function getMessages(conversationId) {
2043
- const db2 = await getDb();
2044
- const rows = await db2.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2431
+ const db = await getDb();
2432
+ const rows = await db.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2045
2433
  return rows.map((row) => {
2046
2434
  let content;
2047
2435
  try {
@@ -2056,15 +2444,15 @@ async function getMessages(conversationId) {
2056
2444
  async function listConversationsWithParticipants(userId) {
2057
2445
  const convs = await listConversationsForUser(userId);
2058
2446
  if (convs.length === 0) return [];
2059
- const db2 = await getDb();
2447
+ const db = await getDb();
2060
2448
  const convIds = convs.map((c) => c.id);
2061
- const rows = await db2.select({
2449
+ const rows = await db.select({
2062
2450
  conversationId: conversationParticipants.conversation_id,
2063
2451
  userId: users.id,
2064
2452
  username: users.username,
2065
2453
  userType: users.user_type,
2066
2454
  role: conversationParticipants.role
2067
- }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2455
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2068
2456
  const byConv = /* @__PURE__ */ new Map();
2069
2457
  for (const r of rows) {
2070
2458
  let arr = byConv.get(r.conversationId);
@@ -2079,32 +2467,32 @@ async function listConversationsWithParticipants(userId) {
2079
2467
  role: r.role
2080
2468
  });
2081
2469
  }
2082
- const lastMsgIds = await db2.select({
2470
+ const lastMsgIds = await db.select({
2083
2471
  conversationId: messages.conversation_id,
2084
2472
  maxId: sql2`MAX(${messages.id})`
2085
2473
  }).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
2086
2474
  const byLastMsg = /* @__PURE__ */ new Map();
2087
2475
  if (lastMsgIds.length > 0) {
2088
- const msgRows = await db2.select().from(messages).where(
2476
+ const msgRows = await db.select().from(messages).where(
2089
2477
  inArray(
2090
2478
  messages.id,
2091
2479
  lastMsgIds.map((r) => r.maxId)
2092
2480
  )
2093
2481
  );
2094
2482
  for (const m of msgRows) {
2095
- let text2 = "";
2483
+ let text = "";
2096
2484
  try {
2097
2485
  const parsed = JSON.parse(m.content);
2098
2486
  const blocks = Array.isArray(parsed) ? parsed : [];
2099
2487
  const textBlock = blocks.find((b) => b.type === "text");
2100
- if (textBlock && "text" in textBlock) text2 = textBlock.text;
2488
+ if (textBlock && "text" in textBlock) text = textBlock.text;
2101
2489
  } catch {
2102
- text2 = m.content;
2490
+ text = m.content;
2103
2491
  }
2104
2492
  byLastMsg.set(m.conversation_id, {
2105
2493
  role: m.role,
2106
2494
  senderName: m.sender_name,
2107
- text: text2,
2495
+ text,
2108
2496
  createdAt: m.created_at
2109
2497
  });
2110
2498
  }
@@ -2116,10 +2504,10 @@ async function listConversationsWithParticipants(userId) {
2116
2504
  }));
2117
2505
  }
2118
2506
  async function findDMConversation(mindName, participantIds) {
2119
- const db2 = await getDb();
2120
- const mindConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq3(conversations.mind_name, mindName)).all();
2507
+ 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();
2121
2509
  for (const conv of mindConvs) {
2122
- const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
2510
+ const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
2123
2511
  if (rows.length !== 2) continue;
2124
2512
  const ids = new Set(rows.map((r) => r.user_id));
2125
2513
  if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
@@ -2129,17 +2517,42 @@ async function findDMConversation(mindName, participantIds) {
2129
2517
  return null;
2130
2518
  }
2131
2519
  async function deleteConversation(id) {
2132
- const db2 = await getDb();
2133
- await db2.delete(conversations).where(eq3(conversations.id, id));
2520
+ const db = await getDb();
2521
+ await db.delete(conversations).where(eq4(conversations.id, id));
2522
+ }
2523
+ async function createChannel(name, creatorId) {
2524
+ const participantIds = creatorId ? [creatorId] : [];
2525
+ return createConversation(null, "volute", {
2526
+ type: "channel",
2527
+ name,
2528
+ title: name,
2529
+ participantIds
2530
+ });
2531
+ }
2532
+ async function getChannelByName(name) {
2533
+ const db = await getDb();
2534
+ const row = await db.select().from(conversations).where(and2(eq4(conversations.name, name), eq4(conversations.type, "channel"))).get();
2535
+ return row ?? null;
2536
+ }
2537
+ async function listChannels() {
2538
+ const db = await getDb();
2539
+ return await db.select().from(conversations).where(eq4(conversations.type, "channel")).orderBy(conversations.name).all();
2540
+ }
2541
+ async function joinChannel(conversationId, userId) {
2542
+ if (await isParticipant(conversationId, userId)) return;
2543
+ await addParticipant(conversationId, userId);
2544
+ }
2545
+ async function leaveChannel(conversationId, userId) {
2546
+ await removeParticipant(conversationId, userId);
2134
2547
  }
2135
2548
 
2136
2549
  // src/lib/convert-session.ts
2137
2550
  import { randomUUID as randomUUID2 } from "crypto";
2138
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2551
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2139
2552
  import { homedir } from "os";
2140
2553
  import { resolve as resolve10 } from "path";
2141
2554
  function convertSession(opts) {
2142
- const lines = readFileSync5(opts.sessionPath, "utf-8").trim().split("\n");
2555
+ const lines = readFileSync6(opts.sessionPath, "utf-8").trim().split("\n");
2143
2556
  const sessionId = randomUUID2();
2144
2557
  const idMap = /* @__PURE__ */ new Map();
2145
2558
  const messages2 = [];
@@ -2254,9 +2667,9 @@ function convertSession(opts) {
2254
2667
  }
2255
2668
  const projectId = opts.projectDir.replace(/\//g, "-");
2256
2669
  const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
2257
- mkdirSync3(sdkDir, { recursive: true });
2670
+ mkdirSync4(sdkDir, { recursive: true });
2258
2671
  const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
2259
- writeFileSync4(sdkPath, `${sdkEvents.join("\n")}
2672
+ writeFileSync5(sdkPath, `${sdkEvents.join("\n")}
2260
2673
  `);
2261
2674
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
2262
2675
  return sessionId;
@@ -2307,6 +2720,34 @@ function convertAssistantContent(content) {
2307
2720
  return result;
2308
2721
  }
2309
2722
 
2723
+ // src/lib/mind-events.ts
2724
+ var subscribers2 = /* @__PURE__ */ new Map();
2725
+ function subscribe2(mind, callback) {
2726
+ let set = subscribers2.get(mind);
2727
+ if (!set) {
2728
+ set = /* @__PURE__ */ new Set();
2729
+ subscribers2.set(mind, set);
2730
+ }
2731
+ set.add(callback);
2732
+ return () => {
2733
+ set.delete(callback);
2734
+ if (set.size === 0) subscribers2.delete(mind);
2735
+ };
2736
+ }
2737
+ function publish2(mind, event) {
2738
+ const set = subscribers2.get(mind);
2739
+ if (!set) return;
2740
+ for (const cb of set) {
2741
+ try {
2742
+ cb(event);
2743
+ } catch (err) {
2744
+ console.error("[mind-events] subscriber threw:", err);
2745
+ set.delete(cb);
2746
+ if (set.size === 0) subscribers2.delete(mind);
2747
+ }
2748
+ }
2749
+ }
2750
+
2310
2751
  // src/lib/typing.ts
2311
2752
  var DEFAULT_TTL_MS = 1e4;
2312
2753
  var SWEEP_INTERVAL_MS = 5e3;
@@ -2335,6 +2776,15 @@ var TypingMap = class {
2335
2776
  }
2336
2777
  }
2337
2778
  }
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
+ }
2786
+ }
2787
+ }
2338
2788
  get(channel) {
2339
2789
  const senders = this.channels.get(channel);
2340
2790
  if (!senders) return [];
@@ -2374,7 +2824,7 @@ function getTypingMap() {
2374
2824
  return instance5;
2375
2825
  }
2376
2826
 
2377
- // src/web/routes/minds.ts
2827
+ // src/web/api/minds.ts
2378
2828
  async function startMindFull(name, baseName, variantName) {
2379
2829
  await getMindManager().startMind(name);
2380
2830
  if (variantName) return;
@@ -2404,7 +2854,7 @@ function extractTextContent(content) {
2404
2854
  }
2405
2855
  function getDaemonPort() {
2406
2856
  try {
2407
- const data = JSON.parse(readFileSync6(resolve11(voluteHome(), "daemon.json"), "utf-8"));
2857
+ const data = JSON.parse(readFileSync7(resolve11(voluteHome(), "daemon.json"), "utf-8"));
2408
2858
  return data.port;
2409
2859
  } catch (err) {
2410
2860
  if (err?.code !== "ENOENT") {
@@ -2467,7 +2917,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2467
2917
  } catch {
2468
2918
  }
2469
2919
  if (existsSync8(tempWorktree)) {
2470
- rmSync(tempWorktree, { recursive: true, force: true });
2920
+ rmSync2(tempWorktree, { recursive: true, force: true });
2471
2921
  }
2472
2922
  const templatesRoot = findTemplatesRoot();
2473
2923
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
@@ -2489,7 +2939,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2489
2939
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
2490
2940
  const initDir = resolve11(tempWorktree, ".init");
2491
2941
  if (existsSync8(initDir)) {
2492
- rmSync(initDir, { recursive: true, force: true });
2942
+ rmSync2(initDir, { recursive: true, force: true });
2493
2943
  }
2494
2944
  await gitExec(["add", "-A"], { cwd: tempWorktree });
2495
2945
  try {
@@ -2503,9 +2953,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2503
2953
  } catch {
2504
2954
  }
2505
2955
  if (existsSync8(tempWorktree)) {
2506
- rmSync(tempWorktree, { recursive: true, force: true });
2956
+ rmSync2(tempWorktree, { recursive: true, force: true });
2507
2957
  }
2508
- rmSync(composedDir, { recursive: true, force: true });
2958
+ rmSync2(composedDir, { recursive: true, force: true });
2509
2959
  }
2510
2960
  }
2511
2961
  async function mergeTemplateBranch(worktreeDir) {
@@ -2533,14 +2983,15 @@ async function npmInstallAsMind(cwd, mindName) {
2533
2983
  await exec("npm", ["install"], { cwd });
2534
2984
  }
2535
2985
  }
2536
- var createMindSchema = z2.object({
2537
- name: z2.string(),
2538
- template: z2.string().optional(),
2539
- stage: z2.enum(["seed", "sprouted"]).optional(),
2540
- description: z2.string().optional(),
2541
- model: z2.string().optional()
2986
+ var createMindSchema = z3.object({
2987
+ name: z3.string(),
2988
+ template: z3.string().optional(),
2989
+ stage: z3.enum(["seed", "sprouted"]).optional(),
2990
+ description: z3.string().optional(),
2991
+ model: z3.string().optional(),
2992
+ seedSoul: z3.string().optional()
2542
2993
  });
2543
- var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSchema), async (c) => {
2994
+ var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
2544
2995
  const body = c.req.valid("json");
2545
2996
  const { name, template = "claude" } = body;
2546
2997
  const nameErr = validateMindName(name);
@@ -2556,11 +3007,17 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2556
3007
  applyInitFiles(dest);
2557
3008
  if (body.model) {
2558
3009
  const configPath = resolve11(dest, "home/.config/config.json");
2559
- const existing = existsSync8(configPath) ? JSON.parse(readFileSync6(configPath, "utf-8")) : {};
3010
+ const existing = existsSync8(configPath) ? JSON.parse(readFileSync7(configPath, "utf-8")) : {};
2560
3011
  existing.model = body.model;
2561
- writeFileSync5(configPath, `${JSON.stringify(existing, null, 2)}
3012
+ writeFileSync6(configPath, `${JSON.stringify(existing, null, 2)}
2562
3013
  `);
2563
3014
  }
3015
+ const mindPrompts = await getMindPromptDefaults();
3016
+ writeFileSync6(
3017
+ resolve11(dest, "home/.config/prompts.json"),
3018
+ `${JSON.stringify(mindPrompts, null, 2)}
3019
+ `
3020
+ );
2564
3021
  const port = nextPort();
2565
3022
  addMind(name, port, body.stage);
2566
3023
  const homeDir = resolve11(dest, "home");
@@ -2575,7 +3032,7 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2575
3032
  await initTemplateBranch(dest, composedDir, manifest, name, env);
2576
3033
  } catch (err) {
2577
3034
  console.error(`[daemon] git setup failed for ${name}:`, err);
2578
- rmSync(resolve11(dest, ".git"), { recursive: true, force: true });
3035
+ rmSync2(resolve11(dest, ".git"), { recursive: true, force: true });
2579
3036
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
2580
3037
  }
2581
3038
  chownMindDir(dest, name);
@@ -2583,19 +3040,23 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2583
3040
  const descLine = body.description ? `
2584
3041
  The human who planted you described you as: "${body.description}"
2585
3042
  ` : "";
2586
- writeFileSync5(
2587
- resolve11(dest, "home/SOUL.md"),
2588
- `You are ${name}.
2589
-
2590
- You don't have a soul yet. You're a seed \u2014 a new mind in the process of discovering who you are.
2591
- ${descLine}
2592
- Have a conversation with the human. Explore what kind of mind you want to be. When you're ready, write your SOUL.md and MEMORY.md, then run \`volute sprout\` to complete the transformation.
2593
- `
2594
- );
3043
+ const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
3044
+ const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
3045
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
2595
3046
  const skillsDir = resolve11(dest, manifest.skillsDir);
2596
3047
  for (const skill of ["volute-mind", "memory", "sessions"]) {
2597
3048
  const skillPath = resolve11(skillsDir, skill);
2598
- if (existsSync8(skillPath)) rmSync(skillPath, { recursive: true, force: true });
3049
+ if (existsSync8(skillPath)) rmSync2(skillPath, { recursive: true, force: true });
3050
+ }
3051
+ }
3052
+ if (body.stage !== "seed") {
3053
+ const customSoul = await getPromptIfCustom("default_soul");
3054
+ if (customSoul) {
3055
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3056
+ }
3057
+ const customMemory = await getPromptIfCustom("default_memory");
3058
+ if (customMemory) {
3059
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
2599
3060
  }
2600
3061
  }
2601
3062
  return c.json({
@@ -2607,14 +3068,14 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
2607
3068
  ...gitWarning && { warning: gitWarning }
2608
3069
  });
2609
3070
  } catch (err) {
2610
- if (existsSync8(dest)) rmSync(dest, { recursive: true, force: true });
3071
+ if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
2611
3072
  try {
2612
3073
  removeMind(name);
2613
3074
  } catch {
2614
3075
  }
2615
3076
  return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
2616
3077
  } finally {
2617
- rmSync(composedDir, { recursive: true, force: true });
3078
+ rmSync2(composedDir, { recursive: true, force: true });
2618
3079
  }
2619
3080
  }).post("/import", requireAdmin, async (c) => {
2620
3081
  let body;
@@ -2627,10 +3088,10 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
2627
3088
  if (!wsDir || !existsSync8(resolve11(wsDir, "SOUL.md")) || !existsSync8(resolve11(wsDir, "IDENTITY.md"))) {
2628
3089
  return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
2629
3090
  }
2630
- const soul = readFileSync6(resolve11(wsDir, "SOUL.md"), "utf-8");
2631
- const identity = readFileSync6(resolve11(wsDir, "IDENTITY.md"), "utf-8");
3091
+ const soul = readFileSync7(resolve11(wsDir, "SOUL.md"), "utf-8");
3092
+ const identity = readFileSync7(resolve11(wsDir, "IDENTITY.md"), "utf-8");
2632
3093
  const userPath = resolve11(wsDir, "USER.md");
2633
- const user = existsSync8(userPath) ? readFileSync6(userPath, "utf-8") : "";
3094
+ const user = existsSync8(userPath) ? readFileSync7(userPath, "utf-8") : "";
2634
3095
  const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
2635
3096
  const template = body.template ?? "claude";
2636
3097
  const nameErr = validateMindName(name);
@@ -2656,26 +3117,26 @@ ${user.trimEnd()}
2656
3117
  try {
2657
3118
  copyTemplateToDir(composedDir, dest, name, manifest);
2658
3119
  applyInitFiles(dest);
2659
- writeFileSync5(resolve11(dest, "home/SOUL.md"), mergedSoul);
3120
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
2660
3121
  const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
2661
3122
  const hasMemory = existsSync8(wsMemoryPath);
2662
3123
  if (hasMemory) {
2663
- const existingMemory = readFileSync6(wsMemoryPath, "utf-8");
2664
- writeFileSync5(
3124
+ const existingMemory = readFileSync7(wsMemoryPath, "utf-8");
3125
+ writeFileSync6(
2665
3126
  resolve11(dest, "home/MEMORY.md"),
2666
3127
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
2667
3128
  );
2668
3129
  } else if (user) {
2669
- writeFileSync5(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
3130
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
2670
3131
  `);
2671
3132
  }
2672
3133
  const wsMemoryDir = resolve11(wsDir, "memory");
2673
3134
  let dailyLogCount = 0;
2674
3135
  if (existsSync8(wsMemoryDir)) {
2675
3136
  const destMemoryDir = resolve11(dest, "home/memory");
2676
- const files = readdirSync3(wsMemoryDir).filter((f) => f.endsWith(".md"));
3137
+ const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
2677
3138
  for (const file of files) {
2678
- cpSync(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
3139
+ cpSync2(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
2679
3140
  }
2680
3141
  dailyLogCount = files.length;
2681
3142
  }
@@ -2700,32 +3161,32 @@ ${user.trimEnd()}
2700
3161
  } else if (template === "claude") {
2701
3162
  const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
2702
3163
  const voluteDir = resolve11(dest, ".volute");
2703
- mkdirSync4(voluteDir, { recursive: true });
2704
- writeFileSync5(resolve11(voluteDir, "session.json"), JSON.stringify({ sessionId }));
3164
+ mkdirSync5(voluteDir, { recursive: true });
3165
+ writeFileSync6(resolve11(voluteDir, "session.json"), JSON.stringify({ sessionId }));
2705
3166
  }
2706
3167
  }
2707
3168
  importOpenClawConnectors(name, dest);
2708
3169
  chownMindDir(dest, name);
2709
3170
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
2710
3171
  } catch (err) {
2711
- if (existsSync8(dest)) rmSync(dest, { recursive: true, force: true });
3172
+ if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
2712
3173
  try {
2713
3174
  removeMind(name);
2714
3175
  } catch {
2715
3176
  }
2716
3177
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
2717
3178
  } finally {
2718
- rmSync(composedDir, { recursive: true, force: true });
3179
+ rmSync2(composedDir, { recursive: true, force: true });
2719
3180
  }
2720
3181
  }).get("/", async (c) => {
2721
3182
  const entries = readRegistry();
2722
3183
  let lastActiveMap = /* @__PURE__ */ new Map();
2723
3184
  try {
2724
- const db2 = await getDb();
2725
- const lastActiveRows = await db2.select({
2726
- mind: mindMessages.mind,
2727
- lastActiveAt: sql3`MAX(${mindMessages.created_at})`
2728
- }).from(mindMessages).groupBy(mindMessages.mind);
3185
+ const db = await getDb();
3186
+ const lastActiveRows = await db.select({
3187
+ mind: mindHistory.mind,
3188
+ lastActiveAt: sql3`MAX(${mindHistory.created_at})`
3189
+ }).from(mindHistory).groupBy(mindHistory.mind);
2729
3190
  lastActiveMap = new Map(lastActiveRows.map((r) => [r.mind, r.lastActiveAt]));
2730
3191
  } catch {
2731
3192
  }
@@ -2751,7 +3212,7 @@ ${user.trimEnd()}
2751
3212
  if (!existsSync8(pagesDir)) continue;
2752
3213
  let items;
2753
3214
  try {
2754
- items = readdirSync3(pagesDir);
3215
+ items = readdirSync4(pagesDir);
2755
3216
  } catch (err) {
2756
3217
  logger_default.warn("Failed to read pages dir", { mind: entry.name, error: err.message });
2757
3218
  continue;
@@ -2773,7 +3234,7 @@ ${user.trimEnd()}
2773
3234
  const indexStat = statSync(indexPath);
2774
3235
  pages.push({
2775
3236
  mind: entry.name,
2776
- file: join(item, "index.html"),
3237
+ file: join2(item, "index.html"),
2777
3238
  modified: indexStat.mtime.toISOString(),
2778
3239
  url: `/pages/${entry.name}/${item}/`
2779
3240
  });
@@ -2927,8 +3388,8 @@ ${user.trimEnd()}
2927
3388
  }
2928
3389
  if (context?.type === "sprouted" && !variantName) {
2929
3390
  try {
2930
- const db2 = await getDb();
2931
- const activeConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.mind_name, baseName)).all();
3391
+ const db = await getDb();
3392
+ const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq5(conversations.mind_name, baseName)).all();
2932
3393
  for (const conv of activeConvs) {
2933
3394
  await addMessage(conv.id, "assistant", "system", [
2934
3395
  { type: "text", text: "[seed has sprouted]" }
@@ -2993,10 +3454,10 @@ ${user.trimEnd()}
2993
3454
  await deleteMindUser2(name);
2994
3455
  const state = stateDir(name);
2995
3456
  if (existsSync8(state)) {
2996
- rmSync(state, { recursive: true, force: true });
3457
+ rmSync2(state, { recursive: true, force: true });
2997
3458
  }
2998
3459
  if (force && existsSync8(dir)) {
2999
- rmSync(dir, { recursive: true, force: true });
3460
+ rmSync2(dir, { recursive: true, force: true });
3000
3461
  deleteMindUser(name);
3001
3462
  }
3002
3463
  return c.json({ ok: true });
@@ -3090,7 +3551,7 @@ ${user.trimEnd()}
3090
3551
  await updateTemplateBranch(dir, template, mindName);
3091
3552
  const parentDir = resolve11(dir, ".variants");
3092
3553
  if (!existsSync8(parentDir)) {
3093
- mkdirSync4(parentDir, { recursive: true });
3554
+ mkdirSync5(parentDir, { recursive: true });
3094
3555
  }
3095
3556
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
3096
3557
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -3168,13 +3629,14 @@ ${user.trimEnd()}
3168
3629
  console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
3169
3630
  }
3170
3631
  const channel = parsed?.channel ?? "unknown";
3171
- const db2 = await getDb();
3632
+ const db = await getDb();
3172
3633
  if (parsed) {
3173
3634
  try {
3174
3635
  const sender2 = parsed.sender ?? null;
3175
3636
  const content = extractTextContent(parsed.content);
3176
- await db2.insert(mindMessages).values({
3637
+ await db.insert(mindHistory).values({
3177
3638
  mind: baseName,
3639
+ type: "inbound",
3178
3640
  channel,
3179
3641
  sender: sender2,
3180
3642
  content
@@ -3219,7 +3681,7 @@ ${user.trimEnd()}
3219
3681
  const seedEntry = findMind(baseName);
3220
3682
  if (seedEntry?.stage === "seed" && parsed) {
3221
3683
  try {
3222
- const countResult = await db2.select({ count: sql3`count(*)` }).from(mindMessages).where(eq4(mindMessages.mind, baseName));
3684
+ const countResult = await db.select({ count: sql3`count(*)` }).from(mindHistory).where(eq5(mindHistory.mind, baseName));
3223
3685
  const msgCount = countResult[0]?.count ?? 0;
3224
3686
  if (msgCount >= 10 && msgCount % 10 === 0) {
3225
3687
  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.]";
@@ -3237,41 +3699,112 @@ ${user.trimEnd()}
3237
3699
  typingMap.set(channel, baseName, { persistent: true });
3238
3700
  const conversationId = parsed?.conversationId ?? null;
3239
3701
  if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
3240
- try {
3241
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
3242
- method: "POST",
3243
- headers: { "Content-Type": "application/json" },
3244
- body: forwardBody
3245
- });
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) => {
3246
3707
  if (!res.ok) {
3247
- const text2 = await res.text().catch(() => "");
3248
- console.error(`[daemon] mind ${name} responded with ${res.status}: ${text2}`);
3249
- return c.json({ error: `Mind responded with ${res.status}` }, res.status);
3250
- }
3251
- let result;
3252
- try {
3253
- result = await res.json();
3254
- } catch (parseErr) {
3255
- console.error(`[daemon] mind ${name} returned non-JSON response:`, parseErr);
3256
- return c.json({ error: "Mind returned invalid response" }, 502);
3257
- }
3258
- if (result.usage) {
3259
- budget.recordUsage(baseName, result.usage.input_tokens, result.usage.output_tokens);
3708
+ const text = await res.text().catch(() => "");
3709
+ console.error(`[daemon] mind ${name} responded with ${res.status}: ${text}`);
3260
3710
  }
3261
- return c.json({ ok: true });
3262
- } catch (err) {
3711
+ }).catch((err) => {
3263
3712
  console.error(`[daemon] mind ${name} unreachable on port ${port}:`, err);
3264
- return c.json({ error: "Mind is not reachable" }, 502);
3265
- } finally {
3266
3713
  typingMap.delete(channel, baseName);
3267
- if (conversationId) typingMap.delete(`volute:${conversationId}`, baseName);
3268
- }
3714
+ });
3715
+ return c.json({ ok: true });
3269
3716
  }).get("/:name/budget", async (c) => {
3270
3717
  const name = c.req.param("name");
3271
3718
  const [baseName] = name.split("@", 2);
3272
3719
  const usage = getTokenBudget().getUsage(baseName);
3273
3720
  if (!usage) return c.json({ error: "No budget configured" }, 404);
3274
3721
  return c.json(usage);
3722
+ }).post("/:name/events", async (c) => {
3723
+ const name = c.req.param("name");
3724
+ const [baseName] = name.split("@", 2);
3725
+ let body;
3726
+ try {
3727
+ body = await c.req.json();
3728
+ } catch {
3729
+ return c.json({ error: "Invalid JSON" }, 400);
3730
+ }
3731
+ if (!body.type) {
3732
+ return c.json({ error: "type required" }, 400);
3733
+ }
3734
+ const db = await getDb();
3735
+ try {
3736
+ await db.insert(mindHistory).values({
3737
+ mind: baseName,
3738
+ type: body.type,
3739
+ session: body.session ?? null,
3740
+ channel: body.channel ?? null,
3741
+ message_id: body.messageId ?? null,
3742
+ content: body.content ?? null,
3743
+ metadata: body.metadata ? JSON.stringify(body.metadata) : null
3744
+ });
3745
+ } catch (err) {
3746
+ console.error(`[daemon] failed to persist event for ${baseName}:`, err);
3747
+ }
3748
+ publish2(baseName, {
3749
+ mind: baseName,
3750
+ type: body.type,
3751
+ session: body.session,
3752
+ channel: body.channel,
3753
+ messageId: body.messageId,
3754
+ content: body.content,
3755
+ metadata: body.metadata
3756
+ });
3757
+ if (body.type === "done") {
3758
+ if (body.channel) {
3759
+ getTypingMap().delete(body.channel, baseName);
3760
+ } else {
3761
+ getTypingMap().deleteSender(baseName);
3762
+ }
3763
+ }
3764
+ if (body.type === "usage" && body.metadata) {
3765
+ const inputTokens = body.metadata.input_tokens ?? 0;
3766
+ const outputTokens = body.metadata.output_tokens ?? 0;
3767
+ getTokenBudget().recordUsage(baseName, inputTokens, outputTokens);
3768
+ }
3769
+ return c.json({ ok: true });
3770
+ }).get("/:name/events", async (c) => {
3771
+ const name = c.req.param("name");
3772
+ const [baseName] = name.split("@", 2);
3773
+ const entry = findMind(baseName);
3774
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3775
+ const typeFilter = c.req.query("type")?.split(",").filter(Boolean);
3776
+ const sessionFilter = c.req.query("session");
3777
+ const channelFilter = c.req.query("channel");
3778
+ const stream = new ReadableStream({
3779
+ start(controller) {
3780
+ const encoder = new TextEncoder();
3781
+ const send = (data) => {
3782
+ controller.enqueue(encoder.encode(`data: ${data}
3783
+
3784
+ `));
3785
+ };
3786
+ const unsubscribe = subscribe2(baseName, (event) => {
3787
+ if (typeFilter && !typeFilter.includes(event.type)) return;
3788
+ if (sessionFilter && event.session !== sessionFilter) return;
3789
+ if (channelFilter && event.channel !== channelFilter) return;
3790
+ send(JSON.stringify(event));
3791
+ });
3792
+ c.req.raw.signal.addEventListener("abort", () => {
3793
+ unsubscribe();
3794
+ try {
3795
+ controller.close();
3796
+ } catch {
3797
+ }
3798
+ });
3799
+ }
3800
+ });
3801
+ return new Response(stream, {
3802
+ headers: {
3803
+ "Content-Type": "text/event-stream",
3804
+ "Cache-Control": "no-cache",
3805
+ Connection: "keep-alive"
3806
+ }
3807
+ });
3275
3808
  }).post("/:name/history", async (c) => {
3276
3809
  const name = c.req.param("name");
3277
3810
  const [baseName] = name.split("@", 2);
@@ -3284,10 +3817,11 @@ ${user.trimEnd()}
3284
3817
  if (!body.channel || !body.content) {
3285
3818
  return c.json({ error: "channel and content required" }, 400);
3286
3819
  }
3287
- const db2 = await getDb();
3820
+ const db = await getDb();
3288
3821
  try {
3289
- await db2.insert(mindMessages).values({
3822
+ await db.insert(mindHistory).values({
3290
3823
  mind: baseName,
3824
+ type: "outbound",
3291
3825
  channel: body.channel,
3292
3826
  sender: body.sender ?? baseName,
3293
3827
  content: body.content
@@ -3297,30 +3831,49 @@ ${user.trimEnd()}
3297
3831
  return c.json({ error: "Failed to persist" }, 500);
3298
3832
  }
3299
3833
  return c.json({ ok: true });
3834
+ }).get("/:name/history/sessions", async (c) => {
3835
+ const name = c.req.param("name");
3836
+ const db = await getDb();
3837
+ const rows = await db.select({
3838
+ 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`);
3844
+ return c.json(rows);
3300
3845
  }).get("/:name/history/channels", async (c) => {
3301
3846
  const name = c.req.param("name");
3302
- const db2 = await getDb();
3303
- const rows = await db2.selectDistinct({ channel: mindMessages.channel }).from(mindMessages).where(eq4(mindMessages.mind, name));
3847
+ const db = await getDb();
3848
+ const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq5(mindHistory.mind, name));
3304
3849
  return c.json(rows.map((r) => r.channel));
3305
3850
  }).get("/:name/history", async (c) => {
3306
3851
  const name = c.req.param("name");
3307
3852
  const channel = c.req.query("channel");
3853
+ const session = c.req.query("session");
3854
+ const full = c.req.query("full") === "true";
3308
3855
  const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
3309
3856
  const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
3310
- const db2 = await getDb();
3311
- const conditions = [eq4(mindMessages.mind, name)];
3857
+ const db = await getDb();
3858
+ const conditions = [eq5(mindHistory.mind, name)];
3312
3859
  if (channel) {
3313
- conditions.push(eq4(mindMessages.channel, channel));
3860
+ conditions.push(eq5(mindHistory.channel, channel));
3861
+ }
3862
+ if (session) {
3863
+ conditions.push(eq5(mindHistory.session, session));
3314
3864
  }
3315
- const rows = await db2.select().from(mindMessages).where(and3(...conditions)).orderBy(desc2(mindMessages.created_at)).limit(limit).offset(offset);
3865
+ if (!full) {
3866
+ conditions.push(sql3`${mindHistory.type} IN ('inbound', 'outbound')`);
3867
+ }
3868
+ const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
3316
3869
  return c.json(rows);
3317
3870
  });
3318
- var minds_default = app7;
3871
+ var minds_default = app8;
3319
3872
 
3320
- // src/web/routes/pages.ts
3873
+ // src/web/api/pages.ts
3321
3874
  import { readFile as readFile2, stat } from "fs/promises";
3322
3875
  import { extname, resolve as resolve12 } from "path";
3323
- import { Hono as Hono8 } from "hono";
3876
+ import { Hono as Hono9 } from "hono";
3324
3877
  var MIME_TYPES = {
3325
3878
  ".html": "text/html",
3326
3879
  ".js": "application/javascript",
@@ -3337,7 +3890,7 @@ var MIME_TYPES = {
3337
3890
  ".txt": "text/plain",
3338
3891
  ".xml": "application/xml"
3339
3892
  };
3340
- var app8 = new Hono8().get("/:name/*", async (c) => {
3893
+ var app9 = new Hono9().get("/:name/*", async (c) => {
3341
3894
  const name = c.req.param("name");
3342
3895
  if (!findMind(name)) return c.text("Not found", 404);
3343
3896
  const pagesRoot = resolve12(mindDir(name), "home", "pages");
@@ -3362,10 +3915,61 @@ var app8 = new Hono8().get("/:name/*", async (c) => {
3362
3915
  }
3363
3916
  return c.text("Not found", 404);
3364
3917
  });
3365
- var pages_default = app8;
3918
+ var pages_default = app9;
3366
3919
 
3367
- // src/web/routes/schedules.ts
3368
- import { Hono as Hono9 } from "hono";
3920
+ // src/web/api/prompts.ts
3921
+ 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";
3924
+ import { z as z4 } from "zod";
3925
+ var app10 = new Hono10().get("/", async (c) => {
3926
+ let rows;
3927
+ try {
3928
+ const db = await getDb();
3929
+ rows = await db.select().from(systemPrompts).all();
3930
+ } catch (err) {
3931
+ console.error("[prompts] failed to query system_prompts:", err);
3932
+ return c.json({ error: "Failed to load prompts from database" }, 500);
3933
+ }
3934
+ const customMap = new Map(rows.map((r) => [r.key, r.content]));
3935
+ const prompts = PROMPT_KEYS.map((key) => {
3936
+ const meta = PROMPT_DEFAULTS[key];
3937
+ const custom = customMap.get(key);
3938
+ return {
3939
+ key,
3940
+ content: custom ?? meta.content,
3941
+ description: meta.description,
3942
+ variables: meta.variables,
3943
+ isCustom: custom !== void 0,
3944
+ category: meta.category
3945
+ };
3946
+ });
3947
+ return c.json(prompts);
3948
+ }).put("/:key", requireAdmin, zValidator4("json", z4.object({ content: z4.string() })), async (c) => {
3949
+ const key = c.req.param("key");
3950
+ if (!PROMPT_KEYS.includes(key)) {
3951
+ return c.json({ error: "Unknown prompt key" }, 404);
3952
+ }
3953
+ const { content } = c.req.valid("json");
3954
+ const db = await getDb();
3955
+ await db.insert(systemPrompts).values({ key, content, updated_at: sql4`(datetime('now'))` }).onConflictDoUpdate({
3956
+ target: systemPrompts.key,
3957
+ set: { content, updated_at: sql4`(datetime('now'))` }
3958
+ });
3959
+ return c.json({ ok: true });
3960
+ }).delete("/:key", requireAdmin, async (c) => {
3961
+ const key = c.req.param("key");
3962
+ if (!PROMPT_KEYS.includes(key)) {
3963
+ return c.json({ error: "Unknown prompt key" }, 404);
3964
+ }
3965
+ const db = await getDb();
3966
+ await db.delete(systemPrompts).where(eq6(systemPrompts.key, key));
3967
+ return c.json({ ok: true });
3968
+ });
3969
+ var prompts_default = app10;
3970
+
3971
+ // src/web/api/schedules.ts
3972
+ import { Hono as Hono11 } from "hono";
3369
3973
  function readSchedules(name) {
3370
3974
  return readVoluteConfig(mindDir(name))?.schedules ?? [];
3371
3975
  }
@@ -3376,7 +3980,7 @@ function writeSchedules(name, schedules) {
3376
3980
  writeVoluteConfig(dir, config);
3377
3981
  getScheduler().loadSchedules(name);
3378
3982
  }
3379
- var app9 = new Hono9().get("/:name/schedules", (c) => {
3983
+ var app11 = new Hono11().get("/:name/schedules", (c) => {
3380
3984
  const name = c.req.param("name");
3381
3985
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
3382
3986
  return c.json(readSchedules(name));
@@ -3447,12 +4051,85 @@ var app9 = new Hono9().get("/:name/schedules", (c) => {
3447
4051
  return c.json({ error: "Failed to reach mind" }, 502);
3448
4052
  }
3449
4053
  });
3450
- var schedules_default = app9;
4054
+ var schedules_default = app11;
3451
4055
 
3452
- // src/web/routes/system.ts
3453
- import { Hono as Hono10 } from "hono";
4056
+ // src/web/api/skills.ts
4057
+ import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
4058
+ import { tmpdir as tmpdir2 } from "os";
4059
+ import { join as join3, resolve as resolve13 } from "path";
4060
+ import AdmZip from "adm-zip";
4061
+ import { Hono as Hono12 } from "hono";
4062
+ var app12 = new Hono12().get("/", async (c) => {
4063
+ const skills = await listSharedSkills();
4064
+ return c.json(skills);
4065
+ }).get("/:id", async (c) => {
4066
+ const id = c.req.param("id");
4067
+ const skill = await getSharedSkill(id);
4068
+ if (!skill) return c.json({ error: "Skill not found" }, 404);
4069
+ const dir = join3(sharedSkillsDir(), id);
4070
+ const files = listFilesRecursive(dir);
4071
+ return c.json({ ...skill, files });
4072
+ }).post("/upload", requireAdmin, async (c) => {
4073
+ const body = await c.req.parseBody();
4074
+ const file = body.file;
4075
+ if (!file || !(file instanceof File)) {
4076
+ return c.json({ error: "No file uploaded" }, 400);
4077
+ }
4078
+ if (!file.name.endsWith(".zip")) {
4079
+ return c.json({ error: "Only .zip files are accepted" }, 400);
4080
+ }
4081
+ const buffer = Buffer.from(await file.arrayBuffer());
4082
+ const tmpDir = mkdtempSync(join3(tmpdir2(), "volute-skill-upload-"));
4083
+ try {
4084
+ const zip = new AdmZip(buffer);
4085
+ for (const entry of zip.getEntries()) {
4086
+ const target = resolve13(tmpDir, entry.entryName);
4087
+ if (!target.startsWith(tmpDir)) {
4088
+ return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
4089
+ }
4090
+ }
4091
+ zip.extractAllTo(tmpDir, true);
4092
+ let skillDir = null;
4093
+ if (existsSync9(join3(tmpDir, "SKILL.md"))) {
4094
+ skillDir = tmpDir;
4095
+ } else {
4096
+ const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4097
+ for (const entry of entries) {
4098
+ if (existsSync9(join3(tmpDir, entry.name, "SKILL.md"))) {
4099
+ skillDir = join3(tmpDir, entry.name);
4100
+ break;
4101
+ }
4102
+ }
4103
+ }
4104
+ if (!skillDir) {
4105
+ return c.json({ error: "No SKILL.md found in zip (checked root and one level deep)" }, 400);
4106
+ }
4107
+ const skill = await importSkillFromDir(skillDir, "upload");
4108
+ return c.json(skill);
4109
+ } catch (e) {
4110
+ if (e instanceof Error && e.message.includes("Invalid skill ID")) {
4111
+ return c.json({ error: e.message }, 400);
4112
+ }
4113
+ throw e;
4114
+ } finally {
4115
+ rmSync3(tmpDir, { recursive: true, force: true });
4116
+ }
4117
+ }).delete("/:id", requireAdmin, async (c) => {
4118
+ const id = c.req.param("id");
4119
+ try {
4120
+ await removeSharedSkill(id);
4121
+ } catch (e) {
4122
+ const msg = e instanceof Error ? e.message : String(e);
4123
+ return c.json({ error: msg }, 404);
4124
+ }
4125
+ return c.json({ ok: true });
4126
+ });
4127
+ var skills_default = app12;
4128
+
4129
+ // src/web/api/system.ts
4130
+ import { Hono as Hono13 } from "hono";
3454
4131
  import { streamSSE as streamSSE2 } from "hono/streaming";
3455
- var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
4132
+ var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
3456
4133
  setTimeout(() => process.exit(1), 200);
3457
4134
  return c.json({ ok: true });
3458
4135
  }).post("/stop", requireAdmin, (c) => {
@@ -3469,10 +4146,10 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
3469
4146
  stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
3470
4147
  });
3471
4148
  });
3472
- await new Promise((resolve18) => {
4149
+ await new Promise((resolve19) => {
3473
4150
  stream.onAbort(() => {
3474
4151
  unsubscribe();
3475
- resolve18();
4152
+ resolve19();
3476
4153
  });
3477
4154
  });
3478
4155
  });
@@ -3480,18 +4157,18 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
3480
4157
  const config = readSystemsConfig();
3481
4158
  return c.json({ system: config?.system ?? null });
3482
4159
  });
3483
- var system_default = app10;
4160
+ var system_default = app13;
3484
4161
 
3485
- // src/web/routes/typing.ts
3486
- import { zValidator as zValidator3 } from "@hono/zod-validator";
3487
- import { Hono as Hono11 } from "hono";
3488
- import { z as z3 } from "zod";
3489
- var typingSchema = z3.object({
3490
- channel: z3.string().min(1),
3491
- sender: z3.string().min(1),
3492
- active: z3.boolean()
4162
+ // src/web/api/typing.ts
4163
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
4164
+ import { Hono as Hono14 } from "hono";
4165
+ import { z as z5 } from "zod";
4166
+ var typingSchema = z5.object({
4167
+ channel: z5.string().min(1),
4168
+ sender: z5.string().min(1),
4169
+ active: z5.boolean()
3493
4170
  });
3494
- var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
4171
+ var app14 = new Hono14().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
3495
4172
  const { channel, sender, active } = c.req.valid("json");
3496
4173
  const map = getTypingMap();
3497
4174
  if (active) {
@@ -3508,13 +4185,13 @@ var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema)
3508
4185
  const map = getTypingMap();
3509
4186
  return c.json({ typing: map.get(channel) });
3510
4187
  });
3511
- var typing_default = app11;
4188
+ var typing_default = app14;
3512
4189
 
3513
- // src/web/routes/update.ts
4190
+ // src/web/api/update.ts
3514
4191
  import { spawn as spawn3 } from "child_process";
3515
- import { Hono as Hono12 } from "hono";
4192
+ import { Hono as Hono15 } from "hono";
3516
4193
  var bin;
3517
- var app12 = new Hono12().get("/update", async (c) => {
4194
+ var app15 = new Hono15().get("/update", async (c) => {
3518
4195
  const result = await checkForUpdate();
3519
4196
  return c.json(result);
3520
4197
  }).post("/update", requireAdmin, async (c) => {
@@ -3529,19 +4206,19 @@ var app12 = new Hono12().get("/update", async (c) => {
3529
4206
  child.unref();
3530
4207
  return c.json({ ok: true, message: "Updating..." });
3531
4208
  });
3532
- var update_default = app12;
4209
+ var update_default = app15;
3533
4210
 
3534
- // src/web/routes/variants.ts
3535
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
3536
- import { resolve as resolve14 } from "path";
3537
- import { Hono as Hono13 } from "hono";
4211
+ // 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";
3538
4215
 
3539
4216
  // src/lib/spawn-server.ts
3540
4217
  import { spawn as spawn4 } from "child_process";
3541
- import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync7 } from "fs";
3542
- import { resolve as resolve13 } from "path";
4218
+ import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync8 } from "fs";
4219
+ import { resolve as resolve14 } from "path";
3543
4220
  function tsxBin(cwd) {
3544
- return resolve13(cwd, "node_modules", ".bin", "tsx");
4221
+ return resolve14(cwd, "node_modules", ".bin", "tsx");
3545
4222
  }
3546
4223
  function spawnServer(cwd, port, options) {
3547
4224
  if (options?.detached) {
@@ -3554,31 +4231,31 @@ function spawnAttached(cwd, port) {
3554
4231
  cwd,
3555
4232
  stdio: ["ignore", "pipe", "pipe"]
3556
4233
  });
3557
- return new Promise((resolve18) => {
3558
- const timeout = setTimeout(() => resolve18(null), 3e4);
4234
+ return new Promise((resolve19) => {
4235
+ const timeout = setTimeout(() => resolve19(null), 3e4);
3559
4236
  function checkOutput(data) {
3560
4237
  const match = data.toString().match(/listening on :(\d+)/);
3561
4238
  if (match) {
3562
4239
  clearTimeout(timeout);
3563
- resolve18({ child, actualPort: parseInt(match[1], 10) });
4240
+ resolve19({ child, actualPort: parseInt(match[1], 10) });
3564
4241
  }
3565
4242
  }
3566
4243
  child.stdout?.on("data", checkOutput);
3567
4244
  child.stderr?.on("data", checkOutput);
3568
4245
  child.on("error", () => {
3569
4246
  clearTimeout(timeout);
3570
- resolve18(null);
4247
+ resolve19(null);
3571
4248
  });
3572
4249
  child.on("exit", () => {
3573
4250
  clearTimeout(timeout);
3574
- resolve18(null);
4251
+ resolve19(null);
3575
4252
  });
3576
4253
  });
3577
4254
  }
3578
4255
  function spawnDetached(cwd, port, logDir) {
3579
- const logsDir = logDir ?? resolve13(cwd, ".volute", "logs");
3580
- mkdirSync5(logsDir, { recursive: true });
3581
- const logPath = resolve13(logsDir, "mind.log");
4256
+ const logsDir = logDir ?? resolve14(cwd, ".volute", "logs");
4257
+ mkdirSync6(logsDir, { recursive: true });
4258
+ const logPath = resolve14(logsDir, "mind.log");
3582
4259
  const logFd = openSync(logPath, "a");
3583
4260
  const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
3584
4261
  cwd,
@@ -3598,7 +4275,7 @@ function spawnDetached(cwd, port, logDir) {
3598
4275
  }
3599
4276
  const interval = setInterval(() => {
3600
4277
  try {
3601
- const content = readFileSync7(logPath, "utf-8");
4278
+ const content = readFileSync8(logPath, "utf-8");
3602
4279
  const match = content.match(/listening on :(\d+)/);
3603
4280
  if (match) {
3604
4281
  finish({ child, actualPort: parseInt(match[1], 10) });
@@ -3649,8 +4326,8 @@ async function verify(port) {
3649
4326
  }
3650
4327
  }
3651
4328
 
3652
- // src/web/routes/variants.ts
3653
- var app13 = new Hono13().get("/:name/variants", async (c) => {
4329
+ // src/web/api/variants.ts
4330
+ var app16 = new Hono16().get("/:name/variants", async (c) => {
3654
4331
  const name = c.req.param("name");
3655
4332
  const entry = findMind(name);
3656
4333
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -3680,11 +4357,11 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3680
4357
  const err = validateBranchName(variantName);
3681
4358
  if (err) return c.json({ error: err }, 400);
3682
4359
  const projectRoot = mindDir(mindName);
3683
- const variantDir = resolve14(projectRoot, ".variants", variantName);
3684
- if (existsSync9(variantDir)) {
4360
+ const variantDir = resolve15(projectRoot, ".variants", variantName);
4361
+ if (existsSync10(variantDir)) {
3685
4362
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
3686
4363
  }
3687
- mkdirSync6(resolve14(projectRoot, ".variants"), { recursive: true });
4364
+ mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
3688
4365
  try {
3689
4366
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
3690
4367
  } catch (e) {
@@ -3697,7 +4374,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3697
4374
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
3698
4375
  await exec(cmd, args, {
3699
4376
  cwd: variantDir,
3700
- env: { ...process.env, HOME: resolve14(variantDir, "home") }
4377
+ env: { ...process.env, HOME: resolve15(variantDir, "home") }
3701
4378
  });
3702
4379
  } else {
3703
4380
  await exec("npm", ["install"], { cwd: variantDir });
@@ -3707,7 +4384,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3707
4384
  return c.json({ error: `npm install failed: ${msg}` }, 500);
3708
4385
  }
3709
4386
  if (body.soul) {
3710
- writeFileSync6(resolve14(variantDir, "home/SOUL.md"), body.soul);
4387
+ writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
3711
4388
  }
3712
4389
  const variantPort = body.port ?? nextPort();
3713
4390
  const variant = {
@@ -3745,7 +4422,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3745
4422
  } catch {
3746
4423
  }
3747
4424
  const projectRoot = mindDir(mindName);
3748
- if (existsSync9(variant.path)) {
4425
+ if (existsSync10(variant.path)) {
3749
4426
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
3750
4427
  if (status) {
3751
4428
  try {
@@ -3802,7 +4479,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3802
4479
  } catch (e) {
3803
4480
  return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
3804
4481
  }
3805
- if (existsSync9(variant.path)) {
4482
+ if (existsSync10(variant.path)) {
3806
4483
  try {
3807
4484
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
3808
4485
  } catch {
@@ -3819,7 +4496,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3819
4496
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
3820
4497
  await exec(cmd, args, {
3821
4498
  cwd: projectRoot,
3822
- env: { ...process.env, HOME: resolve14(projectRoot, "home") }
4499
+ env: { ...process.env, HOME: resolve15(projectRoot, "home") }
3823
4500
  });
3824
4501
  } else {
3825
4502
  await exec("npm", ["install"], { cwd: projectRoot });
@@ -3862,7 +4539,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3862
4539
  } catch {
3863
4540
  }
3864
4541
  }
3865
- if (existsSync9(variant.path)) {
4542
+ if (existsSync10(variant.path)) {
3866
4543
  try {
3867
4544
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
3868
4545
  } catch {
@@ -3876,29 +4553,83 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3876
4553
  chownMindDir(projectRoot, mindName);
3877
4554
  return c.json({ ok: true });
3878
4555
  });
3879
- var variants_default = app13;
4556
+ var variants_default = app16;
3880
4557
 
3881
- // src/web/routes/volute/chat.ts
3882
- import { readFileSync as readFileSync8 } from "fs";
3883
- import { resolve as resolve15 } from "path";
3884
- import { zValidator as zValidator4 } from "@hono/zod-validator";
3885
- import { Hono as Hono14 } from "hono";
4558
+ // src/web/api/volute/channels.ts
4559
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
4560
+ import { Hono as Hono17 } from "hono";
4561
+ import { z as z6 } from "zod";
4562
+ var createSchema = z6.object({
4563
+ name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
4564
+ });
4565
+ var app17 = new Hono17().get("/", async (c) => {
4566
+ const user = c.get("user");
4567
+ const channels = await listChannels();
4568
+ const results = await Promise.all(
4569
+ channels.map(async (ch) => {
4570
+ const participants = await getParticipants(ch.id);
4571
+ const isMember = participants.some((p) => p.userId === user.id);
4572
+ return { ...ch, participantCount: participants.length, isMember };
4573
+ })
4574
+ );
4575
+ return c.json(results);
4576
+ }).post("/", zValidator6("json", createSchema), async (c) => {
4577
+ const user = c.get("user");
4578
+ const body = c.req.valid("json");
4579
+ try {
4580
+ const ch = await createChannel(body.name, user.id);
4581
+ return c.json(ch, 201);
4582
+ } catch (err) {
4583
+ const cause = err instanceof Error ? err.cause : null;
4584
+ if (cause && /UNIQUE/i.test(cause.extendedCode ?? cause.message ?? "")) {
4585
+ return c.json({ error: "Channel already exists" }, 409);
4586
+ }
4587
+ throw err;
4588
+ }
4589
+ }).post("/:name/join", async (c) => {
4590
+ const name = c.req.param("name");
4591
+ const user = c.get("user");
4592
+ const ch = await getChannelByName(name);
4593
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4594
+ await joinChannel(ch.id, user.id);
4595
+ return c.json({ ok: true, conversationId: ch.id });
4596
+ }).post("/:name/leave", async (c) => {
4597
+ const name = c.req.param("name");
4598
+ const user = c.get("user");
4599
+ const ch = await getChannelByName(name);
4600
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4601
+ await leaveChannel(ch.id, user.id);
4602
+ return c.json({ ok: true });
4603
+ }).get("/:name/members", async (c) => {
4604
+ const name = c.req.param("name");
4605
+ const ch = await getChannelByName(name);
4606
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4607
+ const participants = await getParticipants(ch.id);
4608
+ return c.json(participants);
4609
+ });
4610
+ var channels_default2 = app17;
4611
+
4612
+ // src/web/api/volute/chat.ts
4613
+ import { readFileSync as readFileSync9 } from "fs";
4614
+ import { resolve as resolve16 } from "path";
4615
+ import { zValidator as zValidator7 } from "@hono/zod-validator";
4616
+ import { Hono as Hono18 } from "hono";
3886
4617
  import { streamSSE as streamSSE3 } from "hono/streaming";
3887
- import { z as z4 } from "zod";
3888
- var chatSchema = z4.object({
3889
- message: z4.string().optional(),
3890
- conversationId: z4.string().optional(),
3891
- sender: z4.string().optional(),
3892
- images: z4.array(
3893
- z4.object({
3894
- media_type: z4.string(),
3895
- data: z4.string()
4618
+ import { z as z7 } from "zod";
4619
+ var chatSchema = z7.object({
4620
+ message: z7.string().optional(),
4621
+ conversationId: z7.string().optional(),
4622
+ sender: z7.string().optional(),
4623
+ images: z7.array(
4624
+ z7.object({
4625
+ media_type: z7.string(),
4626
+ data: z7.string()
3896
4627
  })
3897
4628
  ).optional()
3898
4629
  });
3899
4630
  function getDaemonUrl() {
3900
4631
  try {
3901
- const data = JSON.parse(readFileSync8(resolve15(voluteHome(), "daemon.json"), "utf-8"));
4632
+ const data = JSON.parse(readFileSync9(resolve16(voluteHome(), "daemon.json"), "utf-8"));
3902
4633
  return `http://${daemonLoopback()}:${data.port}`;
3903
4634
  } catch (err) {
3904
4635
  throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
@@ -3914,7 +4645,7 @@ function daemonFetchInternal(path, body) {
3914
4645
  if (token) headers.Authorization = `Bearer ${token}`;
3915
4646
  return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
3916
4647
  }
3917
- var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
4648
+ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
3918
4649
  const name = c.req.param("name");
3919
4650
  const [baseName] = name.split("@", 2);
3920
4651
  const entry = findMind(baseName);
@@ -3975,7 +4706,7 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
3975
4706
  const participants = await getParticipants(conversationId);
3976
4707
  const mindParticipants = participants.filter((p) => p.userType === "mind");
3977
4708
  const participantNames = participants.map((p) => p.username);
3978
- const { getMindManager: getMindManager2 } = await import("./mind-manager-PN5SUDJ4.js");
4709
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-Z7O7PN2O.js");
3979
4710
  const manager = getMindManager2();
3980
4711
  const runningMinds = mindParticipants.map((ap) => {
3981
4712
  const mindKey = ap.username === baseName ? name : ap.username;
@@ -4020,8 +4751,8 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4020
4751
  });
4021
4752
  daemonFetchInternal(`/api/minds/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
4022
4753
  if (!res.ok) {
4023
- const text2 = await res.text().catch(() => "");
4024
- console.error(`[chat] mind ${mindName} responded ${res.status}: ${text2}`);
4754
+ const text = await res.text().catch(() => "");
4755
+ console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
4025
4756
  }
4026
4757
  }).catch((err) => {
4027
4758
  console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
@@ -4045,27 +4776,116 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4045
4776
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
4046
4777
  });
4047
4778
  }, 15e3);
4048
- await new Promise((resolve18) => {
4779
+ await new Promise((resolve19) => {
4049
4780
  stream.onAbort(() => {
4050
4781
  unsubscribe();
4051
4782
  clearInterval(keepAlive);
4052
- resolve18();
4783
+ resolve19();
4053
4784
  });
4054
4785
  });
4055
4786
  });
4056
4787
  });
4057
- var chat_default = app14;
4788
+ var unifiedChatSchema = z7.object({
4789
+ message: z7.string().optional(),
4790
+ conversationId: z7.string(),
4791
+ images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
4792
+ });
4793
+ var unifiedChatApp = new Hono18().post(
4794
+ "/chat",
4795
+ zValidator7("json", unifiedChatSchema),
4796
+ async (c) => {
4797
+ const user = c.get("user");
4798
+ const body = c.req.valid("json");
4799
+ if (!body.message && (!body.images || body.images.length === 0)) {
4800
+ return c.json({ error: "message or images required" }, 400);
4801
+ }
4802
+ const conv = await getConversation(body.conversationId);
4803
+ if (!conv) return c.json({ error: "Conversation not found" }, 404);
4804
+ if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
4805
+ return c.json({ error: "Conversation not found" }, 404);
4806
+ }
4807
+ const senderName = user.username;
4808
+ const contentBlocks = [];
4809
+ if (body.message) contentBlocks.push({ type: "text", text: body.message });
4810
+ if (body.images) {
4811
+ for (const img of body.images) {
4812
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
4813
+ }
4814
+ }
4815
+ 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
+ }
4874
+ return c.json({ ok: true, conversationId: body.conversationId });
4875
+ }
4876
+ );
4877
+ var chat_default = app18;
4058
4878
 
4059
- // src/web/routes/volute/conversations.ts
4060
- import { zValidator as zValidator5 } from "@hono/zod-validator";
4061
- import { Hono as Hono15 } from "hono";
4062
- import { z as z5 } from "zod";
4063
- var createConvSchema = z5.object({
4064
- title: z5.string().optional(),
4065
- participantIds: z5.array(z5.number()).optional(),
4066
- participantNames: z5.array(z5.string()).optional()
4879
+ // src/web/api/volute/conversations.ts
4880
+ import { zValidator as zValidator8 } from "@hono/zod-validator";
4881
+ import { Hono as Hono19 } from "hono";
4882
+ import { z as z8 } from "zod";
4883
+ var createConvSchema = z8.object({
4884
+ title: z8.string().optional(),
4885
+ participantIds: z8.array(z8.number()).optional(),
4886
+ participantNames: z8.array(z8.string()).optional()
4067
4887
  });
4068
- var app15 = new Hono15().get("/:name/conversations", async (c) => {
4888
+ var app19 = new Hono19().get("/:name/conversations", async (c) => {
4069
4889
  const name = c.req.param("name");
4070
4890
  const user = c.get("user");
4071
4891
  let lookupId = user.id;
@@ -4074,9 +4894,9 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
4074
4894
  lookupId = mindUser.id;
4075
4895
  }
4076
4896
  const all = await listConversationsForUser(lookupId);
4077
- const convs = all.filter((c2) => c2.mind_name === name);
4897
+ const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
4078
4898
  return c.json(convs);
4079
- }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
4899
+ }).post("/:name/conversations", zValidator8("json", createConvSchema), async (c) => {
4080
4900
  const name = c.req.param("name");
4081
4901
  const user = c.get("user");
4082
4902
  const body = c.req.valid("json");
@@ -4150,17 +4970,18 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
4150
4970
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4151
4971
  return c.json({ ok: true });
4152
4972
  });
4153
- var conversations_default = app15;
4973
+ var conversations_default = app19;
4154
4974
 
4155
- // src/web/routes/volute/user-conversations.ts
4156
- import { zValidator as zValidator6 } from "@hono/zod-validator";
4157
- import { Hono as Hono16 } from "hono";
4158
- import { z as z6 } from "zod";
4159
- var createSchema = z6.object({
4160
- title: z6.string().optional(),
4161
- participantNames: z6.array(z6.string()).min(1)
4975
+ // src/web/api/volute/user-conversations.ts
4976
+ import { zValidator as zValidator9 } from "@hono/zod-validator";
4977
+ import { Hono as Hono20 } from "hono";
4978
+ import { streamSSE as streamSSE4 } from "hono/streaming";
4979
+ import { z as z9 } from "zod";
4980
+ var createSchema2 = z9.object({
4981
+ title: z9.string().optional(),
4982
+ participantNames: z9.array(z9.string()).min(1)
4162
4983
  });
4163
- var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4984
+ var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
4164
4985
  const user = c.get("user");
4165
4986
  const convs = await listConversationsWithParticipants(user.id);
4166
4987
  return c.json(convs);
@@ -4172,7 +4993,7 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4172
4993
  }
4173
4994
  const msgs = await getMessages(id);
4174
4995
  return c.json(msgs);
4175
- }).post("/", zValidator6("json", createSchema), async (c) => {
4996
+ }).post("/", zValidator9("json", createSchema2), async (c) => {
4176
4997
  const user = c.get("user");
4177
4998
  const body = c.req.valid("json");
4178
4999
  const participantIds = /* @__PURE__ */ new Set();
@@ -4202,6 +5023,31 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4202
5023
  participantIds: [...participantIds]
4203
5024
  });
4204
5025
  return c.json(conv, 201);
5026
+ }).get("/:id/events", async (c) => {
5027
+ const conversationId = c.req.param("id");
5028
+ const user = c.get("user");
5029
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
5030
+ return c.json({ error: "Conversation not found" }, 404);
5031
+ }
5032
+ return streamSSE4(c, async (stream) => {
5033
+ const unsubscribe = subscribe(conversationId, (event) => {
5034
+ stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
5035
+ if (!stream.aborted) console.error("[chat] SSE write error:", err);
5036
+ });
5037
+ });
5038
+ const keepAlive = setInterval(() => {
5039
+ stream.writeSSE({ data: "" }).catch((err) => {
5040
+ if (!stream.aborted) console.error("[chat] SSE ping error:", err);
5041
+ });
5042
+ }, 15e3);
5043
+ await new Promise((resolve19) => {
5044
+ stream.onAbort(() => {
5045
+ unsubscribe();
5046
+ clearInterval(keepAlive);
5047
+ resolve19();
5048
+ });
5049
+ });
5050
+ });
4205
5051
  }).delete("/:id", async (c) => {
4206
5052
  const id = c.req.param("id");
4207
5053
  const user = c.get("user");
@@ -4209,43 +5055,44 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4209
5055
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4210
5056
  return c.json({ ok: true });
4211
5057
  });
4212
- var user_conversations_default = app16;
5058
+ var user_conversations_default = app20;
4213
5059
 
4214
5060
  // src/web/app.ts
4215
- var app17 = new Hono17();
4216
- app17.onError((err, c) => {
5061
+ var httpLog = logger_default.child("http");
5062
+ var app21 = new Hono21();
5063
+ app21.onError((err, c) => {
4217
5064
  if (err instanceof HTTPException) {
4218
5065
  return err.getResponse();
4219
5066
  }
4220
- logger_default.error("Unhandled error", {
5067
+ logger_default.error("unhandled error", {
4221
5068
  path: c.req.path,
4222
5069
  method: c.req.method,
4223
- error: err.message
5070
+ error: err.stack ?? err.message
4224
5071
  });
4225
5072
  return c.json({ error: "Internal server error" }, 500);
4226
5073
  });
4227
- app17.notFound((c) => {
5074
+ app21.notFound((c) => {
4228
5075
  return c.json({ error: "Not found" }, 404);
4229
5076
  });
4230
- app17.use("*", async (c, next) => {
5077
+ app21.use("*", async (c, next) => {
4231
5078
  const start = Date.now();
4232
5079
  await next();
4233
5080
  const duration = Date.now() - start;
4234
- logger_default.info("request", {
4235
- method: c.req.method,
4236
- path: c.req.path,
4237
- status: c.res.status,
4238
- duration
4239
- });
5081
+ const data = { method: c.req.method, path: c.req.path, status: c.res.status, duration };
5082
+ if (c.res.status >= 400) {
5083
+ httpLog.warn("request error", data);
5084
+ } else {
5085
+ httpLog.debug("request", data);
5086
+ }
4240
5087
  });
4241
- app17.get("/api/health", (c) => {
5088
+ app21.get("/api/health", (c) => {
4242
5089
  let version = "unknown";
4243
5090
  let cached = null;
4244
5091
  try {
4245
5092
  version = getCurrentVersion();
4246
5093
  cached = checkForUpdateCached();
4247
5094
  } catch (err) {
4248
- logger_default.error("Health check error", { error: err.message });
5095
+ logger_default.warn("health check error", { error: err.message });
4249
5096
  }
4250
5097
  return c.json({
4251
5098
  ok: true,
@@ -4253,15 +5100,18 @@ app17.get("/api/health", (c) => {
4253
5100
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
4254
5101
  });
4255
5102
  });
4256
- app17.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
4257
- app17.use("/api/*", csrf());
4258
- app17.use("/api/minds/*", authMiddleware);
4259
- app17.use("/api/conversations/*", authMiddleware);
4260
- app17.use("/api/system/*", authMiddleware);
4261
- app17.use("/api/env/*", authMiddleware);
4262
- app17.route("/pages", pages_default);
4263
- var routes = app17.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", env_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/conversations", user_conversations_default);
4264
- var app_default = app17;
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;
4265
5115
 
4266
5116
  // src/web/server.ts
4267
5117
  var MIME_TYPES2 = {
@@ -4278,20 +5128,20 @@ async function startServer({
4278
5128
  hostname = "127.0.0.1"
4279
5129
  }) {
4280
5130
  let assetsDir = "";
4281
- let searchDir = dirname3(new URL(import.meta.url).pathname);
5131
+ let searchDir = dirname2(new URL(import.meta.url).pathname);
4282
5132
  for (let i = 0; i < 5; i++) {
4283
- const candidate = resolve16(searchDir, "dist", "web-assets");
4284
- if (existsSync10(candidate)) {
5133
+ const candidate = resolve17(searchDir, "dist", "web-assets");
5134
+ if (existsSync11(candidate)) {
4285
5135
  assetsDir = candidate;
4286
5136
  break;
4287
5137
  }
4288
- searchDir = dirname3(searchDir);
5138
+ searchDir = dirname2(searchDir);
4289
5139
  }
4290
5140
  if (assetsDir) {
4291
5141
  app_default.get("*", async (c) => {
4292
5142
  const urlPath = new URL(c.req.url).pathname;
4293
5143
  if (urlPath.startsWith("/api/")) return c.notFound();
4294
- const filePath = resolve16(assetsDir, urlPath.slice(1));
5144
+ const filePath = resolve17(assetsDir, urlPath.slice(1));
4295
5145
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
4296
5146
  const s = await stat2(filePath).catch(() => null);
4297
5147
  if (s?.isFile()) {
@@ -4300,7 +5150,7 @@ async function startServer({
4300
5150
  const body = await readFile3(filePath);
4301
5151
  return c.body(body, 200, { "Content-Type": mime });
4302
5152
  }
4303
- const indexPath = resolve16(assetsDir, "index.html");
5153
+ const indexPath = resolve17(assetsDir, "index.html");
4304
5154
  const indexStat = await stat2(indexPath).catch(() => null);
4305
5155
  if (indexStat?.isFile()) {
4306
5156
  const body = await readFile3(indexPath, "utf-8");
@@ -4310,10 +5160,10 @@ async function startServer({
4310
5160
  });
4311
5161
  }
4312
5162
  const server = serve({ fetch: app_default.fetch, port, hostname });
4313
- await new Promise((resolve18, reject) => {
5163
+ await new Promise((resolve19, reject) => {
4314
5164
  server.on("listening", () => {
4315
5165
  logger_default.info("Volute UI running", { hostname, port });
4316
- resolve18();
5166
+ resolve19();
4317
5167
  });
4318
5168
  server.on("error", (err) => {
4319
5169
  reject(err);
@@ -4324,24 +5174,26 @@ async function startServer({
4324
5174
 
4325
5175
  // src/daemon.ts
4326
5176
  if (!process.env.VOLUTE_HOME) {
4327
- process.env.VOLUTE_HOME = resolve17(homedir2(), ".volute");
5177
+ process.env.VOLUTE_HOME = resolve18(homedir2(), ".volute");
4328
5178
  }
4329
5179
  async function startDaemon(opts) {
4330
5180
  const { port, hostname } = opts;
4331
5181
  const myPid = String(process.pid);
4332
5182
  const home = voluteHome();
4333
5183
  if (!opts.foreground) {
4334
- const log3 = new RotatingLog(resolve17(home, "daemon.log"));
4335
- const write2 = (...args) => log3.write(`${format(...args)}
5184
+ const rotatingLog = new RotatingLog(resolve18(home, "daemon.log"));
5185
+ logger_default.setOutput((line) => rotatingLog.write(`${line}
5186
+ `));
5187
+ const write = (...args) => rotatingLog.write(`${format(...args)}
4336
5188
  `);
4337
- console.log = write2;
4338
- console.error = write2;
4339
- console.warn = write2;
4340
- console.info = write2;
4341
- }
4342
- const DAEMON_PID_PATH = resolve17(home, "daemon.pid");
4343
- const DAEMON_JSON_PATH = resolve17(home, "daemon.json");
4344
- mkdirSync7(home, { recursive: true });
5189
+ console.log = write;
5190
+ console.error = write;
5191
+ console.warn = write;
5192
+ console.info = write;
5193
+ }
5194
+ const DAEMON_PID_PATH = resolve18(home, "daemon.pid");
5195
+ const DAEMON_JSON_PATH = resolve18(home, "daemon.json");
5196
+ mkdirSync8(home, { recursive: true });
4345
5197
  migrateAgentsToMinds();
4346
5198
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
4347
5199
  process.env.VOLUTE_DAEMON_TOKEN = token;
@@ -4353,13 +5205,13 @@ async function startDaemon(opts) {
4353
5205
  } catch (err) {
4354
5206
  const e = err;
4355
5207
  if (e.code === "EADDRINUSE") {
4356
- console.error(`[daemon] port ${port} is already in use`);
5208
+ logger_default.error(`port ${port} is already in use`);
4357
5209
  process.exit(1);
4358
5210
  }
4359
5211
  throw err;
4360
5212
  }
4361
- writeFileSync7(DAEMON_PID_PATH, myPid, { mode: 420 });
4362
- writeFileSync7(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
5213
+ writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
5214
+ writeFileSync8(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
4363
5215
  `, {
4364
5216
  mode: 420
4365
5217
  });
@@ -4377,7 +5229,7 @@ async function startDaemon(opts) {
4377
5229
  try {
4378
5230
  migrateMindState(entry.name);
4379
5231
  } catch (err) {
4380
- console.error(`[daemon] failed to migrate state for ${entry.name}:`, err);
5232
+ logger_default.warn(`failed to migrate state for ${entry.name}`, logger_default.errorData(err));
4381
5233
  }
4382
5234
  }
4383
5235
  for (const entry of registry) {
@@ -4388,9 +5240,8 @@ async function startDaemon(opts) {
4388
5240
  const dir = mindDir(entry.name);
4389
5241
  await connectors.startConnectors(entry.name, dir, entry.port, port);
4390
5242
  scheduler.loadSchedules(entry.name);
4391
- ensureMailAddress(entry.name).catch(
4392
- (err) => console.error(`[mail] failed to ensure address for ${entry.name}:`, err)
4393
- );
5243
+ ensureMailAddress(entry.name).catch(() => {
5244
+ });
4394
5245
  const config = readVoluteConfig(dir);
4395
5246
  if (config?.tokenBudget) {
4396
5247
  tokenBudget.setBudget(
@@ -4400,7 +5251,7 @@ async function startDaemon(opts) {
4400
5251
  );
4401
5252
  }
4402
5253
  } catch (err) {
4403
- console.error(`[daemon] failed to start mind ${entry.name}:`, err);
5254
+ logger_default.error(`failed to start mind ${entry.name}`, logger_default.errorData(err));
4404
5255
  setMindRunning(entry.name, false);
4405
5256
  }
4406
5257
  }
@@ -4410,22 +5261,22 @@ async function startDaemon(opts) {
4410
5261
  try {
4411
5262
  await manager.startMind(compositeKey);
4412
5263
  } catch (err) {
4413
- console.error(`[daemon] failed to start variant ${compositeKey}:`, err);
5264
+ logger_default.error(`failed to start variant ${compositeKey}`, logger_default.errorData(err));
4414
5265
  setVariantRunning(mindName, variant.name, false);
4415
5266
  }
4416
5267
  }
4417
5268
  cleanExpiredSessions().catch(() => {
4418
5269
  });
4419
- console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
5270
+ logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
4420
5271
  function cleanup() {
4421
5272
  try {
4422
- if (readFileSync9(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
5273
+ if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
4423
5274
  unlinkSync2(DAEMON_PID_PATH);
4424
5275
  }
4425
5276
  } catch {
4426
5277
  }
4427
5278
  try {
4428
- const data = JSON.parse(readFileSync9(DAEMON_JSON_PATH, "utf-8"));
5279
+ const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
4429
5280
  if (data.token === token) {
4430
5281
  unlinkSync2(DAEMON_JSON_PATH);
4431
5282
  }
@@ -4436,7 +5287,7 @@ async function startDaemon(opts) {
4436
5287
  async function shutdown() {
4437
5288
  if (shuttingDown) return;
4438
5289
  shuttingDown = true;
4439
- console.error("[daemon] shutting down...");
5290
+ logger_default.info("shutting down...");
4440
5291
  scheduler.stop();
4441
5292
  scheduler.saveState();
4442
5293
  mailPoller.stop();