volute 0.19.0 → 0.20.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 (51) hide show
  1. package/README.md +66 -66
  2. package/dist/activity-events-OMXKXD5N.js +16 -0
  3. package/dist/{chunk-Z524RFCJ.js → chunk-5XNT2472.js} +1 -1
  4. package/dist/{chunk-FGV2H4TX.js → chunk-FGSYHIS3.js} +112 -24
  5. package/dist/chunk-GZ7DW4YL.js +97 -0
  6. package/dist/{chunk-OTWLI7F4.js → chunk-IKMY5X76.js} +2 -2
  7. package/dist/{chunk-VQWDC6UK.js → chunk-NSE7VJQA.js} +17 -0
  8. package/dist/{chunk-EMQSAY3B.js → chunk-O6ASDHFO.js} +2 -1
  9. package/dist/{chunk-2TJGRJ4O.js → chunk-PUVXOZ6T.js} +8 -2
  10. package/dist/{chunk-4KPUF5JD.js → chunk-TIWH32HP.js} +15 -2
  11. package/dist/chunk-UU7A7KLB.js +58 -0
  12. package/dist/cli.js +19 -9
  13. package/dist/{daemon-restart-JMZM3QY4.js → daemon-restart-KPSWNYTH.js} +3 -3
  14. package/dist/daemon.js +1802 -1082
  15. package/dist/{db-5ZVC6MQF.js → db-C2CJ46ZU.js} +2 -2
  16. package/dist/{delivery-manager-ISTJMZDW.js → delivery-manager-CSG7LXA4.js} +3 -3
  17. package/dist/{export-GCDNQCF3.js → export-6QBUOQGC.js} +2 -2
  18. package/dist/file-C57SK5DK.js +204 -0
  19. package/dist/{import-M63VIUJ5.js → import-XEC34Y4Z.js} +1 -1
  20. package/dist/{mind-PQ5NCPSU.js → mind-Z7CKD6DG.js} +2 -2
  21. package/dist/mind-activity-tracker-624QLQLC.js +19 -0
  22. package/dist/{mind-manager-RVCFROAY.js → mind-manager-3DMYKZPB.js} +3 -3
  23. package/dist/{package-MYE2ZJLV.js → package-4NHAVUUI.js} +1 -1
  24. package/dist/{pages-AXCOSY3P.js → pages-4DGQT7ZA.js} +2 -2
  25. package/dist/{publish-YB377JB7.js → publish-TAJUET4I.js} +7 -4
  26. package/dist/{schedule-LMX7GAQZ.js → schedule-FFZG23IW.js} +25 -5
  27. package/dist/{schema-5BW7DFZI.js → schema-GFH6RV3W.js} +3 -1
  28. package/dist/{setup-OH3PJUJO.js → setup-52YRV7VP.js} +16 -0
  29. package/dist/skills/volute-mind/SKILL.md +33 -3
  30. package/dist/{sprout-VBEX63LX.js → sprout-QN7Y4VVO.js} +3 -3
  31. package/dist/{status-JCJAOXTW.js → status-FU2PFVVF.js} +3 -2
  32. package/dist/{up-WG65SWJU.js → up-FS7CKM6V.js} +1 -1
  33. package/dist/web-assets/assets/index-CUZTZzaW.js +64 -0
  34. package/dist/web-assets/assets/index-adVuCkqy.css +1 -0
  35. package/dist/web-assets/index.html +2 -2
  36. package/drizzle/0012_activity.sql +11 -0
  37. package/drizzle/meta/0012_snapshot.json +7 -0
  38. package/drizzle/meta/_journal.json +7 -0
  39. package/package.json +1 -1
  40. package/templates/_base/home/.config/routes.json +2 -2
  41. package/templates/_base/home/VOLUTE.md +1 -1
  42. package/templates/_base/src/lib/daemon-client.ts +22 -0
  43. package/templates/_base/src/lib/transparency.ts +1 -1
  44. package/templates/claude/.init/.config/routes.json +7 -1
  45. package/templates/pi/.init/.config/routes.json +7 -1
  46. package/templates/pi/src/agent.ts +11 -5
  47. package/templates/pi/src/lib/session-context-extension.ts +6 -4
  48. package/templates/pi/src/server.ts +2 -0
  49. package/dist/web-assets/assets/index-BAbuRsVF.css +0 -1
  50. package/dist/web-assets/assets/index-CiQhSKi_.js +0 -63
  51. /package/dist/{chunk-VE4D3GOP.js → chunk-7UFKREVW.js} +0 -0
package/dist/daemon.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  syncBuiltinSkills,
15
15
  uninstallSkill,
16
16
  updateSkill
17
- } from "./chunk-OTWLI7F4.js";
17
+ } from "./chunk-IKMY5X76.js";
18
18
  import {
19
19
  addSharedWorktree,
20
20
  ensureSharedRepo,
@@ -23,17 +23,16 @@ import {
23
23
  sharedMerge,
24
24
  sharedPull,
25
25
  sharedStatus
26
- } from "./chunk-4KPUF5JD.js";
26
+ } from "./chunk-TIWH32HP.js";
27
27
  import {
28
28
  readSystemsConfig
29
29
  } from "./chunk-FCDU5BFX.js";
30
30
  import {
31
- deliverMessage,
32
- extractTextContent,
33
- getDeliveryManager,
34
- getTypingMap,
35
- initDeliveryManager
36
- } from "./chunk-FGV2H4TX.js";
31
+ getActiveMinds,
32
+ markIdle,
33
+ onMindEvent,
34
+ stopAll
35
+ } from "./chunk-GZ7DW4YL.js";
37
36
  import {
38
37
  PROMPT_DEFAULTS,
39
38
  PROMPT_KEYS,
@@ -48,7 +47,22 @@ import {
48
47
  loadJsonMap,
49
48
  saveJsonMap,
50
49
  substitute
51
- } from "./chunk-2TJGRJ4O.js";
50
+ } from "./chunk-PUVXOZ6T.js";
51
+ import {
52
+ deliverMessage,
53
+ extractTextContent,
54
+ getDeliveryManager,
55
+ getTypingMap,
56
+ initDeliveryManager,
57
+ publish,
58
+ publishTypingForChannels,
59
+ subscribe
60
+ } from "./chunk-FGSYHIS3.js";
61
+ import {
62
+ broadcast,
63
+ publish as publish2,
64
+ subscribe as subscribe2
65
+ } from "./chunk-UU7A7KLB.js";
52
66
  import {
53
67
  logBuffer,
54
68
  logger_default
@@ -64,7 +78,7 @@ import {
64
78
  parseNameFromIdentity,
65
79
  readVoluteConfig,
66
80
  writeVoluteConfig
67
- } from "./chunk-EMQSAY3B.js";
81
+ } from "./chunk-O6ASDHFO.js";
68
82
  import {
69
83
  loadMergedEnv,
70
84
  mindEnvPath,
@@ -74,8 +88,9 @@ import {
74
88
  } from "./chunk-VDWCHYTS.js";
75
89
  import {
76
90
  getDb
77
- } from "./chunk-Z524RFCJ.js";
91
+ } from "./chunk-5XNT2472.js";
78
92
  import {
93
+ activity,
79
94
  conversationParticipants,
80
95
  conversations,
81
96
  messages,
@@ -83,7 +98,7 @@ import {
83
98
  sessions,
84
99
  systemPrompts,
85
100
  users
86
- } from "./chunk-VQWDC6UK.js";
101
+ } from "./chunk-NSE7VJQA.js";
87
102
  import "./chunk-D424ZQGI.js";
88
103
  import {
89
104
  exec,
@@ -135,10 +150,10 @@ import {
135
150
  import "./chunk-K3NQKI34.js";
136
151
 
137
152
  // src/daemon.ts
138
- import { randomBytes } from "crypto";
139
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
153
+ import { randomBytes as randomBytes2 } from "crypto";
154
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
140
155
  import { homedir as homedir2 } from "os";
141
- import { resolve as resolve19 } from "path";
156
+ import { resolve as resolve22 } from "path";
142
157
  import { format } from "util";
143
158
 
144
159
  // src/lib/connector-manager.ts
@@ -382,19 +397,19 @@ var ConnectorManager = class {
382
397
  const stopKey = `${mindName}:${type}`;
383
398
  this.stopping.add(stopKey);
384
399
  mindMap.delete(type);
385
- await new Promise((resolve20) => {
386
- tracked.child.on("exit", () => resolve20());
400
+ await new Promise((resolve23) => {
401
+ tracked.child.on("exit", () => resolve23());
387
402
  try {
388
403
  tracked.child.kill("SIGTERM");
389
404
  } catch {
390
- resolve20();
405
+ resolve23();
391
406
  }
392
407
  setTimeout(() => {
393
408
  try {
394
409
  tracked.child.kill("SIGKILL");
395
410
  } catch {
396
411
  }
397
- resolve20();
412
+ resolve23();
398
413
  }, 5e3);
399
414
  });
400
415
  this.stopping.delete(stopKey);
@@ -885,8 +900,213 @@ function migrateMindState(name) {
885
900
  }
886
901
  }
887
902
 
903
+ // src/lib/pages-watcher.ts
904
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync, watch } from "fs";
905
+ import { join, resolve as resolve5 } from "path";
906
+ var watchers = /* @__PURE__ */ new Map();
907
+ var homeWatchers = /* @__PURE__ */ new Map();
908
+ var debounceTimers = /* @__PURE__ */ new Map();
909
+ var sitesCache = null;
910
+ var recentPagesCache = null;
911
+ function startPagesWatcher(mindName, pagesDir) {
912
+ try {
913
+ const watcher = watch(pagesDir, { recursive: true }, (_eventType, filename) => {
914
+ if (!filename || !filename.endsWith(".html")) return;
915
+ const key = `${mindName}:${filename}`;
916
+ const existing = debounceTimers.get(key);
917
+ if (existing) clearTimeout(existing);
918
+ debounceTimers.set(
919
+ key,
920
+ setTimeout(() => {
921
+ debounceTimers.delete(key);
922
+ invalidateCache();
923
+ publish2({
924
+ type: "page_updated",
925
+ mind: mindName,
926
+ summary: `${mindName} updated ${filename}`,
927
+ metadata: { file: filename }
928
+ }).catch(
929
+ (err) => logger_default.error("failed to publish page_updated activity", logger_default.errorData(err))
930
+ );
931
+ }, 100)
932
+ );
933
+ });
934
+ watchers.set(mindName, watcher);
935
+ } catch (err) {
936
+ logger_default.warn(`failed to start pages watcher for ${mindName}`, logger_default.errorData(err));
937
+ }
938
+ }
939
+ function startWatcher(mindName) {
940
+ if (watchers.has(mindName)) return;
941
+ const pagesDir = resolve5(mindDir(mindName), "home", "pages");
942
+ if (existsSync5(pagesDir)) {
943
+ startPagesWatcher(mindName, pagesDir);
944
+ return;
945
+ }
946
+ if (homeWatchers.has(mindName)) return;
947
+ const homeDir = resolve5(mindDir(mindName), "home");
948
+ if (!existsSync5(homeDir)) return;
949
+ try {
950
+ const hw = watch(homeDir, (_eventType, filename) => {
951
+ if (filename !== "pages") return;
952
+ if (!existsSync5(pagesDir)) return;
953
+ hw.close();
954
+ homeWatchers.delete(mindName);
955
+ invalidateCache();
956
+ startPagesWatcher(mindName, pagesDir);
957
+ });
958
+ homeWatchers.set(mindName, hw);
959
+ } catch (err) {
960
+ logger_default.warn(`failed to start home watcher for ${mindName}`, logger_default.errorData(err));
961
+ }
962
+ }
963
+ function stopWatcher(mindName) {
964
+ const watcher = watchers.get(mindName);
965
+ if (watcher) {
966
+ watcher.close();
967
+ watchers.delete(mindName);
968
+ }
969
+ const hw = homeWatchers.get(mindName);
970
+ if (hw) {
971
+ hw.close();
972
+ homeWatchers.delete(mindName);
973
+ }
974
+ for (const [key, timer] of debounceTimers) {
975
+ if (key.startsWith(`${mindName}:`)) {
976
+ clearTimeout(timer);
977
+ debounceTimers.delete(key);
978
+ }
979
+ }
980
+ }
981
+ function stopAllWatchers() {
982
+ for (const [, watcher] of watchers) {
983
+ watcher.close();
984
+ }
985
+ watchers.clear();
986
+ for (const [, hw] of homeWatchers) {
987
+ hw.close();
988
+ }
989
+ homeWatchers.clear();
990
+ for (const [, timer] of debounceTimers) {
991
+ clearTimeout(timer);
992
+ }
993
+ debounceTimers.clear();
994
+ invalidateCache();
995
+ }
996
+ function invalidateCache() {
997
+ sitesCache = null;
998
+ recentPagesCache = null;
999
+ }
1000
+ function scanPagesDir(dir, urlPrefix) {
1001
+ const pages = [];
1002
+ let items;
1003
+ try {
1004
+ items = readdirSync2(dir);
1005
+ } catch {
1006
+ return pages;
1007
+ }
1008
+ for (const item of items) {
1009
+ if (item.startsWith(".")) continue;
1010
+ const fullPath = resolve5(dir, item);
1011
+ try {
1012
+ const s = statSync(fullPath);
1013
+ if (s.isFile() && item.endsWith(".html")) {
1014
+ pages.push({
1015
+ file: item,
1016
+ modified: s.mtime.toISOString(),
1017
+ url: `${urlPrefix}/${item}`
1018
+ });
1019
+ } else if (s.isDirectory()) {
1020
+ const indexPath = resolve5(fullPath, "index.html");
1021
+ if (existsSync5(indexPath)) {
1022
+ const indexStat = statSync(indexPath);
1023
+ pages.push({
1024
+ file: join(item, "index.html"),
1025
+ modified: indexStat.mtime.toISOString(),
1026
+ url: `${urlPrefix}/${item}/`
1027
+ });
1028
+ }
1029
+ }
1030
+ } catch {
1031
+ }
1032
+ }
1033
+ pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
1034
+ return pages;
1035
+ }
1036
+ function buildSites() {
1037
+ const sites = [];
1038
+ const systemPagesDir = resolve5(voluteHome(), "shared", "pages");
1039
+ if (existsSync5(systemPagesDir)) {
1040
+ const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
1041
+ if (systemPages.length > 0) {
1042
+ sites.push({ name: "_system", label: "System", pages: systemPages });
1043
+ }
1044
+ }
1045
+ const entries = readRegistry();
1046
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
1047
+ const pagesDir = resolve5(mindDir(entry.name), "home", "pages");
1048
+ if (!existsSync5(pagesDir)) continue;
1049
+ const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
1050
+ if (mindPages.length > 0) {
1051
+ sites.push({ name: entry.name, label: entry.name, pages: mindPages });
1052
+ }
1053
+ }
1054
+ return sites;
1055
+ }
1056
+ function buildRecentPages() {
1057
+ const entries = readRegistry();
1058
+ const pages = [];
1059
+ for (const entry of entries) {
1060
+ const pagesDir = resolve5(mindDir(entry.name), "home", "pages");
1061
+ if (!existsSync5(pagesDir)) continue;
1062
+ let items;
1063
+ try {
1064
+ items = readdirSync2(pagesDir);
1065
+ } catch {
1066
+ continue;
1067
+ }
1068
+ for (const item of items) {
1069
+ if (item.startsWith(".")) continue;
1070
+ const fullPath = resolve5(pagesDir, item);
1071
+ try {
1072
+ const s = statSync(fullPath);
1073
+ if (s.isFile() && item.endsWith(".html")) {
1074
+ pages.push({
1075
+ mind: entry.name,
1076
+ file: item,
1077
+ modified: s.mtime.toISOString(),
1078
+ url: `/pages/${entry.name}/${item}`
1079
+ });
1080
+ } else if (s.isDirectory()) {
1081
+ const indexPath = resolve5(fullPath, "index.html");
1082
+ if (existsSync5(indexPath)) {
1083
+ const indexStat = statSync(indexPath);
1084
+ pages.push({
1085
+ mind: entry.name,
1086
+ file: join(item, "index.html"),
1087
+ modified: indexStat.mtime.toISOString(),
1088
+ url: `/pages/${entry.name}/${item}/`
1089
+ });
1090
+ }
1091
+ }
1092
+ } catch {
1093
+ }
1094
+ }
1095
+ }
1096
+ pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
1097
+ return pages.slice(0, 10);
1098
+ }
1099
+ function getCachedSites() {
1100
+ if (!sitesCache) sitesCache = buildSites();
1101
+ return sitesCache;
1102
+ }
1103
+ function getCachedRecentPages() {
1104
+ if (!recentPagesCache) recentPagesCache = buildRecentPages();
1105
+ return recentPagesCache;
1106
+ }
1107
+
888
1108
  // src/lib/scheduler.ts
889
- import { resolve as resolve5 } from "path";
1109
+ import { resolve as resolve6 } from "path";
890
1110
  import { CronExpressionParser } from "cron-parser";
891
1111
  var slog = logger_default.child("scheduler");
892
1112
  var Scheduler = class {
@@ -895,7 +1115,7 @@ var Scheduler = class {
895
1115
  lastFired = /* @__PURE__ */ new Map();
896
1116
  // "mind:scheduleId" → epoch minute
897
1117
  get statePath() {
898
- return resolve5(voluteHome(), "scheduler-state.json");
1118
+ return resolve6(voluteHome(), "scheduler-state.json");
899
1119
  }
900
1120
  start() {
901
1121
  this.loadState();
@@ -959,8 +1179,30 @@ var Scheduler = class {
959
1179
  }
960
1180
  async fire(mindName, schedule) {
961
1181
  try {
962
- await deliverMessage(mindName, {
963
- content: [{ type: "text", text: schedule.message }],
1182
+ let text;
1183
+ if (schedule.script) {
1184
+ const homeDir = resolve6(mindDir(mindName), "home");
1185
+ try {
1186
+ const output = await this.runScript(schedule.script, homeDir, mindName);
1187
+ if (!output.trim()) {
1188
+ slog.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
1189
+ return;
1190
+ }
1191
+ text = output;
1192
+ } catch (err) {
1193
+ const stderr = err.stderr ?? "";
1194
+ text = `[script error] ${err.message}${stderr ? `
1195
+ ${stderr}` : ""}`;
1196
+ slog.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
1197
+ }
1198
+ } else if (schedule.message) {
1199
+ text = schedule.message;
1200
+ } else {
1201
+ slog.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
1202
+ return;
1203
+ }
1204
+ await this.deliver(mindName, {
1205
+ content: [{ type: "text", text }],
964
1206
  channel: "system:scheduler",
965
1207
  sender: schedule.id
966
1208
  });
@@ -969,6 +1211,12 @@ var Scheduler = class {
969
1211
  slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
970
1212
  }
971
1213
  }
1214
+ runScript(script, cwd, mindName) {
1215
+ return exec("bash", ["-c", script], { cwd, mindName });
1216
+ }
1217
+ deliver(mindName, payload) {
1218
+ return deliverMessage(mindName, payload);
1219
+ }
972
1220
  };
973
1221
  var instance3 = null;
974
1222
  function initScheduler() {
@@ -982,8 +1230,8 @@ function getScheduler() {
982
1230
  }
983
1231
 
984
1232
  // src/lib/token-budget.ts
985
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
986
- import { resolve as resolve6 } from "path";
1233
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1234
+ import { resolve as resolve7 } from "path";
987
1235
  var tlog = logger_default.child("token-budget");
988
1236
  var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
989
1237
  var MAX_QUEUE_SIZE = 100;
@@ -1090,7 +1338,7 @@ var TokenBudget = class {
1090
1338
  }
1091
1339
  }
1092
1340
  budgetStatePath(mind) {
1093
- return resolve6(stateDir(mind), "budget.json");
1341
+ return resolve7(stateDir(mind), "budget.json");
1094
1342
  }
1095
1343
  saveBudgetState(mind, state) {
1096
1344
  try {
@@ -1111,7 +1359,7 @@ var TokenBudget = class {
1111
1359
  loadBudgetState(mind) {
1112
1360
  try {
1113
1361
  const path = this.budgetStatePath(mind);
1114
- if (!existsSync5(path)) return null;
1362
+ if (!existsSync6(path)) return null;
1115
1363
  const data = JSON.parse(readFileSync4(path, "utf-8"));
1116
1364
  if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
1117
1365
  return {
@@ -1171,6 +1419,11 @@ function getTokenBudget() {
1171
1419
  async function startMindFull(name) {
1172
1420
  const [baseName, variantName] = name.split("@", 2);
1173
1421
  await getMindManager().startMind(name);
1422
+ publish2({
1423
+ type: "mind_started",
1424
+ mind: name,
1425
+ summary: `${name} started`
1426
+ }).catch((err) => logger_default.error("failed to publish mind_started activity", logger_default.errorData(err)));
1174
1427
  if (variantName) return;
1175
1428
  const entry = findMind(baseName);
1176
1429
  if (!entry || entry.stage === "seed") return;
@@ -1189,15 +1442,23 @@ async function startMindFull(name) {
1189
1442
  config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1190
1443
  );
1191
1444
  }
1445
+ startWatcher(baseName);
1192
1446
  }
1193
1447
  async function stopMindFull(name) {
1194
1448
  const [baseName, variantName] = name.split("@", 2);
1195
1449
  if (!variantName) {
1450
+ stopWatcher(baseName);
1451
+ markIdle(baseName);
1196
1452
  await getConnectorManager().stopConnectors(baseName);
1197
1453
  getScheduler().unloadSchedules(baseName);
1198
1454
  getTokenBudget().removeBudget(baseName);
1199
1455
  }
1200
1456
  await getMindManager().stopMind(name);
1457
+ publish2({
1458
+ type: "mind_stopped",
1459
+ mind: name,
1460
+ summary: `${name} stopped`
1461
+ }).catch((err) => logger_default.error("failed to publish mind_stopped activity", logger_default.errorData(err)));
1201
1462
  }
1202
1463
 
1203
1464
  // src/web/middleware/auth.ts
@@ -1418,136 +1679,464 @@ var authMiddleware = createMiddleware(async (c, next) => {
1418
1679
  });
1419
1680
 
1420
1681
  // src/web/server.ts
1421
- import { existsSync as existsSync13 } from "fs";
1682
+ import { existsSync as existsSync15 } from "fs";
1422
1683
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
1423
- import { dirname as dirname3, extname as extname2, resolve as resolve18 } from "path";
1684
+ import { dirname as dirname3, extname as extname2, resolve as resolve21 } from "path";
1424
1685
  import { serve } from "@hono/node-server";
1425
1686
 
1426
1687
  // src/web/app.ts
1427
- import { Hono as Hono23 } from "hono";
1688
+ import { Hono as Hono25 } from "hono";
1428
1689
  import { bodyLimit } from "hono/body-limit";
1429
1690
  import { csrf } from "hono/csrf";
1430
1691
  import { HTTPException } from "hono/http-exception";
1431
1692
 
1432
- // src/web/api/auth.ts
1433
- import { zValidator } from "@hono/zod-validator";
1693
+ // src/web/api/activity.ts
1694
+ import { desc as desc2 } from "drizzle-orm";
1434
1695
  import { Hono } from "hono";
1435
- import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
1436
- import { z } from "zod";
1437
- var credentialsSchema = z.object({
1438
- username: z.string().min(1),
1439
- password: z.string().min(1)
1440
- });
1441
- var changePasswordSchema = z.object({
1442
- currentPassword: z.string().min(1),
1443
- newPassword: z.string().min(1)
1444
- });
1445
- var authenticated = new Hono().use(authMiddleware).post("/change-password", zValidator("json", changePasswordSchema), async (c) => {
1446
- const user = c.get("user");
1447
- const { currentPassword, newPassword } = c.req.valid("json");
1448
- const ok = await changePassword(user.id, currentPassword, newPassword);
1449
- if (!ok) return c.json({ error: "Current password is incorrect" }, 400);
1450
- return c.json({ ok: true });
1451
- });
1452
- var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
1453
- const user = c.get("user");
1454
- if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1455
- const minds = readRegistry();
1456
- for (const mind of minds) {
1457
- await getOrCreateMindUser(mind.name);
1458
- }
1459
- const type = c.req.query("type");
1460
- if (type === "brain" || type === "mind") {
1461
- return c.json(await listUsersByType(type));
1462
- }
1463
- return c.json(await listUsers());
1464
- }).get("/users/pending", async (c) => {
1465
- const user = c.get("user");
1466
- if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1467
- return c.json(await listPendingUsers());
1468
- }).post("/users/:id/approve", async (c) => {
1469
- const user = c.get("user");
1470
- if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1471
- const id = parseInt(c.req.param("id"), 10);
1472
- await approveUser(id);
1473
- return c.json({ ok: true });
1474
- });
1475
- var app = new Hono().post("/register", zValidator("json", credentialsSchema), async (c) => {
1476
- const { username, password } = c.req.valid("json");
1477
- const existing = await getUserByUsername(username);
1478
- if (existing) {
1479
- return c.json({ error: "Username already taken" }, 409);
1480
- }
1481
- const user = await createUser(username, password);
1482
- if (user.role === "admin") {
1483
- const sessionId = await createSession(user.id);
1484
- setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
1485
- }
1486
- return c.json({ id: user.id, username: user.username, role: user.role });
1487
- }).post("/login", zValidator("json", credentialsSchema), async (c) => {
1488
- const { username, password } = c.req.valid("json");
1489
- const user = await verifyUser(username, password);
1490
- if (!user) {
1491
- return c.json({ error: "Invalid credentials" }, 401);
1492
- }
1493
- const sessionId = await createSession(user.id);
1494
- setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
1495
- return c.json({ id: user.id, username: user.username, role: user.role });
1496
- }).post("/logout", async (c) => {
1497
- const sessionId = getCookie2(c, "volute_session");
1498
- if (sessionId) {
1499
- await deleteSession(sessionId);
1500
- deleteCookie(c, "volute_session", { path: "/" });
1501
- }
1502
- return c.json({ ok: true });
1503
- }).get("/me", async (c) => {
1504
- const sessionId = getCookie2(c, "volute_session");
1505
- if (!sessionId) return c.json({ error: "Not logged in" }, 401);
1506
- const userId = await getSessionUserId(sessionId);
1507
- if (userId == null) return c.json({ error: "Not logged in" }, 401);
1508
- const user = await getUser(userId);
1509
- if (!user) return c.json({ error: "Not logged in" }, 401);
1510
- return c.json({ id: user.id, username: user.username, role: user.role });
1511
- }).route("/", admin).route("/", authenticated);
1512
- var auth_default = app;
1696
+ import { streamSSE } from "hono/streaming";
1513
1697
 
1514
- // src/web/api/channels.ts
1515
- import { Hono as Hono2 } from "hono";
1516
- function buildEnv(name) {
1517
- return { ...loadMergedEnv(name), VOLUTE_MIND: name, VOLUTE_MIND_DIR: mindDir(name) };
1698
+ // src/lib/conversations.ts
1699
+ import { randomUUID } from "crypto";
1700
+ import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
1701
+ async function createConversation(mindName, channel, opts) {
1702
+ const db = await getDb();
1703
+ const id = randomUUID();
1704
+ const type = opts?.type ?? "dm";
1705
+ const name = opts?.name ?? null;
1706
+ await db.transaction(async (tx) => {
1707
+ await tx.insert(conversations).values({
1708
+ id,
1709
+ mind_name: mindName,
1710
+ channel,
1711
+ type,
1712
+ name,
1713
+ user_id: opts?.userId ?? null,
1714
+ title: opts?.title ?? null
1715
+ });
1716
+ if (opts?.participantIds && opts.participantIds.length > 0) {
1717
+ await tx.insert(conversationParticipants).values(
1718
+ opts.participantIds.map((uid, i) => ({
1719
+ conversation_id: id,
1720
+ user_id: uid,
1721
+ role: i === 0 ? "owner" : "member"
1722
+ }))
1723
+ );
1724
+ }
1725
+ });
1726
+ return {
1727
+ id,
1728
+ mind_name: mindName,
1729
+ channel,
1730
+ type,
1731
+ name,
1732
+ user_id: opts?.userId ?? null,
1733
+ title: opts?.title ?? null,
1734
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1735
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1736
+ };
1518
1737
  }
1519
- var app2 = new Hono2().post("/:name/channels/send", requireAdmin, async (c) => {
1520
- const name = c.req.param("name");
1521
- if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
1522
- const { platform, uri, message, images } = await c.req.json();
1523
- const driver = getChannelDriver(platform);
1524
- if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
1525
- const env = buildEnv(name);
1526
- try {
1527
- await driver.send(env, uri, message, images);
1528
- return c.json({ ok: true });
1529
- } catch (err) {
1530
- return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
1531
- }
1532
- }).get("/:name/channels/read", async (c) => {
1533
- const name = c.req.param("name");
1534
- if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
1535
- const platform = c.req.query("platform");
1536
- const uri = c.req.query("uri");
1537
- const limit = parseInt(c.req.query("limit") ?? "20", 10) || 20;
1538
- if (!platform || !uri) return c.json({ error: "platform and uri required" }, 400);
1539
- const driver = getChannelDriver(platform);
1540
- if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
1541
- const env = buildEnv(name);
1542
- try {
1543
- const output = await driver.read(env, uri, limit);
1544
- return c.text(output);
1545
- } catch (err) {
1546
- return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
1547
- }
1548
- }).get("/:name/channels/list", async (c) => {
1549
- const name = c.req.param("name");
1550
- if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
1738
+ async function getConversation(id) {
1739
+ const db = await getDb();
1740
+ const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
1741
+ return row ?? null;
1742
+ }
1743
+ async function addParticipant(conversationId, userId, role = "member") {
1744
+ const db = await getDb();
1745
+ await db.insert(conversationParticipants).values({
1746
+ conversation_id: conversationId,
1747
+ user_id: userId,
1748
+ role
1749
+ });
1750
+ }
1751
+ async function removeParticipant(conversationId, userId) {
1752
+ const db = await getDb();
1753
+ await db.delete(conversationParticipants).where(
1754
+ and2(
1755
+ eq3(conversationParticipants.conversation_id, conversationId),
1756
+ eq3(conversationParticipants.user_id, userId)
1757
+ )
1758
+ );
1759
+ }
1760
+ async function getParticipants(conversationId) {
1761
+ const db = await getDb();
1762
+ const rows = await db.select({
1763
+ userId: conversationParticipants.user_id,
1764
+ username: users.username,
1765
+ userType: users.user_type,
1766
+ role: conversationParticipants.role
1767
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
1768
+ return rows;
1769
+ }
1770
+ async function isParticipant(conversationId, userId) {
1771
+ const db = await getDb();
1772
+ const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
1773
+ and2(
1774
+ eq3(conversationParticipants.conversation_id, conversationId),
1775
+ eq3(conversationParticipants.user_id, userId)
1776
+ )
1777
+ ).get();
1778
+ return row != null;
1779
+ }
1780
+ async function listConversationsForUser(userId) {
1781
+ const db = await getDb();
1782
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
1783
+ if (participantRows.length === 0) return [];
1784
+ const convIds = participantRows.map((r) => r.conversation_id);
1785
+ return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
1786
+ }
1787
+ async function isParticipantOrOwner(conversationId, userId) {
1788
+ if (await isParticipant(conversationId, userId)) return true;
1789
+ const db = await getDb();
1790
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
1791
+ return row != null;
1792
+ }
1793
+ async function deleteConversationForUser(id, userId) {
1794
+ if (!await isParticipantOrOwner(id, userId)) return false;
1795
+ await deleteConversation(id);
1796
+ return true;
1797
+ }
1798
+ async function addMessage(conversationId, role, senderName, content) {
1799
+ const db = await getDb();
1800
+ const serialized = JSON.stringify(content);
1801
+ 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 });
1802
+ await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
1803
+ if (role === "user") {
1804
+ const firstText = content.find((b) => b.type === "text");
1805
+ const title = firstText ? firstText.text.slice(0, 80) : "";
1806
+ if (title) {
1807
+ await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
1808
+ }
1809
+ }
1810
+ const msg = {
1811
+ id: result.id,
1812
+ conversation_id: conversationId,
1813
+ role,
1814
+ sender_name: senderName,
1815
+ content,
1816
+ created_at: result.created_at
1817
+ };
1818
+ publish(conversationId, {
1819
+ type: "message",
1820
+ id: msg.id,
1821
+ role: msg.role,
1822
+ senderName: msg.sender_name,
1823
+ content: msg.content,
1824
+ createdAt: msg.created_at
1825
+ });
1826
+ return msg;
1827
+ }
1828
+ async function getMessages(conversationId) {
1829
+ const db = await getDb();
1830
+ const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
1831
+ return rows.map((row) => {
1832
+ let content;
1833
+ try {
1834
+ const parsed = JSON.parse(row.content);
1835
+ content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
1836
+ } catch {
1837
+ content = [{ type: "text", text: row.content }];
1838
+ }
1839
+ return { ...row, content };
1840
+ });
1841
+ }
1842
+ async function listConversationsWithParticipants(userId) {
1843
+ const convs = await listConversationsForUser(userId);
1844
+ if (convs.length === 0) return [];
1845
+ const db = await getDb();
1846
+ const convIds = convs.map((c) => c.id);
1847
+ const rows = await db.select({
1848
+ conversationId: conversationParticipants.conversation_id,
1849
+ userId: users.id,
1850
+ username: users.username,
1851
+ userType: users.user_type,
1852
+ role: conversationParticipants.role
1853
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
1854
+ const byConv = /* @__PURE__ */ new Map();
1855
+ for (const r of rows) {
1856
+ let arr = byConv.get(r.conversationId);
1857
+ if (!arr) {
1858
+ arr = [];
1859
+ byConv.set(r.conversationId, arr);
1860
+ }
1861
+ arr.push({
1862
+ userId: r.userId,
1863
+ username: r.username,
1864
+ userType: r.userType,
1865
+ role: r.role
1866
+ });
1867
+ }
1868
+ const lastMsgIds = await db.select({
1869
+ conversationId: messages.conversation_id,
1870
+ maxId: sql`MAX(${messages.id})`
1871
+ }).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
1872
+ const byLastMsg = /* @__PURE__ */ new Map();
1873
+ if (lastMsgIds.length > 0) {
1874
+ const msgRows = await db.select().from(messages).where(
1875
+ inArray(
1876
+ messages.id,
1877
+ lastMsgIds.map((r) => r.maxId)
1878
+ )
1879
+ );
1880
+ for (const m of msgRows) {
1881
+ let text = "";
1882
+ try {
1883
+ const parsed = JSON.parse(m.content);
1884
+ const blocks = Array.isArray(parsed) ? parsed : [];
1885
+ const textBlock = blocks.find((b) => b.type === "text");
1886
+ if (textBlock && "text" in textBlock) text = textBlock.text;
1887
+ } catch {
1888
+ text = m.content;
1889
+ }
1890
+ byLastMsg.set(m.conversation_id, {
1891
+ role: m.role,
1892
+ senderName: m.sender_name,
1893
+ text,
1894
+ createdAt: m.created_at
1895
+ });
1896
+ }
1897
+ }
1898
+ return convs.map((c) => ({
1899
+ ...c,
1900
+ participants: byConv.get(c.id) ?? [],
1901
+ lastMessage: byLastMsg.get(c.id)
1902
+ }));
1903
+ }
1904
+ async function findDMConversation(mindName, participantIds) {
1905
+ const db = await getDb();
1906
+ const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq3(conversations.mind_name, mindName), eq3(conversations.type, "dm"))).all();
1907
+ for (const conv of mindConvs) {
1908
+ const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
1909
+ if (rows.length !== 2) continue;
1910
+ const ids = new Set(rows.map((r) => r.user_id));
1911
+ if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
1912
+ return conv.id;
1913
+ }
1914
+ }
1915
+ return null;
1916
+ }
1917
+ async function deleteConversation(id) {
1918
+ const db = await getDb();
1919
+ await db.delete(conversations).where(eq3(conversations.id, id));
1920
+ }
1921
+ async function createChannel(name, creatorId) {
1922
+ const participantIds = creatorId ? [creatorId] : [];
1923
+ return createConversation(null, "volute", {
1924
+ type: "channel",
1925
+ name,
1926
+ title: name,
1927
+ participantIds
1928
+ });
1929
+ }
1930
+ async function getChannelByName(name) {
1931
+ const db = await getDb();
1932
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.name, name), eq3(conversations.type, "channel"))).get();
1933
+ return row ?? null;
1934
+ }
1935
+ async function listChannels() {
1936
+ const db = await getDb();
1937
+ return await db.select().from(conversations).where(eq3(conversations.type, "channel")).orderBy(conversations.name).all();
1938
+ }
1939
+ async function joinChannel(conversationId, userId) {
1940
+ if (await isParticipant(conversationId, userId)) return;
1941
+ await addParticipant(conversationId, userId);
1942
+ }
1943
+ async function leaveChannel(conversationId, userId) {
1944
+ await removeParticipant(conversationId, userId);
1945
+ }
1946
+
1947
+ // src/web/api/activity.ts
1948
+ var app = new Hono().get("/events", async (c) => {
1949
+ const user = c.get("user");
1950
+ return streamSSE(c, async (stream) => {
1951
+ const cleanups = [];
1952
+ try {
1953
+ let recentActivity = [];
1954
+ try {
1955
+ const db = await getDb();
1956
+ recentActivity = await db.select().from(activity).orderBy(desc2(activity.created_at)).limit(50);
1957
+ recentActivity = recentActivity.map((row) => ({
1958
+ ...row,
1959
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
1960
+ }));
1961
+ } catch (err) {
1962
+ logger_default.error("[activity-sse] failed to fetch recent activity", logger_default.errorData(err));
1963
+ }
1964
+ let conversations2 = [];
1965
+ try {
1966
+ conversations2 = await listConversationsWithParticipants(user.id);
1967
+ } catch (err) {
1968
+ logger_default.error("[activity-sse] failed to fetch conversations", logger_default.errorData(err));
1969
+ }
1970
+ const sites = getCachedSites();
1971
+ const recentPages = getCachedRecentPages();
1972
+ await stream.writeSSE({
1973
+ data: JSON.stringify({
1974
+ event: "snapshot",
1975
+ activity: recentActivity,
1976
+ conversations: conversations2,
1977
+ sites,
1978
+ recentPages,
1979
+ activeMinds: getActiveMinds()
1980
+ })
1981
+ });
1982
+ const unsubActivity = subscribe2((event) => {
1983
+ stream.writeSSE({
1984
+ data: JSON.stringify({ event: "activity", ...event })
1985
+ }).catch((err) => {
1986
+ if (!stream.aborted) logger_default.error("[activity-sse] write error:", logger_default.errorData(err));
1987
+ });
1988
+ });
1989
+ cleanups.push(unsubActivity);
1990
+ for (const conv of conversations2) {
1991
+ const unsubConv = subscribe(conv.id, (event) => {
1992
+ stream.writeSSE({
1993
+ data: JSON.stringify({ event: "conversation", conversationId: conv.id, ...event })
1994
+ }).catch((err) => {
1995
+ if (!stream.aborted) logger_default.error("[activity-sse] write error:", logger_default.errorData(err));
1996
+ });
1997
+ });
1998
+ cleanups.push(unsubConv);
1999
+ }
2000
+ const keepAlive = setInterval(() => {
2001
+ stream.writeSSE({ data: "" }).catch((err) => {
2002
+ if (!stream.aborted) logger_default.error("[activity-sse] ping error:", logger_default.errorData(err));
2003
+ });
2004
+ }, 15e3);
2005
+ cleanups.push(() => clearInterval(keepAlive));
2006
+ await new Promise((resolve23) => {
2007
+ stream.onAbort(() => resolve23());
2008
+ });
2009
+ } finally {
2010
+ for (const cleanup of cleanups) {
2011
+ try {
2012
+ cleanup();
2013
+ } catch {
2014
+ }
2015
+ }
2016
+ }
2017
+ });
2018
+ });
2019
+ var activity_default = app;
2020
+
2021
+ // src/web/api/auth.ts
2022
+ import { zValidator } from "@hono/zod-validator";
2023
+ import { Hono as Hono2 } from "hono";
2024
+ import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
2025
+ import { z } from "zod";
2026
+ var credentialsSchema = z.object({
2027
+ username: z.string().min(1),
2028
+ password: z.string().min(1)
2029
+ });
2030
+ var changePasswordSchema = z.object({
2031
+ currentPassword: z.string().min(1),
2032
+ newPassword: z.string().min(1)
2033
+ });
2034
+ var authenticated = new Hono2().use(authMiddleware).post("/change-password", zValidator("json", changePasswordSchema), async (c) => {
2035
+ const user = c.get("user");
2036
+ const { currentPassword, newPassword } = c.req.valid("json");
2037
+ const ok = await changePassword(user.id, currentPassword, newPassword);
2038
+ if (!ok) return c.json({ error: "Current password is incorrect" }, 400);
2039
+ return c.json({ ok: true });
2040
+ });
2041
+ var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
2042
+ const user = c.get("user");
2043
+ if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
2044
+ const minds = readRegistry();
2045
+ for (const mind of minds) {
2046
+ await getOrCreateMindUser(mind.name);
2047
+ }
2048
+ const type = c.req.query("type");
2049
+ if (type === "brain" || type === "mind") {
2050
+ return c.json(await listUsersByType(type));
2051
+ }
2052
+ return c.json(await listUsers());
2053
+ }).get("/users/pending", async (c) => {
2054
+ const user = c.get("user");
2055
+ if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
2056
+ return c.json(await listPendingUsers());
2057
+ }).post("/users/:id/approve", async (c) => {
2058
+ const user = c.get("user");
2059
+ if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
2060
+ const id = parseInt(c.req.param("id"), 10);
2061
+ await approveUser(id);
2062
+ return c.json({ ok: true });
2063
+ });
2064
+ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema), async (c) => {
2065
+ const { username, password } = c.req.valid("json");
2066
+ const existing = await getUserByUsername(username);
2067
+ if (existing) {
2068
+ return c.json({ error: "Username already taken" }, 409);
2069
+ }
2070
+ const user = await createUser(username, password);
2071
+ if (user.role === "admin") {
2072
+ const sessionId = await createSession(user.id);
2073
+ setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
2074
+ }
2075
+ return c.json({ id: user.id, username: user.username, role: user.role });
2076
+ }).post("/login", zValidator("json", credentialsSchema), async (c) => {
2077
+ const { username, password } = c.req.valid("json");
2078
+ const user = await verifyUser(username, password);
2079
+ if (!user) {
2080
+ return c.json({ error: "Invalid credentials" }, 401);
2081
+ }
2082
+ const sessionId = await createSession(user.id);
2083
+ setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
2084
+ return c.json({ id: user.id, username: user.username, role: user.role });
2085
+ }).post("/logout", async (c) => {
2086
+ const sessionId = getCookie2(c, "volute_session");
2087
+ if (sessionId) {
2088
+ await deleteSession(sessionId);
2089
+ deleteCookie(c, "volute_session", { path: "/" });
2090
+ }
2091
+ return c.json({ ok: true });
2092
+ }).get("/me", async (c) => {
2093
+ const sessionId = getCookie2(c, "volute_session");
2094
+ if (!sessionId) return c.json({ error: "Not logged in" }, 401);
2095
+ const userId = await getSessionUserId(sessionId);
2096
+ if (userId == null) return c.json({ error: "Not logged in" }, 401);
2097
+ const user = await getUser(userId);
2098
+ if (!user) return c.json({ error: "Not logged in" }, 401);
2099
+ return c.json({ id: user.id, username: user.username, role: user.role });
2100
+ }).route("/", admin).route("/", authenticated);
2101
+ var auth_default = app2;
2102
+
2103
+ // src/web/api/channels.ts
2104
+ import { Hono as Hono3 } from "hono";
2105
+ function buildEnv(name) {
2106
+ return { ...loadMergedEnv(name), VOLUTE_MIND: name, VOLUTE_MIND_DIR: mindDir(name) };
2107
+ }
2108
+ var app3 = new Hono3().post("/:name/channels/send", requireAdmin, async (c) => {
2109
+ const name = c.req.param("name");
2110
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
2111
+ const { platform, uri, message, images } = await c.req.json();
2112
+ const driver = getChannelDriver(platform);
2113
+ if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
2114
+ const env = buildEnv(name);
2115
+ try {
2116
+ await driver.send(env, uri, message, images);
2117
+ return c.json({ ok: true });
2118
+ } catch (err) {
2119
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
2120
+ }
2121
+ }).get("/:name/channels/read", async (c) => {
2122
+ const name = c.req.param("name");
2123
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
2124
+ const platform = c.req.query("platform");
2125
+ const uri = c.req.query("uri");
2126
+ const limit = parseInt(c.req.query("limit") ?? "20", 10) || 20;
2127
+ if (!platform || !uri) return c.json({ error: "platform and uri required" }, 400);
2128
+ const driver = getChannelDriver(platform);
2129
+ if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
2130
+ const env = buildEnv(name);
2131
+ try {
2132
+ const output = await driver.read(env, uri, limit);
2133
+ return c.text(output);
2134
+ } catch (err) {
2135
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
2136
+ }
2137
+ }).get("/:name/channels/list", async (c) => {
2138
+ const name = c.req.param("name");
2139
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
1551
2140
  const platform = c.req.query("platform");
1552
2141
  const platforms = platform ? [platform] : Object.keys(CHANNELS);
1553
2142
  const env = buildEnv(name);
@@ -1606,12 +2195,12 @@ var app2 = new Hono2().post("/:name/channels/send", requireAdmin, async (c) => {
1606
2195
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
1607
2196
  }
1608
2197
  });
1609
- var channels_default = app2;
2198
+ var channels_default = app3;
1610
2199
 
1611
2200
  // src/web/api/connectors.ts
1612
- import { Hono as Hono3 } from "hono";
2201
+ import { Hono as Hono4 } from "hono";
1613
2202
  var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
1614
- var app3 = new Hono3().get("/:name/connectors", (c) => {
2203
+ var app4 = new Hono4().get("/:name/connectors", (c) => {
1615
2204
  const name = c.req.param("name");
1616
2205
  const entry = findMind(name);
1617
2206
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -1685,11 +2274,11 @@ var app3 = new Hono3().get("/:name/connectors", (c) => {
1685
2274
  writeVoluteConfig(dir, config);
1686
2275
  return c.json({ ok: true });
1687
2276
  });
1688
- var connectors_default = app3;
2277
+ var connectors_default = app4;
1689
2278
 
1690
2279
  // src/web/api/env.ts
1691
- import { Hono as Hono4 } from "hono";
1692
- var app4 = new Hono4().get("/:name/env", (c) => {
2280
+ import { Hono as Hono5 } from "hono";
2281
+ var app5 = new Hono5().get("/:name/env", (c) => {
1693
2282
  const name = c.req.param("name");
1694
2283
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
1695
2284
  const shared = readEnv(sharedEnvPath());
@@ -1732,7 +2321,7 @@ var app4 = new Hono4().get("/:name/env", (c) => {
1732
2321
  writeEnv(path, env);
1733
2322
  return c.json({ ok: true });
1734
2323
  });
1735
- var sharedEnvApp = new Hono4().get("/", (c) => {
2324
+ var sharedEnvApp = new Hono5().get("/", (c) => {
1736
2325
  return c.json(readEnv(sharedEnvPath()));
1737
2326
  }).put("/:key", requireAdmin, async (c) => {
1738
2327
  const key = c.req.param("key");
@@ -1759,644 +2348,670 @@ var sharedEnvApp = new Hono4().get("/", (c) => {
1759
2348
  writeEnv(path, env);
1760
2349
  return c.json({ ok: true });
1761
2350
  });
1762
- var env_default = app4;
1763
-
1764
- // src/web/api/files.ts
1765
- import { existsSync as existsSync6 } from "fs";
1766
- import { readdir, readFile } from "fs/promises";
1767
- import { resolve as resolve7 } from "path";
1768
- import { Hono as Hono5 } from "hono";
1769
- var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1770
- var app5 = new Hono5().get("/:name/files", async (c) => {
1771
- const name = c.req.param("name");
1772
- const entry = findMind(name);
1773
- if (!entry) return c.json({ error: "Mind not found" }, 404);
1774
- const dir = mindDir(name);
1775
- const homeDir = resolve7(dir, "home");
1776
- if (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1777
- const allFiles = await readdir(homeDir);
1778
- const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1779
- return c.json(files);
1780
- }).get("/:name/files/:filename", async (c) => {
1781
- const name = c.req.param("name");
1782
- const filename = c.req.param("filename");
1783
- if (!ALLOWED_FILES.has(filename)) {
1784
- return c.json({ error: "File not allowed" }, 403);
1785
- }
1786
- const entry = findMind(name);
1787
- if (!entry) return c.json({ error: "Mind not found" }, 404);
1788
- const dir = mindDir(name);
1789
- const filePath = resolve7(dir, "home", filename);
1790
- if (!existsSync6(filePath)) {
1791
- return c.json({ error: "File not found" }, 404);
1792
- }
1793
- const content = await readFile(filePath, "utf-8");
1794
- return c.json({ filename, content });
1795
- });
1796
- var files_default = app5;
1797
-
1798
- // src/web/api/keys.ts
1799
- import { Hono as Hono6 } from "hono";
1800
-
1801
- // src/lib/identity.ts
1802
- import { createHash, generateKeyPairSync, sign, verify } from "crypto";
1803
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1804
- import { resolve as resolve8 } from "path";
1805
- function generateIdentity(mindDir2) {
1806
- const identityDir = resolve8(mindDir2, ".mind/identity");
1807
- mkdirSync4(identityDir, { recursive: true });
1808
- const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
1809
- publicKeyEncoding: { type: "spki", format: "pem" },
1810
- privateKeyEncoding: { type: "pkcs8", format: "pem" }
1811
- });
1812
- const privatePath = resolve8(identityDir, "private.pem");
1813
- const publicPath = resolve8(identityDir, "public.pem");
1814
- writeFileSync4(privatePath, privateKey, { mode: 384 });
1815
- writeFileSync4(publicPath, publicKey, { mode: 420 });
1816
- const config = readVoluteConfig(mindDir2) ?? {};
1817
- config.identity = {
1818
- privateKey: ".mind/identity/private.pem",
1819
- publicKey: ".mind/identity/public.pem"
1820
- };
1821
- writeVoluteConfig(mindDir2, config);
1822
- return { publicKeyPem: publicKey, privateKeyPem: privateKey };
1823
- }
1824
- function getPrivateKey(mindDir2) {
1825
- const config = readVoluteConfig(mindDir2);
1826
- const relPath = config?.identity?.privateKey;
1827
- if (!relPath) return null;
1828
- const fullPath = resolve8(mindDir2, relPath);
1829
- if (!existsSync7(fullPath)) return null;
1830
- return readFileSync5(fullPath, "utf-8");
1831
- }
1832
- function getPublicKey(mindDir2) {
1833
- const config = readVoluteConfig(mindDir2);
1834
- const relPath = config?.identity?.publicKey;
1835
- if (!relPath) return null;
1836
- const fullPath = resolve8(mindDir2, relPath);
1837
- if (!existsSync7(fullPath)) return null;
1838
- return readFileSync5(fullPath, "utf-8");
1839
- }
1840
- function getFingerprint(publicKeyPem) {
1841
- return createHash("sha256").update(publicKeyPem).digest("hex");
1842
- }
1843
- function signMessage(privateKeyPem, content, timestamp) {
1844
- const data = `${content}
1845
- ${timestamp}`;
1846
- const signature = sign(null, Buffer.from(data), privateKeyPem);
1847
- return signature.toString("base64");
1848
- }
1849
- async function publishPublicKey(mindName, publicKeyPem) {
1850
- const systems = readSystemsConfig();
1851
- if (!systems) return false;
1852
- try {
1853
- const res = await fetch(`${systems.apiUrl}/api/keys/${encodeURIComponent(mindName)}`, {
1854
- method: "PUT",
1855
- headers: {
1856
- "Content-Type": "application/json",
1857
- Authorization: `Bearer ${systems.apiKey}`
1858
- },
1859
- body: JSON.stringify({ publicKey: publicKeyPem })
1860
- });
1861
- if (!res.ok) {
1862
- logger_default.warn(`failed to publish key for ${mindName}: ${res.status}`);
1863
- return false;
1864
- }
1865
- return true;
1866
- } catch (err) {
1867
- logger_default.warn(`failed to publish key for ${mindName}`, logger_default.errorData(err));
1868
- return false;
1869
- }
1870
- }
1871
-
1872
- // src/web/api/keys.ts
1873
- var app6 = new Hono6().get("/:fingerprint", (c) => {
1874
- const fingerprint = c.req.param("fingerprint");
1875
- for (const entry of readRegistry()) {
1876
- try {
1877
- const pubKey = getPublicKey(mindDir(entry.name));
1878
- if (!pubKey) continue;
1879
- if (getFingerprint(pubKey) === fingerprint) {
1880
- return c.json({ publicKey: pubKey, mind: entry.name });
1881
- }
1882
- } catch {
1883
- }
1884
- }
1885
- return c.json({ error: "Key not found" }, 404);
1886
- });
1887
- var keys_default = app6;
2351
+ var env_default = app5;
1888
2352
 
1889
- // src/web/api/logs.ts
1890
- import { spawn as spawn2 } from "child_process";
1891
- import { existsSync as existsSync8 } from "fs";
2353
+ // src/web/api/file-sharing.ts
2354
+ import { readFileSync as readFileSync6, statSync as statSync2 } from "fs";
1892
2355
  import { resolve as resolve9 } from "path";
1893
- import { Hono as Hono7 } from "hono";
1894
- import { streamSSE } from "hono/streaming";
1895
- var app7 = new Hono7().get("/:name/logs", async (c) => {
1896
- const name = c.req.param("name");
1897
- const entry = findMind(name);
1898
- if (!entry) return c.json({ error: "Mind not found" }, 404);
1899
- const logFile = resolve9(stateDir(name), "logs", "mind.log");
1900
- if (!existsSync8(logFile)) {
1901
- return c.json({ error: "No log file found" }, 404);
1902
- }
1903
- return streamSSE(c, async (stream) => {
1904
- const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
1905
- const onData = (data) => {
1906
- const lines = data.toString().split("\n");
1907
- for (const line of lines) {
1908
- if (line) {
1909
- stream.writeSSE({ data: line }).catch(() => {
1910
- });
1911
- }
1912
- }
1913
- };
1914
- tail.stdout.on("data", onData);
1915
- stream.onAbort(() => {
1916
- tail.kill();
1917
- });
1918
- await new Promise((resolve20) => {
1919
- tail.on("exit", resolve20);
1920
- stream.onAbort(resolve20);
1921
- });
1922
- });
1923
- }).get("/:name/logs/tail", async (c) => {
1924
- const name = c.req.param("name");
1925
- const entry = findMind(name);
1926
- if (!entry) return c.json({ error: "Mind not found" }, 404);
1927
- const logFile = resolve9(stateDir(name), "logs", "mind.log");
1928
- if (!existsSync8(logFile)) {
1929
- return c.json({ error: "No log file found" }, 404);
1930
- }
1931
- const nParam = parseInt(c.req.query("n") ?? "50", 10);
1932
- const n = Number.isFinite(nParam) && nParam > 0 ? Math.min(nParam, 1e4) : 50;
1933
- const tail = spawn2("tail", ["-n", String(n), logFile]);
1934
- let output = "";
1935
- tail.stdout.on("data", (data) => {
1936
- output += data.toString();
1937
- });
1938
- await new Promise((resolve20) => {
1939
- tail.on("exit", resolve20);
1940
- });
1941
- return c.text(output);
1942
- });
1943
- var logs_default = app7;
1944
-
1945
- // src/web/api/mind-skills.ts
1946
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1947
- import { Hono as Hono8 } from "hono";
1948
- import { z as z2 } from "zod";
1949
- var app8 = new Hono8().get("/:name/skills", async (c) => {
1950
- const name = c.req.param("name");
1951
- const entry = findMind(name);
1952
- if (!entry) return c.json({ error: "Mind not found" }, 404);
1953
- const dir = mindDir(name);
1954
- const skills = await listMindSkills(dir);
1955
- return c.json(skills);
1956
- }).post(
1957
- "/:name/skills/install",
1958
- requireAdmin,
1959
- zValidator2("json", z2.object({ skillId: z2.string() })),
1960
- async (c) => {
1961
- const name = c.req.param("name");
1962
- const entry = findMind(name);
1963
- if (!entry) return c.json({ error: "Mind not found" }, 404);
1964
- const { skillId } = c.req.valid("json");
1965
- const dir = mindDir(name);
1966
- try {
1967
- await installSkill(name, dir, skillId);
1968
- } catch (e) {
1969
- const msg = e instanceof Error ? e.message : String(e);
1970
- return c.json({ error: msg }, 400);
1971
- }
1972
- return c.json({ ok: true });
1973
- }
1974
- ).post(
1975
- "/:name/skills/update",
1976
- requireAdmin,
1977
- zValidator2("json", z2.object({ skillId: z2.string() })),
1978
- async (c) => {
1979
- const name = c.req.param("name");
1980
- const entry = findMind(name);
1981
- if (!entry) return c.json({ error: "Mind not found" }, 404);
1982
- const { skillId } = c.req.valid("json");
1983
- const dir = mindDir(name);
1984
- try {
1985
- const result = await updateSkill(name, dir, skillId);
1986
- return c.json(result);
1987
- } catch (e) {
1988
- const msg = e instanceof Error ? e.message : String(e);
1989
- return c.json({ error: msg }, 400);
1990
- }
1991
- }
1992
- ).post(
1993
- "/:name/skills/publish",
1994
- requireAdmin,
1995
- zValidator2("json", z2.object({ skillId: z2.string() })),
1996
- async (c) => {
1997
- const name = c.req.param("name");
1998
- const entry = findMind(name);
1999
- if (!entry) return c.json({ error: "Mind not found" }, 404);
2000
- const { skillId } = c.req.valid("json");
2001
- const dir = mindDir(name);
2002
- try {
2003
- const skill = await publishSkill(name, dir, skillId);
2004
- return c.json(skill);
2005
- } catch (e) {
2006
- const msg = e instanceof Error ? e.message : String(e);
2007
- return c.json({ error: msg }, 400);
2008
- }
2009
- }
2010
- ).delete("/:name/skills/:skill", requireAdmin, async (c) => {
2011
- const name = c.req.param("name");
2012
- const skillName = c.req.param("skill");
2013
- const entry = findMind(name);
2014
- if (!entry) return c.json({ error: "Mind not found" }, 404);
2015
- const dir = mindDir(name);
2016
- try {
2017
- await uninstallSkill(name, dir, skillName);
2018
- } catch (e) {
2019
- const msg = e instanceof Error ? e.message : String(e);
2020
- return c.json({ error: msg }, 400);
2021
- }
2022
- return c.json({ ok: true });
2023
- });
2024
- var mind_skills_default = app8;
2025
-
2026
- // src/web/api/minds.ts
2027
- import {
2028
- cpSync as cpSync2,
2029
- existsSync as existsSync10,
2030
- mkdirSync as mkdirSync7,
2031
- readdirSync as readdirSync4,
2032
- readFileSync as readFileSync9,
2033
- rmSync as rmSync2,
2034
- statSync as statSync2,
2035
- writeFileSync as writeFileSync8
2036
- } from "fs";
2037
- import { join as join2, resolve as resolve13 } from "path";
2038
- import { zValidator as zValidator3 } from "@hono/zod-validator";
2039
- import { and as and3, desc as desc2, eq as eq4, sql as sql2 } from "drizzle-orm";
2040
- import { Hono as Hono9 } from "hono";
2041
- import { z as z3 } from "zod";
2042
-
2043
- // src/lib/consolidate.ts
2044
- import { readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2045
- import { resolve as resolve10 } from "path";
2046
- async function consolidateMemory(mindDir2) {
2047
- const soulPath = resolve10(mindDir2, "home/SOUL.md");
2048
- const memoryPath = resolve10(mindDir2, "home/MEMORY.md");
2049
- const memoryDir = resolve10(mindDir2, "home/memory");
2050
- const soul = readFileSync6(soulPath, "utf-8");
2051
- const logs = [];
2052
- try {
2053
- const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2054
- for (const filename of files) {
2055
- const date = filename.replace(".md", "");
2056
- const content2 = readFileSync6(resolve10(memoryDir, filename), "utf-8").trim();
2057
- if (content2) {
2058
- logs.push(`### ${date}
2356
+ import { Hono as Hono6 } from "hono";
2059
2357
 
2060
- ${content2}`);
2061
- }
2062
- }
2063
- } catch {
2064
- }
2065
- if (logs.length === 0) {
2066
- console.log("No daily logs found.");
2067
- return;
2358
+ // src/lib/file-sharing.ts
2359
+ import { randomBytes } from "crypto";
2360
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
2361
+ import { basename, join as join2, normalize, resolve as resolve8 } from "path";
2362
+ function validateFilePath(filePath) {
2363
+ if (!filePath) return "File path is required";
2364
+ const normalized = normalize(filePath);
2365
+ if (normalized.startsWith("/") || normalized.startsWith("\\")) {
2366
+ return "Absolute paths are not allowed";
2068
2367
  }
2069
- const apiKey = process.env.ANTHROPIC_API_KEY;
2070
- if (!apiKey) {
2071
- console.error("ANTHROPIC_API_KEY not set, skipping memory consolidation.");
2072
- return;
2368
+ if (normalized.includes("..")) {
2369
+ return "Path traversal (..) is not allowed";
2073
2370
  }
2074
- console.log("Consolidating memory from daily logs...");
2075
- const userMessage = [
2076
- "You have daily logs from a previous environment but no long-term memory file yet.",
2077
- "Please review the daily logs below and produce consolidated MEMORY.md content.",
2078
- "Keep it concise and organized by topic. Output ONLY the markdown content for MEMORY.md, nothing else.",
2079
- "",
2080
- "## Daily logs",
2081
- "",
2082
- logs.join("\n\n")
2083
- ].join("\n");
2084
- const res = await fetch("https://api.anthropic.com/v1/messages", {
2085
- method: "POST",
2086
- headers: {
2087
- "Content-Type": "application/json",
2088
- "x-api-key": apiKey,
2089
- "anthropic-version": "2023-06-01"
2090
- },
2091
- body: JSON.stringify({
2092
- model: "claude-sonnet-4-20250514",
2093
- max_tokens: 4096,
2094
- system: soul,
2095
- messages: [{ role: "user", content: userMessage }]
2096
- })
2097
- });
2098
- if (!res.ok) {
2099
- const body = await res.text();
2100
- console.error(`Anthropic API error (${res.status}): ${body}`);
2101
- return;
2371
+ return null;
2372
+ }
2373
+ function configPath(dir) {
2374
+ return resolve8(dir, "home", ".config", "file-sharing.json");
2375
+ }
2376
+ function readFileSharingConfig(dir) {
2377
+ const p = configPath(dir);
2378
+ if (!existsSync7(p)) return {};
2379
+ try {
2380
+ return JSON.parse(readFileSync5(p, "utf-8"));
2381
+ } catch (err) {
2382
+ console.warn(`[file-sharing] failed to parse config at ${p}:`, err);
2383
+ return {};
2102
2384
  }
2103
- const data = await res.json();
2104
- const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
2105
- if (content) {
2106
- writeFileSync5(memoryPath, `${content}
2385
+ }
2386
+ function writeFileSharingConfig(dir, config) {
2387
+ const p = configPath(dir);
2388
+ mkdirSync4(resolve8(p, ".."), { recursive: true });
2389
+ writeFileSync4(p, `${JSON.stringify(config, null, 2)}
2107
2390
  `);
2108
- console.log("MEMORY.md created successfully.");
2109
- } else {
2110
- console.warn("Warning: No content produced.");
2111
- }
2112
2391
  }
2113
-
2114
- // src/lib/conversations.ts
2115
- import { randomUUID } from "crypto";
2116
- import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
2117
-
2118
- // src/lib/conversation-events.ts
2119
- var subscribers = /* @__PURE__ */ new Map();
2120
- function subscribe(conversationId, callback) {
2121
- let set = subscribers.get(conversationId);
2122
- if (!set) {
2123
- set = /* @__PURE__ */ new Set();
2124
- subscribers.set(conversationId, set);
2392
+ function isTrustedSender(dir, sender) {
2393
+ const config = readFileSharingConfig(dir);
2394
+ return config.trustedSenders?.includes(sender) ?? false;
2395
+ }
2396
+ function addTrust(dir, sender) {
2397
+ const config = readFileSharingConfig(dir);
2398
+ const trusted = config.trustedSenders ?? [];
2399
+ if (!trusted.includes(sender)) {
2400
+ trusted.push(sender);
2401
+ }
2402
+ config.trustedSenders = trusted;
2403
+ writeFileSharingConfig(dir, config);
2404
+ }
2405
+ function removeTrust(dir, sender) {
2406
+ const config = readFileSharingConfig(dir);
2407
+ const trusted = config.trustedSenders ?? [];
2408
+ config.trustedSenders = trusted.filter((s) => s !== sender);
2409
+ writeFileSharingConfig(dir, config);
2410
+ }
2411
+ function pendingDir(receiver) {
2412
+ return resolve8(stateDir(receiver), "pending-files");
2413
+ }
2414
+ function validateId(id) {
2415
+ if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
2416
+ throw new Error("Invalid pending file id");
2125
2417
  }
2126
- set.add(callback);
2127
- return () => {
2128
- set.delete(callback);
2129
- if (set.size === 0) subscribers.delete(conversationId);
2418
+ }
2419
+ function generateId(sender) {
2420
+ const ts = Date.now();
2421
+ const rand = randomBytes(2).toString("hex");
2422
+ return `${sender}-${ts}-${rand}`;
2423
+ }
2424
+ function stageFile(receiver, sender, filename, content, originalPath) {
2425
+ const err = validateFilePath(filename);
2426
+ if (err) throw new Error(err);
2427
+ if (sender.includes("/") || sender.includes("\\")) {
2428
+ throw new Error("Invalid sender name");
2429
+ }
2430
+ const id = generateId(sender);
2431
+ const dir = resolve8(pendingDir(receiver), id);
2432
+ mkdirSync4(dir, { recursive: true });
2433
+ const metadata = {
2434
+ id,
2435
+ sender,
2436
+ filename: basename(filename),
2437
+ originalPath,
2438
+ size: content.length,
2439
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2130
2440
  };
2441
+ writeFileSync4(resolve8(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
2442
+ `);
2443
+ writeFileSync4(resolve8(dir, "data"), content);
2444
+ return { id };
2131
2445
  }
2132
- function publish(conversationId, event) {
2133
- const set = subscribers.get(conversationId);
2134
- if (!set) return;
2135
- for (const cb of set) {
2446
+ function listPending(receiver) {
2447
+ const dir = pendingDir(receiver);
2448
+ if (!existsSync7(dir)) return [];
2449
+ const entries = readdirSync3(dir, { withFileTypes: true });
2450
+ const result = [];
2451
+ for (const entry of entries) {
2452
+ if (!entry.isDirectory()) continue;
2453
+ const metaPath = resolve8(dir, entry.name, "metadata.json");
2454
+ if (!existsSync7(metaPath)) continue;
2136
2455
  try {
2137
- cb(event);
2456
+ result.push(JSON.parse(readFileSync5(metaPath, "utf-8")));
2138
2457
  } catch (err) {
2139
- console.error("[conversation-events] subscriber threw:", err);
2140
- set.delete(cb);
2141
- if (set.size === 0) subscribers.delete(conversationId);
2458
+ console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
2142
2459
  }
2143
2460
  }
2461
+ return result.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2462
+ }
2463
+ function getPending(receiver, id) {
2464
+ validateId(id);
2465
+ const metaPath = resolve8(pendingDir(receiver), id, "metadata.json");
2466
+ if (!existsSync7(metaPath)) return null;
2467
+ try {
2468
+ return JSON.parse(readFileSync5(metaPath, "utf-8"));
2469
+ } catch (err) {
2470
+ console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
2471
+ return null;
2472
+ }
2473
+ }
2474
+ function deliverFile(receiverDir, sender, filename, content, inboxPath) {
2475
+ const err = validateFilePath(filename);
2476
+ if (err) throw new Error(err);
2477
+ const inbox = inboxPath ?? "inbox";
2478
+ const inboxErr = validateFilePath(inbox);
2479
+ if (inboxErr) throw new Error(`Invalid inboxPath: ${inboxErr}`);
2480
+ if (sender.includes("/") || sender.includes("\\")) {
2481
+ throw new Error("Invalid sender name");
2482
+ }
2483
+ const destDir = resolve8(receiverDir, "home", inbox, sender);
2484
+ mkdirSync4(destDir, { recursive: true });
2485
+ const destPath = resolve8(destDir, basename(filename));
2486
+ writeFileSync4(destPath, content);
2487
+ return join2(inbox, sender, basename(filename));
2488
+ }
2489
+ function acceptPending(receiver, id, receiverDir) {
2490
+ const meta = getPending(receiver, id);
2491
+ if (!meta) throw new Error(`Pending file not found: ${id}`);
2492
+ const dataPath = resolve8(pendingDir(receiver), id, "data");
2493
+ const content = readFileSync5(dataPath);
2494
+ const config = readFileSharingConfig(receiverDir);
2495
+ const inboxPath = config.inboxPath ?? "inbox";
2496
+ const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
2497
+ rmSync(resolve8(pendingDir(receiver), id), { recursive: true });
2498
+ return { sender: meta.sender, filename: meta.filename, destPath };
2499
+ }
2500
+ function rejectPending(receiver, id) {
2501
+ const meta = getPending(receiver, id);
2502
+ if (!meta) throw new Error(`Pending file not found: ${id}`);
2503
+ rmSync(resolve8(pendingDir(receiver), id), { recursive: true });
2504
+ return { sender: meta.sender, filename: meta.filename };
2505
+ }
2506
+ function formatFileSize(bytes) {
2507
+ if (bytes < 1024) return `${bytes} B`;
2508
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2509
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2144
2510
  }
2145
2511
 
2146
- // src/lib/conversations.ts
2147
- async function createConversation(mindName, channel, opts) {
2148
- const db = await getDb();
2149
- const id = randomUUID();
2150
- const type = opts?.type ?? "dm";
2151
- const name = opts?.name ?? null;
2152
- await db.transaction(async (tx) => {
2153
- await tx.insert(conversations).values({
2154
- id,
2155
- mind_name: mindName,
2156
- channel,
2157
- type,
2158
- name,
2159
- user_id: opts?.userId ?? null,
2160
- title: opts?.title ?? null
2512
+ // src/web/api/file-sharing.ts
2513
+ async function notifyMind(port, message, channel, sender) {
2514
+ try {
2515
+ const res = await fetch(`http://127.0.0.1:${port}/message`, {
2516
+ method: "POST",
2517
+ headers: { "Content-Type": "application/json" },
2518
+ body: JSON.stringify({
2519
+ content: [{ type: "text", text: message }],
2520
+ channel,
2521
+ sender
2522
+ })
2161
2523
  });
2162
- if (opts?.participantIds && opts.participantIds.length > 0) {
2163
- await tx.insert(conversationParticipants).values(
2164
- opts.participantIds.map((uid, i) => ({
2165
- conversation_id: id,
2166
- user_id: uid,
2167
- role: i === 0 ? "owner" : "member"
2168
- }))
2524
+ if (!res.ok) {
2525
+ console.warn(`[file-sharing] notify mind on port ${port} failed: ${res.status}`);
2526
+ }
2527
+ } catch (err) {
2528
+ console.warn(`[file-sharing] notify mind on port ${port} failed:`, err);
2529
+ }
2530
+ }
2531
+ var app6 = new Hono6().post("/:name/files/send", async (c) => {
2532
+ const senderName = c.req.param("name");
2533
+ const senderEntry = findMind(senderName);
2534
+ if (!senderEntry) return c.json({ error: "Sender mind not found" }, 404);
2535
+ const body = await c.req.json();
2536
+ if (!body.targetMind || !body.filePath) {
2537
+ return c.json({ error: "targetMind and filePath are required" }, 400);
2538
+ }
2539
+ const receiverEntry = findMind(body.targetMind);
2540
+ if (!receiverEntry) return c.json({ error: "Target mind not found" }, 404);
2541
+ const pathErr = validateFilePath(body.filePath);
2542
+ if (pathErr) return c.json({ error: pathErr }, 400);
2543
+ const senderDir = mindDir(senderName);
2544
+ const filePath = resolve9(senderDir, "home", body.filePath);
2545
+ const MAX_FILE_SIZE = 50 * 1024 * 1024;
2546
+ const stat3 = statSync2(filePath, { throwIfNoEntry: false });
2547
+ if (!stat3) return c.json({ error: `File not found: ${body.filePath}` }, 404);
2548
+ if (stat3.size > MAX_FILE_SIZE) {
2549
+ return c.json(
2550
+ {
2551
+ error: `File too large (${formatFileSize(stat3.size)}, max ${formatFileSize(MAX_FILE_SIZE)})`
2552
+ },
2553
+ 413
2554
+ );
2555
+ }
2556
+ let content;
2557
+ try {
2558
+ content = readFileSync6(filePath);
2559
+ } catch {
2560
+ return c.json({ error: `File not found: ${body.filePath}` }, 404);
2561
+ }
2562
+ const receiverDir = mindDir(body.targetMind);
2563
+ const filename = body.filePath;
2564
+ const sizeStr = formatFileSize(content.length);
2565
+ if (isTrustedSender(receiverDir, senderName)) {
2566
+ const config = readFileSharingConfig(receiverDir);
2567
+ const destPath = deliverFile(receiverDir, senderName, filename, content, config.inboxPath);
2568
+ if (receiverEntry.running) {
2569
+ await notifyMind(
2570
+ receiverEntry.port,
2571
+ `[file] ${senderName} sent ${filename} (${sizeStr}) \u2192 ${destPath}`,
2572
+ "system:file-sharing",
2573
+ senderName
2169
2574
  );
2170
2575
  }
2576
+ return c.json({ status: "delivered", destPath }, 200);
2577
+ }
2578
+ const { id } = stageFile(body.targetMind, senderName, filename, content, body.filePath);
2579
+ if (receiverEntry.running) {
2580
+ await notifyMind(
2581
+ receiverEntry.port,
2582
+ `[file] ${senderName} wants to send ${filename} (${sizeStr}) \u2014 run: volute file accept ${id}`,
2583
+ "system:file-sharing",
2584
+ senderName
2585
+ );
2586
+ }
2587
+ return c.json({ status: "pending", id }, 200);
2588
+ }).get("/:name/files/pending", (c) => {
2589
+ const name = c.req.param("name");
2590
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
2591
+ return c.json(listPending(name));
2592
+ }).post("/:name/files/accept", async (c) => {
2593
+ const name = c.req.param("name");
2594
+ const entry = findMind(name);
2595
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2596
+ const body = await c.req.json();
2597
+ if (!body.id) return c.json({ error: "id is required" }, 400);
2598
+ let result;
2599
+ try {
2600
+ result = acceptPending(name, body.id, mindDir(name));
2601
+ } catch (err) {
2602
+ const message = err.message;
2603
+ if (message.includes("not found") || message.includes("Invalid pending")) {
2604
+ return c.json({ error: message }, 404);
2605
+ }
2606
+ return c.json({ error: `Failed to accept file: ${message}` }, 500);
2607
+ }
2608
+ const senderEntry = findMind(result.sender);
2609
+ if (senderEntry?.running) {
2610
+ await notifyMind(
2611
+ senderEntry.port,
2612
+ `[file] ${name} accepted ${result.filename}`,
2613
+ "system:file-sharing",
2614
+ name
2615
+ );
2616
+ }
2617
+ return c.json({ ok: true, destPath: result.destPath });
2618
+ }).post("/:name/files/reject", async (c) => {
2619
+ const name = c.req.param("name");
2620
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
2621
+ const body = await c.req.json();
2622
+ if (!body.id) return c.json({ error: "id is required" }, 400);
2623
+ let result;
2624
+ try {
2625
+ result = rejectPending(name, body.id);
2626
+ } catch (err) {
2627
+ const message = err.message;
2628
+ if (message.includes("not found") || message.includes("Invalid pending")) {
2629
+ return c.json({ error: message }, 404);
2630
+ }
2631
+ return c.json({ error: `Failed to reject file: ${message}` }, 500);
2632
+ }
2633
+ const senderEntry = findMind(result.sender);
2634
+ if (senderEntry?.running) {
2635
+ await notifyMind(
2636
+ senderEntry.port,
2637
+ `[file] ${name} rejected ${result.filename}`,
2638
+ "system:file-sharing",
2639
+ name
2640
+ );
2641
+ }
2642
+ return c.json({ ok: true });
2643
+ }).post("/:name/files/trust", async (c) => {
2644
+ const name = c.req.param("name");
2645
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
2646
+ const body = await c.req.json();
2647
+ if (!body.sender) return c.json({ error: "sender is required" }, 400);
2648
+ addTrust(mindDir(name), body.sender);
2649
+ return c.json({ ok: true });
2650
+ }).delete("/:name/files/trust/:sender", (c) => {
2651
+ const name = c.req.param("name");
2652
+ if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
2653
+ const sender = c.req.param("sender");
2654
+ removeTrust(mindDir(name), sender);
2655
+ return c.json({ ok: true });
2656
+ });
2657
+ var file_sharing_default = app6;
2658
+
2659
+ // src/web/api/files.ts
2660
+ import { existsSync as existsSync8 } from "fs";
2661
+ import { readdir, readFile } from "fs/promises";
2662
+ import { resolve as resolve10 } from "path";
2663
+ import { Hono as Hono7 } from "hono";
2664
+ var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
2665
+ var app7 = new Hono7().get("/:name/files", async (c) => {
2666
+ const name = c.req.param("name");
2667
+ const entry = findMind(name);
2668
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2669
+ const dir = mindDir(name);
2670
+ const homeDir = resolve10(dir, "home");
2671
+ if (!existsSync8(homeDir)) return c.json({ error: "Home directory missing" }, 404);
2672
+ const allFiles = await readdir(homeDir);
2673
+ const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
2674
+ return c.json(files);
2675
+ }).get("/:name/files/:filename", async (c) => {
2676
+ const name = c.req.param("name");
2677
+ const filename = c.req.param("filename");
2678
+ if (!ALLOWED_FILES.has(filename)) {
2679
+ return c.json({ error: "File not allowed" }, 403);
2680
+ }
2681
+ const entry = findMind(name);
2682
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2683
+ const dir = mindDir(name);
2684
+ const filePath = resolve10(dir, "home", filename);
2685
+ if (!existsSync8(filePath)) {
2686
+ return c.json({ error: "File not found" }, 404);
2687
+ }
2688
+ const content = await readFile(filePath, "utf-8");
2689
+ return c.json({ filename, content });
2690
+ });
2691
+ var files_default = app7;
2692
+
2693
+ // src/web/api/keys.ts
2694
+ import { Hono as Hono8 } from "hono";
2695
+
2696
+ // src/lib/identity.ts
2697
+ import { createHash, generateKeyPairSync, sign, verify } from "crypto";
2698
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
2699
+ import { resolve as resolve11 } from "path";
2700
+ function generateIdentity(mindDir2) {
2701
+ const identityDir = resolve11(mindDir2, ".mind/identity");
2702
+ mkdirSync5(identityDir, { recursive: true });
2703
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
2704
+ publicKeyEncoding: { type: "spki", format: "pem" },
2705
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
2171
2706
  });
2172
- return {
2173
- id,
2174
- mind_name: mindName,
2175
- channel,
2176
- type,
2177
- name,
2178
- user_id: opts?.userId ?? null,
2179
- title: opts?.title ?? null,
2180
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
2181
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
2707
+ const privatePath = resolve11(identityDir, "private.pem");
2708
+ const publicPath = resolve11(identityDir, "public.pem");
2709
+ writeFileSync5(privatePath, privateKey, { mode: 384 });
2710
+ writeFileSync5(publicPath, publicKey, { mode: 420 });
2711
+ const config = readVoluteConfig(mindDir2) ?? {};
2712
+ config.identity = {
2713
+ privateKey: ".mind/identity/private.pem",
2714
+ publicKey: ".mind/identity/public.pem"
2182
2715
  };
2716
+ writeVoluteConfig(mindDir2, config);
2717
+ return { publicKeyPem: publicKey, privateKeyPem: privateKey };
2183
2718
  }
2184
- async function getConversation(id) {
2185
- const db = await getDb();
2186
- const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
2187
- return row ?? null;
2188
- }
2189
- async function addParticipant(conversationId, userId, role = "member") {
2190
- const db = await getDb();
2191
- await db.insert(conversationParticipants).values({
2192
- conversation_id: conversationId,
2193
- user_id: userId,
2194
- role
2195
- });
2196
- }
2197
- async function removeParticipant(conversationId, userId) {
2198
- const db = await getDb();
2199
- await db.delete(conversationParticipants).where(
2200
- and2(
2201
- eq3(conversationParticipants.conversation_id, conversationId),
2202
- eq3(conversationParticipants.user_id, userId)
2203
- )
2204
- );
2205
- }
2206
- async function getParticipants(conversationId) {
2207
- const db = await getDb();
2208
- const rows = await db.select({
2209
- userId: conversationParticipants.user_id,
2210
- username: users.username,
2211
- userType: users.user_type,
2212
- role: conversationParticipants.role
2213
- }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
2214
- return rows;
2215
- }
2216
- async function isParticipant(conversationId, userId) {
2217
- const db = await getDb();
2218
- const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2219
- and2(
2220
- eq3(conversationParticipants.conversation_id, conversationId),
2221
- eq3(conversationParticipants.user_id, userId)
2222
- )
2223
- ).get();
2224
- return row != null;
2719
+ function getPrivateKey(mindDir2) {
2720
+ const config = readVoluteConfig(mindDir2);
2721
+ const relPath = config?.identity?.privateKey;
2722
+ if (!relPath) return null;
2723
+ const fullPath = resolve11(mindDir2, relPath);
2724
+ if (!existsSync9(fullPath)) return null;
2725
+ return readFileSync7(fullPath, "utf-8");
2225
2726
  }
2226
- async function listConversationsForUser(userId) {
2227
- const db = await getDb();
2228
- const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
2229
- if (participantRows.length === 0) return [];
2230
- const convIds = participantRows.map((r) => r.conversation_id);
2231
- return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
2727
+ function getPublicKey(mindDir2) {
2728
+ const config = readVoluteConfig(mindDir2);
2729
+ const relPath = config?.identity?.publicKey;
2730
+ if (!relPath) return null;
2731
+ const fullPath = resolve11(mindDir2, relPath);
2732
+ if (!existsSync9(fullPath)) return null;
2733
+ return readFileSync7(fullPath, "utf-8");
2232
2734
  }
2233
- async function isParticipantOrOwner(conversationId, userId) {
2234
- if (await isParticipant(conversationId, userId)) return true;
2235
- const db = await getDb();
2236
- const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
2237
- return row != null;
2735
+ function getFingerprint(publicKeyPem) {
2736
+ return createHash("sha256").update(publicKeyPem).digest("hex");
2238
2737
  }
2239
- async function deleteConversationForUser(id, userId) {
2240
- if (!await isParticipantOrOwner(id, userId)) return false;
2241
- await deleteConversation(id);
2242
- return true;
2738
+ function signMessage(privateKeyPem, content, timestamp) {
2739
+ const data = `${content}
2740
+ ${timestamp}`;
2741
+ const signature = sign(null, Buffer.from(data), privateKeyPem);
2742
+ return signature.toString("base64");
2243
2743
  }
2244
- async function addMessage(conversationId, role, senderName, content) {
2245
- const db = await getDb();
2246
- const serialized = JSON.stringify(content);
2247
- const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2248
- await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
2249
- if (role === "user") {
2250
- const firstText = content.find((b) => b.type === "text");
2251
- const title = firstText ? firstText.text.slice(0, 80) : "";
2252
- if (title) {
2253
- await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
2744
+ async function publishPublicKey(mindName, publicKeyPem) {
2745
+ const systems = readSystemsConfig();
2746
+ if (!systems) return false;
2747
+ try {
2748
+ const res = await fetch(`${systems.apiUrl}/api/keys/${encodeURIComponent(mindName)}`, {
2749
+ method: "PUT",
2750
+ headers: {
2751
+ "Content-Type": "application/json",
2752
+ Authorization: `Bearer ${systems.apiKey}`
2753
+ },
2754
+ body: JSON.stringify({ publicKey: publicKeyPem })
2755
+ });
2756
+ if (!res.ok) {
2757
+ logger_default.warn(`failed to publish key for ${mindName}: ${res.status}`);
2758
+ return false;
2254
2759
  }
2760
+ return true;
2761
+ } catch (err) {
2762
+ logger_default.warn(`failed to publish key for ${mindName}`, logger_default.errorData(err));
2763
+ return false;
2255
2764
  }
2256
- const msg = {
2257
- id: result.id,
2258
- conversation_id: conversationId,
2259
- role,
2260
- sender_name: senderName,
2261
- content,
2262
- created_at: result.created_at
2263
- };
2264
- publish(conversationId, {
2265
- type: "message",
2266
- id: msg.id,
2267
- role: msg.role,
2268
- senderName: msg.sender_name,
2269
- content: msg.content,
2270
- createdAt: msg.created_at
2271
- });
2272
- return msg;
2273
2765
  }
2274
- async function getMessages(conversationId) {
2275
- const db = await getDb();
2276
- const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2277
- return rows.map((row) => {
2278
- let content;
2766
+
2767
+ // src/web/api/keys.ts
2768
+ var app8 = new Hono8().get("/:fingerprint", (c) => {
2769
+ const fingerprint = c.req.param("fingerprint");
2770
+ for (const entry of readRegistry()) {
2279
2771
  try {
2280
- const parsed = JSON.parse(row.content);
2281
- content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
2772
+ const pubKey = getPublicKey(mindDir(entry.name));
2773
+ if (!pubKey) continue;
2774
+ if (getFingerprint(pubKey) === fingerprint) {
2775
+ return c.json({ publicKey: pubKey, mind: entry.name });
2776
+ }
2282
2777
  } catch {
2283
- content = [{ type: "text", text: row.content }];
2284
2778
  }
2285
- return { ...row, content };
2779
+ }
2780
+ return c.json({ error: "Key not found" }, 404);
2781
+ });
2782
+ var keys_default = app8;
2783
+
2784
+ // src/web/api/logs.ts
2785
+ import { spawn as spawn2 } from "child_process";
2786
+ import { existsSync as existsSync10 } from "fs";
2787
+ import { resolve as resolve12 } from "path";
2788
+ import { Hono as Hono9 } from "hono";
2789
+ import { streamSSE as streamSSE2 } from "hono/streaming";
2790
+ var app9 = new Hono9().get("/:name/logs", async (c) => {
2791
+ const name = c.req.param("name");
2792
+ const entry = findMind(name);
2793
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2794
+ const logFile = resolve12(stateDir(name), "logs", "mind.log");
2795
+ if (!existsSync10(logFile)) {
2796
+ return c.json({ error: "No log file found" }, 404);
2797
+ }
2798
+ return streamSSE2(c, async (stream) => {
2799
+ const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
2800
+ const onData = (data) => {
2801
+ const lines = data.toString().split("\n");
2802
+ for (const line of lines) {
2803
+ if (line) {
2804
+ stream.writeSSE({ data: line }).catch(() => {
2805
+ });
2806
+ }
2807
+ }
2808
+ };
2809
+ tail.stdout.on("data", onData);
2810
+ stream.onAbort(() => {
2811
+ tail.kill();
2812
+ });
2813
+ await new Promise((resolve23) => {
2814
+ tail.on("exit", resolve23);
2815
+ stream.onAbort(resolve23);
2816
+ });
2286
2817
  });
2287
- }
2288
- async function listConversationsWithParticipants(userId) {
2289
- const convs = await listConversationsForUser(userId);
2290
- if (convs.length === 0) return [];
2291
- const db = await getDb();
2292
- const convIds = convs.map((c) => c.id);
2293
- const rows = await db.select({
2294
- conversationId: conversationParticipants.conversation_id,
2295
- userId: users.id,
2296
- username: users.username,
2297
- userType: users.user_type,
2298
- role: conversationParticipants.role
2299
- }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2300
- const byConv = /* @__PURE__ */ new Map();
2301
- for (const r of rows) {
2302
- let arr = byConv.get(r.conversationId);
2303
- if (!arr) {
2304
- arr = [];
2305
- byConv.set(r.conversationId, arr);
2818
+ }).get("/:name/logs/tail", async (c) => {
2819
+ const name = c.req.param("name");
2820
+ const entry = findMind(name);
2821
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2822
+ const logFile = resolve12(stateDir(name), "logs", "mind.log");
2823
+ if (!existsSync10(logFile)) {
2824
+ return c.json({ error: "No log file found" }, 404);
2825
+ }
2826
+ const nParam = parseInt(c.req.query("n") ?? "50", 10);
2827
+ const n = Number.isFinite(nParam) && nParam > 0 ? Math.min(nParam, 1e4) : 50;
2828
+ const tail = spawn2("tail", ["-n", String(n), logFile]);
2829
+ let output = "";
2830
+ tail.stdout.on("data", (data) => {
2831
+ output += data.toString();
2832
+ });
2833
+ await new Promise((resolve23) => {
2834
+ tail.on("exit", resolve23);
2835
+ });
2836
+ return c.text(output);
2837
+ });
2838
+ var logs_default = app9;
2839
+
2840
+ // src/web/api/mind-skills.ts
2841
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
2842
+ import { Hono as Hono10 } from "hono";
2843
+ import { z as z2 } from "zod";
2844
+ var app10 = new Hono10().get("/:name/skills", async (c) => {
2845
+ const name = c.req.param("name");
2846
+ const entry = findMind(name);
2847
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2848
+ const dir = mindDir(name);
2849
+ const skills = await listMindSkills(dir);
2850
+ return c.json(skills);
2851
+ }).post(
2852
+ "/:name/skills/install",
2853
+ requireAdmin,
2854
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2855
+ async (c) => {
2856
+ const name = c.req.param("name");
2857
+ const entry = findMind(name);
2858
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2859
+ const { skillId } = c.req.valid("json");
2860
+ const dir = mindDir(name);
2861
+ try {
2862
+ await installSkill(name, dir, skillId);
2863
+ } catch (e) {
2864
+ const msg = e instanceof Error ? e.message : String(e);
2865
+ return c.json({ error: msg }, 400);
2306
2866
  }
2307
- arr.push({
2308
- userId: r.userId,
2309
- username: r.username,
2310
- userType: r.userType,
2311
- role: r.role
2312
- });
2867
+ return c.json({ ok: true });
2313
2868
  }
2314
- const lastMsgIds = await db.select({
2315
- conversationId: messages.conversation_id,
2316
- maxId: sql`MAX(${messages.id})`
2317
- }).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
2318
- const byLastMsg = /* @__PURE__ */ new Map();
2319
- if (lastMsgIds.length > 0) {
2320
- const msgRows = await db.select().from(messages).where(
2321
- inArray(
2322
- messages.id,
2323
- lastMsgIds.map((r) => r.maxId)
2324
- )
2325
- );
2326
- for (const m of msgRows) {
2327
- let text = "";
2328
- try {
2329
- const parsed = JSON.parse(m.content);
2330
- const blocks = Array.isArray(parsed) ? parsed : [];
2331
- const textBlock = blocks.find((b) => b.type === "text");
2332
- if (textBlock && "text" in textBlock) text = textBlock.text;
2333
- } catch {
2334
- text = m.content;
2869
+ ).post(
2870
+ "/:name/skills/update",
2871
+ requireAdmin,
2872
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2873
+ async (c) => {
2874
+ const name = c.req.param("name");
2875
+ const entry = findMind(name);
2876
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2877
+ const { skillId } = c.req.valid("json");
2878
+ const dir = mindDir(name);
2879
+ try {
2880
+ const result = await updateSkill(name, dir, skillId);
2881
+ return c.json(result);
2882
+ } catch (e) {
2883
+ const msg = e instanceof Error ? e.message : String(e);
2884
+ return c.json({ error: msg }, 400);
2885
+ }
2886
+ }
2887
+ ).post(
2888
+ "/:name/skills/publish",
2889
+ requireAdmin,
2890
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2891
+ async (c) => {
2892
+ const name = c.req.param("name");
2893
+ const entry = findMind(name);
2894
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2895
+ const { skillId } = c.req.valid("json");
2896
+ const dir = mindDir(name);
2897
+ try {
2898
+ const skill = await publishSkill(name, dir, skillId);
2899
+ return c.json(skill);
2900
+ } catch (e) {
2901
+ const msg = e instanceof Error ? e.message : String(e);
2902
+ return c.json({ error: msg }, 400);
2903
+ }
2904
+ }
2905
+ ).delete("/:name/skills/:skill", requireAdmin, async (c) => {
2906
+ const name = c.req.param("name");
2907
+ const skillName = c.req.param("skill");
2908
+ const entry = findMind(name);
2909
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2910
+ const dir = mindDir(name);
2911
+ try {
2912
+ await uninstallSkill(name, dir, skillName);
2913
+ } catch (e) {
2914
+ const msg = e instanceof Error ? e.message : String(e);
2915
+ return c.json({ error: msg }, 400);
2916
+ }
2917
+ return c.json({ ok: true });
2918
+ });
2919
+ var mind_skills_default = app10;
2920
+
2921
+ // src/web/api/minds.ts
2922
+ import {
2923
+ cpSync as cpSync2,
2924
+ existsSync as existsSync12,
2925
+ mkdirSync as mkdirSync8,
2926
+ readdirSync as readdirSync6,
2927
+ readFileSync as readFileSync11,
2928
+ rmSync as rmSync3,
2929
+ writeFileSync as writeFileSync9
2930
+ } from "fs";
2931
+ import { resolve as resolve16 } from "path";
2932
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
2933
+ import { and as and3, desc as desc3, eq as eq4, sql as sql2 } from "drizzle-orm";
2934
+ import { Hono as Hono11 } from "hono";
2935
+ import { z as z3 } from "zod";
2936
+
2937
+ // src/lib/consolidate.ts
2938
+ import { readdirSync as readdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
2939
+ import { resolve as resolve13 } from "path";
2940
+ async function consolidateMemory(mindDir2) {
2941
+ const soulPath = resolve13(mindDir2, "home/SOUL.md");
2942
+ const memoryPath = resolve13(mindDir2, "home/MEMORY.md");
2943
+ const memoryDir = resolve13(mindDir2, "home/memory");
2944
+ const soul = readFileSync8(soulPath, "utf-8");
2945
+ const logs = [];
2946
+ try {
2947
+ const files = readdirSync4(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2948
+ for (const filename of files) {
2949
+ const date = filename.replace(".md", "");
2950
+ const content2 = readFileSync8(resolve13(memoryDir, filename), "utf-8").trim();
2951
+ if (content2) {
2952
+ logs.push(`### ${date}
2953
+
2954
+ ${content2}`);
2335
2955
  }
2336
- byLastMsg.set(m.conversation_id, {
2337
- role: m.role,
2338
- senderName: m.sender_name,
2339
- text,
2340
- createdAt: m.created_at
2341
- });
2342
2956
  }
2957
+ } catch {
2343
2958
  }
2344
- return convs.map((c) => ({
2345
- ...c,
2346
- participants: byConv.get(c.id) ?? [],
2347
- lastMessage: byLastMsg.get(c.id)
2348
- }));
2349
- }
2350
- async function findDMConversation(mindName, participantIds) {
2351
- const db = await getDb();
2352
- const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq3(conversations.mind_name, mindName), eq3(conversations.type, "dm"))).all();
2353
- for (const conv of mindConvs) {
2354
- const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
2355
- if (rows.length !== 2) continue;
2356
- const ids = new Set(rows.map((r) => r.user_id));
2357
- if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
2358
- return conv.id;
2359
- }
2959
+ if (logs.length === 0) {
2960
+ console.log("No daily logs found.");
2961
+ return;
2360
2962
  }
2361
- return null;
2362
- }
2363
- async function deleteConversation(id) {
2364
- const db = await getDb();
2365
- await db.delete(conversations).where(eq3(conversations.id, id));
2366
- }
2367
- async function createChannel(name, creatorId) {
2368
- const participantIds = creatorId ? [creatorId] : [];
2369
- return createConversation(null, "volute", {
2370
- type: "channel",
2371
- name,
2372
- title: name,
2373
- participantIds
2963
+ const apiKey = process.env.ANTHROPIC_API_KEY;
2964
+ if (!apiKey) {
2965
+ console.error("ANTHROPIC_API_KEY not set, skipping memory consolidation.");
2966
+ return;
2967
+ }
2968
+ console.log("Consolidating memory from daily logs...");
2969
+ const userMessage = [
2970
+ "You have daily logs from a previous environment but no long-term memory file yet.",
2971
+ "Please review the daily logs below and produce consolidated MEMORY.md content.",
2972
+ "Keep it concise and organized by topic. Output ONLY the markdown content for MEMORY.md, nothing else.",
2973
+ "",
2974
+ "## Daily logs",
2975
+ "",
2976
+ logs.join("\n\n")
2977
+ ].join("\n");
2978
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
2979
+ method: "POST",
2980
+ headers: {
2981
+ "Content-Type": "application/json",
2982
+ "x-api-key": apiKey,
2983
+ "anthropic-version": "2023-06-01"
2984
+ },
2985
+ body: JSON.stringify({
2986
+ model: "claude-sonnet-4-20250514",
2987
+ max_tokens: 4096,
2988
+ system: soul,
2989
+ messages: [{ role: "user", content: userMessage }]
2990
+ })
2374
2991
  });
2375
- }
2376
- async function getChannelByName(name) {
2377
- const db = await getDb();
2378
- const row = await db.select().from(conversations).where(and2(eq3(conversations.name, name), eq3(conversations.type, "channel"))).get();
2379
- return row ?? null;
2380
- }
2381
- async function listChannels() {
2382
- const db = await getDb();
2383
- return await db.select().from(conversations).where(eq3(conversations.type, "channel")).orderBy(conversations.name).all();
2384
- }
2385
- async function joinChannel(conversationId, userId) {
2386
- if (await isParticipant(conversationId, userId)) return;
2387
- await addParticipant(conversationId, userId);
2388
- }
2389
- async function leaveChannel(conversationId, userId) {
2390
- await removeParticipant(conversationId, userId);
2992
+ if (!res.ok) {
2993
+ const body = await res.text();
2994
+ console.error(`Anthropic API error (${res.status}): ${body}`);
2995
+ return;
2996
+ }
2997
+ const data = await res.json();
2998
+ const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
2999
+ if (content) {
3000
+ writeFileSync6(memoryPath, `${content}
3001
+ `);
3002
+ console.log("MEMORY.md created successfully.");
3003
+ } else {
3004
+ console.warn("Warning: No content produced.");
3005
+ }
2391
3006
  }
2392
3007
 
2393
3008
  // src/lib/convert-session.ts
2394
3009
  import { randomUUID as randomUUID2 } from "crypto";
2395
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
3010
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2396
3011
  import { homedir } from "os";
2397
- import { resolve as resolve11 } from "path";
3012
+ import { resolve as resolve14 } from "path";
2398
3013
  function convertSession(opts) {
2399
- const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
3014
+ const lines = readFileSync9(opts.sessionPath, "utf-8").trim().split("\n");
2400
3015
  const sessionId = randomUUID2();
2401
3016
  const idMap = /* @__PURE__ */ new Map();
2402
3017
  const messages2 = [];
@@ -2510,10 +3125,10 @@ function convertSession(opts) {
2510
3125
  }
2511
3126
  }
2512
3127
  const projectId = opts.projectDir.replace(/\//g, "-");
2513
- const sdkDir = resolve11(homedir(), ".claude", "projects", projectId);
2514
- mkdirSync5(sdkDir, { recursive: true });
2515
- const sdkPath = resolve11(sdkDir, `${sessionId}.jsonl`);
2516
- writeFileSync6(sdkPath, `${sdkEvents.join("\n")}
3128
+ const sdkDir = resolve14(homedir(), ".claude", "projects", projectId);
3129
+ mkdirSync6(sdkDir, { recursive: true });
3130
+ const sdkPath = resolve14(sdkDir, `${sessionId}.jsonl`);
3131
+ writeFileSync7(sdkPath, `${sdkEvents.join("\n")}
2517
3132
  `);
2518
3133
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
2519
3134
  return sessionId;
@@ -2565,21 +3180,21 @@ function convertAssistantContent(content) {
2565
3180
  }
2566
3181
 
2567
3182
  // src/lib/mind-events.ts
2568
- var subscribers2 = /* @__PURE__ */ new Map();
2569
- function subscribe2(mind, callback) {
2570
- let set = subscribers2.get(mind);
3183
+ var subscribers = /* @__PURE__ */ new Map();
3184
+ function subscribe3(mind, callback) {
3185
+ let set = subscribers.get(mind);
2571
3186
  if (!set) {
2572
3187
  set = /* @__PURE__ */ new Set();
2573
- subscribers2.set(mind, set);
3188
+ subscribers.set(mind, set);
2574
3189
  }
2575
3190
  set.add(callback);
2576
3191
  return () => {
2577
3192
  set.delete(callback);
2578
- if (set.size === 0) subscribers2.delete(mind);
3193
+ if (set.size === 0) subscribers.delete(mind);
2579
3194
  };
2580
3195
  }
2581
- function publish2(mind, event) {
2582
- const set = subscribers2.get(mind);
3196
+ function publish3(mind, event) {
3197
+ const set = subscribers.get(mind);
2583
3198
  if (!set) return;
2584
3199
  for (const cb of set) {
2585
3200
  try {
@@ -2587,7 +3202,7 @@ function publish2(mind, event) {
2587
3202
  } catch (err) {
2588
3203
  console.error("[mind-events] subscriber threw:", err);
2589
3204
  set.delete(cb);
2590
- if (set.size === 0) subscribers2.delete(mind);
3205
+ if (set.size === 0) subscribers.delete(mind);
2591
3206
  }
2592
3207
  }
2593
3208
  }
@@ -2595,22 +3210,22 @@ function publish2(mind, event) {
2595
3210
  // src/lib/template.ts
2596
3211
  import {
2597
3212
  cpSync,
2598
- existsSync as existsSync9,
2599
- mkdirSync as mkdirSync6,
2600
- readdirSync as readdirSync3,
2601
- readFileSync as readFileSync8,
3213
+ existsSync as existsSync11,
3214
+ mkdirSync as mkdirSync7,
3215
+ readdirSync as readdirSync5,
3216
+ readFileSync as readFileSync10,
2602
3217
  renameSync as renameSync3,
2603
- rmSync,
2604
- statSync,
2605
- writeFileSync as writeFileSync7
3218
+ rmSync as rmSync2,
3219
+ statSync as statSync3,
3220
+ writeFileSync as writeFileSync8
2606
3221
  } from "fs";
2607
3222
  import { tmpdir } from "os";
2608
- import { dirname as dirname2, join, relative, resolve as resolve12 } from "path";
3223
+ import { dirname as dirname2, join as join3, relative, resolve as resolve15 } from "path";
2609
3224
  function findTemplatesRoot() {
2610
3225
  let dir = dirname2(new URL(import.meta.url).pathname);
2611
3226
  for (let i = 0; i < 5; i++) {
2612
- const candidate = resolve12(dir, "templates");
2613
- if (existsSync9(resolve12(candidate, "_base"))) return candidate;
3227
+ const candidate = resolve15(dir, "templates");
3228
+ if (existsSync11(resolve15(candidate, "_base"))) return candidate;
2614
3229
  dir = dirname2(dir);
2615
3230
  }
2616
3231
  console.error(
@@ -2620,72 +3235,72 @@ function findTemplatesRoot() {
2620
3235
  process.exit(1);
2621
3236
  }
2622
3237
  function composeTemplate(templatesRoot, templateName) {
2623
- const baseDir = resolve12(templatesRoot, "_base");
2624
- const templateDir = resolve12(templatesRoot, templateName);
2625
- if (!existsSync9(baseDir)) {
3238
+ const baseDir = resolve15(templatesRoot, "_base");
3239
+ const templateDir = resolve15(templatesRoot, templateName);
3240
+ if (!existsSync11(baseDir)) {
2626
3241
  console.error("Base template not found:", baseDir);
2627
3242
  process.exit(1);
2628
3243
  }
2629
- if (!existsSync9(templateDir)) {
3244
+ if (!existsSync11(templateDir)) {
2630
3245
  console.error(`Template not found: ${templateName}`);
2631
3246
  process.exit(1);
2632
3247
  }
2633
- const composedDir = resolve12(tmpdir(), `volute-template-${Date.now()}`);
2634
- mkdirSync6(composedDir, { recursive: true });
3248
+ const composedDir = resolve15(tmpdir(), `volute-template-${Date.now()}`);
3249
+ mkdirSync7(composedDir, { recursive: true });
2635
3250
  cpSync(baseDir, composedDir, { recursive: true });
2636
3251
  for (const file of listFiles(templateDir)) {
2637
- const src = resolve12(templateDir, file);
2638
- const dest = resolve12(composedDir, file);
2639
- mkdirSync6(dirname2(dest), { recursive: true });
3252
+ const src = resolve15(templateDir, file);
3253
+ const dest = resolve15(composedDir, file);
3254
+ mkdirSync7(dirname2(dest), { recursive: true });
2640
3255
  cpSync(src, dest);
2641
3256
  }
2642
- const manifestPath = resolve12(composedDir, "volute-template.json");
2643
- if (!existsSync9(manifestPath)) {
2644
- rmSync(composedDir, { recursive: true, force: true });
3257
+ const manifestPath = resolve15(composedDir, "volute-template.json");
3258
+ if (!existsSync11(manifestPath)) {
3259
+ rmSync2(composedDir, { recursive: true, force: true });
2645
3260
  console.error(`Template manifest not found: ${templateName}/volute-template.json`);
2646
3261
  process.exit(1);
2647
3262
  }
2648
- const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
2649
- rmSync(manifestPath);
3263
+ const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
3264
+ rmSync2(manifestPath);
2650
3265
  return { composedDir, manifest };
2651
3266
  }
2652
3267
  function copyTemplateToDir(composedDir, destDir, mindName, manifest) {
2653
3268
  cpSync(composedDir, destDir, { recursive: true });
2654
3269
  for (const [from, to] of Object.entries(manifest.rename)) {
2655
- const fromPath = resolve12(destDir, from);
2656
- if (existsSync9(fromPath)) {
2657
- renameSync3(fromPath, resolve12(destDir, to));
3270
+ const fromPath = resolve15(destDir, from);
3271
+ if (existsSync11(fromPath)) {
3272
+ renameSync3(fromPath, resolve15(destDir, to));
2658
3273
  }
2659
3274
  }
2660
3275
  for (const file of manifest.substitute) {
2661
- const path = resolve12(destDir, file);
2662
- if (existsSync9(path)) {
2663
- const content = readFileSync8(path, "utf-8");
2664
- writeFileSync7(path, content.replaceAll("{{name}}", mindName));
3276
+ const path = resolve15(destDir, file);
3277
+ if (existsSync11(path)) {
3278
+ const content = readFileSync10(path, "utf-8");
3279
+ writeFileSync8(path, content.replaceAll("{{name}}", mindName));
2665
3280
  }
2666
3281
  }
2667
3282
  }
2668
3283
  function applyInitFiles(destDir) {
2669
- const initDir = resolve12(destDir, ".init");
2670
- if (!existsSync9(initDir)) return;
2671
- const homeDir = resolve12(destDir, "home");
3284
+ const initDir = resolve15(destDir, ".init");
3285
+ if (!existsSync11(initDir)) return;
3286
+ const homeDir = resolve15(destDir, "home");
2672
3287
  for (const file of listFiles(initDir)) {
2673
- const src = resolve12(initDir, file);
2674
- const dest = resolve12(homeDir, file);
3288
+ const src = resolve15(initDir, file);
3289
+ const dest = resolve15(homeDir, file);
2675
3290
  const parent = dirname2(dest);
2676
- if (!existsSync9(parent)) {
2677
- mkdirSync6(parent, { recursive: true });
3291
+ if (!existsSync11(parent)) {
3292
+ mkdirSync7(parent, { recursive: true });
2678
3293
  }
2679
3294
  cpSync(src, dest);
2680
3295
  }
2681
- rmSync(initDir, { recursive: true, force: true });
3296
+ rmSync2(initDir, { recursive: true, force: true });
2682
3297
  }
2683
3298
  function listFiles(dir) {
2684
3299
  const results = [];
2685
3300
  function walk(current) {
2686
- for (const entry of readdirSync3(current)) {
2687
- const full = join(current, entry);
2688
- if (statSync(full).isDirectory()) {
3301
+ for (const entry of readdirSync5(current)) {
3302
+ const full = join3(current, entry);
3303
+ if (statSync3(full).isDirectory()) {
2689
3304
  if (entry === ".git") continue;
2690
3305
  walk(full);
2691
3306
  } else {
@@ -2729,6 +3344,12 @@ async function getMindStatus(name, port) {
2729
3344
  return { status, channels };
2730
3345
  }
2731
3346
  var TEMPLATE_BRANCH = "volute/template";
3347
+ async function configureGitIdentity(mindName, opts) {
3348
+ const systemsConfig = readSystemsConfig();
3349
+ const system = systemsConfig?.system ?? "local";
3350
+ await gitExec(["config", "user.name", mindName], opts);
3351
+ await gitExec(["config", "user.email", `${mindName}.${system}@volute.systems`], opts);
3352
+ }
2732
3353
  async function initTemplateBranch(projectRoot, composedDir, manifest, mindName, env) {
2733
3354
  const templateFiles = listFiles(composedDir).filter((f) => !f.startsWith(".init/") && !f.startsWith(".init\\")).map((f) => manifest.rename[f] ?? f);
2734
3355
  const opts = { cwd: projectRoot, mindName, env };
@@ -2740,7 +3361,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
2740
3361
  await gitExec(["commit", "-m", "initial commit"], opts);
2741
3362
  }
2742
3363
  async function updateTemplateBranch(projectRoot, template, mindName) {
2743
- const tempWorktree = resolve13(projectRoot, ".variants", "_template_update");
3364
+ const tempWorktree = resolve16(projectRoot, ".variants", "_template_update");
2744
3365
  let branchExists = false;
2745
3366
  try {
2746
3367
  await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
@@ -2751,8 +3372,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2751
3372
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
2752
3373
  } catch {
2753
3374
  }
2754
- if (existsSync10(tempWorktree)) {
2755
- rmSync2(tempWorktree, { recursive: true, force: true });
3375
+ if (existsSync12(tempWorktree)) {
3376
+ rmSync3(tempWorktree, { recursive: true, force: true });
2756
3377
  }
2757
3378
  const templatesRoot = findTemplatesRoot();
2758
3379
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
@@ -2772,9 +3393,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2772
3393
  });
2773
3394
  }
2774
3395
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
2775
- const initDir = resolve13(tempWorktree, ".init");
2776
- if (existsSync10(initDir)) {
2777
- rmSync2(initDir, { recursive: true, force: true });
3396
+ const initDir = resolve16(tempWorktree, ".init");
3397
+ if (existsSync12(initDir)) {
3398
+ rmSync3(initDir, { recursive: true, force: true });
2778
3399
  }
2779
3400
  await gitExec(["add", "-A"], { cwd: tempWorktree });
2780
3401
  try {
@@ -2787,10 +3408,10 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2787
3408
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
2788
3409
  } catch {
2789
3410
  }
2790
- if (existsSync10(tempWorktree)) {
2791
- rmSync2(tempWorktree, { recursive: true, force: true });
3411
+ if (existsSync12(tempWorktree)) {
3412
+ rmSync3(tempWorktree, { recursive: true, force: true });
2792
3413
  }
2793
- rmSync2(composedDir, { recursive: true, force: true });
3414
+ rmSync3(composedDir, { recursive: true, force: true });
2794
3415
  }
2795
3416
  }
2796
3417
  async function mergeTemplateBranch(worktreeDir) {
@@ -2813,14 +3434,14 @@ async function mergeTemplateBranch(worktreeDir) {
2813
3434
  async function npmInstallAsMind(cwd, mindName) {
2814
3435
  if (isIsolationEnabled()) {
2815
3436
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
2816
- await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve13(cwd, "home") } });
3437
+ await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve16(cwd, "home") } });
2817
3438
  } else {
2818
3439
  await exec("npm", ["install"], { cwd });
2819
3440
  }
2820
3441
  }
2821
3442
  async function importFromArchive(c, tempDir, nameOverride, manifest) {
2822
- const extractedMindDir = resolve13(tempDir, "mind");
2823
- if (!existsSync10(extractedMindDir)) {
3443
+ const extractedMindDir = resolve16(tempDir, "mind");
3444
+ if (!existsSync12(extractedMindDir)) {
2824
3445
  return c.json({ error: "Invalid archive: missing mind/ directory" }, 400);
2825
3446
  }
2826
3447
  if (!manifest?.includes || !manifest.name || !manifest.template) {
@@ -2832,34 +3453,34 @@ async function importFromArchive(c, tempDir, nameOverride, manifest) {
2832
3453
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
2833
3454
  ensureVoluteHome();
2834
3455
  const dest = mindDir(name);
2835
- if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3456
+ if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
2836
3457
  try {
2837
3458
  cpSync2(extractedMindDir, dest, { recursive: true });
2838
3459
  if (!manifest.includes.identity) {
2839
3460
  generateIdentity(dest);
2840
3461
  }
2841
3462
  const state = stateDir(name);
2842
- mkdirSync7(state, { recursive: true });
2843
- const channelsJson = resolve13(tempDir, "state/channels.json");
2844
- if (existsSync10(channelsJson)) {
2845
- cpSync2(channelsJson, resolve13(state, "channels.json"));
3463
+ mkdirSync8(state, { recursive: true });
3464
+ const channelsJson = resolve16(tempDir, "state/channels.json");
3465
+ if (existsSync12(channelsJson)) {
3466
+ cpSync2(channelsJson, resolve16(state, "channels.json"));
2846
3467
  }
2847
- const envJson = resolve13(tempDir, "state/env.json");
2848
- if (existsSync10(envJson)) {
2849
- cpSync2(envJson, resolve13(state, "env.json"));
3468
+ const envJson = resolve16(tempDir, "state/env.json");
3469
+ if (existsSync12(envJson)) {
3470
+ cpSync2(envJson, resolve16(state, "env.json"));
2850
3471
  }
2851
3472
  const port = nextPort();
2852
3473
  addMind(name, port, void 0, manifest.template);
2853
- const homeDir = resolve13(dest, "home");
3474
+ const homeDir = resolve16(dest, "home");
2854
3475
  ensureVoluteGroup();
2855
3476
  createMindUser(name, homeDir);
2856
3477
  chownMindDir(dest, name);
2857
3478
  await npmInstallAsMind(dest, name);
2858
- const historyJsonl = resolve13(tempDir, "history.jsonl");
2859
- if (existsSync10(historyJsonl)) {
3479
+ const historyJsonl = resolve16(tempDir, "history.jsonl");
3480
+ if (existsSync12(historyJsonl)) {
2860
3481
  try {
2861
3482
  const db = await getDb();
2862
- const lines = readFileSync9(historyJsonl, "utf-8").trim().split("\n");
3483
+ const lines = readFileSync11(historyJsonl, "utf-8").trim().split("\n");
2863
3484
  let imported = 0;
2864
3485
  let failed = 0;
2865
3486
  for (const line of lines) {
@@ -2894,31 +3515,32 @@ async function importFromArchive(c, tempDir, nameOverride, manifest) {
2894
3515
  logger_default.error("Failed to open database for history import", logger_default.errorData(err));
2895
3516
  }
2896
3517
  }
2897
- const sessionsDir = resolve13(tempDir, "sessions");
2898
- if (existsSync10(sessionsDir)) {
2899
- const destSessions = resolve13(dest, ".mind/sessions");
2900
- mkdirSync7(destSessions, { recursive: true });
2901
- for (const file of readdirSync4(sessionsDir)) {
2902
- cpSync2(resolve13(sessionsDir, file), resolve13(destSessions, file));
3518
+ const sessionsDir = resolve16(tempDir, "sessions");
3519
+ if (existsSync12(sessionsDir)) {
3520
+ const destSessions = resolve16(dest, ".mind/sessions");
3521
+ mkdirSync8(destSessions, { recursive: true });
3522
+ for (const file of readdirSync6(sessionsDir)) {
3523
+ cpSync2(resolve16(sessionsDir, file), resolve16(destSessions, file));
2903
3524
  }
2904
3525
  }
2905
- if (!existsSync10(resolve13(dest, ".git"))) {
2906
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
3526
+ if (!existsSync12(resolve16(dest, ".git"))) {
3527
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
2907
3528
  await gitExec(["init"], { cwd: dest, mindName: name, env });
3529
+ await configureGitIdentity(name, { cwd: dest, mindName: name, env });
2908
3530
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
2909
3531
  await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
2910
3532
  }
2911
3533
  chownMindDir(dest, name);
2912
- rmSync2(tempDir, { recursive: true, force: true });
3534
+ rmSync3(tempDir, { recursive: true, force: true });
2913
3535
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
2914
3536
  } catch (err) {
2915
- if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
3537
+ if (existsSync12(dest)) rmSync3(dest, { recursive: true, force: true });
2916
3538
  try {
2917
3539
  removeMind(name);
2918
3540
  } catch (cleanupErr) {
2919
3541
  logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
2920
3542
  }
2921
- rmSync2(tempDir, { recursive: true, force: true });
3543
+ rmSync3(tempDir, { recursive: true, force: true });
2922
3544
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
2923
3545
  }
2924
3546
  }
@@ -2931,7 +3553,7 @@ var createMindSchema = z3.object({
2931
3553
  seedSoul: z3.string().optional(),
2932
3554
  skills: z3.array(z3.string()).optional()
2933
3555
  });
2934
- var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
3556
+ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
2935
3557
  const body = c.req.valid("json");
2936
3558
  const { name, template = "claude" } = body;
2937
3559
  const nameErr = validateMindName(name);
@@ -2939,7 +3561,7 @@ var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSch
2939
3561
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
2940
3562
  ensureVoluteHome();
2941
3563
  const dest = mindDir(name);
2942
- if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3564
+ if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
2943
3565
  const templatesRoot = findTemplatesRoot();
2944
3566
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
2945
3567
  try {
@@ -2947,21 +3569,21 @@ var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSch
2947
3569
  applyInitFiles(dest);
2948
3570
  const { publicKeyPem } = generateIdentity(dest);
2949
3571
  if (body.model) {
2950
- const configPath = resolve13(dest, "home/.config/config.json");
2951
- const existing = existsSync10(configPath) ? JSON.parse(readFileSync9(configPath, "utf-8")) : {};
3572
+ const configPath2 = resolve16(dest, "home/.config/config.json");
3573
+ const existing = existsSync12(configPath2) ? JSON.parse(readFileSync11(configPath2, "utf-8")) : {};
2952
3574
  existing.model = body.model;
2953
- writeFileSync8(configPath, `${JSON.stringify(existing, null, 2)}
3575
+ writeFileSync9(configPath2, `${JSON.stringify(existing, null, 2)}
2954
3576
  `);
2955
3577
  }
2956
3578
  const mindPrompts = await getMindPromptDefaults();
2957
- writeFileSync8(
2958
- resolve13(dest, "home/.config/prompts.json"),
3579
+ writeFileSync9(
3580
+ resolve16(dest, "home/.config/prompts.json"),
2959
3581
  `${JSON.stringify(mindPrompts, null, 2)}
2960
3582
  `
2961
3583
  );
2962
3584
  const port = nextPort();
2963
3585
  addMind(name, port, body.stage, template);
2964
- const homeDir = resolve13(dest, "home");
3586
+ const homeDir = resolve16(dest, "home");
2965
3587
  ensureVoluteGroup();
2966
3588
  createMindUser(name, homeDir);
2967
3589
  chownMindDir(dest, name);
@@ -2970,10 +3592,11 @@ var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSch
2970
3592
  try {
2971
3593
  const env = isIsolationEnabled() ? { ...process.env, HOME: homeDir } : void 0;
2972
3594
  await gitExec(["init"], { cwd: dest, mindName: name, env });
3595
+ await configureGitIdentity(name, { cwd: dest, mindName: name, env });
2973
3596
  await initTemplateBranch(dest, composedDir, manifest, name, env);
2974
3597
  } catch (err) {
2975
3598
  logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
2976
- rmSync2(resolve13(dest, ".git"), { recursive: true, force: true });
3599
+ rmSync3(resolve16(dest, ".git"), { recursive: true, force: true });
2977
3600
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
2978
3601
  }
2979
3602
  try {
@@ -2988,7 +3611,7 @@ The human who planted you described you as: "${body.description}"
2988
3611
  ` : "";
2989
3612
  const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
2990
3613
  const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
2991
- writeFileSync8(resolve13(dest, "home/SOUL.md"), seedSoul);
3614
+ writeFileSync9(resolve16(dest, "home/SOUL.md"), seedSoul);
2992
3615
  }
2993
3616
  const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
2994
3617
  const skillWarnings = [];
@@ -3003,11 +3626,11 @@ The human who planted you described you as: "${body.description}"
3003
3626
  if (body.stage !== "seed") {
3004
3627
  const customSoul = await getPromptIfCustom("default_soul");
3005
3628
  if (customSoul) {
3006
- writeFileSync8(resolve13(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3629
+ writeFileSync9(resolve16(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3007
3630
  }
3008
3631
  const customMemory = await getPromptIfCustom("default_memory");
3009
3632
  if (customMemory) {
3010
- writeFileSync8(resolve13(dest, "home/MEMORY.md"), customMemory);
3633
+ writeFileSync9(resolve16(dest, "home/MEMORY.md"), customMemory);
3011
3634
  }
3012
3635
  }
3013
3636
  publishPublicKey(name, publicKeyPem).catch(
@@ -3023,14 +3646,14 @@ The human who planted you described you as: "${body.description}"
3023
3646
  ...skillWarnings.length > 0 && { skillWarnings }
3024
3647
  });
3025
3648
  } catch (err) {
3026
- if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
3649
+ if (existsSync12(dest)) rmSync3(dest, { recursive: true, force: true });
3027
3650
  try {
3028
3651
  removeMind(name);
3029
3652
  } catch {
3030
3653
  }
3031
3654
  return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
3032
3655
  } finally {
3033
- rmSync2(composedDir, { recursive: true, force: true });
3656
+ rmSync3(composedDir, { recursive: true, force: true });
3034
3657
  }
3035
3658
  }).post("/import", requireAdmin, async (c) => {
3036
3659
  let body;
@@ -3043,13 +3666,13 @@ The human who planted you described you as: "${body.description}"
3043
3666
  return importFromArchive(c, body.archivePath, body.name, body.manifest);
3044
3667
  }
3045
3668
  const wsDir = body.workspacePath;
3046
- if (!wsDir || !existsSync10(resolve13(wsDir, "SOUL.md")) || !existsSync10(resolve13(wsDir, "IDENTITY.md"))) {
3669
+ if (!wsDir || !existsSync12(resolve16(wsDir, "SOUL.md")) || !existsSync12(resolve16(wsDir, "IDENTITY.md"))) {
3047
3670
  return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
3048
3671
  }
3049
- const soul = readFileSync9(resolve13(wsDir, "SOUL.md"), "utf-8");
3050
- const identity = readFileSync9(resolve13(wsDir, "IDENTITY.md"), "utf-8");
3051
- const userPath = resolve13(wsDir, "USER.md");
3052
- const user = existsSync10(userPath) ? readFileSync9(userPath, "utf-8") : "";
3672
+ const soul = readFileSync11(resolve16(wsDir, "SOUL.md"), "utf-8");
3673
+ const identity = readFileSync11(resolve16(wsDir, "IDENTITY.md"), "utf-8");
3674
+ const userPath = resolve16(wsDir, "USER.md");
3675
+ const user = existsSync12(userPath) ? readFileSync11(userPath, "utf-8") : "";
3053
3676
  const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
3054
3677
  const template = body.template ?? "claude";
3055
3678
  const nameErr = validateMindName(name);
@@ -3069,39 +3692,39 @@ ${user.trimEnd()}
3069
3692
  ` : "";
3070
3693
  ensureVoluteHome();
3071
3694
  const dest = mindDir(name);
3072
- if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3695
+ if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3073
3696
  const templatesRoot = findTemplatesRoot();
3074
3697
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
3075
3698
  try {
3076
3699
  copyTemplateToDir(composedDir, dest, name, manifest);
3077
3700
  applyInitFiles(dest);
3078
3701
  const { publicKeyPem: importPublicKey } = generateIdentity(dest);
3079
- writeFileSync8(resolve13(dest, "home/SOUL.md"), mergedSoul);
3080
- const wsMemoryPath = resolve13(wsDir, "MEMORY.md");
3081
- const hasMemory = existsSync10(wsMemoryPath);
3702
+ writeFileSync9(resolve16(dest, "home/SOUL.md"), mergedSoul);
3703
+ const wsMemoryPath = resolve16(wsDir, "MEMORY.md");
3704
+ const hasMemory = existsSync12(wsMemoryPath);
3082
3705
  if (hasMemory) {
3083
- const existingMemory = readFileSync9(wsMemoryPath, "utf-8");
3084
- writeFileSync8(
3085
- resolve13(dest, "home/MEMORY.md"),
3706
+ const existingMemory = readFileSync11(wsMemoryPath, "utf-8");
3707
+ writeFileSync9(
3708
+ resolve16(dest, "home/MEMORY.md"),
3086
3709
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
3087
3710
  );
3088
3711
  } else if (user) {
3089
- writeFileSync8(resolve13(dest, "home/MEMORY.md"), `${user.trimEnd()}
3712
+ writeFileSync9(resolve16(dest, "home/MEMORY.md"), `${user.trimEnd()}
3090
3713
  `);
3091
3714
  }
3092
- const wsMemoryDir = resolve13(wsDir, "memory");
3715
+ const wsMemoryDir = resolve16(wsDir, "memory");
3093
3716
  let dailyLogCount = 0;
3094
- if (existsSync10(wsMemoryDir)) {
3095
- const destMemoryDir = resolve13(dest, "home/memory");
3096
- const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
3717
+ if (existsSync12(wsMemoryDir)) {
3718
+ const destMemoryDir = resolve16(dest, "home/memory");
3719
+ const files = readdirSync6(wsMemoryDir).filter((f) => f.endsWith(".md"));
3097
3720
  for (const file of files) {
3098
- cpSync2(resolve13(wsMemoryDir, file), resolve13(destMemoryDir, file));
3721
+ cpSync2(resolve16(wsMemoryDir, file), resolve16(destMemoryDir, file));
3099
3722
  }
3100
3723
  dailyLogCount = files.length;
3101
3724
  }
3102
3725
  const port = nextPort();
3103
3726
  addMind(name, port, void 0, template);
3104
- const homeDir = resolve13(dest, "home");
3727
+ const homeDir = resolve16(dest, "home");
3105
3728
  ensureVoluteGroup();
3106
3729
  createMindUser(name, homeDir);
3107
3730
  chownMindDir(dest, name);
@@ -3109,19 +3732,20 @@ ${user.trimEnd()}
3109
3732
  if (!hasMemory && dailyLogCount > 0) {
3110
3733
  await consolidateMemory(dest);
3111
3734
  }
3112
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
3735
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
3113
3736
  await gitExec(["init"], { cwd: dest, mindName: name, env });
3737
+ await configureGitIdentity(name, { cwd: dest, mindName: name, env });
3114
3738
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
3115
3739
  await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
3116
- const sessionFile = body.sessionPath ? resolve13(body.sessionPath) : findOpenClawSession(wsDir);
3117
- if (sessionFile && existsSync10(sessionFile)) {
3740
+ const sessionFile = body.sessionPath ? resolve16(body.sessionPath) : findOpenClawSession(wsDir);
3741
+ if (sessionFile && existsSync12(sessionFile)) {
3118
3742
  if (template === "pi") {
3119
3743
  importPiSession(sessionFile, dest);
3120
3744
  } else if (template === "claude") {
3121
3745
  const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
3122
- const mindRuntimeDir = resolve13(dest, ".mind");
3123
- mkdirSync7(mindRuntimeDir, { recursive: true });
3124
- writeFileSync8(resolve13(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
3746
+ const mindRuntimeDir = resolve16(dest, ".mind");
3747
+ mkdirSync8(mindRuntimeDir, { recursive: true });
3748
+ writeFileSync9(resolve16(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
3125
3749
  }
3126
3750
  }
3127
3751
  importOpenClawConnectors(name, dest);
@@ -3136,14 +3760,14 @@ ${user.trimEnd()}
3136
3760
  );
3137
3761
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
3138
3762
  } catch (err) {
3139
- if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
3763
+ if (existsSync12(dest)) rmSync3(dest, { recursive: true, force: true });
3140
3764
  try {
3141
3765
  removeMind(name);
3142
3766
  } catch {
3143
3767
  }
3144
3768
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
3145
3769
  } finally {
3146
- rmSync2(composedDir, { recursive: true, force: true });
3770
+ rmSync3(composedDir, { recursive: true, force: true });
3147
3771
  }
3148
3772
  }).get("/", async (c) => {
3149
3773
  const entries = readRegistry();
@@ -3160,7 +3784,7 @@ ${user.trimEnd()}
3160
3784
  const minds = await Promise.all(
3161
3785
  entries.map(async (entry) => {
3162
3786
  const { status, channels } = await getMindStatus(entry.name, entry.port);
3163
- const hasPages = existsSync10(resolve13(mindDir(entry.name), "home", "pages"));
3787
+ const hasPages = existsSync12(resolve16(mindDir(entry.name), "home", "pages"));
3164
3788
  return {
3165
3789
  ...entry,
3166
3790
  status,
@@ -3171,58 +3795,15 @@ ${user.trimEnd()}
3171
3795
  })
3172
3796
  );
3173
3797
  return c.json(minds);
3798
+ }).get("/pages/sites", async (c) => {
3799
+ return c.json(getCachedSites());
3174
3800
  }).get("/pages/recent", async (c) => {
3175
- const entries = readRegistry();
3176
- const pages = [];
3177
- for (const entry of entries) {
3178
- const pagesDir = resolve13(mindDir(entry.name), "home", "pages");
3179
- if (!existsSync10(pagesDir)) continue;
3180
- let items;
3181
- try {
3182
- items = readdirSync4(pagesDir);
3183
- } catch (err) {
3184
- logger_default.warn("Failed to read pages dir", { mind: entry.name, error: err.message });
3185
- continue;
3186
- }
3187
- for (const item of items) {
3188
- const fullPath = resolve13(pagesDir, item);
3189
- try {
3190
- const s = statSync2(fullPath);
3191
- if (s.isFile()) {
3192
- pages.push({
3193
- mind: entry.name,
3194
- file: item,
3195
- modified: s.mtime.toISOString(),
3196
- url: `/pages/${entry.name}/${item}`
3197
- });
3198
- } else if (s.isDirectory()) {
3199
- const indexPath = resolve13(fullPath, "index.html");
3200
- if (existsSync10(indexPath)) {
3201
- const indexStat = statSync2(indexPath);
3202
- pages.push({
3203
- mind: entry.name,
3204
- file: join2(item, "index.html"),
3205
- modified: indexStat.mtime.toISOString(),
3206
- url: `/pages/${entry.name}/${item}/`
3207
- });
3208
- }
3209
- }
3210
- } catch (err) {
3211
- logger_default.warn("Failed to stat page item", {
3212
- mind: entry.name,
3213
- item,
3214
- error: err.message
3215
- });
3216
- }
3217
- }
3218
- }
3219
- pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
3220
- return c.json(pages.slice(0, 10));
3801
+ return c.json(getCachedRecentPages());
3221
3802
  }).get("/:name", async (c) => {
3222
3803
  const name = c.req.param("name");
3223
3804
  const entry = findMind(name);
3224
3805
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3225
- if (!existsSync10(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
3806
+ if (!existsSync12(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
3226
3807
  const { status, channels } = await getMindStatus(name, entry.port);
3227
3808
  const variants = readVariants(name);
3228
3809
  const manager = getMindManager();
@@ -3237,7 +3818,7 @@ ${user.trimEnd()}
3237
3818
  return { name: v.name, port: v.port, status: variantStatus };
3238
3819
  })
3239
3820
  );
3240
- const hasPages = existsSync10(resolve13(mindDir(name), "home", "pages"));
3821
+ const hasPages = existsSync12(resolve16(mindDir(name), "home", "pages"));
3241
3822
  return c.json({ ...entry, status, channels, variants: variantStatuses, hasPages });
3242
3823
  }).post("/:name/start", requireAdmin, async (c) => {
3243
3824
  const name = c.req.param("name");
@@ -3249,7 +3830,7 @@ ${user.trimEnd()}
3249
3830
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
3250
3831
  } else {
3251
3832
  const dir = mindDir(baseName);
3252
- if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
3833
+ if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3253
3834
  }
3254
3835
  if (getMindManager().isRunning(name)) {
3255
3836
  return c.json({ error: "Mind already running" }, 409);
@@ -3270,7 +3851,7 @@ ${user.trimEnd()}
3270
3851
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
3271
3852
  } else {
3272
3853
  const dir = mindDir(baseName);
3273
- if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
3854
+ if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3274
3855
  }
3275
3856
  let context;
3276
3857
  const contentType = c.req.header("content-type");
@@ -3297,7 +3878,7 @@ ${user.trimEnd()}
3297
3878
  const variant = findVariant(baseName, mergeVariantName);
3298
3879
  if (variant) {
3299
3880
  const projectRoot = mindDir(baseName);
3300
- if (existsSync10(variant.path)) {
3881
+ if (existsSync12(variant.path)) {
3301
3882
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
3302
3883
  if (status) {
3303
3884
  try {
@@ -3325,7 +3906,7 @@ ${user.trimEnd()}
3325
3906
  }
3326
3907
  }
3327
3908
  await gitExec(["merge", variant.branch], { cwd: projectRoot });
3328
- if (existsSync10(variant.path)) {
3909
+ if (existsSync12(variant.path)) {
3329
3910
  try {
3330
3911
  await gitExec(["worktree", "remove", "--force", variant.path], {
3331
3912
  cwd: projectRoot
@@ -3414,11 +3995,11 @@ ${user.trimEnd()}
3414
3995
  removeMind(name);
3415
3996
  await deleteMindUser2(name);
3416
3997
  const state = stateDir(name);
3417
- if (existsSync10(state)) {
3418
- rmSync2(state, { recursive: true, force: true });
3998
+ if (existsSync12(state)) {
3999
+ rmSync3(state, { recursive: true, force: true });
3419
4000
  }
3420
- if (force && existsSync10(dir)) {
3421
- rmSync2(dir, { recursive: true, force: true });
4001
+ if (force && existsSync12(dir)) {
4002
+ rmSync3(dir, { recursive: true, force: true });
3422
4003
  deleteMindUser(name);
3423
4004
  }
3424
4005
  return c.json({ ok: true });
@@ -3427,7 +4008,7 @@ ${user.trimEnd()}
3427
4008
  const entry = findMind(mindName);
3428
4009
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3429
4010
  const dir = mindDir(mindName);
3430
- if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
4011
+ if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3431
4012
  let body = {};
3432
4013
  try {
3433
4014
  body = await c.req.json();
@@ -3436,8 +4017,8 @@ ${user.trimEnd()}
3436
4017
  const template = body.template ?? entry.template ?? "claude";
3437
4018
  const UPGRADE_VARIANT = "upgrade";
3438
4019
  if (body.continue) {
3439
- const worktreeDir2 = resolve13(dir, ".variants", UPGRADE_VARIANT);
3440
- if (!existsSync10(worktreeDir2)) {
4020
+ const worktreeDir2 = resolve16(dir, ".variants", UPGRADE_VARIANT);
4021
+ if (!existsSync12(worktreeDir2)) {
3441
4022
  return c.json({ error: "No upgrade in progress" }, 400);
3442
4023
  }
3443
4024
  const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
@@ -3497,19 +4078,37 @@ ${user.trimEnd()}
3497
4078
  );
3498
4079
  }
3499
4080
  }
3500
- const worktreeDir = resolve13(dir, ".variants", UPGRADE_VARIANT);
3501
- if (existsSync10(worktreeDir)) {
4081
+ const worktreeDir = resolve16(dir, ".variants", UPGRADE_VARIANT);
4082
+ if (existsSync12(worktreeDir)) {
3502
4083
  return c.json(
3503
4084
  { error: "Upgrade variant already exists. Use continue or delete it first." },
3504
4085
  409
3505
4086
  );
3506
4087
  }
4088
+ if (!existsSync12(resolve16(dir, ".git"))) {
4089
+ try {
4090
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dir, "home") } : void 0;
4091
+ await gitExec(["init"], { cwd: dir, mindName, env });
4092
+ await configureGitIdentity(mindName, { cwd: dir, mindName, env });
4093
+ await gitExec(["add", "-A"], { cwd: dir, mindName, env });
4094
+ await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
4095
+ chownMindDir(dir, mindName);
4096
+ } catch (err) {
4097
+ rmSync3(resolve16(dir, ".git"), { recursive: true, force: true });
4098
+ return c.json(
4099
+ {
4100
+ error: `Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
4101
+ },
4102
+ 500
4103
+ );
4104
+ }
4105
+ }
3507
4106
  await gitExec(["worktree", "prune"], { cwd: dir });
3508
4107
  try {
3509
4108
  await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
3510
4109
  } catch {
3511
4110
  }
3512
- if (!existsSync10(resolve13(dir, "home", "shared"))) {
4111
+ if (!existsSync12(resolve16(dir, "home", "shared"))) {
3513
4112
  try {
3514
4113
  await addSharedWorktree(mindName, dir);
3515
4114
  } catch (err) {
@@ -3520,9 +4119,9 @@ ${user.trimEnd()}
3520
4119
  }
3521
4120
  }
3522
4121
  await updateTemplateBranch(dir, template, mindName);
3523
- const parentDir = resolve13(dir, ".variants");
3524
- if (!existsSync10(parentDir)) {
3525
- mkdirSync7(parentDir, { recursive: true });
4122
+ const parentDir = resolve16(dir, ".variants");
4123
+ if (!existsSync12(parentDir)) {
4124
+ mkdirSync8(parentDir, { recursive: true });
3526
4125
  }
3527
4126
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
3528
4127
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -3704,7 +4303,79 @@ ${user.trimEnd()}
3704
4303
  const usage = getTokenBudget().getUsage(baseName);
3705
4304
  if (!usage) return c.json({ error: "No budget configured" }, 404);
3706
4305
  return c.json(usage);
3707
- }).get("/:name/delivery/pending", async (c) => {
4306
+ }).get("/:name/config", (c) => {
4307
+ const name = c.req.param("name");
4308
+ const entry = findMind(name);
4309
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4310
+ const dir = mindDir(name);
4311
+ if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
4312
+ let config = readVoluteConfig(dir);
4313
+ if (!config && entry.template === "pi") {
4314
+ const piConfigPath = resolve16(dir, "home/.config/config.json");
4315
+ if (existsSync12(piConfigPath)) {
4316
+ try {
4317
+ config = JSON.parse(readFileSync11(piConfigPath, "utf-8"));
4318
+ } catch {
4319
+ }
4320
+ }
4321
+ }
4322
+ return c.json({
4323
+ registry: {
4324
+ name: entry.name,
4325
+ port: entry.port,
4326
+ created: entry.created,
4327
+ stage: entry.stage,
4328
+ template: entry.template
4329
+ },
4330
+ config: {
4331
+ model: config?.model ?? null,
4332
+ thinkingLevel: config?.thinkingLevel ?? null,
4333
+ tokenBudget: config?.tokenBudget ?? null,
4334
+ tokenBudgetPeriodMinutes: config?.tokenBudgetPeriodMinutes ?? null
4335
+ }
4336
+ });
4337
+ }).put(
4338
+ "/:name/config",
4339
+ requireAdmin,
4340
+ zValidator3(
4341
+ "json",
4342
+ z3.object({
4343
+ model: z3.string().optional(),
4344
+ thinkingLevel: z3.enum(["off", "minimal", "low", "medium", "high", "xhigh"]).optional(),
4345
+ tokenBudget: z3.number().int().positive().nullable().optional(),
4346
+ tokenBudgetPeriodMinutes: z3.number().int().positive().nullable().optional()
4347
+ })
4348
+ ),
4349
+ async (c) => {
4350
+ const name = c.req.param("name");
4351
+ const entry = findMind(name);
4352
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4353
+ const dir = mindDir(name);
4354
+ if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
4355
+ const body = c.req.valid("json");
4356
+ const existing = readVoluteConfig(dir) ?? {};
4357
+ if (body.model !== void 0) existing.model = body.model;
4358
+ if (body.thinkingLevel !== void 0) {
4359
+ existing.thinkingLevel = body.thinkingLevel;
4360
+ }
4361
+ if (body.tokenBudget !== void 0) {
4362
+ if (body.tokenBudget === null) {
4363
+ delete existing.tokenBudget;
4364
+ } else {
4365
+ existing.tokenBudget = body.tokenBudget;
4366
+ }
4367
+ }
4368
+ if (body.tokenBudgetPeriodMinutes !== void 0) {
4369
+ if (body.tokenBudgetPeriodMinutes === null) {
4370
+ delete existing.tokenBudgetPeriodMinutes;
4371
+ } else {
4372
+ existing.tokenBudgetPeriodMinutes = body.tokenBudgetPeriodMinutes;
4373
+ }
4374
+ }
4375
+ writeVoluteConfig(dir, existing);
4376
+ return c.json({ ok: true });
4377
+ }
4378
+ ).get("/:name/delivery/pending", async (c) => {
3708
4379
  const name = c.req.param("name");
3709
4380
  const [baseName] = name.split("@", 2);
3710
4381
  try {
@@ -3743,7 +4414,7 @@ ${user.trimEnd()}
3743
4414
  } catch (err) {
3744
4415
  logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
3745
4416
  }
3746
- publish2(baseName, {
4417
+ publish3(baseName, {
3747
4418
  mind: baseName,
3748
4419
  type: body.type,
3749
4420
  session: body.session,
@@ -3752,15 +4423,17 @@ ${user.trimEnd()}
3752
4423
  content: body.content,
3753
4424
  metadata: body.metadata
3754
4425
  });
4426
+ onMindEvent(baseName, body.type, body.channel);
3755
4427
  if ((body.type === "text" || body.type === "outbound") && body.channel) {
3756
- getTypingMap().delete(body.channel, baseName);
4428
+ const map = getTypingMap();
4429
+ const affected = map.deleteSender(baseName);
4430
+ publishTypingForChannels(affected, map);
3757
4431
  }
3758
4432
  if (body.type === "done") {
3759
- if (body.channel) {
3760
- getTypingMap().delete(body.channel, baseName);
3761
- } else {
3762
- getTypingMap().deleteSender(baseName);
3763
- }
4433
+ const map = getTypingMap();
4434
+ const affected = map.deleteSender(baseName);
4435
+ publishTypingForChannels(affected, map);
4436
+ broadcast({ type: "mind_done", mind: baseName, summary: "Finished processing" });
3764
4437
  try {
3765
4438
  getDeliveryManager().sessionDone(baseName, body.session);
3766
4439
  } catch (err) {
@@ -3791,7 +4464,7 @@ ${user.trimEnd()}
3791
4464
 
3792
4465
  `));
3793
4466
  };
3794
- const unsubscribe = subscribe2(baseName, (event) => {
4467
+ const unsubscribe = subscribe3(baseName, (event) => {
3795
4468
  if (typeFilter && !typeFilter.includes(event.type)) return;
3796
4469
  if (sessionFilter && event.session !== sessionFilter) return;
3797
4470
  if (channelFilter && event.channel !== channelFilter) return;
@@ -3873,15 +4546,15 @@ ${user.trimEnd()}
3873
4546
  if (!full) {
3874
4547
  conditions.push(sql2`${mindHistory.type} IN ('inbound', 'outbound')`);
3875
4548
  }
3876
- const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
4549
+ const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc3(mindHistory.created_at)).limit(limit).offset(offset);
3877
4550
  return c.json(rows);
3878
4551
  });
3879
- var minds_default = app9;
4552
+ var minds_default = app11;
3880
4553
 
3881
4554
  // src/web/api/pages.ts
3882
4555
  import { readFile as readFile2, stat } from "fs/promises";
3883
- import { extname, resolve as resolve14 } from "path";
3884
- import { Hono as Hono10 } from "hono";
4556
+ import { extname, resolve as resolve17 } from "path";
4557
+ import { Hono as Hono12 } from "hono";
3885
4558
  var MIME_TYPES = {
3886
4559
  ".html": "text/html",
3887
4560
  ".js": "application/javascript",
@@ -3898,16 +4571,21 @@ var MIME_TYPES = {
3898
4571
  ".txt": "text/plain",
3899
4572
  ".xml": "application/xml"
3900
4573
  };
3901
- var app10 = new Hono10().get("/:name/*", async (c) => {
4574
+ var app12 = new Hono12().get("/:name/*", async (c) => {
3902
4575
  const name = c.req.param("name");
3903
- if (!findMind(name)) return c.text("Not found", 404);
3904
- const pagesRoot = resolve14(mindDir(name), "home", "pages");
4576
+ let pagesRoot;
4577
+ if (name === "_system") {
4578
+ pagesRoot = resolve17(voluteHome(), "shared", "pages");
4579
+ } else {
4580
+ if (!findMind(name)) return c.text("Not found", 404);
4581
+ pagesRoot = resolve17(mindDir(name), "home", "pages");
4582
+ }
3905
4583
  const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
3906
- const requestedPath = resolve14(pagesRoot, wildcard.slice(1));
4584
+ const requestedPath = resolve17(pagesRoot, wildcard.slice(1));
3907
4585
  if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
3908
4586
  let fileStat = await stat(requestedPath).catch(() => null);
3909
4587
  if (fileStat?.isDirectory()) {
3910
- const indexPath = resolve14(requestedPath, "index.html");
4588
+ const indexPath = resolve17(requestedPath, "index.html");
3911
4589
  fileStat = await stat(indexPath).catch(() => null);
3912
4590
  if (fileStat?.isFile()) {
3913
4591
  const body = await readFile2(indexPath);
@@ -3923,14 +4601,14 @@ var app10 = new Hono10().get("/:name/*", async (c) => {
3923
4601
  }
3924
4602
  return c.text("Not found", 404);
3925
4603
  });
3926
- var pages_default = app10;
4604
+ var pages_default = app12;
3927
4605
 
3928
4606
  // src/web/api/prompts.ts
3929
4607
  import { zValidator as zValidator4 } from "@hono/zod-validator";
3930
4608
  import { eq as eq5, sql as sql3 } from "drizzle-orm";
3931
- import { Hono as Hono11 } from "hono";
4609
+ import { Hono as Hono13 } from "hono";
3932
4610
  import { z as z4 } from "zod";
3933
- var app11 = new Hono11().get("/", async (c) => {
4611
+ var app13 = new Hono13().get("/", async (c) => {
3934
4612
  let rows;
3935
4613
  try {
3936
4614
  const db = await getDb();
@@ -3974,10 +4652,12 @@ var app11 = new Hono11().get("/", async (c) => {
3974
4652
  await db.delete(systemPrompts).where(eq5(systemPrompts.key, key));
3975
4653
  return c.json({ ok: true });
3976
4654
  });
3977
- var prompts_default = app11;
4655
+ var prompts_default = app13;
3978
4656
 
3979
4657
  // src/web/api/schedules.ts
3980
- import { Hono as Hono12 } from "hono";
4658
+ import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
4659
+ import { Hono as Hono14 } from "hono";
4660
+ var slog2 = logger_default.child("schedules");
3981
4661
  function readSchedules(name) {
3982
4662
  return readVoluteConfig(mindDir(name))?.schedules ?? [];
3983
4663
  }
@@ -3988,7 +4668,7 @@ function writeSchedules(name, schedules) {
3988
4668
  writeVoluteConfig(dir, config);
3989
4669
  getScheduler().loadSchedules(name);
3990
4670
  }
3991
- var app12 = new Hono12().get("/:name/schedules", (c) => {
4671
+ var app14 = new Hono14().get("/:name/schedules", (c) => {
3992
4672
  const name = c.req.param("name");
3993
4673
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
3994
4674
  return c.json(readSchedules(name));
@@ -3999,15 +4679,29 @@ var app12 = new Hono12().get("/:name/schedules", (c) => {
3999
4679
  if (entry.stage === "seed")
4000
4680
  return c.json({ error: "Seed minds cannot use schedules \u2014 sprout first" }, 403);
4001
4681
  const body = await c.req.json();
4002
- if (!body.cron || !body.message) {
4003
- return c.json({ error: "cron and message are required" }, 400);
4682
+ if (!body.cron) {
4683
+ return c.json({ error: "cron is required" }, 400);
4684
+ }
4685
+ if (!body.message && !body.script) {
4686
+ return c.json({ error: "message or script is required" }, 400);
4687
+ }
4688
+ if (body.message && body.script) {
4689
+ return c.json({ error: "message and script are mutually exclusive" }, 400);
4690
+ }
4691
+ try {
4692
+ CronExpressionParser2.parse(body.cron);
4693
+ } catch {
4694
+ return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
4004
4695
  }
4005
4696
  const schedules = readSchedules(name);
4006
4697
  const id = body.id || `schedule-${Date.now()}`;
4007
4698
  if (schedules.some((s) => s.id === id)) {
4008
4699
  return c.json({ error: `Schedule "${id}" already exists` }, 409);
4009
4700
  }
4010
- schedules.push({ id, cron: body.cron, message: body.message, enabled: body.enabled ?? true });
4701
+ const schedule = { id, cron: body.cron, enabled: body.enabled ?? true };
4702
+ if (body.message) schedule.message = body.message;
4703
+ if (body.script) schedule.script = body.script;
4704
+ schedules.push(schedule);
4011
4705
  writeSchedules(name, schedules);
4012
4706
  return c.json({ ok: true, id }, 201);
4013
4707
  }).put("/:name/schedules/:id", requireAdmin, async (c) => {
@@ -4018,8 +4712,25 @@ var app12 = new Hono12().get("/:name/schedules", (c) => {
4018
4712
  const idx = schedules.findIndex((s) => s.id === id);
4019
4713
  if (idx === -1) return c.json({ error: "Schedule not found" }, 404);
4020
4714
  const body = await c.req.json();
4021
- if (body.cron !== void 0) schedules[idx].cron = body.cron;
4022
- if (body.message !== void 0) schedules[idx].message = body.message;
4715
+ if (body.message && body.script) {
4716
+ return c.json({ error: "message and script are mutually exclusive" }, 400);
4717
+ }
4718
+ if (body.cron !== void 0) {
4719
+ try {
4720
+ CronExpressionParser2.parse(body.cron);
4721
+ } catch {
4722
+ return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
4723
+ }
4724
+ schedules[idx].cron = body.cron;
4725
+ }
4726
+ if (body.message !== void 0) {
4727
+ schedules[idx].message = body.message;
4728
+ delete schedules[idx].script;
4729
+ }
4730
+ if (body.script !== void 0) {
4731
+ schedules[idx].script = body.script;
4732
+ delete schedules[idx].message;
4733
+ }
4023
4734
  if (body.enabled !== void 0) schedules[idx].enabled = body.enabled;
4024
4735
  writeSchedules(name, schedules);
4025
4736
  return c.json({ ok: true });
@@ -4055,15 +4766,16 @@ var app12 = new Hono12().get("/:name/schedules", (c) => {
4055
4766
  return c.json({ error: `Mind responded with ${res.status}` }, 502);
4056
4767
  }
4057
4768
  return c.json({ ok: true });
4058
- } catch {
4769
+ } catch (err) {
4770
+ slog2.warn(`webhook delivery failed for ${name}`, logger_default.errorData(err));
4059
4771
  return c.json({ error: "Failed to reach mind" }, 502);
4060
4772
  }
4061
4773
  });
4062
- var schedules_default = app12;
4774
+ var schedules_default = app14;
4063
4775
 
4064
4776
  // src/web/api/shared.ts
4065
- import { Hono as Hono13 } from "hono";
4066
- var app13 = new Hono13().post("/:name/shared/merge", requireAdmin, async (c) => {
4777
+ import { Hono as Hono15 } from "hono";
4778
+ var app15 = new Hono15().post("/:name/shared/merge", requireAdmin, async (c) => {
4067
4779
  const name = c.req.param("name");
4068
4780
  const entry = findMind(name);
4069
4781
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -4112,22 +4824,22 @@ var app13 = new Hono13().post("/:name/shared/merge", requireAdmin, async (c) =>
4112
4824
  return c.json({ error: err instanceof Error ? err.message : "Failed to get status" }, 500);
4113
4825
  }
4114
4826
  });
4115
- var shared_default = app13;
4827
+ var shared_default = app15;
4116
4828
 
4117
4829
  // src/web/api/skills.ts
4118
- import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
4830
+ import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync4 } from "fs";
4119
4831
  import { tmpdir as tmpdir2 } from "os";
4120
- import { join as join3, resolve as resolve15 } from "path";
4832
+ import { join as join4, resolve as resolve18 } from "path";
4121
4833
  import AdmZip from "adm-zip";
4122
- import { Hono as Hono14 } from "hono";
4123
- var app14 = new Hono14().get("/", async (c) => {
4834
+ import { Hono as Hono16 } from "hono";
4835
+ var app16 = new Hono16().get("/", async (c) => {
4124
4836
  const skills = await listSharedSkills();
4125
4837
  return c.json(skills);
4126
4838
  }).get("/:id", async (c) => {
4127
4839
  const id = c.req.param("id");
4128
4840
  const skill = await getSharedSkill(id);
4129
4841
  if (!skill) return c.json({ error: "Skill not found" }, 404);
4130
- const dir = join3(sharedSkillsDir(), id);
4842
+ const dir = join4(sharedSkillsDir(), id);
4131
4843
  const files = listFilesRecursive(dir);
4132
4844
  return c.json({ ...skill, files });
4133
4845
  }).post("/upload", requireAdmin, async (c) => {
@@ -4140,24 +4852,24 @@ var app14 = new Hono14().get("/", async (c) => {
4140
4852
  return c.json({ error: "Only .zip files are accepted" }, 400);
4141
4853
  }
4142
4854
  const buffer = Buffer.from(await file.arrayBuffer());
4143
- const tmpDir = mkdtempSync(join3(tmpdir2(), "volute-skill-upload-"));
4855
+ const tmpDir = mkdtempSync(join4(tmpdir2(), "volute-skill-upload-"));
4144
4856
  try {
4145
4857
  const zip = new AdmZip(buffer);
4146
4858
  for (const entry of zip.getEntries()) {
4147
- const target = resolve15(tmpDir, entry.entryName);
4859
+ const target = resolve18(tmpDir, entry.entryName);
4148
4860
  if (!target.startsWith(tmpDir)) {
4149
4861
  return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
4150
4862
  }
4151
4863
  }
4152
4864
  zip.extractAllTo(tmpDir, true);
4153
4865
  let skillDir = null;
4154
- if (existsSync11(join3(tmpDir, "SKILL.md"))) {
4866
+ if (existsSync13(join4(tmpDir, "SKILL.md"))) {
4155
4867
  skillDir = tmpDir;
4156
4868
  } else {
4157
- const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4869
+ const entries = readdirSync7(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4158
4870
  for (const entry of entries) {
4159
- if (existsSync11(join3(tmpDir, entry.name, "SKILL.md"))) {
4160
- skillDir = join3(tmpDir, entry.name);
4871
+ if (existsSync13(join4(tmpDir, entry.name, "SKILL.md"))) {
4872
+ skillDir = join4(tmpDir, entry.name);
4161
4873
  break;
4162
4874
  }
4163
4875
  }
@@ -4173,7 +4885,7 @@ var app14 = new Hono14().get("/", async (c) => {
4173
4885
  }
4174
4886
  throw e;
4175
4887
  } finally {
4176
- rmSync3(tmpDir, { recursive: true, force: true });
4888
+ rmSync4(tmpDir, { recursive: true, force: true });
4177
4889
  }
4178
4890
  }).delete("/:id", requireAdmin, async (c) => {
4179
4891
  const id = c.req.param("id");
@@ -4185,12 +4897,12 @@ var app14 = new Hono14().get("/", async (c) => {
4185
4897
  }
4186
4898
  return c.json({ ok: true });
4187
4899
  });
4188
- var skills_default = app14;
4900
+ var skills_default = app16;
4189
4901
 
4190
4902
  // src/web/api/system.ts
4191
- import { Hono as Hono15 } from "hono";
4192
- import { streamSSE as streamSSE2 } from "hono/streaming";
4193
- var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
4903
+ import { Hono as Hono17 } from "hono";
4904
+ import { streamSSE as streamSSE3 } from "hono/streaming";
4905
+ var app17 = new Hono17().post("/restart", requireAdmin, (c) => {
4194
4906
  setTimeout(() => process.exit(1), 200);
4195
4907
  return c.json({ ok: true });
4196
4908
  }).post("/stop", requireAdmin, (c) => {
@@ -4199,7 +4911,7 @@ var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
4199
4911
  }).get("/logs", async (c) => {
4200
4912
  const user = c.get("user");
4201
4913
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
4202
- return streamSSE2(c, async (stream) => {
4914
+ return streamSSE3(c, async (stream) => {
4203
4915
  for (const entry of logBuffer.getEntries()) {
4204
4916
  await stream.writeSSE({ data: JSON.stringify(entry) });
4205
4917
  }
@@ -4207,10 +4919,10 @@ var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
4207
4919
  stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
4208
4920
  });
4209
4921
  });
4210
- await new Promise((resolve20) => {
4922
+ await new Promise((resolve23) => {
4211
4923
  stream.onAbort(() => {
4212
4924
  unsubscribe();
4213
- resolve20();
4925
+ resolve23();
4214
4926
  });
4215
4927
  });
4216
4928
  });
@@ -4218,18 +4930,18 @@ var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
4218
4930
  const config = readSystemsConfig();
4219
4931
  return c.json({ system: config?.system ?? null });
4220
4932
  });
4221
- var system_default = app15;
4933
+ var system_default = app17;
4222
4934
 
4223
4935
  // src/web/api/typing.ts
4224
4936
  import { zValidator as zValidator5 } from "@hono/zod-validator";
4225
- import { Hono as Hono16 } from "hono";
4937
+ import { Hono as Hono18 } from "hono";
4226
4938
  import { z as z5 } from "zod";
4227
4939
  var typingSchema = z5.object({
4228
4940
  channel: z5.string().min(1),
4229
4941
  sender: z5.string().min(1),
4230
4942
  active: z5.boolean()
4231
4943
  });
4232
- var app16 = new Hono16().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
4944
+ var app18 = new Hono18().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
4233
4945
  const { channel, sender, active } = c.req.valid("json");
4234
4946
  const map = getTypingMap();
4235
4947
  if (active) {
@@ -4237,6 +4949,11 @@ var app16 = new Hono16().post("/:name/typing", zValidator5("json", typingSchema)
4237
4949
  } else {
4238
4950
  map.delete(channel, sender);
4239
4951
  }
4952
+ const volutePrefix = "volute:";
4953
+ if (channel.startsWith(volutePrefix)) {
4954
+ const conversationId = channel.slice(volutePrefix.length);
4955
+ publish(conversationId, { type: "typing", senders: map.get(channel) });
4956
+ }
4240
4957
  return c.json({ ok: true });
4241
4958
  }).get("/:name/typing", (c) => {
4242
4959
  const channel = c.req.query("channel");
@@ -4246,13 +4963,13 @@ var app16 = new Hono16().post("/:name/typing", zValidator5("json", typingSchema)
4246
4963
  const map = getTypingMap();
4247
4964
  return c.json({ typing: map.get(channel) });
4248
4965
  });
4249
- var typing_default = app16;
4966
+ var typing_default = app18;
4250
4967
 
4251
4968
  // src/web/api/update.ts
4252
4969
  import { spawn as spawn3 } from "child_process";
4253
- import { Hono as Hono17 } from "hono";
4970
+ import { Hono as Hono19 } from "hono";
4254
4971
  var bin;
4255
- var app17 = new Hono17().get("/update", async (c) => {
4972
+ var app19 = new Hono19().get("/update", async (c) => {
4256
4973
  const result = await checkForUpdate();
4257
4974
  return c.json(result);
4258
4975
  }).post("/update", requireAdmin, async (c) => {
@@ -4267,19 +4984,19 @@ var app17 = new Hono17().get("/update", async (c) => {
4267
4984
  child.unref();
4268
4985
  return c.json({ ok: true, message: "Updating..." });
4269
4986
  });
4270
- var update_default = app17;
4987
+ var update_default = app19;
4271
4988
 
4272
4989
  // src/web/api/variants.ts
4273
- import { existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
4274
- import { resolve as resolve17 } from "path";
4275
- import { Hono as Hono18 } from "hono";
4990
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
4991
+ import { resolve as resolve20 } from "path";
4992
+ import { Hono as Hono20 } from "hono";
4276
4993
 
4277
4994
  // src/lib/spawn-server.ts
4278
4995
  import { spawn as spawn4 } from "child_process";
4279
- import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync10 } from "fs";
4280
- import { resolve as resolve16 } from "path";
4996
+ import { closeSync, mkdirSync as mkdirSync9, openSync, readFileSync as readFileSync12 } from "fs";
4997
+ import { resolve as resolve19 } from "path";
4281
4998
  function tsxBin(cwd) {
4282
- return resolve16(cwd, "node_modules", ".bin", "tsx");
4999
+ return resolve19(cwd, "node_modules", ".bin", "tsx");
4283
5000
  }
4284
5001
  function spawnServer(cwd, port, options) {
4285
5002
  if (options?.detached) {
@@ -4292,31 +5009,31 @@ function spawnAttached(cwd, port) {
4292
5009
  cwd,
4293
5010
  stdio: ["ignore", "pipe", "pipe"]
4294
5011
  });
4295
- return new Promise((resolve20) => {
4296
- const timeout = setTimeout(() => resolve20(null), 3e4);
5012
+ return new Promise((resolve23) => {
5013
+ const timeout = setTimeout(() => resolve23(null), 3e4);
4297
5014
  function checkOutput(data) {
4298
5015
  const match = data.toString().match(/listening on :(\d+)/);
4299
5016
  if (match) {
4300
5017
  clearTimeout(timeout);
4301
- resolve20({ child, actualPort: parseInt(match[1], 10) });
5018
+ resolve23({ child, actualPort: parseInt(match[1], 10) });
4302
5019
  }
4303
5020
  }
4304
5021
  child.stdout?.on("data", checkOutput);
4305
5022
  child.stderr?.on("data", checkOutput);
4306
5023
  child.on("error", () => {
4307
5024
  clearTimeout(timeout);
4308
- resolve20(null);
5025
+ resolve23(null);
4309
5026
  });
4310
5027
  child.on("exit", () => {
4311
5028
  clearTimeout(timeout);
4312
- resolve20(null);
5029
+ resolve23(null);
4313
5030
  });
4314
5031
  });
4315
5032
  }
4316
5033
  function spawnDetached(cwd, port, logDir) {
4317
- const logsDir = logDir ?? resolve16(cwd, ".mind", "logs");
4318
- mkdirSync8(logsDir, { recursive: true });
4319
- const logPath = resolve16(logsDir, "mind.log");
5034
+ const logsDir = logDir ?? resolve19(cwd, ".mind", "logs");
5035
+ mkdirSync9(logsDir, { recursive: true });
5036
+ const logPath = resolve19(logsDir, "mind.log");
4320
5037
  const logFd = openSync(logPath, "a");
4321
5038
  const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
4322
5039
  cwd,
@@ -4336,7 +5053,7 @@ function spawnDetached(cwd, port, logDir) {
4336
5053
  }
4337
5054
  const interval = setInterval(() => {
4338
5055
  try {
4339
- const content = readFileSync10(logPath, "utf-8");
5056
+ const content = readFileSync12(logPath, "utf-8");
4340
5057
  const match = content.match(/listening on :(\d+)/);
4341
5058
  if (match) {
4342
5059
  finish({ child, actualPort: parseInt(match[1], 10) });
@@ -4388,7 +5105,7 @@ async function verify2(port) {
4388
5105
  }
4389
5106
 
4390
5107
  // src/web/api/variants.ts
4391
- var app18 = new Hono18().get("/:name/variants", async (c) => {
5108
+ var app20 = new Hono20().get("/:name/variants", async (c) => {
4392
5109
  const name = c.req.param("name");
4393
5110
  const entry = findMind(name);
4394
5111
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -4418,11 +5135,11 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4418
5135
  const err = validateBranchName(variantName);
4419
5136
  if (err) return c.json({ error: err }, 400);
4420
5137
  const projectRoot = mindDir(mindName);
4421
- const variantDir = resolve17(projectRoot, ".variants", variantName);
4422
- if (existsSync12(variantDir)) {
5138
+ const variantDir = resolve20(projectRoot, ".variants", variantName);
5139
+ if (existsSync14(variantDir)) {
4423
5140
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
4424
5141
  }
4425
- mkdirSync9(resolve17(projectRoot, ".variants"), { recursive: true });
5142
+ mkdirSync10(resolve20(projectRoot, ".variants"), { recursive: true });
4426
5143
  try {
4427
5144
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
4428
5145
  } catch (e) {
@@ -4435,7 +5152,7 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4435
5152
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4436
5153
  await exec(cmd, args, {
4437
5154
  cwd: variantDir,
4438
- env: { ...process.env, HOME: resolve17(variantDir, "home") }
5155
+ env: { ...process.env, HOME: resolve20(variantDir, "home") }
4439
5156
  });
4440
5157
  } else {
4441
5158
  await exec("npm", ["install"], { cwd: variantDir });
@@ -4445,7 +5162,7 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4445
5162
  return c.json({ error: `npm install failed: ${msg}` }, 500);
4446
5163
  }
4447
5164
  if (body.soul) {
4448
- writeFileSync9(resolve17(variantDir, "home/SOUL.md"), body.soul);
5165
+ writeFileSync10(resolve20(variantDir, "home/SOUL.md"), body.soul);
4449
5166
  }
4450
5167
  const variantPort = body.port ?? nextPort();
4451
5168
  const variant = {
@@ -4483,7 +5200,7 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4483
5200
  } catch {
4484
5201
  }
4485
5202
  const projectRoot = mindDir(mindName);
4486
- if (existsSync12(variant.path)) {
5203
+ if (existsSync14(variant.path)) {
4487
5204
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
4488
5205
  if (status) {
4489
5206
  try {
@@ -4540,7 +5257,7 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4540
5257
  } catch (e) {
4541
5258
  return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
4542
5259
  }
4543
- if (existsSync12(variant.path)) {
5260
+ if (existsSync14(variant.path)) {
4544
5261
  try {
4545
5262
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4546
5263
  } catch {
@@ -4557,7 +5274,7 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4557
5274
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4558
5275
  await exec(cmd, args, {
4559
5276
  cwd: projectRoot,
4560
- env: { ...process.env, HOME: resolve17(projectRoot, "home") }
5277
+ env: { ...process.env, HOME: resolve20(projectRoot, "home") }
4561
5278
  });
4562
5279
  } else {
4563
5280
  await exec("npm", ["install"], { cwd: projectRoot });
@@ -4600,7 +5317,7 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4600
5317
  } catch {
4601
5318
  }
4602
5319
  }
4603
- if (existsSync12(variant.path)) {
5320
+ if (existsSync14(variant.path)) {
4604
5321
  try {
4605
5322
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4606
5323
  } catch {
@@ -4614,11 +5331,11 @@ var app18 = new Hono18().get("/:name/variants", async (c) => {
4614
5331
  chownMindDir(projectRoot, mindName);
4615
5332
  return c.json({ ok: true });
4616
5333
  });
4617
- var variants_default = app18;
5334
+ var variants_default = app20;
4618
5335
 
4619
5336
  // src/web/api/volute/channels.ts
4620
5337
  import { zValidator as zValidator6 } from "@hono/zod-validator";
4621
- import { Hono as Hono19 } from "hono";
5338
+ import { Hono as Hono21 } from "hono";
4622
5339
  import { z as z6 } from "zod";
4623
5340
  var createSchema = z6.object({
4624
5341
  name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
@@ -4626,7 +5343,7 @@ var createSchema = z6.object({
4626
5343
  var inviteSchema = z6.object({
4627
5344
  username: z6.string().min(1)
4628
5345
  });
4629
- var app19 = new Hono19().get("/", async (c) => {
5346
+ var app21 = new Hono21().get("/", async (c) => {
4630
5347
  const user = c.get("user");
4631
5348
  const channels = await listChannels();
4632
5349
  const results = await Promise.all(
@@ -4690,12 +5407,12 @@ var app19 = new Hono19().get("/", async (c) => {
4690
5407
  ]);
4691
5408
  return c.json({ ok: true });
4692
5409
  });
4693
- var channels_default2 = app19;
5410
+ var channels_default2 = app21;
4694
5411
 
4695
5412
  // src/web/api/volute/chat.ts
4696
5413
  import { zValidator as zValidator7 } from "@hono/zod-validator";
4697
- import { Hono as Hono20 } from "hono";
4698
- import { streamSSE as streamSSE3 } from "hono/streaming";
5414
+ import { Hono as Hono22 } from "hono";
5415
+ import { streamSSE as streamSSE4 } from "hono/streaming";
4699
5416
  import { z as z7 } from "zod";
4700
5417
  async function fanOutToMinds(opts) {
4701
5418
  const participants = await getParticipants(opts.conversationId);
@@ -4703,7 +5420,7 @@ async function fanOutToMinds(opts) {
4703
5420
  const participantNames = participants.map((p) => p.username);
4704
5421
  const isDM = opts.isDM ?? participants.length === 2;
4705
5422
  const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
4706
- const { getMindManager: getMindManager2 } = await import("./mind-manager-RVCFROAY.js");
5423
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-3DMYKZPB.js");
4707
5424
  const manager = getMindManager2();
4708
5425
  const runningMinds = mindParticipants.map((ap) => {
4709
5426
  const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
@@ -4735,7 +5452,7 @@ async function fanOutToMinds(opts) {
4735
5452
  const target = opts.targetName ? opts.targetName(mindName) : mindName;
4736
5453
  const channel = slugForMind(mindName);
4737
5454
  const typingMap = getTypingMap();
4738
- const currentlyTyping = typingMap.get(channel);
5455
+ const currentlyTyping = typingMap.get(channel).filter((name) => participantNames.includes(name));
4739
5456
  deliverMessage(target, {
4740
5457
  content: opts.contentBlocks,
4741
5458
  channel,
@@ -4760,7 +5477,7 @@ var chatSchema = z7.object({
4760
5477
  })
4761
5478
  ).optional()
4762
5479
  });
4763
- var app20 = new Hono20().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
5480
+ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
4764
5481
  const name = c.req.param("name");
4765
5482
  const [baseName] = name.split("@", 2);
4766
5483
  const entry = findMind(baseName);
@@ -4832,7 +5549,7 @@ var app20 = new Hono20().post("/:name/chat", zValidator7("json", chatSchema), as
4832
5549
  if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
4833
5550
  return c.json({ error: "Conversation not found" }, 404);
4834
5551
  }
4835
- return streamSSE3(c, async (stream) => {
5552
+ return streamSSE4(c, async (stream) => {
4836
5553
  const unsubscribe = subscribe(conversationId, (event) => {
4837
5554
  stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
4838
5555
  if (!stream.aborted) console.error("[chat] SSE write error:", err);
@@ -4843,11 +5560,11 @@ var app20 = new Hono20().post("/:name/chat", zValidator7("json", chatSchema), as
4843
5560
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
4844
5561
  });
4845
5562
  }, 15e3);
4846
- await new Promise((resolve20) => {
5563
+ await new Promise((resolve23) => {
4847
5564
  stream.onAbort(() => {
4848
5565
  unsubscribe();
4849
5566
  clearInterval(keepAlive);
4850
- resolve20();
5567
+ resolve23();
4851
5568
  });
4852
5569
  });
4853
5570
  });
@@ -4857,7 +5574,7 @@ var unifiedChatSchema = z7.object({
4857
5574
  conversationId: z7.string(),
4858
5575
  images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
4859
5576
  });
4860
- var unifiedChatApp = new Hono20().post(
5577
+ var unifiedChatApp = new Hono22().post(
4861
5578
  "/chat",
4862
5579
  zValidator7("json", unifiedChatSchema),
4863
5580
  async (c) => {
@@ -4893,18 +5610,18 @@ var unifiedChatApp = new Hono20().post(
4893
5610
  return c.json({ ok: true, conversationId: body.conversationId });
4894
5611
  }
4895
5612
  );
4896
- var chat_default = app20;
5613
+ var chat_default = app22;
4897
5614
 
4898
5615
  // src/web/api/volute/conversations.ts
4899
5616
  import { zValidator as zValidator8 } from "@hono/zod-validator";
4900
- import { Hono as Hono21 } from "hono";
5617
+ import { Hono as Hono23 } from "hono";
4901
5618
  import { z as z8 } from "zod";
4902
5619
  var createConvSchema = z8.object({
4903
5620
  title: z8.string().optional(),
4904
5621
  participantIds: z8.array(z8.number()).optional(),
4905
5622
  participantNames: z8.array(z8.string()).optional()
4906
5623
  });
4907
- var app21 = new Hono21().get("/:name/conversations", async (c) => {
5624
+ var app23 = new Hono23().get("/:name/conversations", async (c) => {
4908
5625
  const name = c.req.param("name");
4909
5626
  const user = c.get("user");
4910
5627
  let lookupId = user.id;
@@ -4989,18 +5706,18 @@ var app21 = new Hono21().get("/:name/conversations", async (c) => {
4989
5706
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4990
5707
  return c.json({ ok: true });
4991
5708
  });
4992
- var conversations_default = app21;
5709
+ var conversations_default = app23;
4993
5710
 
4994
5711
  // src/web/api/volute/user-conversations.ts
4995
5712
  import { zValidator as zValidator9 } from "@hono/zod-validator";
4996
- import { Hono as Hono22 } from "hono";
4997
- import { streamSSE as streamSSE4 } from "hono/streaming";
5713
+ import { Hono as Hono24 } from "hono";
5714
+ import { streamSSE as streamSSE5 } from "hono/streaming";
4998
5715
  import { z as z9 } from "zod";
4999
5716
  var createSchema2 = z9.object({
5000
5717
  title: z9.string().optional(),
5001
5718
  participantNames: z9.array(z9.string()).min(1)
5002
5719
  });
5003
- var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
5720
+ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
5004
5721
  const user = c.get("user");
5005
5722
  const convs = await listConversationsWithParticipants(user.id);
5006
5723
  return c.json(convs);
@@ -5048,7 +5765,7 @@ var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
5048
5765
  if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
5049
5766
  return c.json({ error: "Conversation not found" }, 404);
5050
5767
  }
5051
- return streamSSE4(c, async (stream) => {
5768
+ return streamSSE5(c, async (stream) => {
5052
5769
  const unsubscribe = subscribe(conversationId, (event) => {
5053
5770
  stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
5054
5771
  if (!stream.aborted) console.error("[chat] SSE write error:", err);
@@ -5059,11 +5776,11 @@ var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
5059
5776
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
5060
5777
  });
5061
5778
  }, 15e3);
5062
- await new Promise((resolve20) => {
5779
+ await new Promise((resolve23) => {
5063
5780
  stream.onAbort(() => {
5064
5781
  unsubscribe();
5065
5782
  clearInterval(keepAlive);
5066
- resolve20();
5783
+ resolve23();
5067
5784
  });
5068
5785
  });
5069
5786
  });
@@ -5074,12 +5791,12 @@ var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
5074
5791
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
5075
5792
  return c.json({ ok: true });
5076
5793
  });
5077
- var user_conversations_default = app22;
5794
+ var user_conversations_default = app24;
5078
5795
 
5079
5796
  // src/web/app.ts
5080
5797
  var httpLog = logger_default.child("http");
5081
- var app23 = new Hono23();
5082
- app23.onError((err, c) => {
5798
+ var app25 = new Hono25();
5799
+ app25.onError((err, c) => {
5083
5800
  if (err instanceof HTTPException) {
5084
5801
  return err.getResponse();
5085
5802
  }
@@ -5090,10 +5807,10 @@ app23.onError((err, c) => {
5090
5807
  });
5091
5808
  return c.json({ error: "Internal server error" }, 500);
5092
5809
  });
5093
- app23.notFound((c) => {
5810
+ app25.notFound((c) => {
5094
5811
  return c.json({ error: "Not found" }, 404);
5095
5812
  });
5096
- app23.use("*", async (c, next) => {
5813
+ app25.use("*", async (c, next) => {
5097
5814
  const start = Date.now();
5098
5815
  await next();
5099
5816
  const duration = Date.now() - start;
@@ -5104,7 +5821,7 @@ app23.use("*", async (c, next) => {
5104
5821
  httpLog.debug("request", data);
5105
5822
  }
5106
5823
  });
5107
- app23.get("/api/health", (c) => {
5824
+ app25.get("/api/health", (c) => {
5108
5825
  let version = "unknown";
5109
5826
  let cached = null;
5110
5827
  try {
@@ -5119,18 +5836,19 @@ app23.get("/api/health", (c) => {
5119
5836
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
5120
5837
  });
5121
5838
  });
5122
- app23.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
5123
- app23.use("/api/*", csrf());
5124
- app23.use("/api/minds/*", authMiddleware);
5125
- app23.use("/api/conversations/*", authMiddleware);
5126
- app23.use("/api/volute/*", authMiddleware);
5127
- app23.use("/api/system/*", authMiddleware);
5128
- app23.use("/api/env/*", authMiddleware);
5129
- app23.use("/api/prompts/*", authMiddleware);
5130
- app23.use("/api/skills/*", authMiddleware);
5131
- app23.route("/pages", pages_default);
5132
- var routes = app23.route("/api/keys", keys_default).route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", shared_default).route("/api/minds", env_default).route("/api/minds", mind_skills_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/prompts", prompts_default).route("/api/skills", skills_default).route("/api/conversations", user_conversations_default).route("/api/volute/channels", channels_default2).route("/api/volute", unifiedChatApp);
5133
- var app_default = app23;
5839
+ app25.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
5840
+ app25.use("/api/*", csrf());
5841
+ app25.use("/api/activity/*", authMiddleware);
5842
+ app25.use("/api/minds/*", authMiddleware);
5843
+ app25.use("/api/conversations/*", authMiddleware);
5844
+ app25.use("/api/volute/*", authMiddleware);
5845
+ app25.use("/api/system/*", authMiddleware);
5846
+ app25.use("/api/env/*", authMiddleware);
5847
+ app25.use("/api/prompts/*", authMiddleware);
5848
+ app25.use("/api/skills/*", authMiddleware);
5849
+ app25.route("/pages", pages_default);
5850
+ var routes = app25.route("/api/activity", activity_default).route("/api/keys", keys_default).route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", file_sharing_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", shared_default).route("/api/minds", env_default).route("/api/minds", mind_skills_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/prompts", prompts_default).route("/api/skills", skills_default).route("/api/conversations", user_conversations_default).route("/api/volute/channels", channels_default2).route("/api/volute", unifiedChatApp);
5851
+ var app_default = app25;
5134
5852
 
5135
5853
  // src/web/server.ts
5136
5854
  var MIME_TYPES2 = {
@@ -5149,8 +5867,8 @@ async function startServer({
5149
5867
  let assetsDir = "";
5150
5868
  let searchDir = dirname3(new URL(import.meta.url).pathname);
5151
5869
  for (let i = 0; i < 5; i++) {
5152
- const candidate = resolve18(searchDir, "dist", "web-assets");
5153
- if (existsSync13(candidate)) {
5870
+ const candidate = resolve21(searchDir, "dist", "web-assets");
5871
+ if (existsSync15(candidate)) {
5154
5872
  assetsDir = candidate;
5155
5873
  break;
5156
5874
  }
@@ -5160,7 +5878,7 @@ async function startServer({
5160
5878
  app_default.get("*", async (c) => {
5161
5879
  const urlPath = new URL(c.req.url).pathname;
5162
5880
  if (urlPath.startsWith("/api/")) return c.notFound();
5163
- const filePath = resolve18(assetsDir, urlPath.slice(1));
5881
+ const filePath = resolve21(assetsDir, urlPath.slice(1));
5164
5882
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
5165
5883
  const s = await stat2(filePath).catch(() => null);
5166
5884
  if (s?.isFile()) {
@@ -5169,7 +5887,7 @@ async function startServer({
5169
5887
  const body = await readFile3(filePath);
5170
5888
  return c.body(body, 200, { "Content-Type": mime });
5171
5889
  }
5172
- const indexPath = resolve18(assetsDir, "index.html");
5890
+ const indexPath = resolve21(assetsDir, "index.html");
5173
5891
  const indexStat = await stat2(indexPath).catch(() => null);
5174
5892
  if (indexStat?.isFile()) {
5175
5893
  const body = await readFile3(indexPath, "utf-8");
@@ -5179,10 +5897,10 @@ async function startServer({
5179
5897
  });
5180
5898
  }
5181
5899
  const server = serve({ fetch: app_default.fetch, port, hostname });
5182
- await new Promise((resolve20, reject) => {
5900
+ await new Promise((resolve23, reject) => {
5183
5901
  server.on("listening", () => {
5184
5902
  logger_default.info("Volute UI running", { hostname, port });
5185
- resolve20();
5903
+ resolve23();
5186
5904
  });
5187
5905
  server.on("error", (err) => {
5188
5906
  reject(err);
@@ -5193,14 +5911,14 @@ async function startServer({
5193
5911
 
5194
5912
  // src/daemon.ts
5195
5913
  if (!process.env.VOLUTE_HOME) {
5196
- process.env.VOLUTE_HOME = resolve19(homedir2(), ".volute");
5914
+ process.env.VOLUTE_HOME = resolve22(homedir2(), ".volute");
5197
5915
  }
5198
5916
  async function startDaemon(opts) {
5199
5917
  const { port, hostname } = opts;
5200
5918
  const myPid = String(process.pid);
5201
5919
  const home = voluteHome();
5202
5920
  if (!opts.foreground) {
5203
- const rotatingLog = new RotatingLog(resolve19(home, "daemon.log"));
5921
+ const rotatingLog = new RotatingLog(resolve22(home, "daemon.log"));
5204
5922
  logger_default.setOutput((line) => rotatingLog.write(`${line}
5205
5923
  `));
5206
5924
  const write = (...args) => rotatingLog.write(`${format(...args)}
@@ -5210,9 +5928,9 @@ async function startDaemon(opts) {
5210
5928
  console.warn = write;
5211
5929
  console.info = write;
5212
5930
  }
5213
- const DAEMON_PID_PATH = resolve19(home, "daemon.pid");
5214
- const DAEMON_JSON_PATH = resolve19(home, "daemon.json");
5215
- mkdirSync10(home, { recursive: true });
5931
+ const DAEMON_PID_PATH = resolve22(home, "daemon.pid");
5932
+ const DAEMON_JSON_PATH = resolve22(home, "daemon.json");
5933
+ mkdirSync11(home, { recursive: true });
5216
5934
  migrateAgentsToMinds();
5217
5935
  try {
5218
5936
  await ensureSharedRepo();
@@ -5225,7 +5943,7 @@ async function startDaemon(opts) {
5225
5943
  } catch (err) {
5226
5944
  logger_default.error("failed to sync built-in skills", logger_default.errorData(err));
5227
5945
  }
5228
- const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
5946
+ const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes2(32).toString("hex");
5229
5947
  process.env.VOLUTE_DAEMON_TOKEN = token;
5230
5948
  process.env.VOLUTE_DAEMON_PORT = String(port);
5231
5949
  process.env.VOLUTE_DAEMON_HOSTNAME = hostname;
@@ -5240,8 +5958,8 @@ async function startDaemon(opts) {
5240
5958
  }
5241
5959
  throw err;
5242
5960
  }
5243
- writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
5244
- writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
5961
+ writeFileSync11(DAEMON_PID_PATH, myPid, { mode: 420 });
5962
+ writeFileSync11(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
5245
5963
  `, {
5246
5964
  mode: 420
5247
5965
  });
@@ -5305,13 +6023,13 @@ async function startDaemon(opts) {
5305
6023
  logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
5306
6024
  function cleanup() {
5307
6025
  try {
5308
- if (readFileSync11(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
6026
+ if (readFileSync13(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
5309
6027
  unlinkSync2(DAEMON_PID_PATH);
5310
6028
  }
5311
6029
  } catch {
5312
6030
  }
5313
6031
  try {
5314
- const data = JSON.parse(readFileSync11(DAEMON_JSON_PATH, "utf-8"));
6032
+ const data = JSON.parse(readFileSync13(DAEMON_JSON_PATH, "utf-8"));
5315
6033
  if (data.token === token) {
5316
6034
  unlinkSync2(DAEMON_JSON_PATH);
5317
6035
  }
@@ -5323,6 +6041,8 @@ async function startDaemon(opts) {
5323
6041
  if (shuttingDown) return;
5324
6042
  shuttingDown = true;
5325
6043
  logger_default.info("shutting down...");
6044
+ stopAllWatchers();
6045
+ stopAll();
5326
6046
  scheduler.stop();
5327
6047
  scheduler.saveState();
5328
6048
  mailPoller.stop();