volute 0.9.0 → 0.10.1
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-MB3OTRRK.js → agent-ECRX44DB.js} +5 -5
- package/dist/{agent-manager-CMMH5KQQ.js → agent-manager-4OCID725.js} +2 -2
- package/dist/{chunk-IQXBMFZG.js → chunk-46S7YHUB.js} +18 -6
- package/dist/{chunk-YNNK4QN2.js → chunk-FYQGANL6.js} +40 -2
- package/dist/{chunk-W6TMWYU3.js → chunk-KR6WRAJ4.js} +3 -3
- package/dist/chunk-R3VB7NF5.js +205 -0
- package/dist/cli.js +14 -14
- package/dist/create-VBZZNJOG.js +38 -0
- package/dist/daemon-restart-7X72OXOW.js +61 -0
- package/dist/daemon.js +1492 -170
- package/dist/delete-BOTVU4YO.js +35 -0
- package/dist/{down-4DGRZRJU.js → down-4LIQG3CE.js} +3 -1
- package/dist/import-2BZUWT23.js +21 -0
- package/dist/{package-WPX6LCYE.js → package-TNE337RE.js} +1 -1
- package/dist/{setup-7SPMWF2O.js → setup-6QFIHXSH.js} +5 -5
- package/dist/{up-J7AHQHIM.js → up-FCYL2IPZ.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-EKDXXHKH.js +0 -28
- package/dist/delete-WKQKE3FT.js +0 -70
- package/dist/import-CNEDF3TD.js +0 -532
- package/dist/upgrade-BRNMSQBX.js +0 -233
- package/dist/variant-AQRAN6FR.js +0 -493
- package/dist/{channel-G5D4VBXY.js → channel-2WHBRDTD.js} +3 -3
- package/dist/{chunk-AWHQZDB4.js → chunk-M5AEQLB3.js} +0 -0
- package/dist/{connector-PK7D5GTN.js → connector-L2HBLZBW.js} +3 -3
- package/dist/{env-HZMZSWWD.js → env-CGORIKVF.js} +3 -3
- package/dist/{history-SH25BAA5.js → history-NI5QP27M.js} +3 -3
- package/dist/{logs-V54B6QSG.js → logs-APWVWGNX.js} +3 -3
- package/dist/{schedule-XGBUF7NU.js → schedule-E4MFGYSA.js} +3 -3
- package/dist/{send-TFZ62XPZ.js → send-X6OQGSD6.js} +4 -4
- package/dist/{service-56CY4S6Z.js → service-OW35VZ5G.js} +3 -3
package/dist/daemon.js
CHANGED
|
@@ -6,16 +6,33 @@ import {
|
|
|
6
6
|
initAgentManager,
|
|
7
7
|
loadJsonMap,
|
|
8
8
|
saveJsonMap
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-KR6WRAJ4.js";
|
|
10
10
|
import {
|
|
11
11
|
checkForUpdate,
|
|
12
12
|
checkForUpdateCached,
|
|
13
13
|
getCurrentVersion
|
|
14
14
|
} from "./chunk-RT6Y7AR3.js";
|
|
15
15
|
import {
|
|
16
|
+
applyIsolation,
|
|
17
|
+
chownAgentDir,
|
|
18
|
+
createAgentUser,
|
|
19
|
+
deleteAgentUser,
|
|
20
|
+
ensureVoluteGroup,
|
|
21
|
+
getAgentUserIds,
|
|
22
|
+
isIsolationEnabled
|
|
23
|
+
} from "./chunk-46S7YHUB.js";
|
|
24
|
+
import {
|
|
25
|
+
exec,
|
|
26
|
+
resolveVoluteBin
|
|
27
|
+
} from "./chunk-5C5JWR2L.js";
|
|
28
|
+
import {
|
|
29
|
+
findOpenClawSession,
|
|
30
|
+
importOpenClawConnectors,
|
|
31
|
+
importPiSession,
|
|
32
|
+
parseNameFromIdentity,
|
|
16
33
|
readVoluteConfig,
|
|
17
34
|
writeVoluteConfig
|
|
18
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-R3VB7NF5.js";
|
|
19
36
|
import {
|
|
20
37
|
agentEnvPath,
|
|
21
38
|
loadMergedEnv,
|
|
@@ -27,59 +44,43 @@ import {
|
|
|
27
44
|
CHANNELS,
|
|
28
45
|
getChannelDriver
|
|
29
46
|
} from "./chunk-LIPPXNIE.js";
|
|
30
|
-
import
|
|
31
|
-
agentMessages,
|
|
32
|
-
approveUser,
|
|
33
|
-
conversationParticipants,
|
|
34
|
-
conversations,
|
|
35
|
-
createUser,
|
|
36
|
-
deleteAgentUser,
|
|
37
|
-
getDb,
|
|
38
|
-
getOrCreateAgentUser,
|
|
39
|
-
getUser,
|
|
40
|
-
getUserByUsername,
|
|
41
|
-
listPendingUsers,
|
|
42
|
-
listUsers,
|
|
43
|
-
listUsersByType,
|
|
44
|
-
messages,
|
|
45
|
-
sessions,
|
|
46
|
-
users,
|
|
47
|
-
verifyUser
|
|
48
|
-
} from "./chunk-ECPQXRLB.js";
|
|
47
|
+
import "./chunk-D424ZQGI.js";
|
|
49
48
|
import {
|
|
50
49
|
slugify,
|
|
51
50
|
writeChannelEntry
|
|
52
51
|
} from "./chunk-N6MLQ26B.js";
|
|
53
52
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
import {
|
|
57
|
-
resolveVoluteBin
|
|
58
|
-
} from "./chunk-5C5JWR2L.js";
|
|
59
|
-
import {
|
|
53
|
+
addAgent,
|
|
54
|
+
addVariant,
|
|
60
55
|
agentDir,
|
|
61
56
|
checkHealth,
|
|
62
57
|
daemonLoopback,
|
|
58
|
+
ensureVoluteHome,
|
|
63
59
|
findAgent,
|
|
64
60
|
findVariant,
|
|
65
61
|
getAllRunningVariants,
|
|
62
|
+
nextPort,
|
|
66
63
|
readRegistry,
|
|
67
64
|
readVariants,
|
|
68
65
|
removeAgent,
|
|
69
66
|
removeAllVariants,
|
|
67
|
+
removeVariant,
|
|
70
68
|
setAgentRunning,
|
|
71
69
|
setVariantRunning,
|
|
72
70
|
stateDir,
|
|
71
|
+
validateAgentName,
|
|
73
72
|
validateBranchName,
|
|
74
73
|
voluteHome
|
|
75
74
|
} from "./chunk-DP2DX4WV.js";
|
|
76
|
-
import
|
|
75
|
+
import {
|
|
76
|
+
__export
|
|
77
|
+
} from "./chunk-K3NQKI34.js";
|
|
77
78
|
|
|
78
79
|
// src/daemon.ts
|
|
79
80
|
import { randomBytes } from "crypto";
|
|
80
|
-
import { mkdirSync as
|
|
81
|
-
import { homedir } from "os";
|
|
82
|
-
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";
|
|
83
84
|
import { format } from "util";
|
|
84
85
|
|
|
85
86
|
// src/lib/connector-manager.ts
|
|
@@ -319,19 +320,19 @@ var ConnectorManager = class {
|
|
|
319
320
|
const stopKey = `${agentName}:${type}`;
|
|
320
321
|
this.stopping.add(stopKey);
|
|
321
322
|
agentMap.delete(type);
|
|
322
|
-
await new Promise((
|
|
323
|
-
tracked.child.on("exit", () =>
|
|
323
|
+
await new Promise((resolve17) => {
|
|
324
|
+
tracked.child.on("exit", () => resolve17());
|
|
324
325
|
try {
|
|
325
326
|
tracked.child.kill("SIGTERM");
|
|
326
327
|
} catch {
|
|
327
|
-
|
|
328
|
+
resolve17();
|
|
328
329
|
}
|
|
329
330
|
setTimeout(() => {
|
|
330
331
|
try {
|
|
331
332
|
tracked.child.kill("SIGKILL");
|
|
332
333
|
} catch {
|
|
333
334
|
}
|
|
334
|
-
|
|
335
|
+
resolve17();
|
|
335
336
|
}, 5e3);
|
|
336
337
|
});
|
|
337
338
|
this.stopping.delete(stopKey);
|
|
@@ -739,9 +740,248 @@ function getTokenBudget() {
|
|
|
739
740
|
|
|
740
741
|
// src/web/middleware/auth.ts
|
|
741
742
|
import { timingSafeEqual } from "crypto";
|
|
742
|
-
import { eq, lt } from "drizzle-orm";
|
|
743
|
+
import { eq as eq2, lt } from "drizzle-orm";
|
|
743
744
|
import { getCookie } from "hono/cookie";
|
|
744
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
|
|
745
985
|
function isValidDaemonToken(token) {
|
|
746
986
|
const expected = process.env.VOLUTE_DAEMON_TOKEN;
|
|
747
987
|
if (!expected || token.length !== expected.length) return false;
|
|
@@ -749,29 +989,29 @@ function isValidDaemonToken(token) {
|
|
|
749
989
|
}
|
|
750
990
|
var SESSION_MAX_AGE = 864e5;
|
|
751
991
|
async function createSession(userId) {
|
|
752
|
-
const
|
|
992
|
+
const db2 = await getDb();
|
|
753
993
|
const sessionId = crypto.randomUUID();
|
|
754
|
-
await
|
|
994
|
+
await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
|
|
755
995
|
return sessionId;
|
|
756
996
|
}
|
|
757
997
|
async function deleteSession(sessionId) {
|
|
758
|
-
const
|
|
759
|
-
await
|
|
998
|
+
const db2 = await getDb();
|
|
999
|
+
await db2.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
760
1000
|
}
|
|
761
1001
|
async function getSessionUserId(sessionId) {
|
|
762
|
-
const
|
|
763
|
-
const row = await
|
|
1002
|
+
const db2 = await getDb();
|
|
1003
|
+
const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
|
|
764
1004
|
if (!row) return void 0;
|
|
765
1005
|
if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
|
|
766
|
-
await
|
|
1006
|
+
await db2.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
767
1007
|
return void 0;
|
|
768
1008
|
}
|
|
769
1009
|
return row.userId;
|
|
770
1010
|
}
|
|
771
1011
|
async function cleanExpiredSessions() {
|
|
772
|
-
const
|
|
1012
|
+
const db2 = await getDb();
|
|
773
1013
|
const cutoff = Date.now() - SESSION_MAX_AGE;
|
|
774
|
-
await
|
|
1014
|
+
await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
|
|
775
1015
|
}
|
|
776
1016
|
var requireAdmin = createMiddleware(async (c, next) => {
|
|
777
1017
|
const user = c.get("user");
|
|
@@ -802,9 +1042,9 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
802
1042
|
});
|
|
803
1043
|
|
|
804
1044
|
// src/web/server.ts
|
|
805
|
-
import { existsSync as
|
|
1045
|
+
import { existsSync as existsSync10 } from "fs";
|
|
806
1046
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
807
|
-
import { dirname as
|
|
1047
|
+
import { dirname as dirname4, extname, resolve as resolve15 } from "path";
|
|
808
1048
|
import { serve } from "@hono/node-server";
|
|
809
1049
|
|
|
810
1050
|
// src/lib/log-buffer.ts
|
|
@@ -858,13 +1098,376 @@ import { csrf } from "hono/csrf";
|
|
|
858
1098
|
import { HTTPException } from "hono/http-exception";
|
|
859
1099
|
|
|
860
1100
|
// src/web/routes/agents.ts
|
|
861
|
-
import {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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";
|
|
866
1112
|
import { Hono } from "hono";
|
|
867
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
|
+
|
|
868
1471
|
// src/lib/typing.ts
|
|
869
1472
|
var DEFAULT_TTL_MS = 1e4;
|
|
870
1473
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -933,7 +1536,6 @@ function getTypingMap() {
|
|
|
933
1536
|
}
|
|
934
1537
|
|
|
935
1538
|
// src/web/routes/agents.ts
|
|
936
|
-
var execFileAsync = promisify(execFile);
|
|
937
1539
|
async function startAgentFull(name, baseName, variantName) {
|
|
938
1540
|
await getAgentManager().startAgent(name);
|
|
939
1541
|
if (variantName) return;
|
|
@@ -959,7 +1561,7 @@ function extractTextContent(content) {
|
|
|
959
1561
|
}
|
|
960
1562
|
function getDaemonPort() {
|
|
961
1563
|
try {
|
|
962
|
-
const data = JSON.parse(
|
|
1564
|
+
const data = JSON.parse(readFileSync6(resolve9(voluteHome(), "daemon.json"), "utf-8"));
|
|
963
1565
|
return data.port;
|
|
964
1566
|
} catch (err) {
|
|
965
1567
|
if (err?.code !== "ENOENT") {
|
|
@@ -998,7 +1600,247 @@ async function getAgentStatus(name, port) {
|
|
|
998
1600
|
}
|
|
999
1601
|
return { status, channels };
|
|
1000
1602
|
}
|
|
1001
|
-
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) => {
|
|
1002
1844
|
const entries = readRegistry();
|
|
1003
1845
|
const agents = await Promise.all(
|
|
1004
1846
|
entries.map(async (entry) => {
|
|
@@ -1011,7 +1853,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1011
1853
|
const name = c.req.param("name");
|
|
1012
1854
|
const entry = findAgent(name);
|
|
1013
1855
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1014
|
-
if (!
|
|
1856
|
+
if (!existsSync6(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
|
|
1015
1857
|
const { status, channels } = await getAgentStatus(name, entry.port);
|
|
1016
1858
|
const variants = readVariants(name);
|
|
1017
1859
|
const manager = getAgentManager();
|
|
@@ -1037,7 +1879,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1037
1879
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1038
1880
|
} else {
|
|
1039
1881
|
const dir = agentDir(baseName);
|
|
1040
|
-
if (!
|
|
1882
|
+
if (!existsSync6(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
1041
1883
|
}
|
|
1042
1884
|
if (getAgentManager().isRunning(name)) {
|
|
1043
1885
|
return c.json({ error: "Agent already running" }, 409);
|
|
@@ -1058,7 +1900,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1058
1900
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1059
1901
|
} else {
|
|
1060
1902
|
const dir = agentDir(baseName);
|
|
1061
|
-
if (!
|
|
1903
|
+
if (!existsSync6(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
1062
1904
|
}
|
|
1063
1905
|
let context;
|
|
1064
1906
|
const contentType = c.req.header("content-type");
|
|
@@ -1086,21 +1928,58 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1086
1928
|
return c.json({ error: `Invalid variant name: ${branchErr}` }, 400);
|
|
1087
1929
|
}
|
|
1088
1930
|
console.error(`[daemon] merging variant for ${baseName}: ${mergeVariantName}`);
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
+
}
|
|
1104
1983
|
}
|
|
1105
1984
|
if (context) {
|
|
1106
1985
|
manager.setPendingContext(name, context);
|
|
@@ -1148,15 +2027,144 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1148
2027
|
}
|
|
1149
2028
|
removeAllVariants(name);
|
|
1150
2029
|
removeAgent(name);
|
|
1151
|
-
await
|
|
2030
|
+
await deleteAgentUser2(name);
|
|
1152
2031
|
const state = stateDir(name);
|
|
1153
|
-
if (
|
|
1154
|
-
|
|
2032
|
+
if (existsSync6(state)) {
|
|
2033
|
+
rmSync2(state, { recursive: true, force: true });
|
|
1155
2034
|
}
|
|
1156
|
-
if (force &&
|
|
1157
|
-
|
|
2035
|
+
if (force && existsSync6(dir)) {
|
|
2036
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
2037
|
+
deleteAgentUser(name);
|
|
1158
2038
|
}
|
|
1159
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
|
+
}
|
|
1160
2168
|
}).post("/:name/message", async (c) => {
|
|
1161
2169
|
const name = c.req.param("name");
|
|
1162
2170
|
const [baseName, variantName] = name.split("@", 2);
|
|
@@ -1179,12 +2187,12 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1179
2187
|
console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
|
|
1180
2188
|
}
|
|
1181
2189
|
const channel = parsed?.channel ?? "unknown";
|
|
1182
|
-
const
|
|
2190
|
+
const db2 = await getDb();
|
|
1183
2191
|
if (parsed) {
|
|
1184
2192
|
try {
|
|
1185
2193
|
const sender2 = parsed.sender ?? null;
|
|
1186
2194
|
const content = extractTextContent(parsed.content);
|
|
1187
|
-
await
|
|
2195
|
+
await db2.insert(agentMessages).values({
|
|
1188
2196
|
agent: baseName,
|
|
1189
2197
|
channel,
|
|
1190
2198
|
sender: sender2,
|
|
@@ -1237,8 +2245,8 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1237
2245
|
body: forwardBody
|
|
1238
2246
|
});
|
|
1239
2247
|
if (!res.ok) {
|
|
1240
|
-
const
|
|
1241
|
-
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}`);
|
|
1242
2250
|
return c.json({ error: `Agent responded with ${res.status}` }, res.status);
|
|
1243
2251
|
}
|
|
1244
2252
|
let result;
|
|
@@ -1277,9 +2285,9 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1277
2285
|
if (!body.channel || !body.content) {
|
|
1278
2286
|
return c.json({ error: "channel and content required" }, 400);
|
|
1279
2287
|
}
|
|
1280
|
-
const
|
|
2288
|
+
const db2 = await getDb();
|
|
1281
2289
|
try {
|
|
1282
|
-
await
|
|
2290
|
+
await db2.insert(agentMessages).values({
|
|
1283
2291
|
agent: baseName,
|
|
1284
2292
|
channel: body.channel,
|
|
1285
2293
|
sender: body.sender ?? baseName,
|
|
@@ -1292,20 +2300,20 @@ var app = new Hono().get("/", async (c) => {
|
|
|
1292
2300
|
return c.json({ ok: true });
|
|
1293
2301
|
}).get("/:name/history/channels", async (c) => {
|
|
1294
2302
|
const name = c.req.param("name");
|
|
1295
|
-
const
|
|
1296
|
-
const rows = await
|
|
2303
|
+
const db2 = await getDb();
|
|
2304
|
+
const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq3(agentMessages.agent, name));
|
|
1297
2305
|
return c.json(rows.map((r) => r.channel));
|
|
1298
2306
|
}).get("/:name/history", async (c) => {
|
|
1299
2307
|
const name = c.req.param("name");
|
|
1300
2308
|
const channel = c.req.query("channel");
|
|
1301
2309
|
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
1302
2310
|
const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
|
|
1303
|
-
const
|
|
1304
|
-
const conditions = [
|
|
2311
|
+
const db2 = await getDb();
|
|
2312
|
+
const conditions = [eq3(agentMessages.agent, name)];
|
|
1305
2313
|
if (channel) {
|
|
1306
|
-
conditions.push(
|
|
2314
|
+
conditions.push(eq3(agentMessages.channel, channel));
|
|
1307
2315
|
}
|
|
1308
|
-
const rows = await
|
|
2316
|
+
const rows = await db2.select().from(agentMessages).where(and2(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
|
|
1309
2317
|
return c.json(rows);
|
|
1310
2318
|
});
|
|
1311
2319
|
var agents_default = app;
|
|
@@ -1624,9 +2632,9 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
|
|
|
1624
2632
|
var env_default = app5;
|
|
1625
2633
|
|
|
1626
2634
|
// src/web/routes/files.ts
|
|
1627
|
-
import { existsSync as
|
|
2635
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1628
2636
|
import { readdir, readFile } from "fs/promises";
|
|
1629
|
-
import { resolve as
|
|
2637
|
+
import { resolve as resolve10 } from "path";
|
|
1630
2638
|
import { Hono as Hono6 } from "hono";
|
|
1631
2639
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
1632
2640
|
var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
@@ -1634,8 +2642,8 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1634
2642
|
const entry = findAgent(name);
|
|
1635
2643
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1636
2644
|
const dir = agentDir(name);
|
|
1637
|
-
const homeDir =
|
|
1638
|
-
if (!
|
|
2645
|
+
const homeDir = resolve10(dir, "home");
|
|
2646
|
+
if (!existsSync7(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1639
2647
|
const allFiles = await readdir(homeDir);
|
|
1640
2648
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1641
2649
|
return c.json(files);
|
|
@@ -1648,8 +2656,8 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1648
2656
|
const entry = findAgent(name);
|
|
1649
2657
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1650
2658
|
const dir = agentDir(name);
|
|
1651
|
-
const filePath =
|
|
1652
|
-
if (!
|
|
2659
|
+
const filePath = resolve10(dir, "home", filename);
|
|
2660
|
+
if (!existsSync7(filePath)) {
|
|
1653
2661
|
return c.json({ error: "File not found" }, 404);
|
|
1654
2662
|
}
|
|
1655
2663
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -1659,16 +2667,16 @@ var files_default = app6;
|
|
|
1659
2667
|
|
|
1660
2668
|
// src/web/routes/logs.ts
|
|
1661
2669
|
import { spawn as spawn2 } from "child_process";
|
|
1662
|
-
import { existsSync as
|
|
1663
|
-
import { resolve as
|
|
2670
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2671
|
+
import { resolve as resolve11 } from "path";
|
|
1664
2672
|
import { Hono as Hono7 } from "hono";
|
|
1665
2673
|
import { streamSSE } from "hono/streaming";
|
|
1666
2674
|
var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
1667
2675
|
const name = c.req.param("name");
|
|
1668
2676
|
const entry = findAgent(name);
|
|
1669
2677
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1670
|
-
const logFile =
|
|
1671
|
-
if (!
|
|
2678
|
+
const logFile = resolve11(stateDir(name), "logs", "agent.log");
|
|
2679
|
+
if (!existsSync8(logFile)) {
|
|
1672
2680
|
return c.json({ error: "No log file found" }, 404);
|
|
1673
2681
|
}
|
|
1674
2682
|
return streamSSE(c, async (stream) => {
|
|
@@ -1686,17 +2694,17 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
1686
2694
|
stream.onAbort(() => {
|
|
1687
2695
|
tail.kill();
|
|
1688
2696
|
});
|
|
1689
|
-
await new Promise((
|
|
1690
|
-
tail.on("exit",
|
|
1691
|
-
stream.onAbort(
|
|
2697
|
+
await new Promise((resolve17) => {
|
|
2698
|
+
tail.on("exit", resolve17);
|
|
2699
|
+
stream.onAbort(resolve17);
|
|
1692
2700
|
});
|
|
1693
2701
|
});
|
|
1694
2702
|
}).get("/:name/logs/tail", async (c) => {
|
|
1695
2703
|
const name = c.req.param("name");
|
|
1696
2704
|
const entry = findAgent(name);
|
|
1697
2705
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1698
|
-
const logFile =
|
|
1699
|
-
if (!
|
|
2706
|
+
const logFile = resolve11(stateDir(name), "logs", "agent.log");
|
|
2707
|
+
if (!existsSync8(logFile)) {
|
|
1700
2708
|
return c.json({ error: "No log file found" }, 404);
|
|
1701
2709
|
}
|
|
1702
2710
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
@@ -1706,8 +2714,8 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
1706
2714
|
tail.stdout.on("data", (data) => {
|
|
1707
2715
|
output += data.toString();
|
|
1708
2716
|
});
|
|
1709
|
-
await new Promise((
|
|
1710
|
-
tail.on("exit",
|
|
2717
|
+
await new Promise((resolve17) => {
|
|
2718
|
+
tail.on("exit", resolve17);
|
|
1711
2719
|
});
|
|
1712
2720
|
return c.text(output);
|
|
1713
2721
|
});
|
|
@@ -1798,7 +2806,13 @@ var schedules_default = app8;
|
|
|
1798
2806
|
// src/web/routes/system.ts
|
|
1799
2807
|
import { Hono as Hono9 } from "hono";
|
|
1800
2808
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
1801
|
-
var app9 = new Hono9().
|
|
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) => {
|
|
1802
2816
|
const user = c.get("user");
|
|
1803
2817
|
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
1804
2818
|
return streamSSE2(c, async (stream) => {
|
|
@@ -1809,10 +2823,10 @@ var app9 = new Hono9().get("/logs", async (c) => {
|
|
|
1809
2823
|
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
1810
2824
|
});
|
|
1811
2825
|
});
|
|
1812
|
-
await new Promise((
|
|
2826
|
+
await new Promise((resolve17) => {
|
|
1813
2827
|
stream.onAbort(() => {
|
|
1814
2828
|
unsubscribe();
|
|
1815
|
-
|
|
2829
|
+
resolve17();
|
|
1816
2830
|
});
|
|
1817
2831
|
});
|
|
1818
2832
|
});
|
|
@@ -1869,7 +2883,124 @@ var app11 = new Hono11().get("/update", async (c) => {
|
|
|
1869
2883
|
var update_default = app11;
|
|
1870
2884
|
|
|
1871
2885
|
// src/web/routes/variants.ts
|
|
2886
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
2887
|
+
import { resolve as resolve13 } from "path";
|
|
1872
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
|
|
1873
3004
|
var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
1874
3005
|
const name = c.req.param("name");
|
|
1875
3006
|
const entry = findAgent(name);
|
|
@@ -1883,12 +3014,203 @@ var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
|
1883
3014
|
})
|
|
1884
3015
|
);
|
|
1885
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 });
|
|
1886
3208
|
});
|
|
1887
3209
|
var variants_default = app12;
|
|
1888
3210
|
|
|
1889
3211
|
// src/web/routes/volute/chat.ts
|
|
1890
|
-
import { readFileSync as
|
|
1891
|
-
import { resolve as
|
|
3212
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3213
|
+
import { resolve as resolve14 } from "path";
|
|
1892
3214
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
1893
3215
|
import { Hono as Hono13 } from "hono";
|
|
1894
3216
|
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
@@ -1923,12 +3245,12 @@ function publish(conversationId, event) {
|
|
|
1923
3245
|
}
|
|
1924
3246
|
|
|
1925
3247
|
// src/lib/conversations.ts
|
|
1926
|
-
import { randomUUID } from "crypto";
|
|
1927
|
-
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";
|
|
1928
3250
|
async function createConversation(agentName, channel, opts) {
|
|
1929
|
-
const
|
|
1930
|
-
const id =
|
|
1931
|
-
await
|
|
3251
|
+
const db2 = await getDb();
|
|
3252
|
+
const id = randomUUID2();
|
|
3253
|
+
await db2.insert(conversations).values({
|
|
1932
3254
|
id,
|
|
1933
3255
|
agent_name: agentName,
|
|
1934
3256
|
channel,
|
|
@@ -1936,7 +3258,7 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1936
3258
|
title: opts?.title ?? null
|
|
1937
3259
|
});
|
|
1938
3260
|
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
1939
|
-
await
|
|
3261
|
+
await db2.insert(conversationParticipants).values(
|
|
1940
3262
|
opts.participantIds.map((uid, i) => ({
|
|
1941
3263
|
conversation_id: id,
|
|
1942
3264
|
user_id: uid,
|
|
@@ -1955,41 +3277,41 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1955
3277
|
};
|
|
1956
3278
|
}
|
|
1957
3279
|
async function getConversation(id) {
|
|
1958
|
-
const
|
|
1959
|
-
const row = await
|
|
3280
|
+
const db2 = await getDb();
|
|
3281
|
+
const row = await db2.select().from(conversations).where(eq4(conversations.id, id)).get();
|
|
1960
3282
|
return row ?? null;
|
|
1961
3283
|
}
|
|
1962
3284
|
async function getParticipants(conversationId) {
|
|
1963
|
-
const
|
|
1964
|
-
const rows = await
|
|
3285
|
+
const db2 = await getDb();
|
|
3286
|
+
const rows = await db2.select({
|
|
1965
3287
|
userId: conversationParticipants.user_id,
|
|
1966
3288
|
username: users.username,
|
|
1967
3289
|
userType: users.user_type,
|
|
1968
3290
|
role: conversationParticipants.role
|
|
1969
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
3291
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
|
|
1970
3292
|
return rows;
|
|
1971
3293
|
}
|
|
1972
3294
|
async function isParticipant(conversationId, userId) {
|
|
1973
|
-
const
|
|
1974
|
-
const row = await
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
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)
|
|
1978
3300
|
)
|
|
1979
3301
|
).get();
|
|
1980
3302
|
return row != null;
|
|
1981
3303
|
}
|
|
1982
3304
|
async function listConversationsForUser(userId) {
|
|
1983
|
-
const
|
|
1984
|
-
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();
|
|
1985
3307
|
if (participantRows.length === 0) return [];
|
|
1986
3308
|
const convIds = participantRows.map((r) => r.conversation_id);
|
|
1987
|
-
return
|
|
3309
|
+
return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
|
|
1988
3310
|
}
|
|
1989
3311
|
async function isParticipantOrOwner(conversationId, userId) {
|
|
1990
3312
|
if (await isParticipant(conversationId, userId)) return true;
|
|
1991
|
-
const
|
|
1992
|
-
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();
|
|
1993
3315
|
return row != null;
|
|
1994
3316
|
}
|
|
1995
3317
|
async function deleteConversationForUser(id, userId) {
|
|
@@ -1998,15 +3320,15 @@ async function deleteConversationForUser(id, userId) {
|
|
|
1998
3320
|
return true;
|
|
1999
3321
|
}
|
|
2000
3322
|
async function addMessage(conversationId, role, senderName, content) {
|
|
2001
|
-
const
|
|
3323
|
+
const db2 = await getDb();
|
|
2002
3324
|
const serialized = JSON.stringify(content);
|
|
2003
|
-
const [result] = await
|
|
2004
|
-
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));
|
|
2005
3327
|
if (role === "user") {
|
|
2006
3328
|
const firstText = content.find((b) => b.type === "text");
|
|
2007
3329
|
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
2008
3330
|
if (title) {
|
|
2009
|
-
await
|
|
3331
|
+
await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
|
|
2010
3332
|
}
|
|
2011
3333
|
}
|
|
2012
3334
|
const msg = {
|
|
@@ -2028,8 +3350,8 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
2028
3350
|
return msg;
|
|
2029
3351
|
}
|
|
2030
3352
|
async function getMessages(conversationId) {
|
|
2031
|
-
const
|
|
2032
|
-
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();
|
|
2033
3355
|
return rows.map((row) => {
|
|
2034
3356
|
let content;
|
|
2035
3357
|
try {
|
|
@@ -2044,15 +3366,15 @@ async function getMessages(conversationId) {
|
|
|
2044
3366
|
async function listConversationsWithParticipants(userId) {
|
|
2045
3367
|
const convs = await listConversationsForUser(userId);
|
|
2046
3368
|
if (convs.length === 0) return [];
|
|
2047
|
-
const
|
|
3369
|
+
const db2 = await getDb();
|
|
2048
3370
|
const convIds = convs.map((c) => c.id);
|
|
2049
|
-
const rows = await
|
|
3371
|
+
const rows = await db2.select({
|
|
2050
3372
|
conversationId: conversationParticipants.conversation_id,
|
|
2051
3373
|
userId: users.id,
|
|
2052
3374
|
username: users.username,
|
|
2053
3375
|
userType: users.user_type,
|
|
2054
3376
|
role: conversationParticipants.role
|
|
2055
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
3377
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
2056
3378
|
const byConv = /* @__PURE__ */ new Map();
|
|
2057
3379
|
for (const r of rows) {
|
|
2058
3380
|
let arr = byConv.get(r.conversationId);
|
|
@@ -2070,10 +3392,10 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2070
3392
|
return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
|
|
2071
3393
|
}
|
|
2072
3394
|
async function findDMConversation(agentName, participantIds) {
|
|
2073
|
-
const
|
|
2074
|
-
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();
|
|
2075
3397
|
for (const conv of agentConvs) {
|
|
2076
|
-
const rows = await
|
|
3398
|
+
const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
|
|
2077
3399
|
if (rows.length !== 2) continue;
|
|
2078
3400
|
const ids = new Set(rows.map((r) => r.user_id));
|
|
2079
3401
|
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
@@ -2083,8 +3405,8 @@ async function findDMConversation(agentName, participantIds) {
|
|
|
2083
3405
|
return null;
|
|
2084
3406
|
}
|
|
2085
3407
|
async function deleteConversation(id) {
|
|
2086
|
-
const
|
|
2087
|
-
await
|
|
3408
|
+
const db2 = await getDb();
|
|
3409
|
+
await db2.delete(conversations).where(eq4(conversations.id, id));
|
|
2088
3410
|
}
|
|
2089
3411
|
|
|
2090
3412
|
// src/web/routes/volute/chat.ts
|
|
@@ -2101,7 +3423,7 @@ var chatSchema = z3.object({
|
|
|
2101
3423
|
});
|
|
2102
3424
|
function getDaemonUrl() {
|
|
2103
3425
|
try {
|
|
2104
|
-
const data = JSON.parse(
|
|
3426
|
+
const data = JSON.parse(readFileSync8(resolve14(voluteHome(), "daemon.json"), "utf-8"));
|
|
2105
3427
|
return `http://${daemonLoopback()}:${data.port}`;
|
|
2106
3428
|
} catch (err) {
|
|
2107
3429
|
throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
|
|
@@ -2179,7 +3501,7 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
2179
3501
|
const participants = await getParticipants(conversationId);
|
|
2180
3502
|
const agentParticipants = participants.filter((p) => p.userType === "agent");
|
|
2181
3503
|
const participantNames = participants.map((p) => p.username);
|
|
2182
|
-
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-
|
|
3504
|
+
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-4OCID725.js");
|
|
2183
3505
|
const manager = getAgentManager2();
|
|
2184
3506
|
const runningAgents = agentParticipants.map((ap) => {
|
|
2185
3507
|
const agentKey = ap.username === baseName ? name : ap.username;
|
|
@@ -2215,8 +3537,8 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
2215
3537
|
const targetName = agentName === baseName ? name : agentName;
|
|
2216
3538
|
daemonFetchInternal(`/api/agents/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
|
|
2217
3539
|
if (!res.ok) {
|
|
2218
|
-
const
|
|
2219
|
-
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}`);
|
|
2220
3542
|
}
|
|
2221
3543
|
}).catch((err) => {
|
|
2222
3544
|
console.error(`[chat] agent ${agentName} unreachable via daemon:`, err);
|
|
@@ -2240,11 +3562,11 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
2240
3562
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
2241
3563
|
});
|
|
2242
3564
|
}, 15e3);
|
|
2243
|
-
await new Promise((
|
|
3565
|
+
await new Promise((resolve17) => {
|
|
2244
3566
|
stream.onAbort(() => {
|
|
2245
3567
|
unsubscribe();
|
|
2246
3568
|
clearInterval(keepAlive);
|
|
2247
|
-
|
|
3569
|
+
resolve17();
|
|
2248
3570
|
});
|
|
2249
3571
|
});
|
|
2250
3572
|
});
|
|
@@ -2472,20 +3794,20 @@ async function startServer({
|
|
|
2472
3794
|
hostname = "127.0.0.1"
|
|
2473
3795
|
}) {
|
|
2474
3796
|
let assetsDir = "";
|
|
2475
|
-
let searchDir =
|
|
3797
|
+
let searchDir = dirname4(new URL(import.meta.url).pathname);
|
|
2476
3798
|
for (let i = 0; i < 5; i++) {
|
|
2477
|
-
const candidate =
|
|
2478
|
-
if (
|
|
3799
|
+
const candidate = resolve15(searchDir, "dist", "web-assets");
|
|
3800
|
+
if (existsSync10(candidate)) {
|
|
2479
3801
|
assetsDir = candidate;
|
|
2480
3802
|
break;
|
|
2481
3803
|
}
|
|
2482
|
-
searchDir =
|
|
3804
|
+
searchDir = dirname4(searchDir);
|
|
2483
3805
|
}
|
|
2484
3806
|
if (assetsDir) {
|
|
2485
3807
|
app_default.get("*", async (c) => {
|
|
2486
3808
|
const urlPath = new URL(c.req.url).pathname;
|
|
2487
3809
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
2488
|
-
const filePath =
|
|
3810
|
+
const filePath = resolve15(assetsDir, urlPath.slice(1));
|
|
2489
3811
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
2490
3812
|
const s = await stat(filePath).catch(() => null);
|
|
2491
3813
|
if (s?.isFile()) {
|
|
@@ -2494,7 +3816,7 @@ async function startServer({
|
|
|
2494
3816
|
const body = await readFile2(filePath);
|
|
2495
3817
|
return c.body(body, 200, { "Content-Type": mime });
|
|
2496
3818
|
}
|
|
2497
|
-
const indexPath =
|
|
3819
|
+
const indexPath = resolve15(assetsDir, "index.html");
|
|
2498
3820
|
const indexStat = await stat(indexPath).catch(() => null);
|
|
2499
3821
|
if (indexStat?.isFile()) {
|
|
2500
3822
|
const body = await readFile2(indexPath, "utf-8");
|
|
@@ -2504,10 +3826,10 @@ async function startServer({
|
|
|
2504
3826
|
});
|
|
2505
3827
|
}
|
|
2506
3828
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
2507
|
-
await new Promise((
|
|
3829
|
+
await new Promise((resolve17, reject) => {
|
|
2508
3830
|
server.on("listening", () => {
|
|
2509
3831
|
logger_default.info("Volute UI running", { hostname, port });
|
|
2510
|
-
|
|
3832
|
+
resolve17();
|
|
2511
3833
|
});
|
|
2512
3834
|
server.on("error", (err) => {
|
|
2513
3835
|
reject(err);
|
|
@@ -2518,14 +3840,14 @@ async function startServer({
|
|
|
2518
3840
|
|
|
2519
3841
|
// src/daemon.ts
|
|
2520
3842
|
if (!process.env.VOLUTE_HOME) {
|
|
2521
|
-
process.env.VOLUTE_HOME =
|
|
3843
|
+
process.env.VOLUTE_HOME = resolve16(homedir2(), ".volute");
|
|
2522
3844
|
}
|
|
2523
3845
|
async function startDaemon(opts) {
|
|
2524
3846
|
const { port, hostname } = opts;
|
|
2525
3847
|
const myPid = String(process.pid);
|
|
2526
3848
|
const home = voluteHome();
|
|
2527
3849
|
if (!opts.foreground) {
|
|
2528
|
-
const log2 = new RotatingLog(
|
|
3850
|
+
const log2 = new RotatingLog(resolve16(home, "daemon.log"));
|
|
2529
3851
|
const write2 = (...args) => log2.write(`${format(...args)}
|
|
2530
3852
|
`);
|
|
2531
3853
|
console.log = write2;
|
|
@@ -2533,9 +3855,9 @@ async function startDaemon(opts) {
|
|
|
2533
3855
|
console.warn = write2;
|
|
2534
3856
|
console.info = write2;
|
|
2535
3857
|
}
|
|
2536
|
-
const DAEMON_PID_PATH =
|
|
2537
|
-
const DAEMON_JSON_PATH =
|
|
2538
|
-
|
|
3858
|
+
const DAEMON_PID_PATH = resolve16(home, "daemon.pid");
|
|
3859
|
+
const DAEMON_JSON_PATH = resolve16(home, "daemon.json");
|
|
3860
|
+
mkdirSync8(home, { recursive: true });
|
|
2539
3861
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
2540
3862
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
2541
3863
|
process.env.VOLUTE_DAEMON_PORT = String(port);
|
|
@@ -2551,8 +3873,8 @@ async function startDaemon(opts) {
|
|
|
2551
3873
|
}
|
|
2552
3874
|
throw err;
|
|
2553
3875
|
}
|
|
2554
|
-
|
|
2555
|
-
|
|
3876
|
+
writeFileSync7(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
3877
|
+
writeFileSync7(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
2556
3878
|
`, {
|
|
2557
3879
|
mode: 420
|
|
2558
3880
|
});
|
|
@@ -2606,13 +3928,13 @@ async function startDaemon(opts) {
|
|
|
2606
3928
|
console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
|
|
2607
3929
|
function cleanup() {
|
|
2608
3930
|
try {
|
|
2609
|
-
if (
|
|
3931
|
+
if (readFileSync9(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
2610
3932
|
unlinkSync2(DAEMON_PID_PATH);
|
|
2611
3933
|
}
|
|
2612
3934
|
} catch {
|
|
2613
3935
|
}
|
|
2614
3936
|
try {
|
|
2615
|
-
const data = JSON.parse(
|
|
3937
|
+
const data = JSON.parse(readFileSync9(DAEMON_JSON_PATH, "utf-8"));
|
|
2616
3938
|
if (data.token === token) {
|
|
2617
3939
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
2618
3940
|
}
|