volute 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{chunk-CE7WMOVW.js → chunk-AYB7XAWO.js} +323 -25
  2. package/dist/{chunk-MIJIAGGG.js → chunk-FW5API7X.js} +7 -5
  3. package/dist/{chunk-3FC42ZBM.js → chunk-GK4E7LM7.js} +3 -0
  4. package/dist/cli.js +18 -6
  5. package/dist/connectors/discord.js +1 -1
  6. package/dist/connectors/slack.js +1 -1
  7. package/dist/connectors/telegram.js +1 -1
  8. package/dist/{daemon-restart-VRQMZLBK.js → daemon-restart-2HVTHZAT.js} +1 -1
  9. package/dist/daemon.js +1080 -432
  10. package/dist/{history-5F4WQW7S.js → history-YUEKTJ2N.js} +4 -1
  11. package/dist/{mind-manager-ETNCPQJN.js → mind-manager-Z7O7PN2O.js} +1 -1
  12. package/dist/{package-4GTJGUXI.js → package-OKLFO7UY.js} +3 -1
  13. package/dist/{send-4GKDO26C.js → send-BNDTLUPM.js} +2 -2
  14. package/dist/skill-2Y42P4JY.js +287 -0
  15. package/dist/{up-LT3X5Q26.js → up-7B3BWF2U.js} +1 -1
  16. package/dist/web-assets/assets/index-CtiimdWK.css +1 -0
  17. package/dist/web-assets/assets/index-kt1_EcuO.js +63 -0
  18. package/dist/web-assets/index.html +2 -2
  19. package/drizzle/0007_system_prompts.sql +5 -0
  20. package/drizzle/0008_volute_channels.sql +24 -0
  21. package/drizzle/0009_shared_skills.sql +9 -0
  22. package/drizzle/meta/0007_snapshot.json +7 -0
  23. package/drizzle/meta/0008_snapshot.json +7 -0
  24. package/drizzle/meta/0009_snapshot.json +7 -0
  25. package/drizzle/meta/_journal.json +21 -0
  26. package/package.json +3 -1
  27. package/templates/_base/.init/.config/prompts.json +5 -0
  28. package/templates/_base/_skills/volute-mind/SKILL.md +17 -1
  29. package/templates/_base/src/lib/router.ts +45 -28
  30. package/templates/_base/src/lib/routing.ts +4 -1
  31. package/templates/_base/src/lib/startup.ts +43 -0
  32. package/templates/claude/src/agent.ts +4 -3
  33. package/templates/claude/src/lib/hooks/reply-instructions.ts +3 -1
  34. package/templates/pi/src/agent.ts +5 -6
  35. package/templates/pi/src/lib/reply-instructions-extension.ts +3 -1
  36. package/dist/web-assets/assets/index-BcmT7Qxo.js +0 -63
  37. package/dist/web-assets/assets/index-DG01TyLb.css +0 -1
  38. /package/dist/{chunk-77ISBIKI.js → chunk-6DVBMLVN.js} +0 -0
package/dist/daemon.js CHANGED
@@ -10,15 +10,30 @@ import {
10
10
  readSystemsConfig
11
11
  } from "./chunk-37X7ECMF.js";
12
12
  import {
13
+ PROMPT_DEFAULTS,
14
+ PROMPT_KEYS,
13
15
  RotatingLog,
14
16
  clearJsonMap,
17
+ conversationParticipants,
18
+ conversations,
19
+ getDb,
15
20
  getMindManager,
21
+ getMindPromptDefaults,
22
+ getPrompt,
23
+ getPromptIfCustom,
16
24
  initMindManager,
17
25
  loadJsonMap,
18
26
  logBuffer,
19
27
  logger_default,
20
- saveJsonMap
21
- } from "./chunk-CE7WMOVW.js";
28
+ messages,
29
+ mindHistory,
30
+ saveJsonMap,
31
+ sessions,
32
+ sharedSkills,
33
+ substitute,
34
+ systemPrompts,
35
+ users
36
+ } from "./chunk-AYB7XAWO.js";
22
37
  import {
23
38
  findOpenClawSession,
24
39
  importOpenClawConnectors,
@@ -37,7 +52,7 @@ import {
37
52
  import {
38
53
  CHANNELS,
39
54
  getChannelDriver
40
- } from "./chunk-MIJIAGGG.js";
55
+ } from "./chunk-FW5API7X.js";
41
56
  import {
42
57
  exec,
43
58
  gitExec,
@@ -60,7 +75,7 @@ import "./chunk-D424ZQGI.js";
60
75
  import {
61
76
  buildVoluteSlug,
62
77
  writeChannelEntry
63
- } from "./chunk-3FC42ZBM.js";
78
+ } from "./chunk-GK4E7LM7.js";
64
79
  import {
65
80
  addMind,
66
81
  addVariant,
@@ -85,15 +100,13 @@ import {
85
100
  validateMindName,
86
101
  voluteHome
87
102
  } from "./chunk-M77QBTEH.js";
88
- import {
89
- __export
90
- } from "./chunk-K3NQKI34.js";
103
+ import "./chunk-K3NQKI34.js";
91
104
 
92
105
  // src/daemon.ts
93
106
  import { randomBytes } from "crypto";
94
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
107
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "fs";
95
108
  import { homedir as homedir2 } from "os";
96
- import { resolve as resolve17 } from "path";
109
+ import { resolve as resolve18 } from "path";
97
110
  import { format } from "util";
98
111
 
99
112
  // src/lib/connector-manager.ts
@@ -343,19 +356,19 @@ var ConnectorManager = class {
343
356
  const stopKey = `${mindName}:${type}`;
344
357
  this.stopping.add(stopKey);
345
358
  mindMap.delete(type);
346
- await new Promise((resolve18) => {
347
- tracked.child.on("exit", () => resolve18());
359
+ await new Promise((resolve19) => {
360
+ tracked.child.on("exit", () => resolve19());
348
361
  try {
349
362
  tracked.child.kill("SIGTERM");
350
363
  } catch {
351
- resolve18();
364
+ resolve19();
352
365
  }
353
366
  setTimeout(() => {
354
367
  try {
355
368
  tracked.child.kill("SIGKILL");
356
369
  } catch {
357
370
  }
358
- resolve18();
371
+ resolve19();
359
372
  }, 5e3);
360
373
  });
361
374
  this.stopping.delete(stopKey);
@@ -632,9 +645,9 @@ var MailPoller = class {
632
645
  }
633
646
  const channel = `mail:${email.from.address}`;
634
647
  const sender = email.from.name || email.from.address;
635
- const text2 = formatEmailContent(email);
648
+ const text = formatEmailContent(email);
636
649
  const body = JSON.stringify({
637
- content: [{ type: "text", text: text2 }],
650
+ content: [{ type: "text", text }],
638
651
  channel,
639
652
  sender,
640
653
  platform: "Email",
@@ -1164,129 +1177,12 @@ import { createMiddleware } from "hono/factory";
1164
1177
  // src/lib/auth.ts
1165
1178
  import { compareSync, hashSync } from "bcryptjs";
1166
1179
  import { and, count, eq } from "drizzle-orm";
1167
-
1168
- // src/lib/db.ts
1169
- import { chmodSync, existsSync as existsSync5 } from "fs";
1170
- import { dirname as dirname2, resolve as resolve6 } from "path";
1171
- import { fileURLToPath } from "url";
1172
- import { drizzle } from "drizzle-orm/libsql";
1173
- import { migrate } from "drizzle-orm/libsql/migrator";
1174
-
1175
- // src/lib/schema.ts
1176
- var schema_exports = {};
1177
- __export(schema_exports, {
1178
- conversationParticipants: () => conversationParticipants,
1179
- conversations: () => conversations,
1180
- messages: () => messages,
1181
- mindHistory: () => mindHistory,
1182
- sessions: () => sessions,
1183
- users: () => users
1184
- });
1185
- import { sql } from "drizzle-orm";
1186
- import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
1187
- var users = sqliteTable("users", {
1188
- id: integer("id").primaryKey({ autoIncrement: true }),
1189
- username: text("username").unique().notNull(),
1190
- password_hash: text("password_hash").notNull(),
1191
- role: text("role").notNull().default("pending"),
1192
- user_type: text("user_type").notNull().default("human"),
1193
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1194
- });
1195
- var conversations = sqliteTable(
1196
- "conversations",
1197
- {
1198
- id: text("id").primaryKey(),
1199
- mind_name: text("mind_name").notNull(),
1200
- channel: text("channel").notNull(),
1201
- user_id: integer("user_id").references(() => users.id),
1202
- title: text("title"),
1203
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
1204
- updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
1205
- },
1206
- (table) => [
1207
- index("idx_conversations_mind_name").on(table.mind_name),
1208
- index("idx_conversations_user_id").on(table.user_id),
1209
- index("idx_conversations_updated_at").on(table.updated_at)
1210
- ]
1211
- );
1212
- var mindHistory = sqliteTable(
1213
- "mind_history",
1214
- {
1215
- id: integer("id").primaryKey({ autoIncrement: true }),
1216
- mind: text("mind").notNull(),
1217
- channel: text("channel"),
1218
- session: text("session"),
1219
- sender: text("sender"),
1220
- message_id: text("message_id"),
1221
- type: text("type").notNull(),
1222
- content: text("content"),
1223
- metadata: text("metadata"),
1224
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1225
- },
1226
- (table) => [
1227
- index("idx_mind_history_mind").on(table.mind),
1228
- index("idx_mind_history_mind_channel").on(table.mind, table.channel),
1229
- index("idx_mind_history_mind_type").on(table.mind, table.type)
1230
- ]
1231
- );
1232
- var conversationParticipants = sqliteTable(
1233
- "conversation_participants",
1234
- {
1235
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
1236
- user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
1237
- role: text("role").notNull().default("member"),
1238
- joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
1239
- },
1240
- (table) => [
1241
- uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
1242
- index("idx_cp_user_id").on(table.user_id)
1243
- ]
1244
- );
1245
- var sessions = sqliteTable("sessions", {
1246
- id: text("id").primaryKey(),
1247
- userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
1248
- createdAt: integer("created_at").notNull()
1249
- });
1250
- var messages = sqliteTable(
1251
- "messages",
1252
- {
1253
- id: integer("id").primaryKey({ autoIncrement: true }),
1254
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
1255
- role: text("role").notNull(),
1256
- sender_name: text("sender_name"),
1257
- content: text("content").notNull(),
1258
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
1259
- },
1260
- (table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
1261
- );
1262
-
1263
- // src/lib/db.ts
1264
- var __dirname = dirname2(fileURLToPath(import.meta.url));
1265
- var migrationsFolder = existsSync5(resolve6(__dirname, "../drizzle")) ? resolve6(__dirname, "../drizzle") : resolve6(__dirname, "../../drizzle");
1266
- var db = null;
1267
- async function getDb() {
1268
- if (db) return db;
1269
- const dbPath = process.env.VOLUTE_DB_PATH || resolve6(voluteHome(), "volute.db");
1270
- db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
1271
- await migrate(db, { migrationsFolder });
1272
- try {
1273
- chmodSync(dbPath, 384);
1274
- } catch (err) {
1275
- console.error(
1276
- `[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
1277
- err
1278
- );
1279
- }
1280
- return db;
1281
- }
1282
-
1283
- // src/lib/auth.ts
1284
1180
  async function createUser(username, password) {
1285
- const db2 = await getDb();
1181
+ const db = await getDb();
1286
1182
  const hash = hashSync(password, 10);
1287
- const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
1183
+ const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
1288
1184
  const role = value === 0 ? "admin" : "pending";
1289
- const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
1185
+ const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
1290
1186
  id: users.id,
1291
1187
  username: users.username,
1292
1188
  role: users.role,
@@ -1296,8 +1192,8 @@ async function createUser(username, password) {
1296
1192
  return result;
1297
1193
  }
1298
1194
  async function verifyUser(username, password) {
1299
- const db2 = await getDb();
1300
- const row = await db2.select().from(users).where(eq(users.username, username)).get();
1195
+ const db = await getDb();
1196
+ const row = await db.select().from(users).where(eq(users.username, username)).get();
1301
1197
  if (!row) return null;
1302
1198
  if (row.user_type === "mind") return null;
1303
1199
  if (!compareSync(password, row.password_hash)) return null;
@@ -1305,8 +1201,8 @@ async function verifyUser(username, password) {
1305
1201
  return user;
1306
1202
  }
1307
1203
  async function getUser(id) {
1308
- const db2 = await getDb();
1309
- const row = await db2.select({
1204
+ const db = await getDb();
1205
+ const row = await db.select({
1310
1206
  id: users.id,
1311
1207
  username: users.username,
1312
1208
  role: users.role,
@@ -1316,8 +1212,8 @@ async function getUser(id) {
1316
1212
  return row ?? null;
1317
1213
  }
1318
1214
  async function getUserByUsername(username) {
1319
- const db2 = await getDb();
1320
- const row = await db2.select({
1215
+ const db = await getDb();
1216
+ const row = await db.select({
1321
1217
  id: users.id,
1322
1218
  username: users.username,
1323
1219
  role: users.role,
@@ -1327,8 +1223,8 @@ async function getUserByUsername(username) {
1327
1223
  return row ?? null;
1328
1224
  }
1329
1225
  async function listUsers() {
1330
- const db2 = await getDb();
1331
- return db2.select({
1226
+ const db = await getDb();
1227
+ return db.select({
1332
1228
  id: users.id,
1333
1229
  username: users.username,
1334
1230
  role: users.role,
@@ -1337,8 +1233,8 @@ async function listUsers() {
1337
1233
  }).from(users).orderBy(users.created_at).all();
1338
1234
  }
1339
1235
  async function listPendingUsers() {
1340
- const db2 = await getDb();
1341
- return db2.select({
1236
+ const db = await getDb();
1237
+ return db.select({
1342
1238
  id: users.id,
1343
1239
  username: users.username,
1344
1240
  role: users.role,
@@ -1347,8 +1243,8 @@ async function listPendingUsers() {
1347
1243
  }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
1348
1244
  }
1349
1245
  async function listUsersByType(userType) {
1350
- const db2 = await getDb();
1351
- return db2.select({
1246
+ const db = await getDb();
1247
+ return db.select({
1352
1248
  id: users.id,
1353
1249
  username: users.username,
1354
1250
  role: users.role,
@@ -1357,8 +1253,8 @@ async function listUsersByType(userType) {
1357
1253
  }).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
1358
1254
  }
1359
1255
  async function getOrCreateMindUser(mindName) {
1360
- const db2 = await getDb();
1361
- const existing = await db2.select({
1256
+ const db = await getDb();
1257
+ const existing = await db.select({
1362
1258
  id: users.id,
1363
1259
  username: users.username,
1364
1260
  role: users.role,
@@ -1367,7 +1263,7 @@ async function getOrCreateMindUser(mindName) {
1367
1263
  }).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
1368
1264
  if (existing) return existing;
1369
1265
  try {
1370
- const [result] = await db2.insert(users).values({
1266
+ const [result] = await db.insert(users).values({
1371
1267
  username: mindName,
1372
1268
  password_hash: "!mind",
1373
1269
  role: "mind",
@@ -1382,7 +1278,7 @@ async function getOrCreateMindUser(mindName) {
1382
1278
  return result;
1383
1279
  } catch (err) {
1384
1280
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
1385
- const retried = await db2.select({
1281
+ const retried = await db.select({
1386
1282
  id: users.id,
1387
1283
  username: users.username,
1388
1284
  role: users.role,
@@ -1395,12 +1291,21 @@ async function getOrCreateMindUser(mindName) {
1395
1291
  }
1396
1292
  }
1397
1293
  async function deleteMindUser2(mindName) {
1398
- const db2 = await getDb();
1399
- await db2.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
1294
+ const db = await getDb();
1295
+ await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
1296
+ }
1297
+ async function changePassword(userId, currentPassword, newPassword) {
1298
+ const db = await getDb();
1299
+ const row = await db.select().from(users).where(eq(users.id, userId)).get();
1300
+ if (!row) return false;
1301
+ if (!compareSync(currentPassword, row.password_hash)) return false;
1302
+ const hash = hashSync(newPassword, 10);
1303
+ await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
1304
+ return true;
1400
1305
  }
1401
1306
  async function approveUser(id) {
1402
- const db2 = await getDb();
1403
- await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
1307
+ const db = await getDb();
1308
+ await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
1404
1309
  }
1405
1310
 
1406
1311
  // src/web/middleware/auth.ts
@@ -1411,29 +1316,29 @@ function isValidDaemonToken(token) {
1411
1316
  }
1412
1317
  var SESSION_MAX_AGE = 864e5;
1413
1318
  async function createSession(userId) {
1414
- const db2 = await getDb();
1319
+ const db = await getDb();
1415
1320
  const sessionId = crypto.randomUUID();
1416
- await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
1321
+ await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
1417
1322
  return sessionId;
1418
1323
  }
1419
1324
  async function deleteSession(sessionId) {
1420
- const db2 = await getDb();
1421
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
1325
+ const db = await getDb();
1326
+ await db.delete(sessions).where(eq2(sessions.id, sessionId));
1422
1327
  }
1423
1328
  async function getSessionUserId(sessionId) {
1424
- const db2 = await getDb();
1425
- const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1329
+ const db = await getDb();
1330
+ const row = await db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1426
1331
  if (!row) return void 0;
1427
1332
  if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
1428
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
1333
+ await db.delete(sessions).where(eq2(sessions.id, sessionId));
1429
1334
  return void 0;
1430
1335
  }
1431
1336
  return row.userId;
1432
1337
  }
1433
1338
  async function cleanExpiredSessions() {
1434
- const db2 = await getDb();
1339
+ const db = await getDb();
1435
1340
  const cutoff = Date.now() - SESSION_MAX_AGE;
1436
- await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
1341
+ await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
1437
1342
  }
1438
1343
  var requireAdmin = createMiddleware(async (c, next) => {
1439
1344
  const user = c.get("user");
@@ -1464,13 +1369,13 @@ var authMiddleware = createMiddleware(async (c, next) => {
1464
1369
  });
1465
1370
 
1466
1371
  // src/web/server.ts
1467
- import { existsSync as existsSync10 } from "fs";
1372
+ import { existsSync as existsSync11 } from "fs";
1468
1373
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
1469
- import { dirname as dirname3, extname as extname2, resolve as resolve16 } from "path";
1374
+ import { dirname as dirname2, extname as extname2, resolve as resolve17 } from "path";
1470
1375
  import { serve } from "@hono/node-server";
1471
1376
 
1472
1377
  // src/web/app.ts
1473
- import { Hono as Hono17 } from "hono";
1378
+ import { Hono as Hono21 } from "hono";
1474
1379
  import { bodyLimit } from "hono/body-limit";
1475
1380
  import { csrf } from "hono/csrf";
1476
1381
  import { HTTPException } from "hono/http-exception";
@@ -1484,6 +1389,17 @@ var credentialsSchema = z.object({
1484
1389
  username: z.string().min(1),
1485
1390
  password: z.string().min(1)
1486
1391
  });
1392
+ var changePasswordSchema = z.object({
1393
+ currentPassword: z.string().min(1),
1394
+ newPassword: z.string().min(1)
1395
+ });
1396
+ var authenticated = new Hono().use(authMiddleware).post("/change-password", zValidator("json", changePasswordSchema), async (c) => {
1397
+ const user = c.get("user");
1398
+ const { currentPassword, newPassword } = c.req.valid("json");
1399
+ const ok = await changePassword(user.id, currentPassword, newPassword);
1400
+ if (!ok) return c.json({ error: "Current password is incorrect" }, 400);
1401
+ return c.json({ ok: true });
1402
+ });
1487
1403
  var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
1488
1404
  const user = c.get("user");
1489
1405
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
@@ -1543,7 +1459,7 @@ var app = new Hono().post("/register", zValidator("json", credentialsSchema), as
1543
1459
  const user = await getUser(userId);
1544
1460
  if (!user) return c.json({ error: "Not logged in" }, 401);
1545
1461
  return c.json({ id: user.id, username: user.username, role: user.role });
1546
- }).route("/", admin);
1462
+ }).route("/", admin).route("/", authenticated);
1547
1463
  var auth_default = app;
1548
1464
 
1549
1465
  // src/web/api/channels.ts
@@ -1797,9 +1713,9 @@ var sharedEnvApp = new Hono4().get("/", (c) => {
1797
1713
  var env_default = app4;
1798
1714
 
1799
1715
  // src/web/api/files.ts
1800
- import { existsSync as existsSync6 } from "fs";
1716
+ import { existsSync as existsSync5 } from "fs";
1801
1717
  import { readdir, readFile } from "fs/promises";
1802
- import { resolve as resolve7 } from "path";
1718
+ import { resolve as resolve6 } from "path";
1803
1719
  import { Hono as Hono5 } from "hono";
1804
1720
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1805
1721
  var app5 = new Hono5().get("/:name/files", async (c) => {
@@ -1807,8 +1723,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1807
1723
  const entry = findMind(name);
1808
1724
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1809
1725
  const dir = mindDir(name);
1810
- const homeDir = resolve7(dir, "home");
1811
- if (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1726
+ const homeDir = resolve6(dir, "home");
1727
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1812
1728
  const allFiles = await readdir(homeDir);
1813
1729
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1814
1730
  return c.json(files);
@@ -1821,8 +1737,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
1821
1737
  const entry = findMind(name);
1822
1738
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1823
1739
  const dir = mindDir(name);
1824
- const filePath = resolve7(dir, "home", filename);
1825
- if (!existsSync6(filePath)) {
1740
+ const filePath = resolve6(dir, "home", filename);
1741
+ if (!existsSync5(filePath)) {
1826
1742
  return c.json({ error: "File not found" }, 404);
1827
1743
  }
1828
1744
  const content = await readFile(filePath, "utf-8");
@@ -1832,16 +1748,16 @@ var files_default = app5;
1832
1748
 
1833
1749
  // src/web/api/logs.ts
1834
1750
  import { spawn as spawn2 } from "child_process";
1835
- import { existsSync as existsSync7 } from "fs";
1836
- import { resolve as resolve8 } from "path";
1751
+ import { existsSync as existsSync6 } from "fs";
1752
+ import { resolve as resolve7 } from "path";
1837
1753
  import { Hono as Hono6 } from "hono";
1838
1754
  import { streamSSE } from "hono/streaming";
1839
1755
  var app6 = new Hono6().get("/:name/logs", async (c) => {
1840
1756
  const name = c.req.param("name");
1841
1757
  const entry = findMind(name);
1842
1758
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1843
- const logFile = resolve8(stateDir(name), "logs", "mind.log");
1844
- if (!existsSync7(logFile)) {
1759
+ const logFile = resolve7(stateDir(name), "logs", "mind.log");
1760
+ if (!existsSync6(logFile)) {
1845
1761
  return c.json({ error: "No log file found" }, 404);
1846
1762
  }
1847
1763
  return streamSSE(c, async (stream) => {
@@ -1859,17 +1775,17 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1859
1775
  stream.onAbort(() => {
1860
1776
  tail.kill();
1861
1777
  });
1862
- await new Promise((resolve18) => {
1863
- tail.on("exit", resolve18);
1864
- stream.onAbort(resolve18);
1778
+ await new Promise((resolve19) => {
1779
+ tail.on("exit", resolve19);
1780
+ stream.onAbort(resolve19);
1865
1781
  });
1866
1782
  });
1867
1783
  }).get("/:name/logs/tail", async (c) => {
1868
1784
  const name = c.req.param("name");
1869
1785
  const entry = findMind(name);
1870
1786
  if (!entry) return c.json({ error: "Mind not found" }, 404);
1871
- const logFile = resolve8(stateDir(name), "logs", "mind.log");
1872
- if (!existsSync7(logFile)) {
1787
+ const logFile = resolve7(stateDir(name), "logs", "mind.log");
1788
+ if (!existsSync6(logFile)) {
1873
1789
  return c.json({ error: "No log file found" }, 404);
1874
1790
  }
1875
1791
  const nParam = parseInt(c.req.query("n") ?? "50", 10);
@@ -1879,44 +1795,421 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
1879
1795
  tail.stdout.on("data", (data) => {
1880
1796
  output += data.toString();
1881
1797
  });
1882
- await new Promise((resolve18) => {
1883
- tail.on("exit", resolve18);
1798
+ await new Promise((resolve19) => {
1799
+ tail.on("exit", resolve19);
1884
1800
  });
1885
1801
  return c.text(output);
1886
1802
  });
1887
1803
  var logs_default = app6;
1888
1804
 
1889
- // src/web/api/minds.ts
1805
+ // src/web/api/mind-skills.ts
1806
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1807
+ import { Hono as Hono7 } from "hono";
1808
+ import { z as z2 } from "zod";
1809
+
1810
+ // src/lib/skills.ts
1890
1811
  import {
1891
1812
  cpSync,
1892
- existsSync as existsSync8,
1893
- mkdirSync as mkdirSync4,
1894
- readdirSync as readdirSync3,
1895
- readFileSync as readFileSync6,
1813
+ existsSync as existsSync7,
1814
+ mkdirSync as mkdirSync3,
1815
+ readdirSync as readdirSync2,
1816
+ readFileSync as readFileSync4,
1896
1817
  rmSync,
1818
+ writeFileSync as writeFileSync3
1819
+ } from "fs";
1820
+ import { tmpdir } from "os";
1821
+ import { basename, join, resolve as resolve8 } from "path";
1822
+ import { eq as eq3, sql } from "drizzle-orm";
1823
+ var VALID_SKILL_ID = /^[a-zA-Z0-9_-]+$/;
1824
+ function validateSkillId(id) {
1825
+ if (!id || !VALID_SKILL_ID.test(id)) {
1826
+ throw new Error(`Invalid skill ID: ${id}`);
1827
+ }
1828
+ }
1829
+ function sharedSkillsDir() {
1830
+ return resolve8(voluteHome(), "skills");
1831
+ }
1832
+ function parseSkillMd(content) {
1833
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1834
+ if (!match) return { name: "", description: "" };
1835
+ const frontmatter = match[1];
1836
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
1837
+ const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
1838
+ return {
1839
+ name: nameMatch?.[1].trim() ?? "",
1840
+ description: descMatch?.[1].trim() ?? ""
1841
+ };
1842
+ }
1843
+ async function listSharedSkills() {
1844
+ const db = await getDb();
1845
+ return db.select().from(sharedSkills).all();
1846
+ }
1847
+ async function getSharedSkill(id) {
1848
+ const db = await getDb();
1849
+ return db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1850
+ }
1851
+ async function importSkillFromDir(sourceDir, author) {
1852
+ const skillMdPath = join(sourceDir, "SKILL.md");
1853
+ if (!existsSync7(skillMdPath)) {
1854
+ throw new Error("SKILL.md not found in source directory");
1855
+ }
1856
+ const content = readFileSync4(skillMdPath, "utf-8");
1857
+ const { name, description } = parseSkillMd(content);
1858
+ const id = basename(sourceDir);
1859
+ if (!id || id === "." || id === "..") {
1860
+ throw new Error("Invalid skill directory name");
1861
+ }
1862
+ validateSkillId(id);
1863
+ const destDir = join(sharedSkillsDir(), id);
1864
+ if (existsSync7(destDir)) rmSync(destDir, { recursive: true });
1865
+ mkdirSync3(destDir, { recursive: true });
1866
+ cpSync(sourceDir, destDir, { recursive: true });
1867
+ const upstreamPath = join(destDir, ".upstream.json");
1868
+ if (existsSync7(upstreamPath)) rmSync(upstreamPath);
1869
+ const db = await getDb();
1870
+ const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1871
+ const version = existing ? existing.version + 1 : 1;
1872
+ await db.insert(sharedSkills).values({ id, name: name || id, description, author, version }).onConflictDoUpdate({
1873
+ target: sharedSkills.id,
1874
+ set: {
1875
+ name: name || id,
1876
+ description,
1877
+ author,
1878
+ version,
1879
+ updated_at: sql`(datetime('now'))`
1880
+ }
1881
+ });
1882
+ const row = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1883
+ if (!row) throw new Error(`Failed to upsert shared skill: ${id}`);
1884
+ return row;
1885
+ }
1886
+ async function removeSharedSkill(id) {
1887
+ const db = await getDb();
1888
+ const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
1889
+ if (!existing) throw new Error(`Shared skill not found: ${id}`);
1890
+ await db.delete(sharedSkills).where(eq3(sharedSkills.id, id));
1891
+ const dir = join(sharedSkillsDir(), id);
1892
+ if (existsSync7(dir)) rmSync(dir, { recursive: true });
1893
+ }
1894
+ function mindSkillsDir(dir) {
1895
+ return resolve8(dir, "home", ".claude", "skills");
1896
+ }
1897
+ function readUpstream(skillDir) {
1898
+ const upstreamPath = join(skillDir, ".upstream.json");
1899
+ if (!existsSync7(upstreamPath)) return null;
1900
+ try {
1901
+ const data = JSON.parse(readFileSync4(upstreamPath, "utf-8"));
1902
+ if (typeof data?.source !== "string" || typeof data?.version !== "number" || typeof data?.baseCommit !== "string") {
1903
+ return null;
1904
+ }
1905
+ return data;
1906
+ } catch {
1907
+ return null;
1908
+ }
1909
+ }
1910
+ async function installSkill(_mindName, dir, skillId) {
1911
+ validateSkillId(skillId);
1912
+ const shared = await getSharedSkill(skillId);
1913
+ if (!shared) throw new Error(`Shared skill not found: ${skillId}`);
1914
+ const sourceDir = join(sharedSkillsDir(), skillId);
1915
+ if (!existsSync7(sourceDir)) throw new Error(`Shared skill files not found: ${skillId}`);
1916
+ const destDir = join(mindSkillsDir(dir), skillId);
1917
+ if (existsSync7(destDir)) throw new Error(`Skill already installed: ${skillId}`);
1918
+ mkdirSync3(destDir, { recursive: true });
1919
+ cpSync(sourceDir, destDir, { recursive: true });
1920
+ await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
1921
+ await gitExec(["commit", "-m", `Install shared skill: ${skillId}`], { cwd: dir });
1922
+ const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
1923
+ const upstream = {
1924
+ source: skillId,
1925
+ version: shared.version,
1926
+ baseCommit: commitHash
1927
+ };
1928
+ writeFileSync3(join(destDir, ".upstream.json"), `${JSON.stringify(upstream, null, 2)}
1929
+ `);
1930
+ await gitExec(["add", join("home", ".claude", "skills", skillId, ".upstream.json")], {
1931
+ cwd: dir
1932
+ });
1933
+ await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
1934
+ }
1935
+ async function uninstallSkill(_mindName, dir, skillId) {
1936
+ validateSkillId(skillId);
1937
+ const skillDir = join(mindSkillsDir(dir), skillId);
1938
+ if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
1939
+ rmSync(skillDir, { recursive: true });
1940
+ await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
1941
+ await gitExec(["commit", "-m", `Uninstall skill: ${skillId}`], { cwd: dir });
1942
+ }
1943
+ async function updateSkill(_mindName, dir, skillId) {
1944
+ validateSkillId(skillId);
1945
+ const skillDir = join(mindSkillsDir(dir), skillId);
1946
+ if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
1947
+ const upstream = readUpstream(skillDir);
1948
+ if (!upstream) throw new Error(`No upstream tracking for skill: ${skillId}`);
1949
+ const shared = await getSharedSkill(upstream.source);
1950
+ if (!shared) throw new Error(`Shared skill no longer exists: ${upstream.source}`);
1951
+ if (shared.version <= upstream.version) {
1952
+ return { status: "up-to-date" };
1953
+ }
1954
+ const sourceDir = join(sharedSkillsDir(), upstream.source);
1955
+ if (!existsSync7(sourceDir)) throw new Error(`Shared skill files missing: ${upstream.source}`);
1956
+ const relSkillPath = join("home", ".claude", "skills", skillId);
1957
+ const currentFiles = listFilesRecursive(skillDir).filter((f) => f !== ".upstream.json");
1958
+ const newFiles = listFilesRecursive(sourceDir).filter((f) => f !== ".upstream.json");
1959
+ const allFiles = [.../* @__PURE__ */ new Set([...currentFiles, ...newFiles])];
1960
+ const conflictFiles = [];
1961
+ const tmpBase = join(tmpdir(), `volute-merge-${process.pid}-${Date.now()}`);
1962
+ mkdirSync3(tmpBase, { recursive: true });
1963
+ try {
1964
+ for (const file of allFiles) {
1965
+ const currentPath = join(skillDir, file);
1966
+ const newPath = join(sourceDir, file);
1967
+ const currentExists = existsSync7(currentPath);
1968
+ const newExists = existsSync7(newPath);
1969
+ if (!currentExists && newExists) {
1970
+ const destPath = join(skillDir, file);
1971
+ mkdirSync3(join(skillDir, ...file.split("/").slice(0, -1)), { recursive: true });
1972
+ cpSync(newPath, destPath);
1973
+ continue;
1974
+ }
1975
+ if (currentExists && !newExists) {
1976
+ let baseContent2 = null;
1977
+ try {
1978
+ baseContent2 = await gitExec(
1979
+ ["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
1980
+ { cwd: dir }
1981
+ );
1982
+ } catch {
1983
+ continue;
1984
+ }
1985
+ const currentContent2 = readFileSync4(currentPath, "utf-8");
1986
+ if (currentContent2 === baseContent2) {
1987
+ rmSync(currentPath);
1988
+ }
1989
+ continue;
1990
+ }
1991
+ let baseContent;
1992
+ try {
1993
+ baseContent = await gitExec(
1994
+ ["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
1995
+ { cwd: dir }
1996
+ );
1997
+ } catch {
1998
+ baseContent = "";
1999
+ }
2000
+ const currentContent = readFileSync4(currentPath, "utf-8");
2001
+ const newContent = readFileSync4(newPath, "utf-8");
2002
+ if (currentContent === baseContent) {
2003
+ writeFileSync3(currentPath, newContent);
2004
+ continue;
2005
+ }
2006
+ if (newContent === baseContent) {
2007
+ continue;
2008
+ }
2009
+ const baseTmp = join(tmpBase, `${file}.base`);
2010
+ const currentTmp = join(tmpBase, `${file}.current`);
2011
+ const newTmp = join(tmpBase, `${file}.new`);
2012
+ mkdirSync3(join(tmpBase, ...file.split("/").slice(0, -1)), { recursive: true });
2013
+ writeFileSync3(baseTmp, baseContent);
2014
+ writeFileSync3(currentTmp, currentContent);
2015
+ writeFileSync3(newTmp, newContent);
2016
+ try {
2017
+ await exec("git", ["merge-file", currentTmp, baseTmp, newTmp]);
2018
+ writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
2019
+ } catch (e) {
2020
+ const exitCode = e && typeof e === "object" && "code" in e ? e.code : null;
2021
+ if (exitCode === 1) {
2022
+ writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
2023
+ conflictFiles.push(file);
2024
+ } else {
2025
+ throw e;
2026
+ }
2027
+ }
2028
+ }
2029
+ } finally {
2030
+ rmSync(tmpBase, { recursive: true, force: true });
2031
+ }
2032
+ if (conflictFiles.length > 0) {
2033
+ return { status: "conflict", conflictFiles };
2034
+ }
2035
+ const upstreamInfo = {
2036
+ source: upstream.source,
2037
+ version: shared.version,
2038
+ baseCommit: upstream.baseCommit
2039
+ // will update after commit
2040
+ };
2041
+ writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
2042
+ `);
2043
+ await gitExec(["add", relSkillPath], { cwd: dir });
2044
+ await gitExec(["commit", "-m", `Update skill: ${skillId} (v${shared.version})`], { cwd: dir });
2045
+ const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
2046
+ upstreamInfo.baseCommit = commitHash;
2047
+ writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
2048
+ `);
2049
+ await gitExec(["add", join(relSkillPath, ".upstream.json")], { cwd: dir });
2050
+ await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
2051
+ return { status: "updated" };
2052
+ }
2053
+ async function listMindSkills(dir) {
2054
+ const skillsDir = mindSkillsDir(dir);
2055
+ if (!existsSync7(skillsDir)) return [];
2056
+ const entries = readdirSync2(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2057
+ const sharedMap = /* @__PURE__ */ new Map();
2058
+ for (const s of await listSharedSkills()) {
2059
+ sharedMap.set(s.id, s);
2060
+ }
2061
+ const results = [];
2062
+ for (const entry of entries) {
2063
+ const skillDir = join(skillsDir, entry.name);
2064
+ const skillMdPath = join(skillDir, "SKILL.md");
2065
+ let name = entry.name;
2066
+ let description = "";
2067
+ if (existsSync7(skillMdPath)) {
2068
+ const parsed = parseSkillMd(readFileSync4(skillMdPath, "utf-8"));
2069
+ if (parsed.name) name = parsed.name;
2070
+ description = parsed.description;
2071
+ }
2072
+ const upstream = readUpstream(skillDir);
2073
+ let updateAvailable = false;
2074
+ if (upstream) {
2075
+ const shared = sharedMap.get(upstream.source);
2076
+ if (shared && shared.version > upstream.version) {
2077
+ updateAvailable = true;
2078
+ }
2079
+ }
2080
+ results.push({ id: entry.name, name, description, upstream, updateAvailable });
2081
+ }
2082
+ return results;
2083
+ }
2084
+ async function publishSkill(mindName, dir, skillId) {
2085
+ const skillDir = join(mindSkillsDir(dir), skillId);
2086
+ if (!existsSync7(skillDir)) throw new Error(`Skill not found: ${skillId}`);
2087
+ const skillMdPath = join(skillDir, "SKILL.md");
2088
+ if (!existsSync7(skillMdPath)) throw new Error(`SKILL.md not found in ${skillId}`);
2089
+ return importSkillFromDir(skillDir, mindName);
2090
+ }
2091
+ function listFilesRecursive(dir, prefix = "") {
2092
+ const results = [];
2093
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2094
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
2095
+ if (entry.isDirectory()) {
2096
+ results.push(...listFilesRecursive(join(dir, entry.name), rel));
2097
+ } else {
2098
+ results.push(rel);
2099
+ }
2100
+ }
2101
+ return results;
2102
+ }
2103
+
2104
+ // src/web/api/mind-skills.ts
2105
+ var app7 = new Hono7().get("/:name/skills", async (c) => {
2106
+ const name = c.req.param("name");
2107
+ const entry = findMind(name);
2108
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2109
+ const dir = mindDir(name);
2110
+ const skills = await listMindSkills(dir);
2111
+ return c.json(skills);
2112
+ }).post(
2113
+ "/:name/skills/install",
2114
+ requireAdmin,
2115
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2116
+ async (c) => {
2117
+ const name = c.req.param("name");
2118
+ const entry = findMind(name);
2119
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2120
+ const { skillId } = c.req.valid("json");
2121
+ const dir = mindDir(name);
2122
+ try {
2123
+ await installSkill(name, dir, skillId);
2124
+ } catch (e) {
2125
+ const msg = e instanceof Error ? e.message : String(e);
2126
+ return c.json({ error: msg }, 400);
2127
+ }
2128
+ return c.json({ ok: true });
2129
+ }
2130
+ ).post(
2131
+ "/:name/skills/update",
2132
+ requireAdmin,
2133
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2134
+ async (c) => {
2135
+ const name = c.req.param("name");
2136
+ const entry = findMind(name);
2137
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2138
+ const { skillId } = c.req.valid("json");
2139
+ const dir = mindDir(name);
2140
+ try {
2141
+ const result = await updateSkill(name, dir, skillId);
2142
+ return c.json(result);
2143
+ } catch (e) {
2144
+ const msg = e instanceof Error ? e.message : String(e);
2145
+ return c.json({ error: msg }, 400);
2146
+ }
2147
+ }
2148
+ ).post(
2149
+ "/:name/skills/publish",
2150
+ requireAdmin,
2151
+ zValidator2("json", z2.object({ skillId: z2.string() })),
2152
+ async (c) => {
2153
+ const name = c.req.param("name");
2154
+ const entry = findMind(name);
2155
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2156
+ const { skillId } = c.req.valid("json");
2157
+ const dir = mindDir(name);
2158
+ try {
2159
+ const skill = await publishSkill(name, dir, skillId);
2160
+ return c.json(skill);
2161
+ } catch (e) {
2162
+ const msg = e instanceof Error ? e.message : String(e);
2163
+ return c.json({ error: msg }, 400);
2164
+ }
2165
+ }
2166
+ ).delete("/:name/skills/:skill", requireAdmin, async (c) => {
2167
+ const name = c.req.param("name");
2168
+ const skillName = c.req.param("skill");
2169
+ const entry = findMind(name);
2170
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
2171
+ const dir = mindDir(name);
2172
+ try {
2173
+ await uninstallSkill(name, dir, skillName);
2174
+ } catch (e) {
2175
+ const msg = e instanceof Error ? e.message : String(e);
2176
+ return c.json({ error: msg }, 400);
2177
+ }
2178
+ return c.json({ ok: true });
2179
+ });
2180
+ var mind_skills_default = app7;
2181
+
2182
+ // src/web/api/minds.ts
2183
+ import {
2184
+ cpSync as cpSync2,
2185
+ existsSync as existsSync8,
2186
+ mkdirSync as mkdirSync5,
2187
+ readdirSync as readdirSync4,
2188
+ readFileSync as readFileSync7,
2189
+ rmSync as rmSync2,
1897
2190
  statSync,
1898
- writeFileSync as writeFileSync5
2191
+ writeFileSync as writeFileSync6
1899
2192
  } from "fs";
1900
- import { join, resolve as resolve11 } from "path";
1901
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1902
- import { and as and3, desc as desc2, eq as eq4, sql as sql3 } from "drizzle-orm";
1903
- import { Hono as Hono7 } from "hono";
1904
- import { z as z2 } from "zod";
2193
+ import { join as join2, resolve as resolve11 } from "path";
2194
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
2195
+ import { and as and3, desc as desc2, eq as eq5, sql as sql3 } from "drizzle-orm";
2196
+ import { Hono as Hono8 } from "hono";
2197
+ import { z as z3 } from "zod";
1905
2198
 
1906
2199
  // src/lib/consolidate.ts
1907
- import { readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2200
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1908
2201
  import { resolve as resolve9 } from "path";
1909
2202
  async function consolidateMemory(mindDir2) {
1910
2203
  const soulPath = resolve9(mindDir2, "home/SOUL.md");
1911
2204
  const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
1912
2205
  const memoryDir = resolve9(mindDir2, "home/memory");
1913
- const soul = readFileSync4(soulPath, "utf-8");
2206
+ const soul = readFileSync5(soulPath, "utf-8");
1914
2207
  const logs = [];
1915
2208
  try {
1916
- const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2209
+ const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
1917
2210
  for (const filename of files) {
1918
2211
  const date = filename.replace(".md", "");
1919
- const content2 = readFileSync4(resolve9(memoryDir, filename), "utf-8").trim();
2212
+ const content2 = readFileSync5(resolve9(memoryDir, filename), "utf-8").trim();
1920
2213
  if (content2) {
1921
2214
  logs.push(`### ${date}
1922
2215
 
@@ -1966,7 +2259,7 @@ ${content2}`);
1966
2259
  const data = await res.json();
1967
2260
  const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
1968
2261
  if (content) {
1969
- writeFileSync3(memoryPath, `${content}
2262
+ writeFileSync4(memoryPath, `${content}
1970
2263
  `);
1971
2264
  console.log("MEMORY.md created successfully.");
1972
2265
  } else {
@@ -1976,7 +2269,7 @@ ${content2}`);
1976
2269
 
1977
2270
  // src/lib/conversations.ts
1978
2271
  import { randomUUID } from "crypto";
1979
- import { and as and2, desc, eq as eq3, inArray, isNull, sql as sql2 } from "drizzle-orm";
2272
+ import { and as and2, desc, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
1980
2273
 
1981
2274
  // src/lib/conversation-events.ts
1982
2275
  var subscribers = /* @__PURE__ */ new Map();
@@ -2008,13 +2301,17 @@ function publish(conversationId, event) {
2008
2301
 
2009
2302
  // src/lib/conversations.ts
2010
2303
  async function createConversation(mindName, channel, opts) {
2011
- const db2 = await getDb();
2304
+ const db = await getDb();
2012
2305
  const id = randomUUID();
2013
- await db2.transaction(async (tx) => {
2306
+ const type = opts?.type ?? "dm";
2307
+ const name = opts?.name ?? null;
2308
+ await db.transaction(async (tx) => {
2014
2309
  await tx.insert(conversations).values({
2015
2310
  id,
2016
2311
  mind_name: mindName,
2017
2312
  channel,
2313
+ type,
2314
+ name,
2018
2315
  user_id: opts?.userId ?? null,
2019
2316
  title: opts?.title ?? null
2020
2317
  });
@@ -2032,6 +2329,8 @@ async function createConversation(mindName, channel, opts) {
2032
2329
  id,
2033
2330
  mind_name: mindName,
2034
2331
  channel,
2332
+ type,
2333
+ name,
2035
2334
  user_id: opts?.userId ?? null,
2036
2335
  title: opts?.title ?? null,
2037
2336
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2039,41 +2338,58 @@ async function createConversation(mindName, channel, opts) {
2039
2338
  };
2040
2339
  }
2041
2340
  async function getConversation(id) {
2042
- const db2 = await getDb();
2043
- const row = await db2.select().from(conversations).where(eq3(conversations.id, id)).get();
2341
+ const db = await getDb();
2342
+ const row = await db.select().from(conversations).where(eq4(conversations.id, id)).get();
2044
2343
  return row ?? null;
2045
2344
  }
2345
+ async function addParticipant(conversationId, userId, role = "member") {
2346
+ const db = await getDb();
2347
+ await db.insert(conversationParticipants).values({
2348
+ conversation_id: conversationId,
2349
+ user_id: userId,
2350
+ role
2351
+ });
2352
+ }
2353
+ async function removeParticipant(conversationId, userId) {
2354
+ const db = await getDb();
2355
+ await db.delete(conversationParticipants).where(
2356
+ and2(
2357
+ eq4(conversationParticipants.conversation_id, conversationId),
2358
+ eq4(conversationParticipants.user_id, userId)
2359
+ )
2360
+ );
2361
+ }
2046
2362
  async function getParticipants(conversationId) {
2047
- const db2 = await getDb();
2048
- const rows = await db2.select({
2363
+ const db = await getDb();
2364
+ const rows = await db.select({
2049
2365
  userId: conversationParticipants.user_id,
2050
2366
  username: users.username,
2051
2367
  userType: users.user_type,
2052
2368
  role: conversationParticipants.role
2053
- }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
2369
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
2054
2370
  return rows;
2055
2371
  }
2056
2372
  async function isParticipant(conversationId, userId) {
2057
- const db2 = await getDb();
2058
- const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2373
+ const db = await getDb();
2374
+ const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2059
2375
  and2(
2060
- eq3(conversationParticipants.conversation_id, conversationId),
2061
- eq3(conversationParticipants.user_id, userId)
2376
+ eq4(conversationParticipants.conversation_id, conversationId),
2377
+ eq4(conversationParticipants.user_id, userId)
2062
2378
  )
2063
2379
  ).get();
2064
2380
  return row != null;
2065
2381
  }
2066
2382
  async function listConversationsForUser(userId) {
2067
- const db2 = await getDb();
2068
- const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
2383
+ const db = await getDb();
2384
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
2069
2385
  if (participantRows.length === 0) return [];
2070
2386
  const convIds = participantRows.map((r) => r.conversation_id);
2071
- return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
2387
+ return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
2072
2388
  }
2073
2389
  async function isParticipantOrOwner(conversationId, userId) {
2074
2390
  if (await isParticipant(conversationId, userId)) return true;
2075
- const db2 = await getDb();
2076
- const row = await db2.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
2391
+ const db = await getDb();
2392
+ const row = await db.select().from(conversations).where(and2(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
2077
2393
  return row != null;
2078
2394
  }
2079
2395
  async function deleteConversationForUser(id, userId) {
@@ -2082,15 +2398,15 @@ async function deleteConversationForUser(id, userId) {
2082
2398
  return true;
2083
2399
  }
2084
2400
  async function addMessage(conversationId, role, senderName, content) {
2085
- const db2 = await getDb();
2401
+ const db = await getDb();
2086
2402
  const serialized = JSON.stringify(content);
2087
- const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2088
- await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq3(conversations.id, conversationId));
2403
+ const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2404
+ await db.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
2089
2405
  if (role === "user") {
2090
2406
  const firstText = content.find((b) => b.type === "text");
2091
2407
  const title = firstText ? firstText.text.slice(0, 80) : "";
2092
2408
  if (title) {
2093
- await db2.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
2409
+ await db.update(conversations).set({ title }).where(and2(eq4(conversations.id, conversationId), isNull(conversations.title)));
2094
2410
  }
2095
2411
  }
2096
2412
  const msg = {
@@ -2112,8 +2428,8 @@ async function addMessage(conversationId, role, senderName, content) {
2112
2428
  return msg;
2113
2429
  }
2114
2430
  async function getMessages(conversationId) {
2115
- const db2 = await getDb();
2116
- const rows = await db2.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2431
+ const db = await getDb();
2432
+ const rows = await db.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2117
2433
  return rows.map((row) => {
2118
2434
  let content;
2119
2435
  try {
@@ -2128,15 +2444,15 @@ async function getMessages(conversationId) {
2128
2444
  async function listConversationsWithParticipants(userId) {
2129
2445
  const convs = await listConversationsForUser(userId);
2130
2446
  if (convs.length === 0) return [];
2131
- const db2 = await getDb();
2447
+ const db = await getDb();
2132
2448
  const convIds = convs.map((c) => c.id);
2133
- const rows = await db2.select({
2449
+ const rows = await db.select({
2134
2450
  conversationId: conversationParticipants.conversation_id,
2135
2451
  userId: users.id,
2136
2452
  username: users.username,
2137
2453
  userType: users.user_type,
2138
2454
  role: conversationParticipants.role
2139
- }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2455
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2140
2456
  const byConv = /* @__PURE__ */ new Map();
2141
2457
  for (const r of rows) {
2142
2458
  let arr = byConv.get(r.conversationId);
@@ -2151,32 +2467,32 @@ async function listConversationsWithParticipants(userId) {
2151
2467
  role: r.role
2152
2468
  });
2153
2469
  }
2154
- const lastMsgIds = await db2.select({
2470
+ const lastMsgIds = await db.select({
2155
2471
  conversationId: messages.conversation_id,
2156
2472
  maxId: sql2`MAX(${messages.id})`
2157
2473
  }).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
2158
2474
  const byLastMsg = /* @__PURE__ */ new Map();
2159
2475
  if (lastMsgIds.length > 0) {
2160
- const msgRows = await db2.select().from(messages).where(
2476
+ const msgRows = await db.select().from(messages).where(
2161
2477
  inArray(
2162
2478
  messages.id,
2163
2479
  lastMsgIds.map((r) => r.maxId)
2164
2480
  )
2165
2481
  );
2166
2482
  for (const m of msgRows) {
2167
- let text2 = "";
2483
+ let text = "";
2168
2484
  try {
2169
2485
  const parsed = JSON.parse(m.content);
2170
2486
  const blocks = Array.isArray(parsed) ? parsed : [];
2171
2487
  const textBlock = blocks.find((b) => b.type === "text");
2172
- if (textBlock && "text" in textBlock) text2 = textBlock.text;
2488
+ if (textBlock && "text" in textBlock) text = textBlock.text;
2173
2489
  } catch {
2174
- text2 = m.content;
2490
+ text = m.content;
2175
2491
  }
2176
2492
  byLastMsg.set(m.conversation_id, {
2177
2493
  role: m.role,
2178
2494
  senderName: m.sender_name,
2179
- text: text2,
2495
+ text,
2180
2496
  createdAt: m.created_at
2181
2497
  });
2182
2498
  }
@@ -2188,10 +2504,10 @@ async function listConversationsWithParticipants(userId) {
2188
2504
  }));
2189
2505
  }
2190
2506
  async function findDMConversation(mindName, participantIds) {
2191
- const db2 = await getDb();
2192
- const mindConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq3(conversations.mind_name, mindName)).all();
2507
+ const db = await getDb();
2508
+ const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq4(conversations.mind_name, mindName), eq4(conversations.type, "dm"))).all();
2193
2509
  for (const conv of mindConvs) {
2194
- const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
2510
+ const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
2195
2511
  if (rows.length !== 2) continue;
2196
2512
  const ids = new Set(rows.map((r) => r.user_id));
2197
2513
  if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
@@ -2201,17 +2517,42 @@ async function findDMConversation(mindName, participantIds) {
2201
2517
  return null;
2202
2518
  }
2203
2519
  async function deleteConversation(id) {
2204
- const db2 = await getDb();
2205
- await db2.delete(conversations).where(eq3(conversations.id, id));
2520
+ const db = await getDb();
2521
+ await db.delete(conversations).where(eq4(conversations.id, id));
2522
+ }
2523
+ async function createChannel(name, creatorId) {
2524
+ const participantIds = creatorId ? [creatorId] : [];
2525
+ return createConversation(null, "volute", {
2526
+ type: "channel",
2527
+ name,
2528
+ title: name,
2529
+ participantIds
2530
+ });
2531
+ }
2532
+ async function getChannelByName(name) {
2533
+ const db = await getDb();
2534
+ const row = await db.select().from(conversations).where(and2(eq4(conversations.name, name), eq4(conversations.type, "channel"))).get();
2535
+ return row ?? null;
2536
+ }
2537
+ async function listChannels() {
2538
+ const db = await getDb();
2539
+ return await db.select().from(conversations).where(eq4(conversations.type, "channel")).orderBy(conversations.name).all();
2540
+ }
2541
+ async function joinChannel(conversationId, userId) {
2542
+ if (await isParticipant(conversationId, userId)) return;
2543
+ await addParticipant(conversationId, userId);
2544
+ }
2545
+ async function leaveChannel(conversationId, userId) {
2546
+ await removeParticipant(conversationId, userId);
2206
2547
  }
2207
2548
 
2208
2549
  // src/lib/convert-session.ts
2209
2550
  import { randomUUID as randomUUID2 } from "crypto";
2210
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2551
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2211
2552
  import { homedir } from "os";
2212
2553
  import { resolve as resolve10 } from "path";
2213
2554
  function convertSession(opts) {
2214
- const lines = readFileSync5(opts.sessionPath, "utf-8").trim().split("\n");
2555
+ const lines = readFileSync6(opts.sessionPath, "utf-8").trim().split("\n");
2215
2556
  const sessionId = randomUUID2();
2216
2557
  const idMap = /* @__PURE__ */ new Map();
2217
2558
  const messages2 = [];
@@ -2326,9 +2667,9 @@ function convertSession(opts) {
2326
2667
  }
2327
2668
  const projectId = opts.projectDir.replace(/\//g, "-");
2328
2669
  const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
2329
- mkdirSync3(sdkDir, { recursive: true });
2670
+ mkdirSync4(sdkDir, { recursive: true });
2330
2671
  const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
2331
- writeFileSync4(sdkPath, `${sdkEvents.join("\n")}
2672
+ writeFileSync5(sdkPath, `${sdkEvents.join("\n")}
2332
2673
  `);
2333
2674
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
2334
2675
  return sessionId;
@@ -2513,7 +2854,7 @@ function extractTextContent(content) {
2513
2854
  }
2514
2855
  function getDaemonPort() {
2515
2856
  try {
2516
- const data = JSON.parse(readFileSync6(resolve11(voluteHome(), "daemon.json"), "utf-8"));
2857
+ const data = JSON.parse(readFileSync7(resolve11(voluteHome(), "daemon.json"), "utf-8"));
2517
2858
  return data.port;
2518
2859
  } catch (err) {
2519
2860
  if (err?.code !== "ENOENT") {
@@ -2576,7 +2917,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2576
2917
  } catch {
2577
2918
  }
2578
2919
  if (existsSync8(tempWorktree)) {
2579
- rmSync(tempWorktree, { recursive: true, force: true });
2920
+ rmSync2(tempWorktree, { recursive: true, force: true });
2580
2921
  }
2581
2922
  const templatesRoot = findTemplatesRoot();
2582
2923
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
@@ -2598,7 +2939,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2598
2939
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
2599
2940
  const initDir = resolve11(tempWorktree, ".init");
2600
2941
  if (existsSync8(initDir)) {
2601
- rmSync(initDir, { recursive: true, force: true });
2942
+ rmSync2(initDir, { recursive: true, force: true });
2602
2943
  }
2603
2944
  await gitExec(["add", "-A"], { cwd: tempWorktree });
2604
2945
  try {
@@ -2612,9 +2953,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
2612
2953
  } catch {
2613
2954
  }
2614
2955
  if (existsSync8(tempWorktree)) {
2615
- rmSync(tempWorktree, { recursive: true, force: true });
2956
+ rmSync2(tempWorktree, { recursive: true, force: true });
2616
2957
  }
2617
- rmSync(composedDir, { recursive: true, force: true });
2958
+ rmSync2(composedDir, { recursive: true, force: true });
2618
2959
  }
2619
2960
  }
2620
2961
  async function mergeTemplateBranch(worktreeDir) {
@@ -2642,14 +2983,15 @@ async function npmInstallAsMind(cwd, mindName) {
2642
2983
  await exec("npm", ["install"], { cwd });
2643
2984
  }
2644
2985
  }
2645
- var createMindSchema = z2.object({
2646
- name: z2.string(),
2647
- template: z2.string().optional(),
2648
- stage: z2.enum(["seed", "sprouted"]).optional(),
2649
- description: z2.string().optional(),
2650
- model: z2.string().optional()
2986
+ var createMindSchema = z3.object({
2987
+ name: z3.string(),
2988
+ template: z3.string().optional(),
2989
+ stage: z3.enum(["seed", "sprouted"]).optional(),
2990
+ description: z3.string().optional(),
2991
+ model: z3.string().optional(),
2992
+ seedSoul: z3.string().optional()
2651
2993
  });
2652
- var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSchema), async (c) => {
2994
+ var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
2653
2995
  const body = c.req.valid("json");
2654
2996
  const { name, template = "claude" } = body;
2655
2997
  const nameErr = validateMindName(name);
@@ -2665,11 +3007,17 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2665
3007
  applyInitFiles(dest);
2666
3008
  if (body.model) {
2667
3009
  const configPath = resolve11(dest, "home/.config/config.json");
2668
- const existing = existsSync8(configPath) ? JSON.parse(readFileSync6(configPath, "utf-8")) : {};
3010
+ const existing = existsSync8(configPath) ? JSON.parse(readFileSync7(configPath, "utf-8")) : {};
2669
3011
  existing.model = body.model;
2670
- writeFileSync5(configPath, `${JSON.stringify(existing, null, 2)}
3012
+ writeFileSync6(configPath, `${JSON.stringify(existing, null, 2)}
2671
3013
  `);
2672
3014
  }
3015
+ const mindPrompts = await getMindPromptDefaults();
3016
+ writeFileSync6(
3017
+ resolve11(dest, "home/.config/prompts.json"),
3018
+ `${JSON.stringify(mindPrompts, null, 2)}
3019
+ `
3020
+ );
2673
3021
  const port = nextPort();
2674
3022
  addMind(name, port, body.stage);
2675
3023
  const homeDir = resolve11(dest, "home");
@@ -2684,7 +3032,7 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2684
3032
  await initTemplateBranch(dest, composedDir, manifest, name, env);
2685
3033
  } catch (err) {
2686
3034
  console.error(`[daemon] git setup failed for ${name}:`, err);
2687
- rmSync(resolve11(dest, ".git"), { recursive: true, force: true });
3035
+ rmSync2(resolve11(dest, ".git"), { recursive: true, force: true });
2688
3036
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
2689
3037
  }
2690
3038
  chownMindDir(dest, name);
@@ -2692,19 +3040,23 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
2692
3040
  const descLine = body.description ? `
2693
3041
  The human who planted you described you as: "${body.description}"
2694
3042
  ` : "";
2695
- writeFileSync5(
2696
- resolve11(dest, "home/SOUL.md"),
2697
- `You are ${name}.
2698
-
2699
- You don't have a soul yet. You're a seed \u2014 a new mind in the process of discovering who you are.
2700
- ${descLine}
2701
- Have a conversation with the human. Explore what kind of mind you want to be. When you're ready, write your SOUL.md and MEMORY.md, then run \`volute sprout\` to complete the transformation.
2702
- `
2703
- );
3043
+ const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
3044
+ const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
3045
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
2704
3046
  const skillsDir = resolve11(dest, manifest.skillsDir);
2705
3047
  for (const skill of ["volute-mind", "memory", "sessions"]) {
2706
3048
  const skillPath = resolve11(skillsDir, skill);
2707
- if (existsSync8(skillPath)) rmSync(skillPath, { recursive: true, force: true });
3049
+ if (existsSync8(skillPath)) rmSync2(skillPath, { recursive: true, force: true });
3050
+ }
3051
+ }
3052
+ if (body.stage !== "seed") {
3053
+ const customSoul = await getPromptIfCustom("default_soul");
3054
+ if (customSoul) {
3055
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3056
+ }
3057
+ const customMemory = await getPromptIfCustom("default_memory");
3058
+ if (customMemory) {
3059
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
2708
3060
  }
2709
3061
  }
2710
3062
  return c.json({
@@ -2716,14 +3068,14 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
2716
3068
  ...gitWarning && { warning: gitWarning }
2717
3069
  });
2718
3070
  } catch (err) {
2719
- if (existsSync8(dest)) rmSync(dest, { recursive: true, force: true });
3071
+ if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
2720
3072
  try {
2721
3073
  removeMind(name);
2722
3074
  } catch {
2723
3075
  }
2724
3076
  return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
2725
3077
  } finally {
2726
- rmSync(composedDir, { recursive: true, force: true });
3078
+ rmSync2(composedDir, { recursive: true, force: true });
2727
3079
  }
2728
3080
  }).post("/import", requireAdmin, async (c) => {
2729
3081
  let body;
@@ -2736,10 +3088,10 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
2736
3088
  if (!wsDir || !existsSync8(resolve11(wsDir, "SOUL.md")) || !existsSync8(resolve11(wsDir, "IDENTITY.md"))) {
2737
3089
  return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
2738
3090
  }
2739
- const soul = readFileSync6(resolve11(wsDir, "SOUL.md"), "utf-8");
2740
- const identity = readFileSync6(resolve11(wsDir, "IDENTITY.md"), "utf-8");
3091
+ const soul = readFileSync7(resolve11(wsDir, "SOUL.md"), "utf-8");
3092
+ const identity = readFileSync7(resolve11(wsDir, "IDENTITY.md"), "utf-8");
2741
3093
  const userPath = resolve11(wsDir, "USER.md");
2742
- const user = existsSync8(userPath) ? readFileSync6(userPath, "utf-8") : "";
3094
+ const user = existsSync8(userPath) ? readFileSync7(userPath, "utf-8") : "";
2743
3095
  const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
2744
3096
  const template = body.template ?? "claude";
2745
3097
  const nameErr = validateMindName(name);
@@ -2765,26 +3117,26 @@ ${user.trimEnd()}
2765
3117
  try {
2766
3118
  copyTemplateToDir(composedDir, dest, name, manifest);
2767
3119
  applyInitFiles(dest);
2768
- writeFileSync5(resolve11(dest, "home/SOUL.md"), mergedSoul);
3120
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
2769
3121
  const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
2770
3122
  const hasMemory = existsSync8(wsMemoryPath);
2771
3123
  if (hasMemory) {
2772
- const existingMemory = readFileSync6(wsMemoryPath, "utf-8");
2773
- writeFileSync5(
3124
+ const existingMemory = readFileSync7(wsMemoryPath, "utf-8");
3125
+ writeFileSync6(
2774
3126
  resolve11(dest, "home/MEMORY.md"),
2775
3127
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
2776
3128
  );
2777
3129
  } else if (user) {
2778
- writeFileSync5(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
3130
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
2779
3131
  `);
2780
3132
  }
2781
3133
  const wsMemoryDir = resolve11(wsDir, "memory");
2782
3134
  let dailyLogCount = 0;
2783
3135
  if (existsSync8(wsMemoryDir)) {
2784
3136
  const destMemoryDir = resolve11(dest, "home/memory");
2785
- const files = readdirSync3(wsMemoryDir).filter((f) => f.endsWith(".md"));
3137
+ const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
2786
3138
  for (const file of files) {
2787
- cpSync(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
3139
+ cpSync2(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
2788
3140
  }
2789
3141
  dailyLogCount = files.length;
2790
3142
  }
@@ -2809,29 +3161,29 @@ ${user.trimEnd()}
2809
3161
  } else if (template === "claude") {
2810
3162
  const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
2811
3163
  const voluteDir = resolve11(dest, ".volute");
2812
- mkdirSync4(voluteDir, { recursive: true });
2813
- writeFileSync5(resolve11(voluteDir, "session.json"), JSON.stringify({ sessionId }));
3164
+ mkdirSync5(voluteDir, { recursive: true });
3165
+ writeFileSync6(resolve11(voluteDir, "session.json"), JSON.stringify({ sessionId }));
2814
3166
  }
2815
3167
  }
2816
3168
  importOpenClawConnectors(name, dest);
2817
3169
  chownMindDir(dest, name);
2818
3170
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
2819
3171
  } catch (err) {
2820
- if (existsSync8(dest)) rmSync(dest, { recursive: true, force: true });
3172
+ if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
2821
3173
  try {
2822
3174
  removeMind(name);
2823
3175
  } catch {
2824
3176
  }
2825
3177
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
2826
3178
  } finally {
2827
- rmSync(composedDir, { recursive: true, force: true });
3179
+ rmSync2(composedDir, { recursive: true, force: true });
2828
3180
  }
2829
3181
  }).get("/", async (c) => {
2830
3182
  const entries = readRegistry();
2831
3183
  let lastActiveMap = /* @__PURE__ */ new Map();
2832
3184
  try {
2833
- const db2 = await getDb();
2834
- const lastActiveRows = await db2.select({
3185
+ const db = await getDb();
3186
+ const lastActiveRows = await db.select({
2835
3187
  mind: mindHistory.mind,
2836
3188
  lastActiveAt: sql3`MAX(${mindHistory.created_at})`
2837
3189
  }).from(mindHistory).groupBy(mindHistory.mind);
@@ -2860,7 +3212,7 @@ ${user.trimEnd()}
2860
3212
  if (!existsSync8(pagesDir)) continue;
2861
3213
  let items;
2862
3214
  try {
2863
- items = readdirSync3(pagesDir);
3215
+ items = readdirSync4(pagesDir);
2864
3216
  } catch (err) {
2865
3217
  logger_default.warn("Failed to read pages dir", { mind: entry.name, error: err.message });
2866
3218
  continue;
@@ -2882,7 +3234,7 @@ ${user.trimEnd()}
2882
3234
  const indexStat = statSync(indexPath);
2883
3235
  pages.push({
2884
3236
  mind: entry.name,
2885
- file: join(item, "index.html"),
3237
+ file: join2(item, "index.html"),
2886
3238
  modified: indexStat.mtime.toISOString(),
2887
3239
  url: `/pages/${entry.name}/${item}/`
2888
3240
  });
@@ -3036,8 +3388,8 @@ ${user.trimEnd()}
3036
3388
  }
3037
3389
  if (context?.type === "sprouted" && !variantName) {
3038
3390
  try {
3039
- const db2 = await getDb();
3040
- const activeConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.mind_name, baseName)).all();
3391
+ const db = await getDb();
3392
+ const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq5(conversations.mind_name, baseName)).all();
3041
3393
  for (const conv of activeConvs) {
3042
3394
  await addMessage(conv.id, "assistant", "system", [
3043
3395
  { type: "text", text: "[seed has sprouted]" }
@@ -3102,10 +3454,10 @@ ${user.trimEnd()}
3102
3454
  await deleteMindUser2(name);
3103
3455
  const state = stateDir(name);
3104
3456
  if (existsSync8(state)) {
3105
- rmSync(state, { recursive: true, force: true });
3457
+ rmSync2(state, { recursive: true, force: true });
3106
3458
  }
3107
3459
  if (force && existsSync8(dir)) {
3108
- rmSync(dir, { recursive: true, force: true });
3460
+ rmSync2(dir, { recursive: true, force: true });
3109
3461
  deleteMindUser(name);
3110
3462
  }
3111
3463
  return c.json({ ok: true });
@@ -3199,7 +3551,7 @@ ${user.trimEnd()}
3199
3551
  await updateTemplateBranch(dir, template, mindName);
3200
3552
  const parentDir = resolve11(dir, ".variants");
3201
3553
  if (!existsSync8(parentDir)) {
3202
- mkdirSync4(parentDir, { recursive: true });
3554
+ mkdirSync5(parentDir, { recursive: true });
3203
3555
  }
3204
3556
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
3205
3557
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -3277,12 +3629,12 @@ ${user.trimEnd()}
3277
3629
  console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
3278
3630
  }
3279
3631
  const channel = parsed?.channel ?? "unknown";
3280
- const db2 = await getDb();
3632
+ const db = await getDb();
3281
3633
  if (parsed) {
3282
3634
  try {
3283
3635
  const sender2 = parsed.sender ?? null;
3284
3636
  const content = extractTextContent(parsed.content);
3285
- await db2.insert(mindHistory).values({
3637
+ await db.insert(mindHistory).values({
3286
3638
  mind: baseName,
3287
3639
  type: "inbound",
3288
3640
  channel,
@@ -3329,7 +3681,7 @@ ${user.trimEnd()}
3329
3681
  const seedEntry = findMind(baseName);
3330
3682
  if (seedEntry?.stage === "seed" && parsed) {
3331
3683
  try {
3332
- const countResult = await db2.select({ count: sql3`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
3684
+ const countResult = await db.select({ count: sql3`count(*)` }).from(mindHistory).where(eq5(mindHistory.mind, baseName));
3333
3685
  const msgCount = countResult[0]?.count ?? 0;
3334
3686
  if (msgCount >= 10 && msgCount % 10 === 0) {
3335
3687
  const nudge = "\n[You've been exploring for a while. Whenever you feel ready, write your SOUL.md and MEMORY.md, then run volute sprout.]";
@@ -3353,8 +3705,8 @@ ${user.trimEnd()}
3353
3705
  body: forwardBody
3354
3706
  }).then(async (res) => {
3355
3707
  if (!res.ok) {
3356
- const text2 = await res.text().catch(() => "");
3357
- console.error(`[daemon] mind ${name} responded with ${res.status}: ${text2}`);
3708
+ const text = await res.text().catch(() => "");
3709
+ console.error(`[daemon] mind ${name} responded with ${res.status}: ${text}`);
3358
3710
  }
3359
3711
  }).catch((err) => {
3360
3712
  console.error(`[daemon] mind ${name} unreachable on port ${port}:`, err);
@@ -3379,9 +3731,9 @@ ${user.trimEnd()}
3379
3731
  if (!body.type) {
3380
3732
  return c.json({ error: "type required" }, 400);
3381
3733
  }
3382
- const db2 = await getDb();
3734
+ const db = await getDb();
3383
3735
  try {
3384
- await db2.insert(mindHistory).values({
3736
+ await db.insert(mindHistory).values({
3385
3737
  mind: baseName,
3386
3738
  type: body.type,
3387
3739
  session: body.session ?? null,
@@ -3465,9 +3817,9 @@ ${user.trimEnd()}
3465
3817
  if (!body.channel || !body.content) {
3466
3818
  return c.json({ error: "channel and content required" }, 400);
3467
3819
  }
3468
- const db2 = await getDb();
3820
+ const db = await getDb();
3469
3821
  try {
3470
- await db2.insert(mindHistory).values({
3822
+ await db.insert(mindHistory).values({
3471
3823
  mind: baseName,
3472
3824
  type: "outbound",
3473
3825
  channel: body.channel,
@@ -3481,19 +3833,19 @@ ${user.trimEnd()}
3481
3833
  return c.json({ ok: true });
3482
3834
  }).get("/:name/history/sessions", async (c) => {
3483
3835
  const name = c.req.param("name");
3484
- const db2 = await getDb();
3485
- const rows = await db2.select({
3836
+ const db = await getDb();
3837
+ const rows = await db.select({
3486
3838
  session: mindHistory.session,
3487
3839
  started_at: sql3`MIN(${mindHistory.created_at})`,
3488
3840
  event_count: sql3`COUNT(*)`,
3489
3841
  message_count: sql3`SUM(CASE WHEN ${mindHistory.type} IN ('inbound','outbound') THEN 1 ELSE 0 END)`,
3490
3842
  tool_count: sql3`SUM(CASE WHEN ${mindHistory.type}='tool_use' THEN 1 ELSE 0 END)`
3491
- }).from(mindHistory).where(and3(eq4(mindHistory.mind, name), sql3`${mindHistory.session} IS NOT NULL`)).groupBy(mindHistory.session).orderBy(sql3`MIN(${mindHistory.created_at}) DESC`);
3843
+ }).from(mindHistory).where(and3(eq5(mindHistory.mind, name), sql3`${mindHistory.session} IS NOT NULL`)).groupBy(mindHistory.session).orderBy(sql3`MIN(${mindHistory.created_at}) DESC`);
3492
3844
  return c.json(rows);
3493
3845
  }).get("/:name/history/channels", async (c) => {
3494
3846
  const name = c.req.param("name");
3495
- const db2 = await getDb();
3496
- const rows = await db2.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
3847
+ const db = await getDb();
3848
+ const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq5(mindHistory.mind, name));
3497
3849
  return c.json(rows.map((r) => r.channel));
3498
3850
  }).get("/:name/history", async (c) => {
3499
3851
  const name = c.req.param("name");
@@ -3502,26 +3854,26 @@ ${user.trimEnd()}
3502
3854
  const full = c.req.query("full") === "true";
3503
3855
  const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
3504
3856
  const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
3505
- const db2 = await getDb();
3506
- const conditions = [eq4(mindHistory.mind, name)];
3857
+ const db = await getDb();
3858
+ const conditions = [eq5(mindHistory.mind, name)];
3507
3859
  if (channel) {
3508
- conditions.push(eq4(mindHistory.channel, channel));
3860
+ conditions.push(eq5(mindHistory.channel, channel));
3509
3861
  }
3510
3862
  if (session) {
3511
- conditions.push(eq4(mindHistory.session, session));
3863
+ conditions.push(eq5(mindHistory.session, session));
3512
3864
  }
3513
3865
  if (!full) {
3514
3866
  conditions.push(sql3`${mindHistory.type} IN ('inbound', 'outbound')`);
3515
3867
  }
3516
- const rows = await db2.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
3868
+ const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
3517
3869
  return c.json(rows);
3518
3870
  });
3519
- var minds_default = app7;
3871
+ var minds_default = app8;
3520
3872
 
3521
3873
  // src/web/api/pages.ts
3522
3874
  import { readFile as readFile2, stat } from "fs/promises";
3523
3875
  import { extname, resolve as resolve12 } from "path";
3524
- import { Hono as Hono8 } from "hono";
3876
+ import { Hono as Hono9 } from "hono";
3525
3877
  var MIME_TYPES = {
3526
3878
  ".html": "text/html",
3527
3879
  ".js": "application/javascript",
@@ -3538,7 +3890,7 @@ var MIME_TYPES = {
3538
3890
  ".txt": "text/plain",
3539
3891
  ".xml": "application/xml"
3540
3892
  };
3541
- var app8 = new Hono8().get("/:name/*", async (c) => {
3893
+ var app9 = new Hono9().get("/:name/*", async (c) => {
3542
3894
  const name = c.req.param("name");
3543
3895
  if (!findMind(name)) return c.text("Not found", 404);
3544
3896
  const pagesRoot = resolve12(mindDir(name), "home", "pages");
@@ -3563,10 +3915,61 @@ var app8 = new Hono8().get("/:name/*", async (c) => {
3563
3915
  }
3564
3916
  return c.text("Not found", 404);
3565
3917
  });
3566
- var pages_default = app8;
3918
+ var pages_default = app9;
3919
+
3920
+ // src/web/api/prompts.ts
3921
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
3922
+ import { eq as eq6, sql as sql4 } from "drizzle-orm";
3923
+ import { Hono as Hono10 } from "hono";
3924
+ import { z as z4 } from "zod";
3925
+ var app10 = new Hono10().get("/", async (c) => {
3926
+ let rows;
3927
+ try {
3928
+ const db = await getDb();
3929
+ rows = await db.select().from(systemPrompts).all();
3930
+ } catch (err) {
3931
+ console.error("[prompts] failed to query system_prompts:", err);
3932
+ return c.json({ error: "Failed to load prompts from database" }, 500);
3933
+ }
3934
+ const customMap = new Map(rows.map((r) => [r.key, r.content]));
3935
+ const prompts = PROMPT_KEYS.map((key) => {
3936
+ const meta = PROMPT_DEFAULTS[key];
3937
+ const custom = customMap.get(key);
3938
+ return {
3939
+ key,
3940
+ content: custom ?? meta.content,
3941
+ description: meta.description,
3942
+ variables: meta.variables,
3943
+ isCustom: custom !== void 0,
3944
+ category: meta.category
3945
+ };
3946
+ });
3947
+ return c.json(prompts);
3948
+ }).put("/:key", requireAdmin, zValidator4("json", z4.object({ content: z4.string() })), async (c) => {
3949
+ const key = c.req.param("key");
3950
+ if (!PROMPT_KEYS.includes(key)) {
3951
+ return c.json({ error: "Unknown prompt key" }, 404);
3952
+ }
3953
+ const { content } = c.req.valid("json");
3954
+ const db = await getDb();
3955
+ await db.insert(systemPrompts).values({ key, content, updated_at: sql4`(datetime('now'))` }).onConflictDoUpdate({
3956
+ target: systemPrompts.key,
3957
+ set: { content, updated_at: sql4`(datetime('now'))` }
3958
+ });
3959
+ return c.json({ ok: true });
3960
+ }).delete("/:key", requireAdmin, async (c) => {
3961
+ const key = c.req.param("key");
3962
+ if (!PROMPT_KEYS.includes(key)) {
3963
+ return c.json({ error: "Unknown prompt key" }, 404);
3964
+ }
3965
+ const db = await getDb();
3966
+ await db.delete(systemPrompts).where(eq6(systemPrompts.key, key));
3967
+ return c.json({ ok: true });
3968
+ });
3969
+ var prompts_default = app10;
3567
3970
 
3568
3971
  // src/web/api/schedules.ts
3569
- import { Hono as Hono9 } from "hono";
3972
+ import { Hono as Hono11 } from "hono";
3570
3973
  function readSchedules(name) {
3571
3974
  return readVoluteConfig(mindDir(name))?.schedules ?? [];
3572
3975
  }
@@ -3577,7 +3980,7 @@ function writeSchedules(name, schedules) {
3577
3980
  writeVoluteConfig(dir, config);
3578
3981
  getScheduler().loadSchedules(name);
3579
3982
  }
3580
- var app9 = new Hono9().get("/:name/schedules", (c) => {
3983
+ var app11 = new Hono11().get("/:name/schedules", (c) => {
3581
3984
  const name = c.req.param("name");
3582
3985
  if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
3583
3986
  return c.json(readSchedules(name));
@@ -3648,12 +4051,85 @@ var app9 = new Hono9().get("/:name/schedules", (c) => {
3648
4051
  return c.json({ error: "Failed to reach mind" }, 502);
3649
4052
  }
3650
4053
  });
3651
- var schedules_default = app9;
4054
+ var schedules_default = app11;
4055
+
4056
+ // src/web/api/skills.ts
4057
+ import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
4058
+ import { tmpdir as tmpdir2 } from "os";
4059
+ import { join as join3, resolve as resolve13 } from "path";
4060
+ import AdmZip from "adm-zip";
4061
+ import { Hono as Hono12 } from "hono";
4062
+ var app12 = new Hono12().get("/", async (c) => {
4063
+ const skills = await listSharedSkills();
4064
+ return c.json(skills);
4065
+ }).get("/:id", async (c) => {
4066
+ const id = c.req.param("id");
4067
+ const skill = await getSharedSkill(id);
4068
+ if (!skill) return c.json({ error: "Skill not found" }, 404);
4069
+ const dir = join3(sharedSkillsDir(), id);
4070
+ const files = listFilesRecursive(dir);
4071
+ return c.json({ ...skill, files });
4072
+ }).post("/upload", requireAdmin, async (c) => {
4073
+ const body = await c.req.parseBody();
4074
+ const file = body.file;
4075
+ if (!file || !(file instanceof File)) {
4076
+ return c.json({ error: "No file uploaded" }, 400);
4077
+ }
4078
+ if (!file.name.endsWith(".zip")) {
4079
+ return c.json({ error: "Only .zip files are accepted" }, 400);
4080
+ }
4081
+ const buffer = Buffer.from(await file.arrayBuffer());
4082
+ const tmpDir = mkdtempSync(join3(tmpdir2(), "volute-skill-upload-"));
4083
+ try {
4084
+ const zip = new AdmZip(buffer);
4085
+ for (const entry of zip.getEntries()) {
4086
+ const target = resolve13(tmpDir, entry.entryName);
4087
+ if (!target.startsWith(tmpDir)) {
4088
+ return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
4089
+ }
4090
+ }
4091
+ zip.extractAllTo(tmpDir, true);
4092
+ let skillDir = null;
4093
+ if (existsSync9(join3(tmpDir, "SKILL.md"))) {
4094
+ skillDir = tmpDir;
4095
+ } else {
4096
+ const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4097
+ for (const entry of entries) {
4098
+ if (existsSync9(join3(tmpDir, entry.name, "SKILL.md"))) {
4099
+ skillDir = join3(tmpDir, entry.name);
4100
+ break;
4101
+ }
4102
+ }
4103
+ }
4104
+ if (!skillDir) {
4105
+ return c.json({ error: "No SKILL.md found in zip (checked root and one level deep)" }, 400);
4106
+ }
4107
+ const skill = await importSkillFromDir(skillDir, "upload");
4108
+ return c.json(skill);
4109
+ } catch (e) {
4110
+ if (e instanceof Error && e.message.includes("Invalid skill ID")) {
4111
+ return c.json({ error: e.message }, 400);
4112
+ }
4113
+ throw e;
4114
+ } finally {
4115
+ rmSync3(tmpDir, { recursive: true, force: true });
4116
+ }
4117
+ }).delete("/:id", requireAdmin, async (c) => {
4118
+ const id = c.req.param("id");
4119
+ try {
4120
+ await removeSharedSkill(id);
4121
+ } catch (e) {
4122
+ const msg = e instanceof Error ? e.message : String(e);
4123
+ return c.json({ error: msg }, 404);
4124
+ }
4125
+ return c.json({ ok: true });
4126
+ });
4127
+ var skills_default = app12;
3652
4128
 
3653
4129
  // src/web/api/system.ts
3654
- import { Hono as Hono10 } from "hono";
4130
+ import { Hono as Hono13 } from "hono";
3655
4131
  import { streamSSE as streamSSE2 } from "hono/streaming";
3656
- var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
4132
+ var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
3657
4133
  setTimeout(() => process.exit(1), 200);
3658
4134
  return c.json({ ok: true });
3659
4135
  }).post("/stop", requireAdmin, (c) => {
@@ -3670,10 +4146,10 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
3670
4146
  stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
3671
4147
  });
3672
4148
  });
3673
- await new Promise((resolve18) => {
4149
+ await new Promise((resolve19) => {
3674
4150
  stream.onAbort(() => {
3675
4151
  unsubscribe();
3676
- resolve18();
4152
+ resolve19();
3677
4153
  });
3678
4154
  });
3679
4155
  });
@@ -3681,18 +4157,18 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
3681
4157
  const config = readSystemsConfig();
3682
4158
  return c.json({ system: config?.system ?? null });
3683
4159
  });
3684
- var system_default = app10;
4160
+ var system_default = app13;
3685
4161
 
3686
4162
  // src/web/api/typing.ts
3687
- import { zValidator as zValidator3 } from "@hono/zod-validator";
3688
- import { Hono as Hono11 } from "hono";
3689
- import { z as z3 } from "zod";
3690
- var typingSchema = z3.object({
3691
- channel: z3.string().min(1),
3692
- sender: z3.string().min(1),
3693
- active: z3.boolean()
4163
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
4164
+ import { Hono as Hono14 } from "hono";
4165
+ import { z as z5 } from "zod";
4166
+ var typingSchema = z5.object({
4167
+ channel: z5.string().min(1),
4168
+ sender: z5.string().min(1),
4169
+ active: z5.boolean()
3694
4170
  });
3695
- var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
4171
+ var app14 = new Hono14().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
3696
4172
  const { channel, sender, active } = c.req.valid("json");
3697
4173
  const map = getTypingMap();
3698
4174
  if (active) {
@@ -3709,13 +4185,13 @@ var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema)
3709
4185
  const map = getTypingMap();
3710
4186
  return c.json({ typing: map.get(channel) });
3711
4187
  });
3712
- var typing_default = app11;
4188
+ var typing_default = app14;
3713
4189
 
3714
4190
  // src/web/api/update.ts
3715
4191
  import { spawn as spawn3 } from "child_process";
3716
- import { Hono as Hono12 } from "hono";
4192
+ import { Hono as Hono15 } from "hono";
3717
4193
  var bin;
3718
- var app12 = new Hono12().get("/update", async (c) => {
4194
+ var app15 = new Hono15().get("/update", async (c) => {
3719
4195
  const result = await checkForUpdate();
3720
4196
  return c.json(result);
3721
4197
  }).post("/update", requireAdmin, async (c) => {
@@ -3730,19 +4206,19 @@ var app12 = new Hono12().get("/update", async (c) => {
3730
4206
  child.unref();
3731
4207
  return c.json({ ok: true, message: "Updating..." });
3732
4208
  });
3733
- var update_default = app12;
4209
+ var update_default = app15;
3734
4210
 
3735
4211
  // src/web/api/variants.ts
3736
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
3737
- import { resolve as resolve14 } from "path";
3738
- import { Hono as Hono13 } from "hono";
4212
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
4213
+ import { resolve as resolve15 } from "path";
4214
+ import { Hono as Hono16 } from "hono";
3739
4215
 
3740
4216
  // src/lib/spawn-server.ts
3741
4217
  import { spawn as spawn4 } from "child_process";
3742
- import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync7 } from "fs";
3743
- import { resolve as resolve13 } from "path";
4218
+ import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync8 } from "fs";
4219
+ import { resolve as resolve14 } from "path";
3744
4220
  function tsxBin(cwd) {
3745
- return resolve13(cwd, "node_modules", ".bin", "tsx");
4221
+ return resolve14(cwd, "node_modules", ".bin", "tsx");
3746
4222
  }
3747
4223
  function spawnServer(cwd, port, options) {
3748
4224
  if (options?.detached) {
@@ -3755,31 +4231,31 @@ function spawnAttached(cwd, port) {
3755
4231
  cwd,
3756
4232
  stdio: ["ignore", "pipe", "pipe"]
3757
4233
  });
3758
- return new Promise((resolve18) => {
3759
- const timeout = setTimeout(() => resolve18(null), 3e4);
4234
+ return new Promise((resolve19) => {
4235
+ const timeout = setTimeout(() => resolve19(null), 3e4);
3760
4236
  function checkOutput(data) {
3761
4237
  const match = data.toString().match(/listening on :(\d+)/);
3762
4238
  if (match) {
3763
4239
  clearTimeout(timeout);
3764
- resolve18({ child, actualPort: parseInt(match[1], 10) });
4240
+ resolve19({ child, actualPort: parseInt(match[1], 10) });
3765
4241
  }
3766
4242
  }
3767
4243
  child.stdout?.on("data", checkOutput);
3768
4244
  child.stderr?.on("data", checkOutput);
3769
4245
  child.on("error", () => {
3770
4246
  clearTimeout(timeout);
3771
- resolve18(null);
4247
+ resolve19(null);
3772
4248
  });
3773
4249
  child.on("exit", () => {
3774
4250
  clearTimeout(timeout);
3775
- resolve18(null);
4251
+ resolve19(null);
3776
4252
  });
3777
4253
  });
3778
4254
  }
3779
4255
  function spawnDetached(cwd, port, logDir) {
3780
- const logsDir = logDir ?? resolve13(cwd, ".volute", "logs");
3781
- mkdirSync5(logsDir, { recursive: true });
3782
- const logPath = resolve13(logsDir, "mind.log");
4256
+ const logsDir = logDir ?? resolve14(cwd, ".volute", "logs");
4257
+ mkdirSync6(logsDir, { recursive: true });
4258
+ const logPath = resolve14(logsDir, "mind.log");
3783
4259
  const logFd = openSync(logPath, "a");
3784
4260
  const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
3785
4261
  cwd,
@@ -3799,7 +4275,7 @@ function spawnDetached(cwd, port, logDir) {
3799
4275
  }
3800
4276
  const interval = setInterval(() => {
3801
4277
  try {
3802
- const content = readFileSync7(logPath, "utf-8");
4278
+ const content = readFileSync8(logPath, "utf-8");
3803
4279
  const match = content.match(/listening on :(\d+)/);
3804
4280
  if (match) {
3805
4281
  finish({ child, actualPort: parseInt(match[1], 10) });
@@ -3851,7 +4327,7 @@ async function verify(port) {
3851
4327
  }
3852
4328
 
3853
4329
  // src/web/api/variants.ts
3854
- var app13 = new Hono13().get("/:name/variants", async (c) => {
4330
+ var app16 = new Hono16().get("/:name/variants", async (c) => {
3855
4331
  const name = c.req.param("name");
3856
4332
  const entry = findMind(name);
3857
4333
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -3881,11 +4357,11 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3881
4357
  const err = validateBranchName(variantName);
3882
4358
  if (err) return c.json({ error: err }, 400);
3883
4359
  const projectRoot = mindDir(mindName);
3884
- const variantDir = resolve14(projectRoot, ".variants", variantName);
3885
- if (existsSync9(variantDir)) {
4360
+ const variantDir = resolve15(projectRoot, ".variants", variantName);
4361
+ if (existsSync10(variantDir)) {
3886
4362
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
3887
4363
  }
3888
- mkdirSync6(resolve14(projectRoot, ".variants"), { recursive: true });
4364
+ mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
3889
4365
  try {
3890
4366
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
3891
4367
  } catch (e) {
@@ -3898,7 +4374,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3898
4374
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
3899
4375
  await exec(cmd, args, {
3900
4376
  cwd: variantDir,
3901
- env: { ...process.env, HOME: resolve14(variantDir, "home") }
4377
+ env: { ...process.env, HOME: resolve15(variantDir, "home") }
3902
4378
  });
3903
4379
  } else {
3904
4380
  await exec("npm", ["install"], { cwd: variantDir });
@@ -3908,7 +4384,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3908
4384
  return c.json({ error: `npm install failed: ${msg}` }, 500);
3909
4385
  }
3910
4386
  if (body.soul) {
3911
- writeFileSync6(resolve14(variantDir, "home/SOUL.md"), body.soul);
4387
+ writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
3912
4388
  }
3913
4389
  const variantPort = body.port ?? nextPort();
3914
4390
  const variant = {
@@ -3946,7 +4422,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
3946
4422
  } catch {
3947
4423
  }
3948
4424
  const projectRoot = mindDir(mindName);
3949
- if (existsSync9(variant.path)) {
4425
+ if (existsSync10(variant.path)) {
3950
4426
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
3951
4427
  if (status) {
3952
4428
  try {
@@ -4003,7 +4479,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4003
4479
  } catch (e) {
4004
4480
  return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
4005
4481
  }
4006
- if (existsSync9(variant.path)) {
4482
+ if (existsSync10(variant.path)) {
4007
4483
  try {
4008
4484
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4009
4485
  } catch {
@@ -4020,7 +4496,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4020
4496
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4021
4497
  await exec(cmd, args, {
4022
4498
  cwd: projectRoot,
4023
- env: { ...process.env, HOME: resolve14(projectRoot, "home") }
4499
+ env: { ...process.env, HOME: resolve15(projectRoot, "home") }
4024
4500
  });
4025
4501
  } else {
4026
4502
  await exec("npm", ["install"], { cwd: projectRoot });
@@ -4063,7 +4539,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4063
4539
  } catch {
4064
4540
  }
4065
4541
  }
4066
- if (existsSync9(variant.path)) {
4542
+ if (existsSync10(variant.path)) {
4067
4543
  try {
4068
4544
  await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
4069
4545
  } catch {
@@ -4077,29 +4553,83 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
4077
4553
  chownMindDir(projectRoot, mindName);
4078
4554
  return c.json({ ok: true });
4079
4555
  });
4080
- var variants_default = app13;
4556
+ var variants_default = app16;
4557
+
4558
+ // src/web/api/volute/channels.ts
4559
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
4560
+ import { Hono as Hono17 } from "hono";
4561
+ import { z as z6 } from "zod";
4562
+ var createSchema = z6.object({
4563
+ name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
4564
+ });
4565
+ var app17 = new Hono17().get("/", async (c) => {
4566
+ const user = c.get("user");
4567
+ const channels = await listChannels();
4568
+ const results = await Promise.all(
4569
+ channels.map(async (ch) => {
4570
+ const participants = await getParticipants(ch.id);
4571
+ const isMember = participants.some((p) => p.userId === user.id);
4572
+ return { ...ch, participantCount: participants.length, isMember };
4573
+ })
4574
+ );
4575
+ return c.json(results);
4576
+ }).post("/", zValidator6("json", createSchema), async (c) => {
4577
+ const user = c.get("user");
4578
+ const body = c.req.valid("json");
4579
+ try {
4580
+ const ch = await createChannel(body.name, user.id);
4581
+ return c.json(ch, 201);
4582
+ } catch (err) {
4583
+ const cause = err instanceof Error ? err.cause : null;
4584
+ if (cause && /UNIQUE/i.test(cause.extendedCode ?? cause.message ?? "")) {
4585
+ return c.json({ error: "Channel already exists" }, 409);
4586
+ }
4587
+ throw err;
4588
+ }
4589
+ }).post("/:name/join", async (c) => {
4590
+ const name = c.req.param("name");
4591
+ const user = c.get("user");
4592
+ const ch = await getChannelByName(name);
4593
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4594
+ await joinChannel(ch.id, user.id);
4595
+ return c.json({ ok: true, conversationId: ch.id });
4596
+ }).post("/:name/leave", async (c) => {
4597
+ const name = c.req.param("name");
4598
+ const user = c.get("user");
4599
+ const ch = await getChannelByName(name);
4600
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4601
+ await leaveChannel(ch.id, user.id);
4602
+ return c.json({ ok: true });
4603
+ }).get("/:name/members", async (c) => {
4604
+ const name = c.req.param("name");
4605
+ const ch = await getChannelByName(name);
4606
+ if (!ch) return c.json({ error: "Channel not found" }, 404);
4607
+ const participants = await getParticipants(ch.id);
4608
+ return c.json(participants);
4609
+ });
4610
+ var channels_default2 = app17;
4081
4611
 
4082
4612
  // src/web/api/volute/chat.ts
4083
- import { readFileSync as readFileSync8 } from "fs";
4084
- import { resolve as resolve15 } from "path";
4085
- import { zValidator as zValidator4 } from "@hono/zod-validator";
4086
- import { Hono as Hono14 } from "hono";
4613
+ import { readFileSync as readFileSync9 } from "fs";
4614
+ import { resolve as resolve16 } from "path";
4615
+ import { zValidator as zValidator7 } from "@hono/zod-validator";
4616
+ import { Hono as Hono18 } from "hono";
4087
4617
  import { streamSSE as streamSSE3 } from "hono/streaming";
4088
- import { z as z4 } from "zod";
4089
- var chatSchema = z4.object({
4090
- message: z4.string().optional(),
4091
- conversationId: z4.string().optional(),
4092
- sender: z4.string().optional(),
4093
- images: z4.array(
4094
- z4.object({
4095
- media_type: z4.string(),
4096
- data: z4.string()
4618
+ import { z as z7 } from "zod";
4619
+ var chatSchema = z7.object({
4620
+ message: z7.string().optional(),
4621
+ conversationId: z7.string().optional(),
4622
+ sender: z7.string().optional(),
4623
+ images: z7.array(
4624
+ z7.object({
4625
+ media_type: z7.string(),
4626
+ data: z7.string()
4097
4627
  })
4098
4628
  ).optional()
4099
4629
  });
4100
4630
  function getDaemonUrl() {
4101
4631
  try {
4102
- const data = JSON.parse(readFileSync8(resolve15(voluteHome(), "daemon.json"), "utf-8"));
4632
+ const data = JSON.parse(readFileSync9(resolve16(voluteHome(), "daemon.json"), "utf-8"));
4103
4633
  return `http://${daemonLoopback()}:${data.port}`;
4104
4634
  } catch (err) {
4105
4635
  throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
@@ -4115,7 +4645,7 @@ function daemonFetchInternal(path, body) {
4115
4645
  if (token) headers.Authorization = `Bearer ${token}`;
4116
4646
  return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
4117
4647
  }
4118
- var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
4648
+ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
4119
4649
  const name = c.req.param("name");
4120
4650
  const [baseName] = name.split("@", 2);
4121
4651
  const entry = findMind(baseName);
@@ -4176,7 +4706,7 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4176
4706
  const participants = await getParticipants(conversationId);
4177
4707
  const mindParticipants = participants.filter((p) => p.userType === "mind");
4178
4708
  const participantNames = participants.map((p) => p.username);
4179
- const { getMindManager: getMindManager2 } = await import("./mind-manager-ETNCPQJN.js");
4709
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-Z7O7PN2O.js");
4180
4710
  const manager = getMindManager2();
4181
4711
  const runningMinds = mindParticipants.map((ap) => {
4182
4712
  const mindKey = ap.username === baseName ? name : ap.username;
@@ -4221,8 +4751,8 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4221
4751
  });
4222
4752
  daemonFetchInternal(`/api/minds/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
4223
4753
  if (!res.ok) {
4224
- const text2 = await res.text().catch(() => "");
4225
- console.error(`[chat] mind ${mindName} responded ${res.status}: ${text2}`);
4754
+ const text = await res.text().catch(() => "");
4755
+ console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
4226
4756
  }
4227
4757
  }).catch((err) => {
4228
4758
  console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
@@ -4246,27 +4776,116 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
4246
4776
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
4247
4777
  });
4248
4778
  }, 15e3);
4249
- await new Promise((resolve18) => {
4779
+ await new Promise((resolve19) => {
4250
4780
  stream.onAbort(() => {
4251
4781
  unsubscribe();
4252
4782
  clearInterval(keepAlive);
4253
- resolve18();
4783
+ resolve19();
4254
4784
  });
4255
4785
  });
4256
4786
  });
4257
4787
  });
4258
- var chat_default = app14;
4788
+ var unifiedChatSchema = z7.object({
4789
+ message: z7.string().optional(),
4790
+ conversationId: z7.string(),
4791
+ images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
4792
+ });
4793
+ var unifiedChatApp = new Hono18().post(
4794
+ "/chat",
4795
+ zValidator7("json", unifiedChatSchema),
4796
+ async (c) => {
4797
+ const user = c.get("user");
4798
+ const body = c.req.valid("json");
4799
+ if (!body.message && (!body.images || body.images.length === 0)) {
4800
+ return c.json({ error: "message or images required" }, 400);
4801
+ }
4802
+ const conv = await getConversation(body.conversationId);
4803
+ if (!conv) return c.json({ error: "Conversation not found" }, 404);
4804
+ if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
4805
+ return c.json({ error: "Conversation not found" }, 404);
4806
+ }
4807
+ const senderName = user.username;
4808
+ const contentBlocks = [];
4809
+ if (body.message) contentBlocks.push({ type: "text", text: body.message });
4810
+ if (body.images) {
4811
+ for (const img of body.images) {
4812
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
4813
+ }
4814
+ }
4815
+ await addMessage(body.conversationId, "user", senderName, contentBlocks);
4816
+ const participants = await getParticipants(body.conversationId);
4817
+ const mindParticipants = participants.filter((p) => p.userType === "mind");
4818
+ const participantNames = participants.map((p) => p.username);
4819
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-Z7O7PN2O.js");
4820
+ const manager = getMindManager2();
4821
+ const runningMinds = mindParticipants.map((ap) => manager.isRunning(ap.username) ? ap.username : null).filter((n) => n !== null && n !== senderName);
4822
+ const isDM = conv.type === "dm" && participants.length === 2;
4823
+ const channelEntry = {
4824
+ platformId: body.conversationId,
4825
+ platform: "volute",
4826
+ name: conv.title ?? void 0,
4827
+ type: conv.type === "channel" ? "group" : isDM ? "dm" : "group"
4828
+ };
4829
+ for (const ap of mindParticipants) {
4830
+ const slug = buildVoluteSlug({
4831
+ participants,
4832
+ mindUsername: ap.username,
4833
+ convTitle: conv.title,
4834
+ conversationId: conv.id,
4835
+ convType: conv.type,
4836
+ convName: conv.name
4837
+ });
4838
+ try {
4839
+ writeChannelEntry(ap.username, slug, channelEntry);
4840
+ } catch (err) {
4841
+ console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
4842
+ }
4843
+ }
4844
+ for (const mindName of runningMinds) {
4845
+ const channel = buildVoluteSlug({
4846
+ participants,
4847
+ mindUsername: mindName,
4848
+ convTitle: conv.title,
4849
+ conversationId: body.conversationId,
4850
+ convType: conv.type,
4851
+ convName: conv.name
4852
+ });
4853
+ const typingMap = getTypingMap();
4854
+ const currentlyTyping = typingMap.get(channel);
4855
+ const payload = JSON.stringify({
4856
+ content: contentBlocks,
4857
+ channel,
4858
+ conversationId: body.conversationId,
4859
+ sender: senderName,
4860
+ participants: participantNames,
4861
+ participantCount: participants.length,
4862
+ isDM,
4863
+ ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
4864
+ });
4865
+ daemonFetchInternal(`/api/minds/${encodeURIComponent(mindName)}/message`, payload).then(async (res) => {
4866
+ if (!res.ok) {
4867
+ const text = await res.text().catch(() => "");
4868
+ console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
4869
+ }
4870
+ }).catch((err) => {
4871
+ console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
4872
+ });
4873
+ }
4874
+ return c.json({ ok: true, conversationId: body.conversationId });
4875
+ }
4876
+ );
4877
+ var chat_default = app18;
4259
4878
 
4260
4879
  // src/web/api/volute/conversations.ts
4261
- import { zValidator as zValidator5 } from "@hono/zod-validator";
4262
- import { Hono as Hono15 } from "hono";
4263
- import { z as z5 } from "zod";
4264
- var createConvSchema = z5.object({
4265
- title: z5.string().optional(),
4266
- participantIds: z5.array(z5.number()).optional(),
4267
- participantNames: z5.array(z5.string()).optional()
4880
+ import { zValidator as zValidator8 } from "@hono/zod-validator";
4881
+ import { Hono as Hono19 } from "hono";
4882
+ import { z as z8 } from "zod";
4883
+ var createConvSchema = z8.object({
4884
+ title: z8.string().optional(),
4885
+ participantIds: z8.array(z8.number()).optional(),
4886
+ participantNames: z8.array(z8.string()).optional()
4268
4887
  });
4269
- var app15 = new Hono15().get("/:name/conversations", async (c) => {
4888
+ var app19 = new Hono19().get("/:name/conversations", async (c) => {
4270
4889
  const name = c.req.param("name");
4271
4890
  const user = c.get("user");
4272
4891
  let lookupId = user.id;
@@ -4275,9 +4894,9 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
4275
4894
  lookupId = mindUser.id;
4276
4895
  }
4277
4896
  const all = await listConversationsForUser(lookupId);
4278
- const convs = all.filter((c2) => c2.mind_name === name);
4897
+ const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
4279
4898
  return c.json(convs);
4280
- }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
4899
+ }).post("/:name/conversations", zValidator8("json", createConvSchema), async (c) => {
4281
4900
  const name = c.req.param("name");
4282
4901
  const user = c.get("user");
4283
4902
  const body = c.req.valid("json");
@@ -4351,17 +4970,18 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
4351
4970
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4352
4971
  return c.json({ ok: true });
4353
4972
  });
4354
- var conversations_default = app15;
4973
+ var conversations_default = app19;
4355
4974
 
4356
4975
  // src/web/api/volute/user-conversations.ts
4357
- import { zValidator as zValidator6 } from "@hono/zod-validator";
4358
- import { Hono as Hono16 } from "hono";
4359
- import { z as z6 } from "zod";
4360
- var createSchema = z6.object({
4361
- title: z6.string().optional(),
4362
- participantNames: z6.array(z6.string()).min(1)
4976
+ import { zValidator as zValidator9 } from "@hono/zod-validator";
4977
+ import { Hono as Hono20 } from "hono";
4978
+ import { streamSSE as streamSSE4 } from "hono/streaming";
4979
+ import { z as z9 } from "zod";
4980
+ var createSchema2 = z9.object({
4981
+ title: z9.string().optional(),
4982
+ participantNames: z9.array(z9.string()).min(1)
4363
4983
  });
4364
- var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4984
+ var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
4365
4985
  const user = c.get("user");
4366
4986
  const convs = await listConversationsWithParticipants(user.id);
4367
4987
  return c.json(convs);
@@ -4373,7 +4993,7 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4373
4993
  }
4374
4994
  const msgs = await getMessages(id);
4375
4995
  return c.json(msgs);
4376
- }).post("/", zValidator6("json", createSchema), async (c) => {
4996
+ }).post("/", zValidator9("json", createSchema2), async (c) => {
4377
4997
  const user = c.get("user");
4378
4998
  const body = c.req.valid("json");
4379
4999
  const participantIds = /* @__PURE__ */ new Set();
@@ -4403,6 +5023,31 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4403
5023
  participantIds: [...participantIds]
4404
5024
  });
4405
5025
  return c.json(conv, 201);
5026
+ }).get("/:id/events", async (c) => {
5027
+ const conversationId = c.req.param("id");
5028
+ const user = c.get("user");
5029
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
5030
+ return c.json({ error: "Conversation not found" }, 404);
5031
+ }
5032
+ return streamSSE4(c, async (stream) => {
5033
+ const unsubscribe = subscribe(conversationId, (event) => {
5034
+ stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
5035
+ if (!stream.aborted) console.error("[chat] SSE write error:", err);
5036
+ });
5037
+ });
5038
+ const keepAlive = setInterval(() => {
5039
+ stream.writeSSE({ data: "" }).catch((err) => {
5040
+ if (!stream.aborted) console.error("[chat] SSE ping error:", err);
5041
+ });
5042
+ }, 15e3);
5043
+ await new Promise((resolve19) => {
5044
+ stream.onAbort(() => {
5045
+ unsubscribe();
5046
+ clearInterval(keepAlive);
5047
+ resolve19();
5048
+ });
5049
+ });
5050
+ });
4406
5051
  }).delete("/:id", async (c) => {
4407
5052
  const id = c.req.param("id");
4408
5053
  const user = c.get("user");
@@ -4410,12 +5055,12 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
4410
5055
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
4411
5056
  return c.json({ ok: true });
4412
5057
  });
4413
- var user_conversations_default = app16;
5058
+ var user_conversations_default = app20;
4414
5059
 
4415
5060
  // src/web/app.ts
4416
5061
  var httpLog = logger_default.child("http");
4417
- var app17 = new Hono17();
4418
- app17.onError((err, c) => {
5062
+ var app21 = new Hono21();
5063
+ app21.onError((err, c) => {
4419
5064
  if (err instanceof HTTPException) {
4420
5065
  return err.getResponse();
4421
5066
  }
@@ -4426,10 +5071,10 @@ app17.onError((err, c) => {
4426
5071
  });
4427
5072
  return c.json({ error: "Internal server error" }, 500);
4428
5073
  });
4429
- app17.notFound((c) => {
5074
+ app21.notFound((c) => {
4430
5075
  return c.json({ error: "Not found" }, 404);
4431
5076
  });
4432
- app17.use("*", async (c, next) => {
5077
+ app21.use("*", async (c, next) => {
4433
5078
  const start = Date.now();
4434
5079
  await next();
4435
5080
  const duration = Date.now() - start;
@@ -4440,7 +5085,7 @@ app17.use("*", async (c, next) => {
4440
5085
  httpLog.debug("request", data);
4441
5086
  }
4442
5087
  });
4443
- app17.get("/api/health", (c) => {
5088
+ app21.get("/api/health", (c) => {
4444
5089
  let version = "unknown";
4445
5090
  let cached = null;
4446
5091
  try {
@@ -4455,15 +5100,18 @@ app17.get("/api/health", (c) => {
4455
5100
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
4456
5101
  });
4457
5102
  });
4458
- app17.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
4459
- app17.use("/api/*", csrf());
4460
- app17.use("/api/minds/*", authMiddleware);
4461
- app17.use("/api/conversations/*", authMiddleware);
4462
- app17.use("/api/system/*", authMiddleware);
4463
- app17.use("/api/env/*", authMiddleware);
4464
- app17.route("/pages", pages_default);
4465
- var routes = app17.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", env_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/conversations", user_conversations_default);
4466
- var app_default = app17;
5103
+ app21.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
5104
+ app21.use("/api/*", csrf());
5105
+ app21.use("/api/minds/*", authMiddleware);
5106
+ app21.use("/api/conversations/*", authMiddleware);
5107
+ app21.use("/api/volute/*", authMiddleware);
5108
+ app21.use("/api/system/*", authMiddleware);
5109
+ app21.use("/api/env/*", authMiddleware);
5110
+ app21.use("/api/prompts/*", authMiddleware);
5111
+ app21.use("/api/skills/*", authMiddleware);
5112
+ app21.route("/pages", pages_default);
5113
+ var routes = app21.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", env_default).route("/api/minds", mind_skills_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/prompts", prompts_default).route("/api/skills", skills_default).route("/api/conversations", user_conversations_default).route("/api/volute/channels", channels_default2).route("/api/volute", unifiedChatApp);
5114
+ var app_default = app21;
4467
5115
 
4468
5116
  // src/web/server.ts
4469
5117
  var MIME_TYPES2 = {
@@ -4480,20 +5128,20 @@ async function startServer({
4480
5128
  hostname = "127.0.0.1"
4481
5129
  }) {
4482
5130
  let assetsDir = "";
4483
- let searchDir = dirname3(new URL(import.meta.url).pathname);
5131
+ let searchDir = dirname2(new URL(import.meta.url).pathname);
4484
5132
  for (let i = 0; i < 5; i++) {
4485
- const candidate = resolve16(searchDir, "dist", "web-assets");
4486
- if (existsSync10(candidate)) {
5133
+ const candidate = resolve17(searchDir, "dist", "web-assets");
5134
+ if (existsSync11(candidate)) {
4487
5135
  assetsDir = candidate;
4488
5136
  break;
4489
5137
  }
4490
- searchDir = dirname3(searchDir);
5138
+ searchDir = dirname2(searchDir);
4491
5139
  }
4492
5140
  if (assetsDir) {
4493
5141
  app_default.get("*", async (c) => {
4494
5142
  const urlPath = new URL(c.req.url).pathname;
4495
5143
  if (urlPath.startsWith("/api/")) return c.notFound();
4496
- const filePath = resolve16(assetsDir, urlPath.slice(1));
5144
+ const filePath = resolve17(assetsDir, urlPath.slice(1));
4497
5145
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
4498
5146
  const s = await stat2(filePath).catch(() => null);
4499
5147
  if (s?.isFile()) {
@@ -4502,7 +5150,7 @@ async function startServer({
4502
5150
  const body = await readFile3(filePath);
4503
5151
  return c.body(body, 200, { "Content-Type": mime });
4504
5152
  }
4505
- const indexPath = resolve16(assetsDir, "index.html");
5153
+ const indexPath = resolve17(assetsDir, "index.html");
4506
5154
  const indexStat = await stat2(indexPath).catch(() => null);
4507
5155
  if (indexStat?.isFile()) {
4508
5156
  const body = await readFile3(indexPath, "utf-8");
@@ -4512,10 +5160,10 @@ async function startServer({
4512
5160
  });
4513
5161
  }
4514
5162
  const server = serve({ fetch: app_default.fetch, port, hostname });
4515
- await new Promise((resolve18, reject) => {
5163
+ await new Promise((resolve19, reject) => {
4516
5164
  server.on("listening", () => {
4517
5165
  logger_default.info("Volute UI running", { hostname, port });
4518
- resolve18();
5166
+ resolve19();
4519
5167
  });
4520
5168
  server.on("error", (err) => {
4521
5169
  reject(err);
@@ -4526,14 +5174,14 @@ async function startServer({
4526
5174
 
4527
5175
  // src/daemon.ts
4528
5176
  if (!process.env.VOLUTE_HOME) {
4529
- process.env.VOLUTE_HOME = resolve17(homedir2(), ".volute");
5177
+ process.env.VOLUTE_HOME = resolve18(homedir2(), ".volute");
4530
5178
  }
4531
5179
  async function startDaemon(opts) {
4532
5180
  const { port, hostname } = opts;
4533
5181
  const myPid = String(process.pid);
4534
5182
  const home = voluteHome();
4535
5183
  if (!opts.foreground) {
4536
- const rotatingLog = new RotatingLog(resolve17(home, "daemon.log"));
5184
+ const rotatingLog = new RotatingLog(resolve18(home, "daemon.log"));
4537
5185
  logger_default.setOutput((line) => rotatingLog.write(`${line}
4538
5186
  `));
4539
5187
  const write = (...args) => rotatingLog.write(`${format(...args)}
@@ -4543,9 +5191,9 @@ async function startDaemon(opts) {
4543
5191
  console.warn = write;
4544
5192
  console.info = write;
4545
5193
  }
4546
- const DAEMON_PID_PATH = resolve17(home, "daemon.pid");
4547
- const DAEMON_JSON_PATH = resolve17(home, "daemon.json");
4548
- mkdirSync7(home, { recursive: true });
5194
+ const DAEMON_PID_PATH = resolve18(home, "daemon.pid");
5195
+ const DAEMON_JSON_PATH = resolve18(home, "daemon.json");
5196
+ mkdirSync8(home, { recursive: true });
4549
5197
  migrateAgentsToMinds();
4550
5198
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
4551
5199
  process.env.VOLUTE_DAEMON_TOKEN = token;
@@ -4562,8 +5210,8 @@ async function startDaemon(opts) {
4562
5210
  }
4563
5211
  throw err;
4564
5212
  }
4565
- writeFileSync7(DAEMON_PID_PATH, myPid, { mode: 420 });
4566
- writeFileSync7(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
5213
+ writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
5214
+ writeFileSync8(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
4567
5215
  `, {
4568
5216
  mode: 420
4569
5217
  });
@@ -4622,13 +5270,13 @@ async function startDaemon(opts) {
4622
5270
  logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
4623
5271
  function cleanup() {
4624
5272
  try {
4625
- if (readFileSync9(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
5273
+ if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
4626
5274
  unlinkSync2(DAEMON_PID_PATH);
4627
5275
  }
4628
5276
  } catch {
4629
5277
  }
4630
5278
  try {
4631
- const data = JSON.parse(readFileSync9(DAEMON_JSON_PATH, "utf-8"));
5279
+ const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
4632
5280
  if (data.token === token) {
4633
5281
  unlinkSync2(DAEMON_JSON_PATH);
4634
5282
  }