volute 0.11.4 → 0.13.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.
- package/dist/{agent-2AQPI3QV.js → agent-HYX2ZTFM.js} +10 -10
- package/dist/{agent-manager-AZUDAKCP.js → agent-manager-HHBAAL2D.js} +4 -4
- package/dist/{channel-2WHBRDTD.js → channel-72YET5JC.js} +2 -2
- package/dist/{chunk-QF22MYDJ.js → chunk-BJDLYTPS.js} +1 -1
- package/dist/{chunk-46S7YHUB.js → chunk-EN3NHRQC.js} +12 -19
- package/dist/{chunk-DP2DX4WV.js → chunk-ESTOWEG2.js} +12 -3
- package/dist/{chunk-N6MLQ26B.js → chunk-GPZCPGV3.js} +11 -1
- package/dist/{chunk-ZKNBD5P3.js → chunk-IELXXS7E.js} +2 -2
- package/dist/{chunk-STOEJOJO.js → chunk-J3IHIXDB.js} +1 -1
- package/dist/{chunk-R3VB7NF5.js → chunk-KYHC7LHS.js} +2 -2
- package/dist/{chunk-YY2QX2J6.js → chunk-LG4ROCHN.js} +1 -1
- package/dist/{chunk-YTOPX4PB.js → chunk-NQKKTRET.js} +28 -37
- package/dist/{chunk-D5EVQTGJ.js → chunk-PEQQ7MRI.js} +2 -2
- package/dist/{chunk-LIPPXNIE.js → chunk-PTK3GBCG.js} +17 -18
- package/dist/{chunk-RGWADNLT.js → chunk-RH3XLDY2.js} +2 -2
- package/dist/{chunk-WTJI3JVR.js → chunk-VRAOTXDF.js} +9 -6
- package/dist/chunk-XUA3JUFK.js +121 -0
- package/dist/cli.js +26 -17
- package/dist/{connector-L2HBLZBW.js → connector-Z5KYVTZ5.js} +2 -2
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/slack.js +2 -2
- package/dist/connectors/telegram.js +2 -2
- package/dist/{create-VBZZNJOG.js → create-AAI52BC2.js} +1 -1
- package/dist/{daemon-client-P44NU3KU.js → daemon-client-WXN43USO.js} +2 -2
- package/dist/{daemon-restart-5W5AGBZ2.js → daemon-restart-4OXIGWV6.js} +6 -5
- package/dist/daemon.js +533 -557
- package/dist/{delete-BOTVU4YO.js → delete-BJ3LNU2I.js} +1 -1
- package/dist/down-IMZE7V42.js +14 -0
- package/dist/{env-CGORIKVF.js → env-EOO2C7L7.js} +2 -2
- package/dist/{history-NI5QP27M.js → history-J7TURCZS.js} +2 -2
- package/dist/{import-2BZUWT23.js → import-TKF67X4R.js} +3 -3
- package/dist/{logs-APWVWGNX.js → logs-KHBOS6IZ.js} +2 -2
- package/dist/{package-KVUXPTEW.js → package-TPXKJXLG.js} +1 -1
- package/dist/{restart-CCYM3MEC.js → restart-CMP63H6A.js} +2 -2
- package/dist/{schedule-E4MFGYSA.js → schedule-YMAJZ52M.js} +2 -2
- package/dist/seed-M6QPHFTV.js +68 -0
- package/dist/{send-X6OQGSD6.js → send-EZYEIVMA.js} +18 -6
- package/dist/{service-UL3OCODG.js → service-DDFZA7Q3.js} +4 -3
- package/dist/{setup-7N4KYOYN.js → setup-DCZWBBD7.js} +7 -7
- package/dist/sprout-EAWETTWZ.js +89 -0
- package/dist/{start-6YRS6FF6.js → start-54JXJ7VV.js} +2 -2
- package/dist/{status-DFWM342I.js → status-PMMS4XUH.js} +7 -5
- package/dist/{stop-UQSNF4CG.js → stop-WXU42NWP.js} +2 -2
- package/dist/{up-365HL7UT.js → up-N2OWDCT6.js} +5 -4
- package/dist/{update-PV3XM6DX.js → update-FXLGIIWH.js} +5 -4
- package/dist/{update-check-YPGH5X4E.js → update-check-NBHTSTHK.js} +2 -2
- package/dist/{upgrade-RSE4CZNE.js → upgrade-TEI7N6CQ.js} +1 -1
- package/dist/{variant-7IZF6OWO.js → variant-ZCZS3JAP.js} +4 -4
- package/dist/web-assets/assets/index-TqXd1QOX.js +307 -0
- package/dist/web-assets/index.html +1 -1
- package/package.json +1 -1
- package/templates/_base/_skills/orientation/SKILL.md +58 -0
- package/templates/_base/home/.config/config.json.tmpl +3 -0
- package/templates/_base/src/lib/startup.ts +8 -4
- package/templates/agent-sdk/volute-template.json +1 -1
- package/templates/pi/home/.config/config.json.tmpl +3 -0
- package/templates/pi/volute-template.json +1 -1
- package/dist/down-O2EQJ5DO.js +0 -13
- package/dist/web-assets/assets/index-D-3zx6vs.js +0 -307
- package/templates/_base/home/.config/volute.json.tmpl +0 -3
- package/templates/pi/home/.config/volute.json.tmpl +0 -3
package/dist/daemon.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
applyInitFiles,
|
|
4
|
+
composeTemplate,
|
|
5
|
+
copyTemplateToDir,
|
|
6
|
+
findTemplatesRoot,
|
|
7
|
+
listFiles
|
|
8
|
+
} from "./chunk-XUA3JUFK.js";
|
|
2
9
|
import {
|
|
3
10
|
RotatingLog,
|
|
4
11
|
clearJsonMap,
|
|
@@ -6,16 +13,7 @@ import {
|
|
|
6
13
|
initAgentManager,
|
|
7
14
|
loadJsonMap,
|
|
8
15
|
saveJsonMap
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
applyIsolation,
|
|
12
|
-
chownAgentDir,
|
|
13
|
-
createAgentUser,
|
|
14
|
-
deleteAgentUser,
|
|
15
|
-
ensureVoluteGroup,
|
|
16
|
-
getAgentUserIds,
|
|
17
|
-
isIsolationEnabled
|
|
18
|
-
} from "./chunk-46S7YHUB.js";
|
|
16
|
+
} from "./chunk-NQKKTRET.js";
|
|
19
17
|
import {
|
|
20
18
|
findOpenClawSession,
|
|
21
19
|
importOpenClawConnectors,
|
|
@@ -23,33 +21,41 @@ import {
|
|
|
23
21
|
parseNameFromIdentity,
|
|
24
22
|
readVoluteConfig,
|
|
25
23
|
writeVoluteConfig
|
|
26
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-KYHC7LHS.js";
|
|
27
25
|
import {
|
|
28
26
|
agentEnvPath,
|
|
29
27
|
loadMergedEnv,
|
|
30
28
|
readEnv,
|
|
31
29
|
sharedEnvPath,
|
|
32
30
|
writeEnv
|
|
33
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-BJDLYTPS.js";
|
|
34
32
|
import {
|
|
35
33
|
CHANNELS,
|
|
36
34
|
getChannelDriver
|
|
37
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-PTK3GBCG.js";
|
|
38
36
|
import {
|
|
39
37
|
exec,
|
|
40
38
|
gitExec,
|
|
41
39
|
resolveVoluteBin
|
|
42
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-VRAOTXDF.js";
|
|
41
|
+
import {
|
|
42
|
+
chownAgentDir,
|
|
43
|
+
createAgentUser,
|
|
44
|
+
deleteAgentUser,
|
|
45
|
+
ensureVoluteGroup,
|
|
46
|
+
isIsolationEnabled,
|
|
47
|
+
wrapForIsolation
|
|
48
|
+
} from "./chunk-EN3NHRQC.js";
|
|
43
49
|
import {
|
|
44
50
|
checkForUpdate,
|
|
45
51
|
checkForUpdateCached,
|
|
46
52
|
getCurrentVersion
|
|
47
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-LG4ROCHN.js";
|
|
48
54
|
import "./chunk-D424ZQGI.js";
|
|
49
55
|
import {
|
|
50
|
-
|
|
56
|
+
buildVoluteSlug,
|
|
51
57
|
writeChannelEntry
|
|
52
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-GPZCPGV3.js";
|
|
53
59
|
import {
|
|
54
60
|
addAgent,
|
|
55
61
|
addVariant,
|
|
@@ -72,16 +78,16 @@ import {
|
|
|
72
78
|
validateAgentName,
|
|
73
79
|
validateBranchName,
|
|
74
80
|
voluteHome
|
|
75
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-ESTOWEG2.js";
|
|
76
82
|
import {
|
|
77
83
|
__export
|
|
78
84
|
} from "./chunk-K3NQKI34.js";
|
|
79
85
|
|
|
80
86
|
// src/daemon.ts
|
|
81
87
|
import { randomBytes } from "crypto";
|
|
82
|
-
import { mkdirSync as
|
|
88
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
83
89
|
import { homedir as homedir2 } from "os";
|
|
84
|
-
import { resolve as
|
|
90
|
+
import { resolve as resolve15 } from "path";
|
|
85
91
|
import { format } from "util";
|
|
86
92
|
|
|
87
93
|
// src/lib/connector-manager.ts
|
|
@@ -248,8 +254,14 @@ var ConnectorManager = class {
|
|
|
248
254
|
const logsDir = resolve2(agentStateDir, "logs");
|
|
249
255
|
mkdirSync(logsDir, { recursive: true });
|
|
250
256
|
if (isIsolationEnabled()) {
|
|
251
|
-
|
|
252
|
-
|
|
257
|
+
try {
|
|
258
|
+
const [base] = agentName.split("@", 2);
|
|
259
|
+
chownAgentDir(agentStateDir, base);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Cannot start connector ${type} for ${agentName}: failed to set ownership on state directory ${agentStateDir}: ${err instanceof Error ? err.message : err}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
253
265
|
}
|
|
254
266
|
const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
|
|
255
267
|
const agentEnv = loadMergedEnv(agentName);
|
|
@@ -271,8 +283,8 @@ var ConnectorManager = class {
|
|
|
271
283
|
...connectorEnv
|
|
272
284
|
}
|
|
273
285
|
};
|
|
274
|
-
|
|
275
|
-
const child = spawn(
|
|
286
|
+
const [spawnCmd, spawnArgs] = wrapForIsolation(runtime, [connectorScript], agentName);
|
|
287
|
+
const child = spawn(spawnCmd, spawnArgs, spawnOpts);
|
|
276
288
|
let lastStderr = "";
|
|
277
289
|
child.stdout?.pipe(logStream);
|
|
278
290
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -326,19 +338,19 @@ var ConnectorManager = class {
|
|
|
326
338
|
const stopKey = `${agentName}:${type}`;
|
|
327
339
|
this.stopping.add(stopKey);
|
|
328
340
|
agentMap.delete(type);
|
|
329
|
-
await new Promise((
|
|
330
|
-
tracked.child.on("exit", () =>
|
|
341
|
+
await new Promise((resolve16) => {
|
|
342
|
+
tracked.child.on("exit", () => resolve16());
|
|
331
343
|
try {
|
|
332
344
|
tracked.child.kill("SIGTERM");
|
|
333
345
|
} catch {
|
|
334
|
-
|
|
346
|
+
resolve16();
|
|
335
347
|
}
|
|
336
348
|
setTimeout(() => {
|
|
337
349
|
try {
|
|
338
350
|
tracked.child.kill("SIGKILL");
|
|
339
351
|
} catch {
|
|
340
352
|
}
|
|
341
|
-
|
|
353
|
+
resolve16();
|
|
342
354
|
}, 5e3);
|
|
343
355
|
});
|
|
344
356
|
this.stopping.delete(stopKey);
|
|
@@ -1048,9 +1060,9 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
1048
1060
|
});
|
|
1049
1061
|
|
|
1050
1062
|
// src/web/server.ts
|
|
1051
|
-
import { existsSync as
|
|
1063
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1052
1064
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
1053
|
-
import { dirname as
|
|
1065
|
+
import { dirname as dirname3, extname, resolve as resolve14 } from "path";
|
|
1054
1066
|
import { serve } from "@hono/node-server";
|
|
1055
1067
|
|
|
1056
1068
|
// src/lib/log-buffer.ts
|
|
@@ -1105,17 +1117,19 @@ import { HTTPException } from "hono/http-exception";
|
|
|
1105
1117
|
|
|
1106
1118
|
// src/web/routes/agents.ts
|
|
1107
1119
|
import {
|
|
1108
|
-
cpSync
|
|
1109
|
-
existsSync as
|
|
1110
|
-
mkdirSync as
|
|
1111
|
-
readdirSync as
|
|
1112
|
-
readFileSync as
|
|
1113
|
-
rmSync
|
|
1114
|
-
writeFileSync as
|
|
1120
|
+
cpSync,
|
|
1121
|
+
existsSync as existsSync5,
|
|
1122
|
+
mkdirSync as mkdirSync4,
|
|
1123
|
+
readdirSync as readdirSync3,
|
|
1124
|
+
readFileSync as readFileSync5,
|
|
1125
|
+
rmSync,
|
|
1126
|
+
writeFileSync as writeFileSync4
|
|
1115
1127
|
} from "fs";
|
|
1116
|
-
import { resolve as
|
|
1117
|
-
import {
|
|
1128
|
+
import { resolve as resolve8 } from "path";
|
|
1129
|
+
import { zValidator } from "@hono/zod-validator";
|
|
1130
|
+
import { and as and3, desc as desc2, eq as eq4, sql as sql3 } from "drizzle-orm";
|
|
1118
1131
|
import { Hono } from "hono";
|
|
1132
|
+
import { z } from "zod";
|
|
1119
1133
|
|
|
1120
1134
|
// src/lib/consolidate.ts
|
|
1121
1135
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -1188,14 +1202,209 @@ ${content2}`);
|
|
|
1188
1202
|
}
|
|
1189
1203
|
}
|
|
1190
1204
|
|
|
1191
|
-
// src/lib/
|
|
1205
|
+
// src/lib/conversations.ts
|
|
1192
1206
|
import { randomUUID } from "crypto";
|
|
1207
|
+
import { and as and2, desc, eq as eq3, inArray, isNull, sql as sql2 } from "drizzle-orm";
|
|
1208
|
+
|
|
1209
|
+
// src/lib/conversation-events.ts
|
|
1210
|
+
var subscribers = /* @__PURE__ */ new Map();
|
|
1211
|
+
function subscribe(conversationId, callback) {
|
|
1212
|
+
let set = subscribers.get(conversationId);
|
|
1213
|
+
if (!set) {
|
|
1214
|
+
set = /* @__PURE__ */ new Set();
|
|
1215
|
+
subscribers.set(conversationId, set);
|
|
1216
|
+
}
|
|
1217
|
+
set.add(callback);
|
|
1218
|
+
return () => {
|
|
1219
|
+
set.delete(callback);
|
|
1220
|
+
if (set.size === 0) subscribers.delete(conversationId);
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function publish(conversationId, event) {
|
|
1224
|
+
const set = subscribers.get(conversationId);
|
|
1225
|
+
if (!set) return;
|
|
1226
|
+
for (const cb of set) {
|
|
1227
|
+
try {
|
|
1228
|
+
cb(event);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
console.error("[conversation-events] subscriber threw:", err);
|
|
1231
|
+
set.delete(cb);
|
|
1232
|
+
if (set.size === 0) subscribers.delete(conversationId);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/lib/conversations.ts
|
|
1238
|
+
async function createConversation(agentName, channel, opts) {
|
|
1239
|
+
const db2 = await getDb();
|
|
1240
|
+
const id = randomUUID();
|
|
1241
|
+
await db2.insert(conversations).values({
|
|
1242
|
+
id,
|
|
1243
|
+
agent_name: agentName,
|
|
1244
|
+
channel,
|
|
1245
|
+
user_id: opts?.userId ?? null,
|
|
1246
|
+
title: opts?.title ?? null
|
|
1247
|
+
});
|
|
1248
|
+
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
1249
|
+
await db2.insert(conversationParticipants).values(
|
|
1250
|
+
opts.participantIds.map((uid, i) => ({
|
|
1251
|
+
conversation_id: id,
|
|
1252
|
+
user_id: uid,
|
|
1253
|
+
role: i === 0 ? "owner" : "member"
|
|
1254
|
+
}))
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
return {
|
|
1258
|
+
id,
|
|
1259
|
+
agent_name: agentName,
|
|
1260
|
+
channel,
|
|
1261
|
+
user_id: opts?.userId ?? null,
|
|
1262
|
+
title: opts?.title ?? null,
|
|
1263
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1264
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async function getConversation(id) {
|
|
1268
|
+
const db2 = await getDb();
|
|
1269
|
+
const row = await db2.select().from(conversations).where(eq3(conversations.id, id)).get();
|
|
1270
|
+
return row ?? null;
|
|
1271
|
+
}
|
|
1272
|
+
async function getParticipants(conversationId) {
|
|
1273
|
+
const db2 = await getDb();
|
|
1274
|
+
const rows = await db2.select({
|
|
1275
|
+
userId: conversationParticipants.user_id,
|
|
1276
|
+
username: users.username,
|
|
1277
|
+
userType: users.user_type,
|
|
1278
|
+
role: conversationParticipants.role
|
|
1279
|
+
}).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
|
|
1280
|
+
return rows;
|
|
1281
|
+
}
|
|
1282
|
+
async function isParticipant(conversationId, userId) {
|
|
1283
|
+
const db2 = await getDb();
|
|
1284
|
+
const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
1285
|
+
and2(
|
|
1286
|
+
eq3(conversationParticipants.conversation_id, conversationId),
|
|
1287
|
+
eq3(conversationParticipants.user_id, userId)
|
|
1288
|
+
)
|
|
1289
|
+
).get();
|
|
1290
|
+
return row != null;
|
|
1291
|
+
}
|
|
1292
|
+
async function listConversationsForUser(userId) {
|
|
1293
|
+
const db2 = await getDb();
|
|
1294
|
+
const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
|
|
1295
|
+
if (participantRows.length === 0) return [];
|
|
1296
|
+
const convIds = participantRows.map((r) => r.conversation_id);
|
|
1297
|
+
return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
1298
|
+
}
|
|
1299
|
+
async function isParticipantOrOwner(conversationId, userId) {
|
|
1300
|
+
if (await isParticipant(conversationId, userId)) return true;
|
|
1301
|
+
const db2 = await getDb();
|
|
1302
|
+
const row = await db2.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
|
|
1303
|
+
return row != null;
|
|
1304
|
+
}
|
|
1305
|
+
async function deleteConversationForUser(id, userId) {
|
|
1306
|
+
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
1307
|
+
await deleteConversation(id);
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
async function addMessage(conversationId, role, senderName, content) {
|
|
1311
|
+
const db2 = await getDb();
|
|
1312
|
+
const serialized = JSON.stringify(content);
|
|
1313
|
+
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 });
|
|
1314
|
+
await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq3(conversations.id, conversationId));
|
|
1315
|
+
if (role === "user") {
|
|
1316
|
+
const firstText = content.find((b) => b.type === "text");
|
|
1317
|
+
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
1318
|
+
if (title) {
|
|
1319
|
+
await db2.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
const msg = {
|
|
1323
|
+
id: result.id,
|
|
1324
|
+
conversation_id: conversationId,
|
|
1325
|
+
role,
|
|
1326
|
+
sender_name: senderName,
|
|
1327
|
+
content,
|
|
1328
|
+
created_at: result.created_at
|
|
1329
|
+
};
|
|
1330
|
+
publish(conversationId, {
|
|
1331
|
+
type: "message",
|
|
1332
|
+
id: msg.id,
|
|
1333
|
+
role: msg.role,
|
|
1334
|
+
senderName: msg.sender_name,
|
|
1335
|
+
content: msg.content,
|
|
1336
|
+
createdAt: msg.created_at
|
|
1337
|
+
});
|
|
1338
|
+
return msg;
|
|
1339
|
+
}
|
|
1340
|
+
async function getMessages(conversationId) {
|
|
1341
|
+
const db2 = await getDb();
|
|
1342
|
+
const rows = await db2.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1343
|
+
return rows.map((row) => {
|
|
1344
|
+
let content;
|
|
1345
|
+
try {
|
|
1346
|
+
const parsed = JSON.parse(row.content);
|
|
1347
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
1348
|
+
} catch {
|
|
1349
|
+
content = [{ type: "text", text: row.content }];
|
|
1350
|
+
}
|
|
1351
|
+
return { ...row, content };
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
async function listConversationsWithParticipants(userId) {
|
|
1355
|
+
const convs = await listConversationsForUser(userId);
|
|
1356
|
+
if (convs.length === 0) return [];
|
|
1357
|
+
const db2 = await getDb();
|
|
1358
|
+
const convIds = convs.map((c) => c.id);
|
|
1359
|
+
const rows = await db2.select({
|
|
1360
|
+
conversationId: conversationParticipants.conversation_id,
|
|
1361
|
+
userId: users.id,
|
|
1362
|
+
username: users.username,
|
|
1363
|
+
userType: users.user_type,
|
|
1364
|
+
role: conversationParticipants.role
|
|
1365
|
+
}).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
1366
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
1367
|
+
for (const r of rows) {
|
|
1368
|
+
let arr = byConv.get(r.conversationId);
|
|
1369
|
+
if (!arr) {
|
|
1370
|
+
arr = [];
|
|
1371
|
+
byConv.set(r.conversationId, arr);
|
|
1372
|
+
}
|
|
1373
|
+
arr.push({
|
|
1374
|
+
userId: r.userId,
|
|
1375
|
+
username: r.username,
|
|
1376
|
+
userType: r.userType,
|
|
1377
|
+
role: r.role
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
|
|
1381
|
+
}
|
|
1382
|
+
async function findDMConversation(agentName, participantIds) {
|
|
1383
|
+
const db2 = await getDb();
|
|
1384
|
+
const agentConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq3(conversations.agent_name, agentName)).all();
|
|
1385
|
+
for (const conv of agentConvs) {
|
|
1386
|
+
const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
|
|
1387
|
+
if (rows.length !== 2) continue;
|
|
1388
|
+
const ids = new Set(rows.map((r) => r.user_id));
|
|
1389
|
+
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
1390
|
+
return conv.id;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
async function deleteConversation(id) {
|
|
1396
|
+
const db2 = await getDb();
|
|
1397
|
+
await db2.delete(conversations).where(eq3(conversations.id, id));
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/lib/convert-session.ts
|
|
1401
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1193
1402
|
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1194
1403
|
import { homedir } from "os";
|
|
1195
1404
|
import { resolve as resolve7 } from "path";
|
|
1196
1405
|
function convertSession(opts) {
|
|
1197
1406
|
const lines = readFileSync4(opts.sessionPath, "utf-8").trim().split("\n");
|
|
1198
|
-
const sessionId =
|
|
1407
|
+
const sessionId = randomUUID2();
|
|
1199
1408
|
const idMap = /* @__PURE__ */ new Map();
|
|
1200
1409
|
const messages2 = [];
|
|
1201
1410
|
for (const line of lines) {
|
|
@@ -1210,7 +1419,7 @@ function convertSession(opts) {
|
|
|
1210
1419
|
const event = messages2[i];
|
|
1211
1420
|
const msg = event.message;
|
|
1212
1421
|
if (msg.role === "user") {
|
|
1213
|
-
const uuid =
|
|
1422
|
+
const uuid = randomUUID2();
|
|
1214
1423
|
idMap.set(event.id, uuid);
|
|
1215
1424
|
const parentUuid = event.parentId ? idMap.get(event.parentId) ?? null : null;
|
|
1216
1425
|
const sdkEvent = {
|
|
@@ -1234,7 +1443,7 @@ function convertSession(opts) {
|
|
|
1234
1443
|
} else if (msg.role === "assistant") {
|
|
1235
1444
|
const content = convertAssistantContent(msg.content);
|
|
1236
1445
|
if (content.length === 0) continue;
|
|
1237
|
-
const uuid =
|
|
1446
|
+
const uuid = randomUUID2();
|
|
1238
1447
|
idMap.set(event.id, uuid);
|
|
1239
1448
|
const parentUuid = event.parentId ? idMap.get(event.parentId) ?? null : null;
|
|
1240
1449
|
const stopReason = mapStopReason(msg.stopReason);
|
|
@@ -1249,12 +1458,12 @@ function convertSession(opts) {
|
|
|
1249
1458
|
isSidechain: false,
|
|
1250
1459
|
userType: "external",
|
|
1251
1460
|
type: "assistant",
|
|
1252
|
-
requestId: `req_imported_${
|
|
1461
|
+
requestId: `req_imported_${randomUUID2()}`,
|
|
1253
1462
|
message: {
|
|
1254
1463
|
role: "assistant",
|
|
1255
1464
|
content,
|
|
1256
1465
|
type: "message",
|
|
1257
|
-
id: `msg_imported_${
|
|
1466
|
+
id: `msg_imported_${randomUUID2()}`,
|
|
1258
1467
|
model: mapModel(msg.model),
|
|
1259
1468
|
stop_reason: stopReason,
|
|
1260
1469
|
stop_sequence: null,
|
|
@@ -1282,7 +1491,7 @@ function convertSession(opts) {
|
|
|
1282
1491
|
j++;
|
|
1283
1492
|
}
|
|
1284
1493
|
i = j - 1;
|
|
1285
|
-
const uuid =
|
|
1494
|
+
const uuid = randomUUID2();
|
|
1286
1495
|
idMap.set(lastToolResultId, uuid);
|
|
1287
1496
|
const parentUuid = event.parentId ? idMap.get(event.parentId) ?? null : lastSdkUuid;
|
|
1288
1497
|
const sdkEvent = {
|
|
@@ -1362,118 +1571,6 @@ function convertAssistantContent(content) {
|
|
|
1362
1571
|
return result;
|
|
1363
1572
|
}
|
|
1364
1573
|
|
|
1365
|
-
// src/lib/template.ts
|
|
1366
|
-
import {
|
|
1367
|
-
cpSync,
|
|
1368
|
-
existsSync as existsSync5,
|
|
1369
|
-
mkdirSync as mkdirSync4,
|
|
1370
|
-
readdirSync as readdirSync3,
|
|
1371
|
-
readFileSync as readFileSync5,
|
|
1372
|
-
renameSync,
|
|
1373
|
-
rmSync,
|
|
1374
|
-
statSync,
|
|
1375
|
-
writeFileSync as writeFileSync4
|
|
1376
|
-
} from "fs";
|
|
1377
|
-
import { tmpdir } from "os";
|
|
1378
|
-
import { dirname as dirname3, join, relative, resolve as resolve8 } from "path";
|
|
1379
|
-
function findTemplatesRoot() {
|
|
1380
|
-
let dir = dirname3(new URL(import.meta.url).pathname);
|
|
1381
|
-
for (let i = 0; i < 5; i++) {
|
|
1382
|
-
const candidate = resolve8(dir, "templates");
|
|
1383
|
-
if (existsSync5(resolve8(candidate, "_base"))) return candidate;
|
|
1384
|
-
dir = dirname3(dir);
|
|
1385
|
-
}
|
|
1386
|
-
console.error(
|
|
1387
|
-
"Templates directory not found. Searched up from:",
|
|
1388
|
-
dirname3(new URL(import.meta.url).pathname)
|
|
1389
|
-
);
|
|
1390
|
-
process.exit(1);
|
|
1391
|
-
}
|
|
1392
|
-
function composeTemplate(templatesRoot, templateName) {
|
|
1393
|
-
const baseDir = resolve8(templatesRoot, "_base");
|
|
1394
|
-
const templateDir = resolve8(templatesRoot, templateName);
|
|
1395
|
-
if (!existsSync5(baseDir)) {
|
|
1396
|
-
console.error("Base template not found:", baseDir);
|
|
1397
|
-
process.exit(1);
|
|
1398
|
-
}
|
|
1399
|
-
if (!existsSync5(templateDir)) {
|
|
1400
|
-
console.error(`Template not found: ${templateName}`);
|
|
1401
|
-
process.exit(1);
|
|
1402
|
-
}
|
|
1403
|
-
const composedDir = resolve8(tmpdir(), `volute-template-${Date.now()}`);
|
|
1404
|
-
mkdirSync4(composedDir, { recursive: true });
|
|
1405
|
-
cpSync(baseDir, composedDir, { recursive: true });
|
|
1406
|
-
for (const file of listFiles(templateDir)) {
|
|
1407
|
-
const src = resolve8(templateDir, file);
|
|
1408
|
-
const dest = resolve8(composedDir, file);
|
|
1409
|
-
mkdirSync4(dirname3(dest), { recursive: true });
|
|
1410
|
-
cpSync(src, dest);
|
|
1411
|
-
}
|
|
1412
|
-
const manifestPath = resolve8(composedDir, "volute-template.json");
|
|
1413
|
-
if (!existsSync5(manifestPath)) {
|
|
1414
|
-
rmSync(composedDir, { recursive: true, force: true });
|
|
1415
|
-
console.error(`Template manifest not found: ${templateName}/volute-template.json`);
|
|
1416
|
-
process.exit(1);
|
|
1417
|
-
}
|
|
1418
|
-
const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
1419
|
-
const skillsSrc = resolve8(composedDir, "_skills");
|
|
1420
|
-
if (existsSync5(skillsSrc)) {
|
|
1421
|
-
const skillsDest = resolve8(composedDir, manifest.skillsDir);
|
|
1422
|
-
mkdirSync4(skillsDest, { recursive: true });
|
|
1423
|
-
cpSync(skillsSrc, skillsDest, { recursive: true });
|
|
1424
|
-
rmSync(skillsSrc, { recursive: true, force: true });
|
|
1425
|
-
}
|
|
1426
|
-
rmSync(manifestPath);
|
|
1427
|
-
return { composedDir, manifest };
|
|
1428
|
-
}
|
|
1429
|
-
function copyTemplateToDir(composedDir, destDir, agentName, manifest) {
|
|
1430
|
-
cpSync(composedDir, destDir, { recursive: true });
|
|
1431
|
-
for (const [from, to] of Object.entries(manifest.rename)) {
|
|
1432
|
-
const fromPath = resolve8(destDir, from);
|
|
1433
|
-
if (existsSync5(fromPath)) {
|
|
1434
|
-
renameSync(fromPath, resolve8(destDir, to));
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
for (const file of manifest.substitute) {
|
|
1438
|
-
const path = resolve8(destDir, file);
|
|
1439
|
-
if (existsSync5(path)) {
|
|
1440
|
-
const content = readFileSync5(path, "utf-8");
|
|
1441
|
-
writeFileSync4(path, content.replaceAll("{{name}}", agentName));
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
function applyInitFiles(destDir) {
|
|
1446
|
-
const initDir = resolve8(destDir, ".init");
|
|
1447
|
-
if (!existsSync5(initDir)) return;
|
|
1448
|
-
const homeDir = resolve8(destDir, "home");
|
|
1449
|
-
for (const file of listFiles(initDir)) {
|
|
1450
|
-
const src = resolve8(initDir, file);
|
|
1451
|
-
const dest = resolve8(homeDir, file);
|
|
1452
|
-
const parent = dirname3(dest);
|
|
1453
|
-
if (!existsSync5(parent)) {
|
|
1454
|
-
mkdirSync4(parent, { recursive: true });
|
|
1455
|
-
}
|
|
1456
|
-
cpSync(src, dest);
|
|
1457
|
-
}
|
|
1458
|
-
rmSync(initDir, { recursive: true, force: true });
|
|
1459
|
-
}
|
|
1460
|
-
function listFiles(dir) {
|
|
1461
|
-
const results = [];
|
|
1462
|
-
function walk(current) {
|
|
1463
|
-
for (const entry of readdirSync3(current)) {
|
|
1464
|
-
const full = join(current, entry);
|
|
1465
|
-
if (statSync(full).isDirectory()) {
|
|
1466
|
-
if (entry === ".git") continue;
|
|
1467
|
-
walk(full);
|
|
1468
|
-
} else {
|
|
1469
|
-
results.push(relative(dir, full));
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
walk(dir);
|
|
1474
|
-
return results;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
1574
|
// src/lib/typing.ts
|
|
1478
1575
|
var DEFAULT_TTL_MS = 1e4;
|
|
1479
1576
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -1545,6 +1642,7 @@ function getTypingMap() {
|
|
|
1545
1642
|
async function startAgentFull(name, baseName, variantName) {
|
|
1546
1643
|
await getAgentManager().startAgent(name);
|
|
1547
1644
|
if (variantName) return;
|
|
1645
|
+
if (findAgent(baseName)?.stage === "seed") return;
|
|
1548
1646
|
const dir = agentDir(baseName);
|
|
1549
1647
|
const entry = findAgent(baseName);
|
|
1550
1648
|
await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
|
|
@@ -1567,7 +1665,7 @@ function extractTextContent(content) {
|
|
|
1567
1665
|
}
|
|
1568
1666
|
function getDaemonPort() {
|
|
1569
1667
|
try {
|
|
1570
|
-
const data = JSON.parse(
|
|
1668
|
+
const data = JSON.parse(readFileSync5(resolve8(voluteHome(), "daemon.json"), "utf-8"));
|
|
1571
1669
|
return data.port;
|
|
1572
1670
|
} catch (err) {
|
|
1573
1671
|
if (err?.code !== "ENOENT") {
|
|
@@ -1607,9 +1705,9 @@ async function getAgentStatus(name, port) {
|
|
|
1607
1705
|
return { status, channels };
|
|
1608
1706
|
}
|
|
1609
1707
|
var TEMPLATE_BRANCH = "volute/template";
|
|
1610
|
-
async function initTemplateBranch(projectRoot, composedDir, manifest,
|
|
1708
|
+
async function initTemplateBranch(projectRoot, composedDir, manifest, agentName, env) {
|
|
1611
1709
|
const templateFiles = listFiles(composedDir).filter((f) => !f.startsWith(".init/") && !f.startsWith(".init\\")).map((f) => manifest.rename[f] ?? f);
|
|
1612
|
-
const opts = { cwd: projectRoot,
|
|
1710
|
+
const opts = { cwd: projectRoot, agentName, env };
|
|
1613
1711
|
await gitExec(["checkout", "--orphan", TEMPLATE_BRANCH], opts);
|
|
1614
1712
|
await gitExec(["add", "--", ...templateFiles], opts);
|
|
1615
1713
|
await gitExec(["commit", "-m", "template update"], opts);
|
|
@@ -1618,7 +1716,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, ids, env)
|
|
|
1618
1716
|
await gitExec(["commit", "-m", "initial commit"], opts);
|
|
1619
1717
|
}
|
|
1620
1718
|
async function updateTemplateBranch(projectRoot, template, agentName) {
|
|
1621
|
-
const tempWorktree =
|
|
1719
|
+
const tempWorktree = resolve8(projectRoot, ".variants", "_template_update");
|
|
1622
1720
|
let branchExists = false;
|
|
1623
1721
|
try {
|
|
1624
1722
|
await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
|
|
@@ -1629,8 +1727,8 @@ async function updateTemplateBranch(projectRoot, template, agentName) {
|
|
|
1629
1727
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
1630
1728
|
} catch {
|
|
1631
1729
|
}
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
1730
|
+
if (existsSync5(tempWorktree)) {
|
|
1731
|
+
rmSync(tempWorktree, { recursive: true, force: true });
|
|
1634
1732
|
}
|
|
1635
1733
|
const templatesRoot = findTemplatesRoot();
|
|
1636
1734
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
@@ -1650,9 +1748,9 @@ async function updateTemplateBranch(projectRoot, template, agentName) {
|
|
|
1650
1748
|
});
|
|
1651
1749
|
}
|
|
1652
1750
|
copyTemplateToDir(composedDir, tempWorktree, agentName, manifest);
|
|
1653
|
-
const initDir =
|
|
1654
|
-
if (
|
|
1655
|
-
|
|
1751
|
+
const initDir = resolve8(tempWorktree, ".init");
|
|
1752
|
+
if (existsSync5(initDir)) {
|
|
1753
|
+
rmSync(initDir, { recursive: true, force: true });
|
|
1656
1754
|
}
|
|
1657
1755
|
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
1658
1756
|
try {
|
|
@@ -1665,10 +1763,10 @@ async function updateTemplateBranch(projectRoot, template, agentName) {
|
|
|
1665
1763
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
1666
1764
|
} catch {
|
|
1667
1765
|
}
|
|
1668
|
-
if (
|
|
1669
|
-
|
|
1766
|
+
if (existsSync5(tempWorktree)) {
|
|
1767
|
+
rmSync(tempWorktree, { recursive: true, force: true });
|
|
1670
1768
|
}
|
|
1671
|
-
|
|
1769
|
+
rmSync(composedDir, { recursive: true, force: true });
|
|
1672
1770
|
}
|
|
1673
1771
|
}
|
|
1674
1772
|
async function mergeTemplateBranch(worktreeDir) {
|
|
@@ -1688,57 +1786,87 @@ async function mergeTemplateBranch(worktreeDir) {
|
|
|
1688
1786
|
throw e;
|
|
1689
1787
|
}
|
|
1690
1788
|
}
|
|
1691
|
-
var
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1789
|
+
var createAgentSchema = z.object({
|
|
1790
|
+
name: z.string(),
|
|
1791
|
+
template: z.string().optional(),
|
|
1792
|
+
stage: z.enum(["seed", "mind"]).optional(),
|
|
1793
|
+
description: z.string().optional(),
|
|
1794
|
+
model: z.string().optional()
|
|
1795
|
+
});
|
|
1796
|
+
var app = new Hono().post("/", requireAdmin, zValidator("json", createAgentSchema), async (c) => {
|
|
1797
|
+
const body = c.req.valid("json");
|
|
1698
1798
|
const { name, template = "agent-sdk" } = body;
|
|
1699
1799
|
const nameErr = validateAgentName(name);
|
|
1700
1800
|
if (nameErr) return c.json({ error: nameErr }, 400);
|
|
1701
1801
|
if (findAgent(name)) return c.json({ error: `Agent already exists: ${name}` }, 409);
|
|
1702
1802
|
ensureVoluteHome();
|
|
1703
1803
|
const dest = agentDir(name);
|
|
1704
|
-
if (
|
|
1804
|
+
if (existsSync5(dest)) return c.json({ error: "Agent directory already exists" }, 409);
|
|
1705
1805
|
const templatesRoot = findTemplatesRoot();
|
|
1706
1806
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
1707
1807
|
try {
|
|
1708
1808
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
1709
1809
|
applyInitFiles(dest);
|
|
1810
|
+
if (body.model) {
|
|
1811
|
+
const configPath = resolve8(dest, "home/.config/config.json");
|
|
1812
|
+
const existing = existsSync5(configPath) ? JSON.parse(readFileSync5(configPath, "utf-8")) : {};
|
|
1813
|
+
existing.model = body.model;
|
|
1814
|
+
writeFileSync4(configPath, `${JSON.stringify(existing, null, 2)}
|
|
1815
|
+
`);
|
|
1816
|
+
}
|
|
1817
|
+
if (body.stage === "seed") {
|
|
1818
|
+
const descLine = body.description ? `
|
|
1819
|
+
The human who planted you described you as: "${body.description}"
|
|
1820
|
+
` : "";
|
|
1821
|
+
writeFileSync4(
|
|
1822
|
+
resolve8(dest, "home/SOUL.md"),
|
|
1823
|
+
`You are ${name}.
|
|
1824
|
+
|
|
1825
|
+
You don't have a soul yet. You're a seed \u2014 a new agent in the process of discovering who you are.
|
|
1826
|
+
${descLine}
|
|
1827
|
+
Have a conversation with the human. Explore what kind of agent you want to be. When you're ready, write your SOUL.md and MEMORY.md, then run \`volute sprout\` to complete the transformation.
|
|
1828
|
+
`
|
|
1829
|
+
);
|
|
1830
|
+
const skillsDir = resolve8(dest, manifest.skillsDir);
|
|
1831
|
+
for (const skill of ["volute-agent", "memory", "sessions"]) {
|
|
1832
|
+
const skillPath = resolve8(skillsDir, skill);
|
|
1833
|
+
if (existsSync5(skillPath)) rmSync(skillPath, { recursive: true, force: true });
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1710
1836
|
const port = nextPort();
|
|
1711
|
-
addAgent(name, port);
|
|
1837
|
+
addAgent(name, port, body.stage);
|
|
1838
|
+
const homeDir = resolve8(dest, "home");
|
|
1712
1839
|
ensureVoluteGroup();
|
|
1713
|
-
createAgentUser(name);
|
|
1840
|
+
createAgentUser(name, homeDir);
|
|
1714
1841
|
chownAgentDir(dest, name);
|
|
1715
|
-
const
|
|
1716
|
-
const env =
|
|
1717
|
-
await exec("npm", ["install"], { cwd: dest,
|
|
1842
|
+
const agentName = isIsolationEnabled() ? name : void 0;
|
|
1843
|
+
const env = agentName ? { ...process.env, HOME: homeDir } : void 0;
|
|
1844
|
+
await exec("npm", ["install"], { cwd: dest, agentName, env });
|
|
1718
1845
|
let gitWarning;
|
|
1719
1846
|
try {
|
|
1720
|
-
await gitExec(["init"], { cwd: dest,
|
|
1721
|
-
await initTemplateBranch(dest, composedDir, manifest,
|
|
1847
|
+
await gitExec(["init"], { cwd: dest, agentName, env });
|
|
1848
|
+
await initTemplateBranch(dest, composedDir, manifest, agentName, env);
|
|
1722
1849
|
} catch {
|
|
1723
|
-
|
|
1850
|
+
rmSync(resolve8(dest, ".git"), { recursive: true, force: true });
|
|
1724
1851
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
1725
1852
|
}
|
|
1726
1853
|
return c.json({
|
|
1727
1854
|
ok: true,
|
|
1728
1855
|
name,
|
|
1729
1856
|
port,
|
|
1857
|
+
stage: body.stage ?? "mind",
|
|
1730
1858
|
message: `Created agent: ${name} (port ${port})`,
|
|
1731
1859
|
...gitWarning && { warning: gitWarning }
|
|
1732
1860
|
});
|
|
1733
1861
|
} catch (err) {
|
|
1734
|
-
if (
|
|
1862
|
+
if (existsSync5(dest)) rmSync(dest, { recursive: true, force: true });
|
|
1735
1863
|
try {
|
|
1736
1864
|
removeAgent(name);
|
|
1737
1865
|
} catch {
|
|
1738
1866
|
}
|
|
1739
1867
|
return c.json({ error: err instanceof Error ? err.message : "Failed to create agent" }, 500);
|
|
1740
1868
|
} finally {
|
|
1741
|
-
|
|
1869
|
+
rmSync(composedDir, { recursive: true, force: true });
|
|
1742
1870
|
}
|
|
1743
1871
|
}).post("/import", requireAdmin, async (c) => {
|
|
1744
1872
|
let body;
|
|
@@ -1748,13 +1876,13 @@ var app = new Hono().post("/", requireAdmin, async (c) => {
|
|
|
1748
1876
|
return c.json({ error: "Invalid JSON" }, 400);
|
|
1749
1877
|
}
|
|
1750
1878
|
const wsDir = body.workspacePath;
|
|
1751
|
-
if (!wsDir || !
|
|
1879
|
+
if (!wsDir || !existsSync5(resolve8(wsDir, "SOUL.md")) || !existsSync5(resolve8(wsDir, "IDENTITY.md"))) {
|
|
1752
1880
|
return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
|
|
1753
1881
|
}
|
|
1754
|
-
const soul =
|
|
1755
|
-
const identity =
|
|
1756
|
-
const userPath =
|
|
1757
|
-
const user =
|
|
1882
|
+
const soul = readFileSync5(resolve8(wsDir, "SOUL.md"), "utf-8");
|
|
1883
|
+
const identity = readFileSync5(resolve8(wsDir, "IDENTITY.md"), "utf-8");
|
|
1884
|
+
const userPath = resolve8(wsDir, "USER.md");
|
|
1885
|
+
const user = existsSync5(userPath) ? readFileSync5(userPath, "utf-8") : "";
|
|
1758
1886
|
const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-agent";
|
|
1759
1887
|
const template = body.template ?? "agent-sdk";
|
|
1760
1888
|
const nameErr = validateAgentName(name);
|
|
@@ -1774,76 +1902,72 @@ ${user.trimEnd()}
|
|
|
1774
1902
|
` : "";
|
|
1775
1903
|
ensureVoluteHome();
|
|
1776
1904
|
const dest = agentDir(name);
|
|
1777
|
-
if (
|
|
1905
|
+
if (existsSync5(dest)) return c.json({ error: "Agent directory already exists" }, 409);
|
|
1778
1906
|
const templatesRoot = findTemplatesRoot();
|
|
1779
1907
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
1780
1908
|
try {
|
|
1781
1909
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
1782
1910
|
applyInitFiles(dest);
|
|
1783
|
-
|
|
1784
|
-
const wsMemoryPath =
|
|
1785
|
-
const hasMemory =
|
|
1911
|
+
writeFileSync4(resolve8(dest, "home/SOUL.md"), mergedSoul);
|
|
1912
|
+
const wsMemoryPath = resolve8(wsDir, "MEMORY.md");
|
|
1913
|
+
const hasMemory = existsSync5(wsMemoryPath);
|
|
1786
1914
|
if (hasMemory) {
|
|
1787
|
-
const existingMemory =
|
|
1788
|
-
|
|
1789
|
-
|
|
1915
|
+
const existingMemory = readFileSync5(wsMemoryPath, "utf-8");
|
|
1916
|
+
writeFileSync4(
|
|
1917
|
+
resolve8(dest, "home/MEMORY.md"),
|
|
1790
1918
|
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
1791
1919
|
);
|
|
1792
1920
|
} else if (user) {
|
|
1793
|
-
|
|
1921
|
+
writeFileSync4(resolve8(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
1794
1922
|
`);
|
|
1795
1923
|
}
|
|
1796
|
-
const wsMemoryDir =
|
|
1924
|
+
const wsMemoryDir = resolve8(wsDir, "memory");
|
|
1797
1925
|
let dailyLogCount = 0;
|
|
1798
|
-
if (
|
|
1799
|
-
const destMemoryDir =
|
|
1800
|
-
const files =
|
|
1926
|
+
if (existsSync5(wsMemoryDir)) {
|
|
1927
|
+
const destMemoryDir = resolve8(dest, "home/memory");
|
|
1928
|
+
const files = readdirSync3(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
1801
1929
|
for (const file of files) {
|
|
1802
|
-
|
|
1930
|
+
cpSync(resolve8(wsMemoryDir, file), resolve8(destMemoryDir, file));
|
|
1803
1931
|
}
|
|
1804
1932
|
dailyLogCount = files.length;
|
|
1805
1933
|
}
|
|
1806
1934
|
const port = nextPort();
|
|
1807
1935
|
addAgent(name, port);
|
|
1936
|
+
const homeDir = resolve8(dest, "home");
|
|
1808
1937
|
ensureVoluteGroup();
|
|
1809
|
-
createAgentUser(name);
|
|
1938
|
+
createAgentUser(name, homeDir);
|
|
1810
1939
|
chownAgentDir(dest, name);
|
|
1811
|
-
const
|
|
1812
|
-
const env =
|
|
1813
|
-
await exec("npm", ["install"], { cwd: dest,
|
|
1940
|
+
const agentName = isIsolationEnabled() ? name : void 0;
|
|
1941
|
+
const env = agentName ? { ...process.env, HOME: homeDir } : void 0;
|
|
1942
|
+
await exec("npm", ["install"], { cwd: dest, agentName, env });
|
|
1814
1943
|
if (!hasMemory && dailyLogCount > 0) {
|
|
1815
1944
|
await consolidateMemory(dest);
|
|
1816
1945
|
}
|
|
1817
|
-
await gitExec(["init"], { cwd: dest,
|
|
1818
|
-
await gitExec(["add", "-A"], { cwd: dest,
|
|
1819
|
-
await gitExec(["commit", "-m", "import from OpenClaw"], {
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
gid: ids?.gid,
|
|
1823
|
-
env
|
|
1824
|
-
});
|
|
1825
|
-
const sessionFile = body.sessionPath ? resolve9(body.sessionPath) : findOpenClawSession(wsDir);
|
|
1826
|
-
if (sessionFile && existsSync6(sessionFile)) {
|
|
1946
|
+
await gitExec(["init"], { cwd: dest, agentName, env });
|
|
1947
|
+
await gitExec(["add", "-A"], { cwd: dest, agentName, env });
|
|
1948
|
+
await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, agentName, env });
|
|
1949
|
+
const sessionFile = body.sessionPath ? resolve8(body.sessionPath) : findOpenClawSession(wsDir);
|
|
1950
|
+
if (sessionFile && existsSync5(sessionFile)) {
|
|
1827
1951
|
if (template === "pi") {
|
|
1828
1952
|
importPiSession(sessionFile, dest);
|
|
1829
1953
|
} else if (template === "agent-sdk") {
|
|
1830
1954
|
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
1831
|
-
const voluteDir =
|
|
1832
|
-
|
|
1833
|
-
|
|
1955
|
+
const voluteDir = resolve8(dest, ".volute");
|
|
1956
|
+
mkdirSync4(voluteDir, { recursive: true });
|
|
1957
|
+
writeFileSync4(resolve8(voluteDir, "session.json"), JSON.stringify({ sessionId }));
|
|
1834
1958
|
}
|
|
1835
1959
|
}
|
|
1836
1960
|
importOpenClawConnectors(name, dest);
|
|
1837
1961
|
return c.json({ ok: true, name, port, message: `Imported agent: ${name} (port ${port})` });
|
|
1838
1962
|
} catch (err) {
|
|
1839
|
-
if (
|
|
1963
|
+
if (existsSync5(dest)) rmSync(dest, { recursive: true, force: true });
|
|
1840
1964
|
try {
|
|
1841
1965
|
removeAgent(name);
|
|
1842
1966
|
} catch {
|
|
1843
1967
|
}
|
|
1844
1968
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import agent" }, 500);
|
|
1845
1969
|
} finally {
|
|
1846
|
-
|
|
1970
|
+
rmSync(composedDir, { recursive: true, force: true });
|
|
1847
1971
|
}
|
|
1848
1972
|
}).get("/", async (c) => {
|
|
1849
1973
|
const entries = readRegistry();
|
|
@@ -1858,7 +1982,7 @@ ${user.trimEnd()}
|
|
|
1858
1982
|
const name = c.req.param("name");
|
|
1859
1983
|
const entry = findAgent(name);
|
|
1860
1984
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1861
|
-
if (!
|
|
1985
|
+
if (!existsSync5(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
|
|
1862
1986
|
const { status, channels } = await getAgentStatus(name, entry.port);
|
|
1863
1987
|
const variants = readVariants(name);
|
|
1864
1988
|
const manager = getAgentManager();
|
|
@@ -1884,7 +2008,7 @@ ${user.trimEnd()}
|
|
|
1884
2008
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1885
2009
|
} else {
|
|
1886
2010
|
const dir = agentDir(baseName);
|
|
1887
|
-
if (!
|
|
2011
|
+
if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
1888
2012
|
}
|
|
1889
2013
|
if (getAgentManager().isRunning(name)) {
|
|
1890
2014
|
return c.json({ error: "Agent already running" }, 409);
|
|
@@ -1905,7 +2029,7 @@ ${user.trimEnd()}
|
|
|
1905
2029
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1906
2030
|
} else {
|
|
1907
2031
|
const dir = agentDir(baseName);
|
|
1908
|
-
if (!
|
|
2032
|
+
if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
1909
2033
|
}
|
|
1910
2034
|
let context;
|
|
1911
2035
|
const contentType = c.req.header("content-type");
|
|
@@ -1936,7 +2060,7 @@ ${user.trimEnd()}
|
|
|
1936
2060
|
const variant = findVariant(baseName, mergeVariantName);
|
|
1937
2061
|
if (variant) {
|
|
1938
2062
|
const projectRoot = agentDir(baseName);
|
|
1939
|
-
if (
|
|
2063
|
+
if (existsSync5(variant.path)) {
|
|
1940
2064
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
1941
2065
|
if (status) {
|
|
1942
2066
|
try {
|
|
@@ -1964,7 +2088,7 @@ ${user.trimEnd()}
|
|
|
1964
2088
|
}
|
|
1965
2089
|
}
|
|
1966
2090
|
await gitExec(["merge", variant.branch], { cwd: projectRoot });
|
|
1967
|
-
if (
|
|
2091
|
+
if (existsSync5(variant.path)) {
|
|
1968
2092
|
try {
|
|
1969
2093
|
await gitExec(["worktree", "remove", "--force", variant.path], {
|
|
1970
2094
|
cwd: projectRoot
|
|
@@ -1988,6 +2112,19 @@ ${user.trimEnd()}
|
|
|
1988
2112
|
if (context) {
|
|
1989
2113
|
manager.setPendingContext(name, context);
|
|
1990
2114
|
}
|
|
2115
|
+
if (context?.type === "sprouted" && !variantName) {
|
|
2116
|
+
try {
|
|
2117
|
+
const db2 = await getDb();
|
|
2118
|
+
const activeConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.agent_name, baseName)).all();
|
|
2119
|
+
for (const conv of activeConvs) {
|
|
2120
|
+
await addMessage(conv.id, "assistant", "system", [
|
|
2121
|
+
{ type: "text", text: "[seed has sprouted]" }
|
|
2122
|
+
]);
|
|
2123
|
+
}
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
console.error(`[daemon] failed to inject sprouted message for ${baseName}:`, err);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
1991
2128
|
await startAgentFull(name, baseName, variantName);
|
|
1992
2129
|
return c.json({ ok: true });
|
|
1993
2130
|
} catch (err) {
|
|
@@ -2033,11 +2170,11 @@ ${user.trimEnd()}
|
|
|
2033
2170
|
removeAgent(name);
|
|
2034
2171
|
await deleteAgentUser2(name);
|
|
2035
2172
|
const state = stateDir(name);
|
|
2036
|
-
if (
|
|
2037
|
-
|
|
2173
|
+
if (existsSync5(state)) {
|
|
2174
|
+
rmSync(state, { recursive: true, force: true });
|
|
2038
2175
|
}
|
|
2039
|
-
if (force &&
|
|
2040
|
-
|
|
2176
|
+
if (force && existsSync5(dir)) {
|
|
2177
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2041
2178
|
deleteAgentUser(name);
|
|
2042
2179
|
}
|
|
2043
2180
|
return c.json({ ok: true });
|
|
@@ -2046,7 +2183,7 @@ ${user.trimEnd()}
|
|
|
2046
2183
|
const entry = findAgent(agentName);
|
|
2047
2184
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2048
2185
|
const dir = agentDir(agentName);
|
|
2049
|
-
if (!
|
|
2186
|
+
if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
2050
2187
|
let body = {};
|
|
2051
2188
|
try {
|
|
2052
2189
|
body = await c.req.json();
|
|
@@ -2055,8 +2192,8 @@ ${user.trimEnd()}
|
|
|
2055
2192
|
const template = body.template ?? "agent-sdk";
|
|
2056
2193
|
const UPGRADE_VARIANT = "upgrade";
|
|
2057
2194
|
if (body.continue) {
|
|
2058
|
-
const worktreeDir2 =
|
|
2059
|
-
if (!
|
|
2195
|
+
const worktreeDir2 = resolve8(dir, ".variants", UPGRADE_VARIANT);
|
|
2196
|
+
if (!existsSync5(worktreeDir2)) {
|
|
2060
2197
|
return c.json({ error: "No upgrade in progress" }, 400);
|
|
2061
2198
|
}
|
|
2062
2199
|
const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
|
|
@@ -2116,8 +2253,8 @@ ${user.trimEnd()}
|
|
|
2116
2253
|
);
|
|
2117
2254
|
}
|
|
2118
2255
|
}
|
|
2119
|
-
const worktreeDir =
|
|
2120
|
-
if (
|
|
2256
|
+
const worktreeDir = resolve8(dir, ".variants", UPGRADE_VARIANT);
|
|
2257
|
+
if (existsSync5(worktreeDir)) {
|
|
2121
2258
|
return c.json(
|
|
2122
2259
|
{ error: "Upgrade variant already exists. Use continue or delete it first." },
|
|
2123
2260
|
409
|
|
@@ -2129,9 +2266,9 @@ ${user.trimEnd()}
|
|
|
2129
2266
|
} catch {
|
|
2130
2267
|
}
|
|
2131
2268
|
await updateTemplateBranch(dir, template, agentName);
|
|
2132
|
-
const parentDir =
|
|
2133
|
-
if (!
|
|
2134
|
-
|
|
2269
|
+
const parentDir = resolve8(dir, ".variants");
|
|
2270
|
+
if (!existsSync5(parentDir)) {
|
|
2271
|
+
mkdirSync4(parentDir, { recursive: true });
|
|
2135
2272
|
}
|
|
2136
2273
|
await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
2137
2274
|
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
@@ -2258,6 +2395,24 @@ ${user.trimEnd()}
|
|
|
2258
2395
|
budget.acknowledgeWarning(baseName);
|
|
2259
2396
|
forwardBody = JSON.stringify(parsed);
|
|
2260
2397
|
}
|
|
2398
|
+
const seedEntry = findAgent(baseName);
|
|
2399
|
+
if (seedEntry?.stage === "seed" && parsed) {
|
|
2400
|
+
try {
|
|
2401
|
+
const countResult = await db2.select({ count: sql3`count(*)` }).from(agentMessages).where(eq4(agentMessages.agent, baseName));
|
|
2402
|
+
const msgCount = countResult[0]?.count ?? 0;
|
|
2403
|
+
if (msgCount >= 10 && msgCount % 10 === 0) {
|
|
2404
|
+
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.]";
|
|
2405
|
+
if (typeof parsed.content === "string") {
|
|
2406
|
+
parsed.content = parsed.content + nudge;
|
|
2407
|
+
} else if (Array.isArray(parsed.content)) {
|
|
2408
|
+
parsed.content = [...parsed.content, { type: "text", text: nudge }];
|
|
2409
|
+
}
|
|
2410
|
+
forwardBody = JSON.stringify(parsed);
|
|
2411
|
+
}
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
console.error(`[daemon] failed to check seed message count for ${baseName}:`, err);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2261
2416
|
typingMap.set(channel, baseName, { persistent: true });
|
|
2262
2417
|
const conversationId = parsed?.conversationId ?? null;
|
|
2263
2418
|
if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
|
|
@@ -2324,7 +2479,7 @@ ${user.trimEnd()}
|
|
|
2324
2479
|
}).get("/:name/history/channels", async (c) => {
|
|
2325
2480
|
const name = c.req.param("name");
|
|
2326
2481
|
const db2 = await getDb();
|
|
2327
|
-
const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(
|
|
2482
|
+
const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq4(agentMessages.agent, name));
|
|
2328
2483
|
return c.json(rows.map((r) => r.channel));
|
|
2329
2484
|
}).get("/:name/history", async (c) => {
|
|
2330
2485
|
const name = c.req.param("name");
|
|
@@ -2332,23 +2487,23 @@ ${user.trimEnd()}
|
|
|
2332
2487
|
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
2333
2488
|
const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
|
|
2334
2489
|
const db2 = await getDb();
|
|
2335
|
-
const conditions = [
|
|
2490
|
+
const conditions = [eq4(agentMessages.agent, name)];
|
|
2336
2491
|
if (channel) {
|
|
2337
|
-
conditions.push(
|
|
2492
|
+
conditions.push(eq4(agentMessages.channel, channel));
|
|
2338
2493
|
}
|
|
2339
|
-
const rows = await db2.select().from(agentMessages).where(
|
|
2494
|
+
const rows = await db2.select().from(agentMessages).where(and3(...conditions)).orderBy(desc2(agentMessages.created_at)).limit(limit).offset(offset);
|
|
2340
2495
|
return c.json(rows);
|
|
2341
2496
|
});
|
|
2342
2497
|
var agents_default = app;
|
|
2343
2498
|
|
|
2344
2499
|
// src/web/routes/auth.ts
|
|
2345
|
-
import { zValidator } from "@hono/zod-validator";
|
|
2500
|
+
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
2346
2501
|
import { Hono as Hono2 } from "hono";
|
|
2347
2502
|
import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
|
|
2348
|
-
import { z } from "zod";
|
|
2349
|
-
var credentialsSchema =
|
|
2350
|
-
username:
|
|
2351
|
-
password:
|
|
2503
|
+
import { z as z2 } from "zod";
|
|
2504
|
+
var credentialsSchema = z2.object({
|
|
2505
|
+
username: z2.string().min(1),
|
|
2506
|
+
password: z2.string().min(1)
|
|
2352
2507
|
});
|
|
2353
2508
|
var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
|
|
2354
2509
|
const user = c.get("user");
|
|
@@ -2373,7 +2528,7 @@ var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
|
|
|
2373
2528
|
await approveUser(id);
|
|
2374
2529
|
return c.json({ ok: true });
|
|
2375
2530
|
});
|
|
2376
|
-
var app2 = new Hono2().post("/register",
|
|
2531
|
+
var app2 = new Hono2().post("/register", zValidator2("json", credentialsSchema), async (c) => {
|
|
2377
2532
|
const { username, password } = c.req.valid("json");
|
|
2378
2533
|
const existing = await getUserByUsername(username);
|
|
2379
2534
|
if (existing) {
|
|
@@ -2385,7 +2540,7 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
|
|
|
2385
2540
|
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
2386
2541
|
}
|
|
2387
2542
|
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
2388
|
-
}).post("/login",
|
|
2543
|
+
}).post("/login", zValidator2("json", credentialsSchema), async (c) => {
|
|
2389
2544
|
const { username, password } = c.req.valid("json");
|
|
2390
2545
|
const user = await verifyUser(username, password);
|
|
2391
2546
|
if (!user) {
|
|
@@ -2534,6 +2689,8 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
|
2534
2689
|
}
|
|
2535
2690
|
const entry = findAgent(name);
|
|
2536
2691
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2692
|
+
if (entry.stage === "seed")
|
|
2693
|
+
return c.json({ error: "Seed agents cannot use connectors \u2014 sprout first" }, 403);
|
|
2537
2694
|
const dir = agentDir(name);
|
|
2538
2695
|
const manager = getConnectorManager();
|
|
2539
2696
|
const envCheck = manager.checkConnectorEnv(type, name, dir);
|
|
@@ -2655,9 +2812,9 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
|
|
|
2655
2812
|
var env_default = app5;
|
|
2656
2813
|
|
|
2657
2814
|
// src/web/routes/files.ts
|
|
2658
|
-
import { existsSync as
|
|
2815
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2659
2816
|
import { readdir, readFile } from "fs/promises";
|
|
2660
|
-
import { resolve as
|
|
2817
|
+
import { resolve as resolve9 } from "path";
|
|
2661
2818
|
import { Hono as Hono6 } from "hono";
|
|
2662
2819
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
2663
2820
|
var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
@@ -2665,8 +2822,8 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
2665
2822
|
const entry = findAgent(name);
|
|
2666
2823
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2667
2824
|
const dir = agentDir(name);
|
|
2668
|
-
const homeDir =
|
|
2669
|
-
if (!
|
|
2825
|
+
const homeDir = resolve9(dir, "home");
|
|
2826
|
+
if (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
2670
2827
|
const allFiles = await readdir(homeDir);
|
|
2671
2828
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
2672
2829
|
return c.json(files);
|
|
@@ -2679,8 +2836,8 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
2679
2836
|
const entry = findAgent(name);
|
|
2680
2837
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2681
2838
|
const dir = agentDir(name);
|
|
2682
|
-
const filePath =
|
|
2683
|
-
if (!
|
|
2839
|
+
const filePath = resolve9(dir, "home", filename);
|
|
2840
|
+
if (!existsSync6(filePath)) {
|
|
2684
2841
|
return c.json({ error: "File not found" }, 404);
|
|
2685
2842
|
}
|
|
2686
2843
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -2690,16 +2847,16 @@ var files_default = app6;
|
|
|
2690
2847
|
|
|
2691
2848
|
// src/web/routes/logs.ts
|
|
2692
2849
|
import { spawn as spawn2 } from "child_process";
|
|
2693
|
-
import { existsSync as
|
|
2694
|
-
import { resolve as
|
|
2850
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2851
|
+
import { resolve as resolve10 } from "path";
|
|
2695
2852
|
import { Hono as Hono7 } from "hono";
|
|
2696
2853
|
import { streamSSE } from "hono/streaming";
|
|
2697
2854
|
var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
2698
2855
|
const name = c.req.param("name");
|
|
2699
2856
|
const entry = findAgent(name);
|
|
2700
2857
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2701
|
-
const logFile =
|
|
2702
|
-
if (!
|
|
2858
|
+
const logFile = resolve10(stateDir(name), "logs", "agent.log");
|
|
2859
|
+
if (!existsSync7(logFile)) {
|
|
2703
2860
|
return c.json({ error: "No log file found" }, 404);
|
|
2704
2861
|
}
|
|
2705
2862
|
return streamSSE(c, async (stream) => {
|
|
@@ -2717,17 +2874,17 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
2717
2874
|
stream.onAbort(() => {
|
|
2718
2875
|
tail.kill();
|
|
2719
2876
|
});
|
|
2720
|
-
await new Promise((
|
|
2721
|
-
tail.on("exit",
|
|
2722
|
-
stream.onAbort(
|
|
2877
|
+
await new Promise((resolve16) => {
|
|
2878
|
+
tail.on("exit", resolve16);
|
|
2879
|
+
stream.onAbort(resolve16);
|
|
2723
2880
|
});
|
|
2724
2881
|
});
|
|
2725
2882
|
}).get("/:name/logs/tail", async (c) => {
|
|
2726
2883
|
const name = c.req.param("name");
|
|
2727
2884
|
const entry = findAgent(name);
|
|
2728
2885
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2729
|
-
const logFile =
|
|
2730
|
-
if (!
|
|
2886
|
+
const logFile = resolve10(stateDir(name), "logs", "agent.log");
|
|
2887
|
+
if (!existsSync7(logFile)) {
|
|
2731
2888
|
return c.json({ error: "No log file found" }, 404);
|
|
2732
2889
|
}
|
|
2733
2890
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
@@ -2737,8 +2894,8 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
2737
2894
|
tail.stdout.on("data", (data) => {
|
|
2738
2895
|
output += data.toString();
|
|
2739
2896
|
});
|
|
2740
|
-
await new Promise((
|
|
2741
|
-
tail.on("exit",
|
|
2897
|
+
await new Promise((resolve16) => {
|
|
2898
|
+
tail.on("exit", resolve16);
|
|
2742
2899
|
});
|
|
2743
2900
|
return c.text(output);
|
|
2744
2901
|
});
|
|
@@ -2762,7 +2919,10 @@ var app8 = new Hono8().get("/:name/schedules", (c) => {
|
|
|
2762
2919
|
return c.json(readSchedules(name));
|
|
2763
2920
|
}).post("/:name/schedules", requireAdmin, async (c) => {
|
|
2764
2921
|
const name = c.req.param("name");
|
|
2765
|
-
|
|
2922
|
+
const entry = findAgent(name);
|
|
2923
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2924
|
+
if (entry.stage === "seed")
|
|
2925
|
+
return c.json({ error: "Seed agents cannot use schedules \u2014 sprout first" }, 403);
|
|
2766
2926
|
const body = await c.req.json();
|
|
2767
2927
|
if (!body.cron || !body.message) {
|
|
2768
2928
|
return c.json({ error: "cron and message are required" }, 400);
|
|
@@ -2846,10 +3006,10 @@ var app9 = new Hono9().post("/restart", requireAdmin, (c) => {
|
|
|
2846
3006
|
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
2847
3007
|
});
|
|
2848
3008
|
});
|
|
2849
|
-
await new Promise((
|
|
3009
|
+
await new Promise((resolve16) => {
|
|
2850
3010
|
stream.onAbort(() => {
|
|
2851
3011
|
unsubscribe();
|
|
2852
|
-
|
|
3012
|
+
resolve16();
|
|
2853
3013
|
});
|
|
2854
3014
|
});
|
|
2855
3015
|
});
|
|
@@ -2857,15 +3017,15 @@ var app9 = new Hono9().post("/restart", requireAdmin, (c) => {
|
|
|
2857
3017
|
var system_default = app9;
|
|
2858
3018
|
|
|
2859
3019
|
// src/web/routes/typing.ts
|
|
2860
|
-
import { zValidator as
|
|
3020
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
2861
3021
|
import { Hono as Hono10 } from "hono";
|
|
2862
|
-
import { z as
|
|
2863
|
-
var typingSchema =
|
|
2864
|
-
channel:
|
|
2865
|
-
sender:
|
|
2866
|
-
active:
|
|
3022
|
+
import { z as z3 } from "zod";
|
|
3023
|
+
var typingSchema = z3.object({
|
|
3024
|
+
channel: z3.string().min(1),
|
|
3025
|
+
sender: z3.string().min(1),
|
|
3026
|
+
active: z3.boolean()
|
|
2867
3027
|
});
|
|
2868
|
-
var app10 = new Hono10().post("/:name/typing",
|
|
3028
|
+
var app10 = new Hono10().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
|
|
2869
3029
|
const { channel, sender, active } = c.req.valid("json");
|
|
2870
3030
|
const map = getTypingMap();
|
|
2871
3031
|
if (active) {
|
|
@@ -2906,16 +3066,16 @@ var app11 = new Hono11().get("/update", async (c) => {
|
|
|
2906
3066
|
var update_default = app11;
|
|
2907
3067
|
|
|
2908
3068
|
// src/web/routes/variants.ts
|
|
2909
|
-
import { existsSync as
|
|
2910
|
-
import { resolve as
|
|
3069
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
3070
|
+
import { resolve as resolve12 } from "path";
|
|
2911
3071
|
import { Hono as Hono12 } from "hono";
|
|
2912
3072
|
|
|
2913
3073
|
// src/lib/spawn-server.ts
|
|
2914
3074
|
import { spawn as spawn4 } from "child_process";
|
|
2915
|
-
import { closeSync, mkdirSync as
|
|
2916
|
-
import { resolve as
|
|
3075
|
+
import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync6 } from "fs";
|
|
3076
|
+
import { resolve as resolve11 } from "path";
|
|
2917
3077
|
function tsxBin(cwd) {
|
|
2918
|
-
return
|
|
3078
|
+
return resolve11(cwd, "node_modules", ".bin", "tsx");
|
|
2919
3079
|
}
|
|
2920
3080
|
function spawnServer(cwd, port, options) {
|
|
2921
3081
|
if (options?.detached) {
|
|
@@ -2928,31 +3088,31 @@ function spawnAttached(cwd, port) {
|
|
|
2928
3088
|
cwd,
|
|
2929
3089
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2930
3090
|
});
|
|
2931
|
-
return new Promise((
|
|
2932
|
-
const timeout = setTimeout(() =>
|
|
3091
|
+
return new Promise((resolve16) => {
|
|
3092
|
+
const timeout = setTimeout(() => resolve16(null), 3e4);
|
|
2933
3093
|
function checkOutput(data) {
|
|
2934
3094
|
const match = data.toString().match(/listening on :(\d+)/);
|
|
2935
3095
|
if (match) {
|
|
2936
3096
|
clearTimeout(timeout);
|
|
2937
|
-
|
|
3097
|
+
resolve16({ child, actualPort: parseInt(match[1], 10) });
|
|
2938
3098
|
}
|
|
2939
3099
|
}
|
|
2940
3100
|
child.stdout?.on("data", checkOutput);
|
|
2941
3101
|
child.stderr?.on("data", checkOutput);
|
|
2942
3102
|
child.on("error", () => {
|
|
2943
3103
|
clearTimeout(timeout);
|
|
2944
|
-
|
|
3104
|
+
resolve16(null);
|
|
2945
3105
|
});
|
|
2946
3106
|
child.on("exit", () => {
|
|
2947
3107
|
clearTimeout(timeout);
|
|
2948
|
-
|
|
3108
|
+
resolve16(null);
|
|
2949
3109
|
});
|
|
2950
3110
|
});
|
|
2951
3111
|
}
|
|
2952
3112
|
function spawnDetached(cwd, port, logDir) {
|
|
2953
|
-
const logsDir = logDir ??
|
|
2954
|
-
|
|
2955
|
-
const logPath =
|
|
3113
|
+
const logsDir = logDir ?? resolve11(cwd, ".volute", "logs");
|
|
3114
|
+
mkdirSync5(logsDir, { recursive: true });
|
|
3115
|
+
const logPath = resolve11(logsDir, "agent.log");
|
|
2956
3116
|
const logFd = openSync(logPath, "a");
|
|
2957
3117
|
const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
2958
3118
|
cwd,
|
|
@@ -2972,7 +3132,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
2972
3132
|
}
|
|
2973
3133
|
const interval = setInterval(() => {
|
|
2974
3134
|
try {
|
|
2975
|
-
const content =
|
|
3135
|
+
const content = readFileSync6(logPath, "utf-8");
|
|
2976
3136
|
const match = content.match(/listening on :(\d+)/);
|
|
2977
3137
|
if (match) {
|
|
2978
3138
|
finish({ child, actualPort: parseInt(match[1], 10) });
|
|
@@ -3041,6 +3201,8 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
3041
3201
|
const agentName = c.req.param("name");
|
|
3042
3202
|
const entry = findAgent(agentName);
|
|
3043
3203
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
3204
|
+
if (entry.stage === "seed")
|
|
3205
|
+
return c.json({ error: "Seed agents cannot create variants \u2014 sprout first" }, 403);
|
|
3044
3206
|
let body;
|
|
3045
3207
|
try {
|
|
3046
3208
|
body = await c.req.json();
|
|
@@ -3052,11 +3214,11 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
3052
3214
|
const err = validateBranchName(variantName);
|
|
3053
3215
|
if (err) return c.json({ error: err }, 400);
|
|
3054
3216
|
const projectRoot = agentDir(agentName);
|
|
3055
|
-
const variantDir =
|
|
3056
|
-
if (
|
|
3217
|
+
const variantDir = resolve12(projectRoot, ".variants", variantName);
|
|
3218
|
+
if (existsSync8(variantDir)) {
|
|
3057
3219
|
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
3058
3220
|
}
|
|
3059
|
-
|
|
3221
|
+
mkdirSync6(resolve12(projectRoot, ".variants"), { recursive: true });
|
|
3060
3222
|
try {
|
|
3061
3223
|
await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
3062
3224
|
} catch (e) {
|
|
@@ -3070,7 +3232,7 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
3070
3232
|
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
3071
3233
|
}
|
|
3072
3234
|
if (body.soul) {
|
|
3073
|
-
|
|
3235
|
+
writeFileSync5(resolve12(variantDir, "home/SOUL.md"), body.soul);
|
|
3074
3236
|
}
|
|
3075
3237
|
const variantPort = body.port ?? nextPort();
|
|
3076
3238
|
const variant = {
|
|
@@ -3109,7 +3271,7 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
3109
3271
|
} catch {
|
|
3110
3272
|
}
|
|
3111
3273
|
const projectRoot = agentDir(agentName);
|
|
3112
|
-
if (
|
|
3274
|
+
if (existsSync8(variant.path)) {
|
|
3113
3275
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
3114
3276
|
if (status) {
|
|
3115
3277
|
try {
|
|
@@ -3166,7 +3328,7 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
3166
3328
|
} catch (e) {
|
|
3167
3329
|
return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
|
|
3168
3330
|
}
|
|
3169
|
-
if (
|
|
3331
|
+
if (existsSync8(variant.path)) {
|
|
3170
3332
|
try {
|
|
3171
3333
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
3172
3334
|
} catch {
|
|
@@ -3218,7 +3380,7 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
3218
3380
|
} catch {
|
|
3219
3381
|
}
|
|
3220
3382
|
}
|
|
3221
|
-
if (
|
|
3383
|
+
if (existsSync8(variant.path)) {
|
|
3222
3384
|
try {
|
|
3223
3385
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
3224
3386
|
} catch {
|
|
@@ -3235,221 +3397,26 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
3235
3397
|
var variants_default = app12;
|
|
3236
3398
|
|
|
3237
3399
|
// src/web/routes/volute/chat.ts
|
|
3238
|
-
import { readFileSync as
|
|
3239
|
-
import { resolve as
|
|
3240
|
-
import { zValidator as
|
|
3400
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
3401
|
+
import { resolve as resolve13 } from "path";
|
|
3402
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
3241
3403
|
import { Hono as Hono13 } from "hono";
|
|
3242
3404
|
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
3243
|
-
import { z as
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
}
|
|
3253
|
-
set.add(callback);
|
|
3254
|
-
return () => {
|
|
3255
|
-
set.delete(callback);
|
|
3256
|
-
if (set.size === 0) subscribers.delete(conversationId);
|
|
3257
|
-
};
|
|
3258
|
-
}
|
|
3259
|
-
function publish(conversationId, event) {
|
|
3260
|
-
const set = subscribers.get(conversationId);
|
|
3261
|
-
if (!set) return;
|
|
3262
|
-
for (const cb of set) {
|
|
3263
|
-
try {
|
|
3264
|
-
cb(event);
|
|
3265
|
-
} catch (err) {
|
|
3266
|
-
console.error("[conversation-events] subscriber threw:", err);
|
|
3267
|
-
set.delete(cb);
|
|
3268
|
-
if (set.size === 0) subscribers.delete(conversationId);
|
|
3269
|
-
}
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
// src/lib/conversations.ts
|
|
3274
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
3275
|
-
import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
|
|
3276
|
-
async function createConversation(agentName, channel, opts) {
|
|
3277
|
-
const db2 = await getDb();
|
|
3278
|
-
const id = randomUUID2();
|
|
3279
|
-
await db2.insert(conversations).values({
|
|
3280
|
-
id,
|
|
3281
|
-
agent_name: agentName,
|
|
3282
|
-
channel,
|
|
3283
|
-
user_id: opts?.userId ?? null,
|
|
3284
|
-
title: opts?.title ?? null
|
|
3285
|
-
});
|
|
3286
|
-
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
3287
|
-
await db2.insert(conversationParticipants).values(
|
|
3288
|
-
opts.participantIds.map((uid, i) => ({
|
|
3289
|
-
conversation_id: id,
|
|
3290
|
-
user_id: uid,
|
|
3291
|
-
role: i === 0 ? "owner" : "member"
|
|
3292
|
-
}))
|
|
3293
|
-
);
|
|
3294
|
-
}
|
|
3295
|
-
return {
|
|
3296
|
-
id,
|
|
3297
|
-
agent_name: agentName,
|
|
3298
|
-
channel,
|
|
3299
|
-
user_id: opts?.userId ?? null,
|
|
3300
|
-
title: opts?.title ?? null,
|
|
3301
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3302
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3303
|
-
};
|
|
3304
|
-
}
|
|
3305
|
-
async function getConversation(id) {
|
|
3306
|
-
const db2 = await getDb();
|
|
3307
|
-
const row = await db2.select().from(conversations).where(eq4(conversations.id, id)).get();
|
|
3308
|
-
return row ?? null;
|
|
3309
|
-
}
|
|
3310
|
-
async function getParticipants(conversationId) {
|
|
3311
|
-
const db2 = await getDb();
|
|
3312
|
-
const rows = await db2.select({
|
|
3313
|
-
userId: conversationParticipants.user_id,
|
|
3314
|
-
username: users.username,
|
|
3315
|
-
userType: users.user_type,
|
|
3316
|
-
role: conversationParticipants.role
|
|
3317
|
-
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
|
|
3318
|
-
return rows;
|
|
3319
|
-
}
|
|
3320
|
-
async function isParticipant(conversationId, userId) {
|
|
3321
|
-
const db2 = await getDb();
|
|
3322
|
-
const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
3323
|
-
and3(
|
|
3324
|
-
eq4(conversationParticipants.conversation_id, conversationId),
|
|
3325
|
-
eq4(conversationParticipants.user_id, userId)
|
|
3326
|
-
)
|
|
3327
|
-
).get();
|
|
3328
|
-
return row != null;
|
|
3329
|
-
}
|
|
3330
|
-
async function listConversationsForUser(userId) {
|
|
3331
|
-
const db2 = await getDb();
|
|
3332
|
-
const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
|
|
3333
|
-
if (participantRows.length === 0) return [];
|
|
3334
|
-
const convIds = participantRows.map((r) => r.conversation_id);
|
|
3335
|
-
return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
|
|
3336
|
-
}
|
|
3337
|
-
async function isParticipantOrOwner(conversationId, userId) {
|
|
3338
|
-
if (await isParticipant(conversationId, userId)) return true;
|
|
3339
|
-
const db2 = await getDb();
|
|
3340
|
-
const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
|
|
3341
|
-
return row != null;
|
|
3342
|
-
}
|
|
3343
|
-
async function deleteConversationForUser(id, userId) {
|
|
3344
|
-
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
3345
|
-
await deleteConversation(id);
|
|
3346
|
-
return true;
|
|
3347
|
-
}
|
|
3348
|
-
async function addMessage(conversationId, role, senderName, content) {
|
|
3349
|
-
const db2 = await getDb();
|
|
3350
|
-
const serialized = JSON.stringify(content);
|
|
3351
|
-
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 });
|
|
3352
|
-
await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
|
|
3353
|
-
if (role === "user") {
|
|
3354
|
-
const firstText = content.find((b) => b.type === "text");
|
|
3355
|
-
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
3356
|
-
if (title) {
|
|
3357
|
-
await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
const msg = {
|
|
3361
|
-
id: result.id,
|
|
3362
|
-
conversation_id: conversationId,
|
|
3363
|
-
role,
|
|
3364
|
-
sender_name: senderName,
|
|
3365
|
-
content,
|
|
3366
|
-
created_at: result.created_at
|
|
3367
|
-
};
|
|
3368
|
-
publish(conversationId, {
|
|
3369
|
-
type: "message",
|
|
3370
|
-
id: msg.id,
|
|
3371
|
-
role: msg.role,
|
|
3372
|
-
senderName: msg.sender_name,
|
|
3373
|
-
content: msg.content,
|
|
3374
|
-
createdAt: msg.created_at
|
|
3375
|
-
});
|
|
3376
|
-
return msg;
|
|
3377
|
-
}
|
|
3378
|
-
async function getMessages(conversationId) {
|
|
3379
|
-
const db2 = await getDb();
|
|
3380
|
-
const rows = await db2.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
3381
|
-
return rows.map((row) => {
|
|
3382
|
-
let content;
|
|
3383
|
-
try {
|
|
3384
|
-
const parsed = JSON.parse(row.content);
|
|
3385
|
-
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
3386
|
-
} catch {
|
|
3387
|
-
content = [{ type: "text", text: row.content }];
|
|
3388
|
-
}
|
|
3389
|
-
return { ...row, content };
|
|
3390
|
-
});
|
|
3391
|
-
}
|
|
3392
|
-
async function listConversationsWithParticipants(userId) {
|
|
3393
|
-
const convs = await listConversationsForUser(userId);
|
|
3394
|
-
if (convs.length === 0) return [];
|
|
3395
|
-
const db2 = await getDb();
|
|
3396
|
-
const convIds = convs.map((c) => c.id);
|
|
3397
|
-
const rows = await db2.select({
|
|
3398
|
-
conversationId: conversationParticipants.conversation_id,
|
|
3399
|
-
userId: users.id,
|
|
3400
|
-
username: users.username,
|
|
3401
|
-
userType: users.user_type,
|
|
3402
|
-
role: conversationParticipants.role
|
|
3403
|
-
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
3404
|
-
const byConv = /* @__PURE__ */ new Map();
|
|
3405
|
-
for (const r of rows) {
|
|
3406
|
-
let arr = byConv.get(r.conversationId);
|
|
3407
|
-
if (!arr) {
|
|
3408
|
-
arr = [];
|
|
3409
|
-
byConv.set(r.conversationId, arr);
|
|
3410
|
-
}
|
|
3411
|
-
arr.push({
|
|
3412
|
-
userId: r.userId,
|
|
3413
|
-
username: r.username,
|
|
3414
|
-
userType: r.userType,
|
|
3415
|
-
role: r.role
|
|
3416
|
-
});
|
|
3417
|
-
}
|
|
3418
|
-
return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
|
|
3419
|
-
}
|
|
3420
|
-
async function findDMConversation(agentName, participantIds) {
|
|
3421
|
-
const db2 = await getDb();
|
|
3422
|
-
const agentConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.agent_name, agentName)).all();
|
|
3423
|
-
for (const conv of agentConvs) {
|
|
3424
|
-
const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
|
|
3425
|
-
if (rows.length !== 2) continue;
|
|
3426
|
-
const ids = new Set(rows.map((r) => r.user_id));
|
|
3427
|
-
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
3428
|
-
return conv.id;
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
return null;
|
|
3432
|
-
}
|
|
3433
|
-
async function deleteConversation(id) {
|
|
3434
|
-
const db2 = await getDb();
|
|
3435
|
-
await db2.delete(conversations).where(eq4(conversations.id, id));
|
|
3436
|
-
}
|
|
3437
|
-
|
|
3438
|
-
// src/web/routes/volute/chat.ts
|
|
3439
|
-
var chatSchema = z3.object({
|
|
3440
|
-
message: z3.string().optional(),
|
|
3441
|
-
conversationId: z3.string().optional(),
|
|
3442
|
-
sender: z3.string().optional(),
|
|
3443
|
-
images: z3.array(
|
|
3444
|
-
z3.object({
|
|
3445
|
-
media_type: z3.string(),
|
|
3446
|
-
data: z3.string()
|
|
3405
|
+
import { z as z4 } from "zod";
|
|
3406
|
+
var chatSchema = z4.object({
|
|
3407
|
+
message: z4.string().optional(),
|
|
3408
|
+
conversationId: z4.string().optional(),
|
|
3409
|
+
sender: z4.string().optional(),
|
|
3410
|
+
images: z4.array(
|
|
3411
|
+
z4.object({
|
|
3412
|
+
media_type: z4.string(),
|
|
3413
|
+
data: z4.string()
|
|
3447
3414
|
})
|
|
3448
3415
|
).optional()
|
|
3449
3416
|
});
|
|
3450
3417
|
function getDaemonUrl() {
|
|
3451
3418
|
try {
|
|
3452
|
-
const data = JSON.parse(
|
|
3419
|
+
const data = JSON.parse(readFileSync7(resolve13(voluteHome(), "daemon.json"), "utf-8"));
|
|
3453
3420
|
return `http://${daemonLoopback()}:${data.port}`;
|
|
3454
3421
|
} catch (err) {
|
|
3455
3422
|
throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3465,7 +3432,7 @@ function daemonFetchInternal(path, body) {
|
|
|
3465
3432
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
3466
3433
|
return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
|
|
3467
3434
|
}
|
|
3468
|
-
var app13 = new Hono13().post("/:name/chat",
|
|
3435
|
+
var app13 = new Hono13().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
|
|
3469
3436
|
const name = c.req.param("name");
|
|
3470
3437
|
const [baseName] = name.split("@", 2);
|
|
3471
3438
|
const entry = findAgent(baseName);
|
|
@@ -3513,7 +3480,6 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
3513
3480
|
}
|
|
3514
3481
|
const conv = await getConversation(conversationId);
|
|
3515
3482
|
const convTitle = conv?.title;
|
|
3516
|
-
const channel = convTitle ? `volute:${slugify(convTitle)}` : `volute:${conversationId}`;
|
|
3517
3483
|
const contentBlocks = [];
|
|
3518
3484
|
if (body.message) {
|
|
3519
3485
|
contentBlocks.push({ type: "text", text: body.message });
|
|
@@ -3527,13 +3493,21 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
3527
3493
|
const participants = await getParticipants(conversationId);
|
|
3528
3494
|
const agentParticipants = participants.filter((p) => p.userType === "agent");
|
|
3529
3495
|
const participantNames = participants.map((p) => p.username);
|
|
3530
|
-
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-
|
|
3496
|
+
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-HHBAAL2D.js");
|
|
3531
3497
|
const manager = getAgentManager2();
|
|
3532
3498
|
const runningAgents = agentParticipants.map((ap) => {
|
|
3533
3499
|
const agentKey = ap.username === baseName ? name : ap.username;
|
|
3534
3500
|
return manager.isRunning(agentKey) ? ap.username : null;
|
|
3535
3501
|
}).filter((n) => n !== null && n !== senderName);
|
|
3536
3502
|
const isDM = participants.length === 2;
|
|
3503
|
+
function channelForAgent(agentUsername) {
|
|
3504
|
+
return buildVoluteSlug({
|
|
3505
|
+
participants,
|
|
3506
|
+
agentUsername,
|
|
3507
|
+
convTitle,
|
|
3508
|
+
conversationId
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3537
3511
|
const channelEntry = {
|
|
3538
3512
|
platformId: conversationId,
|
|
3539
3513
|
platform: "volute",
|
|
@@ -3542,25 +3516,26 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
3542
3516
|
};
|
|
3543
3517
|
for (const ap of agentParticipants) {
|
|
3544
3518
|
try {
|
|
3545
|
-
writeChannelEntry(ap.username,
|
|
3519
|
+
writeChannelEntry(ap.username, channelForAgent(ap.username), channelEntry);
|
|
3546
3520
|
} catch (err) {
|
|
3547
3521
|
console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
|
|
3548
3522
|
}
|
|
3549
3523
|
}
|
|
3550
|
-
const typingMap = getTypingMap();
|
|
3551
|
-
const currentlyTyping = typingMap.get(channel);
|
|
3552
|
-
const payload = JSON.stringify({
|
|
3553
|
-
content: contentBlocks,
|
|
3554
|
-
channel,
|
|
3555
|
-
conversationId,
|
|
3556
|
-
sender: senderName,
|
|
3557
|
-
participants: participantNames,
|
|
3558
|
-
participantCount: participants.length,
|
|
3559
|
-
isDM,
|
|
3560
|
-
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
3561
|
-
});
|
|
3562
3524
|
for (const agentName of runningAgents) {
|
|
3563
3525
|
const targetName = agentName === baseName ? name : agentName;
|
|
3526
|
+
const channel = channelForAgent(agentName);
|
|
3527
|
+
const typingMap = getTypingMap();
|
|
3528
|
+
const currentlyTyping = typingMap.get(channel);
|
|
3529
|
+
const payload = JSON.stringify({
|
|
3530
|
+
content: contentBlocks,
|
|
3531
|
+
channel,
|
|
3532
|
+
conversationId,
|
|
3533
|
+
sender: senderName,
|
|
3534
|
+
participants: participantNames,
|
|
3535
|
+
participantCount: participants.length,
|
|
3536
|
+
isDM,
|
|
3537
|
+
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
3538
|
+
});
|
|
3564
3539
|
daemonFetchInternal(`/api/agents/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
|
|
3565
3540
|
if (!res.ok) {
|
|
3566
3541
|
const text2 = await res.text().catch(() => "");
|
|
@@ -3588,11 +3563,11 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
3588
3563
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
3589
3564
|
});
|
|
3590
3565
|
}, 15e3);
|
|
3591
|
-
await new Promise((
|
|
3566
|
+
await new Promise((resolve16) => {
|
|
3592
3567
|
stream.onAbort(() => {
|
|
3593
3568
|
unsubscribe();
|
|
3594
3569
|
clearInterval(keepAlive);
|
|
3595
|
-
|
|
3570
|
+
resolve16();
|
|
3596
3571
|
});
|
|
3597
3572
|
});
|
|
3598
3573
|
});
|
|
@@ -3600,13 +3575,13 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
3600
3575
|
var chat_default = app13;
|
|
3601
3576
|
|
|
3602
3577
|
// src/web/routes/volute/conversations.ts
|
|
3603
|
-
import { zValidator as
|
|
3578
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
3604
3579
|
import { Hono as Hono14 } from "hono";
|
|
3605
|
-
import { z as
|
|
3606
|
-
var createConvSchema =
|
|
3607
|
-
title:
|
|
3608
|
-
participantIds:
|
|
3609
|
-
participantNames:
|
|
3580
|
+
import { z as z5 } from "zod";
|
|
3581
|
+
var createConvSchema = z5.object({
|
|
3582
|
+
title: z5.string().optional(),
|
|
3583
|
+
participantIds: z5.array(z5.number()).optional(),
|
|
3584
|
+
participantNames: z5.array(z5.string()).optional()
|
|
3610
3585
|
});
|
|
3611
3586
|
var app14 = new Hono14().get("/:name/conversations", async (c) => {
|
|
3612
3587
|
const name = c.req.param("name");
|
|
@@ -3619,7 +3594,7 @@ var app14 = new Hono14().get("/:name/conversations", async (c) => {
|
|
|
3619
3594
|
const all = await listConversationsForUser(lookupId);
|
|
3620
3595
|
const convs = all.filter((c2) => c2.agent_name === name);
|
|
3621
3596
|
return c.json(convs);
|
|
3622
|
-
}).post("/:name/conversations",
|
|
3597
|
+
}).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
|
|
3623
3598
|
const name = c.req.param("name");
|
|
3624
3599
|
const user = c.get("user");
|
|
3625
3600
|
const body = c.req.valid("json");
|
|
@@ -3696,12 +3671,12 @@ var app14 = new Hono14().get("/:name/conversations", async (c) => {
|
|
|
3696
3671
|
var conversations_default = app14;
|
|
3697
3672
|
|
|
3698
3673
|
// src/web/routes/volute/user-conversations.ts
|
|
3699
|
-
import { zValidator as
|
|
3674
|
+
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
3700
3675
|
import { Hono as Hono15 } from "hono";
|
|
3701
|
-
import { z as
|
|
3702
|
-
var createSchema =
|
|
3703
|
-
title:
|
|
3704
|
-
participantNames:
|
|
3676
|
+
import { z as z6 } from "zod";
|
|
3677
|
+
var createSchema = z6.object({
|
|
3678
|
+
title: z6.string().optional(),
|
|
3679
|
+
participantNames: z6.array(z6.string()).min(1)
|
|
3705
3680
|
});
|
|
3706
3681
|
var app15 = new Hono15().use("*", authMiddleware).get("/", async (c) => {
|
|
3707
3682
|
const user = c.get("user");
|
|
@@ -3715,7 +3690,7 @@ var app15 = new Hono15().use("*", authMiddleware).get("/", async (c) => {
|
|
|
3715
3690
|
}
|
|
3716
3691
|
const msgs = await getMessages(id);
|
|
3717
3692
|
return c.json(msgs);
|
|
3718
|
-
}).post("/",
|
|
3693
|
+
}).post("/", zValidator6("json", createSchema), async (c) => {
|
|
3719
3694
|
const user = c.get("user");
|
|
3720
3695
|
const body = c.req.valid("json");
|
|
3721
3696
|
const participantIds = /* @__PURE__ */ new Set();
|
|
@@ -3820,20 +3795,20 @@ async function startServer({
|
|
|
3820
3795
|
hostname = "127.0.0.1"
|
|
3821
3796
|
}) {
|
|
3822
3797
|
let assetsDir = "";
|
|
3823
|
-
let searchDir =
|
|
3798
|
+
let searchDir = dirname3(new URL(import.meta.url).pathname);
|
|
3824
3799
|
for (let i = 0; i < 5; i++) {
|
|
3825
|
-
const candidate =
|
|
3826
|
-
if (
|
|
3800
|
+
const candidate = resolve14(searchDir, "dist", "web-assets");
|
|
3801
|
+
if (existsSync9(candidate)) {
|
|
3827
3802
|
assetsDir = candidate;
|
|
3828
3803
|
break;
|
|
3829
3804
|
}
|
|
3830
|
-
searchDir =
|
|
3805
|
+
searchDir = dirname3(searchDir);
|
|
3831
3806
|
}
|
|
3832
3807
|
if (assetsDir) {
|
|
3833
3808
|
app_default.get("*", async (c) => {
|
|
3834
3809
|
const urlPath = new URL(c.req.url).pathname;
|
|
3835
3810
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
3836
|
-
const filePath =
|
|
3811
|
+
const filePath = resolve14(assetsDir, urlPath.slice(1));
|
|
3837
3812
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
3838
3813
|
const s = await stat(filePath).catch(() => null);
|
|
3839
3814
|
if (s?.isFile()) {
|
|
@@ -3842,7 +3817,7 @@ async function startServer({
|
|
|
3842
3817
|
const body = await readFile2(filePath);
|
|
3843
3818
|
return c.body(body, 200, { "Content-Type": mime });
|
|
3844
3819
|
}
|
|
3845
|
-
const indexPath =
|
|
3820
|
+
const indexPath = resolve14(assetsDir, "index.html");
|
|
3846
3821
|
const indexStat = await stat(indexPath).catch(() => null);
|
|
3847
3822
|
if (indexStat?.isFile()) {
|
|
3848
3823
|
const body = await readFile2(indexPath, "utf-8");
|
|
@@ -3852,10 +3827,10 @@ async function startServer({
|
|
|
3852
3827
|
});
|
|
3853
3828
|
}
|
|
3854
3829
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
3855
|
-
await new Promise((
|
|
3830
|
+
await new Promise((resolve16, reject) => {
|
|
3856
3831
|
server.on("listening", () => {
|
|
3857
3832
|
logger_default.info("Volute UI running", { hostname, port });
|
|
3858
|
-
|
|
3833
|
+
resolve16();
|
|
3859
3834
|
});
|
|
3860
3835
|
server.on("error", (err) => {
|
|
3861
3836
|
reject(err);
|
|
@@ -3866,14 +3841,14 @@ async function startServer({
|
|
|
3866
3841
|
|
|
3867
3842
|
// src/daemon.ts
|
|
3868
3843
|
if (!process.env.VOLUTE_HOME) {
|
|
3869
|
-
process.env.VOLUTE_HOME =
|
|
3844
|
+
process.env.VOLUTE_HOME = resolve15(homedir2(), ".volute");
|
|
3870
3845
|
}
|
|
3871
3846
|
async function startDaemon(opts) {
|
|
3872
3847
|
const { port, hostname } = opts;
|
|
3873
3848
|
const myPid = String(process.pid);
|
|
3874
3849
|
const home = voluteHome();
|
|
3875
3850
|
if (!opts.foreground) {
|
|
3876
|
-
const log2 = new RotatingLog(
|
|
3851
|
+
const log2 = new RotatingLog(resolve15(home, "daemon.log"));
|
|
3877
3852
|
const write2 = (...args) => log2.write(`${format(...args)}
|
|
3878
3853
|
`);
|
|
3879
3854
|
console.log = write2;
|
|
@@ -3881,9 +3856,9 @@ async function startDaemon(opts) {
|
|
|
3881
3856
|
console.warn = write2;
|
|
3882
3857
|
console.info = write2;
|
|
3883
3858
|
}
|
|
3884
|
-
const DAEMON_PID_PATH =
|
|
3885
|
-
const DAEMON_JSON_PATH =
|
|
3886
|
-
|
|
3859
|
+
const DAEMON_PID_PATH = resolve15(home, "daemon.pid");
|
|
3860
|
+
const DAEMON_JSON_PATH = resolve15(home, "daemon.json");
|
|
3861
|
+
mkdirSync7(home, { recursive: true });
|
|
3887
3862
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
3888
3863
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
3889
3864
|
process.env.VOLUTE_DAEMON_PORT = String(port);
|
|
@@ -3899,8 +3874,8 @@ async function startDaemon(opts) {
|
|
|
3899
3874
|
}
|
|
3900
3875
|
throw err;
|
|
3901
3876
|
}
|
|
3902
|
-
|
|
3903
|
-
|
|
3877
|
+
writeFileSync6(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
3878
|
+
writeFileSync6(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
3904
3879
|
`, {
|
|
3905
3880
|
mode: 420
|
|
3906
3881
|
});
|
|
@@ -3923,6 +3898,7 @@ async function startDaemon(opts) {
|
|
|
3923
3898
|
if (!entry.running) continue;
|
|
3924
3899
|
try {
|
|
3925
3900
|
await manager.startAgent(entry.name);
|
|
3901
|
+
if (entry.stage === "seed") continue;
|
|
3926
3902
|
const dir = agentDir(entry.name);
|
|
3927
3903
|
await connectors.startConnectors(entry.name, dir, entry.port, port);
|
|
3928
3904
|
scheduler.loadSchedules(entry.name);
|
|
@@ -3954,13 +3930,13 @@ async function startDaemon(opts) {
|
|
|
3954
3930
|
console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
|
|
3955
3931
|
function cleanup() {
|
|
3956
3932
|
try {
|
|
3957
|
-
if (
|
|
3933
|
+
if (readFileSync8(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
3958
3934
|
unlinkSync2(DAEMON_PID_PATH);
|
|
3959
3935
|
}
|
|
3960
3936
|
} catch {
|
|
3961
3937
|
}
|
|
3962
3938
|
try {
|
|
3963
|
-
const data = JSON.parse(
|
|
3939
|
+
const data = JSON.parse(readFileSync8(DAEMON_JSON_PATH, "utf-8"));
|
|
3964
3940
|
if (data.token === token) {
|
|
3965
3941
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
3966
3942
|
}
|