volute 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/{activity-events-OMXKXD5N.js → activity-events-3WHHCOBB.js} +3 -4
- package/dist/{archive-ZCFOSTKB.js → archive-4ZQYK5MN.js} +4 -2
- package/dist/auth-HM2RSPY7.js +37 -0
- package/dist/{channel-PUQKGSQM.js → channel-BOOMFULW.js} +2 -2
- package/dist/{chunk-IKMY5X76.js → chunk-5462YKWP.js} +12 -9
- package/dist/{chunk-PUVXOZ6T.js → chunk-7LPTHFIL.js} +63 -64
- package/dist/{chunk-UU7A7KLB.js → chunk-A4S7H6G6.js} +5 -7
- package/dist/chunk-AKPFNL7L.js +148 -0
- package/dist/{chunk-EBGCNDMM.js → chunk-B2CPS4QU.js} +128 -114
- package/dist/{chunk-FCDU5BFX.js → chunk-HFCBO2GL.js} +2 -2
- package/dist/{chunk-GZ7DW4YL.js → chunk-HGCDWKSP.js} +2 -2
- package/dist/{chunk-DYZGP3EW.js → chunk-IPJXU366.js} +1 -1
- package/dist/{chunk-7UFKREVW.js → chunk-J5A3DF2U.js} +2 -2
- package/dist/{chunk-WC6ZHVRL.js → chunk-KFI7TQJ6.js} +2 -2
- package/dist/{chunk-AW7P4EVV.js → chunk-KTJGZ7M7.js} +55 -7
- package/dist/{chunk-TIWH32HP.js → chunk-L3LHXZD7.js} +3 -3
- package/dist/{chunk-OGXOMR65.js → chunk-NWPT4ASZ.js} +1 -1
- package/dist/{chunk-FGSYHIS3.js → chunk-OGZYB5GL.js} +252 -296
- package/dist/{chunk-SCUDS4US.js → chunk-ON3FF5JA.js} +1 -1
- package/dist/{chunk-O6ASDHFO.js → chunk-PC6R6UUW.js} +4 -4
- package/dist/{chunk-VDWCHYTS.js → chunk-PHU4DEAJ.js} +1 -1
- package/dist/{chunk-7NO7EV5Z.js → chunk-Q7AITQ44.js} +2 -2
- package/dist/{chunk-32VR2EOH.js → chunk-QUJUKM4U.js} +2 -2
- package/dist/{chunk-NSE7VJQA.js → chunk-SGPEZ32F.js} +29 -1
- package/dist/{chunk-RHEGSQFJ.js → chunk-WSLPZF72.js} +1 -1
- package/dist/cli.js +57 -119
- package/dist/{connector-JBVNZ7VK.js → connector-PYT5UOTZ.js} +6 -6
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/slack.js +2 -2
- package/dist/connectors/telegram.js +2 -2
- package/dist/{create-HP4OVVHF.js → create-WIDA3M4C.js} +1 -1
- package/dist/{daemon-client-ITWUCNFO.js → daemon-client-ZHCDL4RS.js} +2 -2
- package/dist/{daemon-restart-KPSWNYTH.js → daemon-restart-BH67ZOTE.js} +6 -6
- package/dist/daemon.js +1538 -687
- package/dist/{delete-BSU7K3RY.js → delete-LOIANQGD.js} +1 -1
- package/dist/down-LIOQ5JDH.js +14 -0
- package/dist/{env-A3LMO777.js → env-4PHIHTF4.js} +2 -2
- package/dist/{export-6QBUOQGC.js → export-XD6PJBQP.js} +19 -8
- package/dist/{file-C57SK5DK.js → file-X4L5TTOL.js} +2 -2
- package/dist/{history-WNK3DFUM.js → history-HTEKRNID.js} +2 -2
- package/dist/{import-XEC34Y4Z.js → import-E433B4KG.js} +3 -3
- package/dist/{log-PPPZDVEF.js → log-SRO5Q6AD.js} +2 -2
- package/dist/{login-HNH3EUQV.js → login-UO6AOVEA.js} +4 -4
- package/dist/{logout-I5CB5UZS.js → logout-UKD5LA37.js} +2 -2
- package/dist/{logs-SF2IMJN4.js → logs-HNTNNBDW.js} +2 -2
- package/dist/{merge-33C237A4.js → merge-B6SYTGI7.js} +2 -2
- package/dist/{mind-Z7CKD6DG.js → mind-BIDOF65R.js} +27 -11
- package/dist/{mind-activity-tracker-624QLQLC.js → mind-activity-tracker-PGC3DBJ7.js} +4 -5
- package/dist/{mind-manager-3DMYKZPB.js → mind-manager-3V2NXX4I.js} +5 -6
- package/dist/{package-4NHAVUUI.js → package-HQR52XSG.js} +1 -1
- package/dist/{pages-4DGQT7ZA.js → pages-KQBR5TAZ.js} +6 -6
- package/dist/{publish-TAJUET4I.js → publish-OJ4QMXVZ.js} +6 -6
- package/dist/{pull-XAEWQJ47.js → pull-GRQAXM2E.js} +2 -2
- package/dist/{register-VSPCMHKX.js → register-U2UO6TC4.js} +5 -5
- package/dist/registry-D2BSQ2X5.js +42 -0
- package/dist/{restart-IQKMCK5M.js → restart-CIDAKGG2.js} +3 -6
- package/dist/{schedule-FFZG23IW.js → schedule-NLR3LZLY.js} +2 -2
- package/dist/{seed-J43YDKXG.js → seed-3H2MRREW.js} +2 -2
- package/dist/{send-KVIZIGCE.js → send-RP2TA7SG.js} +132 -36
- package/dist/{service-LUR7WDO7.js → service-TVNEORO7.js} +31 -13
- package/dist/{setup-52YRV7VP.js → setup-OZDYCKDI.js} +9 -34
- package/dist/{shared-KO35ZM44.js → shared-DCQ2UXOM.js} +4 -4
- package/dist/{skill-BCVNI6TV.js → skill-Q2Y6PQ3L.js} +2 -2
- package/dist/skills/orientation/SKILL.md +2 -2
- package/dist/skills/volute-mind/SKILL.md +5 -5
- package/dist/{sprout-QN7Y4VVO.js → sprout-6Z6C42YM.js} +34 -30
- package/dist/{start-I5JYB65M.js → start-JR6CUUWF.js} +3 -6
- package/dist/{status-D7E5HHBV.js → status-5XDGYHKP.js} +2 -2
- package/dist/{status-FU2PFVVF.js → status-LV34BG6G.js} +3 -3
- package/dist/{status-4ESFLGH4.js → status-Z7NAFMBI.js} +5 -5
- package/dist/{stop-NBVKEFQQ.js → stop-VKPGK25U.js} +2 -5
- package/dist/template-hash-BIMA4ILT.js +8 -0
- package/dist/{up-FS7CKM6V.js → up-7BGDMFRT.js} +5 -5
- package/dist/{update-FJIHDJKM.js → update-4WT7VWHW.js} +5 -5
- package/dist/{update-check-MWE5AH4U.js → update-check-F5Z3ALXX.js} +2 -2
- package/dist/{upgrade-AIT24B5I.js → upgrade-ZEC2GGFO.js} +1 -1
- package/dist/{variant-63ZWO2W7.js → variant-A4I7PHXS.js} +16 -24
- package/dist/version-notify-TFS2U5CF.js +173 -0
- package/dist/web-assets/assets/index-BR3gtK3E.css +1 -0
- package/dist/web-assets/assets/index-CWmrZRQd.js +64 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-5XNT2472.js +0 -36
- package/dist/chunk-UJ6GHNR7.js +0 -675
- package/dist/db-C2CJ46ZU.js +0 -10
- package/dist/delivery-manager-CSG7LXA4.js +0 -16
- package/dist/down-ZY35KMHR.js +0 -14
- package/dist/schema-GFH6RV3W.js +0 -26
- package/dist/variants-JAGWGBXG.js +0 -26
- package/dist/web-assets/assets/index-CUZTZzaW.js +0 -64
- package/dist/web-assets/assets/index-adVuCkqy.css +0 -1
package/dist/daemon.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
SEED_SKILLS,
|
|
4
|
-
STANDARD_SKILLS,
|
|
5
|
-
getSharedSkill,
|
|
6
|
-
importSkillFromDir,
|
|
7
|
-
installSkill,
|
|
8
|
-
listFilesRecursive,
|
|
9
|
-
listMindSkills,
|
|
10
|
-
listSharedSkills,
|
|
11
|
-
publishSkill,
|
|
12
|
-
removeSharedSkill,
|
|
13
|
-
sharedSkillsDir,
|
|
14
|
-
syncBuiltinSkills,
|
|
15
|
-
uninstallSkill,
|
|
16
|
-
updateSkill
|
|
17
|
-
} from "./chunk-IKMY5X76.js";
|
|
18
2
|
import {
|
|
19
3
|
addSharedWorktree,
|
|
20
4
|
ensureSharedRepo,
|
|
@@ -23,16 +7,10 @@ import {
|
|
|
23
7
|
sharedMerge,
|
|
24
8
|
sharedPull,
|
|
25
9
|
sharedStatus
|
|
26
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-L3LHXZD7.js";
|
|
27
11
|
import {
|
|
28
12
|
readSystemsConfig
|
|
29
|
-
} from "./chunk-
|
|
30
|
-
import {
|
|
31
|
-
getActiveMinds,
|
|
32
|
-
markIdle,
|
|
33
|
-
onMindEvent,
|
|
34
|
-
stopAll
|
|
35
|
-
} from "./chunk-GZ7DW4YL.js";
|
|
13
|
+
} from "./chunk-HFCBO2GL.js";
|
|
36
14
|
import {
|
|
37
15
|
PROMPT_DEFAULTS,
|
|
38
16
|
PROMPT_KEYS,
|
|
@@ -47,7 +25,7 @@ import {
|
|
|
47
25
|
loadJsonMap,
|
|
48
26
|
saveJsonMap,
|
|
49
27
|
substitute
|
|
50
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-7LPTHFIL.js";
|
|
51
29
|
import {
|
|
52
30
|
deliverMessage,
|
|
53
31
|
extractTextContent,
|
|
@@ -57,20 +35,26 @@ import {
|
|
|
57
35
|
publish,
|
|
58
36
|
publishTypingForChannels,
|
|
59
37
|
subscribe
|
|
60
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-OGZYB5GL.js";
|
|
39
|
+
import {
|
|
40
|
+
applyInitFiles,
|
|
41
|
+
composeTemplate,
|
|
42
|
+
computeTemplateHash,
|
|
43
|
+
copyTemplateToDir,
|
|
44
|
+
findTemplatesRoot,
|
|
45
|
+
listFiles
|
|
46
|
+
} from "./chunk-AKPFNL7L.js";
|
|
47
|
+
import {
|
|
48
|
+
getActiveMinds,
|
|
49
|
+
markIdle,
|
|
50
|
+
onMindEvent,
|
|
51
|
+
stopAll
|
|
52
|
+
} from "./chunk-HGCDWKSP.js";
|
|
61
53
|
import {
|
|
62
54
|
broadcast,
|
|
63
55
|
publish as publish2,
|
|
64
56
|
subscribe as subscribe2
|
|
65
|
-
} from "./chunk-
|
|
66
|
-
import {
|
|
67
|
-
logBuffer,
|
|
68
|
-
logger_default
|
|
69
|
-
} from "./chunk-YUIHSKR6.js";
|
|
70
|
-
import {
|
|
71
|
-
CHANNELS,
|
|
72
|
-
getChannelDriver
|
|
73
|
-
} from "./chunk-UJ6GHNR7.js";
|
|
57
|
+
} from "./chunk-A4S7H6G6.js";
|
|
74
58
|
import {
|
|
75
59
|
findOpenClawSession,
|
|
76
60
|
importOpenClawConnectors,
|
|
@@ -78,33 +62,54 @@ import {
|
|
|
78
62
|
parseNameFromIdentity,
|
|
79
63
|
readVoluteConfig,
|
|
80
64
|
writeVoluteConfig
|
|
81
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-PC6R6UUW.js";
|
|
82
66
|
import {
|
|
83
67
|
loadMergedEnv,
|
|
84
68
|
mindEnvPath,
|
|
85
69
|
readEnv,
|
|
86
70
|
sharedEnvPath,
|
|
87
71
|
writeEnv
|
|
88
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-PHU4DEAJ.js";
|
|
73
|
+
import {
|
|
74
|
+
isHomeOnlyArchive
|
|
75
|
+
} from "./chunk-KTJGZ7M7.js";
|
|
89
76
|
import {
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
SEED_SKILLS,
|
|
78
|
+
STANDARD_SKILLS,
|
|
79
|
+
getSharedSkill,
|
|
80
|
+
importSkillFromDir,
|
|
81
|
+
installSkill,
|
|
82
|
+
listFilesRecursive,
|
|
83
|
+
listMindSkills,
|
|
84
|
+
listSharedSkills,
|
|
85
|
+
publishSkill,
|
|
86
|
+
removeSharedSkill,
|
|
87
|
+
sharedSkillsDir,
|
|
88
|
+
syncBuiltinSkills,
|
|
89
|
+
uninstallSkill,
|
|
90
|
+
updateSkill
|
|
91
|
+
} from "./chunk-5462YKWP.js";
|
|
92
92
|
import {
|
|
93
93
|
activity,
|
|
94
94
|
conversationParticipants,
|
|
95
95
|
conversations,
|
|
96
|
+
getDb,
|
|
96
97
|
messages,
|
|
97
98
|
mindHistory,
|
|
98
99
|
sessions,
|
|
99
100
|
systemPrompts,
|
|
100
101
|
users
|
|
101
|
-
} from "./chunk-
|
|
102
|
+
} from "./chunk-SGPEZ32F.js";
|
|
103
|
+
import {
|
|
104
|
+
logBuffer,
|
|
105
|
+
logger_default
|
|
106
|
+
} from "./chunk-YUIHSKR6.js";
|
|
102
107
|
import "./chunk-D424ZQGI.js";
|
|
103
108
|
import {
|
|
104
109
|
exec,
|
|
105
110
|
gitExec,
|
|
106
111
|
resolveVoluteBin
|
|
107
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-IPJXU366.js";
|
|
108
113
|
import {
|
|
109
114
|
chownMindDir,
|
|
110
115
|
createMindUser,
|
|
@@ -112,16 +117,19 @@ import {
|
|
|
112
117
|
ensureVoluteGroup,
|
|
113
118
|
isIsolationEnabled,
|
|
114
119
|
wrapForIsolation
|
|
115
|
-
} from "./chunk-
|
|
120
|
+
} from "./chunk-NWPT4ASZ.js";
|
|
116
121
|
import {
|
|
117
122
|
checkForUpdate,
|
|
118
123
|
checkForUpdateCached,
|
|
119
124
|
getCurrentVersion
|
|
120
|
-
} from "./chunk-
|
|
125
|
+
} from "./chunk-ON3FF5JA.js";
|
|
121
126
|
import {
|
|
122
127
|
buildVoluteSlug,
|
|
128
|
+
resolveChannelId,
|
|
129
|
+
slugify,
|
|
130
|
+
splitMessage,
|
|
123
131
|
writeChannelEntry
|
|
124
|
-
} from "./chunk-
|
|
132
|
+
} from "./chunk-WSLPZF72.js";
|
|
125
133
|
import {
|
|
126
134
|
addMind,
|
|
127
135
|
addVariant,
|
|
@@ -141,22 +149,26 @@ import {
|
|
|
141
149
|
removeVariant,
|
|
142
150
|
setMindRunning,
|
|
143
151
|
setMindStage,
|
|
152
|
+
setMindTemplateHash,
|
|
144
153
|
setVariantRunning,
|
|
145
154
|
stateDir,
|
|
146
155
|
validateBranchName,
|
|
147
156
|
validateMindName,
|
|
148
|
-
voluteHome
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
voluteHome,
|
|
158
|
+
writeVariants
|
|
159
|
+
} from "./chunk-B2CPS4QU.js";
|
|
160
|
+
import {
|
|
161
|
+
__export
|
|
162
|
+
} from "./chunk-K3NQKI34.js";
|
|
151
163
|
|
|
152
164
|
// src/daemon.ts
|
|
153
165
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
154
|
-
import { mkdirSync as
|
|
166
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
155
167
|
import { homedir as homedir2 } from "os";
|
|
156
168
|
import { resolve as resolve22 } from "path";
|
|
157
169
|
import { format } from "util";
|
|
158
170
|
|
|
159
|
-
// src/lib/connector-manager.ts
|
|
171
|
+
// src/lib/daemon/connector-manager.ts
|
|
160
172
|
import { spawn } from "child_process";
|
|
161
173
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
162
174
|
import { dirname, resolve as resolve2 } from "path";
|
|
@@ -233,7 +245,7 @@ function checkMissingEnvVars(def, env) {
|
|
|
233
245
|
return def.envVars.filter((v) => v.required && !env[v.name]);
|
|
234
246
|
}
|
|
235
247
|
|
|
236
|
-
// src/lib/connector-manager.ts
|
|
248
|
+
// src/lib/daemon/connector-manager.ts
|
|
237
249
|
var clog = logger_default.child("connectors");
|
|
238
250
|
function searchUpwards(...segments) {
|
|
239
251
|
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
@@ -253,13 +265,13 @@ var ConnectorManager = class {
|
|
|
253
265
|
async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
|
|
254
266
|
const config = readVoluteConfig(mindDir2) ?? {};
|
|
255
267
|
const types = config.connectors ?? [];
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
await Promise.all(
|
|
269
|
+
types.map(
|
|
270
|
+
(type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
271
|
+
clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
);
|
|
263
275
|
}
|
|
264
276
|
checkConnectorEnv(type, mindName, mindDir2) {
|
|
265
277
|
const mindConnectorDir = resolve2(mindDir2, "connectors", type);
|
|
@@ -281,13 +293,21 @@ var ConnectorManager = class {
|
|
|
281
293
|
await new Promise((res) => {
|
|
282
294
|
existing.child.on("exit", () => res());
|
|
283
295
|
try {
|
|
284
|
-
existing.child.
|
|
296
|
+
if (existing.child.pid) {
|
|
297
|
+
process.kill(-existing.child.pid, "SIGTERM");
|
|
298
|
+
} else {
|
|
299
|
+
existing.child.kill("SIGTERM");
|
|
300
|
+
}
|
|
285
301
|
} catch {
|
|
286
302
|
res();
|
|
287
303
|
}
|
|
288
304
|
setTimeout(() => {
|
|
289
305
|
try {
|
|
290
|
-
existing.child.
|
|
306
|
+
if (existing.child.pid) {
|
|
307
|
+
process.kill(-existing.child.pid, "SIGKILL");
|
|
308
|
+
} else {
|
|
309
|
+
existing.child.kill("SIGKILL");
|
|
310
|
+
}
|
|
291
311
|
} catch {
|
|
292
312
|
}
|
|
293
313
|
res();
|
|
@@ -334,6 +354,7 @@ var ConnectorManager = class {
|
|
|
334
354
|
);
|
|
335
355
|
const spawnOpts = {
|
|
336
356
|
stdio: ["ignore", "pipe", "pipe"],
|
|
357
|
+
detached: true,
|
|
337
358
|
env: {
|
|
338
359
|
...process.env,
|
|
339
360
|
VOLUTE_MIND_PORT: String(mindPort),
|
|
@@ -400,13 +421,13 @@ var ConnectorManager = class {
|
|
|
400
421
|
await new Promise((resolve23) => {
|
|
401
422
|
tracked.child.on("exit", () => resolve23());
|
|
402
423
|
try {
|
|
403
|
-
tracked.child.
|
|
424
|
+
process.kill(-tracked.child.pid, "SIGTERM");
|
|
404
425
|
} catch {
|
|
405
426
|
resolve23();
|
|
406
427
|
}
|
|
407
428
|
setTimeout(() => {
|
|
408
429
|
try {
|
|
409
|
-
tracked.child.
|
|
430
|
+
process.kill(-tracked.child.pid, "SIGKILL");
|
|
410
431
|
} catch {
|
|
411
432
|
}
|
|
412
433
|
resolve23();
|
|
@@ -461,7 +482,11 @@ var ConnectorManager = class {
|
|
|
461
482
|
try {
|
|
462
483
|
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
463
484
|
if (pid > 0) {
|
|
464
|
-
|
|
485
|
+
try {
|
|
486
|
+
process.kill(-pid, "SIGTERM");
|
|
487
|
+
} catch {
|
|
488
|
+
process.kill(pid, "SIGTERM");
|
|
489
|
+
}
|
|
465
490
|
clog.warn(`killed orphan connector ${type} (pid ${pid})`);
|
|
466
491
|
}
|
|
467
492
|
} catch {
|
|
@@ -490,7 +515,7 @@ function getConnectorManager() {
|
|
|
490
515
|
return instance;
|
|
491
516
|
}
|
|
492
517
|
|
|
493
|
-
// src/lib/mail-poller.ts
|
|
518
|
+
// src/lib/daemon/mail-poller.ts
|
|
494
519
|
var mlog = logger_default.child("mail");
|
|
495
520
|
function formatEmailContent(email) {
|
|
496
521
|
if (email.body) {
|
|
@@ -516,13 +541,14 @@ var MailPoller = class {
|
|
|
516
541
|
reconnectDelay = INITIAL_RECONNECT_MS;
|
|
517
542
|
reconnectAttempts = 0;
|
|
518
543
|
disconnectedAt = null;
|
|
544
|
+
config = null;
|
|
519
545
|
start() {
|
|
520
546
|
if (this.running) {
|
|
521
547
|
mlog.warn("already running \u2014 ignoring duplicate start");
|
|
522
548
|
return;
|
|
523
549
|
}
|
|
524
|
-
|
|
525
|
-
if (!config) {
|
|
550
|
+
this.config = readSystemsConfig();
|
|
551
|
+
if (!this.config) {
|
|
526
552
|
mlog.info("no systems config \u2014 mail disabled");
|
|
527
553
|
return;
|
|
528
554
|
}
|
|
@@ -531,6 +557,7 @@ var MailPoller = class {
|
|
|
531
557
|
}
|
|
532
558
|
stop() {
|
|
533
559
|
this.running = false;
|
|
560
|
+
this.config = null;
|
|
534
561
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
535
562
|
this.pingTimer = null;
|
|
536
563
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
@@ -545,16 +572,16 @@ var MailPoller = class {
|
|
|
545
572
|
}
|
|
546
573
|
connect() {
|
|
547
574
|
if (!this.running) return;
|
|
548
|
-
|
|
549
|
-
if (!config) {
|
|
575
|
+
this.config = readSystemsConfig();
|
|
576
|
+
if (!this.config) {
|
|
550
577
|
mlog.info("systems config removed \u2014 stopping");
|
|
551
578
|
this.stop();
|
|
552
579
|
return;
|
|
553
580
|
}
|
|
554
|
-
const wsUrl = `${config.apiUrl.replace(/^http/, "ws")}/api/ws`;
|
|
581
|
+
const wsUrl = `${this.config.apiUrl.replace(/^http/, "ws")}/api/ws`;
|
|
555
582
|
try {
|
|
556
583
|
this.ws = new WebSocket(wsUrl, {
|
|
557
|
-
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
584
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}` }
|
|
558
585
|
});
|
|
559
586
|
} catch (err) {
|
|
560
587
|
mlog.warn("failed to create WebSocket", logger_default.errorData(err));
|
|
@@ -620,11 +647,10 @@ var MailPoller = class {
|
|
|
620
647
|
}
|
|
621
648
|
/** Fetch emails that arrived while disconnected */
|
|
622
649
|
catchUp(since) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const url = `${config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
|
|
650
|
+
if (!this.config) return;
|
|
651
|
+
const url = `${this.config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
|
|
626
652
|
fetch(url, {
|
|
627
|
-
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
653
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}` }
|
|
628
654
|
}).then(async (res) => {
|
|
629
655
|
if (!res.ok) {
|
|
630
656
|
mlog.warn(`catch-up poll failed: HTTP ${res.status}`);
|
|
@@ -659,14 +685,13 @@ var MailPoller = class {
|
|
|
659
685
|
});
|
|
660
686
|
}
|
|
661
687
|
async fetchAndDeliver(mind, notification) {
|
|
662
|
-
|
|
663
|
-
if (!config) {
|
|
688
|
+
if (!this.config) {
|
|
664
689
|
mlog.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
|
|
665
690
|
return;
|
|
666
691
|
}
|
|
667
|
-
const url = `${config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
|
|
692
|
+
const url = `${this.config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
|
|
668
693
|
const res = await fetch(url, {
|
|
669
|
-
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
694
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}` }
|
|
670
695
|
});
|
|
671
696
|
if (!res.ok) {
|
|
672
697
|
mlog.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
|
|
@@ -727,182 +752,9 @@ async function ensureMailAddress(mindName) {
|
|
|
727
752
|
}
|
|
728
753
|
}
|
|
729
754
|
|
|
730
|
-
// src/lib/migrate-agents-to-minds.ts
|
|
731
|
-
import { execFileSync } from "child_process";
|
|
732
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
733
|
-
import { resolve as resolve3 } from "path";
|
|
734
|
-
var TAG = "[migrate]";
|
|
735
|
-
function log(msg) {
|
|
736
|
-
console.error(`${TAG} ${msg}`);
|
|
737
|
-
}
|
|
738
|
-
function migrateAgentsToMinds() {
|
|
739
|
-
const home = voluteHome();
|
|
740
|
-
bridgeEnvVar();
|
|
741
|
-
const names = migrateRegistry(home);
|
|
742
|
-
migrateMindsDirectory(home);
|
|
743
|
-
migrateLogFiles(home, names);
|
|
744
|
-
migrateLinuxUsers(names);
|
|
745
|
-
migrateProfileScript();
|
|
746
|
-
}
|
|
747
|
-
function bridgeEnvVar() {
|
|
748
|
-
if (process.env.VOLUTE_AGENTS_DIR && !process.env.VOLUTE_MINDS_DIR) {
|
|
749
|
-
process.env.VOLUTE_MINDS_DIR = process.env.VOLUTE_AGENTS_DIR;
|
|
750
|
-
log(`bridged VOLUTE_AGENTS_DIR=${process.env.VOLUTE_AGENTS_DIR} \u2192 VOLUTE_MINDS_DIR`);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
function migrateRegistry(home) {
|
|
754
|
-
const oldPath = resolve3(home, "agents.json");
|
|
755
|
-
const newPath = resolve3(home, "minds.json");
|
|
756
|
-
if (!existsSync3(oldPath) || existsSync3(newPath)) {
|
|
757
|
-
return readNamesFromRegistry(newPath);
|
|
758
|
-
}
|
|
759
|
-
const raw = readFileSync3(oldPath, "utf-8");
|
|
760
|
-
const entries = JSON.parse(raw);
|
|
761
|
-
for (const entry of entries) {
|
|
762
|
-
if (entry.stage === "mind") {
|
|
763
|
-
entry.stage = "sprouted";
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
writeFileSync2(newPath, `${JSON.stringify(entries, null, 2)}
|
|
767
|
-
`);
|
|
768
|
-
try {
|
|
769
|
-
renameSync(oldPath, `${oldPath}.bak`);
|
|
770
|
-
} catch {
|
|
771
|
-
}
|
|
772
|
-
log("renamed agents.json \u2192 minds.json");
|
|
773
|
-
return entries.map((e) => e.name);
|
|
774
|
-
}
|
|
775
|
-
function readNamesFromRegistry(path) {
|
|
776
|
-
if (!existsSync3(path)) return [];
|
|
777
|
-
try {
|
|
778
|
-
const entries = JSON.parse(readFileSync3(path, "utf-8"));
|
|
779
|
-
return entries.map((e) => e.name);
|
|
780
|
-
} catch {
|
|
781
|
-
return [];
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
function migrateMindsDirectory(home) {
|
|
785
|
-
if (process.env.VOLUTE_MINDS_DIR) return;
|
|
786
|
-
const oldDir = resolve3(home, "agents");
|
|
787
|
-
const newDir = resolve3(home, "minds");
|
|
788
|
-
if (existsSync3(oldDir) && !existsSync3(newDir)) {
|
|
789
|
-
try {
|
|
790
|
-
renameSync(oldDir, newDir);
|
|
791
|
-
log("renamed agents/ \u2192 minds/");
|
|
792
|
-
} catch (err) {
|
|
793
|
-
log(`failed to rename agents/ \u2192 minds/: ${err}`);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
function migrateLogFiles(home, names) {
|
|
798
|
-
for (const name of names) {
|
|
799
|
-
const logsDir = resolve3(home, "state", name, "logs");
|
|
800
|
-
const oldLog = resolve3(logsDir, "agent.log");
|
|
801
|
-
const newLog = resolve3(logsDir, "mind.log");
|
|
802
|
-
if (existsSync3(oldLog) && !existsSync3(newLog)) {
|
|
803
|
-
try {
|
|
804
|
-
renameSync(oldLog, newLog);
|
|
805
|
-
log(`renamed ${name} agent.log \u2192 mind.log`);
|
|
806
|
-
} catch (err) {
|
|
807
|
-
log(`failed to rename ${name} log file: ${err}`);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function migrateLinuxUsers(names) {
|
|
813
|
-
if (process.env.VOLUTE_ISOLATION !== "user") return;
|
|
814
|
-
const oldPrefix = "agent-";
|
|
815
|
-
const newPrefix = process.env.VOLUTE_USER_PREFIX ?? "mind-";
|
|
816
|
-
if (newPrefix !== "mind-") return;
|
|
817
|
-
for (const name of names) {
|
|
818
|
-
const oldUser = `${oldPrefix}${name}`;
|
|
819
|
-
const newUser = `${newPrefix}${name}`;
|
|
820
|
-
try {
|
|
821
|
-
execFileSync("id", [oldUser], { stdio: "ignore" });
|
|
822
|
-
} catch {
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
try {
|
|
826
|
-
execFileSync("id", [newUser], { stdio: "ignore" });
|
|
827
|
-
continue;
|
|
828
|
-
} catch {
|
|
829
|
-
}
|
|
830
|
-
try {
|
|
831
|
-
execFileSync("usermod", ["-l", newUser, oldUser], {
|
|
832
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
833
|
-
});
|
|
834
|
-
log(`renamed user ${oldUser} \u2192 ${newUser}`);
|
|
835
|
-
} catch (err) {
|
|
836
|
-
const stderr = err?.stderr?.toString().trim();
|
|
837
|
-
log(`failed to rename user ${oldUser}: ${stderr || err}`);
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
try {
|
|
841
|
-
execFileSync("getent", ["group", oldUser], { stdio: "ignore" });
|
|
842
|
-
execFileSync("groupmod", ["-n", newUser, oldUser], {
|
|
843
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
844
|
-
});
|
|
845
|
-
log(`renamed group ${oldUser} \u2192 ${newUser}`);
|
|
846
|
-
} catch {
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
function migrateProfileScript() {
|
|
851
|
-
const profilePath = "/etc/profile.d/volute.sh";
|
|
852
|
-
if (!existsSync3(profilePath)) return;
|
|
853
|
-
try {
|
|
854
|
-
const content = readFileSync3(profilePath, "utf-8");
|
|
855
|
-
if (!content.includes("VOLUTE_AGENTS_DIR")) return;
|
|
856
|
-
const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
|
|
857
|
-
writeFileSync2(profilePath, updated);
|
|
858
|
-
log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
|
|
859
|
-
} catch (err) {
|
|
860
|
-
log(`failed to update profile script: ${err}`);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// src/lib/migrate-state.ts
|
|
865
|
-
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, renameSync as renameSync2 } from "fs";
|
|
866
|
-
import { resolve as resolve4 } from "path";
|
|
867
|
-
function migrateDotVoluteDir(name) {
|
|
868
|
-
const dir = mindDir(name);
|
|
869
|
-
const oldDir = resolve4(dir, ".volute");
|
|
870
|
-
const newDir = resolve4(dir, ".mind");
|
|
871
|
-
if (existsSync4(oldDir) && !existsSync4(newDir)) {
|
|
872
|
-
renameSync2(oldDir, newDir);
|
|
873
|
-
} else if (existsSync4(oldDir) && existsSync4(newDir)) {
|
|
874
|
-
console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
function migrateMindState(name) {
|
|
878
|
-
const src = resolve4(mindDir(name), ".mind");
|
|
879
|
-
if (!existsSync4(src)) return;
|
|
880
|
-
const dest = stateDir(name);
|
|
881
|
-
mkdirSync2(dest, { recursive: true });
|
|
882
|
-
for (const file of ["env.json", "channels.json"]) {
|
|
883
|
-
const srcPath = resolve4(src, file);
|
|
884
|
-
const destPath = resolve4(dest, file);
|
|
885
|
-
if (existsSync4(srcPath) && !existsSync4(destPath)) {
|
|
886
|
-
copyFileSync(srcPath, destPath);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
const srcLogs = resolve4(src, "logs");
|
|
890
|
-
const destLogs = resolve4(dest, "logs");
|
|
891
|
-
if (existsSync4(srcLogs) && !existsSync4(destLogs)) {
|
|
892
|
-
mkdirSync2(destLogs, { recursive: true });
|
|
893
|
-
for (const file of readdirSync(srcLogs)) {
|
|
894
|
-
try {
|
|
895
|
-
copyFileSync(resolve4(srcLogs, file), resolve4(destLogs, file));
|
|
896
|
-
} catch (err) {
|
|
897
|
-
console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
755
|
// src/lib/pages-watcher.ts
|
|
904
|
-
import { existsSync as
|
|
905
|
-
import { join, resolve as
|
|
756
|
+
import { existsSync as existsSync3, readdirSync, statSync, watch } from "fs";
|
|
757
|
+
import { join, resolve as resolve3 } from "path";
|
|
906
758
|
var watchers = /* @__PURE__ */ new Map();
|
|
907
759
|
var homeWatchers = /* @__PURE__ */ new Map();
|
|
908
760
|
var debounceTimers = /* @__PURE__ */ new Map();
|
|
@@ -938,18 +790,18 @@ function startPagesWatcher(mindName, pagesDir) {
|
|
|
938
790
|
}
|
|
939
791
|
function startWatcher(mindName) {
|
|
940
792
|
if (watchers.has(mindName)) return;
|
|
941
|
-
const pagesDir =
|
|
942
|
-
if (
|
|
793
|
+
const pagesDir = resolve3(mindDir(mindName), "home", "pages");
|
|
794
|
+
if (existsSync3(pagesDir)) {
|
|
943
795
|
startPagesWatcher(mindName, pagesDir);
|
|
944
796
|
return;
|
|
945
797
|
}
|
|
946
798
|
if (homeWatchers.has(mindName)) return;
|
|
947
|
-
const homeDir =
|
|
948
|
-
if (!
|
|
799
|
+
const homeDir = resolve3(mindDir(mindName), "home");
|
|
800
|
+
if (!existsSync3(homeDir)) return;
|
|
949
801
|
try {
|
|
950
802
|
const hw = watch(homeDir, (_eventType, filename) => {
|
|
951
803
|
if (filename !== "pages") return;
|
|
952
|
-
if (!
|
|
804
|
+
if (!existsSync3(pagesDir)) return;
|
|
953
805
|
hw.close();
|
|
954
806
|
homeWatchers.delete(mindName);
|
|
955
807
|
invalidateCache();
|
|
@@ -1001,13 +853,13 @@ function scanPagesDir(dir, urlPrefix) {
|
|
|
1001
853
|
const pages = [];
|
|
1002
854
|
let items;
|
|
1003
855
|
try {
|
|
1004
|
-
items =
|
|
856
|
+
items = readdirSync(dir);
|
|
1005
857
|
} catch {
|
|
1006
858
|
return pages;
|
|
1007
859
|
}
|
|
1008
860
|
for (const item of items) {
|
|
1009
861
|
if (item.startsWith(".")) continue;
|
|
1010
|
-
const fullPath =
|
|
862
|
+
const fullPath = resolve3(dir, item);
|
|
1011
863
|
try {
|
|
1012
864
|
const s = statSync(fullPath);
|
|
1013
865
|
if (s.isFile() && item.endsWith(".html")) {
|
|
@@ -1017,8 +869,8 @@ function scanPagesDir(dir, urlPrefix) {
|
|
|
1017
869
|
url: `${urlPrefix}/${item}`
|
|
1018
870
|
});
|
|
1019
871
|
} else if (s.isDirectory()) {
|
|
1020
|
-
const indexPath =
|
|
1021
|
-
if (
|
|
872
|
+
const indexPath = resolve3(fullPath, "index.html");
|
|
873
|
+
if (existsSync3(indexPath)) {
|
|
1022
874
|
const indexStat = statSync(indexPath);
|
|
1023
875
|
pages.push({
|
|
1024
876
|
file: join(item, "index.html"),
|
|
@@ -1035,8 +887,8 @@ function scanPagesDir(dir, urlPrefix) {
|
|
|
1035
887
|
}
|
|
1036
888
|
function buildSites() {
|
|
1037
889
|
const sites = [];
|
|
1038
|
-
const systemPagesDir =
|
|
1039
|
-
if (
|
|
890
|
+
const systemPagesDir = resolve3(voluteHome(), "shared", "pages");
|
|
891
|
+
if (existsSync3(systemPagesDir)) {
|
|
1040
892
|
const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
|
|
1041
893
|
if (systemPages.length > 0) {
|
|
1042
894
|
sites.push({ name: "_system", label: "System", pages: systemPages });
|
|
@@ -1044,8 +896,8 @@ function buildSites() {
|
|
|
1044
896
|
}
|
|
1045
897
|
const entries = readRegistry();
|
|
1046
898
|
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1047
|
-
const pagesDir =
|
|
1048
|
-
if (!
|
|
899
|
+
const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
|
|
900
|
+
if (!existsSync3(pagesDir)) continue;
|
|
1049
901
|
const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
|
|
1050
902
|
if (mindPages.length > 0) {
|
|
1051
903
|
sites.push({ name: entry.name, label: entry.name, pages: mindPages });
|
|
@@ -1057,17 +909,17 @@ function buildRecentPages() {
|
|
|
1057
909
|
const entries = readRegistry();
|
|
1058
910
|
const pages = [];
|
|
1059
911
|
for (const entry of entries) {
|
|
1060
|
-
const pagesDir =
|
|
1061
|
-
if (!
|
|
912
|
+
const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
|
|
913
|
+
if (!existsSync3(pagesDir)) continue;
|
|
1062
914
|
let items;
|
|
1063
915
|
try {
|
|
1064
|
-
items =
|
|
916
|
+
items = readdirSync(pagesDir);
|
|
1065
917
|
} catch {
|
|
1066
918
|
continue;
|
|
1067
919
|
}
|
|
1068
920
|
for (const item of items) {
|
|
1069
921
|
if (item.startsWith(".")) continue;
|
|
1070
|
-
const fullPath =
|
|
922
|
+
const fullPath = resolve3(pagesDir, item);
|
|
1071
923
|
try {
|
|
1072
924
|
const s = statSync(fullPath);
|
|
1073
925
|
if (s.isFile() && item.endsWith(".html")) {
|
|
@@ -1078,8 +930,8 @@ function buildRecentPages() {
|
|
|
1078
930
|
url: `/pages/${entry.name}/${item}`
|
|
1079
931
|
});
|
|
1080
932
|
} else if (s.isDirectory()) {
|
|
1081
|
-
const indexPath =
|
|
1082
|
-
if (
|
|
933
|
+
const indexPath = resolve3(fullPath, "index.html");
|
|
934
|
+
if (existsSync3(indexPath)) {
|
|
1083
935
|
const indexStat = statSync(indexPath);
|
|
1084
936
|
pages.push({
|
|
1085
937
|
mind: entry.name,
|
|
@@ -1105,8 +957,8 @@ function getCachedRecentPages() {
|
|
|
1105
957
|
return recentPagesCache;
|
|
1106
958
|
}
|
|
1107
959
|
|
|
1108
|
-
// src/lib/scheduler.ts
|
|
1109
|
-
import { resolve as
|
|
960
|
+
// src/lib/daemon/scheduler.ts
|
|
961
|
+
import { resolve as resolve4 } from "path";
|
|
1110
962
|
import { CronExpressionParser } from "cron-parser";
|
|
1111
963
|
var slog = logger_default.child("scheduler");
|
|
1112
964
|
var Scheduler = class {
|
|
@@ -1115,7 +967,7 @@ var Scheduler = class {
|
|
|
1115
967
|
lastFired = /* @__PURE__ */ new Map();
|
|
1116
968
|
// "mind:scheduleId" → epoch minute
|
|
1117
969
|
get statePath() {
|
|
1118
|
-
return
|
|
970
|
+
return resolve4(voluteHome(), "scheduler-state.json");
|
|
1119
971
|
}
|
|
1120
972
|
start() {
|
|
1121
973
|
this.loadState();
|
|
@@ -1149,39 +1001,46 @@ var Scheduler = class {
|
|
|
1149
1001
|
}
|
|
1150
1002
|
tick() {
|
|
1151
1003
|
const now = /* @__PURE__ */ new Date();
|
|
1004
|
+
const epochMinute = Math.floor(now.getTime() / 6e4);
|
|
1005
|
+
const cronCache = /* @__PURE__ */ new Map();
|
|
1006
|
+
let anyFired = false;
|
|
1152
1007
|
for (const [mind, schedules] of this.schedules) {
|
|
1153
1008
|
for (const schedule of schedules) {
|
|
1154
1009
|
if (!schedule.enabled) continue;
|
|
1155
|
-
if (this.shouldFire(schedule,
|
|
1010
|
+
if (this.shouldFire(schedule, epochMinute, mind, cronCache)) {
|
|
1011
|
+
anyFired = true;
|
|
1156
1012
|
this.fire(mind, schedule);
|
|
1157
1013
|
}
|
|
1158
1014
|
}
|
|
1159
1015
|
}
|
|
1016
|
+
if (anyFired) this.saveState();
|
|
1160
1017
|
}
|
|
1161
|
-
shouldFire(schedule,
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1018
|
+
shouldFire(schedule, epochMinute, mind, cronCache) {
|
|
1019
|
+
const key = `${mind}:${schedule.id}`;
|
|
1020
|
+
if (this.lastFired.get(key) === epochMinute) return false;
|
|
1021
|
+
let prevMinute = cronCache.get(schedule.cron);
|
|
1022
|
+
if (prevMinute === void 0) {
|
|
1023
|
+
try {
|
|
1024
|
+
const interval = CronExpressionParser.parse(schedule.cron);
|
|
1025
|
+
const prev = interval.prev().toDate();
|
|
1026
|
+
prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
1027
|
+
cronCache.set(schedule.cron, prevMinute);
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
1030
|
+
return false;
|
|
1173
1031
|
}
|
|
1174
|
-
return false;
|
|
1175
|
-
} catch (err) {
|
|
1176
|
-
slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
1177
|
-
return false;
|
|
1178
1032
|
}
|
|
1033
|
+
if (prevMinute === epochMinute) {
|
|
1034
|
+
this.lastFired.set(key, epochMinute);
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
return false;
|
|
1179
1038
|
}
|
|
1180
1039
|
async fire(mindName, schedule) {
|
|
1181
1040
|
try {
|
|
1182
1041
|
let text;
|
|
1183
1042
|
if (schedule.script) {
|
|
1184
|
-
const homeDir =
|
|
1043
|
+
const homeDir = resolve4(mindDir(mindName), "home");
|
|
1185
1044
|
try {
|
|
1186
1045
|
const output = await this.runScript(schedule.script, homeDir, mindName);
|
|
1187
1046
|
if (!output.trim()) {
|
|
@@ -1229,19 +1088,21 @@ function getScheduler() {
|
|
|
1229
1088
|
return instance3;
|
|
1230
1089
|
}
|
|
1231
1090
|
|
|
1232
|
-
// src/lib/token-budget.ts
|
|
1233
|
-
import { existsSync as
|
|
1234
|
-
import { resolve as
|
|
1091
|
+
// src/lib/daemon/token-budget.ts
|
|
1092
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1093
|
+
import { resolve as resolve5 } from "path";
|
|
1235
1094
|
var tlog = logger_default.child("token-budget");
|
|
1236
1095
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
1237
1096
|
var MAX_QUEUE_SIZE = 100;
|
|
1238
1097
|
var TokenBudget = class {
|
|
1239
1098
|
budgets = /* @__PURE__ */ new Map();
|
|
1240
1099
|
interval = null;
|
|
1100
|
+
dirty = /* @__PURE__ */ new Set();
|
|
1241
1101
|
start() {
|
|
1242
1102
|
this.interval = setInterval(() => this.tick(), 6e4);
|
|
1243
1103
|
}
|
|
1244
1104
|
stop() {
|
|
1105
|
+
this.flush();
|
|
1245
1106
|
if (this.interval) clearInterval(this.interval);
|
|
1246
1107
|
this.interval = null;
|
|
1247
1108
|
}
|
|
@@ -1276,7 +1137,7 @@ var TokenBudget = class {
|
|
|
1276
1137
|
const state = this.budgets.get(mind);
|
|
1277
1138
|
if (!state) return;
|
|
1278
1139
|
state.tokensUsed += inputTokens + outputTokens;
|
|
1279
|
-
this.
|
|
1140
|
+
this.dirty.add(mind);
|
|
1280
1141
|
}
|
|
1281
1142
|
/** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
|
|
1282
1143
|
checkBudget(mind) {
|
|
@@ -1327,7 +1188,7 @@ var TokenBudget = class {
|
|
|
1327
1188
|
state.tokensUsed = 0;
|
|
1328
1189
|
state.periodStart = now;
|
|
1329
1190
|
state.warningInjected = false;
|
|
1330
|
-
this.
|
|
1191
|
+
this.dirty.add(mind);
|
|
1331
1192
|
const queued = this.drain(mind);
|
|
1332
1193
|
if (queued.length > 0) {
|
|
1333
1194
|
this.replay(mind, queued).catch((err) => {
|
|
@@ -1336,21 +1197,30 @@ var TokenBudget = class {
|
|
|
1336
1197
|
}
|
|
1337
1198
|
}
|
|
1338
1199
|
}
|
|
1200
|
+
this.flush();
|
|
1201
|
+
}
|
|
1202
|
+
/** Flush all dirty budget states to disk. */
|
|
1203
|
+
flush() {
|
|
1204
|
+
for (const mind of this.dirty) {
|
|
1205
|
+
const state = this.budgets.get(mind);
|
|
1206
|
+
if (state) this.saveBudgetState(mind, state);
|
|
1207
|
+
}
|
|
1208
|
+
this.dirty.clear();
|
|
1339
1209
|
}
|
|
1340
1210
|
budgetStatePath(mind) {
|
|
1341
|
-
return
|
|
1211
|
+
return resolve5(stateDir(mind), "budget.json");
|
|
1342
1212
|
}
|
|
1343
1213
|
saveBudgetState(mind, state) {
|
|
1344
1214
|
try {
|
|
1345
1215
|
const dir = stateDir(mind);
|
|
1346
|
-
|
|
1216
|
+
mkdirSync2(dir, { recursive: true });
|
|
1347
1217
|
const data = {
|
|
1348
1218
|
periodStart: state.periodStart,
|
|
1349
1219
|
tokensUsed: state.tokensUsed,
|
|
1350
1220
|
warningInjected: state.warningInjected,
|
|
1351
1221
|
queue: state.queue
|
|
1352
1222
|
};
|
|
1353
|
-
|
|
1223
|
+
writeFileSync2(this.budgetStatePath(mind), `${JSON.stringify(data)}
|
|
1354
1224
|
`);
|
|
1355
1225
|
} catch (err) {
|
|
1356
1226
|
tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
|
|
@@ -1359,8 +1229,8 @@ var TokenBudget = class {
|
|
|
1359
1229
|
loadBudgetState(mind) {
|
|
1360
1230
|
try {
|
|
1361
1231
|
const path = this.budgetStatePath(mind);
|
|
1362
|
-
if (!
|
|
1363
|
-
const data = JSON.parse(
|
|
1232
|
+
if (!existsSync4(path)) return null;
|
|
1233
|
+
const data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1364
1234
|
if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
|
|
1365
1235
|
return {
|
|
1366
1236
|
periodStart: data.periodStart,
|
|
@@ -1415,7 +1285,7 @@ function getTokenBudget() {
|
|
|
1415
1285
|
return instance4;
|
|
1416
1286
|
}
|
|
1417
1287
|
|
|
1418
|
-
// src/lib/mind-service.ts
|
|
1288
|
+
// src/lib/daemon/mind-service.ts
|
|
1419
1289
|
async function startMindFull(name) {
|
|
1420
1290
|
const [baseName, variantName] = name.split("@", 2);
|
|
1421
1291
|
await getMindManager().startMind(name);
|
|
@@ -1461,42 +1331,215 @@ async function stopMindFull(name) {
|
|
|
1461
1331
|
}).catch((err) => logger_default.error("failed to publish mind_stopped activity", logger_default.errorData(err)));
|
|
1462
1332
|
}
|
|
1463
1333
|
|
|
1464
|
-
// src/
|
|
1465
|
-
import {
|
|
1466
|
-
import {
|
|
1467
|
-
import {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
import { compareSync, hashSync } from "bcryptjs";
|
|
1472
|
-
import { and, count, eq } from "drizzle-orm";
|
|
1473
|
-
async function createUser(username, password) {
|
|
1474
|
-
const db = await getDb();
|
|
1475
|
-
const hash = hashSync(password, 10);
|
|
1476
|
-
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
|
|
1477
|
-
const role = value === 0 ? "admin" : "pending";
|
|
1478
|
-
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
1479
|
-
id: users.id,
|
|
1480
|
-
username: users.username,
|
|
1481
|
-
role: users.role,
|
|
1482
|
-
user_type: users.user_type,
|
|
1483
|
-
created_at: users.created_at
|
|
1484
|
-
});
|
|
1485
|
-
return result;
|
|
1486
|
-
}
|
|
1487
|
-
async function verifyUser(username, password) {
|
|
1488
|
-
const db = await getDb();
|
|
1489
|
-
const row = await db.select().from(users).where(eq(users.username, username)).get();
|
|
1490
|
-
if (!row) return null;
|
|
1491
|
-
if (row.user_type === "mind") return null;
|
|
1492
|
-
if (!compareSync(password, row.password_hash)) return null;
|
|
1493
|
-
const { password_hash: _, ...user } = row;
|
|
1494
|
-
return user;
|
|
1334
|
+
// src/lib/migrate-agents-to-minds.ts
|
|
1335
|
+
import { execFileSync } from "child_process";
|
|
1336
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1337
|
+
import { resolve as resolve6 } from "path";
|
|
1338
|
+
var TAG = "[migrate]";
|
|
1339
|
+
function log(msg) {
|
|
1340
|
+
console.error(`${TAG} ${msg}`);
|
|
1495
1341
|
}
|
|
1496
|
-
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1342
|
+
function migrateAgentsToMinds() {
|
|
1343
|
+
const home = voluteHome();
|
|
1344
|
+
bridgeEnvVar();
|
|
1345
|
+
const names = migrateRegistry(home);
|
|
1346
|
+
migrateMindsDirectory(home);
|
|
1347
|
+
migrateLogFiles(home, names);
|
|
1348
|
+
migrateLinuxUsers(names);
|
|
1349
|
+
migrateProfileScript();
|
|
1350
|
+
}
|
|
1351
|
+
function bridgeEnvVar() {
|
|
1352
|
+
if (process.env.VOLUTE_AGENTS_DIR && !process.env.VOLUTE_MINDS_DIR) {
|
|
1353
|
+
process.env.VOLUTE_MINDS_DIR = process.env.VOLUTE_AGENTS_DIR;
|
|
1354
|
+
log(`bridged VOLUTE_AGENTS_DIR=${process.env.VOLUTE_AGENTS_DIR} \u2192 VOLUTE_MINDS_DIR`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function migrateRegistry(home) {
|
|
1358
|
+
const oldPath = resolve6(home, "agents.json");
|
|
1359
|
+
const newPath = resolve6(home, "minds.json");
|
|
1360
|
+
if (!existsSync5(oldPath) || existsSync5(newPath)) {
|
|
1361
|
+
return readNamesFromRegistry(newPath);
|
|
1362
|
+
}
|
|
1363
|
+
const raw = readFileSync4(oldPath, "utf-8");
|
|
1364
|
+
const entries = JSON.parse(raw);
|
|
1365
|
+
for (const entry of entries) {
|
|
1366
|
+
if (entry.stage === "mind") {
|
|
1367
|
+
entry.stage = "sprouted";
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
writeFileSync3(newPath, `${JSON.stringify(entries, null, 2)}
|
|
1371
|
+
`);
|
|
1372
|
+
try {
|
|
1373
|
+
renameSync(oldPath, `${oldPath}.bak`);
|
|
1374
|
+
} catch {
|
|
1375
|
+
}
|
|
1376
|
+
log("renamed agents.json \u2192 minds.json");
|
|
1377
|
+
return entries.map((e) => e.name);
|
|
1378
|
+
}
|
|
1379
|
+
function readNamesFromRegistry(path) {
|
|
1380
|
+
if (!existsSync5(path)) return [];
|
|
1381
|
+
try {
|
|
1382
|
+
const entries = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1383
|
+
return entries.map((e) => e.name);
|
|
1384
|
+
} catch {
|
|
1385
|
+
return [];
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function migrateMindsDirectory(home) {
|
|
1389
|
+
if (process.env.VOLUTE_MINDS_DIR) return;
|
|
1390
|
+
const oldDir = resolve6(home, "agents");
|
|
1391
|
+
const newDir = resolve6(home, "minds");
|
|
1392
|
+
if (existsSync5(oldDir) && !existsSync5(newDir)) {
|
|
1393
|
+
try {
|
|
1394
|
+
renameSync(oldDir, newDir);
|
|
1395
|
+
log("renamed agents/ \u2192 minds/");
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
log(`failed to rename agents/ \u2192 minds/: ${err}`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function migrateLogFiles(home, names) {
|
|
1402
|
+
for (const name of names) {
|
|
1403
|
+
const logsDir = resolve6(home, "state", name, "logs");
|
|
1404
|
+
const oldLog = resolve6(logsDir, "agent.log");
|
|
1405
|
+
const newLog = resolve6(logsDir, "mind.log");
|
|
1406
|
+
if (existsSync5(oldLog) && !existsSync5(newLog)) {
|
|
1407
|
+
try {
|
|
1408
|
+
renameSync(oldLog, newLog);
|
|
1409
|
+
log(`renamed ${name} agent.log \u2192 mind.log`);
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
log(`failed to rename ${name} log file: ${err}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function migrateLinuxUsers(names) {
|
|
1417
|
+
if (process.env.VOLUTE_ISOLATION !== "user") return;
|
|
1418
|
+
const oldPrefix = "agent-";
|
|
1419
|
+
const newPrefix = process.env.VOLUTE_USER_PREFIX ?? "mind-";
|
|
1420
|
+
if (newPrefix !== "mind-") return;
|
|
1421
|
+
for (const name of names) {
|
|
1422
|
+
const oldUser = `${oldPrefix}${name}`;
|
|
1423
|
+
const newUser = `${newPrefix}${name}`;
|
|
1424
|
+
try {
|
|
1425
|
+
execFileSync("id", [oldUser], { stdio: "ignore" });
|
|
1426
|
+
} catch {
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
try {
|
|
1430
|
+
execFileSync("id", [newUser], { stdio: "ignore" });
|
|
1431
|
+
continue;
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
1434
|
+
try {
|
|
1435
|
+
execFileSync("usermod", ["-l", newUser, oldUser], {
|
|
1436
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
1437
|
+
});
|
|
1438
|
+
log(`renamed user ${oldUser} \u2192 ${newUser}`);
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
const stderr = err?.stderr?.toString().trim();
|
|
1441
|
+
log(`failed to rename user ${oldUser}: ${stderr || err}`);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
execFileSync("getent", ["group", oldUser], { stdio: "ignore" });
|
|
1446
|
+
execFileSync("groupmod", ["-n", newUser, oldUser], {
|
|
1447
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
1448
|
+
});
|
|
1449
|
+
log(`renamed group ${oldUser} \u2192 ${newUser}`);
|
|
1450
|
+
} catch {
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
function migrateProfileScript() {
|
|
1455
|
+
const profilePath = "/etc/profile.d/volute.sh";
|
|
1456
|
+
if (!existsSync5(profilePath)) return;
|
|
1457
|
+
try {
|
|
1458
|
+
const content = readFileSync4(profilePath, "utf-8");
|
|
1459
|
+
if (!content.includes("VOLUTE_AGENTS_DIR")) return;
|
|
1460
|
+
const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
|
|
1461
|
+
writeFileSync3(profilePath, updated);
|
|
1462
|
+
log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
log(`failed to update profile script: ${err}`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// src/lib/migrate-state.ts
|
|
1469
|
+
import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync2, renameSync as renameSync2 } from "fs";
|
|
1470
|
+
import { resolve as resolve7 } from "path";
|
|
1471
|
+
function migrateDotVoluteDir(name) {
|
|
1472
|
+
const dir = mindDir(name);
|
|
1473
|
+
const oldDir = resolve7(dir, ".volute");
|
|
1474
|
+
const newDir = resolve7(dir, ".mind");
|
|
1475
|
+
if (existsSync6(oldDir) && !existsSync6(newDir)) {
|
|
1476
|
+
renameSync2(oldDir, newDir);
|
|
1477
|
+
} else if (existsSync6(oldDir) && existsSync6(newDir)) {
|
|
1478
|
+
console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function migrateMindState(name) {
|
|
1482
|
+
const src = resolve7(mindDir(name), ".mind");
|
|
1483
|
+
if (!existsSync6(src)) return;
|
|
1484
|
+
const dest = stateDir(name);
|
|
1485
|
+
mkdirSync3(dest, { recursive: true });
|
|
1486
|
+
for (const file of ["env.json", "channels.json"]) {
|
|
1487
|
+
const srcPath = resolve7(src, file);
|
|
1488
|
+
const destPath = resolve7(dest, file);
|
|
1489
|
+
if (existsSync6(srcPath) && !existsSync6(destPath)) {
|
|
1490
|
+
copyFileSync(srcPath, destPath);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const srcLogs = resolve7(src, "logs");
|
|
1494
|
+
const destLogs = resolve7(dest, "logs");
|
|
1495
|
+
if (existsSync6(srcLogs) && !existsSync6(destLogs)) {
|
|
1496
|
+
mkdirSync3(destLogs, { recursive: true });
|
|
1497
|
+
for (const file of readdirSync2(srcLogs)) {
|
|
1498
|
+
try {
|
|
1499
|
+
copyFileSync(resolve7(srcLogs, file), resolve7(destLogs, file));
|
|
1500
|
+
} catch (err) {
|
|
1501
|
+
console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/web/middleware/auth.ts
|
|
1508
|
+
import { timingSafeEqual } from "crypto";
|
|
1509
|
+
import { eq as eq2, lt } from "drizzle-orm";
|
|
1510
|
+
import { getCookie } from "hono/cookie";
|
|
1511
|
+
import { createMiddleware } from "hono/factory";
|
|
1512
|
+
|
|
1513
|
+
// src/lib/auth.ts
|
|
1514
|
+
import { compareSync, hashSync } from "bcryptjs";
|
|
1515
|
+
import { and, count, eq } from "drizzle-orm";
|
|
1516
|
+
async function createUser(username, password) {
|
|
1517
|
+
const db = await getDb();
|
|
1518
|
+
const hash = hashSync(password, 10);
|
|
1519
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
|
|
1520
|
+
const role = value === 0 ? "admin" : "pending";
|
|
1521
|
+
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
1522
|
+
id: users.id,
|
|
1523
|
+
username: users.username,
|
|
1524
|
+
role: users.role,
|
|
1525
|
+
user_type: users.user_type,
|
|
1526
|
+
created_at: users.created_at
|
|
1527
|
+
});
|
|
1528
|
+
return result;
|
|
1529
|
+
}
|
|
1530
|
+
async function verifyUser(username, password) {
|
|
1531
|
+
const db = await getDb();
|
|
1532
|
+
const row = await db.select().from(users).where(eq(users.username, username)).get();
|
|
1533
|
+
if (!row) return null;
|
|
1534
|
+
if (row.user_type === "mind") return null;
|
|
1535
|
+
if (!compareSync(password, row.password_hash)) return null;
|
|
1536
|
+
const { password_hash: _, ...user } = row;
|
|
1537
|
+
return user;
|
|
1538
|
+
}
|
|
1539
|
+
async function getUser(id) {
|
|
1540
|
+
const db = await getDb();
|
|
1541
|
+
const row = await db.select({
|
|
1542
|
+
id: users.id,
|
|
1500
1543
|
username: users.username,
|
|
1501
1544
|
role: users.role,
|
|
1502
1545
|
user_type: users.user_type,
|
|
@@ -1680,8 +1723,8 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
1680
1723
|
|
|
1681
1724
|
// src/web/server.ts
|
|
1682
1725
|
import { existsSync as existsSync15 } from "fs";
|
|
1683
|
-
import { readFile as readFile3, stat as
|
|
1684
|
-
import { dirname as
|
|
1726
|
+
import { readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
1727
|
+
import { dirname as dirname2, extname as extname3, resolve as resolve21 } from "path";
|
|
1685
1728
|
import { serve } from "@hono/node-server";
|
|
1686
1729
|
|
|
1687
1730
|
// src/web/app.ts
|
|
@@ -1695,7 +1738,7 @@ import { desc as desc2 } from "drizzle-orm";
|
|
|
1695
1738
|
import { Hono } from "hono";
|
|
1696
1739
|
import { streamSSE } from "hono/streaming";
|
|
1697
1740
|
|
|
1698
|
-
// src/lib/conversations.ts
|
|
1741
|
+
// src/lib/events/conversations.ts
|
|
1699
1742
|
import { randomUUID } from "crypto";
|
|
1700
1743
|
import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
|
|
1701
1744
|
async function createConversation(mindName, channel, opts) {
|
|
@@ -2067,51 +2110,710 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
|
|
|
2067
2110
|
if (existing) {
|
|
2068
2111
|
return c.json({ error: "Username already taken" }, 409);
|
|
2069
2112
|
}
|
|
2070
|
-
const user = await createUser(username, password);
|
|
2071
|
-
if (user.role === "admin") {
|
|
2072
|
-
const sessionId = await createSession(user.id);
|
|
2073
|
-
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
2113
|
+
const user = await createUser(username, password);
|
|
2114
|
+
if (user.role === "admin") {
|
|
2115
|
+
const sessionId = await createSession(user.id);
|
|
2116
|
+
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
2117
|
+
}
|
|
2118
|
+
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
2119
|
+
}).post("/login", zValidator("json", credentialsSchema), async (c) => {
|
|
2120
|
+
const { username, password } = c.req.valid("json");
|
|
2121
|
+
const user = await verifyUser(username, password);
|
|
2122
|
+
if (!user) {
|
|
2123
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
2124
|
+
}
|
|
2125
|
+
const sessionId = await createSession(user.id);
|
|
2126
|
+
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
2127
|
+
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
2128
|
+
}).post("/logout", async (c) => {
|
|
2129
|
+
const sessionId = getCookie2(c, "volute_session");
|
|
2130
|
+
if (sessionId) {
|
|
2131
|
+
await deleteSession(sessionId);
|
|
2132
|
+
deleteCookie(c, "volute_session", { path: "/" });
|
|
2133
|
+
}
|
|
2134
|
+
return c.json({ ok: true });
|
|
2135
|
+
}).get("/me", async (c) => {
|
|
2136
|
+
const sessionId = getCookie2(c, "volute_session");
|
|
2137
|
+
if (!sessionId) return c.json({ error: "Not logged in" }, 401);
|
|
2138
|
+
const userId = await getSessionUserId(sessionId);
|
|
2139
|
+
if (userId == null) return c.json({ error: "Not logged in" }, 401);
|
|
2140
|
+
const user = await getUser(userId);
|
|
2141
|
+
if (!user) return c.json({ error: "Not logged in" }, 401);
|
|
2142
|
+
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
2143
|
+
}).route("/", admin).route("/", authenticated);
|
|
2144
|
+
var auth_default = app2;
|
|
2145
|
+
|
|
2146
|
+
// src/web/api/channels.ts
|
|
2147
|
+
import { Hono as Hono3 } from "hono";
|
|
2148
|
+
|
|
2149
|
+
// src/lib/channels/discord.ts
|
|
2150
|
+
var discord_exports = {};
|
|
2151
|
+
__export(discord_exports, {
|
|
2152
|
+
createConversation: () => createConversation2,
|
|
2153
|
+
listConversations: () => listConversations,
|
|
2154
|
+
listUsers: () => listUsers2,
|
|
2155
|
+
read: () => read,
|
|
2156
|
+
send: () => send
|
|
2157
|
+
});
|
|
2158
|
+
var DISCORD_MAX_LENGTH = 2e3;
|
|
2159
|
+
var API_BASE = "https://discord.com/api/v10";
|
|
2160
|
+
function requireToken(env) {
|
|
2161
|
+
const token = env.DISCORD_TOKEN;
|
|
2162
|
+
if (!token) throw new Error("DISCORD_TOKEN not set");
|
|
2163
|
+
return token;
|
|
2164
|
+
}
|
|
2165
|
+
async function discordGet(token, path) {
|
|
2166
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
2167
|
+
headers: { Authorization: `Bot ${token}` }
|
|
2168
|
+
});
|
|
2169
|
+
if (!res.ok) {
|
|
2170
|
+
throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
|
|
2171
|
+
}
|
|
2172
|
+
return res.json();
|
|
2173
|
+
}
|
|
2174
|
+
async function read(env, channelSlug, limit) {
|
|
2175
|
+
const token = requireToken(env);
|
|
2176
|
+
const channelId = resolveChannelId2(env, channelSlug);
|
|
2177
|
+
const res = await fetch(`${API_BASE}/channels/${channelId}/messages?limit=${limit}`, {
|
|
2178
|
+
headers: { Authorization: `Bot ${token}` }
|
|
2179
|
+
});
|
|
2180
|
+
if (!res.ok) {
|
|
2181
|
+
throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
|
|
2182
|
+
}
|
|
2183
|
+
const messages2 = await res.json();
|
|
2184
|
+
return messages2.reverse().map((m) => `${m.author.username}: ${m.content}`).join("\n");
|
|
2185
|
+
}
|
|
2186
|
+
async function send(env, channelSlug, message, images) {
|
|
2187
|
+
const token = requireToken(env);
|
|
2188
|
+
const channelId = resolveChannelId2(env, channelSlug);
|
|
2189
|
+
if (images?.length) {
|
|
2190
|
+
for (let i = 0; i < images.length; i++) {
|
|
2191
|
+
const img = images[i];
|
|
2192
|
+
const ext = img.media_type.split("/")[1] || "png";
|
|
2193
|
+
const form = new FormData();
|
|
2194
|
+
const content = i === 0 ? message.slice(0, DISCORD_MAX_LENGTH) : "";
|
|
2195
|
+
form.append("payload_json", JSON.stringify({ content }));
|
|
2196
|
+
form.append(
|
|
2197
|
+
"files[0]",
|
|
2198
|
+
new Blob([Buffer.from(img.data, "base64")], { type: img.media_type }),
|
|
2199
|
+
`image.${ext}`
|
|
2200
|
+
);
|
|
2201
|
+
const res = await fetch(`${API_BASE}/channels/${channelId}/messages`, {
|
|
2202
|
+
method: "POST",
|
|
2203
|
+
headers: { Authorization: `Bot ${token}` },
|
|
2204
|
+
body: form
|
|
2205
|
+
});
|
|
2206
|
+
if (!res.ok) {
|
|
2207
|
+
const body = await res.text().catch(() => "");
|
|
2208
|
+
const partial = i > 0 ? ` (${i}/${images.length} images were already sent)` : "";
|
|
2209
|
+
throw new Error(`Discord API error: ${res.status} ${body || res.statusText}${partial}`);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const chunks = splitMessage(message, DISCORD_MAX_LENGTH);
|
|
2215
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2216
|
+
const res = await fetch(`${API_BASE}/channels/${channelId}/messages`, {
|
|
2217
|
+
method: "POST",
|
|
2218
|
+
headers: {
|
|
2219
|
+
Authorization: `Bot ${token}`,
|
|
2220
|
+
"Content-Type": "application/json"
|
|
2221
|
+
},
|
|
2222
|
+
body: JSON.stringify({ content: chunks[i] })
|
|
2223
|
+
});
|
|
2224
|
+
if (!res.ok) {
|
|
2225
|
+
const partial = i > 0 ? ` (${i}/${chunks.length} chunks were already sent)` : "";
|
|
2226
|
+
throw new Error(`Discord API error: ${res.status} ${res.statusText}${partial}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
async function listConversations(env) {
|
|
2231
|
+
const token = requireToken(env);
|
|
2232
|
+
const results = [];
|
|
2233
|
+
const guilds = await discordGet(token, "/users/@me/guilds");
|
|
2234
|
+
for (const guild of guilds) {
|
|
2235
|
+
const channels = await discordGet(token, `/guilds/${guild.id}/channels`);
|
|
2236
|
+
for (const ch of channels) {
|
|
2237
|
+
if (ch.type !== 0) continue;
|
|
2238
|
+
results.push({
|
|
2239
|
+
id: `discord:${slugify(guild.name)}/${slugify(ch.name)}`,
|
|
2240
|
+
platformId: ch.id,
|
|
2241
|
+
name: `#${ch.name}`,
|
|
2242
|
+
type: "channel"
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const dms = await discordGet(token, "/users/@me/channels");
|
|
2247
|
+
for (const dm of dms) {
|
|
2248
|
+
const recipients = dm.recipients?.map((r) => r.username) ?? [];
|
|
2249
|
+
const slug = recipients.length === 0 ? `discord:${dm.id}` : recipients.length === 1 ? `discord:@${slugify(recipients[0])}` : `discord:@${recipients.map(slugify).sort().join(",")}`;
|
|
2250
|
+
results.push({
|
|
2251
|
+
id: slug,
|
|
2252
|
+
platformId: dm.id,
|
|
2253
|
+
name: recipients.join(", ") || "DM",
|
|
2254
|
+
type: dm.type === 1 ? "dm" : "group"
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
return results;
|
|
2258
|
+
}
|
|
2259
|
+
async function listUsers2(env) {
|
|
2260
|
+
const token = requireToken(env);
|
|
2261
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2262
|
+
const guilds = await discordGet(token, "/users/@me/guilds");
|
|
2263
|
+
for (const guild of guilds) {
|
|
2264
|
+
const members = await discordGet(token, `/guilds/${guild.id}/members?limit=1000`);
|
|
2265
|
+
for (const m of members) {
|
|
2266
|
+
if (!seen.has(m.user.id)) {
|
|
2267
|
+
seen.set(m.user.id, {
|
|
2268
|
+
id: m.user.id,
|
|
2269
|
+
username: m.user.username,
|
|
2270
|
+
type: m.user.bot ? "bot" : "human"
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return [...seen.values()];
|
|
2276
|
+
}
|
|
2277
|
+
async function createConversation2(env, participants, _name) {
|
|
2278
|
+
const token = requireToken(env);
|
|
2279
|
+
if (participants.length !== 1) {
|
|
2280
|
+
throw new Error(
|
|
2281
|
+
"Discord group creation not supported via bot \u2014 use threads in an existing channel"
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
const allUsers = await listUsers2(env);
|
|
2285
|
+
const target = allUsers.find((u) => u.username.toLowerCase() === participants[0].toLowerCase());
|
|
2286
|
+
if (!target) {
|
|
2287
|
+
throw new Error(`User not found: ${participants[0]}`);
|
|
2288
|
+
}
|
|
2289
|
+
const res = await fetch(`${API_BASE}/users/@me/channels`, {
|
|
2290
|
+
method: "POST",
|
|
2291
|
+
headers: {
|
|
2292
|
+
Authorization: `Bot ${token}`,
|
|
2293
|
+
"Content-Type": "application/json"
|
|
2294
|
+
},
|
|
2295
|
+
body: JSON.stringify({ recipient_id: target.id })
|
|
2296
|
+
});
|
|
2297
|
+
if (!res.ok) {
|
|
2298
|
+
throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
|
|
2299
|
+
}
|
|
2300
|
+
const dm = await res.json();
|
|
2301
|
+
const slug = `discord:@${slugify(participants[0])}`;
|
|
2302
|
+
const mindName = env.VOLUTE_MIND;
|
|
2303
|
+
if (mindName) {
|
|
2304
|
+
writeChannelEntry(mindName, slug, {
|
|
2305
|
+
platformId: dm.id,
|
|
2306
|
+
platform: "discord",
|
|
2307
|
+
name: participants[0],
|
|
2308
|
+
type: "dm"
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
return slug;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
// src/lib/channels/slack.ts
|
|
2315
|
+
var slack_exports = {};
|
|
2316
|
+
__export(slack_exports, {
|
|
2317
|
+
createConversation: () => createConversation3,
|
|
2318
|
+
listConversations: () => listConversations2,
|
|
2319
|
+
listUsers: () => listUsers3,
|
|
2320
|
+
read: () => read2,
|
|
2321
|
+
send: () => send2
|
|
2322
|
+
});
|
|
2323
|
+
var SLACK_MAX_LENGTH = 4e3;
|
|
2324
|
+
var API_BASE2 = "https://slack.com/api";
|
|
2325
|
+
function requireToken2(env) {
|
|
2326
|
+
const token = env.SLACK_BOT_TOKEN;
|
|
2327
|
+
if (!token) throw new Error("SLACK_BOT_TOKEN not set");
|
|
2328
|
+
return token;
|
|
2329
|
+
}
|
|
2330
|
+
async function slackApi(token, method, body) {
|
|
2331
|
+
const res = await fetch(`${API_BASE2}/${method}`, {
|
|
2332
|
+
method: "POST",
|
|
2333
|
+
headers: {
|
|
2334
|
+
Authorization: `Bearer ${token}`,
|
|
2335
|
+
"Content-Type": "application/json"
|
|
2336
|
+
},
|
|
2337
|
+
body: JSON.stringify(body)
|
|
2338
|
+
});
|
|
2339
|
+
if (!res.ok) {
|
|
2340
|
+
throw new Error(`Slack API HTTP error: ${res.status} ${res.statusText}`);
|
|
2341
|
+
}
|
|
2342
|
+
const data = await res.json();
|
|
2343
|
+
if (!data.ok) {
|
|
2344
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
2345
|
+
}
|
|
2346
|
+
return data;
|
|
2347
|
+
}
|
|
2348
|
+
async function read2(env, channelSlug, limit) {
|
|
2349
|
+
const token = requireToken2(env);
|
|
2350
|
+
const channelId = resolveChannelId2(env, channelSlug);
|
|
2351
|
+
const data = await slackApi(token, "conversations.history", {
|
|
2352
|
+
channel: channelId,
|
|
2353
|
+
limit
|
|
2354
|
+
});
|
|
2355
|
+
return data.messages.reverse().map((m) => `${m.user ?? m.bot_id ?? "unknown"}: ${m.text}`).join("\n");
|
|
2356
|
+
}
|
|
2357
|
+
async function send2(env, channelSlug, message, images) {
|
|
2358
|
+
const token = requireToken2(env);
|
|
2359
|
+
const channelId = resolveChannelId2(env, channelSlug);
|
|
2360
|
+
if (images?.length) {
|
|
2361
|
+
for (const img of images) {
|
|
2362
|
+
const ext = img.media_type.split("/")[1] || "png";
|
|
2363
|
+
const filename = `image.${ext}`;
|
|
2364
|
+
const binary = Buffer.from(img.data, "base64");
|
|
2365
|
+
const uploadData = await slackApi(token, "files.getUploadURLExternal", {
|
|
2366
|
+
filename,
|
|
2367
|
+
length: binary.length
|
|
2368
|
+
});
|
|
2369
|
+
const uploadRes = await fetch(uploadData.upload_url, {
|
|
2370
|
+
method: "POST",
|
|
2371
|
+
body: binary
|
|
2372
|
+
});
|
|
2373
|
+
if (!uploadRes.ok) {
|
|
2374
|
+
throw new Error(`Slack file upload failed: ${uploadRes.status} ${uploadRes.statusText}`);
|
|
2375
|
+
}
|
|
2376
|
+
await slackApi(token, "files.completeUploadExternal", {
|
|
2377
|
+
files: [{ id: uploadData.file_id }],
|
|
2378
|
+
channel_id: channelId
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
if (message) {
|
|
2382
|
+
const chunks2 = splitMessage(message, SLACK_MAX_LENGTH);
|
|
2383
|
+
for (const chunk of chunks2) {
|
|
2384
|
+
await slackApi(token, "chat.postMessage", {
|
|
2385
|
+
channel: channelId,
|
|
2386
|
+
text: chunk
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
const chunks = splitMessage(message, SLACK_MAX_LENGTH);
|
|
2393
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2394
|
+
try {
|
|
2395
|
+
await slackApi(token, "chat.postMessage", {
|
|
2396
|
+
channel: channelId,
|
|
2397
|
+
text: chunks[i]
|
|
2398
|
+
});
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
const partial = i > 0 ? ` (${i}/${chunks.length} chunks were already sent)` : "";
|
|
2401
|
+
throw new Error(`${err instanceof Error ? err.message : err}${partial}`);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
async function listConversations2(env) {
|
|
2406
|
+
const token = requireToken2(env);
|
|
2407
|
+
const authData = await slackApi(token, "auth.test", {});
|
|
2408
|
+
const teamName = authData.team ?? "workspace";
|
|
2409
|
+
const data = await slackApi(token, "conversations.list", {
|
|
2410
|
+
types: "public_channel,private_channel,mpim,im",
|
|
2411
|
+
limit: 1e3
|
|
2412
|
+
});
|
|
2413
|
+
const userMap = /* @__PURE__ */ new Map();
|
|
2414
|
+
const imChannels = data.channels.filter((ch) => ch.is_im && ch.user);
|
|
2415
|
+
if (imChannels.length > 0) {
|
|
2416
|
+
const users2 = await listUsers3(env);
|
|
2417
|
+
for (const u of users2) {
|
|
2418
|
+
userMap.set(u.id, u.username);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return data.channels.map((ch) => {
|
|
2422
|
+
let type = "channel";
|
|
2423
|
+
if (ch.is_im) type = "dm";
|
|
2424
|
+
else if (ch.is_mpim) type = "group";
|
|
2425
|
+
let slug;
|
|
2426
|
+
let name;
|
|
2427
|
+
if (ch.is_im && ch.user) {
|
|
2428
|
+
const username = userMap.get(ch.user) ?? ch.user;
|
|
2429
|
+
slug = `slack:@${slugify(username)}`;
|
|
2430
|
+
name = username;
|
|
2431
|
+
} else if (ch.name) {
|
|
2432
|
+
slug = `slack:${slugify(teamName)}/${slugify(ch.name)}`;
|
|
2433
|
+
name = ch.name;
|
|
2434
|
+
} else {
|
|
2435
|
+
slug = `slack:${ch.id}`;
|
|
2436
|
+
name = ch.id;
|
|
2437
|
+
}
|
|
2438
|
+
return {
|
|
2439
|
+
id: slug,
|
|
2440
|
+
platformId: ch.id,
|
|
2441
|
+
name,
|
|
2442
|
+
type,
|
|
2443
|
+
participantCount: ch.num_members
|
|
2444
|
+
};
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
async function listUsers3(env) {
|
|
2448
|
+
const token = requireToken2(env);
|
|
2449
|
+
const data = await slackApi(token, "users.list", {});
|
|
2450
|
+
return data.members.filter((m) => !m.deleted).map((m) => ({
|
|
2451
|
+
id: m.id,
|
|
2452
|
+
username: m.name,
|
|
2453
|
+
type: m.is_bot ? "bot" : "human"
|
|
2454
|
+
}));
|
|
2455
|
+
}
|
|
2456
|
+
async function createConversation3(env, participants, name) {
|
|
2457
|
+
const token = requireToken2(env);
|
|
2458
|
+
const allUsers = await listUsers3(env);
|
|
2459
|
+
const ids = [];
|
|
2460
|
+
for (const p of participants) {
|
|
2461
|
+
const user = allUsers.find((u) => u.username.toLowerCase() === p.toLowerCase());
|
|
2462
|
+
if (!user) throw new Error(`User not found: ${p}`);
|
|
2463
|
+
ids.push(user.id);
|
|
2464
|
+
}
|
|
2465
|
+
const mindName = env.VOLUTE_MIND;
|
|
2466
|
+
if (name) {
|
|
2467
|
+
const createData = await slackApi(token, "conversations.create", {
|
|
2468
|
+
name,
|
|
2469
|
+
is_private: true
|
|
2470
|
+
});
|
|
2471
|
+
const channelId = createData.channel.id;
|
|
2472
|
+
for (const userId of ids) {
|
|
2473
|
+
await slackApi(token, "conversations.invite", {
|
|
2474
|
+
channel: channelId,
|
|
2475
|
+
users: userId
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
const authData = await slackApi(token, "auth.test", {});
|
|
2479
|
+
const teamName = authData.team ?? "workspace";
|
|
2480
|
+
const slug2 = `slack:${slugify(teamName)}/${slugify(name)}`;
|
|
2481
|
+
if (mindName) {
|
|
2482
|
+
writeChannelEntry(mindName, slug2, {
|
|
2483
|
+
platformId: channelId,
|
|
2484
|
+
platform: "slack",
|
|
2485
|
+
name,
|
|
2486
|
+
type: "channel"
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
return slug2;
|
|
2490
|
+
}
|
|
2491
|
+
const openData = await slackApi(token, "conversations.open", {
|
|
2492
|
+
users: ids.join(",")
|
|
2493
|
+
});
|
|
2494
|
+
const platformId = openData.channel.id;
|
|
2495
|
+
const slug = participants.length === 1 ? `slack:@${slugify(participants[0])}` : `slack:@${participants.map(slugify).sort().join(",")}`;
|
|
2496
|
+
if (mindName) {
|
|
2497
|
+
writeChannelEntry(mindName, slug, {
|
|
2498
|
+
platformId,
|
|
2499
|
+
platform: "slack",
|
|
2500
|
+
name: participants.join(", "),
|
|
2501
|
+
type: participants.length === 1 ? "dm" : "group"
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
return slug;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// src/lib/channels/telegram.ts
|
|
2508
|
+
var telegram_exports = {};
|
|
2509
|
+
__export(telegram_exports, {
|
|
2510
|
+
createConversation: () => createConversation4,
|
|
2511
|
+
listConversations: () => listConversations3,
|
|
2512
|
+
listUsers: () => listUsers4,
|
|
2513
|
+
read: () => read3,
|
|
2514
|
+
send: () => send3
|
|
2515
|
+
});
|
|
2516
|
+
var TELEGRAM_MAX_LENGTH = 4096;
|
|
2517
|
+
var API_BASE3 = "https://api.telegram.org";
|
|
2518
|
+
function requireToken3(env) {
|
|
2519
|
+
const token = env.TELEGRAM_BOT_TOKEN;
|
|
2520
|
+
if (!token) throw new Error("TELEGRAM_BOT_TOKEN not set");
|
|
2521
|
+
return token;
|
|
2522
|
+
}
|
|
2523
|
+
async function read3(_env, _channelSlug, _limit) {
|
|
2524
|
+
throw new Error(
|
|
2525
|
+
"Telegram Bot API does not support reading chat history. Use volute send instead."
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
async function send3(env, channelSlug, message, images) {
|
|
2529
|
+
const token = requireToken3(env);
|
|
2530
|
+
const chatId = resolveChannelId2(env, channelSlug);
|
|
2531
|
+
if (images?.length) {
|
|
2532
|
+
const CAPTION_MAX = 1024;
|
|
2533
|
+
for (let i = 0; i < images.length; i++) {
|
|
2534
|
+
const img = images[i];
|
|
2535
|
+
const ext = img.media_type.split("/")[1] || "png";
|
|
2536
|
+
const form = new FormData();
|
|
2537
|
+
form.append("chat_id", chatId);
|
|
2538
|
+
form.append(
|
|
2539
|
+
"photo",
|
|
2540
|
+
new Blob([Buffer.from(img.data, "base64")], { type: img.media_type }),
|
|
2541
|
+
`image.${ext}`
|
|
2542
|
+
);
|
|
2543
|
+
if (i === 0 && message) {
|
|
2544
|
+
form.append("caption", message.slice(0, CAPTION_MAX));
|
|
2545
|
+
}
|
|
2546
|
+
const res = await fetch(`${API_BASE3}/bot${token}/sendPhoto`, {
|
|
2547
|
+
method: "POST",
|
|
2548
|
+
body: form
|
|
2549
|
+
});
|
|
2550
|
+
if (!res.ok) {
|
|
2551
|
+
const body = await res.text().catch(() => "");
|
|
2552
|
+
const partial = i > 0 ? ` (${i}/${images.length} images were already sent)` : "";
|
|
2553
|
+
throw new Error(`Telegram API error: ${res.status} ${body}${partial}`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
if (message && message.length > CAPTION_MAX) {
|
|
2557
|
+
const remaining = message.slice(CAPTION_MAX);
|
|
2558
|
+
const chunks2 = splitMessage(remaining, TELEGRAM_MAX_LENGTH);
|
|
2559
|
+
for (const chunk of chunks2) {
|
|
2560
|
+
const res = await fetch(`${API_BASE3}/bot${token}/sendMessage`, {
|
|
2561
|
+
method: "POST",
|
|
2562
|
+
headers: { "Content-Type": "application/json" },
|
|
2563
|
+
body: JSON.stringify({ chat_id: chatId, text: chunk })
|
|
2564
|
+
});
|
|
2565
|
+
if (!res.ok) {
|
|
2566
|
+
const body = await res.text().catch(() => "");
|
|
2567
|
+
throw new Error(`Telegram API error: ${res.status} ${body}`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
const chunks = splitMessage(message, TELEGRAM_MAX_LENGTH);
|
|
2574
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2575
|
+
const res = await fetch(`${API_BASE3}/bot${token}/sendMessage`, {
|
|
2576
|
+
method: "POST",
|
|
2577
|
+
headers: { "Content-Type": "application/json" },
|
|
2578
|
+
body: JSON.stringify({ chat_id: chatId, text: chunks[i] })
|
|
2579
|
+
});
|
|
2580
|
+
if (!res.ok) {
|
|
2581
|
+
const body = await res.text().catch(() => "");
|
|
2582
|
+
const partial = i > 0 ? ` (${i}/${chunks.length} chunks were already sent)` : "";
|
|
2583
|
+
throw new Error(`Telegram API error: ${res.status} ${body}${partial}`);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
async function listConversations3() {
|
|
2588
|
+
throw new Error(
|
|
2589
|
+
"Telegram Bot API does not support listing conversations. Users must message the bot first."
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
async function listUsers4() {
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
"Telegram Bot API does not support listing users. Users must message the bot first."
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
async function createConversation4() {
|
|
2598
|
+
throw new Error(
|
|
2599
|
+
"Telegram Bot API does not support creating conversations. Users must message the bot first."
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// src/lib/channels/volute.ts
|
|
2604
|
+
var volute_exports = {};
|
|
2605
|
+
__export(volute_exports, {
|
|
2606
|
+
createConversation: () => createConversation5,
|
|
2607
|
+
listConversations: () => listConversations4,
|
|
2608
|
+
listUsers: () => listUsers5,
|
|
2609
|
+
read: () => read4,
|
|
2610
|
+
send: () => send4
|
|
2611
|
+
});
|
|
2612
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
2613
|
+
import { resolve as resolve8 } from "path";
|
|
2614
|
+
function getDaemonConfig() {
|
|
2615
|
+
const configPath2 = resolve8(voluteHome(), "daemon.json");
|
|
2616
|
+
if (!existsSync7(configPath2)) {
|
|
2617
|
+
throw new Error("Volute daemon is not running");
|
|
2618
|
+
}
|
|
2619
|
+
let config;
|
|
2620
|
+
try {
|
|
2621
|
+
config = JSON.parse(readFileSync5(configPath2, "utf-8"));
|
|
2622
|
+
} catch (err) {
|
|
2623
|
+
throw new Error(`Failed to parse ${configPath2}: ${err}`);
|
|
2624
|
+
}
|
|
2625
|
+
if (typeof config.port !== "number") {
|
|
2626
|
+
throw new Error(`Invalid or missing port in ${configPath2}`);
|
|
2627
|
+
}
|
|
2628
|
+
const url = new URL("http://localhost");
|
|
2629
|
+
url.hostname = config.hostname || "localhost";
|
|
2630
|
+
url.port = String(config.port);
|
|
2631
|
+
return { url: url.origin, token: config.token };
|
|
2632
|
+
}
|
|
2633
|
+
async function read4(env, channelSlug, limit) {
|
|
2634
|
+
const mindName = env.VOLUTE_MIND;
|
|
2635
|
+
if (!mindName) throw new Error("VOLUTE_MIND not set");
|
|
2636
|
+
const conversationId = resolveChannelId2(env, channelSlug);
|
|
2637
|
+
const { url, token } = getDaemonConfig();
|
|
2638
|
+
const headers = { Origin: url };
|
|
2639
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2640
|
+
const res = await fetch(
|
|
2641
|
+
`${url}/api/minds/${encodeURIComponent(mindName)}/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
2642
|
+
{ headers }
|
|
2643
|
+
);
|
|
2644
|
+
if (!res.ok) {
|
|
2645
|
+
throw new Error(`Failed to read conversation: ${res.status} ${res.statusText}`);
|
|
2646
|
+
}
|
|
2647
|
+
const messages2 = await res.json();
|
|
2648
|
+
return messages2.slice(-limit).map((m) => {
|
|
2649
|
+
const text = Array.isArray(m.content) ? m.content.filter((b) => b.type === "text").map((b) => b.text).join("") : m.content;
|
|
2650
|
+
return `${m.sender_name ?? m.role}: ${text}`;
|
|
2651
|
+
}).join("\n");
|
|
2652
|
+
}
|
|
2653
|
+
async function send4(env, channelSlug, message, images) {
|
|
2654
|
+
const mindName = env.VOLUTE_MIND;
|
|
2655
|
+
if (!mindName) throw new Error("VOLUTE_MIND not set");
|
|
2656
|
+
const conversationId = resolveChannelId2(env, channelSlug);
|
|
2657
|
+
const { url, token } = getDaemonConfig();
|
|
2658
|
+
const headers = {
|
|
2659
|
+
"Content-Type": "application/json",
|
|
2660
|
+
Origin: url
|
|
2661
|
+
};
|
|
2662
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2663
|
+
const res = await fetch(`${url}/api/minds/${encodeURIComponent(mindName)}/chat`, {
|
|
2664
|
+
method: "POST",
|
|
2665
|
+
headers,
|
|
2666
|
+
body: JSON.stringify({
|
|
2667
|
+
message,
|
|
2668
|
+
conversationId,
|
|
2669
|
+
sender: env.VOLUTE_SENDER ?? mindName,
|
|
2670
|
+
images
|
|
2671
|
+
})
|
|
2672
|
+
});
|
|
2673
|
+
if (!res.ok) {
|
|
2674
|
+
const data = await res.json().catch(() => ({}));
|
|
2675
|
+
throw new Error(data.error ?? `Failed to send: ${res.status}`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
async function listConversations4(env) {
|
|
2679
|
+
const mindName = env.VOLUTE_MIND;
|
|
2680
|
+
if (!mindName) throw new Error("VOLUTE_MIND not set");
|
|
2681
|
+
const { url, token } = getDaemonConfig();
|
|
2682
|
+
const headers = { Origin: url };
|
|
2683
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2684
|
+
const res = await fetch(`${url}/api/minds/${encodeURIComponent(mindName)}/conversations`, {
|
|
2685
|
+
headers
|
|
2686
|
+
});
|
|
2687
|
+
if (!res.ok) {
|
|
2688
|
+
throw new Error(`Failed to list conversations: ${res.status} ${res.statusText}`);
|
|
2689
|
+
}
|
|
2690
|
+
const convs = await res.json();
|
|
2691
|
+
const results = [];
|
|
2692
|
+
for (const conv of convs) {
|
|
2693
|
+
let participants = [];
|
|
2694
|
+
try {
|
|
2695
|
+
const pRes = await fetch(
|
|
2696
|
+
`${url}/api/minds/${encodeURIComponent(mindName)}/conversations/${encodeURIComponent(conv.id)}/participants`,
|
|
2697
|
+
{ headers }
|
|
2698
|
+
);
|
|
2699
|
+
if (pRes.ok) {
|
|
2700
|
+
participants = await pRes.json();
|
|
2701
|
+
} else {
|
|
2702
|
+
console.error(`[volute] failed to fetch participants for ${conv.id}: HTTP ${pRes.status}`);
|
|
2703
|
+
}
|
|
2704
|
+
} catch (err) {
|
|
2705
|
+
console.error(`[volute] failed to fetch participants for ${conv.id}:`, err);
|
|
2706
|
+
}
|
|
2707
|
+
const slug = buildVoluteSlug({
|
|
2708
|
+
participants,
|
|
2709
|
+
mindUsername: mindName,
|
|
2710
|
+
convTitle: conv.title,
|
|
2711
|
+
conversationId: conv.id,
|
|
2712
|
+
convType: conv.type,
|
|
2713
|
+
convName: conv.name
|
|
2714
|
+
});
|
|
2715
|
+
const convType = conv.type === "channel" ? "channel" : participants.length === 2 ? "dm" : "group";
|
|
2716
|
+
results.push({
|
|
2717
|
+
id: slug,
|
|
2718
|
+
platformId: conv.id,
|
|
2719
|
+
name: conv.type === "channel" ? `#${conv.name}` : conv.title ?? "(untitled)",
|
|
2720
|
+
type: convType,
|
|
2721
|
+
participantCount: participants.length
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
return results;
|
|
2725
|
+
}
|
|
2726
|
+
async function listUsers5(_env) {
|
|
2727
|
+
const { url, token } = getDaemonConfig();
|
|
2728
|
+
const headers = { Origin: url };
|
|
2729
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2730
|
+
const res = await fetch(`${url}/api/auth/users`, { headers });
|
|
2731
|
+
if (!res.ok) {
|
|
2732
|
+
throw new Error(`Failed to list users: ${res.status} ${res.statusText}`);
|
|
2074
2733
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2734
|
+
const data = await res.json();
|
|
2735
|
+
return data.map((u) => ({
|
|
2736
|
+
id: String(u.id),
|
|
2737
|
+
username: u.username,
|
|
2738
|
+
type: u.user_type
|
|
2739
|
+
}));
|
|
2740
|
+
}
|
|
2741
|
+
async function createConversation5(env, participants, name) {
|
|
2742
|
+
const mindName = env.VOLUTE_MIND;
|
|
2743
|
+
if (!mindName) throw new Error("VOLUTE_MIND not set");
|
|
2744
|
+
const { url, token } = getDaemonConfig();
|
|
2745
|
+
const headers = {
|
|
2746
|
+
"Content-Type": "application/json",
|
|
2747
|
+
Origin: url
|
|
2748
|
+
};
|
|
2749
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2750
|
+
const res = await fetch(`${url}/api/minds/${encodeURIComponent(mindName)}/conversations`, {
|
|
2751
|
+
method: "POST",
|
|
2752
|
+
headers,
|
|
2753
|
+
body: JSON.stringify({ participantNames: participants, title: name })
|
|
2754
|
+
});
|
|
2755
|
+
if (!res.ok) {
|
|
2756
|
+
const data = await res.json().catch(() => ({}));
|
|
2757
|
+
throw new Error(data.error ?? `Failed to create conversation: ${res.status}`);
|
|
2081
2758
|
}
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2759
|
+
const conv = await res.json();
|
|
2760
|
+
return `volute:${conv.id}`;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// src/lib/channels.ts
|
|
2764
|
+
var CHANNELS = {
|
|
2765
|
+
volute: {
|
|
2766
|
+
name: "volute",
|
|
2767
|
+
displayName: "Volute",
|
|
2768
|
+
showToolCalls: true,
|
|
2769
|
+
builtIn: true,
|
|
2770
|
+
driver: volute_exports
|
|
2771
|
+
},
|
|
2772
|
+
discord: {
|
|
2773
|
+
name: "discord",
|
|
2774
|
+
displayName: "Discord",
|
|
2775
|
+
showToolCalls: false,
|
|
2776
|
+
driver: discord_exports
|
|
2777
|
+
},
|
|
2778
|
+
slack: {
|
|
2779
|
+
name: "slack",
|
|
2780
|
+
displayName: "Slack",
|
|
2781
|
+
showToolCalls: false,
|
|
2782
|
+
driver: slack_exports
|
|
2783
|
+
},
|
|
2784
|
+
telegram: {
|
|
2785
|
+
name: "telegram",
|
|
2786
|
+
displayName: "Telegram",
|
|
2787
|
+
showToolCalls: false,
|
|
2788
|
+
driver: telegram_exports
|
|
2789
|
+
},
|
|
2790
|
+
mail: { name: "mail", displayName: "Email", showToolCalls: false },
|
|
2791
|
+
system: { name: "system", displayName: "System", showToolCalls: false }
|
|
2792
|
+
};
|
|
2793
|
+
function getChannelDriver(platform) {
|
|
2794
|
+
return CHANNELS[platform]?.driver ?? null;
|
|
2795
|
+
}
|
|
2796
|
+
function resolveChannelId2(env, slug) {
|
|
2797
|
+
const mindName = env.VOLUTE_MIND;
|
|
2798
|
+
if (!mindName) {
|
|
2799
|
+
const colonIdx = slug.indexOf(":");
|
|
2800
|
+
return colonIdx !== -1 ? slug.slice(colonIdx + 1) : slug;
|
|
2090
2801
|
}
|
|
2091
|
-
return
|
|
2092
|
-
}
|
|
2093
|
-
const sessionId = getCookie2(c, "volute_session");
|
|
2094
|
-
if (!sessionId) return c.json({ error: "Not logged in" }, 401);
|
|
2095
|
-
const userId = await getSessionUserId(sessionId);
|
|
2096
|
-
if (userId == null) return c.json({ error: "Not logged in" }, 401);
|
|
2097
|
-
const user = await getUser(userId);
|
|
2098
|
-
if (!user) return c.json({ error: "Not logged in" }, 401);
|
|
2099
|
-
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
2100
|
-
}).route("/", admin).route("/", authenticated);
|
|
2101
|
-
var auth_default = app2;
|
|
2802
|
+
return resolveChannelId(mindName, slug);
|
|
2803
|
+
}
|
|
2102
2804
|
|
|
2103
2805
|
// src/web/api/channels.ts
|
|
2104
|
-
import { Hono as Hono3 } from "hono";
|
|
2105
2806
|
function buildEnv(name) {
|
|
2106
2807
|
return { ...loadMergedEnv(name), VOLUTE_MIND: name, VOLUTE_MIND_DIR: mindDir(name) };
|
|
2107
2808
|
}
|
|
2108
2809
|
var app3 = new Hono3().post("/:name/channels/send", requireAdmin, async (c) => {
|
|
2109
2810
|
const name = c.req.param("name");
|
|
2110
2811
|
if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
|
|
2111
|
-
const { platform, uri, message, images } = await c.req.json();
|
|
2812
|
+
const { platform, uri, message, images, sender } = await c.req.json();
|
|
2112
2813
|
const driver = getChannelDriver(platform);
|
|
2113
2814
|
if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
|
|
2114
2815
|
const env = buildEnv(name);
|
|
2816
|
+
if (sender) env.VOLUTE_SENDER = sender;
|
|
2115
2817
|
try {
|
|
2116
2818
|
await driver.send(env, uri, message, images);
|
|
2117
2819
|
return c.json({ ok: true });
|
|
@@ -2181,13 +2883,15 @@ var app3 = new Hono3().post("/:name/channels/send", requireAdmin, async (c) => {
|
|
|
2181
2883
|
const {
|
|
2182
2884
|
platform,
|
|
2183
2885
|
participants,
|
|
2184
|
-
name: convName
|
|
2886
|
+
name: convName,
|
|
2887
|
+
sender
|
|
2185
2888
|
} = await c.req.json();
|
|
2186
2889
|
const driver = getChannelDriver(platform);
|
|
2187
2890
|
if (!driver?.createConversation) {
|
|
2188
2891
|
return c.json({ error: `Platform ${platform} does not support creating conversations` }, 400);
|
|
2189
2892
|
}
|
|
2190
2893
|
const env = buildEnv(name);
|
|
2894
|
+
if (sender) env.VOLUTE_SENDER = sender;
|
|
2191
2895
|
try {
|
|
2192
2896
|
const slug = await driver.createConversation(env, participants, convName);
|
|
2193
2897
|
return c.json({ slug });
|
|
@@ -2351,14 +3055,14 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
|
|
|
2351
3055
|
var env_default = app5;
|
|
2352
3056
|
|
|
2353
3057
|
// src/web/api/file-sharing.ts
|
|
2354
|
-
import { readFileSync as
|
|
2355
|
-
import { resolve as
|
|
3058
|
+
import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
3059
|
+
import { resolve as resolve10 } from "path";
|
|
2356
3060
|
import { Hono as Hono6 } from "hono";
|
|
2357
3061
|
|
|
2358
3062
|
// src/lib/file-sharing.ts
|
|
2359
3063
|
import { randomBytes } from "crypto";
|
|
2360
|
-
import { existsSync as
|
|
2361
|
-
import { basename, join as join2, normalize, resolve as
|
|
3064
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
3065
|
+
import { basename, join as join2, normalize, resolve as resolve9 } from "path";
|
|
2362
3066
|
function validateFilePath(filePath) {
|
|
2363
3067
|
if (!filePath) return "File path is required";
|
|
2364
3068
|
const normalized = normalize(filePath);
|
|
@@ -2371,13 +3075,13 @@ function validateFilePath(filePath) {
|
|
|
2371
3075
|
return null;
|
|
2372
3076
|
}
|
|
2373
3077
|
function configPath(dir) {
|
|
2374
|
-
return
|
|
3078
|
+
return resolve9(dir, "home", ".config", "file-sharing.json");
|
|
2375
3079
|
}
|
|
2376
3080
|
function readFileSharingConfig(dir) {
|
|
2377
3081
|
const p = configPath(dir);
|
|
2378
|
-
if (!
|
|
3082
|
+
if (!existsSync8(p)) return {};
|
|
2379
3083
|
try {
|
|
2380
|
-
return JSON.parse(
|
|
3084
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
2381
3085
|
} catch (err) {
|
|
2382
3086
|
console.warn(`[file-sharing] failed to parse config at ${p}:`, err);
|
|
2383
3087
|
return {};
|
|
@@ -2385,7 +3089,7 @@ function readFileSharingConfig(dir) {
|
|
|
2385
3089
|
}
|
|
2386
3090
|
function writeFileSharingConfig(dir, config) {
|
|
2387
3091
|
const p = configPath(dir);
|
|
2388
|
-
mkdirSync4(
|
|
3092
|
+
mkdirSync4(resolve9(p, ".."), { recursive: true });
|
|
2389
3093
|
writeFileSync4(p, `${JSON.stringify(config, null, 2)}
|
|
2390
3094
|
`);
|
|
2391
3095
|
}
|
|
@@ -2409,7 +3113,7 @@ function removeTrust(dir, sender) {
|
|
|
2409
3113
|
writeFileSharingConfig(dir, config);
|
|
2410
3114
|
}
|
|
2411
3115
|
function pendingDir(receiver) {
|
|
2412
|
-
return
|
|
3116
|
+
return resolve9(stateDir(receiver), "pending-files");
|
|
2413
3117
|
}
|
|
2414
3118
|
function validateId(id) {
|
|
2415
3119
|
if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
@@ -2428,7 +3132,7 @@ function stageFile(receiver, sender, filename, content, originalPath) {
|
|
|
2428
3132
|
throw new Error("Invalid sender name");
|
|
2429
3133
|
}
|
|
2430
3134
|
const id = generateId(sender);
|
|
2431
|
-
const dir =
|
|
3135
|
+
const dir = resolve9(pendingDir(receiver), id);
|
|
2432
3136
|
mkdirSync4(dir, { recursive: true });
|
|
2433
3137
|
const metadata = {
|
|
2434
3138
|
id,
|
|
@@ -2438,22 +3142,22 @@ function stageFile(receiver, sender, filename, content, originalPath) {
|
|
|
2438
3142
|
size: content.length,
|
|
2439
3143
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2440
3144
|
};
|
|
2441
|
-
writeFileSync4(
|
|
3145
|
+
writeFileSync4(resolve9(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
|
|
2442
3146
|
`);
|
|
2443
|
-
writeFileSync4(
|
|
3147
|
+
writeFileSync4(resolve9(dir, "data"), content);
|
|
2444
3148
|
return { id };
|
|
2445
3149
|
}
|
|
2446
3150
|
function listPending(receiver) {
|
|
2447
3151
|
const dir = pendingDir(receiver);
|
|
2448
|
-
if (!
|
|
3152
|
+
if (!existsSync8(dir)) return [];
|
|
2449
3153
|
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
2450
3154
|
const result = [];
|
|
2451
3155
|
for (const entry of entries) {
|
|
2452
3156
|
if (!entry.isDirectory()) continue;
|
|
2453
|
-
const metaPath =
|
|
2454
|
-
if (!
|
|
3157
|
+
const metaPath = resolve9(dir, entry.name, "metadata.json");
|
|
3158
|
+
if (!existsSync8(metaPath)) continue;
|
|
2455
3159
|
try {
|
|
2456
|
-
result.push(JSON.parse(
|
|
3160
|
+
result.push(JSON.parse(readFileSync6(metaPath, "utf-8")));
|
|
2457
3161
|
} catch (err) {
|
|
2458
3162
|
console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
|
|
2459
3163
|
}
|
|
@@ -2462,10 +3166,10 @@ function listPending(receiver) {
|
|
|
2462
3166
|
}
|
|
2463
3167
|
function getPending(receiver, id) {
|
|
2464
3168
|
validateId(id);
|
|
2465
|
-
const metaPath =
|
|
2466
|
-
if (!
|
|
3169
|
+
const metaPath = resolve9(pendingDir(receiver), id, "metadata.json");
|
|
3170
|
+
if (!existsSync8(metaPath)) return null;
|
|
2467
3171
|
try {
|
|
2468
|
-
return JSON.parse(
|
|
3172
|
+
return JSON.parse(readFileSync6(metaPath, "utf-8"));
|
|
2469
3173
|
} catch (err) {
|
|
2470
3174
|
console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
|
|
2471
3175
|
return null;
|
|
@@ -2480,27 +3184,27 @@ function deliverFile(receiverDir, sender, filename, content, inboxPath) {
|
|
|
2480
3184
|
if (sender.includes("/") || sender.includes("\\")) {
|
|
2481
3185
|
throw new Error("Invalid sender name");
|
|
2482
3186
|
}
|
|
2483
|
-
const destDir =
|
|
3187
|
+
const destDir = resolve9(receiverDir, "home", inbox, sender);
|
|
2484
3188
|
mkdirSync4(destDir, { recursive: true });
|
|
2485
|
-
const destPath =
|
|
3189
|
+
const destPath = resolve9(destDir, basename(filename));
|
|
2486
3190
|
writeFileSync4(destPath, content);
|
|
2487
3191
|
return join2(inbox, sender, basename(filename));
|
|
2488
3192
|
}
|
|
2489
3193
|
function acceptPending(receiver, id, receiverDir) {
|
|
2490
3194
|
const meta = getPending(receiver, id);
|
|
2491
3195
|
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
2492
|
-
const dataPath =
|
|
2493
|
-
const content =
|
|
3196
|
+
const dataPath = resolve9(pendingDir(receiver), id, "data");
|
|
3197
|
+
const content = readFileSync6(dataPath);
|
|
2494
3198
|
const config = readFileSharingConfig(receiverDir);
|
|
2495
3199
|
const inboxPath = config.inboxPath ?? "inbox";
|
|
2496
3200
|
const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
|
|
2497
|
-
rmSync(
|
|
3201
|
+
rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
|
|
2498
3202
|
return { sender: meta.sender, filename: meta.filename, destPath };
|
|
2499
3203
|
}
|
|
2500
3204
|
function rejectPending(receiver, id) {
|
|
2501
3205
|
const meta = getPending(receiver, id);
|
|
2502
3206
|
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
2503
|
-
rmSync(
|
|
3207
|
+
rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
|
|
2504
3208
|
return { sender: meta.sender, filename: meta.filename };
|
|
2505
3209
|
}
|
|
2506
3210
|
function formatFileSize(bytes) {
|
|
@@ -2541,21 +3245,21 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
2541
3245
|
const pathErr = validateFilePath(body.filePath);
|
|
2542
3246
|
if (pathErr) return c.json({ error: pathErr }, 400);
|
|
2543
3247
|
const senderDir = mindDir(senderName);
|
|
2544
|
-
const filePath =
|
|
3248
|
+
const filePath = resolve10(senderDir, "home", body.filePath);
|
|
2545
3249
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
2546
|
-
const
|
|
2547
|
-
if (!
|
|
2548
|
-
if (
|
|
3250
|
+
const stat4 = statSync2(filePath, { throwIfNoEntry: false });
|
|
3251
|
+
if (!stat4) return c.json({ error: `File not found: ${body.filePath}` }, 404);
|
|
3252
|
+
if (stat4.size > MAX_FILE_SIZE) {
|
|
2549
3253
|
return c.json(
|
|
2550
3254
|
{
|
|
2551
|
-
error: `File too large (${formatFileSize(
|
|
3255
|
+
error: `File too large (${formatFileSize(stat4.size)}, max ${formatFileSize(MAX_FILE_SIZE)})`
|
|
2552
3256
|
},
|
|
2553
3257
|
413
|
|
2554
3258
|
);
|
|
2555
3259
|
}
|
|
2556
3260
|
let content;
|
|
2557
3261
|
try {
|
|
2558
|
-
content =
|
|
3262
|
+
content = readFileSync7(filePath);
|
|
2559
3263
|
} catch {
|
|
2560
3264
|
return c.json({ error: `File not found: ${body.filePath}` }, 404);
|
|
2561
3265
|
}
|
|
@@ -2657,18 +3361,61 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
2657
3361
|
var file_sharing_default = app6;
|
|
2658
3362
|
|
|
2659
3363
|
// src/web/api/files.ts
|
|
2660
|
-
import { existsSync as
|
|
2661
|
-
import { readdir, readFile } from "fs/promises";
|
|
2662
|
-
import { resolve as
|
|
3364
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3365
|
+
import { readdir, readFile, realpath, stat } from "fs/promises";
|
|
3366
|
+
import { extname, resolve as resolve11 } from "path";
|
|
2663
3367
|
import { Hono as Hono7 } from "hono";
|
|
2664
3368
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
2665
|
-
var
|
|
3369
|
+
var AVATAR_MIME = {
|
|
3370
|
+
".png": "image/png",
|
|
3371
|
+
".jpg": "image/jpeg",
|
|
3372
|
+
".jpeg": "image/jpeg",
|
|
3373
|
+
".gif": "image/gif",
|
|
3374
|
+
".webp": "image/webp"
|
|
3375
|
+
};
|
|
3376
|
+
var MAX_AVATAR_SIZE = 2 * 1024 * 1024;
|
|
3377
|
+
var app7 = new Hono7().get("/:name/avatar", async (c) => {
|
|
3378
|
+
const name = c.req.param("name");
|
|
3379
|
+
const entry = findMind(name);
|
|
3380
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3381
|
+
const dir = mindDir(name);
|
|
3382
|
+
const config = readVoluteConfig(dir);
|
|
3383
|
+
if (!config?.avatar) return c.json({ error: "No avatar configured" }, 404);
|
|
3384
|
+
const ext = extname(config.avatar).toLowerCase();
|
|
3385
|
+
const mime = AVATAR_MIME[ext];
|
|
3386
|
+
if (!mime) return c.json({ error: "Invalid avatar extension" }, 400);
|
|
3387
|
+
const homeDir = resolve11(dir, "home");
|
|
3388
|
+
const avatarPath = resolve11(homeDir, config.avatar);
|
|
3389
|
+
if (!avatarPath.startsWith(`${homeDir}/`)) return c.json({ error: "Invalid avatar path" }, 400);
|
|
3390
|
+
let realAvatarPath;
|
|
3391
|
+
try {
|
|
3392
|
+
const realHome = await realpath(homeDir);
|
|
3393
|
+
realAvatarPath = await realpath(avatarPath);
|
|
3394
|
+
if (!realAvatarPath.startsWith(`${realHome}/`))
|
|
3395
|
+
return c.json({ error: "Invalid avatar path" }, 400);
|
|
3396
|
+
} catch (err) {
|
|
3397
|
+
if (err.code === "ENOENT")
|
|
3398
|
+
return c.json({ error: "Avatar file not found" }, 404);
|
|
3399
|
+
return c.json({ error: "Failed to resolve avatar path" }, 500);
|
|
3400
|
+
}
|
|
3401
|
+
try {
|
|
3402
|
+
const fileStat = await stat(realAvatarPath);
|
|
3403
|
+
if (fileStat.size > MAX_AVATAR_SIZE) return c.json({ error: "Avatar file too large" }, 400);
|
|
3404
|
+
const body = await readFile(realAvatarPath);
|
|
3405
|
+
return c.body(body, 200, {
|
|
3406
|
+
"Content-Type": mime,
|
|
3407
|
+
"Cache-Control": "public, max-age=300"
|
|
3408
|
+
});
|
|
3409
|
+
} catch {
|
|
3410
|
+
return c.json({ error: "Failed to read avatar file" }, 500);
|
|
3411
|
+
}
|
|
3412
|
+
}).get("/:name/files", async (c) => {
|
|
2666
3413
|
const name = c.req.param("name");
|
|
2667
3414
|
const entry = findMind(name);
|
|
2668
3415
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2669
3416
|
const dir = mindDir(name);
|
|
2670
|
-
const homeDir =
|
|
2671
|
-
if (!
|
|
3417
|
+
const homeDir = resolve11(dir, "home");
|
|
3418
|
+
if (!existsSync9(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
2672
3419
|
const allFiles = await readdir(homeDir);
|
|
2673
3420
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
2674
3421
|
return c.json(files);
|
|
@@ -2681,8 +3428,8 @@ var app7 = new Hono7().get("/:name/files", async (c) => {
|
|
|
2681
3428
|
const entry = findMind(name);
|
|
2682
3429
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2683
3430
|
const dir = mindDir(name);
|
|
2684
|
-
const filePath =
|
|
2685
|
-
if (!
|
|
3431
|
+
const filePath = resolve11(dir, "home", filename);
|
|
3432
|
+
if (!existsSync9(filePath)) {
|
|
2686
3433
|
return c.json({ error: "File not found" }, 404);
|
|
2687
3434
|
}
|
|
2688
3435
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -2695,17 +3442,17 @@ import { Hono as Hono8 } from "hono";
|
|
|
2695
3442
|
|
|
2696
3443
|
// src/lib/identity.ts
|
|
2697
3444
|
import { createHash, generateKeyPairSync, sign, verify } from "crypto";
|
|
2698
|
-
import { existsSync as
|
|
2699
|
-
import { resolve as
|
|
3445
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
3446
|
+
import { resolve as resolve12 } from "path";
|
|
2700
3447
|
function generateIdentity(mindDir2) {
|
|
2701
|
-
const identityDir =
|
|
3448
|
+
const identityDir = resolve12(mindDir2, ".mind/identity");
|
|
2702
3449
|
mkdirSync5(identityDir, { recursive: true });
|
|
2703
3450
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
2704
3451
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
2705
3452
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
2706
3453
|
});
|
|
2707
|
-
const privatePath =
|
|
2708
|
-
const publicPath =
|
|
3454
|
+
const privatePath = resolve12(identityDir, "private.pem");
|
|
3455
|
+
const publicPath = resolve12(identityDir, "public.pem");
|
|
2709
3456
|
writeFileSync5(privatePath, privateKey, { mode: 384 });
|
|
2710
3457
|
writeFileSync5(publicPath, publicKey, { mode: 420 });
|
|
2711
3458
|
const config = readVoluteConfig(mindDir2) ?? {};
|
|
@@ -2720,17 +3467,17 @@ function getPrivateKey(mindDir2) {
|
|
|
2720
3467
|
const config = readVoluteConfig(mindDir2);
|
|
2721
3468
|
const relPath = config?.identity?.privateKey;
|
|
2722
3469
|
if (!relPath) return null;
|
|
2723
|
-
const fullPath =
|
|
2724
|
-
if (!
|
|
2725
|
-
return
|
|
3470
|
+
const fullPath = resolve12(mindDir2, relPath);
|
|
3471
|
+
if (!existsSync10(fullPath)) return null;
|
|
3472
|
+
return readFileSync8(fullPath, "utf-8");
|
|
2726
3473
|
}
|
|
2727
3474
|
function getPublicKey(mindDir2) {
|
|
2728
3475
|
const config = readVoluteConfig(mindDir2);
|
|
2729
3476
|
const relPath = config?.identity?.publicKey;
|
|
2730
3477
|
if (!relPath) return null;
|
|
2731
|
-
const fullPath =
|
|
2732
|
-
if (!
|
|
2733
|
-
return
|
|
3478
|
+
const fullPath = resolve12(mindDir2, relPath);
|
|
3479
|
+
if (!existsSync10(fullPath)) return null;
|
|
3480
|
+
return readFileSync8(fullPath, "utf-8");
|
|
2734
3481
|
}
|
|
2735
3482
|
function getFingerprint(publicKeyPem) {
|
|
2736
3483
|
return createHash("sha256").update(publicKeyPem).digest("hex");
|
|
@@ -2783,16 +3530,16 @@ var keys_default = app8;
|
|
|
2783
3530
|
|
|
2784
3531
|
// src/web/api/logs.ts
|
|
2785
3532
|
import { spawn as spawn2 } from "child_process";
|
|
2786
|
-
import { existsSync as
|
|
2787
|
-
import { resolve as
|
|
3533
|
+
import { existsSync as existsSync11 } from "fs";
|
|
3534
|
+
import { resolve as resolve13 } from "path";
|
|
2788
3535
|
import { Hono as Hono9 } from "hono";
|
|
2789
3536
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
2790
3537
|
var app9 = new Hono9().get("/:name/logs", async (c) => {
|
|
2791
3538
|
const name = c.req.param("name");
|
|
2792
3539
|
const entry = findMind(name);
|
|
2793
3540
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2794
|
-
const logFile =
|
|
2795
|
-
if (!
|
|
3541
|
+
const logFile = resolve13(stateDir(name), "logs", "mind.log");
|
|
3542
|
+
if (!existsSync11(logFile)) {
|
|
2796
3543
|
return c.json({ error: "No log file found" }, 404);
|
|
2797
3544
|
}
|
|
2798
3545
|
return streamSSE2(c, async (stream) => {
|
|
@@ -2819,8 +3566,8 @@ var app9 = new Hono9().get("/:name/logs", async (c) => {
|
|
|
2819
3566
|
const name = c.req.param("name");
|
|
2820
3567
|
const entry = findMind(name);
|
|
2821
3568
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2822
|
-
const logFile =
|
|
2823
|
-
if (!
|
|
3569
|
+
const logFile = resolve13(stateDir(name), "logs", "mind.log");
|
|
3570
|
+
if (!existsSync11(logFile)) {
|
|
2824
3571
|
return c.json({ error: "No log file found" }, 404);
|
|
2825
3572
|
}
|
|
2826
3573
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
@@ -2920,13 +3667,13 @@ var mind_skills_default = app10;
|
|
|
2920
3667
|
|
|
2921
3668
|
// src/web/api/minds.ts
|
|
2922
3669
|
import {
|
|
2923
|
-
cpSync
|
|
3670
|
+
cpSync,
|
|
2924
3671
|
existsSync as existsSync12,
|
|
2925
|
-
mkdirSync as
|
|
2926
|
-
readdirSync as
|
|
3672
|
+
mkdirSync as mkdirSync7,
|
|
3673
|
+
readdirSync as readdirSync5,
|
|
2927
3674
|
readFileSync as readFileSync11,
|
|
2928
|
-
rmSync as
|
|
2929
|
-
writeFileSync as
|
|
3675
|
+
rmSync as rmSync2,
|
|
3676
|
+
writeFileSync as writeFileSync8
|
|
2930
3677
|
} from "fs";
|
|
2931
3678
|
import { resolve as resolve16 } from "path";
|
|
2932
3679
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
@@ -2935,19 +3682,19 @@ import { Hono as Hono11 } from "hono";
|
|
|
2935
3682
|
import { z as z3 } from "zod";
|
|
2936
3683
|
|
|
2937
3684
|
// src/lib/consolidate.ts
|
|
2938
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
2939
|
-
import { resolve as
|
|
3685
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
|
|
3686
|
+
import { resolve as resolve14 } from "path";
|
|
2940
3687
|
async function consolidateMemory(mindDir2) {
|
|
2941
|
-
const soulPath =
|
|
2942
|
-
const memoryPath =
|
|
2943
|
-
const memoryDir =
|
|
2944
|
-
const soul =
|
|
3688
|
+
const soulPath = resolve14(mindDir2, "home/SOUL.md");
|
|
3689
|
+
const memoryPath = resolve14(mindDir2, "home/MEMORY.md");
|
|
3690
|
+
const memoryDir = resolve14(mindDir2, "home/memory");
|
|
3691
|
+
const soul = readFileSync9(soulPath, "utf-8");
|
|
2945
3692
|
const logs = [];
|
|
2946
3693
|
try {
|
|
2947
3694
|
const files = readdirSync4(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
2948
3695
|
for (const filename of files) {
|
|
2949
3696
|
const date = filename.replace(".md", "");
|
|
2950
|
-
const content2 =
|
|
3697
|
+
const content2 = readFileSync9(resolve14(memoryDir, filename), "utf-8").trim();
|
|
2951
3698
|
if (content2) {
|
|
2952
3699
|
logs.push(`### ${date}
|
|
2953
3700
|
|
|
@@ -3007,11 +3754,11 @@ ${content2}`);
|
|
|
3007
3754
|
|
|
3008
3755
|
// src/lib/convert-session.ts
|
|
3009
3756
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3010
|
-
import { mkdirSync as mkdirSync6, readFileSync as
|
|
3757
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
3011
3758
|
import { homedir } from "os";
|
|
3012
|
-
import { resolve as
|
|
3759
|
+
import { resolve as resolve15 } from "path";
|
|
3013
3760
|
function convertSession(opts) {
|
|
3014
|
-
const lines =
|
|
3761
|
+
const lines = readFileSync10(opts.sessionPath, "utf-8").trim().split("\n");
|
|
3015
3762
|
const sessionId = randomUUID2();
|
|
3016
3763
|
const idMap = /* @__PURE__ */ new Map();
|
|
3017
3764
|
const messages2 = [];
|
|
@@ -3125,9 +3872,9 @@ function convertSession(opts) {
|
|
|
3125
3872
|
}
|
|
3126
3873
|
}
|
|
3127
3874
|
const projectId = opts.projectDir.replace(/\//g, "-");
|
|
3128
|
-
const sdkDir =
|
|
3875
|
+
const sdkDir = resolve15(homedir(), ".claude", "projects", projectId);
|
|
3129
3876
|
mkdirSync6(sdkDir, { recursive: true });
|
|
3130
|
-
const sdkPath =
|
|
3877
|
+
const sdkPath = resolve15(sdkDir, `${sessionId}.jsonl`);
|
|
3131
3878
|
writeFileSync7(sdkPath, `${sdkEvents.join("\n")}
|
|
3132
3879
|
`);
|
|
3133
3880
|
console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
|
|
@@ -3179,7 +3926,7 @@ function convertAssistantContent(content) {
|
|
|
3179
3926
|
return result;
|
|
3180
3927
|
}
|
|
3181
3928
|
|
|
3182
|
-
// src/lib/mind-events.ts
|
|
3929
|
+
// src/lib/events/mind-events.ts
|
|
3183
3930
|
var subscribers = /* @__PURE__ */ new Map();
|
|
3184
3931
|
function subscribe3(mind, callback) {
|
|
3185
3932
|
let set = subscribers.get(mind);
|
|
@@ -3207,111 +3954,6 @@ function publish3(mind, event) {
|
|
|
3207
3954
|
}
|
|
3208
3955
|
}
|
|
3209
3956
|
|
|
3210
|
-
// src/lib/template.ts
|
|
3211
|
-
import {
|
|
3212
|
-
cpSync,
|
|
3213
|
-
existsSync as existsSync11,
|
|
3214
|
-
mkdirSync as mkdirSync7,
|
|
3215
|
-
readdirSync as readdirSync5,
|
|
3216
|
-
readFileSync as readFileSync10,
|
|
3217
|
-
renameSync as renameSync3,
|
|
3218
|
-
rmSync as rmSync2,
|
|
3219
|
-
statSync as statSync3,
|
|
3220
|
-
writeFileSync as writeFileSync8
|
|
3221
|
-
} from "fs";
|
|
3222
|
-
import { tmpdir } from "os";
|
|
3223
|
-
import { dirname as dirname2, join as join3, relative, resolve as resolve15 } from "path";
|
|
3224
|
-
function findTemplatesRoot() {
|
|
3225
|
-
let dir = dirname2(new URL(import.meta.url).pathname);
|
|
3226
|
-
for (let i = 0; i < 5; i++) {
|
|
3227
|
-
const candidate = resolve15(dir, "templates");
|
|
3228
|
-
if (existsSync11(resolve15(candidate, "_base"))) return candidate;
|
|
3229
|
-
dir = dirname2(dir);
|
|
3230
|
-
}
|
|
3231
|
-
console.error(
|
|
3232
|
-
"Templates directory not found. Searched up from:",
|
|
3233
|
-
dirname2(new URL(import.meta.url).pathname)
|
|
3234
|
-
);
|
|
3235
|
-
process.exit(1);
|
|
3236
|
-
}
|
|
3237
|
-
function composeTemplate(templatesRoot, templateName) {
|
|
3238
|
-
const baseDir = resolve15(templatesRoot, "_base");
|
|
3239
|
-
const templateDir = resolve15(templatesRoot, templateName);
|
|
3240
|
-
if (!existsSync11(baseDir)) {
|
|
3241
|
-
console.error("Base template not found:", baseDir);
|
|
3242
|
-
process.exit(1);
|
|
3243
|
-
}
|
|
3244
|
-
if (!existsSync11(templateDir)) {
|
|
3245
|
-
console.error(`Template not found: ${templateName}`);
|
|
3246
|
-
process.exit(1);
|
|
3247
|
-
}
|
|
3248
|
-
const composedDir = resolve15(tmpdir(), `volute-template-${Date.now()}`);
|
|
3249
|
-
mkdirSync7(composedDir, { recursive: true });
|
|
3250
|
-
cpSync(baseDir, composedDir, { recursive: true });
|
|
3251
|
-
for (const file of listFiles(templateDir)) {
|
|
3252
|
-
const src = resolve15(templateDir, file);
|
|
3253
|
-
const dest = resolve15(composedDir, file);
|
|
3254
|
-
mkdirSync7(dirname2(dest), { recursive: true });
|
|
3255
|
-
cpSync(src, dest);
|
|
3256
|
-
}
|
|
3257
|
-
const manifestPath = resolve15(composedDir, "volute-template.json");
|
|
3258
|
-
if (!existsSync11(manifestPath)) {
|
|
3259
|
-
rmSync2(composedDir, { recursive: true, force: true });
|
|
3260
|
-
console.error(`Template manifest not found: ${templateName}/volute-template.json`);
|
|
3261
|
-
process.exit(1);
|
|
3262
|
-
}
|
|
3263
|
-
const manifest = JSON.parse(readFileSync10(manifestPath, "utf-8"));
|
|
3264
|
-
rmSync2(manifestPath);
|
|
3265
|
-
return { composedDir, manifest };
|
|
3266
|
-
}
|
|
3267
|
-
function copyTemplateToDir(composedDir, destDir, mindName, manifest) {
|
|
3268
|
-
cpSync(composedDir, destDir, { recursive: true });
|
|
3269
|
-
for (const [from, to] of Object.entries(manifest.rename)) {
|
|
3270
|
-
const fromPath = resolve15(destDir, from);
|
|
3271
|
-
if (existsSync11(fromPath)) {
|
|
3272
|
-
renameSync3(fromPath, resolve15(destDir, to));
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
for (const file of manifest.substitute) {
|
|
3276
|
-
const path = resolve15(destDir, file);
|
|
3277
|
-
if (existsSync11(path)) {
|
|
3278
|
-
const content = readFileSync10(path, "utf-8");
|
|
3279
|
-
writeFileSync8(path, content.replaceAll("{{name}}", mindName));
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
}
|
|
3283
|
-
function applyInitFiles(destDir) {
|
|
3284
|
-
const initDir = resolve15(destDir, ".init");
|
|
3285
|
-
if (!existsSync11(initDir)) return;
|
|
3286
|
-
const homeDir = resolve15(destDir, "home");
|
|
3287
|
-
for (const file of listFiles(initDir)) {
|
|
3288
|
-
const src = resolve15(initDir, file);
|
|
3289
|
-
const dest = resolve15(homeDir, file);
|
|
3290
|
-
const parent = dirname2(dest);
|
|
3291
|
-
if (!existsSync11(parent)) {
|
|
3292
|
-
mkdirSync7(parent, { recursive: true });
|
|
3293
|
-
}
|
|
3294
|
-
cpSync(src, dest);
|
|
3295
|
-
}
|
|
3296
|
-
rmSync2(initDir, { recursive: true, force: true });
|
|
3297
|
-
}
|
|
3298
|
-
function listFiles(dir) {
|
|
3299
|
-
const results = [];
|
|
3300
|
-
function walk(current) {
|
|
3301
|
-
for (const entry of readdirSync5(current)) {
|
|
3302
|
-
const full = join3(current, entry);
|
|
3303
|
-
if (statSync3(full).isDirectory()) {
|
|
3304
|
-
if (entry === ".git") continue;
|
|
3305
|
-
walk(full);
|
|
3306
|
-
} else {
|
|
3307
|
-
results.push(relative(dir, full));
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
walk(dir);
|
|
3312
|
-
return results;
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
3957
|
// src/web/api/minds.ts
|
|
3316
3958
|
async function getMindStatus(name, port) {
|
|
3317
3959
|
const manager = getMindManager();
|
|
@@ -3320,7 +3962,8 @@ async function getMindStatus(name, port) {
|
|
|
3320
3962
|
const health = await checkHealth(port);
|
|
3321
3963
|
status = health.ok ? "running" : "starting";
|
|
3322
3964
|
}
|
|
3323
|
-
const
|
|
3965
|
+
const config = readVoluteConfig(mindDir(name));
|
|
3966
|
+
const channelConfig = config?.channels;
|
|
3324
3967
|
const channels = [];
|
|
3325
3968
|
for (const [, provider] of Object.entries(CHANNELS)) {
|
|
3326
3969
|
if (!provider.builtIn) continue;
|
|
@@ -3341,7 +3984,13 @@ async function getMindStatus(name, port) {
|
|
|
3341
3984
|
showToolCalls: channelConfig?.[cs.type]?.showToolCalls ?? provider?.showToolCalls ?? false
|
|
3342
3985
|
});
|
|
3343
3986
|
}
|
|
3344
|
-
return {
|
|
3987
|
+
return {
|
|
3988
|
+
status,
|
|
3989
|
+
channels,
|
|
3990
|
+
displayName: config?.displayName,
|
|
3991
|
+
description: config?.description,
|
|
3992
|
+
avatar: config?.avatar
|
|
3993
|
+
};
|
|
3345
3994
|
}
|
|
3346
3995
|
var TEMPLATE_BRANCH = "volute/template";
|
|
3347
3996
|
async function configureGitIdentity(mindName, opts) {
|
|
@@ -3373,7 +4022,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
3373
4022
|
} catch {
|
|
3374
4023
|
}
|
|
3375
4024
|
if (existsSync12(tempWorktree)) {
|
|
3376
|
-
|
|
4025
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
3377
4026
|
}
|
|
3378
4027
|
const templatesRoot = findTemplatesRoot();
|
|
3379
4028
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
@@ -3395,7 +4044,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
3395
4044
|
copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
|
|
3396
4045
|
const initDir = resolve16(tempWorktree, ".init");
|
|
3397
4046
|
if (existsSync12(initDir)) {
|
|
3398
|
-
|
|
4047
|
+
rmSync2(initDir, { recursive: true, force: true });
|
|
3399
4048
|
}
|
|
3400
4049
|
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
3401
4050
|
try {
|
|
@@ -3409,9 +4058,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
3409
4058
|
} catch {
|
|
3410
4059
|
}
|
|
3411
4060
|
if (existsSync12(tempWorktree)) {
|
|
3412
|
-
|
|
4061
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
3413
4062
|
}
|
|
3414
|
-
|
|
4063
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
3415
4064
|
}
|
|
3416
4065
|
}
|
|
3417
4066
|
async function mergeTemplateBranch(worktreeDir) {
|
|
@@ -3447,6 +4096,12 @@ async function importFromArchive(c, tempDir, nameOverride, manifest) {
|
|
|
3447
4096
|
if (!manifest?.includes || !manifest.name || !manifest.template) {
|
|
3448
4097
|
return c.json({ error: "Invalid archive manifest" }, 400);
|
|
3449
4098
|
}
|
|
4099
|
+
if (isHomeOnlyArchive(manifest)) {
|
|
4100
|
+
return importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverride, manifest);
|
|
4101
|
+
}
|
|
4102
|
+
return importFromFullArchive(c, tempDir, extractedMindDir, nameOverride, manifest);
|
|
4103
|
+
}
|
|
4104
|
+
async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride, manifest) {
|
|
3450
4105
|
const name = nameOverride ?? manifest.name;
|
|
3451
4106
|
const nameErr = validateMindName(name);
|
|
3452
4107
|
if (nameErr) return c.json({ error: nameErr }, 400);
|
|
@@ -3455,93 +4110,220 @@ async function importFromArchive(c, tempDir, nameOverride, manifest) {
|
|
|
3455
4110
|
const dest = mindDir(name);
|
|
3456
4111
|
if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
3457
4112
|
try {
|
|
3458
|
-
|
|
4113
|
+
cpSync(extractedMindDir, dest, { recursive: true });
|
|
3459
4114
|
if (!manifest.includes.identity) {
|
|
3460
4115
|
generateIdentity(dest);
|
|
3461
4116
|
}
|
|
3462
4117
|
const state = stateDir(name);
|
|
3463
|
-
|
|
4118
|
+
mkdirSync7(state, { recursive: true });
|
|
3464
4119
|
const channelsJson = resolve16(tempDir, "state/channels.json");
|
|
3465
4120
|
if (existsSync12(channelsJson)) {
|
|
3466
|
-
|
|
4121
|
+
cpSync(channelsJson, resolve16(state, "channels.json"));
|
|
3467
4122
|
}
|
|
3468
4123
|
const envJson = resolve16(tempDir, "state/env.json");
|
|
3469
4124
|
if (existsSync12(envJson)) {
|
|
3470
|
-
|
|
4125
|
+
cpSync(envJson, resolve16(state, "env.json"));
|
|
3471
4126
|
}
|
|
3472
4127
|
const port = nextPort();
|
|
3473
|
-
addMind(name, port,
|
|
4128
|
+
addMind(name, port, manifest.stage, manifest.template);
|
|
4129
|
+
try {
|
|
4130
|
+
setMindTemplateHash(name, computeTemplateHash(manifest.template));
|
|
4131
|
+
} catch (err) {
|
|
4132
|
+
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4133
|
+
}
|
|
3474
4134
|
const homeDir = resolve16(dest, "home");
|
|
3475
4135
|
ensureVoluteGroup();
|
|
3476
4136
|
createMindUser(name, homeDir);
|
|
3477
4137
|
chownMindDir(dest, name);
|
|
3478
4138
|
await npmInstallAsMind(dest, name);
|
|
3479
|
-
|
|
3480
|
-
|
|
4139
|
+
await importHistoryFromArchive(name, tempDir);
|
|
4140
|
+
importSessionsFromArchive(dest, tempDir);
|
|
4141
|
+
if (!existsSync12(resolve16(dest, ".git"))) {
|
|
3481
4142
|
try {
|
|
3482
|
-
const
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
if (!line) continue;
|
|
3488
|
-
try {
|
|
3489
|
-
const row = JSON.parse(line);
|
|
3490
|
-
if (!row.type) {
|
|
3491
|
-
failed++;
|
|
3492
|
-
continue;
|
|
3493
|
-
}
|
|
3494
|
-
await db.insert(mindHistory).values({
|
|
3495
|
-
mind: name,
|
|
3496
|
-
channel: row.channel ?? null,
|
|
3497
|
-
session: row.session ?? null,
|
|
3498
|
-
sender: row.sender ?? null,
|
|
3499
|
-
message_id: row.message_id ?? null,
|
|
3500
|
-
type: row.type,
|
|
3501
|
-
content: row.content ?? null,
|
|
3502
|
-
metadata: row.metadata ?? null,
|
|
3503
|
-
created_at: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3504
|
-
});
|
|
3505
|
-
imported++;
|
|
3506
|
-
} catch (lineErr) {
|
|
3507
|
-
logger_default.warn("Failed to import history line", logger_default.errorData(lineErr));
|
|
3508
|
-
failed++;
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
if (failed > 0) {
|
|
3512
|
-
logger_default.warn(`History import: ${imported} imported, ${failed} failed`);
|
|
3513
|
-
}
|
|
4143
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
|
|
4144
|
+
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
4145
|
+
await configureGitIdentity(name, { cwd: dest, mindName: name, env });
|
|
4146
|
+
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
4147
|
+
await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
|
|
3514
4148
|
} catch (err) {
|
|
3515
|
-
logger_default.error(
|
|
4149
|
+
logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
|
|
4150
|
+
rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
|
|
3516
4151
|
}
|
|
3517
4152
|
}
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
4153
|
+
chownMindDir(dest, name);
|
|
4154
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
4155
|
+
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
4156
|
+
} catch (err) {
|
|
4157
|
+
if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
4158
|
+
try {
|
|
4159
|
+
removeMind(name);
|
|
4160
|
+
} catch (cleanupErr) {
|
|
4161
|
+
logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
|
|
3525
4162
|
}
|
|
3526
|
-
|
|
3527
|
-
|
|
4163
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
4164
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverride, manifest) {
|
|
4168
|
+
const name = nameOverride ?? manifest.name;
|
|
4169
|
+
const nameErr = validateMindName(name);
|
|
4170
|
+
if (nameErr) return c.json({ error: nameErr }, 400);
|
|
4171
|
+
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
4172
|
+
ensureVoluteHome();
|
|
4173
|
+
const dest = mindDir(name);
|
|
4174
|
+
if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4175
|
+
const templatesRoot = findTemplatesRoot();
|
|
4176
|
+
const { composedDir, manifest: templateManifest } = composeTemplate(
|
|
4177
|
+
templatesRoot,
|
|
4178
|
+
manifest.template
|
|
4179
|
+
);
|
|
4180
|
+
try {
|
|
4181
|
+
copyTemplateToDir(composedDir, dest, name, templateManifest);
|
|
4182
|
+
applyInitFiles(dest);
|
|
4183
|
+
const extractedHome = resolve16(extractedMindDir, "home");
|
|
4184
|
+
if (existsSync12(extractedHome)) {
|
|
4185
|
+
cpSync(extractedHome, resolve16(dest, "home"), { recursive: true });
|
|
4186
|
+
}
|
|
4187
|
+
const extractedMindInternal = resolve16(extractedMindDir, ".mind");
|
|
4188
|
+
if (existsSync12(extractedMindInternal)) {
|
|
4189
|
+
cpSync(extractedMindInternal, resolve16(dest, ".mind"), { recursive: true });
|
|
4190
|
+
}
|
|
4191
|
+
const identityDir = resolve16(dest, ".mind/identity");
|
|
4192
|
+
let publicKeyPem;
|
|
4193
|
+
if (!manifest.includes.identity || !existsSync12(resolve16(identityDir, "private.pem"))) {
|
|
4194
|
+
({ publicKeyPem } = generateIdentity(dest));
|
|
4195
|
+
} else {
|
|
4196
|
+
publicKeyPem = readFileSync11(resolve16(identityDir, "public.pem"), "utf-8");
|
|
4197
|
+
}
|
|
4198
|
+
const promptsPath = resolve16(dest, "home/.config/prompts.json");
|
|
4199
|
+
if (!existsSync12(promptsPath)) {
|
|
4200
|
+
const mindPrompts = await getMindPromptDefaults();
|
|
4201
|
+
writeFileSync8(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
|
|
4202
|
+
`);
|
|
4203
|
+
}
|
|
4204
|
+
const state = stateDir(name);
|
|
4205
|
+
mkdirSync7(state, { recursive: true });
|
|
4206
|
+
const channelsJson = resolve16(tempDir, "state/channels.json");
|
|
4207
|
+
if (existsSync12(channelsJson)) {
|
|
4208
|
+
cpSync(channelsJson, resolve16(state, "channels.json"));
|
|
4209
|
+
}
|
|
4210
|
+
const envJson = resolve16(tempDir, "state/env.json");
|
|
4211
|
+
if (existsSync12(envJson)) {
|
|
4212
|
+
cpSync(envJson, resolve16(state, "env.json"));
|
|
4213
|
+
}
|
|
4214
|
+
const port = nextPort();
|
|
4215
|
+
addMind(name, port, manifest.stage, manifest.template);
|
|
4216
|
+
const homeDir = resolve16(dest, "home");
|
|
4217
|
+
ensureVoluteGroup();
|
|
4218
|
+
createMindUser(name, homeDir);
|
|
4219
|
+
chownMindDir(dest, name);
|
|
4220
|
+
await npmInstallAsMind(dest, name);
|
|
4221
|
+
let gitWarning;
|
|
4222
|
+
try {
|
|
4223
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: homeDir } : void 0;
|
|
3528
4224
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
3529
4225
|
await configureGitIdentity(name, { cwd: dest, mindName: name, env });
|
|
3530
|
-
await
|
|
3531
|
-
|
|
4226
|
+
await initTemplateBranch(dest, composedDir, templateManifest, name, env);
|
|
4227
|
+
} catch (err) {
|
|
4228
|
+
logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
|
|
4229
|
+
rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
|
|
4230
|
+
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
4231
|
+
}
|
|
4232
|
+
try {
|
|
4233
|
+
await addSharedWorktree(name, dest);
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
logger_default.warn(`failed to add shared worktree for ${name}`, logger_default.errorData(err));
|
|
4236
|
+
}
|
|
4237
|
+
const skillSet = manifest.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS;
|
|
4238
|
+
const skillWarnings = [];
|
|
4239
|
+
for (const skillId of skillSet) {
|
|
4240
|
+
try {
|
|
4241
|
+
await installSkill(name, dest, skillId);
|
|
4242
|
+
} catch (err) {
|
|
4243
|
+
logger_default.error(`failed to install skill ${skillId} for ${name}`, logger_default.errorData(err));
|
|
4244
|
+
skillWarnings.push(`Failed to install skill: ${skillId}`);
|
|
4245
|
+
}
|
|
3532
4246
|
}
|
|
4247
|
+
await importHistoryFromArchive(name, tempDir);
|
|
4248
|
+
importSessionsFromArchive(dest, tempDir);
|
|
3533
4249
|
chownMindDir(dest, name);
|
|
3534
|
-
|
|
3535
|
-
|
|
4250
|
+
publishPublicKey(name, publicKeyPem).catch(
|
|
4251
|
+
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
4252
|
+
);
|
|
4253
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
4254
|
+
return c.json({
|
|
4255
|
+
ok: true,
|
|
4256
|
+
name,
|
|
4257
|
+
port,
|
|
4258
|
+
stage: manifest.stage ?? "sprouted",
|
|
4259
|
+
message: `Imported mind: ${name} (port ${port})`,
|
|
4260
|
+
...gitWarning && { warning: gitWarning },
|
|
4261
|
+
...skillWarnings.length > 0 && { skillWarnings }
|
|
4262
|
+
});
|
|
3536
4263
|
} catch (err) {
|
|
3537
|
-
if (existsSync12(dest))
|
|
4264
|
+
if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
3538
4265
|
try {
|
|
3539
4266
|
removeMind(name);
|
|
3540
4267
|
} catch (cleanupErr) {
|
|
3541
4268
|
logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
|
|
3542
4269
|
}
|
|
3543
|
-
|
|
4270
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
3544
4271
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4272
|
+
} finally {
|
|
4273
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
async function importHistoryFromArchive(name, tempDir) {
|
|
4277
|
+
const historyJsonl = resolve16(tempDir, "history.jsonl");
|
|
4278
|
+
if (!existsSync12(historyJsonl)) return;
|
|
4279
|
+
try {
|
|
4280
|
+
const db = await getDb();
|
|
4281
|
+
const lines = readFileSync11(historyJsonl, "utf-8").trim().split("\n");
|
|
4282
|
+
let imported = 0;
|
|
4283
|
+
let failed = 0;
|
|
4284
|
+
for (const line of lines) {
|
|
4285
|
+
if (!line) continue;
|
|
4286
|
+
try {
|
|
4287
|
+
const row = JSON.parse(line);
|
|
4288
|
+
if (!row.type) {
|
|
4289
|
+
failed++;
|
|
4290
|
+
continue;
|
|
4291
|
+
}
|
|
4292
|
+
await db.insert(mindHistory).values({
|
|
4293
|
+
mind: name,
|
|
4294
|
+
channel: row.channel ?? null,
|
|
4295
|
+
session: row.session ?? null,
|
|
4296
|
+
sender: row.sender ?? null,
|
|
4297
|
+
message_id: row.message_id ?? null,
|
|
4298
|
+
type: row.type,
|
|
4299
|
+
content: row.content ?? null,
|
|
4300
|
+
metadata: row.metadata ?? null,
|
|
4301
|
+
created_at: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4302
|
+
});
|
|
4303
|
+
imported++;
|
|
4304
|
+
} catch (lineErr) {
|
|
4305
|
+
logger_default.warn("Failed to import history line", logger_default.errorData(lineErr));
|
|
4306
|
+
failed++;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
if (failed > 0) {
|
|
4310
|
+
logger_default.warn(`History import: ${imported} imported, ${failed} failed`);
|
|
4311
|
+
}
|
|
4312
|
+
} catch (err) {
|
|
4313
|
+
logger_default.error("Failed to open database for history import", logger_default.errorData(err));
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
function importSessionsFromArchive(dest, tempDir) {
|
|
4317
|
+
const sessionsDir = resolve16(tempDir, "sessions");
|
|
4318
|
+
if (!existsSync12(sessionsDir)) return;
|
|
4319
|
+
try {
|
|
4320
|
+
const destSessions = resolve16(dest, ".mind/sessions");
|
|
4321
|
+
mkdirSync7(destSessions, { recursive: true });
|
|
4322
|
+
for (const file of readdirSync5(sessionsDir)) {
|
|
4323
|
+
cpSync(resolve16(sessionsDir, file), resolve16(destSessions, file));
|
|
4324
|
+
}
|
|
4325
|
+
} catch (err) {
|
|
4326
|
+
logger_default.error("Failed to import sessions from archive", logger_default.errorData(err));
|
|
3545
4327
|
}
|
|
3546
4328
|
}
|
|
3547
4329
|
var createMindSchema = z3.object({
|
|
@@ -3568,21 +4350,32 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
3568
4350
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
3569
4351
|
applyInitFiles(dest);
|
|
3570
4352
|
const { publicKeyPem } = generateIdentity(dest);
|
|
4353
|
+
if (body.description) {
|
|
4354
|
+
const seedConfig = readVoluteConfig(dest);
|
|
4355
|
+
if (!seedConfig) throw new Error("Failed to read volute.json after identity generation");
|
|
4356
|
+
seedConfig.description = body.description;
|
|
4357
|
+
writeVoluteConfig(dest, seedConfig);
|
|
4358
|
+
}
|
|
3571
4359
|
if (body.model) {
|
|
3572
4360
|
const configPath2 = resolve16(dest, "home/.config/config.json");
|
|
3573
4361
|
const existing = existsSync12(configPath2) ? JSON.parse(readFileSync11(configPath2, "utf-8")) : {};
|
|
3574
4362
|
existing.model = body.model;
|
|
3575
|
-
|
|
4363
|
+
writeFileSync8(configPath2, `${JSON.stringify(existing, null, 2)}
|
|
3576
4364
|
`);
|
|
3577
4365
|
}
|
|
3578
4366
|
const mindPrompts = await getMindPromptDefaults();
|
|
3579
|
-
|
|
4367
|
+
writeFileSync8(
|
|
3580
4368
|
resolve16(dest, "home/.config/prompts.json"),
|
|
3581
4369
|
`${JSON.stringify(mindPrompts, null, 2)}
|
|
3582
4370
|
`
|
|
3583
4371
|
);
|
|
3584
4372
|
const port = nextPort();
|
|
3585
4373
|
addMind(name, port, body.stage, template);
|
|
4374
|
+
try {
|
|
4375
|
+
setMindTemplateHash(name, computeTemplateHash(template));
|
|
4376
|
+
} catch (err) {
|
|
4377
|
+
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4378
|
+
}
|
|
3586
4379
|
const homeDir = resolve16(dest, "home");
|
|
3587
4380
|
ensureVoluteGroup();
|
|
3588
4381
|
createMindUser(name, homeDir);
|
|
@@ -3596,7 +4389,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
3596
4389
|
await initTemplateBranch(dest, composedDir, manifest, name, env);
|
|
3597
4390
|
} catch (err) {
|
|
3598
4391
|
logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
|
|
3599
|
-
|
|
4392
|
+
rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
|
|
3600
4393
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
3601
4394
|
}
|
|
3602
4395
|
try {
|
|
@@ -3611,7 +4404,7 @@ The human who planted you described you as: "${body.description}"
|
|
|
3611
4404
|
` : "";
|
|
3612
4405
|
const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
|
|
3613
4406
|
const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
|
|
3614
|
-
|
|
4407
|
+
writeFileSync8(resolve16(dest, "home/SOUL.md"), seedSoul);
|
|
3615
4408
|
}
|
|
3616
4409
|
const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
|
|
3617
4410
|
const skillWarnings = [];
|
|
@@ -3626,11 +4419,11 @@ The human who planted you described you as: "${body.description}"
|
|
|
3626
4419
|
if (body.stage !== "seed") {
|
|
3627
4420
|
const customSoul = await getPromptIfCustom("default_soul");
|
|
3628
4421
|
if (customSoul) {
|
|
3629
|
-
|
|
4422
|
+
writeFileSync8(resolve16(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
|
|
3630
4423
|
}
|
|
3631
4424
|
const customMemory = await getPromptIfCustom("default_memory");
|
|
3632
4425
|
if (customMemory) {
|
|
3633
|
-
|
|
4426
|
+
writeFileSync8(resolve16(dest, "home/MEMORY.md"), customMemory);
|
|
3634
4427
|
}
|
|
3635
4428
|
}
|
|
3636
4429
|
publishPublicKey(name, publicKeyPem).catch(
|
|
@@ -3646,14 +4439,14 @@ The human who planted you described you as: "${body.description}"
|
|
|
3646
4439
|
...skillWarnings.length > 0 && { skillWarnings }
|
|
3647
4440
|
});
|
|
3648
4441
|
} catch (err) {
|
|
3649
|
-
if (existsSync12(dest))
|
|
4442
|
+
if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
3650
4443
|
try {
|
|
3651
4444
|
removeMind(name);
|
|
3652
4445
|
} catch {
|
|
3653
4446
|
}
|
|
3654
4447
|
return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
|
|
3655
4448
|
} finally {
|
|
3656
|
-
|
|
4449
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
3657
4450
|
}
|
|
3658
4451
|
}).post("/import", requireAdmin, async (c) => {
|
|
3659
4452
|
let body;
|
|
@@ -3699,31 +4492,36 @@ ${user.trimEnd()}
|
|
|
3699
4492
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
3700
4493
|
applyInitFiles(dest);
|
|
3701
4494
|
const { publicKeyPem: importPublicKey } = generateIdentity(dest);
|
|
3702
|
-
|
|
4495
|
+
writeFileSync8(resolve16(dest, "home/SOUL.md"), mergedSoul);
|
|
3703
4496
|
const wsMemoryPath = resolve16(wsDir, "MEMORY.md");
|
|
3704
4497
|
const hasMemory = existsSync12(wsMemoryPath);
|
|
3705
4498
|
if (hasMemory) {
|
|
3706
4499
|
const existingMemory = readFileSync11(wsMemoryPath, "utf-8");
|
|
3707
|
-
|
|
4500
|
+
writeFileSync8(
|
|
3708
4501
|
resolve16(dest, "home/MEMORY.md"),
|
|
3709
4502
|
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
3710
4503
|
);
|
|
3711
4504
|
} else if (user) {
|
|
3712
|
-
|
|
4505
|
+
writeFileSync8(resolve16(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
3713
4506
|
`);
|
|
3714
4507
|
}
|
|
3715
4508
|
const wsMemoryDir = resolve16(wsDir, "memory");
|
|
3716
4509
|
let dailyLogCount = 0;
|
|
3717
4510
|
if (existsSync12(wsMemoryDir)) {
|
|
3718
4511
|
const destMemoryDir = resolve16(dest, "home/memory");
|
|
3719
|
-
const files =
|
|
4512
|
+
const files = readdirSync5(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
3720
4513
|
for (const file of files) {
|
|
3721
|
-
|
|
4514
|
+
cpSync(resolve16(wsMemoryDir, file), resolve16(destMemoryDir, file));
|
|
3722
4515
|
}
|
|
3723
4516
|
dailyLogCount = files.length;
|
|
3724
4517
|
}
|
|
3725
4518
|
const port = nextPort();
|
|
3726
4519
|
addMind(name, port, void 0, template);
|
|
4520
|
+
try {
|
|
4521
|
+
setMindTemplateHash(name, computeTemplateHash(template));
|
|
4522
|
+
} catch (err) {
|
|
4523
|
+
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4524
|
+
}
|
|
3727
4525
|
const homeDir = resolve16(dest, "home");
|
|
3728
4526
|
ensureVoluteGroup();
|
|
3729
4527
|
createMindUser(name, homeDir);
|
|
@@ -3744,8 +4542,8 @@ ${user.trimEnd()}
|
|
|
3744
4542
|
} else if (template === "claude") {
|
|
3745
4543
|
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
3746
4544
|
const mindRuntimeDir = resolve16(dest, ".mind");
|
|
3747
|
-
|
|
3748
|
-
|
|
4545
|
+
mkdirSync7(mindRuntimeDir, { recursive: true });
|
|
4546
|
+
writeFileSync8(resolve16(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
|
|
3749
4547
|
}
|
|
3750
4548
|
}
|
|
3751
4549
|
importOpenClawConnectors(name, dest);
|
|
@@ -3760,14 +4558,14 @@ ${user.trimEnd()}
|
|
|
3760
4558
|
);
|
|
3761
4559
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
3762
4560
|
} catch (err) {
|
|
3763
|
-
if (existsSync12(dest))
|
|
4561
|
+
if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
3764
4562
|
try {
|
|
3765
4563
|
removeMind(name);
|
|
3766
4564
|
} catch {
|
|
3767
4565
|
}
|
|
3768
4566
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
3769
4567
|
} finally {
|
|
3770
|
-
|
|
4568
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
3771
4569
|
}
|
|
3772
4570
|
}).get("/", async (c) => {
|
|
3773
4571
|
const entries = readRegistry();
|
|
@@ -3783,12 +4581,11 @@ ${user.trimEnd()}
|
|
|
3783
4581
|
}
|
|
3784
4582
|
const minds = await Promise.all(
|
|
3785
4583
|
entries.map(async (entry) => {
|
|
3786
|
-
const
|
|
4584
|
+
const mindStatus = await getMindStatus(entry.name, entry.port);
|
|
3787
4585
|
const hasPages = existsSync12(resolve16(mindDir(entry.name), "home", "pages"));
|
|
3788
4586
|
return {
|
|
3789
4587
|
...entry,
|
|
3790
|
-
|
|
3791
|
-
channels,
|
|
4588
|
+
...mindStatus,
|
|
3792
4589
|
hasPages,
|
|
3793
4590
|
lastActiveAt: lastActiveMap.get(entry.name) ?? null
|
|
3794
4591
|
};
|
|
@@ -3804,7 +4601,7 @@ ${user.trimEnd()}
|
|
|
3804
4601
|
const entry = findMind(name);
|
|
3805
4602
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3806
4603
|
if (!existsSync12(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
|
|
3807
|
-
const
|
|
4604
|
+
const mindStatus = await getMindStatus(name, entry.port);
|
|
3808
4605
|
const variants = readVariants(name);
|
|
3809
4606
|
const manager = getMindManager();
|
|
3810
4607
|
const variantStatuses = await Promise.all(
|
|
@@ -3819,15 +4616,17 @@ ${user.trimEnd()}
|
|
|
3819
4616
|
})
|
|
3820
4617
|
);
|
|
3821
4618
|
const hasPages = existsSync12(resolve16(mindDir(name), "home", "pages"));
|
|
3822
|
-
return c.json({ ...entry,
|
|
4619
|
+
return c.json({ ...entry, ...mindStatus, variants: variantStatuses, hasPages });
|
|
3823
4620
|
}).post("/:name/start", requireAdmin, async (c) => {
|
|
3824
4621
|
const name = c.req.param("name");
|
|
3825
4622
|
const [baseName, variantName] = name.split("@", 2);
|
|
3826
4623
|
const entry = findMind(baseName);
|
|
3827
4624
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4625
|
+
let targetPort = entry.port;
|
|
3828
4626
|
if (variantName) {
|
|
3829
4627
|
const variant = findVariant(baseName, variantName);
|
|
3830
4628
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
4629
|
+
targetPort = variant.port;
|
|
3831
4630
|
} else {
|
|
3832
4631
|
const dir = mindDir(baseName);
|
|
3833
4632
|
if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
@@ -3837,7 +4636,7 @@ ${user.trimEnd()}
|
|
|
3837
4636
|
}
|
|
3838
4637
|
try {
|
|
3839
4638
|
await startMindFull(name);
|
|
3840
|
-
return c.json({ ok: true });
|
|
4639
|
+
return c.json({ ok: true, port: targetPort });
|
|
3841
4640
|
} catch (err) {
|
|
3842
4641
|
return c.json({ error: err instanceof Error ? err.message : "Failed to start mind" }, 500);
|
|
3843
4642
|
}
|
|
@@ -3846,9 +4645,11 @@ ${user.trimEnd()}
|
|
|
3846
4645
|
const [baseName, variantName] = name.split("@", 2);
|
|
3847
4646
|
const entry = findMind(baseName);
|
|
3848
4647
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4648
|
+
let targetPort = entry.port;
|
|
3849
4649
|
if (variantName) {
|
|
3850
4650
|
const variant = findVariant(baseName, variantName);
|
|
3851
4651
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
4652
|
+
targetPort = variant.port;
|
|
3852
4653
|
} else {
|
|
3853
4654
|
const dir = mindDir(baseName);
|
|
3854
4655
|
if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
@@ -3944,7 +4745,7 @@ ${user.trimEnd()}
|
|
|
3944
4745
|
}
|
|
3945
4746
|
}
|
|
3946
4747
|
await startMindFull(name);
|
|
3947
|
-
return c.json({ ok: true });
|
|
4748
|
+
return c.json({ ok: true, port: targetPort });
|
|
3948
4749
|
} catch (err) {
|
|
3949
4750
|
return c.json({ error: err instanceof Error ? err.message : "Failed to restart mind" }, 500);
|
|
3950
4751
|
}
|
|
@@ -3996,10 +4797,10 @@ ${user.trimEnd()}
|
|
|
3996
4797
|
await deleteMindUser2(name);
|
|
3997
4798
|
const state = stateDir(name);
|
|
3998
4799
|
if (existsSync12(state)) {
|
|
3999
|
-
|
|
4800
|
+
rmSync2(state, { recursive: true, force: true });
|
|
4000
4801
|
}
|
|
4001
4802
|
if (force && existsSync12(dir)) {
|
|
4002
|
-
|
|
4803
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
4003
4804
|
deleteMindUser(name);
|
|
4004
4805
|
}
|
|
4005
4806
|
return c.json({ ok: true });
|
|
@@ -4094,7 +4895,7 @@ ${user.trimEnd()}
|
|
|
4094
4895
|
await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
|
|
4095
4896
|
chownMindDir(dir, mindName);
|
|
4096
4897
|
} catch (err) {
|
|
4097
|
-
|
|
4898
|
+
rmSync2(resolve16(dir, ".git"), { recursive: true, force: true });
|
|
4098
4899
|
return c.json(
|
|
4099
4900
|
{
|
|
4100
4901
|
error: `Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4121,7 +4922,7 @@ ${user.trimEnd()}
|
|
|
4121
4922
|
await updateTemplateBranch(dir, template, mindName);
|
|
4122
4923
|
const parentDir = resolve16(dir, ".variants");
|
|
4123
4924
|
if (!existsSync12(parentDir)) {
|
|
4124
|
-
|
|
4925
|
+
mkdirSync7(parentDir, { recursive: true });
|
|
4125
4926
|
}
|
|
4126
4927
|
await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
4127
4928
|
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
@@ -4266,7 +5067,7 @@ ${user.trimEnd()}
|
|
|
4266
5067
|
const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
|
|
4267
5068
|
const msgCount = countResult[0]?.count ?? 0;
|
|
4268
5069
|
if (msgCount >= 10 && msgCount % 10 === 0) {
|
|
4269
|
-
const nudge = "\n[You've been exploring for a while. Whenever you feel ready, write your SOUL.md and MEMORY.md, then run volute sprout.]";
|
|
5070
|
+
const nudge = "\n[You've been exploring for a while. Whenever you feel ready, write your SOUL.md and MEMORY.md, then run volute mind sprout.]";
|
|
4270
5071
|
if (typeof parsed.content === "string") {
|
|
4271
5072
|
parsed.content = parsed.content + nudge;
|
|
4272
5073
|
} else if (Array.isArray(parsed.content)) {
|
|
@@ -4459,7 +5260,7 @@ ${user.trimEnd()}
|
|
|
4459
5260
|
const stream = new ReadableStream({
|
|
4460
5261
|
start(controller) {
|
|
4461
5262
|
const encoder = new TextEncoder();
|
|
4462
|
-
const
|
|
5263
|
+
const send5 = (data) => {
|
|
4463
5264
|
controller.enqueue(encoder.encode(`data: ${data}
|
|
4464
5265
|
|
|
4465
5266
|
`));
|
|
@@ -4468,7 +5269,7 @@ ${user.trimEnd()}
|
|
|
4468
5269
|
if (typeFilter && !typeFilter.includes(event.type)) return;
|
|
4469
5270
|
if (sessionFilter && event.session !== sessionFilter) return;
|
|
4470
5271
|
if (channelFilter && event.channel !== channelFilter) return;
|
|
4471
|
-
|
|
5272
|
+
send5(JSON.stringify(event));
|
|
4472
5273
|
});
|
|
4473
5274
|
c.req.raw.signal.addEventListener("abort", () => {
|
|
4474
5275
|
unsubscribe();
|
|
@@ -4528,6 +5329,12 @@ ${user.trimEnd()}
|
|
|
4528
5329
|
const db = await getDb();
|
|
4529
5330
|
const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
|
|
4530
5331
|
return c.json(rows.map((r) => r.channel));
|
|
5332
|
+
}).get("/:name/history/export", async (c) => {
|
|
5333
|
+
const name = c.req.param("name");
|
|
5334
|
+
if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
|
|
5335
|
+
const db = await getDb();
|
|
5336
|
+
const rows = await db.select().from(mindHistory).where(eq4(mindHistory.mind, name));
|
|
5337
|
+
return c.json(rows);
|
|
4531
5338
|
}).get("/:name/history", async (c) => {
|
|
4532
5339
|
const name = c.req.param("name");
|
|
4533
5340
|
const channel = c.req.query("channel");
|
|
@@ -4552,8 +5359,8 @@ ${user.trimEnd()}
|
|
|
4552
5359
|
var minds_default = app11;
|
|
4553
5360
|
|
|
4554
5361
|
// src/web/api/pages.ts
|
|
4555
|
-
import { readFile as readFile2, stat } from "fs/promises";
|
|
4556
|
-
import { extname, resolve as resolve17 } from "path";
|
|
5362
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
5363
|
+
import { extname as extname2, resolve as resolve17 } from "path";
|
|
4557
5364
|
import { Hono as Hono12 } from "hono";
|
|
4558
5365
|
var MIME_TYPES = {
|
|
4559
5366
|
".html": "text/html",
|
|
@@ -4583,10 +5390,10 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
|
|
|
4583
5390
|
const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
|
|
4584
5391
|
const requestedPath = resolve17(pagesRoot, wildcard.slice(1));
|
|
4585
5392
|
if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
|
|
4586
|
-
let fileStat = await
|
|
5393
|
+
let fileStat = await stat2(requestedPath).catch(() => null);
|
|
4587
5394
|
if (fileStat?.isDirectory()) {
|
|
4588
5395
|
const indexPath = resolve17(requestedPath, "index.html");
|
|
4589
|
-
fileStat = await
|
|
5396
|
+
fileStat = await stat2(indexPath).catch(() => null);
|
|
4590
5397
|
if (fileStat?.isFile()) {
|
|
4591
5398
|
const body = await readFile2(indexPath);
|
|
4592
5399
|
return c.body(body, 200, { "Content-Type": "text/html" });
|
|
@@ -4594,7 +5401,7 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
|
|
|
4594
5401
|
return c.text("Not found", 404);
|
|
4595
5402
|
}
|
|
4596
5403
|
if (fileStat?.isFile()) {
|
|
4597
|
-
const ext =
|
|
5404
|
+
const ext = extname2(requestedPath);
|
|
4598
5405
|
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
4599
5406
|
const body = await readFile2(requestedPath);
|
|
4600
5407
|
return c.body(body, 200, { "Content-Type": mime });
|
|
@@ -4827,9 +5634,9 @@ var app15 = new Hono15().post("/:name/shared/merge", requireAdmin, async (c) =>
|
|
|
4827
5634
|
var shared_default = app15;
|
|
4828
5635
|
|
|
4829
5636
|
// src/web/api/skills.ts
|
|
4830
|
-
import { existsSync as existsSync13, mkdtempSync, readdirSync as
|
|
4831
|
-
import { tmpdir
|
|
4832
|
-
import { join as
|
|
5637
|
+
import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
|
|
5638
|
+
import { tmpdir } from "os";
|
|
5639
|
+
import { join as join3, resolve as resolve18 } from "path";
|
|
4833
5640
|
import AdmZip from "adm-zip";
|
|
4834
5641
|
import { Hono as Hono16 } from "hono";
|
|
4835
5642
|
var app16 = new Hono16().get("/", async (c) => {
|
|
@@ -4839,7 +5646,7 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
4839
5646
|
const id = c.req.param("id");
|
|
4840
5647
|
const skill = await getSharedSkill(id);
|
|
4841
5648
|
if (!skill) return c.json({ error: "Skill not found" }, 404);
|
|
4842
|
-
const dir =
|
|
5649
|
+
const dir = join3(sharedSkillsDir(), id);
|
|
4843
5650
|
const files = listFilesRecursive(dir);
|
|
4844
5651
|
return c.json({ ...skill, files });
|
|
4845
5652
|
}).post("/upload", requireAdmin, async (c) => {
|
|
@@ -4852,7 +5659,7 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
4852
5659
|
return c.json({ error: "Only .zip files are accepted" }, 400);
|
|
4853
5660
|
}
|
|
4854
5661
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
4855
|
-
const tmpDir = mkdtempSync(
|
|
5662
|
+
const tmpDir = mkdtempSync(join3(tmpdir(), "volute-skill-upload-"));
|
|
4856
5663
|
try {
|
|
4857
5664
|
const zip = new AdmZip(buffer);
|
|
4858
5665
|
for (const entry of zip.getEntries()) {
|
|
@@ -4863,13 +5670,13 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
4863
5670
|
}
|
|
4864
5671
|
zip.extractAllTo(tmpDir, true);
|
|
4865
5672
|
let skillDir = null;
|
|
4866
|
-
if (existsSync13(
|
|
5673
|
+
if (existsSync13(join3(tmpDir, "SKILL.md"))) {
|
|
4867
5674
|
skillDir = tmpDir;
|
|
4868
5675
|
} else {
|
|
4869
|
-
const entries =
|
|
5676
|
+
const entries = readdirSync6(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
4870
5677
|
for (const entry of entries) {
|
|
4871
|
-
if (existsSync13(
|
|
4872
|
-
skillDir =
|
|
5678
|
+
if (existsSync13(join3(tmpDir, entry.name, "SKILL.md"))) {
|
|
5679
|
+
skillDir = join3(tmpDir, entry.name);
|
|
4873
5680
|
break;
|
|
4874
5681
|
}
|
|
4875
5682
|
}
|
|
@@ -4885,7 +5692,7 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
4885
5692
|
}
|
|
4886
5693
|
throw e;
|
|
4887
5694
|
} finally {
|
|
4888
|
-
|
|
5695
|
+
rmSync3(tmpDir, { recursive: true, force: true });
|
|
4889
5696
|
}
|
|
4890
5697
|
}).delete("/:id", requireAdmin, async (c) => {
|
|
4891
5698
|
const id = c.req.param("id");
|
|
@@ -4987,13 +5794,13 @@ var app19 = new Hono19().get("/update", async (c) => {
|
|
|
4987
5794
|
var update_default = app19;
|
|
4988
5795
|
|
|
4989
5796
|
// src/web/api/variants.ts
|
|
4990
|
-
import { existsSync as existsSync14, mkdirSync as
|
|
5797
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
4991
5798
|
import { resolve as resolve20 } from "path";
|
|
4992
5799
|
import { Hono as Hono20 } from "hono";
|
|
4993
5800
|
|
|
4994
5801
|
// src/lib/spawn-server.ts
|
|
4995
5802
|
import { spawn as spawn4 } from "child_process";
|
|
4996
|
-
import { closeSync, mkdirSync as
|
|
5803
|
+
import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync12 } from "fs";
|
|
4997
5804
|
import { resolve as resolve19 } from "path";
|
|
4998
5805
|
function tsxBin(cwd) {
|
|
4999
5806
|
return resolve19(cwd, "node_modules", ".bin", "tsx");
|
|
@@ -5032,7 +5839,7 @@ function spawnAttached(cwd, port) {
|
|
|
5032
5839
|
}
|
|
5033
5840
|
function spawnDetached(cwd, port, logDir) {
|
|
5034
5841
|
const logsDir = logDir ?? resolve19(cwd, ".mind", "logs");
|
|
5035
|
-
|
|
5842
|
+
mkdirSync8(logsDir, { recursive: true });
|
|
5036
5843
|
const logPath = resolve19(logsDir, "mind.log");
|
|
5037
5844
|
const logFd = openSync(logPath, "a");
|
|
5038
5845
|
const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
@@ -5117,6 +5924,16 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5117
5924
|
return { ...v, status: health.ok ? "running" : "dead" };
|
|
5118
5925
|
})
|
|
5119
5926
|
);
|
|
5927
|
+
try {
|
|
5928
|
+
const updated = results.map(({ status, ...v }) => ({
|
|
5929
|
+
...v,
|
|
5930
|
+
running: status === "running"
|
|
5931
|
+
}));
|
|
5932
|
+
const changed = variants.some((v, i) => v.running !== updated[i].running);
|
|
5933
|
+
if (changed) writeVariants(name, updated);
|
|
5934
|
+
} catch (err) {
|
|
5935
|
+
logger_default.warn(`failed to sync variant status for ${name}`, logger_default.errorData(err));
|
|
5936
|
+
}
|
|
5120
5937
|
return c.json(results);
|
|
5121
5938
|
}).post("/:name/variants", requireAdmin, async (c) => {
|
|
5122
5939
|
const mindName = c.req.param("name");
|
|
@@ -5139,7 +5956,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5139
5956
|
if (existsSync14(variantDir)) {
|
|
5140
5957
|
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
5141
5958
|
}
|
|
5142
|
-
|
|
5959
|
+
mkdirSync9(resolve20(projectRoot, ".variants"), { recursive: true });
|
|
5143
5960
|
try {
|
|
5144
5961
|
await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
5145
5962
|
} catch (e) {
|
|
@@ -5162,7 +5979,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5162
5979
|
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
5163
5980
|
}
|
|
5164
5981
|
if (body.soul) {
|
|
5165
|
-
|
|
5982
|
+
writeFileSync9(resolve20(variantDir, "home/SOUL.md"), body.soul);
|
|
5166
5983
|
}
|
|
5167
5984
|
const variantPort = body.port ?? nextPort();
|
|
5168
5985
|
const variant = {
|
|
@@ -5268,6 +6085,16 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5268
6085
|
} catch {
|
|
5269
6086
|
}
|
|
5270
6087
|
removeVariant(mindName, variantName);
|
|
6088
|
+
if (variantName === "upgrade") {
|
|
6089
|
+
try {
|
|
6090
|
+
const { computeTemplateHash: computeTemplateHash2 } = await import("./template-hash-BIMA4ILT.js");
|
|
6091
|
+
const { setMindTemplateHash: setMindTemplateHash2 } = await import("./registry-D2BSQ2X5.js");
|
|
6092
|
+
const tmpl = entry.template ?? "claude";
|
|
6093
|
+
setMindTemplateHash2(mindName, computeTemplateHash2(tmpl));
|
|
6094
|
+
} catch (err) {
|
|
6095
|
+
console.error(`[daemon] failed to update template hash for ${mindName}:`, err);
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
5271
6098
|
chownMindDir(projectRoot, mindName);
|
|
5272
6099
|
try {
|
|
5273
6100
|
if (isIsolationEnabled()) {
|
|
@@ -5420,7 +6247,7 @@ async function fanOutToMinds(opts) {
|
|
|
5420
6247
|
const participantNames = participants.map((p) => p.username);
|
|
5421
6248
|
const isDM = opts.isDM ?? participants.length === 2;
|
|
5422
6249
|
const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
|
|
5423
|
-
const { getMindManager: getMindManager2 } = await import("./mind-manager-
|
|
6250
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-3V2NXX4I.js");
|
|
5424
6251
|
const manager = getMindManager2();
|
|
5425
6252
|
const runningMinds = mindParticipants.map((ap) => {
|
|
5426
6253
|
const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
|
|
@@ -5865,14 +6692,14 @@ async function startServer({
|
|
|
5865
6692
|
hostname = "127.0.0.1"
|
|
5866
6693
|
}) {
|
|
5867
6694
|
let assetsDir = "";
|
|
5868
|
-
let searchDir =
|
|
6695
|
+
let searchDir = dirname2(new URL(import.meta.url).pathname);
|
|
5869
6696
|
for (let i = 0; i < 5; i++) {
|
|
5870
6697
|
const candidate = resolve21(searchDir, "dist", "web-assets");
|
|
5871
6698
|
if (existsSync15(candidate)) {
|
|
5872
6699
|
assetsDir = candidate;
|
|
5873
6700
|
break;
|
|
5874
6701
|
}
|
|
5875
|
-
searchDir =
|
|
6702
|
+
searchDir = dirname2(searchDir);
|
|
5876
6703
|
}
|
|
5877
6704
|
if (assetsDir) {
|
|
5878
6705
|
app_default.get("*", async (c) => {
|
|
@@ -5880,15 +6707,15 @@ async function startServer({
|
|
|
5880
6707
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
5881
6708
|
const filePath = resolve21(assetsDir, urlPath.slice(1));
|
|
5882
6709
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
5883
|
-
const s = await
|
|
6710
|
+
const s = await stat3(filePath).catch(() => null);
|
|
5884
6711
|
if (s?.isFile()) {
|
|
5885
|
-
const ext =
|
|
6712
|
+
const ext = extname3(filePath);
|
|
5886
6713
|
const mime = MIME_TYPES2[ext] || "application/octet-stream";
|
|
5887
6714
|
const body = await readFile3(filePath);
|
|
5888
6715
|
return c.body(body, 200, { "Content-Type": mime });
|
|
5889
6716
|
}
|
|
5890
6717
|
const indexPath = resolve21(assetsDir, "index.html");
|
|
5891
|
-
const indexStat = await
|
|
6718
|
+
const indexStat = await stat3(indexPath).catch(() => null);
|
|
5892
6719
|
if (indexStat?.isFile()) {
|
|
5893
6720
|
const body = await readFile3(indexPath, "utf-8");
|
|
5894
6721
|
return c.html(body);
|
|
@@ -5930,7 +6757,7 @@ async function startDaemon(opts) {
|
|
|
5930
6757
|
}
|
|
5931
6758
|
const DAEMON_PID_PATH = resolve22(home, "daemon.pid");
|
|
5932
6759
|
const DAEMON_JSON_PATH = resolve22(home, "daemon.json");
|
|
5933
|
-
|
|
6760
|
+
mkdirSync10(home, { recursive: true });
|
|
5934
6761
|
migrateAgentsToMinds();
|
|
5935
6762
|
try {
|
|
5936
6763
|
await ensureSharedRepo();
|
|
@@ -5958,8 +6785,8 @@ async function startDaemon(opts) {
|
|
|
5958
6785
|
}
|
|
5959
6786
|
throw err;
|
|
5960
6787
|
}
|
|
5961
|
-
|
|
5962
|
-
|
|
6788
|
+
writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
6789
|
+
writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
5963
6790
|
`, {
|
|
5964
6791
|
mode: 420
|
|
5965
6792
|
});
|
|
@@ -6015,10 +6842,20 @@ async function startDaemon(opts) {
|
|
|
6015
6842
|
});
|
|
6016
6843
|
await Promise.all(workers);
|
|
6017
6844
|
}
|
|
6845
|
+
try {
|
|
6846
|
+
const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-TFS2U5CF.js");
|
|
6847
|
+
backfillTemplateHashes();
|
|
6848
|
+
notifyVersionUpdate().catch((err) => {
|
|
6849
|
+
logger_default.warn("failed to send version update notifications", logger_default.errorData(err));
|
|
6850
|
+
});
|
|
6851
|
+
} catch (err) {
|
|
6852
|
+
logger_default.warn("failed to initialize version notifications", logger_default.errorData(err));
|
|
6853
|
+
}
|
|
6018
6854
|
delivery.restoreFromDb().catch((err) => {
|
|
6019
6855
|
logger_default.warn("failed to restore delivery queue", logger_default.errorData(err));
|
|
6020
6856
|
});
|
|
6021
|
-
cleanExpiredSessions().catch(() => {
|
|
6857
|
+
cleanExpiredSessions().catch((err) => {
|
|
6858
|
+
logger_default.warn("failed to clean expired sessions", logger_default.errorData(err));
|
|
6022
6859
|
});
|
|
6023
6860
|
logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
|
|
6024
6861
|
function cleanup() {
|
|
@@ -6041,19 +6878,33 @@ async function startDaemon(opts) {
|
|
|
6041
6878
|
if (shuttingDown) return;
|
|
6042
6879
|
shuttingDown = true;
|
|
6043
6880
|
logger_default.info("shutting down...");
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6881
|
+
const safe = (label, fn) => {
|
|
6882
|
+
try {
|
|
6883
|
+
const result = fn();
|
|
6884
|
+
if (result instanceof Promise)
|
|
6885
|
+
return result.catch((err) => logger_default.error(`shutdown: ${label} failed`, logger_default.errorData(err)));
|
|
6886
|
+
} catch (err) {
|
|
6887
|
+
logger_default.error(`shutdown: ${label} failed`, logger_default.errorData(err));
|
|
6888
|
+
}
|
|
6889
|
+
};
|
|
6890
|
+
try {
|
|
6891
|
+
safe("stopAllWatchers", stopAllWatchers);
|
|
6892
|
+
safe("stopAllActivityTrackers", stopAll);
|
|
6893
|
+
safe("scheduler.stop", () => scheduler.stop());
|
|
6894
|
+
safe("scheduler.saveState", () => scheduler.saveState());
|
|
6895
|
+
safe("mailPoller.stop", () => mailPoller.stop());
|
|
6896
|
+
safe("tokenBudget.stop", () => tokenBudget.stop());
|
|
6897
|
+
safe("delivery.dispose", () => delivery.dispose());
|
|
6898
|
+
await safe("connectors.stopAll", () => connectors.stopAll());
|
|
6899
|
+
await safe("manager.stopAll", () => manager.stopAll());
|
|
6900
|
+
safe("clearCrashAttempts", () => manager.clearCrashAttempts());
|
|
6901
|
+
safe("server.close", () => server.close());
|
|
6902
|
+
} catch (err) {
|
|
6903
|
+
logger_default.error("error during shutdown", logger_default.errorData(err));
|
|
6904
|
+
} finally {
|
|
6905
|
+
cleanup();
|
|
6906
|
+
process.exit(0);
|
|
6907
|
+
}
|
|
6057
6908
|
}
|
|
6058
6909
|
process.on("SIGINT", shutdown);
|
|
6059
6910
|
process.on("SIGTERM", shutdown);
|