volute 0.8.3 → 0.10.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-YORVRB6I.js → agent-ECRX44DB.js} +10 -10
- package/dist/{agent-manager-CMMH5KQQ.js → agent-manager-MRHHKAB6.js} +2 -2
- package/dist/api-client-YPKOZP2O.js +10 -0
- package/dist/{channel-RDGHBFSI.js → channel-2WHBRDTD.js} +66 -81
- package/dist/chunk-4RQBJWQX.js +17 -0
- package/dist/{chunk-YNNK4QN2.js → chunk-FYQGANL6.js} +40 -2
- package/dist/chunk-R3VB7NF5.js +205 -0
- package/dist/{chunk-23L3MKEV.js → chunk-STOEJOJO.js} +18 -4
- package/dist/cli.js +14 -14
- package/dist/{connector-ZP6MEFF4.js → connector-L2HBLZBW.js} +37 -20
- package/dist/create-VBZZNJOG.js +38 -0
- package/dist/{daemon-client-54J3EIZD.js → daemon-client-P44NU3KU.js} +1 -1
- package/dist/daemon-restart-QCLR6ZZV.js +61 -0
- package/dist/daemon.js +1732 -214
- package/dist/delete-BOTVU4YO.js +35 -0
- package/dist/{down-4DGRZRJU.js → down-4LIQG3CE.js} +3 -1
- package/dist/{env-KMNYGVZ2.js → env-CGORIKVF.js} +86 -37
- package/dist/{history-PXJVYLVY.js → history-NI5QP27M.js} +11 -8
- package/dist/import-2BZUWT23.js +21 -0
- package/dist/logs-APWVWGNX.js +77 -0
- package/dist/{package-2S7APQBC.js → package-ERGXEDAF.js} +1 -1
- package/dist/{restart-KVH3TK5N.js → restart-CCYM3MEC.js} +10 -4
- package/dist/{schedule-HCUCBNQI.js → schedule-E4MFGYSA.js} +24 -8
- package/dist/{send-BNC2S5BY.js → send-X6OQGSD6.js} +36 -28
- package/dist/{start-QU73YTJW.js → start-6YRS6FF6.js} +7 -2
- package/dist/{status-Q6ZQJXNI.js → status-SIMKH3ZE.js} +8 -3
- package/dist/{stop-N7U5N6A7.js → stop-UQSNF4CG.js} +7 -2
- package/dist/{up-RZJMSVQS.js → up-MNNPCMFF.js} +1 -1
- package/dist/upgrade-RSE4CZNE.js +55 -0
- package/dist/variant-7IZF6OWO.js +215 -0
- package/package.json +1 -1
- package/dist/chunk-ECPQXRLB.js +0 -264
- package/dist/chunk-NETNFBA5.js +0 -28
- package/dist/chunk-XUA3JUFK.js +0 -121
- package/dist/create-HGJHLABX.js +0 -96
- package/dist/daemon-restart-IMNCBWFV.js +0 -28
- package/dist/delete-45TGQC4N.js +0 -67
- package/dist/import-CNEDF3TD.js +0 -532
- package/dist/logs-TZB3MTLZ.js +0 -37
- package/dist/upgrade-CZF6PN7Y.js +0 -224
- package/dist/variant-RKXPN5DH.js +0 -476
- package/dist/{chunk-6RDCTVQK.js → chunk-4NAGJV3I.js} +0 -0
- package/dist/{chunk-W6TMWYU3.js → chunk-WV4W7BAT.js} +3 -3
- package/dist/{service-56CY4S6Z.js → service-OW35VZ5G.js} +3 -3
- package/dist/{setup-7SPMWF2O.js → setup-ABMZK6LS.js} +3 -3
package/dist/daemon.js
CHANGED
|
@@ -6,75 +6,81 @@ import {
|
|
|
6
6
|
initAgentManager,
|
|
7
7
|
loadJsonMap,
|
|
8
8
|
saveJsonMap
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-WV4W7BAT.js";
|
|
10
10
|
import {
|
|
11
11
|
checkForUpdate,
|
|
12
12
|
checkForUpdateCached,
|
|
13
13
|
getCurrentVersion
|
|
14
14
|
} from "./chunk-RT6Y7AR3.js";
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
agentMessages,
|
|
20
|
-
approveUser,
|
|
21
|
-
conversationParticipants,
|
|
22
|
-
conversations,
|
|
23
|
-
createUser,
|
|
16
|
+
applyIsolation,
|
|
17
|
+
chownAgentDir,
|
|
18
|
+
createAgentUser,
|
|
24
19
|
deleteAgentUser,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
sessions,
|
|
34
|
-
users,
|
|
35
|
-
verifyUser
|
|
36
|
-
} from "./chunk-ECPQXRLB.js";
|
|
20
|
+
ensureVoluteGroup,
|
|
21
|
+
getAgentUserIds,
|
|
22
|
+
isIsolationEnabled
|
|
23
|
+
} from "./chunk-IQXBMFZG.js";
|
|
24
|
+
import {
|
|
25
|
+
exec,
|
|
26
|
+
resolveVoluteBin
|
|
27
|
+
} from "./chunk-5C5JWR2L.js";
|
|
37
28
|
import {
|
|
29
|
+
findOpenClawSession,
|
|
30
|
+
importOpenClawConnectors,
|
|
31
|
+
importPiSession,
|
|
32
|
+
parseNameFromIdentity,
|
|
38
33
|
readVoluteConfig,
|
|
39
34
|
writeVoluteConfig
|
|
40
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-R3VB7NF5.js";
|
|
41
36
|
import {
|
|
42
|
-
|
|
37
|
+
agentEnvPath,
|
|
38
|
+
loadMergedEnv,
|
|
39
|
+
readEnv,
|
|
40
|
+
sharedEnvPath,
|
|
41
|
+
writeEnv
|
|
43
42
|
} from "./chunk-QF22MYDJ.js";
|
|
43
|
+
import {
|
|
44
|
+
CHANNELS,
|
|
45
|
+
getChannelDriver
|
|
46
|
+
} from "./chunk-LIPPXNIE.js";
|
|
47
|
+
import "./chunk-D424ZQGI.js";
|
|
44
48
|
import {
|
|
45
49
|
slugify,
|
|
46
50
|
writeChannelEntry
|
|
47
51
|
} from "./chunk-N6MLQ26B.js";
|
|
48
52
|
import {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
import {
|
|
52
|
-
resolveVoluteBin
|
|
53
|
-
} from "./chunk-5C5JWR2L.js";
|
|
54
|
-
import {
|
|
53
|
+
addAgent,
|
|
54
|
+
addVariant,
|
|
55
55
|
agentDir,
|
|
56
56
|
checkHealth,
|
|
57
57
|
daemonLoopback,
|
|
58
|
+
ensureVoluteHome,
|
|
58
59
|
findAgent,
|
|
59
60
|
findVariant,
|
|
60
61
|
getAllRunningVariants,
|
|
62
|
+
nextPort,
|
|
61
63
|
readRegistry,
|
|
62
64
|
readVariants,
|
|
63
65
|
removeAgent,
|
|
64
66
|
removeAllVariants,
|
|
67
|
+
removeVariant,
|
|
65
68
|
setAgentRunning,
|
|
66
69
|
setVariantRunning,
|
|
67
70
|
stateDir,
|
|
71
|
+
validateAgentName,
|
|
68
72
|
validateBranchName,
|
|
69
73
|
voluteHome
|
|
70
74
|
} from "./chunk-DP2DX4WV.js";
|
|
71
|
-
import
|
|
75
|
+
import {
|
|
76
|
+
__export
|
|
77
|
+
} from "./chunk-K3NQKI34.js";
|
|
72
78
|
|
|
73
79
|
// src/daemon.ts
|
|
74
80
|
import { randomBytes } from "crypto";
|
|
75
|
-
import { mkdirSync as
|
|
76
|
-
import { homedir } from "os";
|
|
77
|
-
import { resolve as
|
|
81
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
82
|
+
import { homedir as homedir2 } from "os";
|
|
83
|
+
import { resolve as resolve16 } from "path";
|
|
78
84
|
import { format } from "util";
|
|
79
85
|
|
|
80
86
|
// src/lib/connector-manager.ts
|
|
@@ -314,19 +320,19 @@ var ConnectorManager = class {
|
|
|
314
320
|
const stopKey = `${agentName}:${type}`;
|
|
315
321
|
this.stopping.add(stopKey);
|
|
316
322
|
agentMap.delete(type);
|
|
317
|
-
await new Promise((
|
|
318
|
-
tracked.child.on("exit", () =>
|
|
323
|
+
await new Promise((resolve17) => {
|
|
324
|
+
tracked.child.on("exit", () => resolve17());
|
|
319
325
|
try {
|
|
320
326
|
tracked.child.kill("SIGTERM");
|
|
321
327
|
} catch {
|
|
322
|
-
|
|
328
|
+
resolve17();
|
|
323
329
|
}
|
|
324
330
|
setTimeout(() => {
|
|
325
331
|
try {
|
|
326
332
|
tracked.child.kill("SIGKILL");
|
|
327
333
|
} catch {
|
|
328
334
|
}
|
|
329
|
-
|
|
335
|
+
resolve17();
|
|
330
336
|
}, 5e3);
|
|
331
337
|
});
|
|
332
338
|
this.stopping.delete(stopKey);
|
|
@@ -734,9 +740,248 @@ function getTokenBudget() {
|
|
|
734
740
|
|
|
735
741
|
// src/web/middleware/auth.ts
|
|
736
742
|
import { timingSafeEqual } from "crypto";
|
|
737
|
-
import { eq, lt } from "drizzle-orm";
|
|
743
|
+
import { eq as eq2, lt } from "drizzle-orm";
|
|
738
744
|
import { getCookie } from "hono/cookie";
|
|
739
745
|
import { createMiddleware } from "hono/factory";
|
|
746
|
+
|
|
747
|
+
// src/lib/auth.ts
|
|
748
|
+
import { compareSync, hashSync } from "bcryptjs";
|
|
749
|
+
import { and, count, eq } from "drizzle-orm";
|
|
750
|
+
|
|
751
|
+
// src/lib/db.ts
|
|
752
|
+
import { chmodSync, existsSync as existsSync4 } from "fs";
|
|
753
|
+
import { dirname as dirname2, resolve as resolve5 } from "path";
|
|
754
|
+
import { fileURLToPath } from "url";
|
|
755
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
756
|
+
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
757
|
+
|
|
758
|
+
// src/lib/schema.ts
|
|
759
|
+
var schema_exports = {};
|
|
760
|
+
__export(schema_exports, {
|
|
761
|
+
agentMessages: () => agentMessages,
|
|
762
|
+
conversationParticipants: () => conversationParticipants,
|
|
763
|
+
conversations: () => conversations,
|
|
764
|
+
messages: () => messages,
|
|
765
|
+
sessions: () => sessions,
|
|
766
|
+
users: () => users
|
|
767
|
+
});
|
|
768
|
+
import { sql } from "drizzle-orm";
|
|
769
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
770
|
+
var users = sqliteTable("users", {
|
|
771
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
772
|
+
username: text("username").unique().notNull(),
|
|
773
|
+
password_hash: text("password_hash").notNull(),
|
|
774
|
+
role: text("role").notNull().default("pending"),
|
|
775
|
+
user_type: text("user_type").notNull().default("human"),
|
|
776
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
777
|
+
});
|
|
778
|
+
var conversations = sqliteTable(
|
|
779
|
+
"conversations",
|
|
780
|
+
{
|
|
781
|
+
id: text("id").primaryKey(),
|
|
782
|
+
agent_name: text("agent_name").notNull(),
|
|
783
|
+
channel: text("channel").notNull(),
|
|
784
|
+
user_id: integer("user_id").references(() => users.id),
|
|
785
|
+
title: text("title"),
|
|
786
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
787
|
+
updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
788
|
+
},
|
|
789
|
+
(table) => [
|
|
790
|
+
index("idx_conversations_agent_name").on(table.agent_name),
|
|
791
|
+
index("idx_conversations_user_id").on(table.user_id),
|
|
792
|
+
index("idx_conversations_updated_at").on(table.updated_at)
|
|
793
|
+
]
|
|
794
|
+
);
|
|
795
|
+
var agentMessages = sqliteTable(
|
|
796
|
+
"agent_messages",
|
|
797
|
+
{
|
|
798
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
799
|
+
agent: text("agent").notNull(),
|
|
800
|
+
channel: text("channel").notNull(),
|
|
801
|
+
sender: text("sender"),
|
|
802
|
+
content: text("content").notNull(),
|
|
803
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
804
|
+
},
|
|
805
|
+
(table) => [
|
|
806
|
+
index("idx_agent_messages_agent").on(table.agent),
|
|
807
|
+
index("idx_agent_messages_channel").on(table.agent, table.channel)
|
|
808
|
+
]
|
|
809
|
+
);
|
|
810
|
+
var conversationParticipants = sqliteTable(
|
|
811
|
+
"conversation_participants",
|
|
812
|
+
{
|
|
813
|
+
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
814
|
+
user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
815
|
+
role: text("role").notNull().default("member"),
|
|
816
|
+
joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
|
|
817
|
+
},
|
|
818
|
+
(table) => [
|
|
819
|
+
uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
|
|
820
|
+
index("idx_cp_user_id").on(table.user_id)
|
|
821
|
+
]
|
|
822
|
+
);
|
|
823
|
+
var sessions = sqliteTable("sessions", {
|
|
824
|
+
id: text("id").primaryKey(),
|
|
825
|
+
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
|
|
826
|
+
createdAt: integer("created_at").notNull()
|
|
827
|
+
});
|
|
828
|
+
var messages = sqliteTable(
|
|
829
|
+
"messages",
|
|
830
|
+
{
|
|
831
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
832
|
+
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
833
|
+
role: text("role").notNull(),
|
|
834
|
+
sender_name: text("sender_name"),
|
|
835
|
+
content: text("content").notNull(),
|
|
836
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
837
|
+
},
|
|
838
|
+
(table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
// src/lib/db.ts
|
|
842
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
843
|
+
var migrationsFolder = existsSync4(resolve5(__dirname, "../drizzle")) ? resolve5(__dirname, "../drizzle") : resolve5(__dirname, "../../drizzle");
|
|
844
|
+
var db = null;
|
|
845
|
+
async function getDb() {
|
|
846
|
+
if (db) return db;
|
|
847
|
+
const dbPath = process.env.VOLUTE_DB_PATH || resolve5(voluteHome(), "volute.db");
|
|
848
|
+
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
849
|
+
await migrate(db, { migrationsFolder });
|
|
850
|
+
try {
|
|
851
|
+
chmodSync(dbPath, 384);
|
|
852
|
+
} catch (err) {
|
|
853
|
+
console.error(
|
|
854
|
+
`[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
|
|
855
|
+
err
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
return db;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/lib/auth.ts
|
|
862
|
+
async function createUser(username, password) {
|
|
863
|
+
const db2 = await getDb();
|
|
864
|
+
const hash = hashSync(password, 10);
|
|
865
|
+
const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
|
|
866
|
+
const role = value === 0 ? "admin" : "pending";
|
|
867
|
+
const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
868
|
+
id: users.id,
|
|
869
|
+
username: users.username,
|
|
870
|
+
role: users.role,
|
|
871
|
+
user_type: users.user_type,
|
|
872
|
+
created_at: users.created_at
|
|
873
|
+
});
|
|
874
|
+
return result;
|
|
875
|
+
}
|
|
876
|
+
async function verifyUser(username, password) {
|
|
877
|
+
const db2 = await getDb();
|
|
878
|
+
const row = await db2.select().from(users).where(eq(users.username, username)).get();
|
|
879
|
+
if (!row) return null;
|
|
880
|
+
if (row.user_type === "agent") return null;
|
|
881
|
+
if (!compareSync(password, row.password_hash)) return null;
|
|
882
|
+
const { password_hash: _, ...user } = row;
|
|
883
|
+
return user;
|
|
884
|
+
}
|
|
885
|
+
async function getUser(id) {
|
|
886
|
+
const db2 = await getDb();
|
|
887
|
+
const row = await db2.select({
|
|
888
|
+
id: users.id,
|
|
889
|
+
username: users.username,
|
|
890
|
+
role: users.role,
|
|
891
|
+
user_type: users.user_type,
|
|
892
|
+
created_at: users.created_at
|
|
893
|
+
}).from(users).where(eq(users.id, id)).get();
|
|
894
|
+
return row ?? null;
|
|
895
|
+
}
|
|
896
|
+
async function getUserByUsername(username) {
|
|
897
|
+
const db2 = await getDb();
|
|
898
|
+
const row = await db2.select({
|
|
899
|
+
id: users.id,
|
|
900
|
+
username: users.username,
|
|
901
|
+
role: users.role,
|
|
902
|
+
user_type: users.user_type,
|
|
903
|
+
created_at: users.created_at
|
|
904
|
+
}).from(users).where(eq(users.username, username)).get();
|
|
905
|
+
return row ?? null;
|
|
906
|
+
}
|
|
907
|
+
async function listUsers() {
|
|
908
|
+
const db2 = await getDb();
|
|
909
|
+
return db2.select({
|
|
910
|
+
id: users.id,
|
|
911
|
+
username: users.username,
|
|
912
|
+
role: users.role,
|
|
913
|
+
user_type: users.user_type,
|
|
914
|
+
created_at: users.created_at
|
|
915
|
+
}).from(users).orderBy(users.created_at).all();
|
|
916
|
+
}
|
|
917
|
+
async function listPendingUsers() {
|
|
918
|
+
const db2 = await getDb();
|
|
919
|
+
return db2.select({
|
|
920
|
+
id: users.id,
|
|
921
|
+
username: users.username,
|
|
922
|
+
role: users.role,
|
|
923
|
+
user_type: users.user_type,
|
|
924
|
+
created_at: users.created_at
|
|
925
|
+
}).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
926
|
+
}
|
|
927
|
+
async function listUsersByType(userType) {
|
|
928
|
+
const db2 = await getDb();
|
|
929
|
+
return db2.select({
|
|
930
|
+
id: users.id,
|
|
931
|
+
username: users.username,
|
|
932
|
+
role: users.role,
|
|
933
|
+
user_type: users.user_type,
|
|
934
|
+
created_at: users.created_at
|
|
935
|
+
}).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
|
|
936
|
+
}
|
|
937
|
+
async function getOrCreateAgentUser(agentName) {
|
|
938
|
+
const db2 = await getDb();
|
|
939
|
+
const existing = await db2.select({
|
|
940
|
+
id: users.id,
|
|
941
|
+
username: users.username,
|
|
942
|
+
role: users.role,
|
|
943
|
+
user_type: users.user_type,
|
|
944
|
+
created_at: users.created_at
|
|
945
|
+
}).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
|
|
946
|
+
if (existing) return existing;
|
|
947
|
+
try {
|
|
948
|
+
const [result] = await db2.insert(users).values({
|
|
949
|
+
username: agentName,
|
|
950
|
+
password_hash: "!agent",
|
|
951
|
+
role: "agent",
|
|
952
|
+
user_type: "agent"
|
|
953
|
+
}).returning({
|
|
954
|
+
id: users.id,
|
|
955
|
+
username: users.username,
|
|
956
|
+
role: users.role,
|
|
957
|
+
user_type: users.user_type,
|
|
958
|
+
created_at: users.created_at
|
|
959
|
+
});
|
|
960
|
+
return result;
|
|
961
|
+
} catch (err) {
|
|
962
|
+
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
963
|
+
const retried = await db2.select({
|
|
964
|
+
id: users.id,
|
|
965
|
+
username: users.username,
|
|
966
|
+
role: users.role,
|
|
967
|
+
user_type: users.user_type,
|
|
968
|
+
created_at: users.created_at
|
|
969
|
+
}).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
|
|
970
|
+
if (retried) return retried;
|
|
971
|
+
}
|
|
972
|
+
throw err;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function deleteAgentUser2(agentName) {
|
|
976
|
+
const db2 = await getDb();
|
|
977
|
+
await db2.delete(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent")));
|
|
978
|
+
}
|
|
979
|
+
async function approveUser(id) {
|
|
980
|
+
const db2 = await getDb();
|
|
981
|
+
await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/web/middleware/auth.ts
|
|
740
985
|
function isValidDaemonToken(token) {
|
|
741
986
|
const expected = process.env.VOLUTE_DAEMON_TOKEN;
|
|
742
987
|
if (!expected || token.length !== expected.length) return false;
|
|
@@ -744,29 +989,29 @@ function isValidDaemonToken(token) {
|
|
|
744
989
|
}
|
|
745
990
|
var SESSION_MAX_AGE = 864e5;
|
|
746
991
|
async function createSession(userId) {
|
|
747
|
-
const
|
|
992
|
+
const db2 = await getDb();
|
|
748
993
|
const sessionId = crypto.randomUUID();
|
|
749
|
-
await
|
|
994
|
+
await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
|
|
750
995
|
return sessionId;
|
|
751
996
|
}
|
|
752
997
|
async function deleteSession(sessionId) {
|
|
753
|
-
const
|
|
754
|
-
await
|
|
998
|
+
const db2 = await getDb();
|
|
999
|
+
await db2.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
755
1000
|
}
|
|
756
1001
|
async function getSessionUserId(sessionId) {
|
|
757
|
-
const
|
|
758
|
-
const row = await
|
|
1002
|
+
const db2 = await getDb();
|
|
1003
|
+
const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
|
|
759
1004
|
if (!row) return void 0;
|
|
760
1005
|
if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
|
|
761
|
-
await
|
|
1006
|
+
await db2.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
762
1007
|
return void 0;
|
|
763
1008
|
}
|
|
764
1009
|
return row.userId;
|
|
765
1010
|
}
|
|
766
1011
|
async function cleanExpiredSessions() {
|
|
767
|
-
const
|
|
1012
|
+
const db2 = await getDb();
|
|
768
1013
|
const cutoff = Date.now() - SESSION_MAX_AGE;
|
|
769
|
-
await
|
|
1014
|
+
await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
|
|
770
1015
|
}
|
|
771
1016
|
var requireAdmin = createMiddleware(async (c, next) => {
|
|
772
1017
|
const user = c.get("user");
|
|
@@ -797,9 +1042,9 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
797
1042
|
});
|
|
798
1043
|
|
|
799
1044
|
// src/web/server.ts
|
|
800
|
-
import { existsSync as
|
|
1045
|
+
import { existsSync as existsSync10 } from "fs";
|
|
801
1046
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
802
|
-
import { dirname as
|
|
1047
|
+
import { dirname as dirname4, extname, resolve as resolve15 } from "path";
|
|
803
1048
|
import { serve } from "@hono/node-server";
|
|
804
1049
|
|
|
805
1050
|
// src/lib/log-buffer.ts
|
|
@@ -847,19 +1092,382 @@ var log = {
|
|
|
847
1092
|
var logger_default = log;
|
|
848
1093
|
|
|
849
1094
|
// src/web/app.ts
|
|
850
|
-
import { Hono as
|
|
1095
|
+
import { Hono as Hono16 } from "hono";
|
|
851
1096
|
import { bodyLimit } from "hono/body-limit";
|
|
852
1097
|
import { csrf } from "hono/csrf";
|
|
853
1098
|
import { HTTPException } from "hono/http-exception";
|
|
854
1099
|
|
|
855
1100
|
// src/web/routes/agents.ts
|
|
856
|
-
import {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1101
|
+
import {
|
|
1102
|
+
cpSync as cpSync2,
|
|
1103
|
+
existsSync as existsSync6,
|
|
1104
|
+
mkdirSync as mkdirSync5,
|
|
1105
|
+
readdirSync as readdirSync4,
|
|
1106
|
+
readFileSync as readFileSync6,
|
|
1107
|
+
rmSync as rmSync2,
|
|
1108
|
+
writeFileSync as writeFileSync5
|
|
1109
|
+
} from "fs";
|
|
1110
|
+
import { resolve as resolve9 } from "path";
|
|
1111
|
+
import { and as and2, desc, eq as eq3 } from "drizzle-orm";
|
|
861
1112
|
import { Hono } from "hono";
|
|
862
1113
|
|
|
1114
|
+
// src/lib/consolidate.ts
|
|
1115
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1116
|
+
import { resolve as resolve6 } from "path";
|
|
1117
|
+
async function consolidateMemory(agentDir2) {
|
|
1118
|
+
const soulPath = resolve6(agentDir2, "home/SOUL.md");
|
|
1119
|
+
const memoryPath = resolve6(agentDir2, "home/MEMORY.md");
|
|
1120
|
+
const memoryDir = resolve6(agentDir2, "home/memory");
|
|
1121
|
+
const soul = readFileSync3(soulPath, "utf-8");
|
|
1122
|
+
const logs = [];
|
|
1123
|
+
try {
|
|
1124
|
+
const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
1125
|
+
for (const filename of files) {
|
|
1126
|
+
const date = filename.replace(".md", "");
|
|
1127
|
+
const content2 = readFileSync3(resolve6(memoryDir, filename), "utf-8").trim();
|
|
1128
|
+
if (content2) {
|
|
1129
|
+
logs.push(`### ${date}
|
|
1130
|
+
|
|
1131
|
+
${content2}`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
if (logs.length === 0) {
|
|
1137
|
+
console.log("No daily logs found.");
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1141
|
+
if (!apiKey) {
|
|
1142
|
+
console.error("ANTHROPIC_API_KEY not set, skipping memory consolidation.");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
console.log("Consolidating memory from daily logs...");
|
|
1146
|
+
const userMessage = [
|
|
1147
|
+
"You have daily logs from a previous environment but no long-term memory file yet.",
|
|
1148
|
+
"Please review the daily logs below and produce consolidated MEMORY.md content.",
|
|
1149
|
+
"Keep it concise and organized by topic. Output ONLY the markdown content for MEMORY.md, nothing else.",
|
|
1150
|
+
"",
|
|
1151
|
+
"## Daily logs",
|
|
1152
|
+
"",
|
|
1153
|
+
logs.join("\n\n")
|
|
1154
|
+
].join("\n");
|
|
1155
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1156
|
+
method: "POST",
|
|
1157
|
+
headers: {
|
|
1158
|
+
"Content-Type": "application/json",
|
|
1159
|
+
"x-api-key": apiKey,
|
|
1160
|
+
"anthropic-version": "2023-06-01"
|
|
1161
|
+
},
|
|
1162
|
+
body: JSON.stringify({
|
|
1163
|
+
model: "claude-sonnet-4-20250514",
|
|
1164
|
+
max_tokens: 4096,
|
|
1165
|
+
system: soul,
|
|
1166
|
+
messages: [{ role: "user", content: userMessage }]
|
|
1167
|
+
})
|
|
1168
|
+
});
|
|
1169
|
+
if (!res.ok) {
|
|
1170
|
+
const body = await res.text();
|
|
1171
|
+
console.error(`Anthropic API error (${res.status}): ${body}`);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const data = await res.json();
|
|
1175
|
+
const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
|
|
1176
|
+
if (content) {
|
|
1177
|
+
writeFileSync2(memoryPath, `${content}
|
|
1178
|
+
`);
|
|
1179
|
+
console.log("MEMORY.md created successfully.");
|
|
1180
|
+
} else {
|
|
1181
|
+
console.warn("Warning: No content produced.");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/lib/convert-session.ts
|
|
1186
|
+
import { randomUUID } from "crypto";
|
|
1187
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1188
|
+
import { homedir } from "os";
|
|
1189
|
+
import { resolve as resolve7 } from "path";
|
|
1190
|
+
function convertSession(opts) {
|
|
1191
|
+
const lines = readFileSync4(opts.sessionPath, "utf-8").trim().split("\n");
|
|
1192
|
+
const sessionId = randomUUID();
|
|
1193
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1194
|
+
const messages2 = [];
|
|
1195
|
+
for (const line of lines) {
|
|
1196
|
+
const event = JSON.parse(line);
|
|
1197
|
+
if (event.type === "message" && event.message) {
|
|
1198
|
+
messages2.push(event);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const sdkEvents = [];
|
|
1202
|
+
let lastSdkUuid = null;
|
|
1203
|
+
for (let i = 0; i < messages2.length; i++) {
|
|
1204
|
+
const event = messages2[i];
|
|
1205
|
+
const msg = event.message;
|
|
1206
|
+
if (msg.role === "user") {
|
|
1207
|
+
const uuid = randomUUID();
|
|
1208
|
+
idMap.set(event.id, uuid);
|
|
1209
|
+
const parentUuid = event.parentId ? idMap.get(event.parentId) ?? null : null;
|
|
1210
|
+
const sdkEvent = {
|
|
1211
|
+
uuid,
|
|
1212
|
+
parentUuid,
|
|
1213
|
+
sessionId,
|
|
1214
|
+
timestamp: event.timestamp,
|
|
1215
|
+
cwd: opts.projectDir,
|
|
1216
|
+
version: "0.1.0",
|
|
1217
|
+
gitBranch: "main",
|
|
1218
|
+
isSidechain: false,
|
|
1219
|
+
userType: "external",
|
|
1220
|
+
type: "user",
|
|
1221
|
+
message: {
|
|
1222
|
+
role: "user",
|
|
1223
|
+
content: msg.content
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
sdkEvents.push(JSON.stringify(sdkEvent));
|
|
1227
|
+
lastSdkUuid = uuid;
|
|
1228
|
+
} else if (msg.role === "assistant") {
|
|
1229
|
+
const content = convertAssistantContent(msg.content);
|
|
1230
|
+
if (content.length === 0) continue;
|
|
1231
|
+
const uuid = randomUUID();
|
|
1232
|
+
idMap.set(event.id, uuid);
|
|
1233
|
+
const parentUuid = event.parentId ? idMap.get(event.parentId) ?? null : null;
|
|
1234
|
+
const stopReason = mapStopReason(msg.stopReason);
|
|
1235
|
+
const sdkEvent = {
|
|
1236
|
+
uuid,
|
|
1237
|
+
parentUuid,
|
|
1238
|
+
sessionId,
|
|
1239
|
+
timestamp: event.timestamp,
|
|
1240
|
+
cwd: opts.projectDir,
|
|
1241
|
+
version: "0.1.0",
|
|
1242
|
+
gitBranch: "main",
|
|
1243
|
+
isSidechain: false,
|
|
1244
|
+
userType: "external",
|
|
1245
|
+
type: "assistant",
|
|
1246
|
+
requestId: `req_imported_${randomUUID()}`,
|
|
1247
|
+
message: {
|
|
1248
|
+
role: "assistant",
|
|
1249
|
+
content,
|
|
1250
|
+
type: "message",
|
|
1251
|
+
id: `msg_imported_${randomUUID()}`,
|
|
1252
|
+
model: mapModel(msg.model),
|
|
1253
|
+
stop_reason: stopReason,
|
|
1254
|
+
stop_sequence: null,
|
|
1255
|
+
usage: mapUsage(msg.usage)
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
sdkEvents.push(JSON.stringify(sdkEvent));
|
|
1259
|
+
lastSdkUuid = uuid;
|
|
1260
|
+
} else if (msg.role === "toolResult") {
|
|
1261
|
+
const toolResults = [];
|
|
1262
|
+
let lastToolResultId = event.id;
|
|
1263
|
+
let lastTimestamp = event.timestamp;
|
|
1264
|
+
let j = i;
|
|
1265
|
+
while (j < messages2.length && messages2[j].message.role === "toolResult") {
|
|
1266
|
+
const tr = messages2[j];
|
|
1267
|
+
const trMsg = tr.message;
|
|
1268
|
+
lastToolResultId = tr.id;
|
|
1269
|
+
lastTimestamp = tr.timestamp;
|
|
1270
|
+
toolResults.push({
|
|
1271
|
+
type: "tool_result",
|
|
1272
|
+
tool_use_id: trMsg.toolCallId,
|
|
1273
|
+
content: trMsg.content,
|
|
1274
|
+
...trMsg.isError ? { is_error: true } : {}
|
|
1275
|
+
});
|
|
1276
|
+
j++;
|
|
1277
|
+
}
|
|
1278
|
+
i = j - 1;
|
|
1279
|
+
const uuid = randomUUID();
|
|
1280
|
+
idMap.set(lastToolResultId, uuid);
|
|
1281
|
+
const parentUuid = event.parentId ? idMap.get(event.parentId) ?? null : lastSdkUuid;
|
|
1282
|
+
const sdkEvent = {
|
|
1283
|
+
uuid,
|
|
1284
|
+
parentUuid,
|
|
1285
|
+
sessionId,
|
|
1286
|
+
timestamp: lastTimestamp,
|
|
1287
|
+
cwd: opts.projectDir,
|
|
1288
|
+
version: "0.1.0",
|
|
1289
|
+
gitBranch: "main",
|
|
1290
|
+
isSidechain: false,
|
|
1291
|
+
userType: "external",
|
|
1292
|
+
type: "user",
|
|
1293
|
+
sourceToolAssistantUUID: lastSdkUuid ?? void 0,
|
|
1294
|
+
toolUseResult: "imported",
|
|
1295
|
+
message: {
|
|
1296
|
+
role: "user",
|
|
1297
|
+
content: toolResults
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
sdkEvents.push(JSON.stringify(sdkEvent));
|
|
1301
|
+
lastSdkUuid = uuid;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
const projectId = opts.projectDir.replace(/\//g, "-");
|
|
1305
|
+
const sdkDir = resolve7(homedir(), ".claude", "projects", projectId);
|
|
1306
|
+
mkdirSync3(sdkDir, { recursive: true });
|
|
1307
|
+
const sdkPath = resolve7(sdkDir, `${sessionId}.jsonl`);
|
|
1308
|
+
writeFileSync3(sdkPath, `${sdkEvents.join("\n")}
|
|
1309
|
+
`);
|
|
1310
|
+
console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
|
|
1311
|
+
return sessionId;
|
|
1312
|
+
}
|
|
1313
|
+
var MODEL_MAP = {
|
|
1314
|
+
"claude-opus-4-5": "claude-opus-4-5-20251101",
|
|
1315
|
+
"claude-sonnet-4": "claude-sonnet-4-20250514"
|
|
1316
|
+
};
|
|
1317
|
+
function mapModel(model) {
|
|
1318
|
+
if (!model) return "claude-opus-4-5-20251101";
|
|
1319
|
+
return MODEL_MAP[model] ?? model;
|
|
1320
|
+
}
|
|
1321
|
+
function mapStopReason(stopReason) {
|
|
1322
|
+
if (!stopReason) return "end_turn";
|
|
1323
|
+
const map = {
|
|
1324
|
+
toolUse: "tool_use",
|
|
1325
|
+
endTurn: "end_turn",
|
|
1326
|
+
stop: "end_turn",
|
|
1327
|
+
maxTokens: "max_tokens"
|
|
1328
|
+
};
|
|
1329
|
+
return map[stopReason] ?? stopReason;
|
|
1330
|
+
}
|
|
1331
|
+
function mapUsage(usage) {
|
|
1332
|
+
if (!usage) return { input_tokens: 0, output_tokens: 0 };
|
|
1333
|
+
return {
|
|
1334
|
+
input_tokens: usage.input ?? usage.input_tokens ?? 0,
|
|
1335
|
+
output_tokens: usage.output ?? usage.output_tokens ?? 0,
|
|
1336
|
+
cache_read_input_tokens: usage.cacheRead ?? usage.cache_read_input_tokens ?? 0,
|
|
1337
|
+
cache_creation_input_tokens: usage.cacheWrite ?? usage.cache_creation_input_tokens ?? 0
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
function convertAssistantContent(content) {
|
|
1341
|
+
const result = [];
|
|
1342
|
+
for (const block of content) {
|
|
1343
|
+
if (block.type === "thinking") {
|
|
1344
|
+
} else if (block.type === "toolCall") {
|
|
1345
|
+
result.push({
|
|
1346
|
+
type: "tool_use",
|
|
1347
|
+
id: block.id,
|
|
1348
|
+
name: block.name,
|
|
1349
|
+
input: block.arguments ?? block.input ?? {},
|
|
1350
|
+
caller: { type: "direct" }
|
|
1351
|
+
});
|
|
1352
|
+
} else {
|
|
1353
|
+
result.push(block);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return result;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/lib/template.ts
|
|
1360
|
+
import {
|
|
1361
|
+
cpSync,
|
|
1362
|
+
existsSync as existsSync5,
|
|
1363
|
+
mkdirSync as mkdirSync4,
|
|
1364
|
+
readdirSync as readdirSync3,
|
|
1365
|
+
readFileSync as readFileSync5,
|
|
1366
|
+
renameSync,
|
|
1367
|
+
rmSync,
|
|
1368
|
+
statSync,
|
|
1369
|
+
writeFileSync as writeFileSync4
|
|
1370
|
+
} from "fs";
|
|
1371
|
+
import { tmpdir } from "os";
|
|
1372
|
+
import { dirname as dirname3, join, relative, resolve as resolve8 } from "path";
|
|
1373
|
+
function findTemplatesRoot() {
|
|
1374
|
+
let dir = dirname3(new URL(import.meta.url).pathname);
|
|
1375
|
+
for (let i = 0; i < 5; i++) {
|
|
1376
|
+
const candidate = resolve8(dir, "templates");
|
|
1377
|
+
if (existsSync5(resolve8(candidate, "_base"))) return candidate;
|
|
1378
|
+
dir = dirname3(dir);
|
|
1379
|
+
}
|
|
1380
|
+
console.error(
|
|
1381
|
+
"Templates directory not found. Searched up from:",
|
|
1382
|
+
dirname3(new URL(import.meta.url).pathname)
|
|
1383
|
+
);
|
|
1384
|
+
process.exit(1);
|
|
1385
|
+
}
|
|
1386
|
+
function composeTemplate(templatesRoot, templateName) {
|
|
1387
|
+
const baseDir = resolve8(templatesRoot, "_base");
|
|
1388
|
+
const templateDir = resolve8(templatesRoot, templateName);
|
|
1389
|
+
if (!existsSync5(baseDir)) {
|
|
1390
|
+
console.error("Base template not found:", baseDir);
|
|
1391
|
+
process.exit(1);
|
|
1392
|
+
}
|
|
1393
|
+
if (!existsSync5(templateDir)) {
|
|
1394
|
+
console.error(`Template not found: ${templateName}`);
|
|
1395
|
+
process.exit(1);
|
|
1396
|
+
}
|
|
1397
|
+
const composedDir = resolve8(tmpdir(), `volute-template-${Date.now()}`);
|
|
1398
|
+
mkdirSync4(composedDir, { recursive: true });
|
|
1399
|
+
cpSync(baseDir, composedDir, { recursive: true });
|
|
1400
|
+
for (const file of listFiles(templateDir)) {
|
|
1401
|
+
const src = resolve8(templateDir, file);
|
|
1402
|
+
const dest = resolve8(composedDir, file);
|
|
1403
|
+
mkdirSync4(dirname3(dest), { recursive: true });
|
|
1404
|
+
cpSync(src, dest);
|
|
1405
|
+
}
|
|
1406
|
+
const manifestPath = resolve8(composedDir, "volute-template.json");
|
|
1407
|
+
if (!existsSync5(manifestPath)) {
|
|
1408
|
+
rmSync(composedDir, { recursive: true, force: true });
|
|
1409
|
+
console.error(`Template manifest not found: ${templateName}/volute-template.json`);
|
|
1410
|
+
process.exit(1);
|
|
1411
|
+
}
|
|
1412
|
+
const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
1413
|
+
const skillsSrc = resolve8(composedDir, "_skills");
|
|
1414
|
+
if (existsSync5(skillsSrc)) {
|
|
1415
|
+
const skillsDest = resolve8(composedDir, manifest.skillsDir);
|
|
1416
|
+
mkdirSync4(skillsDest, { recursive: true });
|
|
1417
|
+
cpSync(skillsSrc, skillsDest, { recursive: true });
|
|
1418
|
+
rmSync(skillsSrc, { recursive: true, force: true });
|
|
1419
|
+
}
|
|
1420
|
+
rmSync(manifestPath);
|
|
1421
|
+
return { composedDir, manifest };
|
|
1422
|
+
}
|
|
1423
|
+
function copyTemplateToDir(composedDir, destDir, agentName, manifest) {
|
|
1424
|
+
cpSync(composedDir, destDir, { recursive: true });
|
|
1425
|
+
for (const [from, to] of Object.entries(manifest.rename)) {
|
|
1426
|
+
const fromPath = resolve8(destDir, from);
|
|
1427
|
+
if (existsSync5(fromPath)) {
|
|
1428
|
+
renameSync(fromPath, resolve8(destDir, to));
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
for (const file of manifest.substitute) {
|
|
1432
|
+
const path = resolve8(destDir, file);
|
|
1433
|
+
if (existsSync5(path)) {
|
|
1434
|
+
const content = readFileSync5(path, "utf-8");
|
|
1435
|
+
writeFileSync4(path, content.replaceAll("{{name}}", agentName));
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function applyInitFiles(destDir) {
|
|
1440
|
+
const initDir = resolve8(destDir, ".init");
|
|
1441
|
+
if (!existsSync5(initDir)) return;
|
|
1442
|
+
const homeDir = resolve8(destDir, "home");
|
|
1443
|
+
for (const file of listFiles(initDir)) {
|
|
1444
|
+
const src = resolve8(initDir, file);
|
|
1445
|
+
const dest = resolve8(homeDir, file);
|
|
1446
|
+
const parent = dirname3(dest);
|
|
1447
|
+
if (!existsSync5(parent)) {
|
|
1448
|
+
mkdirSync4(parent, { recursive: true });
|
|
1449
|
+
}
|
|
1450
|
+
cpSync(src, dest);
|
|
1451
|
+
}
|
|
1452
|
+
rmSync(initDir, { recursive: true, force: true });
|
|
1453
|
+
}
|
|
1454
|
+
function listFiles(dir) {
|
|
1455
|
+
const results = [];
|
|
1456
|
+
function walk(current) {
|
|
1457
|
+
for (const entry of readdirSync3(current)) {
|
|
1458
|
+
const full = join(current, entry);
|
|
1459
|
+
if (statSync(full).isDirectory()) {
|
|
1460
|
+
if (entry === ".git") continue;
|
|
1461
|
+
walk(full);
|
|
1462
|
+
} else {
|
|
1463
|
+
results.push(relative(dir, full));
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
walk(dir);
|
|
1468
|
+
return results;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
863
1471
|
// src/lib/typing.ts
|
|
864
1472
|
var DEFAULT_TTL_MS = 1e4;
|
|
865
1473
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -928,7 +1536,6 @@ function getTypingMap() {
|
|
|
928
1536
|
}
|
|
929
1537
|
|
|
930
1538
|
// src/web/routes/agents.ts
|
|
931
|
-
var execFileAsync = promisify(execFile);
|
|
932
1539
|
async function startAgentFull(name, baseName, variantName) {
|
|
933
1540
|
await getAgentManager().startAgent(name);
|
|
934
1541
|
if (variantName) return;
|
|
@@ -954,7 +1561,7 @@ function extractTextContent(content) {
|
|
|
954
1561
|
}
|
|
955
1562
|
function getDaemonPort() {
|
|
956
1563
|
try {
|
|
957
|
-
const data = JSON.parse(
|
|
1564
|
+
const data = JSON.parse(readFileSync6(resolve9(voluteHome(), "daemon.json"), "utf-8"));
|
|
958
1565
|
return data.port;
|
|
959
1566
|
} catch (err) {
|
|
960
1567
|
if (err?.code !== "ENOENT") {
|
|
@@ -993,7 +1600,247 @@ async function getAgentStatus(name, port) {
|
|
|
993
1600
|
}
|
|
994
1601
|
return { status, channels };
|
|
995
1602
|
}
|
|
996
|
-
var
|
|
1603
|
+
var TEMPLATE_BRANCH = "volute/template";
|
|
1604
|
+
async function initTemplateBranch(projectRoot, composedDir, manifest, ids, env) {
|
|
1605
|
+
const templateFiles = listFiles(composedDir).filter((f) => !f.startsWith(".init/") && !f.startsWith(".init\\")).map((f) => manifest.rename[f] ?? f);
|
|
1606
|
+
const opts = { cwd: projectRoot, uid: ids?.uid, gid: ids?.gid, env };
|
|
1607
|
+
await exec("git", ["checkout", "--orphan", TEMPLATE_BRANCH], opts);
|
|
1608
|
+
await exec("git", ["add", "--", ...templateFiles], opts);
|
|
1609
|
+
await exec("git", ["commit", "-m", "template update"], opts);
|
|
1610
|
+
await exec("git", ["checkout", "-b", "main"], opts);
|
|
1611
|
+
await exec("git", ["add", "-A"], opts);
|
|
1612
|
+
await exec("git", ["commit", "-m", "initial commit"], opts);
|
|
1613
|
+
}
|
|
1614
|
+
async function updateTemplateBranch(projectRoot, template, agentName) {
|
|
1615
|
+
const tempWorktree = resolve9(projectRoot, ".variants", "_template_update");
|
|
1616
|
+
let branchExists = false;
|
|
1617
|
+
try {
|
|
1618
|
+
await exec("git", ["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
|
|
1619
|
+
branchExists = true;
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
try {
|
|
1623
|
+
await exec("git", ["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
1624
|
+
} catch {
|
|
1625
|
+
}
|
|
1626
|
+
if (existsSync6(tempWorktree)) {
|
|
1627
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
1628
|
+
}
|
|
1629
|
+
const templatesRoot = findTemplatesRoot();
|
|
1630
|
+
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
1631
|
+
try {
|
|
1632
|
+
if (branchExists) {
|
|
1633
|
+
await exec("git", ["worktree", "add", tempWorktree, TEMPLATE_BRANCH], {
|
|
1634
|
+
cwd: projectRoot
|
|
1635
|
+
});
|
|
1636
|
+
} else {
|
|
1637
|
+
await exec("git", ["worktree", "add", "--detach", tempWorktree], { cwd: projectRoot });
|
|
1638
|
+
await exec("git", ["checkout", "--orphan", TEMPLATE_BRANCH], { cwd: tempWorktree });
|
|
1639
|
+
await exec("git", ["rm", "-rf", "--cached", "."], { cwd: tempWorktree });
|
|
1640
|
+
await exec("git", ["clean", "-fd"], { cwd: tempWorktree });
|
|
1641
|
+
}
|
|
1642
|
+
if (branchExists) {
|
|
1643
|
+
await exec("git", ["rm", "-rf", "."], { cwd: tempWorktree }).catch(() => {
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
copyTemplateToDir(composedDir, tempWorktree, agentName, manifest);
|
|
1647
|
+
const initDir = resolve9(tempWorktree, ".init");
|
|
1648
|
+
if (existsSync6(initDir)) {
|
|
1649
|
+
rmSync2(initDir, { recursive: true, force: true });
|
|
1650
|
+
}
|
|
1651
|
+
await exec("git", ["add", "-A"], { cwd: tempWorktree });
|
|
1652
|
+
try {
|
|
1653
|
+
await exec("git", ["diff", "--cached", "--quiet"], { cwd: tempWorktree });
|
|
1654
|
+
} catch {
|
|
1655
|
+
await exec("git", ["commit", "-m", "template update"], { cwd: tempWorktree });
|
|
1656
|
+
}
|
|
1657
|
+
} finally {
|
|
1658
|
+
try {
|
|
1659
|
+
await exec("git", ["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
if (existsSync6(tempWorktree)) {
|
|
1663
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
1664
|
+
}
|
|
1665
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
async function mergeTemplateBranch(worktreeDir) {
|
|
1669
|
+
try {
|
|
1670
|
+
await exec(
|
|
1671
|
+
"git",
|
|
1672
|
+
["merge", TEMPLATE_BRANCH, "--allow-unrelated-histories", "-m", "merge template update"],
|
|
1673
|
+
{ cwd: worktreeDir }
|
|
1674
|
+
);
|
|
1675
|
+
return false;
|
|
1676
|
+
} catch (e) {
|
|
1677
|
+
try {
|
|
1678
|
+
const status = await exec("git", ["status", "--porcelain"], { cwd: worktreeDir });
|
|
1679
|
+
const hasConflictMarkers = status.split("\n").some((line) => line.startsWith("UU") || line.startsWith("AA"));
|
|
1680
|
+
if (hasConflictMarkers) return true;
|
|
1681
|
+
} catch {
|
|
1682
|
+
}
|
|
1683
|
+
throw e;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
var app = new Hono().post("/", requireAdmin, async (c) => {
|
|
1687
|
+
let body;
|
|
1688
|
+
try {
|
|
1689
|
+
body = await c.req.json();
|
|
1690
|
+
} catch {
|
|
1691
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1692
|
+
}
|
|
1693
|
+
const { name, template = "agent-sdk" } = body;
|
|
1694
|
+
const nameErr = validateAgentName(name);
|
|
1695
|
+
if (nameErr) return c.json({ error: nameErr }, 400);
|
|
1696
|
+
if (findAgent(name)) return c.json({ error: `Agent already exists: ${name}` }, 409);
|
|
1697
|
+
ensureVoluteHome();
|
|
1698
|
+
const dest = agentDir(name);
|
|
1699
|
+
if (existsSync6(dest)) return c.json({ error: "Agent directory already exists" }, 409);
|
|
1700
|
+
const templatesRoot = findTemplatesRoot();
|
|
1701
|
+
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
1702
|
+
try {
|
|
1703
|
+
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
1704
|
+
applyInitFiles(dest);
|
|
1705
|
+
const port = nextPort();
|
|
1706
|
+
addAgent(name, port);
|
|
1707
|
+
ensureVoluteGroup();
|
|
1708
|
+
createAgentUser(name);
|
|
1709
|
+
chownAgentDir(dest, name);
|
|
1710
|
+
const ids = isIsolationEnabled() ? await getAgentUserIds(name) : void 0;
|
|
1711
|
+
const env = ids ? { ...process.env, HOME: dest } : void 0;
|
|
1712
|
+
await exec("npm", ["install"], { cwd: dest, uid: ids?.uid, gid: ids?.gid, env });
|
|
1713
|
+
let gitWarning;
|
|
1714
|
+
try {
|
|
1715
|
+
await exec("git", ["init"], { cwd: dest, uid: ids?.uid, gid: ids?.gid, env });
|
|
1716
|
+
await initTemplateBranch(dest, composedDir, manifest, ids, env);
|
|
1717
|
+
} catch {
|
|
1718
|
+
rmSync2(resolve9(dest, ".git"), { recursive: true, force: true });
|
|
1719
|
+
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
1720
|
+
}
|
|
1721
|
+
return c.json({
|
|
1722
|
+
ok: true,
|
|
1723
|
+
name,
|
|
1724
|
+
port,
|
|
1725
|
+
message: `Created agent: ${name} (port ${port})`,
|
|
1726
|
+
...gitWarning && { warning: gitWarning }
|
|
1727
|
+
});
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
if (existsSync6(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
1730
|
+
try {
|
|
1731
|
+
removeAgent(name);
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1734
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to create agent" }, 500);
|
|
1735
|
+
} finally {
|
|
1736
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
1737
|
+
}
|
|
1738
|
+
}).post("/import", requireAdmin, async (c) => {
|
|
1739
|
+
let body;
|
|
1740
|
+
try {
|
|
1741
|
+
body = await c.req.json();
|
|
1742
|
+
} catch {
|
|
1743
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
1744
|
+
}
|
|
1745
|
+
const wsDir = body.workspacePath;
|
|
1746
|
+
if (!wsDir || !existsSync6(resolve9(wsDir, "SOUL.md")) || !existsSync6(resolve9(wsDir, "IDENTITY.md"))) {
|
|
1747
|
+
return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
|
|
1748
|
+
}
|
|
1749
|
+
const soul = readFileSync6(resolve9(wsDir, "SOUL.md"), "utf-8");
|
|
1750
|
+
const identity = readFileSync6(resolve9(wsDir, "IDENTITY.md"), "utf-8");
|
|
1751
|
+
const userPath = resolve9(wsDir, "USER.md");
|
|
1752
|
+
const user = existsSync6(userPath) ? readFileSync6(userPath, "utf-8") : "";
|
|
1753
|
+
const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-agent";
|
|
1754
|
+
const template = body.template ?? "agent-sdk";
|
|
1755
|
+
const nameErr = validateAgentName(name);
|
|
1756
|
+
if (nameErr) return c.json({ error: nameErr }, 400);
|
|
1757
|
+
if (findAgent(name)) return c.json({ error: `Agent already exists: ${name}` }, 409);
|
|
1758
|
+
const mergedSoul = `${soul.trimEnd()}
|
|
1759
|
+
|
|
1760
|
+
---
|
|
1761
|
+
|
|
1762
|
+
${identity.trimEnd()}
|
|
1763
|
+
`;
|
|
1764
|
+
const mergedMemoryExtra = user ? `
|
|
1765
|
+
|
|
1766
|
+
---
|
|
1767
|
+
|
|
1768
|
+
${user.trimEnd()}
|
|
1769
|
+
` : "";
|
|
1770
|
+
ensureVoluteHome();
|
|
1771
|
+
const dest = agentDir(name);
|
|
1772
|
+
if (existsSync6(dest)) return c.json({ error: "Agent directory already exists" }, 409);
|
|
1773
|
+
const templatesRoot = findTemplatesRoot();
|
|
1774
|
+
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
1775
|
+
try {
|
|
1776
|
+
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
1777
|
+
applyInitFiles(dest);
|
|
1778
|
+
writeFileSync5(resolve9(dest, "home/SOUL.md"), mergedSoul);
|
|
1779
|
+
const wsMemoryPath = resolve9(wsDir, "MEMORY.md");
|
|
1780
|
+
const hasMemory = existsSync6(wsMemoryPath);
|
|
1781
|
+
if (hasMemory) {
|
|
1782
|
+
const existingMemory = readFileSync6(wsMemoryPath, "utf-8");
|
|
1783
|
+
writeFileSync5(
|
|
1784
|
+
resolve9(dest, "home/MEMORY.md"),
|
|
1785
|
+
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
1786
|
+
);
|
|
1787
|
+
} else if (user) {
|
|
1788
|
+
writeFileSync5(resolve9(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
1789
|
+
`);
|
|
1790
|
+
}
|
|
1791
|
+
const wsMemoryDir = resolve9(wsDir, "memory");
|
|
1792
|
+
let dailyLogCount = 0;
|
|
1793
|
+
if (existsSync6(wsMemoryDir)) {
|
|
1794
|
+
const destMemoryDir = resolve9(dest, "home/memory");
|
|
1795
|
+
const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
1796
|
+
for (const file of files) {
|
|
1797
|
+
cpSync2(resolve9(wsMemoryDir, file), resolve9(destMemoryDir, file));
|
|
1798
|
+
}
|
|
1799
|
+
dailyLogCount = files.length;
|
|
1800
|
+
}
|
|
1801
|
+
const port = nextPort();
|
|
1802
|
+
addAgent(name, port);
|
|
1803
|
+
ensureVoluteGroup();
|
|
1804
|
+
createAgentUser(name);
|
|
1805
|
+
chownAgentDir(dest, name);
|
|
1806
|
+
const ids = isIsolationEnabled() ? await getAgentUserIds(name) : void 0;
|
|
1807
|
+
const env = ids ? { ...process.env, HOME: dest } : void 0;
|
|
1808
|
+
await exec("npm", ["install"], { cwd: dest, uid: ids?.uid, gid: ids?.gid, env });
|
|
1809
|
+
if (!hasMemory && dailyLogCount > 0) {
|
|
1810
|
+
await consolidateMemory(dest);
|
|
1811
|
+
}
|
|
1812
|
+
await exec("git", ["init"], { cwd: dest, uid: ids?.uid, gid: ids?.gid, env });
|
|
1813
|
+
await exec("git", ["add", "-A"], { cwd: dest, uid: ids?.uid, gid: ids?.gid, env });
|
|
1814
|
+
await exec("git", ["commit", "-m", "import from OpenClaw"], {
|
|
1815
|
+
cwd: dest,
|
|
1816
|
+
uid: ids?.uid,
|
|
1817
|
+
gid: ids?.gid,
|
|
1818
|
+
env
|
|
1819
|
+
});
|
|
1820
|
+
const sessionFile = body.sessionPath ? resolve9(body.sessionPath) : findOpenClawSession(wsDir);
|
|
1821
|
+
if (sessionFile && existsSync6(sessionFile)) {
|
|
1822
|
+
if (template === "pi") {
|
|
1823
|
+
importPiSession(sessionFile, dest);
|
|
1824
|
+
} else if (template === "agent-sdk") {
|
|
1825
|
+
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
1826
|
+
const voluteDir = resolve9(dest, ".volute");
|
|
1827
|
+
mkdirSync5(voluteDir, { recursive: true });
|
|
1828
|
+
writeFileSync5(resolve9(voluteDir, "session.json"), JSON.stringify({ sessionId }));
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
importOpenClawConnectors(name, dest);
|
|
1832
|
+
return c.json({ ok: true, name, port, message: `Imported agent: ${name} (port ${port})` });
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
if (existsSync6(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
1835
|
+
try {
|
|
1836
|
+
removeAgent(name);
|
|
1837
|
+
} catch {
|
|
1838
|
+
}
|
|
1839
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to import agent" }, 500);
|
|
1840
|
+
} finally {
|
|
1841
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
1842
|
+
}
|
|
1843
|
+
}).get("/", async (c) => {
|
|
997
1844
|
const entries = readRegistry();
|
|
998
1845
|
const agents = await Promise.all(
|
|
999
1846
|
entries.map(async (entry) => {
|
|
@@ -1006,7 +1853,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1006
1853
|
const name = c.req.param("name");
|
|
1007
1854
|
const entry = findAgent(name);
|
|
1008
1855
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1009
|
-
if (!
|
|
1856
|
+
if (!existsSync6(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
|
|
1010
1857
|
const { status, channels } = await getAgentStatus(name, entry.port);
|
|
1011
1858
|
const variants = readVariants(name);
|
|
1012
1859
|
const manager = getAgentManager();
|
|
@@ -1032,7 +1879,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1032
1879
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1033
1880
|
} else {
|
|
1034
1881
|
const dir = agentDir(baseName);
|
|
1035
|
-
if (!
|
|
1882
|
+
if (!existsSync6(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
1036
1883
|
}
|
|
1037
1884
|
if (getAgentManager().isRunning(name)) {
|
|
1038
1885
|
return c.json({ error: "Agent already running" }, 409);
|
|
@@ -1053,7 +1900,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1053
1900
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1054
1901
|
} else {
|
|
1055
1902
|
const dir = agentDir(baseName);
|
|
1056
|
-
if (!
|
|
1903
|
+
if (!existsSync6(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
1057
1904
|
}
|
|
1058
1905
|
let context;
|
|
1059
1906
|
const contentType = c.req.header("content-type");
|
|
@@ -1081,21 +1928,58 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1081
1928
|
return c.json({ error: `Invalid variant name: ${branchErr}` }, 400);
|
|
1082
1929
|
}
|
|
1083
1930
|
console.error(`[daemon] merging variant for ${baseName}: ${mergeVariantName}`);
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1931
|
+
const variant = findVariant(baseName, mergeVariantName);
|
|
1932
|
+
if (variant) {
|
|
1933
|
+
const projectRoot = agentDir(baseName);
|
|
1934
|
+
if (existsSync6(variant.path)) {
|
|
1935
|
+
const status = (await exec("git", ["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
1936
|
+
if (status) {
|
|
1937
|
+
try {
|
|
1938
|
+
await exec("git", ["add", "-A"], { cwd: variant.path });
|
|
1939
|
+
await exec(
|
|
1940
|
+
"git",
|
|
1941
|
+
["commit", "-m", "Auto-commit uncommitted changes before merge"],
|
|
1942
|
+
{ cwd: variant.path }
|
|
1943
|
+
);
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
console.error(
|
|
1946
|
+
`[daemon] failed to auto-commit variant worktree for ${baseName}:`,
|
|
1947
|
+
e
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const mainStatus = (await exec("git", ["status", "--porcelain"], { cwd: projectRoot })).trim();
|
|
1953
|
+
if (mainStatus) {
|
|
1954
|
+
try {
|
|
1955
|
+
await exec("git", ["add", "-A"], { cwd: projectRoot });
|
|
1956
|
+
await exec("git", ["commit", "-m", "Auto-commit uncommitted changes before merge"], {
|
|
1957
|
+
cwd: projectRoot
|
|
1958
|
+
});
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
console.error(`[daemon] failed to auto-commit main worktree for ${baseName}:`, e);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
await exec("git", ["merge", variant.branch], { cwd: projectRoot });
|
|
1964
|
+
if (existsSync6(variant.path)) {
|
|
1965
|
+
try {
|
|
1966
|
+
await exec("git", ["worktree", "remove", "--force", variant.path], {
|
|
1967
|
+
cwd: projectRoot
|
|
1968
|
+
});
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
try {
|
|
1973
|
+
await exec("git", ["branch", "-D", variant.branch], { cwd: projectRoot });
|
|
1974
|
+
} catch {
|
|
1975
|
+
}
|
|
1976
|
+
removeVariant(baseName, mergeVariantName);
|
|
1977
|
+
try {
|
|
1978
|
+
await exec("npm", ["install"], { cwd: projectRoot });
|
|
1979
|
+
} catch (e) {
|
|
1980
|
+
console.error(`[daemon] npm install failed after merge for ${baseName}:`, e);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1099
1983
|
}
|
|
1100
1984
|
if (context) {
|
|
1101
1985
|
manager.setPendingContext(name, context);
|
|
@@ -1143,15 +2027,144 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1143
2027
|
}
|
|
1144
2028
|
removeAllVariants(name);
|
|
1145
2029
|
removeAgent(name);
|
|
1146
|
-
await
|
|
2030
|
+
await deleteAgentUser2(name);
|
|
1147
2031
|
const state = stateDir(name);
|
|
1148
|
-
if (
|
|
1149
|
-
|
|
2032
|
+
if (existsSync6(state)) {
|
|
2033
|
+
rmSync2(state, { recursive: true, force: true });
|
|
1150
2034
|
}
|
|
1151
|
-
if (force &&
|
|
1152
|
-
|
|
2035
|
+
if (force && existsSync6(dir)) {
|
|
2036
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
2037
|
+
deleteAgentUser(name);
|
|
1153
2038
|
}
|
|
1154
2039
|
return c.json({ ok: true });
|
|
2040
|
+
}).post("/:name/upgrade", requireAdmin, async (c) => {
|
|
2041
|
+
const agentName = c.req.param("name");
|
|
2042
|
+
const entry = findAgent(agentName);
|
|
2043
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2044
|
+
const dir = agentDir(agentName);
|
|
2045
|
+
if (!existsSync6(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
2046
|
+
let body = {};
|
|
2047
|
+
try {
|
|
2048
|
+
body = await c.req.json();
|
|
2049
|
+
} catch {
|
|
2050
|
+
}
|
|
2051
|
+
const template = body.template ?? "agent-sdk";
|
|
2052
|
+
const UPGRADE_VARIANT = "upgrade";
|
|
2053
|
+
if (body.continue) {
|
|
2054
|
+
const worktreeDir2 = resolve9(dir, ".variants", UPGRADE_VARIANT);
|
|
2055
|
+
if (!existsSync6(worktreeDir2)) {
|
|
2056
|
+
return c.json({ error: "No upgrade in progress" }, 400);
|
|
2057
|
+
}
|
|
2058
|
+
const status = await exec("git", ["status", "--porcelain"], { cwd: worktreeDir2 });
|
|
2059
|
+
const hasConflicts2 = status.split("\n").some((line) => line.startsWith("UU") || line.startsWith("AA"));
|
|
2060
|
+
if (hasConflicts2) {
|
|
2061
|
+
return c.json({ error: "Unresolved conflicts remain" }, 409);
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
await exec("git", ["add", "-A"], { cwd: worktreeDir2 });
|
|
2065
|
+
await exec("git", ["commit", "--no-edit"], { cwd: worktreeDir2 });
|
|
2066
|
+
} catch (e) {
|
|
2067
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2068
|
+
if (!msg.includes("nothing to commit")) throw e;
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
await exec("npm", ["install"], { cwd: worktreeDir2 });
|
|
2072
|
+
const variantPort = nextPort();
|
|
2073
|
+
addVariant(agentName, {
|
|
2074
|
+
name: UPGRADE_VARIANT,
|
|
2075
|
+
branch: UPGRADE_VARIANT,
|
|
2076
|
+
path: worktreeDir2,
|
|
2077
|
+
port: variantPort,
|
|
2078
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2079
|
+
});
|
|
2080
|
+
await getAgentManager().startAgent(`${agentName}@${UPGRADE_VARIANT}`);
|
|
2081
|
+
return c.json({
|
|
2082
|
+
ok: true,
|
|
2083
|
+
name: agentName,
|
|
2084
|
+
variant: UPGRADE_VARIANT,
|
|
2085
|
+
port: variantPort
|
|
2086
|
+
});
|
|
2087
|
+
} catch (err) {
|
|
2088
|
+
try {
|
|
2089
|
+
removeVariant(agentName, UPGRADE_VARIANT);
|
|
2090
|
+
} catch {
|
|
2091
|
+
}
|
|
2092
|
+
try {
|
|
2093
|
+
await exec("git", ["worktree", "remove", "--force", worktreeDir2], { cwd: dir });
|
|
2094
|
+
} catch {
|
|
2095
|
+
}
|
|
2096
|
+
try {
|
|
2097
|
+
await exec("git", ["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
return c.json(
|
|
2101
|
+
{ error: err instanceof Error ? err.message : "Failed to continue upgrade" },
|
|
2102
|
+
500
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const worktreeDir = resolve9(dir, ".variants", UPGRADE_VARIANT);
|
|
2107
|
+
if (existsSync6(worktreeDir)) {
|
|
2108
|
+
return c.json(
|
|
2109
|
+
{ error: "Upgrade variant already exists. Use continue or delete it first." },
|
|
2110
|
+
409
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
await exec("git", ["worktree", "prune"], { cwd: dir });
|
|
2114
|
+
try {
|
|
2115
|
+
await exec("git", ["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
2116
|
+
} catch {
|
|
2117
|
+
}
|
|
2118
|
+
await updateTemplateBranch(dir, template, agentName);
|
|
2119
|
+
const parentDir = resolve9(dir, ".variants");
|
|
2120
|
+
if (!existsSync6(parentDir)) {
|
|
2121
|
+
mkdirSync5(parentDir, { recursive: true });
|
|
2122
|
+
}
|
|
2123
|
+
await exec("git", ["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
2124
|
+
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
2125
|
+
if (hasConflicts) {
|
|
2126
|
+
return c.json({
|
|
2127
|
+
ok: false,
|
|
2128
|
+
conflicts: true,
|
|
2129
|
+
worktreeDir,
|
|
2130
|
+
message: "Merge conflicts detected. Resolve them, then run with continue."
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
await exec("npm", ["install"], { cwd: worktreeDir });
|
|
2135
|
+
const variantPort = nextPort();
|
|
2136
|
+
addVariant(agentName, {
|
|
2137
|
+
name: UPGRADE_VARIANT,
|
|
2138
|
+
branch: UPGRADE_VARIANT,
|
|
2139
|
+
path: worktreeDir,
|
|
2140
|
+
port: variantPort,
|
|
2141
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2142
|
+
});
|
|
2143
|
+
await getAgentManager().startAgent(`${agentName}@${UPGRADE_VARIANT}`);
|
|
2144
|
+
return c.json({
|
|
2145
|
+
ok: true,
|
|
2146
|
+
name: agentName,
|
|
2147
|
+
variant: UPGRADE_VARIANT,
|
|
2148
|
+
port: variantPort
|
|
2149
|
+
});
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
try {
|
|
2152
|
+
removeVariant(agentName, UPGRADE_VARIANT);
|
|
2153
|
+
} catch {
|
|
2154
|
+
}
|
|
2155
|
+
try {
|
|
2156
|
+
await exec("git", ["worktree", "remove", "--force", worktreeDir], { cwd: dir });
|
|
2157
|
+
} catch {
|
|
2158
|
+
}
|
|
2159
|
+
try {
|
|
2160
|
+
await exec("git", ["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
2161
|
+
} catch {
|
|
2162
|
+
}
|
|
2163
|
+
return c.json(
|
|
2164
|
+
{ error: err instanceof Error ? err.message : "Failed to complete upgrade" },
|
|
2165
|
+
500
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
1155
2168
|
}).post("/:name/message", async (c) => {
|
|
1156
2169
|
const name = c.req.param("name");
|
|
1157
2170
|
const [baseName, variantName] = name.split("@", 2);
|
|
@@ -1174,12 +2187,12 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1174
2187
|
console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
|
|
1175
2188
|
}
|
|
1176
2189
|
const channel = parsed?.channel ?? "unknown";
|
|
1177
|
-
const
|
|
2190
|
+
const db2 = await getDb();
|
|
1178
2191
|
if (parsed) {
|
|
1179
2192
|
try {
|
|
1180
2193
|
const sender2 = parsed.sender ?? null;
|
|
1181
2194
|
const content = extractTextContent(parsed.content);
|
|
1182
|
-
await
|
|
2195
|
+
await db2.insert(agentMessages).values({
|
|
1183
2196
|
agent: baseName,
|
|
1184
2197
|
channel,
|
|
1185
2198
|
sender: sender2,
|
|
@@ -1232,8 +2245,8 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1232
2245
|
body: forwardBody
|
|
1233
2246
|
});
|
|
1234
2247
|
if (!res.ok) {
|
|
1235
|
-
const
|
|
1236
|
-
console.error(`[daemon] agent ${name} responded with ${res.status}: ${
|
|
2248
|
+
const text2 = await res.text().catch(() => "");
|
|
2249
|
+
console.error(`[daemon] agent ${name} responded with ${res.status}: ${text2}`);
|
|
1237
2250
|
return c.json({ error: `Agent responded with ${res.status}` }, res.status);
|
|
1238
2251
|
}
|
|
1239
2252
|
let result;
|
|
@@ -1272,9 +2285,9 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1272
2285
|
if (!body.channel || !body.content) {
|
|
1273
2286
|
return c.json({ error: "channel and content required" }, 400);
|
|
1274
2287
|
}
|
|
1275
|
-
const
|
|
2288
|
+
const db2 = await getDb();
|
|
1276
2289
|
try {
|
|
1277
|
-
await
|
|
2290
|
+
await db2.insert(agentMessages).values({
|
|
1278
2291
|
agent: baseName,
|
|
1279
2292
|
channel: body.channel,
|
|
1280
2293
|
sender: body.sender ?? baseName,
|
|
@@ -1287,20 +2300,20 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1287
2300
|
return c.json({ ok: true });
|
|
1288
2301
|
}).get("/:name/history/channels", async (c) => {
|
|
1289
2302
|
const name = c.req.param("name");
|
|
1290
|
-
const
|
|
1291
|
-
const rows = await
|
|
2303
|
+
const db2 = await getDb();
|
|
2304
|
+
const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq3(agentMessages.agent, name));
|
|
1292
2305
|
return c.json(rows.map((r) => r.channel));
|
|
1293
2306
|
}).get("/:name/history", async (c) => {
|
|
1294
2307
|
const name = c.req.param("name");
|
|
1295
2308
|
const channel = c.req.query("channel");
|
|
1296
2309
|
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
1297
2310
|
const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
|
|
1298
|
-
const
|
|
1299
|
-
const conditions = [
|
|
2311
|
+
const db2 = await getDb();
|
|
2312
|
+
const conditions = [eq3(agentMessages.agent, name)];
|
|
1300
2313
|
if (channel) {
|
|
1301
|
-
conditions.push(
|
|
2314
|
+
conditions.push(eq3(agentMessages.channel, channel));
|
|
1302
2315
|
}
|
|
1303
|
-
const rows = await
|
|
2316
|
+
const rows = await db2.select().from(agentMessages).where(and2(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
|
|
1304
2317
|
return c.json(rows);
|
|
1305
2318
|
});
|
|
1306
2319
|
var agents_default = app;
|
|
@@ -1376,10 +2389,107 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
|
|
|
1376
2389
|
}).route("/", admin);
|
|
1377
2390
|
var auth_default = app2;
|
|
1378
2391
|
|
|
1379
|
-
// src/web/routes/
|
|
2392
|
+
// src/web/routes/channels.ts
|
|
1380
2393
|
import { Hono as Hono3 } from "hono";
|
|
2394
|
+
function buildEnv(name) {
|
|
2395
|
+
return { ...loadMergedEnv(name), VOLUTE_AGENT: name, VOLUTE_AGENT_DIR: agentDir(name) };
|
|
2396
|
+
}
|
|
2397
|
+
var app3 = new Hono3().post("/:name/channels/send", requireAdmin, async (c) => {
|
|
2398
|
+
const name = c.req.param("name");
|
|
2399
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2400
|
+
const { platform, uri, message } = await c.req.json();
|
|
2401
|
+
const driver = getChannelDriver(platform);
|
|
2402
|
+
if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
|
|
2403
|
+
const env = buildEnv(name);
|
|
2404
|
+
try {
|
|
2405
|
+
await driver.send(env, uri, message);
|
|
2406
|
+
return c.json({ ok: true });
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2409
|
+
}
|
|
2410
|
+
}).get("/:name/channels/read", async (c) => {
|
|
2411
|
+
const name = c.req.param("name");
|
|
2412
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2413
|
+
const platform = c.req.query("platform");
|
|
2414
|
+
const uri = c.req.query("uri");
|
|
2415
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10) || 20;
|
|
2416
|
+
if (!platform || !uri) return c.json({ error: "platform and uri required" }, 400);
|
|
2417
|
+
const driver = getChannelDriver(platform);
|
|
2418
|
+
if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
|
|
2419
|
+
const env = buildEnv(name);
|
|
2420
|
+
try {
|
|
2421
|
+
const output = await driver.read(env, uri, limit);
|
|
2422
|
+
return c.text(output);
|
|
2423
|
+
} catch (err) {
|
|
2424
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2425
|
+
}
|
|
2426
|
+
}).get("/:name/channels/list", async (c) => {
|
|
2427
|
+
const name = c.req.param("name");
|
|
2428
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2429
|
+
const platform = c.req.query("platform");
|
|
2430
|
+
const platforms = platform ? [platform] : Object.keys(CHANNELS);
|
|
2431
|
+
const env = buildEnv(name);
|
|
2432
|
+
const results = {};
|
|
2433
|
+
for (const p of platforms) {
|
|
2434
|
+
const driver = getChannelDriver(p);
|
|
2435
|
+
if (!driver?.listConversations) continue;
|
|
2436
|
+
try {
|
|
2437
|
+
const convs = await driver.listConversations(env);
|
|
2438
|
+
for (const conv of convs) {
|
|
2439
|
+
writeChannelEntry(name, conv.id, {
|
|
2440
|
+
platformId: conv.platformId,
|
|
2441
|
+
platform: p,
|
|
2442
|
+
name: conv.name,
|
|
2443
|
+
type: conv.type
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
results[p] = convs;
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
results[p] = [{ error: err instanceof Error ? err.message : String(err) }];
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return c.json(results);
|
|
2452
|
+
}).get("/:name/channels/users", async (c) => {
|
|
2453
|
+
const name = c.req.param("name");
|
|
2454
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2455
|
+
const platform = c.req.query("platform");
|
|
2456
|
+
if (!platform) return c.json({ error: "platform required" }, 400);
|
|
2457
|
+
const driver = getChannelDriver(platform);
|
|
2458
|
+
if (!driver?.listUsers)
|
|
2459
|
+
return c.json({ error: `Platform ${platform} does not support listing users` }, 400);
|
|
2460
|
+
const env = buildEnv(name);
|
|
2461
|
+
try {
|
|
2462
|
+
const users2 = await driver.listUsers(env);
|
|
2463
|
+
return c.json(users2);
|
|
2464
|
+
} catch (err) {
|
|
2465
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2466
|
+
}
|
|
2467
|
+
}).post("/:name/channels/create", requireAdmin, async (c) => {
|
|
2468
|
+
const name = c.req.param("name");
|
|
2469
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2470
|
+
const {
|
|
2471
|
+
platform,
|
|
2472
|
+
participants,
|
|
2473
|
+
name: convName
|
|
2474
|
+
} = await c.req.json();
|
|
2475
|
+
const driver = getChannelDriver(platform);
|
|
2476
|
+
if (!driver?.createConversation) {
|
|
2477
|
+
return c.json({ error: `Platform ${platform} does not support creating conversations` }, 400);
|
|
2478
|
+
}
|
|
2479
|
+
const env = buildEnv(name);
|
|
2480
|
+
try {
|
|
2481
|
+
const slug = await driver.createConversation(env, participants, convName);
|
|
2482
|
+
return c.json({ slug });
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2487
|
+
var channels_default = app3;
|
|
2488
|
+
|
|
2489
|
+
// src/web/routes/connectors.ts
|
|
2490
|
+
import { Hono as Hono4 } from "hono";
|
|
1381
2491
|
var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
|
|
1382
|
-
var
|
|
2492
|
+
var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
1383
2493
|
const name = c.req.param("name");
|
|
1384
2494
|
const entry = findAgent(name);
|
|
1385
2495
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
@@ -1445,21 +2555,95 @@ var app3 = new Hono3().get("/:name/connectors", (c) => {
|
|
|
1445
2555
|
writeVoluteConfig(dir, config);
|
|
1446
2556
|
return c.json({ ok: true });
|
|
1447
2557
|
});
|
|
1448
|
-
var connectors_default =
|
|
2558
|
+
var connectors_default = app4;
|
|
2559
|
+
|
|
2560
|
+
// src/web/routes/env.ts
|
|
2561
|
+
import { Hono as Hono5 } from "hono";
|
|
2562
|
+
var app5 = new Hono5().get("/:name/env", (c) => {
|
|
2563
|
+
const name = c.req.param("name");
|
|
2564
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2565
|
+
const shared = readEnv(sharedEnvPath());
|
|
2566
|
+
const agent = readEnv(agentEnvPath(name));
|
|
2567
|
+
return c.json({ shared, agent });
|
|
2568
|
+
}).get("/:name/env/:key", (c) => {
|
|
2569
|
+
const name = c.req.param("name");
|
|
2570
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2571
|
+
const key = c.req.param("key");
|
|
2572
|
+
const merged = loadMergedEnv(name);
|
|
2573
|
+
const value = merged[key];
|
|
2574
|
+
if (value === void 0) return c.json({ error: "Key not found" }, 404);
|
|
2575
|
+
return c.json({ value });
|
|
2576
|
+
}).put("/:name/env/:key", requireAdmin, async (c) => {
|
|
2577
|
+
const name = c.req.param("name");
|
|
2578
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2579
|
+
const key = c.req.param("key");
|
|
2580
|
+
let body;
|
|
2581
|
+
try {
|
|
2582
|
+
body = await c.req.json();
|
|
2583
|
+
} catch {
|
|
2584
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
2585
|
+
}
|
|
2586
|
+
if (typeof body.value !== "string") {
|
|
2587
|
+
return c.json({ error: "Missing required field: value" }, 400);
|
|
2588
|
+
}
|
|
2589
|
+
const path = agentEnvPath(name);
|
|
2590
|
+
const env = readEnv(path);
|
|
2591
|
+
env[key] = body.value;
|
|
2592
|
+
writeEnv(path, env);
|
|
2593
|
+
return c.json({ ok: true });
|
|
2594
|
+
}).delete("/:name/env/:key", requireAdmin, (c) => {
|
|
2595
|
+
const name = c.req.param("name");
|
|
2596
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
2597
|
+
const key = c.req.param("key");
|
|
2598
|
+
const path = agentEnvPath(name);
|
|
2599
|
+
const env = readEnv(path);
|
|
2600
|
+
if (!(key in env)) return c.json({ error: "Key not found" }, 404);
|
|
2601
|
+
delete env[key];
|
|
2602
|
+
writeEnv(path, env);
|
|
2603
|
+
return c.json({ ok: true });
|
|
2604
|
+
});
|
|
2605
|
+
var sharedEnvApp = new Hono5().get("/", (c) => {
|
|
2606
|
+
return c.json(readEnv(sharedEnvPath()));
|
|
2607
|
+
}).put("/:key", requireAdmin, async (c) => {
|
|
2608
|
+
const key = c.req.param("key");
|
|
2609
|
+
let body;
|
|
2610
|
+
try {
|
|
2611
|
+
body = await c.req.json();
|
|
2612
|
+
} catch {
|
|
2613
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
2614
|
+
}
|
|
2615
|
+
if (typeof body.value !== "string") {
|
|
2616
|
+
return c.json({ error: "Missing required field: value" }, 400);
|
|
2617
|
+
}
|
|
2618
|
+
const path = sharedEnvPath();
|
|
2619
|
+
const env = readEnv(path);
|
|
2620
|
+
env[key] = body.value;
|
|
2621
|
+
writeEnv(path, env);
|
|
2622
|
+
return c.json({ ok: true });
|
|
2623
|
+
}).delete("/:key", requireAdmin, (c) => {
|
|
2624
|
+
const key = c.req.param("key");
|
|
2625
|
+
const path = sharedEnvPath();
|
|
2626
|
+
const env = readEnv(path);
|
|
2627
|
+
if (!(key in env)) return c.json({ error: "Key not found" }, 404);
|
|
2628
|
+
delete env[key];
|
|
2629
|
+
writeEnv(path, env);
|
|
2630
|
+
return c.json({ ok: true });
|
|
2631
|
+
});
|
|
2632
|
+
var env_default = app5;
|
|
1449
2633
|
|
|
1450
2634
|
// src/web/routes/files.ts
|
|
1451
|
-
import { existsSync as
|
|
2635
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1452
2636
|
import { readdir, readFile } from "fs/promises";
|
|
1453
|
-
import { resolve as
|
|
1454
|
-
import { Hono as
|
|
2637
|
+
import { resolve as resolve10 } from "path";
|
|
2638
|
+
import { Hono as Hono6 } from "hono";
|
|
1455
2639
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
1456
|
-
var
|
|
2640
|
+
var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
1457
2641
|
const name = c.req.param("name");
|
|
1458
2642
|
const entry = findAgent(name);
|
|
1459
2643
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1460
2644
|
const dir = agentDir(name);
|
|
1461
|
-
const homeDir =
|
|
1462
|
-
if (!
|
|
2645
|
+
const homeDir = resolve10(dir, "home");
|
|
2646
|
+
if (!existsSync7(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1463
2647
|
const allFiles = await readdir(homeDir);
|
|
1464
2648
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1465
2649
|
return c.json(files);
|
|
@@ -1472,27 +2656,27 @@ var app4 = new Hono4().get("/:name/files", async (c) => {
|
|
|
1472
2656
|
const entry = findAgent(name);
|
|
1473
2657
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1474
2658
|
const dir = agentDir(name);
|
|
1475
|
-
const filePath =
|
|
1476
|
-
if (!
|
|
2659
|
+
const filePath = resolve10(dir, "home", filename);
|
|
2660
|
+
if (!existsSync7(filePath)) {
|
|
1477
2661
|
return c.json({ error: "File not found" }, 404);
|
|
1478
2662
|
}
|
|
1479
2663
|
const content = await readFile(filePath, "utf-8");
|
|
1480
2664
|
return c.json({ filename, content });
|
|
1481
2665
|
});
|
|
1482
|
-
var files_default =
|
|
2666
|
+
var files_default = app6;
|
|
1483
2667
|
|
|
1484
2668
|
// src/web/routes/logs.ts
|
|
1485
2669
|
import { spawn as spawn2 } from "child_process";
|
|
1486
|
-
import { existsSync as
|
|
1487
|
-
import { resolve as
|
|
1488
|
-
import { Hono as
|
|
2670
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2671
|
+
import { resolve as resolve11 } from "path";
|
|
2672
|
+
import { Hono as Hono7 } from "hono";
|
|
1489
2673
|
import { streamSSE } from "hono/streaming";
|
|
1490
|
-
var
|
|
2674
|
+
var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
1491
2675
|
const name = c.req.param("name");
|
|
1492
2676
|
const entry = findAgent(name);
|
|
1493
2677
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1494
|
-
const logFile =
|
|
1495
|
-
if (!
|
|
2678
|
+
const logFile = resolve11(stateDir(name), "logs", "agent.log");
|
|
2679
|
+
if (!existsSync8(logFile)) {
|
|
1496
2680
|
return c.json({ error: "No log file found" }, 404);
|
|
1497
2681
|
}
|
|
1498
2682
|
return streamSSE(c, async (stream) => {
|
|
@@ -1510,16 +2694,35 @@ var app5 = new Hono5().get("/:name/logs", async (c) => {
|
|
|
1510
2694
|
stream.onAbort(() => {
|
|
1511
2695
|
tail.kill();
|
|
1512
2696
|
});
|
|
1513
|
-
await new Promise((
|
|
1514
|
-
tail.on("exit",
|
|
1515
|
-
stream.onAbort(
|
|
2697
|
+
await new Promise((resolve17) => {
|
|
2698
|
+
tail.on("exit", resolve17);
|
|
2699
|
+
stream.onAbort(resolve17);
|
|
1516
2700
|
});
|
|
1517
2701
|
});
|
|
2702
|
+
}).get("/:name/logs/tail", async (c) => {
|
|
2703
|
+
const name = c.req.param("name");
|
|
2704
|
+
const entry = findAgent(name);
|
|
2705
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
2706
|
+
const logFile = resolve11(stateDir(name), "logs", "agent.log");
|
|
2707
|
+
if (!existsSync8(logFile)) {
|
|
2708
|
+
return c.json({ error: "No log file found" }, 404);
|
|
2709
|
+
}
|
|
2710
|
+
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
2711
|
+
const n = Number.isFinite(nParam) && nParam > 0 ? Math.min(nParam, 1e4) : 50;
|
|
2712
|
+
const tail = spawn2("tail", ["-n", String(n), logFile]);
|
|
2713
|
+
let output = "";
|
|
2714
|
+
tail.stdout.on("data", (data) => {
|
|
2715
|
+
output += data.toString();
|
|
2716
|
+
});
|
|
2717
|
+
await new Promise((resolve17) => {
|
|
2718
|
+
tail.on("exit", resolve17);
|
|
2719
|
+
});
|
|
2720
|
+
return c.text(output);
|
|
1518
2721
|
});
|
|
1519
|
-
var logs_default =
|
|
2722
|
+
var logs_default = app7;
|
|
1520
2723
|
|
|
1521
2724
|
// src/web/routes/schedules.ts
|
|
1522
|
-
import { Hono as
|
|
2725
|
+
import { Hono as Hono8 } from "hono";
|
|
1523
2726
|
function readSchedules(name) {
|
|
1524
2727
|
return readVoluteConfig(agentDir(name))?.schedules ?? [];
|
|
1525
2728
|
}
|
|
@@ -1530,7 +2733,7 @@ function writeSchedules(name, schedules) {
|
|
|
1530
2733
|
writeVoluteConfig(dir, config);
|
|
1531
2734
|
getScheduler().loadSchedules(name);
|
|
1532
2735
|
}
|
|
1533
|
-
var
|
|
2736
|
+
var app8 = new Hono8().get("/:name/schedules", (c) => {
|
|
1534
2737
|
const name = c.req.param("name");
|
|
1535
2738
|
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
1536
2739
|
return c.json(readSchedules(name));
|
|
@@ -1598,12 +2801,18 @@ var app6 = new Hono6().get("/:name/schedules", (c) => {
|
|
|
1598
2801
|
return c.json({ error: "Failed to reach agent" }, 502);
|
|
1599
2802
|
}
|
|
1600
2803
|
});
|
|
1601
|
-
var schedules_default =
|
|
2804
|
+
var schedules_default = app8;
|
|
1602
2805
|
|
|
1603
2806
|
// src/web/routes/system.ts
|
|
1604
|
-
import { Hono as
|
|
2807
|
+
import { Hono as Hono9 } from "hono";
|
|
1605
2808
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
1606
|
-
var
|
|
2809
|
+
var app9 = new Hono9().post("/restart", requireAdmin, (c) => {
|
|
2810
|
+
setTimeout(() => process.exit(1), 200);
|
|
2811
|
+
return c.json({ ok: true });
|
|
2812
|
+
}).post("/stop", requireAdmin, (c) => {
|
|
2813
|
+
setTimeout(() => process.exit(0), 200);
|
|
2814
|
+
return c.json({ ok: true });
|
|
2815
|
+
}).get("/logs", async (c) => {
|
|
1607
2816
|
const user = c.get("user");
|
|
1608
2817
|
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
1609
2818
|
return streamSSE2(c, async (stream) => {
|
|
@@ -1614,26 +2823,26 @@ var app7 = new Hono7().get("/logs", async (c) => {
|
|
|
1614
2823
|
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
1615
2824
|
});
|
|
1616
2825
|
});
|
|
1617
|
-
await new Promise((
|
|
2826
|
+
await new Promise((resolve17) => {
|
|
1618
2827
|
stream.onAbort(() => {
|
|
1619
2828
|
unsubscribe();
|
|
1620
|
-
|
|
2829
|
+
resolve17();
|
|
1621
2830
|
});
|
|
1622
2831
|
});
|
|
1623
2832
|
});
|
|
1624
2833
|
});
|
|
1625
|
-
var system_default =
|
|
2834
|
+
var system_default = app9;
|
|
1626
2835
|
|
|
1627
2836
|
// src/web/routes/typing.ts
|
|
1628
2837
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
1629
|
-
import { Hono as
|
|
2838
|
+
import { Hono as Hono10 } from "hono";
|
|
1630
2839
|
import { z as z2 } from "zod";
|
|
1631
2840
|
var typingSchema = z2.object({
|
|
1632
2841
|
channel: z2.string().min(1),
|
|
1633
2842
|
sender: z2.string().min(1),
|
|
1634
2843
|
active: z2.boolean()
|
|
1635
2844
|
});
|
|
1636
|
-
var
|
|
2845
|
+
var app10 = new Hono10().post("/:name/typing", zValidator2("json", typingSchema), (c) => {
|
|
1637
2846
|
const { channel, sender, active } = c.req.valid("json");
|
|
1638
2847
|
const map = getTypingMap();
|
|
1639
2848
|
if (active) {
|
|
@@ -1650,13 +2859,13 @@ var app8 = new Hono8().post("/:name/typing", zValidator2("json", typingSchema),
|
|
|
1650
2859
|
const map = getTypingMap();
|
|
1651
2860
|
return c.json({ typing: map.get(channel) });
|
|
1652
2861
|
});
|
|
1653
|
-
var typing_default =
|
|
2862
|
+
var typing_default = app10;
|
|
1654
2863
|
|
|
1655
2864
|
// src/web/routes/update.ts
|
|
1656
2865
|
import { spawn as spawn3 } from "child_process";
|
|
1657
|
-
import { Hono as
|
|
2866
|
+
import { Hono as Hono11 } from "hono";
|
|
1658
2867
|
var bin;
|
|
1659
|
-
var
|
|
2868
|
+
var app11 = new Hono11().get("/update", async (c) => {
|
|
1660
2869
|
const result = await checkForUpdate();
|
|
1661
2870
|
return c.json(result);
|
|
1662
2871
|
}).post("/update", requireAdmin, async (c) => {
|
|
@@ -1671,11 +2880,128 @@ var app9 = new Hono9().get("/update", async (c) => {
|
|
|
1671
2880
|
child.unref();
|
|
1672
2881
|
return c.json({ ok: true, message: "Updating..." });
|
|
1673
2882
|
});
|
|
1674
|
-
var update_default =
|
|
2883
|
+
var update_default = app11;
|
|
1675
2884
|
|
|
1676
2885
|
// src/web/routes/variants.ts
|
|
1677
|
-
import {
|
|
1678
|
-
|
|
2886
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
2887
|
+
import { resolve as resolve13 } from "path";
|
|
2888
|
+
import { Hono as Hono12 } from "hono";
|
|
2889
|
+
|
|
2890
|
+
// src/lib/spawn-server.ts
|
|
2891
|
+
import { spawn as spawn4 } from "child_process";
|
|
2892
|
+
import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync7 } from "fs";
|
|
2893
|
+
import { resolve as resolve12 } from "path";
|
|
2894
|
+
function tsxBin(cwd) {
|
|
2895
|
+
return resolve12(cwd, "node_modules", ".bin", "tsx");
|
|
2896
|
+
}
|
|
2897
|
+
function spawnServer(cwd, port, options) {
|
|
2898
|
+
if (options?.detached) {
|
|
2899
|
+
return spawnDetached(cwd, port, options.logDir);
|
|
2900
|
+
}
|
|
2901
|
+
return spawnAttached(cwd, port);
|
|
2902
|
+
}
|
|
2903
|
+
function spawnAttached(cwd, port) {
|
|
2904
|
+
const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
2905
|
+
cwd,
|
|
2906
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2907
|
+
});
|
|
2908
|
+
return new Promise((resolve17) => {
|
|
2909
|
+
const timeout = setTimeout(() => resolve17(null), 3e4);
|
|
2910
|
+
function checkOutput(data) {
|
|
2911
|
+
const match = data.toString().match(/listening on :(\d+)/);
|
|
2912
|
+
if (match) {
|
|
2913
|
+
clearTimeout(timeout);
|
|
2914
|
+
resolve17({ child, actualPort: parseInt(match[1], 10) });
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
child.stdout?.on("data", checkOutput);
|
|
2918
|
+
child.stderr?.on("data", checkOutput);
|
|
2919
|
+
child.on("error", () => {
|
|
2920
|
+
clearTimeout(timeout);
|
|
2921
|
+
resolve17(null);
|
|
2922
|
+
});
|
|
2923
|
+
child.on("exit", () => {
|
|
2924
|
+
clearTimeout(timeout);
|
|
2925
|
+
resolve17(null);
|
|
2926
|
+
});
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
function spawnDetached(cwd, port, logDir) {
|
|
2930
|
+
const logsDir = logDir ?? resolve12(cwd, ".volute", "logs");
|
|
2931
|
+
mkdirSync6(logsDir, { recursive: true });
|
|
2932
|
+
const logPath = resolve12(logsDir, "agent.log");
|
|
2933
|
+
const logFd = openSync(logPath, "a");
|
|
2934
|
+
const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
2935
|
+
cwd,
|
|
2936
|
+
stdio: ["ignore", logFd, logFd],
|
|
2937
|
+
detached: true
|
|
2938
|
+
});
|
|
2939
|
+
child.unref();
|
|
2940
|
+
closeSync(logFd);
|
|
2941
|
+
return new Promise((res) => {
|
|
2942
|
+
let done = false;
|
|
2943
|
+
function finish(result) {
|
|
2944
|
+
if (done) return;
|
|
2945
|
+
done = true;
|
|
2946
|
+
clearInterval(interval);
|
|
2947
|
+
clearTimeout(timeout);
|
|
2948
|
+
res(result);
|
|
2949
|
+
}
|
|
2950
|
+
const interval = setInterval(() => {
|
|
2951
|
+
try {
|
|
2952
|
+
const content = readFileSync7(logPath, "utf-8");
|
|
2953
|
+
const match = content.match(/listening on :(\d+)/);
|
|
2954
|
+
if (match) {
|
|
2955
|
+
finish({ child, actualPort: parseInt(match[1], 10) });
|
|
2956
|
+
}
|
|
2957
|
+
} catch {
|
|
2958
|
+
}
|
|
2959
|
+
}, 100);
|
|
2960
|
+
const timeout = setTimeout(() => finish(null), 3e4);
|
|
2961
|
+
child.on("error", () => finish(null));
|
|
2962
|
+
child.on("exit", () => finish(null));
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// src/lib/verify.ts
|
|
2967
|
+
async function verify(port) {
|
|
2968
|
+
const health = await checkHealth(port);
|
|
2969
|
+
if (!health.ok) {
|
|
2970
|
+
console.error(" Health check: failed");
|
|
2971
|
+
return false;
|
|
2972
|
+
}
|
|
2973
|
+
console.log(" Health check: OK");
|
|
2974
|
+
try {
|
|
2975
|
+
const res = await fetch(`http://127.0.0.1:${port}/message`, {
|
|
2976
|
+
method: "POST",
|
|
2977
|
+
headers: { "Content-Type": "application/json" },
|
|
2978
|
+
body: JSON.stringify({
|
|
2979
|
+
content: [{ type: "text", text: "ping" }],
|
|
2980
|
+
channel: "system"
|
|
2981
|
+
}),
|
|
2982
|
+
signal: AbortSignal.timeout(6e4)
|
|
2983
|
+
});
|
|
2984
|
+
if (!res.ok) {
|
|
2985
|
+
console.error(" Test message: failed to send");
|
|
2986
|
+
return false;
|
|
2987
|
+
}
|
|
2988
|
+
const result = await res.json();
|
|
2989
|
+
if (result.ok) {
|
|
2990
|
+
console.log(" Test message: OK");
|
|
2991
|
+
return true;
|
|
2992
|
+
} else {
|
|
2993
|
+
console.error(" Test message: unexpected response");
|
|
2994
|
+
return false;
|
|
2995
|
+
}
|
|
2996
|
+
} catch (e) {
|
|
2997
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2998
|
+
console.error(` Test message: ${msg}`);
|
|
2999
|
+
return false;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
// src/web/routes/variants.ts
|
|
3004
|
+
var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
1679
3005
|
const name = c.req.param("name");
|
|
1680
3006
|
const entry = findAgent(name);
|
|
1681
3007
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
@@ -1688,14 +3014,205 @@ var app10 = new Hono10().get("/:name/variants", async (c) => {
|
|
|
1688
3014
|
})
|
|
1689
3015
|
);
|
|
1690
3016
|
return c.json(results);
|
|
3017
|
+
}).post("/:name/variants", requireAdmin, async (c) => {
|
|
3018
|
+
const agentName = c.req.param("name");
|
|
3019
|
+
const entry = findAgent(agentName);
|
|
3020
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
3021
|
+
let body;
|
|
3022
|
+
try {
|
|
3023
|
+
body = await c.req.json();
|
|
3024
|
+
} catch {
|
|
3025
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
3026
|
+
}
|
|
3027
|
+
const variantName = body.name;
|
|
3028
|
+
if (!variantName) return c.json({ error: "Variant name required" }, 400);
|
|
3029
|
+
const err = validateBranchName(variantName);
|
|
3030
|
+
if (err) return c.json({ error: err }, 400);
|
|
3031
|
+
const projectRoot = agentDir(agentName);
|
|
3032
|
+
const variantDir = resolve13(projectRoot, ".variants", variantName);
|
|
3033
|
+
if (existsSync9(variantDir)) {
|
|
3034
|
+
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
3035
|
+
}
|
|
3036
|
+
mkdirSync7(resolve13(projectRoot, ".variants"), { recursive: true });
|
|
3037
|
+
try {
|
|
3038
|
+
await exec("git", ["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
3039
|
+
} catch (e) {
|
|
3040
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3041
|
+
return c.json({ error: `Failed to create worktree: ${msg}` }, 500);
|
|
3042
|
+
}
|
|
3043
|
+
try {
|
|
3044
|
+
await exec("npm", ["install"], { cwd: variantDir });
|
|
3045
|
+
} catch (e) {
|
|
3046
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3047
|
+
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
3048
|
+
}
|
|
3049
|
+
if (body.soul) {
|
|
3050
|
+
writeFileSync6(resolve13(variantDir, "home/SOUL.md"), body.soul);
|
|
3051
|
+
}
|
|
3052
|
+
const variantPort = body.port ?? nextPort();
|
|
3053
|
+
const variant = {
|
|
3054
|
+
name: variantName,
|
|
3055
|
+
branch: variantName,
|
|
3056
|
+
path: variantDir,
|
|
3057
|
+
port: variantPort,
|
|
3058
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
3059
|
+
};
|
|
3060
|
+
addVariant(agentName, variant);
|
|
3061
|
+
if (!body.noStart) {
|
|
3062
|
+
try {
|
|
3063
|
+
await getAgentManager().startAgent(`${agentName}@${variantName}`);
|
|
3064
|
+
} catch (e) {
|
|
3065
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3066
|
+
return c.json({ error: `Variant created but failed to start: ${msg}` }, 500);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
return c.json({
|
|
3070
|
+
ok: true,
|
|
3071
|
+
variant: { name: variantName, branch: variantName, path: variantDir, port: variantPort }
|
|
3072
|
+
});
|
|
3073
|
+
}).post("/:name/variants/:variant/merge", requireAdmin, async (c) => {
|
|
3074
|
+
const agentName = c.req.param("name");
|
|
3075
|
+
const variantName = c.req.param("variant");
|
|
3076
|
+
const entry = findAgent(agentName);
|
|
3077
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
3078
|
+
const variant = findVariant(agentName, variantName);
|
|
3079
|
+
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
3080
|
+
const branchErr = validateBranchName(variant.branch);
|
|
3081
|
+
if (branchErr) return c.json({ error: branchErr }, 400);
|
|
3082
|
+
let body = {};
|
|
3083
|
+
try {
|
|
3084
|
+
body = await c.req.json();
|
|
3085
|
+
} catch {
|
|
3086
|
+
}
|
|
3087
|
+
const projectRoot = agentDir(agentName);
|
|
3088
|
+
if (existsSync9(variant.path)) {
|
|
3089
|
+
const status = (await exec("git", ["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
3090
|
+
if (status) {
|
|
3091
|
+
try {
|
|
3092
|
+
await exec("git", ["add", "-A"], { cwd: variant.path });
|
|
3093
|
+
await exec("git", ["commit", "-m", "Auto-commit uncommitted changes before merge"], {
|
|
3094
|
+
cwd: variant.path
|
|
3095
|
+
});
|
|
3096
|
+
} catch (e) {
|
|
3097
|
+
return c.json(
|
|
3098
|
+
{
|
|
3099
|
+
error: "Failed to auto-commit variant changes. Commit or stash manually before merging."
|
|
3100
|
+
},
|
|
3101
|
+
500
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
if (!body.skipVerify) {
|
|
3107
|
+
const result = await spawnServer(variant.path, 0, { detached: true });
|
|
3108
|
+
if (!result) {
|
|
3109
|
+
return c.json(
|
|
3110
|
+
{ error: "Failed to start server for verification. Use skipVerify to skip." },
|
|
3111
|
+
500
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
const verified = await verify(result.actualPort);
|
|
3115
|
+
try {
|
|
3116
|
+
process.kill(result.child.pid);
|
|
3117
|
+
} catch {
|
|
3118
|
+
}
|
|
3119
|
+
if (!verified) {
|
|
3120
|
+
return c.json(
|
|
3121
|
+
{ error: "Verification failed. Fix issues or use skipVerify to proceed." },
|
|
3122
|
+
500
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
const mainStatus = (await exec("git", ["status", "--porcelain"], { cwd: projectRoot })).trim();
|
|
3127
|
+
if (mainStatus) {
|
|
3128
|
+
try {
|
|
3129
|
+
await exec("git", ["add", "-A"], { cwd: projectRoot });
|
|
3130
|
+
await exec("git", ["commit", "-m", "Auto-commit uncommitted changes before merge"], {
|
|
3131
|
+
cwd: projectRoot
|
|
3132
|
+
});
|
|
3133
|
+
} catch (e) {
|
|
3134
|
+
return c.json(
|
|
3135
|
+
{ error: "Failed to auto-commit main changes. Commit or stash manually before merging." },
|
|
3136
|
+
500
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
try {
|
|
3141
|
+
await exec("git", ["merge", variant.branch], { cwd: projectRoot });
|
|
3142
|
+
} catch (e) {
|
|
3143
|
+
return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
|
|
3144
|
+
}
|
|
3145
|
+
if (existsSync9(variant.path)) {
|
|
3146
|
+
try {
|
|
3147
|
+
await exec("git", ["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
3148
|
+
} catch {
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
try {
|
|
3152
|
+
await exec("git", ["branch", "-D", variant.branch], { cwd: projectRoot });
|
|
3153
|
+
} catch {
|
|
3154
|
+
}
|
|
3155
|
+
removeVariant(agentName, variantName);
|
|
3156
|
+
try {
|
|
3157
|
+
await exec("npm", ["install"], { cwd: projectRoot });
|
|
3158
|
+
} catch {
|
|
3159
|
+
}
|
|
3160
|
+
const manager = getAgentManager();
|
|
3161
|
+
const context = {
|
|
3162
|
+
type: "merged",
|
|
3163
|
+
name: variantName,
|
|
3164
|
+
...body.summary && { summary: body.summary },
|
|
3165
|
+
...body.justification && { justification: body.justification },
|
|
3166
|
+
...body.memory && { memory: body.memory }
|
|
3167
|
+
};
|
|
3168
|
+
let restartWarning;
|
|
3169
|
+
try {
|
|
3170
|
+
if (manager.isRunning(agentName)) {
|
|
3171
|
+
await manager.stopAgent(agentName);
|
|
3172
|
+
}
|
|
3173
|
+
manager.setPendingContext(agentName, context);
|
|
3174
|
+
await manager.startAgent(agentName);
|
|
3175
|
+
} catch (e) {
|
|
3176
|
+
restartWarning = `Merge succeeded but agent restart failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
3177
|
+
console.error(`[daemon] ${restartWarning}`);
|
|
3178
|
+
}
|
|
3179
|
+
return c.json({ ok: true, ...restartWarning && { warning: restartWarning } });
|
|
3180
|
+
}).delete("/:name/variants/:variant", requireAdmin, async (c) => {
|
|
3181
|
+
const agentName = c.req.param("name");
|
|
3182
|
+
const variantName = c.req.param("variant");
|
|
3183
|
+
const entry = findAgent(agentName);
|
|
3184
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
3185
|
+
const variant = findVariant(agentName, variantName);
|
|
3186
|
+
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
3187
|
+
const projectRoot = agentDir(agentName);
|
|
3188
|
+
const manager = getAgentManager();
|
|
3189
|
+
const compositeKey = `${agentName}@${variantName}`;
|
|
3190
|
+
if (manager.isRunning(compositeKey)) {
|
|
3191
|
+
try {
|
|
3192
|
+
await manager.stopAgent(compositeKey);
|
|
3193
|
+
} catch {
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
if (existsSync9(variant.path)) {
|
|
3197
|
+
try {
|
|
3198
|
+
await exec("git", ["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
3199
|
+
} catch {
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
try {
|
|
3203
|
+
await exec("git", ["branch", "-D", variant.branch], { cwd: projectRoot });
|
|
3204
|
+
} catch {
|
|
3205
|
+
}
|
|
3206
|
+
removeVariant(agentName, variantName);
|
|
3207
|
+
return c.json({ ok: true });
|
|
1691
3208
|
});
|
|
1692
|
-
var variants_default =
|
|
3209
|
+
var variants_default = app12;
|
|
1693
3210
|
|
|
1694
3211
|
// src/web/routes/volute/chat.ts
|
|
1695
|
-
import { readFileSync as
|
|
1696
|
-
import { resolve as
|
|
3212
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3213
|
+
import { resolve as resolve14 } from "path";
|
|
1697
3214
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
1698
|
-
import { Hono as
|
|
3215
|
+
import { Hono as Hono13 } from "hono";
|
|
1699
3216
|
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
1700
3217
|
import { z as z3 } from "zod";
|
|
1701
3218
|
|
|
@@ -1728,12 +3245,12 @@ function publish(conversationId, event) {
|
|
|
1728
3245
|
}
|
|
1729
3246
|
|
|
1730
3247
|
// src/lib/conversations.ts
|
|
1731
|
-
import { randomUUID } from "crypto";
|
|
1732
|
-
import { and as
|
|
3248
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3249
|
+
import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
|
|
1733
3250
|
async function createConversation(agentName, channel, opts) {
|
|
1734
|
-
const
|
|
1735
|
-
const id =
|
|
1736
|
-
await
|
|
3251
|
+
const db2 = await getDb();
|
|
3252
|
+
const id = randomUUID2();
|
|
3253
|
+
await db2.insert(conversations).values({
|
|
1737
3254
|
id,
|
|
1738
3255
|
agent_name: agentName,
|
|
1739
3256
|
channel,
|
|
@@ -1741,7 +3258,7 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1741
3258
|
title: opts?.title ?? null
|
|
1742
3259
|
});
|
|
1743
3260
|
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
1744
|
-
await
|
|
3261
|
+
await db2.insert(conversationParticipants).values(
|
|
1745
3262
|
opts.participantIds.map((uid, i) => ({
|
|
1746
3263
|
conversation_id: id,
|
|
1747
3264
|
user_id: uid,
|
|
@@ -1760,41 +3277,41 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1760
3277
|
};
|
|
1761
3278
|
}
|
|
1762
3279
|
async function getConversation(id) {
|
|
1763
|
-
const
|
|
1764
|
-
const row = await
|
|
3280
|
+
const db2 = await getDb();
|
|
3281
|
+
const row = await db2.select().from(conversations).where(eq4(conversations.id, id)).get();
|
|
1765
3282
|
return row ?? null;
|
|
1766
3283
|
}
|
|
1767
3284
|
async function getParticipants(conversationId) {
|
|
1768
|
-
const
|
|
1769
|
-
const rows = await
|
|
3285
|
+
const db2 = await getDb();
|
|
3286
|
+
const rows = await db2.select({
|
|
1770
3287
|
userId: conversationParticipants.user_id,
|
|
1771
3288
|
username: users.username,
|
|
1772
3289
|
userType: users.user_type,
|
|
1773
3290
|
role: conversationParticipants.role
|
|
1774
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
3291
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
|
|
1775
3292
|
return rows;
|
|
1776
3293
|
}
|
|
1777
3294
|
async function isParticipant(conversationId, userId) {
|
|
1778
|
-
const
|
|
1779
|
-
const row = await
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
3295
|
+
const db2 = await getDb();
|
|
3296
|
+
const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
3297
|
+
and3(
|
|
3298
|
+
eq4(conversationParticipants.conversation_id, conversationId),
|
|
3299
|
+
eq4(conversationParticipants.user_id, userId)
|
|
1783
3300
|
)
|
|
1784
3301
|
).get();
|
|
1785
3302
|
return row != null;
|
|
1786
3303
|
}
|
|
1787
3304
|
async function listConversationsForUser(userId) {
|
|
1788
|
-
const
|
|
1789
|
-
const participantRows = await
|
|
3305
|
+
const db2 = await getDb();
|
|
3306
|
+
const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
|
|
1790
3307
|
if (participantRows.length === 0) return [];
|
|
1791
3308
|
const convIds = participantRows.map((r) => r.conversation_id);
|
|
1792
|
-
return
|
|
3309
|
+
return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
|
|
1793
3310
|
}
|
|
1794
3311
|
async function isParticipantOrOwner(conversationId, userId) {
|
|
1795
3312
|
if (await isParticipant(conversationId, userId)) return true;
|
|
1796
|
-
const
|
|
1797
|
-
const row = await
|
|
3313
|
+
const db2 = await getDb();
|
|
3314
|
+
const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
|
|
1798
3315
|
return row != null;
|
|
1799
3316
|
}
|
|
1800
3317
|
async function deleteConversationForUser(id, userId) {
|
|
@@ -1803,15 +3320,15 @@ async function deleteConversationForUser(id, userId) {
|
|
|
1803
3320
|
return true;
|
|
1804
3321
|
}
|
|
1805
3322
|
async function addMessage(conversationId, role, senderName, content) {
|
|
1806
|
-
const
|
|
3323
|
+
const db2 = await getDb();
|
|
1807
3324
|
const serialized = JSON.stringify(content);
|
|
1808
|
-
const [result] = await
|
|
1809
|
-
await
|
|
3325
|
+
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 });
|
|
3326
|
+
await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
|
|
1810
3327
|
if (role === "user") {
|
|
1811
3328
|
const firstText = content.find((b) => b.type === "text");
|
|
1812
3329
|
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
1813
3330
|
if (title) {
|
|
1814
|
-
await
|
|
3331
|
+
await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
|
|
1815
3332
|
}
|
|
1816
3333
|
}
|
|
1817
3334
|
const msg = {
|
|
@@ -1833,8 +3350,8 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
1833
3350
|
return msg;
|
|
1834
3351
|
}
|
|
1835
3352
|
async function getMessages(conversationId) {
|
|
1836
|
-
const
|
|
1837
|
-
const rows = await
|
|
3353
|
+
const db2 = await getDb();
|
|
3354
|
+
const rows = await db2.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1838
3355
|
return rows.map((row) => {
|
|
1839
3356
|
let content;
|
|
1840
3357
|
try {
|
|
@@ -1849,15 +3366,15 @@ async function getMessages(conversationId) {
|
|
|
1849
3366
|
async function listConversationsWithParticipants(userId) {
|
|
1850
3367
|
const convs = await listConversationsForUser(userId);
|
|
1851
3368
|
if (convs.length === 0) return [];
|
|
1852
|
-
const
|
|
3369
|
+
const db2 = await getDb();
|
|
1853
3370
|
const convIds = convs.map((c) => c.id);
|
|
1854
|
-
const rows = await
|
|
3371
|
+
const rows = await db2.select({
|
|
1855
3372
|
conversationId: conversationParticipants.conversation_id,
|
|
1856
3373
|
userId: users.id,
|
|
1857
3374
|
username: users.username,
|
|
1858
3375
|
userType: users.user_type,
|
|
1859
3376
|
role: conversationParticipants.role
|
|
1860
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
3377
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
1861
3378
|
const byConv = /* @__PURE__ */ new Map();
|
|
1862
3379
|
for (const r of rows) {
|
|
1863
3380
|
let arr = byConv.get(r.conversationId);
|
|
@@ -1875,10 +3392,10 @@ async function listConversationsWithParticipants(userId) {
|
|
|
1875
3392
|
return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
|
|
1876
3393
|
}
|
|
1877
3394
|
async function findDMConversation(agentName, participantIds) {
|
|
1878
|
-
const
|
|
1879
|
-
const agentConvs = await
|
|
3395
|
+
const db2 = await getDb();
|
|
3396
|
+
const agentConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.agent_name, agentName)).all();
|
|
1880
3397
|
for (const conv of agentConvs) {
|
|
1881
|
-
const rows = await
|
|
3398
|
+
const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
|
|
1882
3399
|
if (rows.length !== 2) continue;
|
|
1883
3400
|
const ids = new Set(rows.map((r) => r.user_id));
|
|
1884
3401
|
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
@@ -1888,8 +3405,8 @@ async function findDMConversation(agentName, participantIds) {
|
|
|
1888
3405
|
return null;
|
|
1889
3406
|
}
|
|
1890
3407
|
async function deleteConversation(id) {
|
|
1891
|
-
const
|
|
1892
|
-
await
|
|
3408
|
+
const db2 = await getDb();
|
|
3409
|
+
await db2.delete(conversations).where(eq4(conversations.id, id));
|
|
1893
3410
|
}
|
|
1894
3411
|
|
|
1895
3412
|
// src/web/routes/volute/chat.ts
|
|
@@ -1906,7 +3423,7 @@ var chatSchema = z3.object({
|
|
|
1906
3423
|
});
|
|
1907
3424
|
function getDaemonUrl() {
|
|
1908
3425
|
try {
|
|
1909
|
-
const data = JSON.parse(
|
|
3426
|
+
const data = JSON.parse(readFileSync8(resolve14(voluteHome(), "daemon.json"), "utf-8"));
|
|
1910
3427
|
return `http://${daemonLoopback()}:${data.port}`;
|
|
1911
3428
|
} catch (err) {
|
|
1912
3429
|
throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
|
|
@@ -1922,7 +3439,7 @@ function daemonFetchInternal(path, body) {
|
|
|
1922
3439
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1923
3440
|
return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
|
|
1924
3441
|
}
|
|
1925
|
-
var
|
|
3442
|
+
var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), async (c) => {
|
|
1926
3443
|
const name = c.req.param("name");
|
|
1927
3444
|
const [baseName] = name.split("@", 2);
|
|
1928
3445
|
const entry = findAgent(baseName);
|
|
@@ -1984,7 +3501,7 @@ var app11 = new Hono11().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
1984
3501
|
const participants = await getParticipants(conversationId);
|
|
1985
3502
|
const agentParticipants = participants.filter((p) => p.userType === "agent");
|
|
1986
3503
|
const participantNames = participants.map((p) => p.username);
|
|
1987
|
-
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-
|
|
3504
|
+
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-MRHHKAB6.js");
|
|
1988
3505
|
const manager = getAgentManager2();
|
|
1989
3506
|
const runningAgents = agentParticipants.map((ap) => {
|
|
1990
3507
|
const agentKey = ap.username === baseName ? name : ap.username;
|
|
@@ -2020,8 +3537,8 @@ var app11 = new Hono11().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
2020
3537
|
const targetName = agentName === baseName ? name : agentName;
|
|
2021
3538
|
daemonFetchInternal(`/api/agents/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
|
|
2022
3539
|
if (!res.ok) {
|
|
2023
|
-
const
|
|
2024
|
-
console.error(`[chat] agent ${agentName} responded ${res.status}: ${
|
|
3540
|
+
const text2 = await res.text().catch(() => "");
|
|
3541
|
+
console.error(`[chat] agent ${agentName} responded ${res.status}: ${text2}`);
|
|
2025
3542
|
}
|
|
2026
3543
|
}).catch((err) => {
|
|
2027
3544
|
console.error(`[chat] agent ${agentName} unreachable via daemon:`, err);
|
|
@@ -2045,27 +3562,27 @@ var app11 = new Hono11().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
2045
3562
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
2046
3563
|
});
|
|
2047
3564
|
}, 15e3);
|
|
2048
|
-
await new Promise((
|
|
3565
|
+
await new Promise((resolve17) => {
|
|
2049
3566
|
stream.onAbort(() => {
|
|
2050
3567
|
unsubscribe();
|
|
2051
3568
|
clearInterval(keepAlive);
|
|
2052
|
-
|
|
3569
|
+
resolve17();
|
|
2053
3570
|
});
|
|
2054
3571
|
});
|
|
2055
3572
|
});
|
|
2056
3573
|
});
|
|
2057
|
-
var chat_default =
|
|
3574
|
+
var chat_default = app13;
|
|
2058
3575
|
|
|
2059
3576
|
// src/web/routes/volute/conversations.ts
|
|
2060
3577
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
2061
|
-
import { Hono as
|
|
3578
|
+
import { Hono as Hono14 } from "hono";
|
|
2062
3579
|
import { z as z4 } from "zod";
|
|
2063
3580
|
var createConvSchema = z4.object({
|
|
2064
3581
|
title: z4.string().optional(),
|
|
2065
3582
|
participantIds: z4.array(z4.number()).optional(),
|
|
2066
3583
|
participantNames: z4.array(z4.string()).optional()
|
|
2067
3584
|
});
|
|
2068
|
-
var
|
|
3585
|
+
var app14 = new Hono14().get("/:name/conversations", async (c) => {
|
|
2069
3586
|
const name = c.req.param("name");
|
|
2070
3587
|
const user = c.get("user");
|
|
2071
3588
|
let lookupId = user.id;
|
|
@@ -2150,17 +3667,17 @@ var app12 = new Hono12().get("/:name/conversations", async (c) => {
|
|
|
2150
3667
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
2151
3668
|
return c.json({ ok: true });
|
|
2152
3669
|
});
|
|
2153
|
-
var conversations_default =
|
|
3670
|
+
var conversations_default = app14;
|
|
2154
3671
|
|
|
2155
3672
|
// src/web/routes/volute/user-conversations.ts
|
|
2156
3673
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
2157
|
-
import { Hono as
|
|
3674
|
+
import { Hono as Hono15 } from "hono";
|
|
2158
3675
|
import { z as z5 } from "zod";
|
|
2159
3676
|
var createSchema = z5.object({
|
|
2160
3677
|
title: z5.string().optional(),
|
|
2161
3678
|
participantNames: z5.array(z5.string()).min(1)
|
|
2162
3679
|
});
|
|
2163
|
-
var
|
|
3680
|
+
var app15 = new Hono15().use("*", authMiddleware).get("/", async (c) => {
|
|
2164
3681
|
const user = c.get("user");
|
|
2165
3682
|
const convs = await listConversationsWithParticipants(user.id);
|
|
2166
3683
|
return c.json(convs);
|
|
@@ -2209,11 +3726,11 @@ var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
|
|
|
2209
3726
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
2210
3727
|
return c.json({ ok: true });
|
|
2211
3728
|
});
|
|
2212
|
-
var user_conversations_default =
|
|
3729
|
+
var user_conversations_default = app15;
|
|
2213
3730
|
|
|
2214
3731
|
// src/web/app.ts
|
|
2215
|
-
var
|
|
2216
|
-
|
|
3732
|
+
var app16 = new Hono16();
|
|
3733
|
+
app16.onError((err, c) => {
|
|
2217
3734
|
if (err instanceof HTTPException) {
|
|
2218
3735
|
return err.getResponse();
|
|
2219
3736
|
}
|
|
@@ -2224,10 +3741,10 @@ app14.onError((err, c) => {
|
|
|
2224
3741
|
});
|
|
2225
3742
|
return c.json({ error: "Internal server error" }, 500);
|
|
2226
3743
|
});
|
|
2227
|
-
|
|
3744
|
+
app16.notFound((c) => {
|
|
2228
3745
|
return c.json({ error: "Not found" }, 404);
|
|
2229
3746
|
});
|
|
2230
|
-
|
|
3747
|
+
app16.use("*", async (c, next) => {
|
|
2231
3748
|
const start = Date.now();
|
|
2232
3749
|
await next();
|
|
2233
3750
|
const duration = Date.now() - start;
|
|
@@ -2238,7 +3755,7 @@ app14.use("*", async (c, next) => {
|
|
|
2238
3755
|
duration
|
|
2239
3756
|
});
|
|
2240
3757
|
});
|
|
2241
|
-
|
|
3758
|
+
app16.get("/api/health", (c) => {
|
|
2242
3759
|
let version = "unknown";
|
|
2243
3760
|
let cached = null;
|
|
2244
3761
|
try {
|
|
@@ -2253,13 +3770,14 @@ app14.get("/api/health", (c) => {
|
|
|
2253
3770
|
...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
|
|
2254
3771
|
});
|
|
2255
3772
|
});
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
var
|
|
3773
|
+
app16.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
3774
|
+
app16.use("/api/*", csrf());
|
|
3775
|
+
app16.use("/api/agents/*", authMiddleware);
|
|
3776
|
+
app16.use("/api/conversations/*", authMiddleware);
|
|
3777
|
+
app16.use("/api/system/*", authMiddleware);
|
|
3778
|
+
app16.use("/api/env/*", authMiddleware);
|
|
3779
|
+
var routes = app16.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/agents", agents_default).route("/api/agents", chat_default).route("/api/agents", connectors_default).route("/api/agents", schedules_default).route("/api/agents", logs_default).route("/api/agents", typing_default).route("/api/agents", variants_default).route("/api/agents", files_default).route("/api/agents", channels_default).route("/api/agents", env_default).route("/api/agents", conversations_default).route("/api/env", sharedEnvApp).route("/api/conversations", user_conversations_default);
|
|
3780
|
+
var app_default = app16;
|
|
2263
3781
|
|
|
2264
3782
|
// src/web/server.ts
|
|
2265
3783
|
var MIME_TYPES = {
|
|
@@ -2276,20 +3794,20 @@ async function startServer({
|
|
|
2276
3794
|
hostname = "127.0.0.1"
|
|
2277
3795
|
}) {
|
|
2278
3796
|
let assetsDir = "";
|
|
2279
|
-
let searchDir =
|
|
3797
|
+
let searchDir = dirname4(new URL(import.meta.url).pathname);
|
|
2280
3798
|
for (let i = 0; i < 5; i++) {
|
|
2281
|
-
const candidate =
|
|
2282
|
-
if (
|
|
3799
|
+
const candidate = resolve15(searchDir, "dist", "web-assets");
|
|
3800
|
+
if (existsSync10(candidate)) {
|
|
2283
3801
|
assetsDir = candidate;
|
|
2284
3802
|
break;
|
|
2285
3803
|
}
|
|
2286
|
-
searchDir =
|
|
3804
|
+
searchDir = dirname4(searchDir);
|
|
2287
3805
|
}
|
|
2288
3806
|
if (assetsDir) {
|
|
2289
3807
|
app_default.get("*", async (c) => {
|
|
2290
3808
|
const urlPath = new URL(c.req.url).pathname;
|
|
2291
3809
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
2292
|
-
const filePath =
|
|
3810
|
+
const filePath = resolve15(assetsDir, urlPath.slice(1));
|
|
2293
3811
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
2294
3812
|
const s = await stat(filePath).catch(() => null);
|
|
2295
3813
|
if (s?.isFile()) {
|
|
@@ -2298,7 +3816,7 @@ async function startServer({
|
|
|
2298
3816
|
const body = await readFile2(filePath);
|
|
2299
3817
|
return c.body(body, 200, { "Content-Type": mime });
|
|
2300
3818
|
}
|
|
2301
|
-
const indexPath =
|
|
3819
|
+
const indexPath = resolve15(assetsDir, "index.html");
|
|
2302
3820
|
const indexStat = await stat(indexPath).catch(() => null);
|
|
2303
3821
|
if (indexStat?.isFile()) {
|
|
2304
3822
|
const body = await readFile2(indexPath, "utf-8");
|
|
@@ -2308,10 +3826,10 @@ async function startServer({
|
|
|
2308
3826
|
});
|
|
2309
3827
|
}
|
|
2310
3828
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
2311
|
-
await new Promise((
|
|
3829
|
+
await new Promise((resolve17, reject) => {
|
|
2312
3830
|
server.on("listening", () => {
|
|
2313
3831
|
logger_default.info("Volute UI running", { hostname, port });
|
|
2314
|
-
|
|
3832
|
+
resolve17();
|
|
2315
3833
|
});
|
|
2316
3834
|
server.on("error", (err) => {
|
|
2317
3835
|
reject(err);
|
|
@@ -2322,14 +3840,14 @@ async function startServer({
|
|
|
2322
3840
|
|
|
2323
3841
|
// src/daemon.ts
|
|
2324
3842
|
if (!process.env.VOLUTE_HOME) {
|
|
2325
|
-
process.env.VOLUTE_HOME =
|
|
3843
|
+
process.env.VOLUTE_HOME = resolve16(homedir2(), ".volute");
|
|
2326
3844
|
}
|
|
2327
3845
|
async function startDaemon(opts) {
|
|
2328
3846
|
const { port, hostname } = opts;
|
|
2329
3847
|
const myPid = String(process.pid);
|
|
2330
3848
|
const home = voluteHome();
|
|
2331
3849
|
if (!opts.foreground) {
|
|
2332
|
-
const log2 = new RotatingLog(
|
|
3850
|
+
const log2 = new RotatingLog(resolve16(home, "daemon.log"));
|
|
2333
3851
|
const write2 = (...args) => log2.write(`${format(...args)}
|
|
2334
3852
|
`);
|
|
2335
3853
|
console.log = write2;
|
|
@@ -2337,9 +3855,9 @@ async function startDaemon(opts) {
|
|
|
2337
3855
|
console.warn = write2;
|
|
2338
3856
|
console.info = write2;
|
|
2339
3857
|
}
|
|
2340
|
-
const DAEMON_PID_PATH =
|
|
2341
|
-
const DAEMON_JSON_PATH =
|
|
2342
|
-
|
|
3858
|
+
const DAEMON_PID_PATH = resolve16(home, "daemon.pid");
|
|
3859
|
+
const DAEMON_JSON_PATH = resolve16(home, "daemon.json");
|
|
3860
|
+
mkdirSync8(home, { recursive: true });
|
|
2343
3861
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
2344
3862
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
2345
3863
|
process.env.VOLUTE_DAEMON_PORT = String(port);
|
|
@@ -2355,10 +3873,10 @@ async function startDaemon(opts) {
|
|
|
2355
3873
|
}
|
|
2356
3874
|
throw err;
|
|
2357
3875
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
3876
|
+
writeFileSync7(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
3877
|
+
writeFileSync7(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
2360
3878
|
`, {
|
|
2361
|
-
mode:
|
|
3879
|
+
mode: 420
|
|
2362
3880
|
});
|
|
2363
3881
|
const manager = initAgentManager();
|
|
2364
3882
|
manager.loadCrashAttempts();
|
|
@@ -2410,13 +3928,13 @@ async function startDaemon(opts) {
|
|
|
2410
3928
|
console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
|
|
2411
3929
|
function cleanup() {
|
|
2412
3930
|
try {
|
|
2413
|
-
if (
|
|
3931
|
+
if (readFileSync9(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
2414
3932
|
unlinkSync2(DAEMON_PID_PATH);
|
|
2415
3933
|
}
|
|
2416
3934
|
} catch {
|
|
2417
3935
|
}
|
|
2418
3936
|
try {
|
|
2419
|
-
const data = JSON.parse(
|
|
3937
|
+
const data = JSON.parse(readFileSync9(DAEMON_JSON_PATH, "utf-8"));
|
|
2420
3938
|
if (data.token === token) {
|
|
2421
3939
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
2422
3940
|
}
|