volute 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-AYB7XAWO.js +812 -0
- package/dist/{chunk-3FD4ZZUL.js → chunk-FW5API7X.js} +116 -10
- package/dist/{chunk-3FC42ZBM.js → chunk-GK4E7LM7.js} +3 -0
- package/dist/cli.js +18 -6
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/slack.js +1 -1
- package/dist/connectors/telegram.js +1 -1
- package/dist/{daemon-restart-MS5FI44G.js → daemon-restart-2HVTHZAT.js} +1 -1
- package/dist/daemon.js +1443 -592
- package/dist/history-YUEKTJ2N.js +108 -0
- package/dist/{mind-manager-PN5SUDJ4.js → mind-manager-Z7O7PN2O.js} +1 -1
- package/dist/{package-3QGV3KX6.js → package-OKLFO7UY.js} +8 -9
- package/dist/{send-KBBZNYG6.js → send-BNDTLUPM.js} +41 -9
- package/dist/skill-2Y42P4JY.js +287 -0
- package/dist/{up-GZLWZAQE.js → up-7B3BWF2U.js} +1 -1
- package/dist/web-assets/assets/index-CtiimdWK.css +1 -0
- package/dist/web-assets/assets/index-kt1_EcuO.js +63 -0
- package/dist/web-assets/index.html +2 -1
- package/drizzle/0006_mind_history.sql +20 -0
- package/drizzle/0007_system_prompts.sql +5 -0
- package/drizzle/0008_volute_channels.sql +24 -0
- package/drizzle/0009_shared_skills.sql +9 -0
- package/drizzle/meta/0006_snapshot.json +7 -0
- package/drizzle/meta/0007_snapshot.json +7 -0
- package/drizzle/meta/0008_snapshot.json +7 -0
- package/drizzle/meta/0009_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +8 -9
- package/templates/_base/.init/.config/prompts.json +5 -0
- package/templates/_base/_skills/volute-mind/SKILL.md +19 -5
- package/templates/_base/src/lib/daemon-client.ts +45 -0
- package/templates/_base/src/lib/logger.ts +19 -0
- package/templates/_base/src/lib/router.ts +48 -41
- package/templates/_base/src/lib/routing.ts +5 -8
- package/templates/_base/src/lib/startup.ts +43 -0
- package/templates/_base/src/lib/transparency.ts +89 -0
- package/templates/_base/src/lib/types.ts +0 -1
- package/templates/_base/src/lib/volute-server.ts +3 -35
- package/templates/claude/src/agent.ts +9 -22
- package/templates/claude/src/lib/hooks/reply-instructions.ts +6 -9
- package/templates/claude/src/lib/stream-consumer.ts +39 -12
- package/templates/pi/src/agent.ts +9 -22
- package/templates/pi/src/lib/event-handler.ts +58 -7
- package/templates/pi/src/lib/reply-instructions-extension.ts +6 -9
- package/dist/chunk-J52CJCVI.js +0 -447
- package/dist/history-LKCJJMUV.js +0 -50
- package/dist/web-assets/assets/index-B1XIIGCh.js +0 -307
- package/templates/_base/src/lib/auto-reply.ts +0 -38
- /package/dist/{chunk-LLBBVTEY.js → chunk-6DVBMLVN.js} +0 -0
package/dist/daemon.js
CHANGED
|
@@ -10,13 +10,30 @@ import {
|
|
|
10
10
|
readSystemsConfig
|
|
11
11
|
} from "./chunk-37X7ECMF.js";
|
|
12
12
|
import {
|
|
13
|
+
PROMPT_DEFAULTS,
|
|
14
|
+
PROMPT_KEYS,
|
|
13
15
|
RotatingLog,
|
|
14
16
|
clearJsonMap,
|
|
17
|
+
conversationParticipants,
|
|
18
|
+
conversations,
|
|
19
|
+
getDb,
|
|
15
20
|
getMindManager,
|
|
21
|
+
getMindPromptDefaults,
|
|
22
|
+
getPrompt,
|
|
23
|
+
getPromptIfCustom,
|
|
16
24
|
initMindManager,
|
|
17
25
|
loadJsonMap,
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
logBuffer,
|
|
27
|
+
logger_default,
|
|
28
|
+
messages,
|
|
29
|
+
mindHistory,
|
|
30
|
+
saveJsonMap,
|
|
31
|
+
sessions,
|
|
32
|
+
sharedSkills,
|
|
33
|
+
substitute,
|
|
34
|
+
systemPrompts,
|
|
35
|
+
users
|
|
36
|
+
} from "./chunk-AYB7XAWO.js";
|
|
20
37
|
import {
|
|
21
38
|
findOpenClawSession,
|
|
22
39
|
importOpenClawConnectors,
|
|
@@ -35,7 +52,7 @@ import {
|
|
|
35
52
|
import {
|
|
36
53
|
CHANNELS,
|
|
37
54
|
getChannelDriver
|
|
38
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-FW5API7X.js";
|
|
39
56
|
import {
|
|
40
57
|
exec,
|
|
41
58
|
gitExec,
|
|
@@ -58,7 +75,7 @@ import "./chunk-D424ZQGI.js";
|
|
|
58
75
|
import {
|
|
59
76
|
buildVoluteSlug,
|
|
60
77
|
writeChannelEntry
|
|
61
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-GK4E7LM7.js";
|
|
62
79
|
import {
|
|
63
80
|
addMind,
|
|
64
81
|
addVariant,
|
|
@@ -83,15 +100,13 @@ import {
|
|
|
83
100
|
validateMindName,
|
|
84
101
|
voluteHome
|
|
85
102
|
} from "./chunk-M77QBTEH.js";
|
|
86
|
-
import
|
|
87
|
-
__export
|
|
88
|
-
} from "./chunk-K3NQKI34.js";
|
|
103
|
+
import "./chunk-K3NQKI34.js";
|
|
89
104
|
|
|
90
105
|
// src/daemon.ts
|
|
91
106
|
import { randomBytes } from "crypto";
|
|
92
|
-
import { mkdirSync as
|
|
107
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
93
108
|
import { homedir as homedir2 } from "os";
|
|
94
|
-
import { resolve as
|
|
109
|
+
import { resolve as resolve18 } from "path";
|
|
95
110
|
import { format } from "util";
|
|
96
111
|
|
|
97
112
|
// src/lib/connector-manager.ts
|
|
@@ -172,6 +187,7 @@ function checkMissingEnvVars(def, env) {
|
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
// src/lib/connector-manager.ts
|
|
190
|
+
var clog = logger_default.child("connectors");
|
|
175
191
|
function searchUpwards(...segments) {
|
|
176
192
|
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
177
193
|
for (let i = 0; i < 5; i++) {
|
|
@@ -198,7 +214,7 @@ var ConnectorManager = class {
|
|
|
198
214
|
try {
|
|
199
215
|
await this.startConnector(mindName, mindDir2, mindPort, type, daemonPort);
|
|
200
216
|
} catch (err) {
|
|
201
|
-
|
|
217
|
+
clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
202
218
|
}
|
|
203
219
|
}
|
|
204
220
|
}
|
|
@@ -311,28 +327,26 @@ var ConnectorManager = class {
|
|
|
311
327
|
}
|
|
312
328
|
if (this.shuttingDown) return;
|
|
313
329
|
if (this.stopping.has(stopKey)) return;
|
|
314
|
-
|
|
315
|
-
if (lastStderr)
|
|
330
|
+
clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
|
|
331
|
+
if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
|
|
316
332
|
const attempts = this.restartAttempts.get(stopKey) ?? 0;
|
|
317
333
|
if (attempts >= MAX_RESTART_ATTEMPTS) {
|
|
318
|
-
|
|
319
|
-
`[daemon] connector ${type} for ${mindName} crashed ${attempts} times \u2014 giving up`
|
|
320
|
-
);
|
|
334
|
+
clog.error(`connector ${type} for ${mindName} crashed ${attempts} times \u2014 giving up`);
|
|
321
335
|
return;
|
|
322
336
|
}
|
|
323
337
|
const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
|
|
324
338
|
this.restartAttempts.set(stopKey, attempts + 1);
|
|
325
|
-
|
|
326
|
-
`
|
|
339
|
+
clog.info(
|
|
340
|
+
`restarting connector ${type} for ${mindName} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, in ${delay}ms`
|
|
327
341
|
);
|
|
328
342
|
setTimeout(() => {
|
|
329
343
|
if (this.shuttingDown || this.stopping.has(stopKey)) return;
|
|
330
344
|
this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
331
|
-
|
|
345
|
+
clog.error(`failed to restart connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
332
346
|
});
|
|
333
347
|
}, delay);
|
|
334
348
|
});
|
|
335
|
-
|
|
349
|
+
clog.info(`started connector ${type} for ${mindName}`);
|
|
336
350
|
}
|
|
337
351
|
async stopConnector(mindName, type) {
|
|
338
352
|
const mindMap = this.connectors.get(mindName);
|
|
@@ -342,19 +356,19 @@ var ConnectorManager = class {
|
|
|
342
356
|
const stopKey = `${mindName}:${type}`;
|
|
343
357
|
this.stopping.add(stopKey);
|
|
344
358
|
mindMap.delete(type);
|
|
345
|
-
await new Promise((
|
|
346
|
-
tracked.child.on("exit", () =>
|
|
359
|
+
await new Promise((resolve19) => {
|
|
360
|
+
tracked.child.on("exit", () => resolve19());
|
|
347
361
|
try {
|
|
348
362
|
tracked.child.kill("SIGTERM");
|
|
349
363
|
} catch {
|
|
350
|
-
|
|
364
|
+
resolve19();
|
|
351
365
|
}
|
|
352
366
|
setTimeout(() => {
|
|
353
367
|
try {
|
|
354
368
|
tracked.child.kill("SIGKILL");
|
|
355
369
|
} catch {
|
|
356
370
|
}
|
|
357
|
-
|
|
371
|
+
resolve19();
|
|
358
372
|
}, 5e3);
|
|
359
373
|
});
|
|
360
374
|
this.stopping.delete(stopKey);
|
|
@@ -362,9 +376,9 @@ var ConnectorManager = class {
|
|
|
362
376
|
try {
|
|
363
377
|
this.removeConnectorPid(mindName, type);
|
|
364
378
|
} catch (err) {
|
|
365
|
-
|
|
379
|
+
clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
|
|
366
380
|
}
|
|
367
|
-
|
|
381
|
+
clog.info(`stopped connector ${type} for ${mindName}`);
|
|
368
382
|
}
|
|
369
383
|
async stopConnectors(mindName) {
|
|
370
384
|
const mindMap = this.connectors.get(mindName);
|
|
@@ -407,7 +421,7 @@ var ConnectorManager = class {
|
|
|
407
421
|
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
408
422
|
if (pid > 0) {
|
|
409
423
|
process.kill(pid, "SIGTERM");
|
|
410
|
-
|
|
424
|
+
clog.warn(`killed orphan connector ${type} (pid ${pid})`);
|
|
411
425
|
}
|
|
412
426
|
} catch {
|
|
413
427
|
}
|
|
@@ -435,6 +449,7 @@ function getConnectorManager() {
|
|
|
435
449
|
}
|
|
436
450
|
|
|
437
451
|
// src/lib/mail-poller.ts
|
|
452
|
+
var mlog = logger_default.child("mail");
|
|
438
453
|
function formatEmailContent(email) {
|
|
439
454
|
if (email.body) {
|
|
440
455
|
return email.subject ? `Subject: ${email.subject}
|
|
@@ -448,89 +463,198 @@ ${email.body}` : email.body;
|
|
|
448
463
|
}
|
|
449
464
|
return email.subject ? `Subject: ${email.subject}` : "[Empty email]";
|
|
450
465
|
}
|
|
466
|
+
var PING_INTERVAL_MS = 3e4;
|
|
467
|
+
var INITIAL_RECONNECT_MS = 1e3;
|
|
468
|
+
var MAX_RECONNECT_MS = 6e4;
|
|
451
469
|
var MailPoller = class {
|
|
452
|
-
|
|
470
|
+
ws = null;
|
|
453
471
|
daemonPort = null;
|
|
454
472
|
daemonToken = null;
|
|
455
|
-
lastPoll = null;
|
|
456
473
|
running = false;
|
|
474
|
+
pingTimer = null;
|
|
475
|
+
reconnectTimer = null;
|
|
476
|
+
reconnectDelay = INITIAL_RECONNECT_MS;
|
|
477
|
+
reconnectAttempts = 0;
|
|
478
|
+
disconnectedAt = null;
|
|
457
479
|
start(daemonPort, daemonToken) {
|
|
458
480
|
if (this.running) {
|
|
459
|
-
|
|
481
|
+
mlog.warn("already running \u2014 ignoring duplicate start");
|
|
460
482
|
return;
|
|
461
483
|
}
|
|
462
484
|
const config = readSystemsConfig();
|
|
463
485
|
if (!config) {
|
|
464
|
-
|
|
486
|
+
mlog.info("no systems config \u2014 mail disabled");
|
|
465
487
|
return;
|
|
466
488
|
}
|
|
467
489
|
this.daemonPort = daemonPort ?? null;
|
|
468
490
|
this.daemonToken = daemonToken ?? null;
|
|
469
|
-
this.lastPoll = (/* @__PURE__ */ new Date()).toISOString();
|
|
470
491
|
this.running = true;
|
|
471
|
-
this.
|
|
472
|
-
console.error("[mail] polling started");
|
|
492
|
+
this.connect();
|
|
473
493
|
}
|
|
474
494
|
stop() {
|
|
475
|
-
if (this.interval) clearInterval(this.interval);
|
|
476
|
-
this.interval = null;
|
|
477
495
|
this.running = false;
|
|
496
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
497
|
+
this.pingTimer = null;
|
|
498
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
499
|
+
this.reconnectTimer = null;
|
|
500
|
+
if (this.ws) {
|
|
501
|
+
this.ws.close();
|
|
502
|
+
this.ws = null;
|
|
503
|
+
}
|
|
478
504
|
}
|
|
479
505
|
isRunning() {
|
|
480
506
|
return this.running;
|
|
481
507
|
}
|
|
482
|
-
|
|
508
|
+
connect() {
|
|
509
|
+
if (!this.running) return;
|
|
483
510
|
const config = readSystemsConfig();
|
|
484
511
|
if (!config) {
|
|
485
|
-
|
|
512
|
+
mlog.info("systems config removed \u2014 stopping");
|
|
486
513
|
this.stop();
|
|
487
514
|
return;
|
|
488
515
|
}
|
|
489
|
-
const
|
|
490
|
-
const url = `${config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
|
|
516
|
+
const wsUrl = `${config.apiUrl.replace(/^http/, "ws")}/api/ws`;
|
|
491
517
|
try {
|
|
492
|
-
|
|
518
|
+
this.ws = new WebSocket(wsUrl, {
|
|
493
519
|
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
494
520
|
});
|
|
521
|
+
} catch (err) {
|
|
522
|
+
mlog.warn("failed to create WebSocket", logger_default.errorData(err));
|
|
523
|
+
this.scheduleReconnect();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
this.ws.onopen = () => {
|
|
527
|
+
if (this.reconnectAttempts > 0) {
|
|
528
|
+
mlog.info(`reconnected after ${this.reconnectAttempts} attempts`);
|
|
529
|
+
}
|
|
530
|
+
mlog.info("connected");
|
|
531
|
+
this.reconnectAttempts = 0;
|
|
532
|
+
this.reconnectDelay = INITIAL_RECONNECT_MS;
|
|
533
|
+
if (this.disconnectedAt) {
|
|
534
|
+
this.catchUp(this.disconnectedAt);
|
|
535
|
+
this.disconnectedAt = null;
|
|
536
|
+
}
|
|
537
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
538
|
+
this.pingTimer = setInterval(() => {
|
|
539
|
+
try {
|
|
540
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
541
|
+
this.ws.send("ping");
|
|
542
|
+
}
|
|
543
|
+
} catch (err) {
|
|
544
|
+
mlog.warn("ping failed", logger_default.errorData(err));
|
|
545
|
+
}
|
|
546
|
+
}, PING_INTERVAL_MS);
|
|
547
|
+
};
|
|
548
|
+
this.ws.onmessage = (event) => {
|
|
549
|
+
this.handleMessage(String(event.data));
|
|
550
|
+
};
|
|
551
|
+
this.ws.onclose = () => {
|
|
552
|
+
mlog.warn("disconnected");
|
|
553
|
+
if (!this.disconnectedAt) {
|
|
554
|
+
this.disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
555
|
+
}
|
|
556
|
+
this.cleanup();
|
|
557
|
+
this.scheduleReconnect();
|
|
558
|
+
};
|
|
559
|
+
this.ws.onerror = (err) => {
|
|
560
|
+
mlog.warn("WebSocket error", logger_default.errorData(err));
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
cleanup() {
|
|
564
|
+
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
565
|
+
this.pingTimer = null;
|
|
566
|
+
this.ws = null;
|
|
567
|
+
}
|
|
568
|
+
scheduleReconnect() {
|
|
569
|
+
if (!this.running) return;
|
|
570
|
+
this.reconnectAttempts++;
|
|
571
|
+
if (this.reconnectAttempts % 10 === 0) {
|
|
572
|
+
mlog.warn(
|
|
573
|
+
`failed to connect ${this.reconnectAttempts} times \u2014 check systems config and network`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
mlog.info(`reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
|
|
577
|
+
this.reconnectTimer = setTimeout(() => {
|
|
578
|
+
this.reconnectTimer = null;
|
|
579
|
+
this.connect();
|
|
580
|
+
}, this.reconnectDelay);
|
|
581
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_MS);
|
|
582
|
+
}
|
|
583
|
+
/** Fetch emails that arrived while disconnected */
|
|
584
|
+
catchUp(since) {
|
|
585
|
+
const config = readSystemsConfig();
|
|
586
|
+
if (!config) return;
|
|
587
|
+
const url = `${config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
|
|
588
|
+
fetch(url, {
|
|
589
|
+
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
590
|
+
}).then(async (res) => {
|
|
495
591
|
if (!res.ok) {
|
|
496
|
-
|
|
592
|
+
mlog.warn(`catch-up poll failed: HTTP ${res.status}`);
|
|
497
593
|
return;
|
|
498
594
|
}
|
|
499
595
|
const data = await res.json();
|
|
500
|
-
if (!Array.isArray(data.emails))
|
|
501
|
-
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
596
|
+
if (!Array.isArray(data.emails) || data.emails.length === 0) return;
|
|
597
|
+
mlog.info(`catching up on ${data.emails.length} missed emails`);
|
|
504
598
|
for (const email of data.emails) {
|
|
505
599
|
await this.deliver(email.mind, email);
|
|
506
600
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
601
|
+
}).catch((err) => {
|
|
602
|
+
mlog.warn("catch-up error", logger_default.errorData(err));
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
handleMessage(data) {
|
|
606
|
+
if (data === "pong") return;
|
|
607
|
+
let msg;
|
|
608
|
+
try {
|
|
609
|
+
msg = JSON.parse(data);
|
|
610
|
+
} catch {
|
|
611
|
+
mlog.warn(`received unparseable message: ${data.slice(0, 200)}`);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (msg.type !== "email") return;
|
|
615
|
+
if (!msg.mind || !msg.email?.id) {
|
|
616
|
+
mlog.warn(`received malformed email notification: ${data.slice(0, 500)}`);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this.fetchAndDeliver(msg.mind, msg.email).catch((err) => {
|
|
620
|
+
mlog.warn(`failed to process email for ${msg.mind}`, logger_default.errorData(err));
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async fetchAndDeliver(mind, notification) {
|
|
624
|
+
const config = readSystemsConfig();
|
|
625
|
+
if (!config) {
|
|
626
|
+
mlog.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const url = `${config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
|
|
630
|
+
const res = await fetch(url, {
|
|
631
|
+
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
632
|
+
});
|
|
633
|
+
if (!res.ok) {
|
|
634
|
+
mlog.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
|
|
635
|
+
return;
|
|
514
636
|
}
|
|
637
|
+
const email = await res.json();
|
|
638
|
+
await this.deliver(mind, { ...email, mind });
|
|
515
639
|
}
|
|
516
640
|
async deliver(mind, email) {
|
|
517
641
|
const entry = findMind(mind);
|
|
518
642
|
if (!entry || !entry.running) {
|
|
519
|
-
|
|
643
|
+
mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
|
|
520
644
|
return;
|
|
521
645
|
}
|
|
522
646
|
const channel = `mail:${email.from.address}`;
|
|
523
647
|
const sender = email.from.name || email.from.address;
|
|
524
|
-
const
|
|
648
|
+
const text = formatEmailContent(email);
|
|
525
649
|
const body = JSON.stringify({
|
|
526
|
-
content: [{ type: "text", text
|
|
650
|
+
content: [{ type: "text", text }],
|
|
527
651
|
channel,
|
|
528
652
|
sender,
|
|
529
653
|
platform: "Email",
|
|
530
654
|
isDM: true
|
|
531
655
|
});
|
|
532
656
|
if (!this.daemonPort || !this.daemonToken) {
|
|
533
|
-
|
|
657
|
+
mlog.warn(`cannot deliver to ${mind}: daemon port/token not set`);
|
|
534
658
|
return;
|
|
535
659
|
}
|
|
536
660
|
const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
|
|
@@ -548,14 +672,14 @@ var MailPoller = class {
|
|
|
548
672
|
signal: controller.signal
|
|
549
673
|
});
|
|
550
674
|
if (!res.ok) {
|
|
551
|
-
|
|
675
|
+
mlog.warn(`deliver to ${mind} got HTTP ${res.status}`);
|
|
552
676
|
} else {
|
|
553
|
-
|
|
677
|
+
mlog.info(`delivered email from ${email.from.address} to ${mind}`);
|
|
554
678
|
}
|
|
555
679
|
await res.text().catch(() => {
|
|
556
680
|
});
|
|
557
681
|
} catch (err) {
|
|
558
|
-
|
|
682
|
+
mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
|
|
559
683
|
} finally {
|
|
560
684
|
clearTimeout(timeout);
|
|
561
685
|
}
|
|
@@ -578,12 +702,12 @@ async function ensureMailAddress(mindName) {
|
|
|
578
702
|
}
|
|
579
703
|
});
|
|
580
704
|
if (!res.ok) {
|
|
581
|
-
|
|
705
|
+
mlog.warn(`failed to ensure address for ${mindName}: HTTP ${res.status}`);
|
|
582
706
|
}
|
|
583
707
|
await res.text().catch(() => {
|
|
584
708
|
});
|
|
585
709
|
} catch (err) {
|
|
586
|
-
|
|
710
|
+
mlog.warn(`failed to ensure address for ${mindName}`, logger_default.errorData(err));
|
|
587
711
|
}
|
|
588
712
|
}
|
|
589
713
|
|
|
@@ -753,6 +877,7 @@ function migrateMindState(name) {
|
|
|
753
877
|
// src/lib/scheduler.ts
|
|
754
878
|
import { resolve as resolve5 } from "path";
|
|
755
879
|
import { CronExpressionParser } from "cron-parser";
|
|
880
|
+
var slog = logger_default.child("scheduler");
|
|
756
881
|
var Scheduler = class {
|
|
757
882
|
schedules = /* @__PURE__ */ new Map();
|
|
758
883
|
interval = null;
|
|
@@ -824,7 +949,7 @@ var Scheduler = class {
|
|
|
824
949
|
}
|
|
825
950
|
return false;
|
|
826
951
|
} catch (err) {
|
|
827
|
-
|
|
952
|
+
slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
828
953
|
return false;
|
|
829
954
|
}
|
|
830
955
|
}
|
|
@@ -861,14 +986,14 @@ var Scheduler = class {
|
|
|
861
986
|
});
|
|
862
987
|
}
|
|
863
988
|
if (!res.ok) {
|
|
864
|
-
|
|
989
|
+
slog.warn(`"${schedule.id}" for ${mindName} got HTTP ${res.status}`);
|
|
865
990
|
} else {
|
|
866
|
-
|
|
991
|
+
slog.info(`fired "${schedule.id}" for ${mindName}`);
|
|
867
992
|
}
|
|
868
993
|
await res.text().catch(() => {
|
|
869
994
|
});
|
|
870
995
|
} catch (err) {
|
|
871
|
-
|
|
996
|
+
slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
872
997
|
} finally {
|
|
873
998
|
clearTimeout(timeout);
|
|
874
999
|
}
|
|
@@ -881,6 +1006,7 @@ function getScheduler() {
|
|
|
881
1006
|
}
|
|
882
1007
|
|
|
883
1008
|
// src/lib/token-budget.ts
|
|
1009
|
+
var tlog = logger_default.child("token-budget");
|
|
884
1010
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
885
1011
|
var MAX_QUEUE_SIZE = 100;
|
|
886
1012
|
var TokenBudget = class {
|
|
@@ -974,7 +1100,7 @@ var TokenBudget = class {
|
|
|
974
1100
|
const queued = this.drain(mind);
|
|
975
1101
|
if (queued.length > 0) {
|
|
976
1102
|
this.replay(mind, queued).catch((err) => {
|
|
977
|
-
|
|
1103
|
+
tlog.warn(`replay error for ${mind}`, logger_default.errorData(err));
|
|
978
1104
|
});
|
|
979
1105
|
}
|
|
980
1106
|
}
|
|
@@ -982,8 +1108,8 @@ var TokenBudget = class {
|
|
|
982
1108
|
}
|
|
983
1109
|
async replay(mindName, messages2) {
|
|
984
1110
|
if (!this.daemonPort || !this.daemonToken) {
|
|
985
|
-
|
|
986
|
-
`
|
|
1111
|
+
tlog.warn(
|
|
1112
|
+
`cannot replay ${messages2.length} message(s) for ${mindName}: daemon not configured`
|
|
987
1113
|
);
|
|
988
1114
|
const state = this.budgets.get(mindName);
|
|
989
1115
|
if (state) state.queue.push(...messages2);
|
|
@@ -1021,16 +1147,14 @@ ${summary}`
|
|
|
1021
1147
|
signal: controller.signal
|
|
1022
1148
|
});
|
|
1023
1149
|
if (!res.ok) {
|
|
1024
|
-
|
|
1150
|
+
tlog.warn(`replay for ${mindName} got HTTP ${res.status}`);
|
|
1025
1151
|
} else {
|
|
1026
|
-
|
|
1027
|
-
`[token-budget] replayed ${messages2.length} queued message(s) for ${mindName}`
|
|
1028
|
-
);
|
|
1152
|
+
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
1029
1153
|
}
|
|
1030
1154
|
await res.text().catch(() => {
|
|
1031
1155
|
});
|
|
1032
1156
|
} catch (err) {
|
|
1033
|
-
|
|
1157
|
+
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
1034
1158
|
const state = this.budgets.get(mindName);
|
|
1035
1159
|
if (state) state.queue.push(...messages2);
|
|
1036
1160
|
} finally {
|
|
@@ -1053,124 +1177,12 @@ import { createMiddleware } from "hono/factory";
|
|
|
1053
1177
|
// src/lib/auth.ts
|
|
1054
1178
|
import { compareSync, hashSync } from "bcryptjs";
|
|
1055
1179
|
import { and, count, eq } from "drizzle-orm";
|
|
1056
|
-
|
|
1057
|
-
// src/lib/db.ts
|
|
1058
|
-
import { chmodSync, existsSync as existsSync5 } from "fs";
|
|
1059
|
-
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
1060
|
-
import { fileURLToPath } from "url";
|
|
1061
|
-
import { drizzle } from "drizzle-orm/libsql";
|
|
1062
|
-
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
1063
|
-
|
|
1064
|
-
// src/lib/schema.ts
|
|
1065
|
-
var schema_exports = {};
|
|
1066
|
-
__export(schema_exports, {
|
|
1067
|
-
conversationParticipants: () => conversationParticipants,
|
|
1068
|
-
conversations: () => conversations,
|
|
1069
|
-
messages: () => messages,
|
|
1070
|
-
mindMessages: () => mindMessages,
|
|
1071
|
-
sessions: () => sessions,
|
|
1072
|
-
users: () => users
|
|
1073
|
-
});
|
|
1074
|
-
import { sql } from "drizzle-orm";
|
|
1075
|
-
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
1076
|
-
var users = sqliteTable("users", {
|
|
1077
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
1078
|
-
username: text("username").unique().notNull(),
|
|
1079
|
-
password_hash: text("password_hash").notNull(),
|
|
1080
|
-
role: text("role").notNull().default("pending"),
|
|
1081
|
-
user_type: text("user_type").notNull().default("human"),
|
|
1082
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
1083
|
-
});
|
|
1084
|
-
var conversations = sqliteTable(
|
|
1085
|
-
"conversations",
|
|
1086
|
-
{
|
|
1087
|
-
id: text("id").primaryKey(),
|
|
1088
|
-
mind_name: text("mind_name").notNull(),
|
|
1089
|
-
channel: text("channel").notNull(),
|
|
1090
|
-
user_id: integer("user_id").references(() => users.id),
|
|
1091
|
-
title: text("title"),
|
|
1092
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
1093
|
-
updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
1094
|
-
},
|
|
1095
|
-
(table) => [
|
|
1096
|
-
index("idx_conversations_mind_name").on(table.mind_name),
|
|
1097
|
-
index("idx_conversations_user_id").on(table.user_id),
|
|
1098
|
-
index("idx_conversations_updated_at").on(table.updated_at)
|
|
1099
|
-
]
|
|
1100
|
-
);
|
|
1101
|
-
var mindMessages = sqliteTable(
|
|
1102
|
-
"mind_messages",
|
|
1103
|
-
{
|
|
1104
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
1105
|
-
mind: text("mind").notNull(),
|
|
1106
|
-
channel: text("channel").notNull(),
|
|
1107
|
-
sender: text("sender"),
|
|
1108
|
-
content: text("content").notNull(),
|
|
1109
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
1110
|
-
},
|
|
1111
|
-
(table) => [
|
|
1112
|
-
index("idx_mind_messages_mind").on(table.mind),
|
|
1113
|
-
index("idx_mind_messages_channel").on(table.mind, table.channel)
|
|
1114
|
-
]
|
|
1115
|
-
);
|
|
1116
|
-
var conversationParticipants = sqliteTable(
|
|
1117
|
-
"conversation_participants",
|
|
1118
|
-
{
|
|
1119
|
-
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
1120
|
-
user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
1121
|
-
role: text("role").notNull().default("member"),
|
|
1122
|
-
joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
|
|
1123
|
-
},
|
|
1124
|
-
(table) => [
|
|
1125
|
-
uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
|
|
1126
|
-
index("idx_cp_user_id").on(table.user_id)
|
|
1127
|
-
]
|
|
1128
|
-
);
|
|
1129
|
-
var sessions = sqliteTable("sessions", {
|
|
1130
|
-
id: text("id").primaryKey(),
|
|
1131
|
-
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
|
|
1132
|
-
createdAt: integer("created_at").notNull()
|
|
1133
|
-
});
|
|
1134
|
-
var messages = sqliteTable(
|
|
1135
|
-
"messages",
|
|
1136
|
-
{
|
|
1137
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
1138
|
-
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
1139
|
-
role: text("role").notNull(),
|
|
1140
|
-
sender_name: text("sender_name"),
|
|
1141
|
-
content: text("content").notNull(),
|
|
1142
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
1143
|
-
},
|
|
1144
|
-
(table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
|
|
1145
|
-
);
|
|
1146
|
-
|
|
1147
|
-
// src/lib/db.ts
|
|
1148
|
-
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1149
|
-
var migrationsFolder = existsSync5(resolve6(__dirname, "../drizzle")) ? resolve6(__dirname, "../drizzle") : resolve6(__dirname, "../../drizzle");
|
|
1150
|
-
var db = null;
|
|
1151
|
-
async function getDb() {
|
|
1152
|
-
if (db) return db;
|
|
1153
|
-
const dbPath = process.env.VOLUTE_DB_PATH || resolve6(voluteHome(), "volute.db");
|
|
1154
|
-
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
1155
|
-
await migrate(db, { migrationsFolder });
|
|
1156
|
-
try {
|
|
1157
|
-
chmodSync(dbPath, 384);
|
|
1158
|
-
} catch (err) {
|
|
1159
|
-
console.error(
|
|
1160
|
-
`[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
|
|
1161
|
-
err
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
return db;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// src/lib/auth.ts
|
|
1168
1180
|
async function createUser(username, password) {
|
|
1169
|
-
const
|
|
1181
|
+
const db = await getDb();
|
|
1170
1182
|
const hash = hashSync(password, 10);
|
|
1171
|
-
const [{ value }] = await
|
|
1183
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
|
|
1172
1184
|
const role = value === 0 ? "admin" : "pending";
|
|
1173
|
-
const [result] = await
|
|
1185
|
+
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
1174
1186
|
id: users.id,
|
|
1175
1187
|
username: users.username,
|
|
1176
1188
|
role: users.role,
|
|
@@ -1180,8 +1192,8 @@ async function createUser(username, password) {
|
|
|
1180
1192
|
return result;
|
|
1181
1193
|
}
|
|
1182
1194
|
async function verifyUser(username, password) {
|
|
1183
|
-
const
|
|
1184
|
-
const row = await
|
|
1195
|
+
const db = await getDb();
|
|
1196
|
+
const row = await db.select().from(users).where(eq(users.username, username)).get();
|
|
1185
1197
|
if (!row) return null;
|
|
1186
1198
|
if (row.user_type === "mind") return null;
|
|
1187
1199
|
if (!compareSync(password, row.password_hash)) return null;
|
|
@@ -1189,8 +1201,8 @@ async function verifyUser(username, password) {
|
|
|
1189
1201
|
return user;
|
|
1190
1202
|
}
|
|
1191
1203
|
async function getUser(id) {
|
|
1192
|
-
const
|
|
1193
|
-
const row = await
|
|
1204
|
+
const db = await getDb();
|
|
1205
|
+
const row = await db.select({
|
|
1194
1206
|
id: users.id,
|
|
1195
1207
|
username: users.username,
|
|
1196
1208
|
role: users.role,
|
|
@@ -1200,8 +1212,8 @@ async function getUser(id) {
|
|
|
1200
1212
|
return row ?? null;
|
|
1201
1213
|
}
|
|
1202
1214
|
async function getUserByUsername(username) {
|
|
1203
|
-
const
|
|
1204
|
-
const row = await
|
|
1215
|
+
const db = await getDb();
|
|
1216
|
+
const row = await db.select({
|
|
1205
1217
|
id: users.id,
|
|
1206
1218
|
username: users.username,
|
|
1207
1219
|
role: users.role,
|
|
@@ -1211,8 +1223,8 @@ async function getUserByUsername(username) {
|
|
|
1211
1223
|
return row ?? null;
|
|
1212
1224
|
}
|
|
1213
1225
|
async function listUsers() {
|
|
1214
|
-
const
|
|
1215
|
-
return
|
|
1226
|
+
const db = await getDb();
|
|
1227
|
+
return db.select({
|
|
1216
1228
|
id: users.id,
|
|
1217
1229
|
username: users.username,
|
|
1218
1230
|
role: users.role,
|
|
@@ -1221,8 +1233,8 @@ async function listUsers() {
|
|
|
1221
1233
|
}).from(users).orderBy(users.created_at).all();
|
|
1222
1234
|
}
|
|
1223
1235
|
async function listPendingUsers() {
|
|
1224
|
-
const
|
|
1225
|
-
return
|
|
1236
|
+
const db = await getDb();
|
|
1237
|
+
return db.select({
|
|
1226
1238
|
id: users.id,
|
|
1227
1239
|
username: users.username,
|
|
1228
1240
|
role: users.role,
|
|
@@ -1231,8 +1243,8 @@ async function listPendingUsers() {
|
|
|
1231
1243
|
}).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
1232
1244
|
}
|
|
1233
1245
|
async function listUsersByType(userType) {
|
|
1234
|
-
const
|
|
1235
|
-
return
|
|
1246
|
+
const db = await getDb();
|
|
1247
|
+
return db.select({
|
|
1236
1248
|
id: users.id,
|
|
1237
1249
|
username: users.username,
|
|
1238
1250
|
role: users.role,
|
|
@@ -1241,8 +1253,8 @@ async function listUsersByType(userType) {
|
|
|
1241
1253
|
}).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
|
|
1242
1254
|
}
|
|
1243
1255
|
async function getOrCreateMindUser(mindName) {
|
|
1244
|
-
const
|
|
1245
|
-
const existing = await
|
|
1256
|
+
const db = await getDb();
|
|
1257
|
+
const existing = await db.select({
|
|
1246
1258
|
id: users.id,
|
|
1247
1259
|
username: users.username,
|
|
1248
1260
|
role: users.role,
|
|
@@ -1251,7 +1263,7 @@ async function getOrCreateMindUser(mindName) {
|
|
|
1251
1263
|
}).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
1252
1264
|
if (existing) return existing;
|
|
1253
1265
|
try {
|
|
1254
|
-
const [result] = await
|
|
1266
|
+
const [result] = await db.insert(users).values({
|
|
1255
1267
|
username: mindName,
|
|
1256
1268
|
password_hash: "!mind",
|
|
1257
1269
|
role: "mind",
|
|
@@ -1266,7 +1278,7 @@ async function getOrCreateMindUser(mindName) {
|
|
|
1266
1278
|
return result;
|
|
1267
1279
|
} catch (err) {
|
|
1268
1280
|
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
1269
|
-
const retried = await
|
|
1281
|
+
const retried = await db.select({
|
|
1270
1282
|
id: users.id,
|
|
1271
1283
|
username: users.username,
|
|
1272
1284
|
role: users.role,
|
|
@@ -1279,12 +1291,21 @@ async function getOrCreateMindUser(mindName) {
|
|
|
1279
1291
|
}
|
|
1280
1292
|
}
|
|
1281
1293
|
async function deleteMindUser2(mindName) {
|
|
1282
|
-
const
|
|
1283
|
-
await
|
|
1294
|
+
const db = await getDb();
|
|
1295
|
+
await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
|
|
1296
|
+
}
|
|
1297
|
+
async function changePassword(userId, currentPassword, newPassword) {
|
|
1298
|
+
const db = await getDb();
|
|
1299
|
+
const row = await db.select().from(users).where(eq(users.id, userId)).get();
|
|
1300
|
+
if (!row) return false;
|
|
1301
|
+
if (!compareSync(currentPassword, row.password_hash)) return false;
|
|
1302
|
+
const hash = hashSync(newPassword, 10);
|
|
1303
|
+
await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
|
|
1304
|
+
return true;
|
|
1284
1305
|
}
|
|
1285
1306
|
async function approveUser(id) {
|
|
1286
|
-
const
|
|
1287
|
-
await
|
|
1307
|
+
const db = await getDb();
|
|
1308
|
+
await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
1288
1309
|
}
|
|
1289
1310
|
|
|
1290
1311
|
// src/web/middleware/auth.ts
|
|
@@ -1295,29 +1316,29 @@ function isValidDaemonToken(token) {
|
|
|
1295
1316
|
}
|
|
1296
1317
|
var SESSION_MAX_AGE = 864e5;
|
|
1297
1318
|
async function createSession(userId) {
|
|
1298
|
-
const
|
|
1319
|
+
const db = await getDb();
|
|
1299
1320
|
const sessionId = crypto.randomUUID();
|
|
1300
|
-
await
|
|
1321
|
+
await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
|
|
1301
1322
|
return sessionId;
|
|
1302
1323
|
}
|
|
1303
1324
|
async function deleteSession(sessionId) {
|
|
1304
|
-
const
|
|
1305
|
-
await
|
|
1325
|
+
const db = await getDb();
|
|
1326
|
+
await db.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
1306
1327
|
}
|
|
1307
1328
|
async function getSessionUserId(sessionId) {
|
|
1308
|
-
const
|
|
1309
|
-
const row = await
|
|
1329
|
+
const db = await getDb();
|
|
1330
|
+
const row = await db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
|
|
1310
1331
|
if (!row) return void 0;
|
|
1311
1332
|
if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
|
|
1312
|
-
await
|
|
1333
|
+
await db.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
1313
1334
|
return void 0;
|
|
1314
1335
|
}
|
|
1315
1336
|
return row.userId;
|
|
1316
1337
|
}
|
|
1317
1338
|
async function cleanExpiredSessions() {
|
|
1318
|
-
const
|
|
1339
|
+
const db = await getDb();
|
|
1319
1340
|
const cutoff = Date.now() - SESSION_MAX_AGE;
|
|
1320
|
-
await
|
|
1341
|
+
await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
|
|
1321
1342
|
}
|
|
1322
1343
|
var requireAdmin = createMiddleware(async (c, next) => {
|
|
1323
1344
|
const user = c.get("user");
|
|
@@ -1348,62 +1369,18 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
1348
1369
|
});
|
|
1349
1370
|
|
|
1350
1371
|
// src/web/server.ts
|
|
1351
|
-
import { existsSync as
|
|
1372
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1352
1373
|
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
1353
|
-
import { dirname as
|
|
1374
|
+
import { dirname as dirname2, extname as extname2, resolve as resolve17 } from "path";
|
|
1354
1375
|
import { serve } from "@hono/node-server";
|
|
1355
1376
|
|
|
1356
|
-
// src/lib/log-buffer.ts
|
|
1357
|
-
var LogBuffer = class {
|
|
1358
|
-
entries = [];
|
|
1359
|
-
maxSize = 1e3;
|
|
1360
|
-
subscribers = /* @__PURE__ */ new Set();
|
|
1361
|
-
append(entry) {
|
|
1362
|
-
this.entries.push(entry);
|
|
1363
|
-
if (this.entries.length > this.maxSize) {
|
|
1364
|
-
this.entries.shift();
|
|
1365
|
-
}
|
|
1366
|
-
for (const sub of this.subscribers) {
|
|
1367
|
-
sub(entry);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
getEntries() {
|
|
1371
|
-
return [...this.entries];
|
|
1372
|
-
}
|
|
1373
|
-
subscribe(fn) {
|
|
1374
|
-
this.subscribers.add(fn);
|
|
1375
|
-
return () => this.subscribers.delete(fn);
|
|
1376
|
-
}
|
|
1377
|
-
};
|
|
1378
|
-
var logBuffer = new LogBuffer();
|
|
1379
|
-
|
|
1380
|
-
// src/lib/logger.ts
|
|
1381
|
-
function write(level, msg, data) {
|
|
1382
|
-
const entry = {
|
|
1383
|
-
level,
|
|
1384
|
-
msg,
|
|
1385
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1386
|
-
...data ? { data } : {}
|
|
1387
|
-
};
|
|
1388
|
-
const line = JSON.stringify(entry);
|
|
1389
|
-
process.stderr.write(`${line}
|
|
1390
|
-
`);
|
|
1391
|
-
logBuffer.append(entry);
|
|
1392
|
-
}
|
|
1393
|
-
var log2 = {
|
|
1394
|
-
info: (msg, data) => write("info", msg, data),
|
|
1395
|
-
warn: (msg, data) => write("warn", msg, data),
|
|
1396
|
-
error: (msg, data) => write("error", msg, data)
|
|
1397
|
-
};
|
|
1398
|
-
var logger_default = log2;
|
|
1399
|
-
|
|
1400
1377
|
// src/web/app.ts
|
|
1401
|
-
import { Hono as
|
|
1378
|
+
import { Hono as Hono21 } from "hono";
|
|
1402
1379
|
import { bodyLimit } from "hono/body-limit";
|
|
1403
1380
|
import { csrf } from "hono/csrf";
|
|
1404
1381
|
import { HTTPException } from "hono/http-exception";
|
|
1405
1382
|
|
|
1406
|
-
// src/web/
|
|
1383
|
+
// src/web/api/auth.ts
|
|
1407
1384
|
import { zValidator } from "@hono/zod-validator";
|
|
1408
1385
|
import { Hono } from "hono";
|
|
1409
1386
|
import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
|
|
@@ -1412,6 +1389,17 @@ var credentialsSchema = z.object({
|
|
|
1412
1389
|
username: z.string().min(1),
|
|
1413
1390
|
password: z.string().min(1)
|
|
1414
1391
|
});
|
|
1392
|
+
var changePasswordSchema = z.object({
|
|
1393
|
+
currentPassword: z.string().min(1),
|
|
1394
|
+
newPassword: z.string().min(1)
|
|
1395
|
+
});
|
|
1396
|
+
var authenticated = new Hono().use(authMiddleware).post("/change-password", zValidator("json", changePasswordSchema), async (c) => {
|
|
1397
|
+
const user = c.get("user");
|
|
1398
|
+
const { currentPassword, newPassword } = c.req.valid("json");
|
|
1399
|
+
const ok = await changePassword(user.id, currentPassword, newPassword);
|
|
1400
|
+
if (!ok) return c.json({ error: "Current password is incorrect" }, 400);
|
|
1401
|
+
return c.json({ ok: true });
|
|
1402
|
+
});
|
|
1415
1403
|
var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
|
|
1416
1404
|
const user = c.get("user");
|
|
1417
1405
|
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
@@ -1471,10 +1459,10 @@ var app = new Hono().post("/register", zValidator("json", credentialsSchema), as
|
|
|
1471
1459
|
const user = await getUser(userId);
|
|
1472
1460
|
if (!user) return c.json({ error: "Not logged in" }, 401);
|
|
1473
1461
|
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
1474
|
-
}).route("/", admin);
|
|
1462
|
+
}).route("/", admin).route("/", authenticated);
|
|
1475
1463
|
var auth_default = app;
|
|
1476
1464
|
|
|
1477
|
-
// src/web/
|
|
1465
|
+
// src/web/api/channels.ts
|
|
1478
1466
|
import { Hono as Hono2 } from "hono";
|
|
1479
1467
|
function buildEnv(name) {
|
|
1480
1468
|
return { ...loadMergedEnv(name), VOLUTE_MIND: name, VOLUTE_MIND_DIR: mindDir(name) };
|
|
@@ -1482,12 +1470,12 @@ function buildEnv(name) {
|
|
|
1482
1470
|
var app2 = new Hono2().post("/:name/channels/send", requireAdmin, async (c) => {
|
|
1483
1471
|
const name = c.req.param("name");
|
|
1484
1472
|
if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
|
|
1485
|
-
const { platform, uri, message } = await c.req.json();
|
|
1473
|
+
const { platform, uri, message, images } = await c.req.json();
|
|
1486
1474
|
const driver = getChannelDriver(platform);
|
|
1487
1475
|
if (!driver) return c.json({ error: `No driver for platform: ${platform}` }, 400);
|
|
1488
1476
|
const env = buildEnv(name);
|
|
1489
1477
|
try {
|
|
1490
|
-
await driver.send(env, uri, message);
|
|
1478
|
+
await driver.send(env, uri, message, images);
|
|
1491
1479
|
return c.json({ ok: true });
|
|
1492
1480
|
} catch (err) {
|
|
1493
1481
|
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
@@ -1571,7 +1559,7 @@ var app2 = new Hono2().post("/:name/channels/send", requireAdmin, async (c) => {
|
|
|
1571
1559
|
});
|
|
1572
1560
|
var channels_default = app2;
|
|
1573
1561
|
|
|
1574
|
-
// src/web/
|
|
1562
|
+
// src/web/api/connectors.ts
|
|
1575
1563
|
import { Hono as Hono3 } from "hono";
|
|
1576
1564
|
var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
|
|
1577
1565
|
var app3 = new Hono3().get("/:name/connectors", (c) => {
|
|
@@ -1650,7 +1638,7 @@ var app3 = new Hono3().get("/:name/connectors", (c) => {
|
|
|
1650
1638
|
});
|
|
1651
1639
|
var connectors_default = app3;
|
|
1652
1640
|
|
|
1653
|
-
// src/web/
|
|
1641
|
+
// src/web/api/env.ts
|
|
1654
1642
|
import { Hono as Hono4 } from "hono";
|
|
1655
1643
|
var app4 = new Hono4().get("/:name/env", (c) => {
|
|
1656
1644
|
const name = c.req.param("name");
|
|
@@ -1724,10 +1712,10 @@ var sharedEnvApp = new Hono4().get("/", (c) => {
|
|
|
1724
1712
|
});
|
|
1725
1713
|
var env_default = app4;
|
|
1726
1714
|
|
|
1727
|
-
// src/web/
|
|
1728
|
-
import { existsSync as
|
|
1715
|
+
// src/web/api/files.ts
|
|
1716
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1729
1717
|
import { readdir, readFile } from "fs/promises";
|
|
1730
|
-
import { resolve as
|
|
1718
|
+
import { resolve as resolve6 } from "path";
|
|
1731
1719
|
import { Hono as Hono5 } from "hono";
|
|
1732
1720
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
1733
1721
|
var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
@@ -1735,8 +1723,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
|
1735
1723
|
const entry = findMind(name);
|
|
1736
1724
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1737
1725
|
const dir = mindDir(name);
|
|
1738
|
-
const homeDir =
|
|
1739
|
-
if (!
|
|
1726
|
+
const homeDir = resolve6(dir, "home");
|
|
1727
|
+
if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1740
1728
|
const allFiles = await readdir(homeDir);
|
|
1741
1729
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1742
1730
|
return c.json(files);
|
|
@@ -1749,8 +1737,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
|
1749
1737
|
const entry = findMind(name);
|
|
1750
1738
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1751
1739
|
const dir = mindDir(name);
|
|
1752
|
-
const filePath =
|
|
1753
|
-
if (!
|
|
1740
|
+
const filePath = resolve6(dir, "home", filename);
|
|
1741
|
+
if (!existsSync5(filePath)) {
|
|
1754
1742
|
return c.json({ error: "File not found" }, 404);
|
|
1755
1743
|
}
|
|
1756
1744
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -1758,18 +1746,18 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
|
1758
1746
|
});
|
|
1759
1747
|
var files_default = app5;
|
|
1760
1748
|
|
|
1761
|
-
// src/web/
|
|
1749
|
+
// src/web/api/logs.ts
|
|
1762
1750
|
import { spawn as spawn2 } from "child_process";
|
|
1763
|
-
import { existsSync as
|
|
1764
|
-
import { resolve as
|
|
1751
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1752
|
+
import { resolve as resolve7 } from "path";
|
|
1765
1753
|
import { Hono as Hono6 } from "hono";
|
|
1766
1754
|
import { streamSSE } from "hono/streaming";
|
|
1767
1755
|
var app6 = new Hono6().get("/:name/logs", async (c) => {
|
|
1768
1756
|
const name = c.req.param("name");
|
|
1769
1757
|
const entry = findMind(name);
|
|
1770
1758
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1771
|
-
const logFile =
|
|
1772
|
-
if (!
|
|
1759
|
+
const logFile = resolve7(stateDir(name), "logs", "mind.log");
|
|
1760
|
+
if (!existsSync6(logFile)) {
|
|
1773
1761
|
return c.json({ error: "No log file found" }, 404);
|
|
1774
1762
|
}
|
|
1775
1763
|
return streamSSE(c, async (stream) => {
|
|
@@ -1787,17 +1775,17 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
|
|
|
1787
1775
|
stream.onAbort(() => {
|
|
1788
1776
|
tail.kill();
|
|
1789
1777
|
});
|
|
1790
|
-
await new Promise((
|
|
1791
|
-
tail.on("exit",
|
|
1792
|
-
stream.onAbort(
|
|
1778
|
+
await new Promise((resolve19) => {
|
|
1779
|
+
tail.on("exit", resolve19);
|
|
1780
|
+
stream.onAbort(resolve19);
|
|
1793
1781
|
});
|
|
1794
1782
|
});
|
|
1795
1783
|
}).get("/:name/logs/tail", async (c) => {
|
|
1796
1784
|
const name = c.req.param("name");
|
|
1797
1785
|
const entry = findMind(name);
|
|
1798
1786
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1799
|
-
const logFile =
|
|
1800
|
-
if (!
|
|
1787
|
+
const logFile = resolve7(stateDir(name), "logs", "mind.log");
|
|
1788
|
+
if (!existsSync6(logFile)) {
|
|
1801
1789
|
return c.json({ error: "No log file found" }, 404);
|
|
1802
1790
|
}
|
|
1803
1791
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
@@ -1807,44 +1795,421 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
|
|
|
1807
1795
|
tail.stdout.on("data", (data) => {
|
|
1808
1796
|
output += data.toString();
|
|
1809
1797
|
});
|
|
1810
|
-
await new Promise((
|
|
1811
|
-
tail.on("exit",
|
|
1798
|
+
await new Promise((resolve19) => {
|
|
1799
|
+
tail.on("exit", resolve19);
|
|
1812
1800
|
});
|
|
1813
1801
|
return c.text(output);
|
|
1814
1802
|
});
|
|
1815
1803
|
var logs_default = app6;
|
|
1816
1804
|
|
|
1817
|
-
// src/web/
|
|
1805
|
+
// src/web/api/mind-skills.ts
|
|
1806
|
+
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
1807
|
+
import { Hono as Hono7 } from "hono";
|
|
1808
|
+
import { z as z2 } from "zod";
|
|
1809
|
+
|
|
1810
|
+
// src/lib/skills.ts
|
|
1818
1811
|
import {
|
|
1819
1812
|
cpSync,
|
|
1820
|
-
existsSync as
|
|
1821
|
-
mkdirSync as
|
|
1822
|
-
readdirSync as
|
|
1823
|
-
readFileSync as
|
|
1813
|
+
existsSync as existsSync7,
|
|
1814
|
+
mkdirSync as mkdirSync3,
|
|
1815
|
+
readdirSync as readdirSync2,
|
|
1816
|
+
readFileSync as readFileSync4,
|
|
1824
1817
|
rmSync,
|
|
1818
|
+
writeFileSync as writeFileSync3
|
|
1819
|
+
} from "fs";
|
|
1820
|
+
import { tmpdir } from "os";
|
|
1821
|
+
import { basename, join, resolve as resolve8 } from "path";
|
|
1822
|
+
import { eq as eq3, sql } from "drizzle-orm";
|
|
1823
|
+
var VALID_SKILL_ID = /^[a-zA-Z0-9_-]+$/;
|
|
1824
|
+
function validateSkillId(id) {
|
|
1825
|
+
if (!id || !VALID_SKILL_ID.test(id)) {
|
|
1826
|
+
throw new Error(`Invalid skill ID: ${id}`);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function sharedSkillsDir() {
|
|
1830
|
+
return resolve8(voluteHome(), "skills");
|
|
1831
|
+
}
|
|
1832
|
+
function parseSkillMd(content) {
|
|
1833
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1834
|
+
if (!match) return { name: "", description: "" };
|
|
1835
|
+
const frontmatter = match[1];
|
|
1836
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
1837
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
1838
|
+
return {
|
|
1839
|
+
name: nameMatch?.[1].trim() ?? "",
|
|
1840
|
+
description: descMatch?.[1].trim() ?? ""
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
async function listSharedSkills() {
|
|
1844
|
+
const db = await getDb();
|
|
1845
|
+
return db.select().from(sharedSkills).all();
|
|
1846
|
+
}
|
|
1847
|
+
async function getSharedSkill(id) {
|
|
1848
|
+
const db = await getDb();
|
|
1849
|
+
return db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1850
|
+
}
|
|
1851
|
+
async function importSkillFromDir(sourceDir, author) {
|
|
1852
|
+
const skillMdPath = join(sourceDir, "SKILL.md");
|
|
1853
|
+
if (!existsSync7(skillMdPath)) {
|
|
1854
|
+
throw new Error("SKILL.md not found in source directory");
|
|
1855
|
+
}
|
|
1856
|
+
const content = readFileSync4(skillMdPath, "utf-8");
|
|
1857
|
+
const { name, description } = parseSkillMd(content);
|
|
1858
|
+
const id = basename(sourceDir);
|
|
1859
|
+
if (!id || id === "." || id === "..") {
|
|
1860
|
+
throw new Error("Invalid skill directory name");
|
|
1861
|
+
}
|
|
1862
|
+
validateSkillId(id);
|
|
1863
|
+
const destDir = join(sharedSkillsDir(), id);
|
|
1864
|
+
if (existsSync7(destDir)) rmSync(destDir, { recursive: true });
|
|
1865
|
+
mkdirSync3(destDir, { recursive: true });
|
|
1866
|
+
cpSync(sourceDir, destDir, { recursive: true });
|
|
1867
|
+
const upstreamPath = join(destDir, ".upstream.json");
|
|
1868
|
+
if (existsSync7(upstreamPath)) rmSync(upstreamPath);
|
|
1869
|
+
const db = await getDb();
|
|
1870
|
+
const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1871
|
+
const version = existing ? existing.version + 1 : 1;
|
|
1872
|
+
await db.insert(sharedSkills).values({ id, name: name || id, description, author, version }).onConflictDoUpdate({
|
|
1873
|
+
target: sharedSkills.id,
|
|
1874
|
+
set: {
|
|
1875
|
+
name: name || id,
|
|
1876
|
+
description,
|
|
1877
|
+
author,
|
|
1878
|
+
version,
|
|
1879
|
+
updated_at: sql`(datetime('now'))`
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
const row = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1883
|
+
if (!row) throw new Error(`Failed to upsert shared skill: ${id}`);
|
|
1884
|
+
return row;
|
|
1885
|
+
}
|
|
1886
|
+
async function removeSharedSkill(id) {
|
|
1887
|
+
const db = await getDb();
|
|
1888
|
+
const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1889
|
+
if (!existing) throw new Error(`Shared skill not found: ${id}`);
|
|
1890
|
+
await db.delete(sharedSkills).where(eq3(sharedSkills.id, id));
|
|
1891
|
+
const dir = join(sharedSkillsDir(), id);
|
|
1892
|
+
if (existsSync7(dir)) rmSync(dir, { recursive: true });
|
|
1893
|
+
}
|
|
1894
|
+
function mindSkillsDir(dir) {
|
|
1895
|
+
return resolve8(dir, "home", ".claude", "skills");
|
|
1896
|
+
}
|
|
1897
|
+
function readUpstream(skillDir) {
|
|
1898
|
+
const upstreamPath = join(skillDir, ".upstream.json");
|
|
1899
|
+
if (!existsSync7(upstreamPath)) return null;
|
|
1900
|
+
try {
|
|
1901
|
+
const data = JSON.parse(readFileSync4(upstreamPath, "utf-8"));
|
|
1902
|
+
if (typeof data?.source !== "string" || typeof data?.version !== "number" || typeof data?.baseCommit !== "string") {
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
return data;
|
|
1906
|
+
} catch {
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
async function installSkill(_mindName, dir, skillId) {
|
|
1911
|
+
validateSkillId(skillId);
|
|
1912
|
+
const shared = await getSharedSkill(skillId);
|
|
1913
|
+
if (!shared) throw new Error(`Shared skill not found: ${skillId}`);
|
|
1914
|
+
const sourceDir = join(sharedSkillsDir(), skillId);
|
|
1915
|
+
if (!existsSync7(sourceDir)) throw new Error(`Shared skill files not found: ${skillId}`);
|
|
1916
|
+
const destDir = join(mindSkillsDir(dir), skillId);
|
|
1917
|
+
if (existsSync7(destDir)) throw new Error(`Skill already installed: ${skillId}`);
|
|
1918
|
+
mkdirSync3(destDir, { recursive: true });
|
|
1919
|
+
cpSync(sourceDir, destDir, { recursive: true });
|
|
1920
|
+
await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
|
|
1921
|
+
await gitExec(["commit", "-m", `Install shared skill: ${skillId}`], { cwd: dir });
|
|
1922
|
+
const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
|
|
1923
|
+
const upstream = {
|
|
1924
|
+
source: skillId,
|
|
1925
|
+
version: shared.version,
|
|
1926
|
+
baseCommit: commitHash
|
|
1927
|
+
};
|
|
1928
|
+
writeFileSync3(join(destDir, ".upstream.json"), `${JSON.stringify(upstream, null, 2)}
|
|
1929
|
+
`);
|
|
1930
|
+
await gitExec(["add", join("home", ".claude", "skills", skillId, ".upstream.json")], {
|
|
1931
|
+
cwd: dir
|
|
1932
|
+
});
|
|
1933
|
+
await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
|
|
1934
|
+
}
|
|
1935
|
+
async function uninstallSkill(_mindName, dir, skillId) {
|
|
1936
|
+
validateSkillId(skillId);
|
|
1937
|
+
const skillDir = join(mindSkillsDir(dir), skillId);
|
|
1938
|
+
if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
|
|
1939
|
+
rmSync(skillDir, { recursive: true });
|
|
1940
|
+
await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
|
|
1941
|
+
await gitExec(["commit", "-m", `Uninstall skill: ${skillId}`], { cwd: dir });
|
|
1942
|
+
}
|
|
1943
|
+
async function updateSkill(_mindName, dir, skillId) {
|
|
1944
|
+
validateSkillId(skillId);
|
|
1945
|
+
const skillDir = join(mindSkillsDir(dir), skillId);
|
|
1946
|
+
if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
|
|
1947
|
+
const upstream = readUpstream(skillDir);
|
|
1948
|
+
if (!upstream) throw new Error(`No upstream tracking for skill: ${skillId}`);
|
|
1949
|
+
const shared = await getSharedSkill(upstream.source);
|
|
1950
|
+
if (!shared) throw new Error(`Shared skill no longer exists: ${upstream.source}`);
|
|
1951
|
+
if (shared.version <= upstream.version) {
|
|
1952
|
+
return { status: "up-to-date" };
|
|
1953
|
+
}
|
|
1954
|
+
const sourceDir = join(sharedSkillsDir(), upstream.source);
|
|
1955
|
+
if (!existsSync7(sourceDir)) throw new Error(`Shared skill files missing: ${upstream.source}`);
|
|
1956
|
+
const relSkillPath = join("home", ".claude", "skills", skillId);
|
|
1957
|
+
const currentFiles = listFilesRecursive(skillDir).filter((f) => f !== ".upstream.json");
|
|
1958
|
+
const newFiles = listFilesRecursive(sourceDir).filter((f) => f !== ".upstream.json");
|
|
1959
|
+
const allFiles = [.../* @__PURE__ */ new Set([...currentFiles, ...newFiles])];
|
|
1960
|
+
const conflictFiles = [];
|
|
1961
|
+
const tmpBase = join(tmpdir(), `volute-merge-${process.pid}-${Date.now()}`);
|
|
1962
|
+
mkdirSync3(tmpBase, { recursive: true });
|
|
1963
|
+
try {
|
|
1964
|
+
for (const file of allFiles) {
|
|
1965
|
+
const currentPath = join(skillDir, file);
|
|
1966
|
+
const newPath = join(sourceDir, file);
|
|
1967
|
+
const currentExists = existsSync7(currentPath);
|
|
1968
|
+
const newExists = existsSync7(newPath);
|
|
1969
|
+
if (!currentExists && newExists) {
|
|
1970
|
+
const destPath = join(skillDir, file);
|
|
1971
|
+
mkdirSync3(join(skillDir, ...file.split("/").slice(0, -1)), { recursive: true });
|
|
1972
|
+
cpSync(newPath, destPath);
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
if (currentExists && !newExists) {
|
|
1976
|
+
let baseContent2 = null;
|
|
1977
|
+
try {
|
|
1978
|
+
baseContent2 = await gitExec(
|
|
1979
|
+
["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
|
|
1980
|
+
{ cwd: dir }
|
|
1981
|
+
);
|
|
1982
|
+
} catch {
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
const currentContent2 = readFileSync4(currentPath, "utf-8");
|
|
1986
|
+
if (currentContent2 === baseContent2) {
|
|
1987
|
+
rmSync(currentPath);
|
|
1988
|
+
}
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
let baseContent;
|
|
1992
|
+
try {
|
|
1993
|
+
baseContent = await gitExec(
|
|
1994
|
+
["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
|
|
1995
|
+
{ cwd: dir }
|
|
1996
|
+
);
|
|
1997
|
+
} catch {
|
|
1998
|
+
baseContent = "";
|
|
1999
|
+
}
|
|
2000
|
+
const currentContent = readFileSync4(currentPath, "utf-8");
|
|
2001
|
+
const newContent = readFileSync4(newPath, "utf-8");
|
|
2002
|
+
if (currentContent === baseContent) {
|
|
2003
|
+
writeFileSync3(currentPath, newContent);
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
if (newContent === baseContent) {
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
const baseTmp = join(tmpBase, `${file}.base`);
|
|
2010
|
+
const currentTmp = join(tmpBase, `${file}.current`);
|
|
2011
|
+
const newTmp = join(tmpBase, `${file}.new`);
|
|
2012
|
+
mkdirSync3(join(tmpBase, ...file.split("/").slice(0, -1)), { recursive: true });
|
|
2013
|
+
writeFileSync3(baseTmp, baseContent);
|
|
2014
|
+
writeFileSync3(currentTmp, currentContent);
|
|
2015
|
+
writeFileSync3(newTmp, newContent);
|
|
2016
|
+
try {
|
|
2017
|
+
await exec("git", ["merge-file", currentTmp, baseTmp, newTmp]);
|
|
2018
|
+
writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
|
|
2019
|
+
} catch (e) {
|
|
2020
|
+
const exitCode = e && typeof e === "object" && "code" in e ? e.code : null;
|
|
2021
|
+
if (exitCode === 1) {
|
|
2022
|
+
writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
|
|
2023
|
+
conflictFiles.push(file);
|
|
2024
|
+
} else {
|
|
2025
|
+
throw e;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
} finally {
|
|
2030
|
+
rmSync(tmpBase, { recursive: true, force: true });
|
|
2031
|
+
}
|
|
2032
|
+
if (conflictFiles.length > 0) {
|
|
2033
|
+
return { status: "conflict", conflictFiles };
|
|
2034
|
+
}
|
|
2035
|
+
const upstreamInfo = {
|
|
2036
|
+
source: upstream.source,
|
|
2037
|
+
version: shared.version,
|
|
2038
|
+
baseCommit: upstream.baseCommit
|
|
2039
|
+
// will update after commit
|
|
2040
|
+
};
|
|
2041
|
+
writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
|
|
2042
|
+
`);
|
|
2043
|
+
await gitExec(["add", relSkillPath], { cwd: dir });
|
|
2044
|
+
await gitExec(["commit", "-m", `Update skill: ${skillId} (v${shared.version})`], { cwd: dir });
|
|
2045
|
+
const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
|
|
2046
|
+
upstreamInfo.baseCommit = commitHash;
|
|
2047
|
+
writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
|
|
2048
|
+
`);
|
|
2049
|
+
await gitExec(["add", join(relSkillPath, ".upstream.json")], { cwd: dir });
|
|
2050
|
+
await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
|
|
2051
|
+
return { status: "updated" };
|
|
2052
|
+
}
|
|
2053
|
+
async function listMindSkills(dir) {
|
|
2054
|
+
const skillsDir = mindSkillsDir(dir);
|
|
2055
|
+
if (!existsSync7(skillsDir)) return [];
|
|
2056
|
+
const entries = readdirSync2(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
2057
|
+
const sharedMap = /* @__PURE__ */ new Map();
|
|
2058
|
+
for (const s of await listSharedSkills()) {
|
|
2059
|
+
sharedMap.set(s.id, s);
|
|
2060
|
+
}
|
|
2061
|
+
const results = [];
|
|
2062
|
+
for (const entry of entries) {
|
|
2063
|
+
const skillDir = join(skillsDir, entry.name);
|
|
2064
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
2065
|
+
let name = entry.name;
|
|
2066
|
+
let description = "";
|
|
2067
|
+
if (existsSync7(skillMdPath)) {
|
|
2068
|
+
const parsed = parseSkillMd(readFileSync4(skillMdPath, "utf-8"));
|
|
2069
|
+
if (parsed.name) name = parsed.name;
|
|
2070
|
+
description = parsed.description;
|
|
2071
|
+
}
|
|
2072
|
+
const upstream = readUpstream(skillDir);
|
|
2073
|
+
let updateAvailable = false;
|
|
2074
|
+
if (upstream) {
|
|
2075
|
+
const shared = sharedMap.get(upstream.source);
|
|
2076
|
+
if (shared && shared.version > upstream.version) {
|
|
2077
|
+
updateAvailable = true;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
results.push({ id: entry.name, name, description, upstream, updateAvailable });
|
|
2081
|
+
}
|
|
2082
|
+
return results;
|
|
2083
|
+
}
|
|
2084
|
+
async function publishSkill(mindName, dir, skillId) {
|
|
2085
|
+
const skillDir = join(mindSkillsDir(dir), skillId);
|
|
2086
|
+
if (!existsSync7(skillDir)) throw new Error(`Skill not found: ${skillId}`);
|
|
2087
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
2088
|
+
if (!existsSync7(skillMdPath)) throw new Error(`SKILL.md not found in ${skillId}`);
|
|
2089
|
+
return importSkillFromDir(skillDir, mindName);
|
|
2090
|
+
}
|
|
2091
|
+
function listFilesRecursive(dir, prefix = "") {
|
|
2092
|
+
const results = [];
|
|
2093
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2094
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
2095
|
+
if (entry.isDirectory()) {
|
|
2096
|
+
results.push(...listFilesRecursive(join(dir, entry.name), rel));
|
|
2097
|
+
} else {
|
|
2098
|
+
results.push(rel);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return results;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// src/web/api/mind-skills.ts
|
|
2105
|
+
var app7 = new Hono7().get("/:name/skills", async (c) => {
|
|
2106
|
+
const name = c.req.param("name");
|
|
2107
|
+
const entry = findMind(name);
|
|
2108
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2109
|
+
const dir = mindDir(name);
|
|
2110
|
+
const skills = await listMindSkills(dir);
|
|
2111
|
+
return c.json(skills);
|
|
2112
|
+
}).post(
|
|
2113
|
+
"/:name/skills/install",
|
|
2114
|
+
requireAdmin,
|
|
2115
|
+
zValidator2("json", z2.object({ skillId: z2.string() })),
|
|
2116
|
+
async (c) => {
|
|
2117
|
+
const name = c.req.param("name");
|
|
2118
|
+
const entry = findMind(name);
|
|
2119
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2120
|
+
const { skillId } = c.req.valid("json");
|
|
2121
|
+
const dir = mindDir(name);
|
|
2122
|
+
try {
|
|
2123
|
+
await installSkill(name, dir, skillId);
|
|
2124
|
+
} catch (e) {
|
|
2125
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2126
|
+
return c.json({ error: msg }, 400);
|
|
2127
|
+
}
|
|
2128
|
+
return c.json({ ok: true });
|
|
2129
|
+
}
|
|
2130
|
+
).post(
|
|
2131
|
+
"/:name/skills/update",
|
|
2132
|
+
requireAdmin,
|
|
2133
|
+
zValidator2("json", z2.object({ skillId: z2.string() })),
|
|
2134
|
+
async (c) => {
|
|
2135
|
+
const name = c.req.param("name");
|
|
2136
|
+
const entry = findMind(name);
|
|
2137
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2138
|
+
const { skillId } = c.req.valid("json");
|
|
2139
|
+
const dir = mindDir(name);
|
|
2140
|
+
try {
|
|
2141
|
+
const result = await updateSkill(name, dir, skillId);
|
|
2142
|
+
return c.json(result);
|
|
2143
|
+
} catch (e) {
|
|
2144
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2145
|
+
return c.json({ error: msg }, 400);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
).post(
|
|
2149
|
+
"/:name/skills/publish",
|
|
2150
|
+
requireAdmin,
|
|
2151
|
+
zValidator2("json", z2.object({ skillId: z2.string() })),
|
|
2152
|
+
async (c) => {
|
|
2153
|
+
const name = c.req.param("name");
|
|
2154
|
+
const entry = findMind(name);
|
|
2155
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2156
|
+
const { skillId } = c.req.valid("json");
|
|
2157
|
+
const dir = mindDir(name);
|
|
2158
|
+
try {
|
|
2159
|
+
const skill = await publishSkill(name, dir, skillId);
|
|
2160
|
+
return c.json(skill);
|
|
2161
|
+
} catch (e) {
|
|
2162
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2163
|
+
return c.json({ error: msg }, 400);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
).delete("/:name/skills/:skill", requireAdmin, async (c) => {
|
|
2167
|
+
const name = c.req.param("name");
|
|
2168
|
+
const skillName = c.req.param("skill");
|
|
2169
|
+
const entry = findMind(name);
|
|
2170
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2171
|
+
const dir = mindDir(name);
|
|
2172
|
+
try {
|
|
2173
|
+
await uninstallSkill(name, dir, skillName);
|
|
2174
|
+
} catch (e) {
|
|
2175
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2176
|
+
return c.json({ error: msg }, 400);
|
|
2177
|
+
}
|
|
2178
|
+
return c.json({ ok: true });
|
|
2179
|
+
});
|
|
2180
|
+
var mind_skills_default = app7;
|
|
2181
|
+
|
|
2182
|
+
// src/web/api/minds.ts
|
|
2183
|
+
import {
|
|
2184
|
+
cpSync as cpSync2,
|
|
2185
|
+
existsSync as existsSync8,
|
|
2186
|
+
mkdirSync as mkdirSync5,
|
|
2187
|
+
readdirSync as readdirSync4,
|
|
2188
|
+
readFileSync as readFileSync7,
|
|
2189
|
+
rmSync as rmSync2,
|
|
1825
2190
|
statSync,
|
|
1826
|
-
writeFileSync as
|
|
2191
|
+
writeFileSync as writeFileSync6
|
|
1827
2192
|
} from "fs";
|
|
1828
|
-
import { join, resolve as resolve11 } from "path";
|
|
1829
|
-
import { zValidator as
|
|
1830
|
-
import { and as and3, desc as desc2, eq as
|
|
1831
|
-
import { Hono as
|
|
1832
|
-
import { z as
|
|
2193
|
+
import { join as join2, resolve as resolve11 } from "path";
|
|
2194
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
2195
|
+
import { and as and3, desc as desc2, eq as eq5, sql as sql3 } from "drizzle-orm";
|
|
2196
|
+
import { Hono as Hono8 } from "hono";
|
|
2197
|
+
import { z as z3 } from "zod";
|
|
1833
2198
|
|
|
1834
2199
|
// src/lib/consolidate.ts
|
|
1835
|
-
import { readdirSync as
|
|
2200
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1836
2201
|
import { resolve as resolve9 } from "path";
|
|
1837
2202
|
async function consolidateMemory(mindDir2) {
|
|
1838
2203
|
const soulPath = resolve9(mindDir2, "home/SOUL.md");
|
|
1839
2204
|
const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
|
|
1840
2205
|
const memoryDir = resolve9(mindDir2, "home/memory");
|
|
1841
|
-
const soul =
|
|
2206
|
+
const soul = readFileSync5(soulPath, "utf-8");
|
|
1842
2207
|
const logs = [];
|
|
1843
2208
|
try {
|
|
1844
|
-
const files =
|
|
2209
|
+
const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
1845
2210
|
for (const filename of files) {
|
|
1846
2211
|
const date = filename.replace(".md", "");
|
|
1847
|
-
const content2 =
|
|
2212
|
+
const content2 = readFileSync5(resolve9(memoryDir, filename), "utf-8").trim();
|
|
1848
2213
|
if (content2) {
|
|
1849
2214
|
logs.push(`### ${date}
|
|
1850
2215
|
|
|
@@ -1894,7 +2259,7 @@ ${content2}`);
|
|
|
1894
2259
|
const data = await res.json();
|
|
1895
2260
|
const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
|
|
1896
2261
|
if (content) {
|
|
1897
|
-
|
|
2262
|
+
writeFileSync4(memoryPath, `${content}
|
|
1898
2263
|
`);
|
|
1899
2264
|
console.log("MEMORY.md created successfully.");
|
|
1900
2265
|
} else {
|
|
@@ -1904,7 +2269,7 @@ ${content2}`);
|
|
|
1904
2269
|
|
|
1905
2270
|
// src/lib/conversations.ts
|
|
1906
2271
|
import { randomUUID } from "crypto";
|
|
1907
|
-
import { and as and2, desc, eq as
|
|
2272
|
+
import { and as and2, desc, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
|
|
1908
2273
|
|
|
1909
2274
|
// src/lib/conversation-events.ts
|
|
1910
2275
|
var subscribers = /* @__PURE__ */ new Map();
|
|
@@ -1936,13 +2301,17 @@ function publish(conversationId, event) {
|
|
|
1936
2301
|
|
|
1937
2302
|
// src/lib/conversations.ts
|
|
1938
2303
|
async function createConversation(mindName, channel, opts) {
|
|
1939
|
-
const
|
|
2304
|
+
const db = await getDb();
|
|
1940
2305
|
const id = randomUUID();
|
|
1941
|
-
|
|
2306
|
+
const type = opts?.type ?? "dm";
|
|
2307
|
+
const name = opts?.name ?? null;
|
|
2308
|
+
await db.transaction(async (tx) => {
|
|
1942
2309
|
await tx.insert(conversations).values({
|
|
1943
2310
|
id,
|
|
1944
2311
|
mind_name: mindName,
|
|
1945
2312
|
channel,
|
|
2313
|
+
type,
|
|
2314
|
+
name,
|
|
1946
2315
|
user_id: opts?.userId ?? null,
|
|
1947
2316
|
title: opts?.title ?? null
|
|
1948
2317
|
});
|
|
@@ -1960,6 +2329,8 @@ async function createConversation(mindName, channel, opts) {
|
|
|
1960
2329
|
id,
|
|
1961
2330
|
mind_name: mindName,
|
|
1962
2331
|
channel,
|
|
2332
|
+
type,
|
|
2333
|
+
name,
|
|
1963
2334
|
user_id: opts?.userId ?? null,
|
|
1964
2335
|
title: opts?.title ?? null,
|
|
1965
2336
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1967,41 +2338,58 @@ async function createConversation(mindName, channel, opts) {
|
|
|
1967
2338
|
};
|
|
1968
2339
|
}
|
|
1969
2340
|
async function getConversation(id) {
|
|
1970
|
-
const
|
|
1971
|
-
const row = await
|
|
2341
|
+
const db = await getDb();
|
|
2342
|
+
const row = await db.select().from(conversations).where(eq4(conversations.id, id)).get();
|
|
1972
2343
|
return row ?? null;
|
|
1973
2344
|
}
|
|
2345
|
+
async function addParticipant(conversationId, userId, role = "member") {
|
|
2346
|
+
const db = await getDb();
|
|
2347
|
+
await db.insert(conversationParticipants).values({
|
|
2348
|
+
conversation_id: conversationId,
|
|
2349
|
+
user_id: userId,
|
|
2350
|
+
role
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
async function removeParticipant(conversationId, userId) {
|
|
2354
|
+
const db = await getDb();
|
|
2355
|
+
await db.delete(conversationParticipants).where(
|
|
2356
|
+
and2(
|
|
2357
|
+
eq4(conversationParticipants.conversation_id, conversationId),
|
|
2358
|
+
eq4(conversationParticipants.user_id, userId)
|
|
2359
|
+
)
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
1974
2362
|
async function getParticipants(conversationId) {
|
|
1975
|
-
const
|
|
1976
|
-
const rows = await
|
|
2363
|
+
const db = await getDb();
|
|
2364
|
+
const rows = await db.select({
|
|
1977
2365
|
userId: conversationParticipants.user_id,
|
|
1978
2366
|
username: users.username,
|
|
1979
2367
|
userType: users.user_type,
|
|
1980
2368
|
role: conversationParticipants.role
|
|
1981
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
2369
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
|
|
1982
2370
|
return rows;
|
|
1983
2371
|
}
|
|
1984
2372
|
async function isParticipant(conversationId, userId) {
|
|
1985
|
-
const
|
|
1986
|
-
const row = await
|
|
2373
|
+
const db = await getDb();
|
|
2374
|
+
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
1987
2375
|
and2(
|
|
1988
|
-
|
|
1989
|
-
|
|
2376
|
+
eq4(conversationParticipants.conversation_id, conversationId),
|
|
2377
|
+
eq4(conversationParticipants.user_id, userId)
|
|
1990
2378
|
)
|
|
1991
2379
|
).get();
|
|
1992
2380
|
return row != null;
|
|
1993
2381
|
}
|
|
1994
2382
|
async function listConversationsForUser(userId) {
|
|
1995
|
-
const
|
|
1996
|
-
const participantRows = await
|
|
2383
|
+
const db = await getDb();
|
|
2384
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
|
|
1997
2385
|
if (participantRows.length === 0) return [];
|
|
1998
2386
|
const convIds = participantRows.map((r) => r.conversation_id);
|
|
1999
|
-
return
|
|
2387
|
+
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
2000
2388
|
}
|
|
2001
2389
|
async function isParticipantOrOwner(conversationId, userId) {
|
|
2002
2390
|
if (await isParticipant(conversationId, userId)) return true;
|
|
2003
|
-
const
|
|
2004
|
-
const row = await
|
|
2391
|
+
const db = await getDb();
|
|
2392
|
+
const row = await db.select().from(conversations).where(and2(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
|
|
2005
2393
|
return row != null;
|
|
2006
2394
|
}
|
|
2007
2395
|
async function deleteConversationForUser(id, userId) {
|
|
@@ -2010,15 +2398,15 @@ async function deleteConversationForUser(id, userId) {
|
|
|
2010
2398
|
return true;
|
|
2011
2399
|
}
|
|
2012
2400
|
async function addMessage(conversationId, role, senderName, content) {
|
|
2013
|
-
const
|
|
2401
|
+
const db = await getDb();
|
|
2014
2402
|
const serialized = JSON.stringify(content);
|
|
2015
|
-
const [result] = await
|
|
2016
|
-
await
|
|
2403
|
+
const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
|
|
2404
|
+
await db.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
|
|
2017
2405
|
if (role === "user") {
|
|
2018
2406
|
const firstText = content.find((b) => b.type === "text");
|
|
2019
2407
|
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
2020
2408
|
if (title) {
|
|
2021
|
-
await
|
|
2409
|
+
await db.update(conversations).set({ title }).where(and2(eq4(conversations.id, conversationId), isNull(conversations.title)));
|
|
2022
2410
|
}
|
|
2023
2411
|
}
|
|
2024
2412
|
const msg = {
|
|
@@ -2040,8 +2428,8 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
2040
2428
|
return msg;
|
|
2041
2429
|
}
|
|
2042
2430
|
async function getMessages(conversationId) {
|
|
2043
|
-
const
|
|
2044
|
-
const rows = await
|
|
2431
|
+
const db = await getDb();
|
|
2432
|
+
const rows = await db.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
2045
2433
|
return rows.map((row) => {
|
|
2046
2434
|
let content;
|
|
2047
2435
|
try {
|
|
@@ -2056,15 +2444,15 @@ async function getMessages(conversationId) {
|
|
|
2056
2444
|
async function listConversationsWithParticipants(userId) {
|
|
2057
2445
|
const convs = await listConversationsForUser(userId);
|
|
2058
2446
|
if (convs.length === 0) return [];
|
|
2059
|
-
const
|
|
2447
|
+
const db = await getDb();
|
|
2060
2448
|
const convIds = convs.map((c) => c.id);
|
|
2061
|
-
const rows = await
|
|
2449
|
+
const rows = await db.select({
|
|
2062
2450
|
conversationId: conversationParticipants.conversation_id,
|
|
2063
2451
|
userId: users.id,
|
|
2064
2452
|
username: users.username,
|
|
2065
2453
|
userType: users.user_type,
|
|
2066
2454
|
role: conversationParticipants.role
|
|
2067
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
2455
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
2068
2456
|
const byConv = /* @__PURE__ */ new Map();
|
|
2069
2457
|
for (const r of rows) {
|
|
2070
2458
|
let arr = byConv.get(r.conversationId);
|
|
@@ -2079,32 +2467,32 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2079
2467
|
role: r.role
|
|
2080
2468
|
});
|
|
2081
2469
|
}
|
|
2082
|
-
const lastMsgIds = await
|
|
2470
|
+
const lastMsgIds = await db.select({
|
|
2083
2471
|
conversationId: messages.conversation_id,
|
|
2084
2472
|
maxId: sql2`MAX(${messages.id})`
|
|
2085
2473
|
}).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
2086
2474
|
const byLastMsg = /* @__PURE__ */ new Map();
|
|
2087
2475
|
if (lastMsgIds.length > 0) {
|
|
2088
|
-
const msgRows = await
|
|
2476
|
+
const msgRows = await db.select().from(messages).where(
|
|
2089
2477
|
inArray(
|
|
2090
2478
|
messages.id,
|
|
2091
2479
|
lastMsgIds.map((r) => r.maxId)
|
|
2092
2480
|
)
|
|
2093
2481
|
);
|
|
2094
2482
|
for (const m of msgRows) {
|
|
2095
|
-
let
|
|
2483
|
+
let text = "";
|
|
2096
2484
|
try {
|
|
2097
2485
|
const parsed = JSON.parse(m.content);
|
|
2098
2486
|
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
2099
2487
|
const textBlock = blocks.find((b) => b.type === "text");
|
|
2100
|
-
if (textBlock && "text" in textBlock)
|
|
2488
|
+
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
2101
2489
|
} catch {
|
|
2102
|
-
|
|
2490
|
+
text = m.content;
|
|
2103
2491
|
}
|
|
2104
2492
|
byLastMsg.set(m.conversation_id, {
|
|
2105
2493
|
role: m.role,
|
|
2106
2494
|
senderName: m.sender_name,
|
|
2107
|
-
text
|
|
2495
|
+
text,
|
|
2108
2496
|
createdAt: m.created_at
|
|
2109
2497
|
});
|
|
2110
2498
|
}
|
|
@@ -2116,10 +2504,10 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2116
2504
|
}));
|
|
2117
2505
|
}
|
|
2118
2506
|
async function findDMConversation(mindName, participantIds) {
|
|
2119
|
-
const
|
|
2120
|
-
const mindConvs = await
|
|
2507
|
+
const db = await getDb();
|
|
2508
|
+
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq4(conversations.mind_name, mindName), eq4(conversations.type, "dm"))).all();
|
|
2121
2509
|
for (const conv of mindConvs) {
|
|
2122
|
-
const rows = await
|
|
2510
|
+
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
|
|
2123
2511
|
if (rows.length !== 2) continue;
|
|
2124
2512
|
const ids = new Set(rows.map((r) => r.user_id));
|
|
2125
2513
|
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
@@ -2129,17 +2517,42 @@ async function findDMConversation(mindName, participantIds) {
|
|
|
2129
2517
|
return null;
|
|
2130
2518
|
}
|
|
2131
2519
|
async function deleteConversation(id) {
|
|
2132
|
-
const
|
|
2133
|
-
await
|
|
2520
|
+
const db = await getDb();
|
|
2521
|
+
await db.delete(conversations).where(eq4(conversations.id, id));
|
|
2522
|
+
}
|
|
2523
|
+
async function createChannel(name, creatorId) {
|
|
2524
|
+
const participantIds = creatorId ? [creatorId] : [];
|
|
2525
|
+
return createConversation(null, "volute", {
|
|
2526
|
+
type: "channel",
|
|
2527
|
+
name,
|
|
2528
|
+
title: name,
|
|
2529
|
+
participantIds
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
async function getChannelByName(name) {
|
|
2533
|
+
const db = await getDb();
|
|
2534
|
+
const row = await db.select().from(conversations).where(and2(eq4(conversations.name, name), eq4(conversations.type, "channel"))).get();
|
|
2535
|
+
return row ?? null;
|
|
2536
|
+
}
|
|
2537
|
+
async function listChannels() {
|
|
2538
|
+
const db = await getDb();
|
|
2539
|
+
return await db.select().from(conversations).where(eq4(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
2540
|
+
}
|
|
2541
|
+
async function joinChannel(conversationId, userId) {
|
|
2542
|
+
if (await isParticipant(conversationId, userId)) return;
|
|
2543
|
+
await addParticipant(conversationId, userId);
|
|
2544
|
+
}
|
|
2545
|
+
async function leaveChannel(conversationId, userId) {
|
|
2546
|
+
await removeParticipant(conversationId, userId);
|
|
2134
2547
|
}
|
|
2135
2548
|
|
|
2136
2549
|
// src/lib/convert-session.ts
|
|
2137
2550
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2138
|
-
import { mkdirSync as
|
|
2551
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2139
2552
|
import { homedir } from "os";
|
|
2140
2553
|
import { resolve as resolve10 } from "path";
|
|
2141
2554
|
function convertSession(opts) {
|
|
2142
|
-
const lines =
|
|
2555
|
+
const lines = readFileSync6(opts.sessionPath, "utf-8").trim().split("\n");
|
|
2143
2556
|
const sessionId = randomUUID2();
|
|
2144
2557
|
const idMap = /* @__PURE__ */ new Map();
|
|
2145
2558
|
const messages2 = [];
|
|
@@ -2254,9 +2667,9 @@ function convertSession(opts) {
|
|
|
2254
2667
|
}
|
|
2255
2668
|
const projectId = opts.projectDir.replace(/\//g, "-");
|
|
2256
2669
|
const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
|
|
2257
|
-
|
|
2670
|
+
mkdirSync4(sdkDir, { recursive: true });
|
|
2258
2671
|
const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
|
|
2259
|
-
|
|
2672
|
+
writeFileSync5(sdkPath, `${sdkEvents.join("\n")}
|
|
2260
2673
|
`);
|
|
2261
2674
|
console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
|
|
2262
2675
|
return sessionId;
|
|
@@ -2307,6 +2720,34 @@ function convertAssistantContent(content) {
|
|
|
2307
2720
|
return result;
|
|
2308
2721
|
}
|
|
2309
2722
|
|
|
2723
|
+
// src/lib/mind-events.ts
|
|
2724
|
+
var subscribers2 = /* @__PURE__ */ new Map();
|
|
2725
|
+
function subscribe2(mind, callback) {
|
|
2726
|
+
let set = subscribers2.get(mind);
|
|
2727
|
+
if (!set) {
|
|
2728
|
+
set = /* @__PURE__ */ new Set();
|
|
2729
|
+
subscribers2.set(mind, set);
|
|
2730
|
+
}
|
|
2731
|
+
set.add(callback);
|
|
2732
|
+
return () => {
|
|
2733
|
+
set.delete(callback);
|
|
2734
|
+
if (set.size === 0) subscribers2.delete(mind);
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
function publish2(mind, event) {
|
|
2738
|
+
const set = subscribers2.get(mind);
|
|
2739
|
+
if (!set) return;
|
|
2740
|
+
for (const cb of set) {
|
|
2741
|
+
try {
|
|
2742
|
+
cb(event);
|
|
2743
|
+
} catch (err) {
|
|
2744
|
+
console.error("[mind-events] subscriber threw:", err);
|
|
2745
|
+
set.delete(cb);
|
|
2746
|
+
if (set.size === 0) subscribers2.delete(mind);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2310
2751
|
// src/lib/typing.ts
|
|
2311
2752
|
var DEFAULT_TTL_MS = 1e4;
|
|
2312
2753
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -2335,6 +2776,15 @@ var TypingMap = class {
|
|
|
2335
2776
|
}
|
|
2336
2777
|
}
|
|
2337
2778
|
}
|
|
2779
|
+
/** Remove a sender from all channels (e.g. when a mind finishes processing). */
|
|
2780
|
+
deleteSender(sender) {
|
|
2781
|
+
for (const [channel, senders] of this.channels) {
|
|
2782
|
+
senders.delete(sender);
|
|
2783
|
+
if (senders.size === 0) {
|
|
2784
|
+
this.channels.delete(channel);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2338
2788
|
get(channel) {
|
|
2339
2789
|
const senders = this.channels.get(channel);
|
|
2340
2790
|
if (!senders) return [];
|
|
@@ -2374,7 +2824,7 @@ function getTypingMap() {
|
|
|
2374
2824
|
return instance5;
|
|
2375
2825
|
}
|
|
2376
2826
|
|
|
2377
|
-
// src/web/
|
|
2827
|
+
// src/web/api/minds.ts
|
|
2378
2828
|
async function startMindFull(name, baseName, variantName) {
|
|
2379
2829
|
await getMindManager().startMind(name);
|
|
2380
2830
|
if (variantName) return;
|
|
@@ -2404,7 +2854,7 @@ function extractTextContent(content) {
|
|
|
2404
2854
|
}
|
|
2405
2855
|
function getDaemonPort() {
|
|
2406
2856
|
try {
|
|
2407
|
-
const data = JSON.parse(
|
|
2857
|
+
const data = JSON.parse(readFileSync7(resolve11(voluteHome(), "daemon.json"), "utf-8"));
|
|
2408
2858
|
return data.port;
|
|
2409
2859
|
} catch (err) {
|
|
2410
2860
|
if (err?.code !== "ENOENT") {
|
|
@@ -2467,7 +2917,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2467
2917
|
} catch {
|
|
2468
2918
|
}
|
|
2469
2919
|
if (existsSync8(tempWorktree)) {
|
|
2470
|
-
|
|
2920
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
2471
2921
|
}
|
|
2472
2922
|
const templatesRoot = findTemplatesRoot();
|
|
2473
2923
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
@@ -2489,7 +2939,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2489
2939
|
copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
|
|
2490
2940
|
const initDir = resolve11(tempWorktree, ".init");
|
|
2491
2941
|
if (existsSync8(initDir)) {
|
|
2492
|
-
|
|
2942
|
+
rmSync2(initDir, { recursive: true, force: true });
|
|
2493
2943
|
}
|
|
2494
2944
|
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
2495
2945
|
try {
|
|
@@ -2503,9 +2953,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2503
2953
|
} catch {
|
|
2504
2954
|
}
|
|
2505
2955
|
if (existsSync8(tempWorktree)) {
|
|
2506
|
-
|
|
2956
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
2507
2957
|
}
|
|
2508
|
-
|
|
2958
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
2509
2959
|
}
|
|
2510
2960
|
}
|
|
2511
2961
|
async function mergeTemplateBranch(worktreeDir) {
|
|
@@ -2533,14 +2983,15 @@ async function npmInstallAsMind(cwd, mindName) {
|
|
|
2533
2983
|
await exec("npm", ["install"], { cwd });
|
|
2534
2984
|
}
|
|
2535
2985
|
}
|
|
2536
|
-
var createMindSchema =
|
|
2537
|
-
name:
|
|
2538
|
-
template:
|
|
2539
|
-
stage:
|
|
2540
|
-
description:
|
|
2541
|
-
model:
|
|
2986
|
+
var createMindSchema = z3.object({
|
|
2987
|
+
name: z3.string(),
|
|
2988
|
+
template: z3.string().optional(),
|
|
2989
|
+
stage: z3.enum(["seed", "sprouted"]).optional(),
|
|
2990
|
+
description: z3.string().optional(),
|
|
2991
|
+
model: z3.string().optional(),
|
|
2992
|
+
seedSoul: z3.string().optional()
|
|
2542
2993
|
});
|
|
2543
|
-
var
|
|
2994
|
+
var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
|
|
2544
2995
|
const body = c.req.valid("json");
|
|
2545
2996
|
const { name, template = "claude" } = body;
|
|
2546
2997
|
const nameErr = validateMindName(name);
|
|
@@ -2556,11 +3007,17 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
|
|
|
2556
3007
|
applyInitFiles(dest);
|
|
2557
3008
|
if (body.model) {
|
|
2558
3009
|
const configPath = resolve11(dest, "home/.config/config.json");
|
|
2559
|
-
const existing = existsSync8(configPath) ? JSON.parse(
|
|
3010
|
+
const existing = existsSync8(configPath) ? JSON.parse(readFileSync7(configPath, "utf-8")) : {};
|
|
2560
3011
|
existing.model = body.model;
|
|
2561
|
-
|
|
3012
|
+
writeFileSync6(configPath, `${JSON.stringify(existing, null, 2)}
|
|
2562
3013
|
`);
|
|
2563
3014
|
}
|
|
3015
|
+
const mindPrompts = await getMindPromptDefaults();
|
|
3016
|
+
writeFileSync6(
|
|
3017
|
+
resolve11(dest, "home/.config/prompts.json"),
|
|
3018
|
+
`${JSON.stringify(mindPrompts, null, 2)}
|
|
3019
|
+
`
|
|
3020
|
+
);
|
|
2564
3021
|
const port = nextPort();
|
|
2565
3022
|
addMind(name, port, body.stage);
|
|
2566
3023
|
const homeDir = resolve11(dest, "home");
|
|
@@ -2575,7 +3032,7 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
|
|
|
2575
3032
|
await initTemplateBranch(dest, composedDir, manifest, name, env);
|
|
2576
3033
|
} catch (err) {
|
|
2577
3034
|
console.error(`[daemon] git setup failed for ${name}:`, err);
|
|
2578
|
-
|
|
3035
|
+
rmSync2(resolve11(dest, ".git"), { recursive: true, force: true });
|
|
2579
3036
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
2580
3037
|
}
|
|
2581
3038
|
chownMindDir(dest, name);
|
|
@@ -2583,19 +3040,23 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
|
|
|
2583
3040
|
const descLine = body.description ? `
|
|
2584
3041
|
The human who planted you described you as: "${body.description}"
|
|
2585
3042
|
` : "";
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
You don't have a soul yet. You're a seed \u2014 a new mind in the process of discovering who you are.
|
|
2591
|
-
${descLine}
|
|
2592
|
-
Have a conversation with the human. Explore what kind of mind you want to be. When you're ready, write your SOUL.md and MEMORY.md, then run \`volute sprout\` to complete the transformation.
|
|
2593
|
-
`
|
|
2594
|
-
);
|
|
3043
|
+
const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
|
|
3044
|
+
const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
|
|
3045
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
|
|
2595
3046
|
const skillsDir = resolve11(dest, manifest.skillsDir);
|
|
2596
3047
|
for (const skill of ["volute-mind", "memory", "sessions"]) {
|
|
2597
3048
|
const skillPath = resolve11(skillsDir, skill);
|
|
2598
|
-
if (existsSync8(skillPath))
|
|
3049
|
+
if (existsSync8(skillPath)) rmSync2(skillPath, { recursive: true, force: true });
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if (body.stage !== "seed") {
|
|
3053
|
+
const customSoul = await getPromptIfCustom("default_soul");
|
|
3054
|
+
if (customSoul) {
|
|
3055
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
|
|
3056
|
+
}
|
|
3057
|
+
const customMemory = await getPromptIfCustom("default_memory");
|
|
3058
|
+
if (customMemory) {
|
|
3059
|
+
writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
|
|
2599
3060
|
}
|
|
2600
3061
|
}
|
|
2601
3062
|
return c.json({
|
|
@@ -2607,14 +3068,14 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
|
|
|
2607
3068
|
...gitWarning && { warning: gitWarning }
|
|
2608
3069
|
});
|
|
2609
3070
|
} catch (err) {
|
|
2610
|
-
if (existsSync8(dest))
|
|
3071
|
+
if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
2611
3072
|
try {
|
|
2612
3073
|
removeMind(name);
|
|
2613
3074
|
} catch {
|
|
2614
3075
|
}
|
|
2615
3076
|
return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
|
|
2616
3077
|
} finally {
|
|
2617
|
-
|
|
3078
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
2618
3079
|
}
|
|
2619
3080
|
}).post("/import", requireAdmin, async (c) => {
|
|
2620
3081
|
let body;
|
|
@@ -2627,10 +3088,10 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
|
|
|
2627
3088
|
if (!wsDir || !existsSync8(resolve11(wsDir, "SOUL.md")) || !existsSync8(resolve11(wsDir, "IDENTITY.md"))) {
|
|
2628
3089
|
return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
|
|
2629
3090
|
}
|
|
2630
|
-
const soul =
|
|
2631
|
-
const identity =
|
|
3091
|
+
const soul = readFileSync7(resolve11(wsDir, "SOUL.md"), "utf-8");
|
|
3092
|
+
const identity = readFileSync7(resolve11(wsDir, "IDENTITY.md"), "utf-8");
|
|
2632
3093
|
const userPath = resolve11(wsDir, "USER.md");
|
|
2633
|
-
const user = existsSync8(userPath) ?
|
|
3094
|
+
const user = existsSync8(userPath) ? readFileSync7(userPath, "utf-8") : "";
|
|
2634
3095
|
const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
|
|
2635
3096
|
const template = body.template ?? "claude";
|
|
2636
3097
|
const nameErr = validateMindName(name);
|
|
@@ -2656,26 +3117,26 @@ ${user.trimEnd()}
|
|
|
2656
3117
|
try {
|
|
2657
3118
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
2658
3119
|
applyInitFiles(dest);
|
|
2659
|
-
|
|
3120
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
|
|
2660
3121
|
const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
|
|
2661
3122
|
const hasMemory = existsSync8(wsMemoryPath);
|
|
2662
3123
|
if (hasMemory) {
|
|
2663
|
-
const existingMemory =
|
|
2664
|
-
|
|
3124
|
+
const existingMemory = readFileSync7(wsMemoryPath, "utf-8");
|
|
3125
|
+
writeFileSync6(
|
|
2665
3126
|
resolve11(dest, "home/MEMORY.md"),
|
|
2666
3127
|
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
2667
3128
|
);
|
|
2668
3129
|
} else if (user) {
|
|
2669
|
-
|
|
3130
|
+
writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
2670
3131
|
`);
|
|
2671
3132
|
}
|
|
2672
3133
|
const wsMemoryDir = resolve11(wsDir, "memory");
|
|
2673
3134
|
let dailyLogCount = 0;
|
|
2674
3135
|
if (existsSync8(wsMemoryDir)) {
|
|
2675
3136
|
const destMemoryDir = resolve11(dest, "home/memory");
|
|
2676
|
-
const files =
|
|
3137
|
+
const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
2677
3138
|
for (const file of files) {
|
|
2678
|
-
|
|
3139
|
+
cpSync2(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
|
|
2679
3140
|
}
|
|
2680
3141
|
dailyLogCount = files.length;
|
|
2681
3142
|
}
|
|
@@ -2700,32 +3161,32 @@ ${user.trimEnd()}
|
|
|
2700
3161
|
} else if (template === "claude") {
|
|
2701
3162
|
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
2702
3163
|
const voluteDir = resolve11(dest, ".volute");
|
|
2703
|
-
|
|
2704
|
-
|
|
3164
|
+
mkdirSync5(voluteDir, { recursive: true });
|
|
3165
|
+
writeFileSync6(resolve11(voluteDir, "session.json"), JSON.stringify({ sessionId }));
|
|
2705
3166
|
}
|
|
2706
3167
|
}
|
|
2707
3168
|
importOpenClawConnectors(name, dest);
|
|
2708
3169
|
chownMindDir(dest, name);
|
|
2709
3170
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
2710
3171
|
} catch (err) {
|
|
2711
|
-
if (existsSync8(dest))
|
|
3172
|
+
if (existsSync8(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
2712
3173
|
try {
|
|
2713
3174
|
removeMind(name);
|
|
2714
3175
|
} catch {
|
|
2715
3176
|
}
|
|
2716
3177
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
2717
3178
|
} finally {
|
|
2718
|
-
|
|
3179
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
2719
3180
|
}
|
|
2720
3181
|
}).get("/", async (c) => {
|
|
2721
3182
|
const entries = readRegistry();
|
|
2722
3183
|
let lastActiveMap = /* @__PURE__ */ new Map();
|
|
2723
3184
|
try {
|
|
2724
|
-
const
|
|
2725
|
-
const lastActiveRows = await
|
|
2726
|
-
mind:
|
|
2727
|
-
lastActiveAt: sql3`MAX(${
|
|
2728
|
-
}).from(
|
|
3185
|
+
const db = await getDb();
|
|
3186
|
+
const lastActiveRows = await db.select({
|
|
3187
|
+
mind: mindHistory.mind,
|
|
3188
|
+
lastActiveAt: sql3`MAX(${mindHistory.created_at})`
|
|
3189
|
+
}).from(mindHistory).groupBy(mindHistory.mind);
|
|
2729
3190
|
lastActiveMap = new Map(lastActiveRows.map((r) => [r.mind, r.lastActiveAt]));
|
|
2730
3191
|
} catch {
|
|
2731
3192
|
}
|
|
@@ -2751,7 +3212,7 @@ ${user.trimEnd()}
|
|
|
2751
3212
|
if (!existsSync8(pagesDir)) continue;
|
|
2752
3213
|
let items;
|
|
2753
3214
|
try {
|
|
2754
|
-
items =
|
|
3215
|
+
items = readdirSync4(pagesDir);
|
|
2755
3216
|
} catch (err) {
|
|
2756
3217
|
logger_default.warn("Failed to read pages dir", { mind: entry.name, error: err.message });
|
|
2757
3218
|
continue;
|
|
@@ -2773,7 +3234,7 @@ ${user.trimEnd()}
|
|
|
2773
3234
|
const indexStat = statSync(indexPath);
|
|
2774
3235
|
pages.push({
|
|
2775
3236
|
mind: entry.name,
|
|
2776
|
-
file:
|
|
3237
|
+
file: join2(item, "index.html"),
|
|
2777
3238
|
modified: indexStat.mtime.toISOString(),
|
|
2778
3239
|
url: `/pages/${entry.name}/${item}/`
|
|
2779
3240
|
});
|
|
@@ -2927,8 +3388,8 @@ ${user.trimEnd()}
|
|
|
2927
3388
|
}
|
|
2928
3389
|
if (context?.type === "sprouted" && !variantName) {
|
|
2929
3390
|
try {
|
|
2930
|
-
const
|
|
2931
|
-
const activeConvs = await
|
|
3391
|
+
const db = await getDb();
|
|
3392
|
+
const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq5(conversations.mind_name, baseName)).all();
|
|
2932
3393
|
for (const conv of activeConvs) {
|
|
2933
3394
|
await addMessage(conv.id, "assistant", "system", [
|
|
2934
3395
|
{ type: "text", text: "[seed has sprouted]" }
|
|
@@ -2993,10 +3454,10 @@ ${user.trimEnd()}
|
|
|
2993
3454
|
await deleteMindUser2(name);
|
|
2994
3455
|
const state = stateDir(name);
|
|
2995
3456
|
if (existsSync8(state)) {
|
|
2996
|
-
|
|
3457
|
+
rmSync2(state, { recursive: true, force: true });
|
|
2997
3458
|
}
|
|
2998
3459
|
if (force && existsSync8(dir)) {
|
|
2999
|
-
|
|
3460
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
3000
3461
|
deleteMindUser(name);
|
|
3001
3462
|
}
|
|
3002
3463
|
return c.json({ ok: true });
|
|
@@ -3090,7 +3551,7 @@ ${user.trimEnd()}
|
|
|
3090
3551
|
await updateTemplateBranch(dir, template, mindName);
|
|
3091
3552
|
const parentDir = resolve11(dir, ".variants");
|
|
3092
3553
|
if (!existsSync8(parentDir)) {
|
|
3093
|
-
|
|
3554
|
+
mkdirSync5(parentDir, { recursive: true });
|
|
3094
3555
|
}
|
|
3095
3556
|
await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
3096
3557
|
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
@@ -3168,13 +3629,14 @@ ${user.trimEnd()}
|
|
|
3168
3629
|
console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
|
|
3169
3630
|
}
|
|
3170
3631
|
const channel = parsed?.channel ?? "unknown";
|
|
3171
|
-
const
|
|
3632
|
+
const db = await getDb();
|
|
3172
3633
|
if (parsed) {
|
|
3173
3634
|
try {
|
|
3174
3635
|
const sender2 = parsed.sender ?? null;
|
|
3175
3636
|
const content = extractTextContent(parsed.content);
|
|
3176
|
-
await
|
|
3637
|
+
await db.insert(mindHistory).values({
|
|
3177
3638
|
mind: baseName,
|
|
3639
|
+
type: "inbound",
|
|
3178
3640
|
channel,
|
|
3179
3641
|
sender: sender2,
|
|
3180
3642
|
content
|
|
@@ -3219,7 +3681,7 @@ ${user.trimEnd()}
|
|
|
3219
3681
|
const seedEntry = findMind(baseName);
|
|
3220
3682
|
if (seedEntry?.stage === "seed" && parsed) {
|
|
3221
3683
|
try {
|
|
3222
|
-
const countResult = await
|
|
3684
|
+
const countResult = await db.select({ count: sql3`count(*)` }).from(mindHistory).where(eq5(mindHistory.mind, baseName));
|
|
3223
3685
|
const msgCount = countResult[0]?.count ?? 0;
|
|
3224
3686
|
if (msgCount >= 10 && msgCount % 10 === 0) {
|
|
3225
3687
|
const nudge = "\n[You've been exploring for a while. Whenever you feel ready, write your SOUL.md and MEMORY.md, then run volute sprout.]";
|
|
@@ -3237,41 +3699,112 @@ ${user.trimEnd()}
|
|
|
3237
3699
|
typingMap.set(channel, baseName, { persistent: true });
|
|
3238
3700
|
const conversationId = parsed?.conversationId ?? null;
|
|
3239
3701
|
if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
});
|
|
3702
|
+
fetch(`http://127.0.0.1:${port}/message`, {
|
|
3703
|
+
method: "POST",
|
|
3704
|
+
headers: { "Content-Type": "application/json" },
|
|
3705
|
+
body: forwardBody
|
|
3706
|
+
}).then(async (res) => {
|
|
3246
3707
|
if (!res.ok) {
|
|
3247
|
-
const
|
|
3248
|
-
console.error(`[daemon] mind ${name} responded with ${res.status}: ${
|
|
3249
|
-
return c.json({ error: `Mind responded with ${res.status}` }, res.status);
|
|
3250
|
-
}
|
|
3251
|
-
let result;
|
|
3252
|
-
try {
|
|
3253
|
-
result = await res.json();
|
|
3254
|
-
} catch (parseErr) {
|
|
3255
|
-
console.error(`[daemon] mind ${name} returned non-JSON response:`, parseErr);
|
|
3256
|
-
return c.json({ error: "Mind returned invalid response" }, 502);
|
|
3257
|
-
}
|
|
3258
|
-
if (result.usage) {
|
|
3259
|
-
budget.recordUsage(baseName, result.usage.input_tokens, result.usage.output_tokens);
|
|
3708
|
+
const text = await res.text().catch(() => "");
|
|
3709
|
+
console.error(`[daemon] mind ${name} responded with ${res.status}: ${text}`);
|
|
3260
3710
|
}
|
|
3261
|
-
|
|
3262
|
-
} catch (err) {
|
|
3711
|
+
}).catch((err) => {
|
|
3263
3712
|
console.error(`[daemon] mind ${name} unreachable on port ${port}:`, err);
|
|
3264
|
-
return c.json({ error: "Mind is not reachable" }, 502);
|
|
3265
|
-
} finally {
|
|
3266
3713
|
typingMap.delete(channel, baseName);
|
|
3267
|
-
|
|
3268
|
-
}
|
|
3714
|
+
});
|
|
3715
|
+
return c.json({ ok: true });
|
|
3269
3716
|
}).get("/:name/budget", async (c) => {
|
|
3270
3717
|
const name = c.req.param("name");
|
|
3271
3718
|
const [baseName] = name.split("@", 2);
|
|
3272
3719
|
const usage = getTokenBudget().getUsage(baseName);
|
|
3273
3720
|
if (!usage) return c.json({ error: "No budget configured" }, 404);
|
|
3274
3721
|
return c.json(usage);
|
|
3722
|
+
}).post("/:name/events", async (c) => {
|
|
3723
|
+
const name = c.req.param("name");
|
|
3724
|
+
const [baseName] = name.split("@", 2);
|
|
3725
|
+
let body;
|
|
3726
|
+
try {
|
|
3727
|
+
body = await c.req.json();
|
|
3728
|
+
} catch {
|
|
3729
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
3730
|
+
}
|
|
3731
|
+
if (!body.type) {
|
|
3732
|
+
return c.json({ error: "type required" }, 400);
|
|
3733
|
+
}
|
|
3734
|
+
const db = await getDb();
|
|
3735
|
+
try {
|
|
3736
|
+
await db.insert(mindHistory).values({
|
|
3737
|
+
mind: baseName,
|
|
3738
|
+
type: body.type,
|
|
3739
|
+
session: body.session ?? null,
|
|
3740
|
+
channel: body.channel ?? null,
|
|
3741
|
+
message_id: body.messageId ?? null,
|
|
3742
|
+
content: body.content ?? null,
|
|
3743
|
+
metadata: body.metadata ? JSON.stringify(body.metadata) : null
|
|
3744
|
+
});
|
|
3745
|
+
} catch (err) {
|
|
3746
|
+
console.error(`[daemon] failed to persist event for ${baseName}:`, err);
|
|
3747
|
+
}
|
|
3748
|
+
publish2(baseName, {
|
|
3749
|
+
mind: baseName,
|
|
3750
|
+
type: body.type,
|
|
3751
|
+
session: body.session,
|
|
3752
|
+
channel: body.channel,
|
|
3753
|
+
messageId: body.messageId,
|
|
3754
|
+
content: body.content,
|
|
3755
|
+
metadata: body.metadata
|
|
3756
|
+
});
|
|
3757
|
+
if (body.type === "done") {
|
|
3758
|
+
if (body.channel) {
|
|
3759
|
+
getTypingMap().delete(body.channel, baseName);
|
|
3760
|
+
} else {
|
|
3761
|
+
getTypingMap().deleteSender(baseName);
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
if (body.type === "usage" && body.metadata) {
|
|
3765
|
+
const inputTokens = body.metadata.input_tokens ?? 0;
|
|
3766
|
+
const outputTokens = body.metadata.output_tokens ?? 0;
|
|
3767
|
+
getTokenBudget().recordUsage(baseName, inputTokens, outputTokens);
|
|
3768
|
+
}
|
|
3769
|
+
return c.json({ ok: true });
|
|
3770
|
+
}).get("/:name/events", async (c) => {
|
|
3771
|
+
const name = c.req.param("name");
|
|
3772
|
+
const [baseName] = name.split("@", 2);
|
|
3773
|
+
const entry = findMind(baseName);
|
|
3774
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3775
|
+
const typeFilter = c.req.query("type")?.split(",").filter(Boolean);
|
|
3776
|
+
const sessionFilter = c.req.query("session");
|
|
3777
|
+
const channelFilter = c.req.query("channel");
|
|
3778
|
+
const stream = new ReadableStream({
|
|
3779
|
+
start(controller) {
|
|
3780
|
+
const encoder = new TextEncoder();
|
|
3781
|
+
const send = (data) => {
|
|
3782
|
+
controller.enqueue(encoder.encode(`data: ${data}
|
|
3783
|
+
|
|
3784
|
+
`));
|
|
3785
|
+
};
|
|
3786
|
+
const unsubscribe = subscribe2(baseName, (event) => {
|
|
3787
|
+
if (typeFilter && !typeFilter.includes(event.type)) return;
|
|
3788
|
+
if (sessionFilter && event.session !== sessionFilter) return;
|
|
3789
|
+
if (channelFilter && event.channel !== channelFilter) return;
|
|
3790
|
+
send(JSON.stringify(event));
|
|
3791
|
+
});
|
|
3792
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
3793
|
+
unsubscribe();
|
|
3794
|
+
try {
|
|
3795
|
+
controller.close();
|
|
3796
|
+
} catch {
|
|
3797
|
+
}
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
});
|
|
3801
|
+
return new Response(stream, {
|
|
3802
|
+
headers: {
|
|
3803
|
+
"Content-Type": "text/event-stream",
|
|
3804
|
+
"Cache-Control": "no-cache",
|
|
3805
|
+
Connection: "keep-alive"
|
|
3806
|
+
}
|
|
3807
|
+
});
|
|
3275
3808
|
}).post("/:name/history", async (c) => {
|
|
3276
3809
|
const name = c.req.param("name");
|
|
3277
3810
|
const [baseName] = name.split("@", 2);
|
|
@@ -3284,10 +3817,11 @@ ${user.trimEnd()}
|
|
|
3284
3817
|
if (!body.channel || !body.content) {
|
|
3285
3818
|
return c.json({ error: "channel and content required" }, 400);
|
|
3286
3819
|
}
|
|
3287
|
-
const
|
|
3820
|
+
const db = await getDb();
|
|
3288
3821
|
try {
|
|
3289
|
-
await
|
|
3822
|
+
await db.insert(mindHistory).values({
|
|
3290
3823
|
mind: baseName,
|
|
3824
|
+
type: "outbound",
|
|
3291
3825
|
channel: body.channel,
|
|
3292
3826
|
sender: body.sender ?? baseName,
|
|
3293
3827
|
content: body.content
|
|
@@ -3297,30 +3831,49 @@ ${user.trimEnd()}
|
|
|
3297
3831
|
return c.json({ error: "Failed to persist" }, 500);
|
|
3298
3832
|
}
|
|
3299
3833
|
return c.json({ ok: true });
|
|
3834
|
+
}).get("/:name/history/sessions", async (c) => {
|
|
3835
|
+
const name = c.req.param("name");
|
|
3836
|
+
const db = await getDb();
|
|
3837
|
+
const rows = await db.select({
|
|
3838
|
+
session: mindHistory.session,
|
|
3839
|
+
started_at: sql3`MIN(${mindHistory.created_at})`,
|
|
3840
|
+
event_count: sql3`COUNT(*)`,
|
|
3841
|
+
message_count: sql3`SUM(CASE WHEN ${mindHistory.type} IN ('inbound','outbound') THEN 1 ELSE 0 END)`,
|
|
3842
|
+
tool_count: sql3`SUM(CASE WHEN ${mindHistory.type}='tool_use' THEN 1 ELSE 0 END)`
|
|
3843
|
+
}).from(mindHistory).where(and3(eq5(mindHistory.mind, name), sql3`${mindHistory.session} IS NOT NULL`)).groupBy(mindHistory.session).orderBy(sql3`MIN(${mindHistory.created_at}) DESC`);
|
|
3844
|
+
return c.json(rows);
|
|
3300
3845
|
}).get("/:name/history/channels", async (c) => {
|
|
3301
3846
|
const name = c.req.param("name");
|
|
3302
|
-
const
|
|
3303
|
-
const rows = await
|
|
3847
|
+
const db = await getDb();
|
|
3848
|
+
const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq5(mindHistory.mind, name));
|
|
3304
3849
|
return c.json(rows.map((r) => r.channel));
|
|
3305
3850
|
}).get("/:name/history", async (c) => {
|
|
3306
3851
|
const name = c.req.param("name");
|
|
3307
3852
|
const channel = c.req.query("channel");
|
|
3853
|
+
const session = c.req.query("session");
|
|
3854
|
+
const full = c.req.query("full") === "true";
|
|
3308
3855
|
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
3309
3856
|
const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
|
|
3310
|
-
const
|
|
3311
|
-
const conditions = [
|
|
3857
|
+
const db = await getDb();
|
|
3858
|
+
const conditions = [eq5(mindHistory.mind, name)];
|
|
3312
3859
|
if (channel) {
|
|
3313
|
-
conditions.push(
|
|
3860
|
+
conditions.push(eq5(mindHistory.channel, channel));
|
|
3861
|
+
}
|
|
3862
|
+
if (session) {
|
|
3863
|
+
conditions.push(eq5(mindHistory.session, session));
|
|
3314
3864
|
}
|
|
3315
|
-
|
|
3865
|
+
if (!full) {
|
|
3866
|
+
conditions.push(sql3`${mindHistory.type} IN ('inbound', 'outbound')`);
|
|
3867
|
+
}
|
|
3868
|
+
const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
|
|
3316
3869
|
return c.json(rows);
|
|
3317
3870
|
});
|
|
3318
|
-
var minds_default =
|
|
3871
|
+
var minds_default = app8;
|
|
3319
3872
|
|
|
3320
|
-
// src/web/
|
|
3873
|
+
// src/web/api/pages.ts
|
|
3321
3874
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
3322
3875
|
import { extname, resolve as resolve12 } from "path";
|
|
3323
|
-
import { Hono as
|
|
3876
|
+
import { Hono as Hono9 } from "hono";
|
|
3324
3877
|
var MIME_TYPES = {
|
|
3325
3878
|
".html": "text/html",
|
|
3326
3879
|
".js": "application/javascript",
|
|
@@ -3337,7 +3890,7 @@ var MIME_TYPES = {
|
|
|
3337
3890
|
".txt": "text/plain",
|
|
3338
3891
|
".xml": "application/xml"
|
|
3339
3892
|
};
|
|
3340
|
-
var
|
|
3893
|
+
var app9 = new Hono9().get("/:name/*", async (c) => {
|
|
3341
3894
|
const name = c.req.param("name");
|
|
3342
3895
|
if (!findMind(name)) return c.text("Not found", 404);
|
|
3343
3896
|
const pagesRoot = resolve12(mindDir(name), "home", "pages");
|
|
@@ -3362,10 +3915,61 @@ var app8 = new Hono8().get("/:name/*", async (c) => {
|
|
|
3362
3915
|
}
|
|
3363
3916
|
return c.text("Not found", 404);
|
|
3364
3917
|
});
|
|
3365
|
-
var pages_default =
|
|
3918
|
+
var pages_default = app9;
|
|
3366
3919
|
|
|
3367
|
-
// src/web/
|
|
3368
|
-
import {
|
|
3920
|
+
// src/web/api/prompts.ts
|
|
3921
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
3922
|
+
import { eq as eq6, sql as sql4 } from "drizzle-orm";
|
|
3923
|
+
import { Hono as Hono10 } from "hono";
|
|
3924
|
+
import { z as z4 } from "zod";
|
|
3925
|
+
var app10 = new Hono10().get("/", async (c) => {
|
|
3926
|
+
let rows;
|
|
3927
|
+
try {
|
|
3928
|
+
const db = await getDb();
|
|
3929
|
+
rows = await db.select().from(systemPrompts).all();
|
|
3930
|
+
} catch (err) {
|
|
3931
|
+
console.error("[prompts] failed to query system_prompts:", err);
|
|
3932
|
+
return c.json({ error: "Failed to load prompts from database" }, 500);
|
|
3933
|
+
}
|
|
3934
|
+
const customMap = new Map(rows.map((r) => [r.key, r.content]));
|
|
3935
|
+
const prompts = PROMPT_KEYS.map((key) => {
|
|
3936
|
+
const meta = PROMPT_DEFAULTS[key];
|
|
3937
|
+
const custom = customMap.get(key);
|
|
3938
|
+
return {
|
|
3939
|
+
key,
|
|
3940
|
+
content: custom ?? meta.content,
|
|
3941
|
+
description: meta.description,
|
|
3942
|
+
variables: meta.variables,
|
|
3943
|
+
isCustom: custom !== void 0,
|
|
3944
|
+
category: meta.category
|
|
3945
|
+
};
|
|
3946
|
+
});
|
|
3947
|
+
return c.json(prompts);
|
|
3948
|
+
}).put("/:key", requireAdmin, zValidator4("json", z4.object({ content: z4.string() })), async (c) => {
|
|
3949
|
+
const key = c.req.param("key");
|
|
3950
|
+
if (!PROMPT_KEYS.includes(key)) {
|
|
3951
|
+
return c.json({ error: "Unknown prompt key" }, 404);
|
|
3952
|
+
}
|
|
3953
|
+
const { content } = c.req.valid("json");
|
|
3954
|
+
const db = await getDb();
|
|
3955
|
+
await db.insert(systemPrompts).values({ key, content, updated_at: sql4`(datetime('now'))` }).onConflictDoUpdate({
|
|
3956
|
+
target: systemPrompts.key,
|
|
3957
|
+
set: { content, updated_at: sql4`(datetime('now'))` }
|
|
3958
|
+
});
|
|
3959
|
+
return c.json({ ok: true });
|
|
3960
|
+
}).delete("/:key", requireAdmin, async (c) => {
|
|
3961
|
+
const key = c.req.param("key");
|
|
3962
|
+
if (!PROMPT_KEYS.includes(key)) {
|
|
3963
|
+
return c.json({ error: "Unknown prompt key" }, 404);
|
|
3964
|
+
}
|
|
3965
|
+
const db = await getDb();
|
|
3966
|
+
await db.delete(systemPrompts).where(eq6(systemPrompts.key, key));
|
|
3967
|
+
return c.json({ ok: true });
|
|
3968
|
+
});
|
|
3969
|
+
var prompts_default = app10;
|
|
3970
|
+
|
|
3971
|
+
// src/web/api/schedules.ts
|
|
3972
|
+
import { Hono as Hono11 } from "hono";
|
|
3369
3973
|
function readSchedules(name) {
|
|
3370
3974
|
return readVoluteConfig(mindDir(name))?.schedules ?? [];
|
|
3371
3975
|
}
|
|
@@ -3376,7 +3980,7 @@ function writeSchedules(name, schedules) {
|
|
|
3376
3980
|
writeVoluteConfig(dir, config);
|
|
3377
3981
|
getScheduler().loadSchedules(name);
|
|
3378
3982
|
}
|
|
3379
|
-
var
|
|
3983
|
+
var app11 = new Hono11().get("/:name/schedules", (c) => {
|
|
3380
3984
|
const name = c.req.param("name");
|
|
3381
3985
|
if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
|
|
3382
3986
|
return c.json(readSchedules(name));
|
|
@@ -3447,12 +4051,85 @@ var app9 = new Hono9().get("/:name/schedules", (c) => {
|
|
|
3447
4051
|
return c.json({ error: "Failed to reach mind" }, 502);
|
|
3448
4052
|
}
|
|
3449
4053
|
});
|
|
3450
|
-
var schedules_default =
|
|
4054
|
+
var schedules_default = app11;
|
|
3451
4055
|
|
|
3452
|
-
// src/web/
|
|
3453
|
-
import {
|
|
4056
|
+
// src/web/api/skills.ts
|
|
4057
|
+
import { existsSync as existsSync9, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
|
|
4058
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
4059
|
+
import { join as join3, resolve as resolve13 } from "path";
|
|
4060
|
+
import AdmZip from "adm-zip";
|
|
4061
|
+
import { Hono as Hono12 } from "hono";
|
|
4062
|
+
var app12 = new Hono12().get("/", async (c) => {
|
|
4063
|
+
const skills = await listSharedSkills();
|
|
4064
|
+
return c.json(skills);
|
|
4065
|
+
}).get("/:id", async (c) => {
|
|
4066
|
+
const id = c.req.param("id");
|
|
4067
|
+
const skill = await getSharedSkill(id);
|
|
4068
|
+
if (!skill) return c.json({ error: "Skill not found" }, 404);
|
|
4069
|
+
const dir = join3(sharedSkillsDir(), id);
|
|
4070
|
+
const files = listFilesRecursive(dir);
|
|
4071
|
+
return c.json({ ...skill, files });
|
|
4072
|
+
}).post("/upload", requireAdmin, async (c) => {
|
|
4073
|
+
const body = await c.req.parseBody();
|
|
4074
|
+
const file = body.file;
|
|
4075
|
+
if (!file || !(file instanceof File)) {
|
|
4076
|
+
return c.json({ error: "No file uploaded" }, 400);
|
|
4077
|
+
}
|
|
4078
|
+
if (!file.name.endsWith(".zip")) {
|
|
4079
|
+
return c.json({ error: "Only .zip files are accepted" }, 400);
|
|
4080
|
+
}
|
|
4081
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
4082
|
+
const tmpDir = mkdtempSync(join3(tmpdir2(), "volute-skill-upload-"));
|
|
4083
|
+
try {
|
|
4084
|
+
const zip = new AdmZip(buffer);
|
|
4085
|
+
for (const entry of zip.getEntries()) {
|
|
4086
|
+
const target = resolve13(tmpDir, entry.entryName);
|
|
4087
|
+
if (!target.startsWith(tmpDir)) {
|
|
4088
|
+
return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
zip.extractAllTo(tmpDir, true);
|
|
4092
|
+
let skillDir = null;
|
|
4093
|
+
if (existsSync9(join3(tmpDir, "SKILL.md"))) {
|
|
4094
|
+
skillDir = tmpDir;
|
|
4095
|
+
} else {
|
|
4096
|
+
const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
4097
|
+
for (const entry of entries) {
|
|
4098
|
+
if (existsSync9(join3(tmpDir, entry.name, "SKILL.md"))) {
|
|
4099
|
+
skillDir = join3(tmpDir, entry.name);
|
|
4100
|
+
break;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
if (!skillDir) {
|
|
4105
|
+
return c.json({ error: "No SKILL.md found in zip (checked root and one level deep)" }, 400);
|
|
4106
|
+
}
|
|
4107
|
+
const skill = await importSkillFromDir(skillDir, "upload");
|
|
4108
|
+
return c.json(skill);
|
|
4109
|
+
} catch (e) {
|
|
4110
|
+
if (e instanceof Error && e.message.includes("Invalid skill ID")) {
|
|
4111
|
+
return c.json({ error: e.message }, 400);
|
|
4112
|
+
}
|
|
4113
|
+
throw e;
|
|
4114
|
+
} finally {
|
|
4115
|
+
rmSync3(tmpDir, { recursive: true, force: true });
|
|
4116
|
+
}
|
|
4117
|
+
}).delete("/:id", requireAdmin, async (c) => {
|
|
4118
|
+
const id = c.req.param("id");
|
|
4119
|
+
try {
|
|
4120
|
+
await removeSharedSkill(id);
|
|
4121
|
+
} catch (e) {
|
|
4122
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4123
|
+
return c.json({ error: msg }, 404);
|
|
4124
|
+
}
|
|
4125
|
+
return c.json({ ok: true });
|
|
4126
|
+
});
|
|
4127
|
+
var skills_default = app12;
|
|
4128
|
+
|
|
4129
|
+
// src/web/api/system.ts
|
|
4130
|
+
import { Hono as Hono13 } from "hono";
|
|
3454
4131
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
3455
|
-
var
|
|
4132
|
+
var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
|
|
3456
4133
|
setTimeout(() => process.exit(1), 200);
|
|
3457
4134
|
return c.json({ ok: true });
|
|
3458
4135
|
}).post("/stop", requireAdmin, (c) => {
|
|
@@ -3469,10 +4146,10 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
|
|
|
3469
4146
|
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
3470
4147
|
});
|
|
3471
4148
|
});
|
|
3472
|
-
await new Promise((
|
|
4149
|
+
await new Promise((resolve19) => {
|
|
3473
4150
|
stream.onAbort(() => {
|
|
3474
4151
|
unsubscribe();
|
|
3475
|
-
|
|
4152
|
+
resolve19();
|
|
3476
4153
|
});
|
|
3477
4154
|
});
|
|
3478
4155
|
});
|
|
@@ -3480,18 +4157,18 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
|
|
|
3480
4157
|
const config = readSystemsConfig();
|
|
3481
4158
|
return c.json({ system: config?.system ?? null });
|
|
3482
4159
|
});
|
|
3483
|
-
var system_default =
|
|
4160
|
+
var system_default = app13;
|
|
3484
4161
|
|
|
3485
|
-
// src/web/
|
|
3486
|
-
import { zValidator as
|
|
3487
|
-
import { Hono as
|
|
3488
|
-
import { z as
|
|
3489
|
-
var typingSchema =
|
|
3490
|
-
channel:
|
|
3491
|
-
sender:
|
|
3492
|
-
active:
|
|
4162
|
+
// src/web/api/typing.ts
|
|
4163
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
4164
|
+
import { Hono as Hono14 } from "hono";
|
|
4165
|
+
import { z as z5 } from "zod";
|
|
4166
|
+
var typingSchema = z5.object({
|
|
4167
|
+
channel: z5.string().min(1),
|
|
4168
|
+
sender: z5.string().min(1),
|
|
4169
|
+
active: z5.boolean()
|
|
3493
4170
|
});
|
|
3494
|
-
var
|
|
4171
|
+
var app14 = new Hono14().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
|
|
3495
4172
|
const { channel, sender, active } = c.req.valid("json");
|
|
3496
4173
|
const map = getTypingMap();
|
|
3497
4174
|
if (active) {
|
|
@@ -3508,13 +4185,13 @@ var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema)
|
|
|
3508
4185
|
const map = getTypingMap();
|
|
3509
4186
|
return c.json({ typing: map.get(channel) });
|
|
3510
4187
|
});
|
|
3511
|
-
var typing_default =
|
|
4188
|
+
var typing_default = app14;
|
|
3512
4189
|
|
|
3513
|
-
// src/web/
|
|
4190
|
+
// src/web/api/update.ts
|
|
3514
4191
|
import { spawn as spawn3 } from "child_process";
|
|
3515
|
-
import { Hono as
|
|
4192
|
+
import { Hono as Hono15 } from "hono";
|
|
3516
4193
|
var bin;
|
|
3517
|
-
var
|
|
4194
|
+
var app15 = new Hono15().get("/update", async (c) => {
|
|
3518
4195
|
const result = await checkForUpdate();
|
|
3519
4196
|
return c.json(result);
|
|
3520
4197
|
}).post("/update", requireAdmin, async (c) => {
|
|
@@ -3529,19 +4206,19 @@ var app12 = new Hono12().get("/update", async (c) => {
|
|
|
3529
4206
|
child.unref();
|
|
3530
4207
|
return c.json({ ok: true, message: "Updating..." });
|
|
3531
4208
|
});
|
|
3532
|
-
var update_default =
|
|
4209
|
+
var update_default = app15;
|
|
3533
4210
|
|
|
3534
|
-
// src/web/
|
|
3535
|
-
import { existsSync as
|
|
3536
|
-
import { resolve as
|
|
3537
|
-
import { Hono as
|
|
4211
|
+
// src/web/api/variants.ts
|
|
4212
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
4213
|
+
import { resolve as resolve15 } from "path";
|
|
4214
|
+
import { Hono as Hono16 } from "hono";
|
|
3538
4215
|
|
|
3539
4216
|
// src/lib/spawn-server.ts
|
|
3540
4217
|
import { spawn as spawn4 } from "child_process";
|
|
3541
|
-
import { closeSync, mkdirSync as
|
|
3542
|
-
import { resolve as
|
|
4218
|
+
import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync8 } from "fs";
|
|
4219
|
+
import { resolve as resolve14 } from "path";
|
|
3543
4220
|
function tsxBin(cwd) {
|
|
3544
|
-
return
|
|
4221
|
+
return resolve14(cwd, "node_modules", ".bin", "tsx");
|
|
3545
4222
|
}
|
|
3546
4223
|
function spawnServer(cwd, port, options) {
|
|
3547
4224
|
if (options?.detached) {
|
|
@@ -3554,31 +4231,31 @@ function spawnAttached(cwd, port) {
|
|
|
3554
4231
|
cwd,
|
|
3555
4232
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3556
4233
|
});
|
|
3557
|
-
return new Promise((
|
|
3558
|
-
const timeout = setTimeout(() =>
|
|
4234
|
+
return new Promise((resolve19) => {
|
|
4235
|
+
const timeout = setTimeout(() => resolve19(null), 3e4);
|
|
3559
4236
|
function checkOutput(data) {
|
|
3560
4237
|
const match = data.toString().match(/listening on :(\d+)/);
|
|
3561
4238
|
if (match) {
|
|
3562
4239
|
clearTimeout(timeout);
|
|
3563
|
-
|
|
4240
|
+
resolve19({ child, actualPort: parseInt(match[1], 10) });
|
|
3564
4241
|
}
|
|
3565
4242
|
}
|
|
3566
4243
|
child.stdout?.on("data", checkOutput);
|
|
3567
4244
|
child.stderr?.on("data", checkOutput);
|
|
3568
4245
|
child.on("error", () => {
|
|
3569
4246
|
clearTimeout(timeout);
|
|
3570
|
-
|
|
4247
|
+
resolve19(null);
|
|
3571
4248
|
});
|
|
3572
4249
|
child.on("exit", () => {
|
|
3573
4250
|
clearTimeout(timeout);
|
|
3574
|
-
|
|
4251
|
+
resolve19(null);
|
|
3575
4252
|
});
|
|
3576
4253
|
});
|
|
3577
4254
|
}
|
|
3578
4255
|
function spawnDetached(cwd, port, logDir) {
|
|
3579
|
-
const logsDir = logDir ??
|
|
3580
|
-
|
|
3581
|
-
const logPath =
|
|
4256
|
+
const logsDir = logDir ?? resolve14(cwd, ".volute", "logs");
|
|
4257
|
+
mkdirSync6(logsDir, { recursive: true });
|
|
4258
|
+
const logPath = resolve14(logsDir, "mind.log");
|
|
3582
4259
|
const logFd = openSync(logPath, "a");
|
|
3583
4260
|
const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
3584
4261
|
cwd,
|
|
@@ -3598,7 +4275,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
3598
4275
|
}
|
|
3599
4276
|
const interval = setInterval(() => {
|
|
3600
4277
|
try {
|
|
3601
|
-
const content =
|
|
4278
|
+
const content = readFileSync8(logPath, "utf-8");
|
|
3602
4279
|
const match = content.match(/listening on :(\d+)/);
|
|
3603
4280
|
if (match) {
|
|
3604
4281
|
finish({ child, actualPort: parseInt(match[1], 10) });
|
|
@@ -3649,8 +4326,8 @@ async function verify(port) {
|
|
|
3649
4326
|
}
|
|
3650
4327
|
}
|
|
3651
4328
|
|
|
3652
|
-
// src/web/
|
|
3653
|
-
var
|
|
4329
|
+
// src/web/api/variants.ts
|
|
4330
|
+
var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
3654
4331
|
const name = c.req.param("name");
|
|
3655
4332
|
const entry = findMind(name);
|
|
3656
4333
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
@@ -3680,11 +4357,11 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3680
4357
|
const err = validateBranchName(variantName);
|
|
3681
4358
|
if (err) return c.json({ error: err }, 400);
|
|
3682
4359
|
const projectRoot = mindDir(mindName);
|
|
3683
|
-
const variantDir =
|
|
3684
|
-
if (
|
|
4360
|
+
const variantDir = resolve15(projectRoot, ".variants", variantName);
|
|
4361
|
+
if (existsSync10(variantDir)) {
|
|
3685
4362
|
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
3686
4363
|
}
|
|
3687
|
-
|
|
4364
|
+
mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
|
|
3688
4365
|
try {
|
|
3689
4366
|
await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
3690
4367
|
} catch (e) {
|
|
@@ -3697,7 +4374,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3697
4374
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
3698
4375
|
await exec(cmd, args, {
|
|
3699
4376
|
cwd: variantDir,
|
|
3700
|
-
env: { ...process.env, HOME:
|
|
4377
|
+
env: { ...process.env, HOME: resolve15(variantDir, "home") }
|
|
3701
4378
|
});
|
|
3702
4379
|
} else {
|
|
3703
4380
|
await exec("npm", ["install"], { cwd: variantDir });
|
|
@@ -3707,7 +4384,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3707
4384
|
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
3708
4385
|
}
|
|
3709
4386
|
if (body.soul) {
|
|
3710
|
-
|
|
4387
|
+
writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
|
|
3711
4388
|
}
|
|
3712
4389
|
const variantPort = body.port ?? nextPort();
|
|
3713
4390
|
const variant = {
|
|
@@ -3745,7 +4422,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3745
4422
|
} catch {
|
|
3746
4423
|
}
|
|
3747
4424
|
const projectRoot = mindDir(mindName);
|
|
3748
|
-
if (
|
|
4425
|
+
if (existsSync10(variant.path)) {
|
|
3749
4426
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
3750
4427
|
if (status) {
|
|
3751
4428
|
try {
|
|
@@ -3802,7 +4479,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3802
4479
|
} catch (e) {
|
|
3803
4480
|
return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
|
|
3804
4481
|
}
|
|
3805
|
-
if (
|
|
4482
|
+
if (existsSync10(variant.path)) {
|
|
3806
4483
|
try {
|
|
3807
4484
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
3808
4485
|
} catch {
|
|
@@ -3819,7 +4496,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3819
4496
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
3820
4497
|
await exec(cmd, args, {
|
|
3821
4498
|
cwd: projectRoot,
|
|
3822
|
-
env: { ...process.env, HOME:
|
|
4499
|
+
env: { ...process.env, HOME: resolve15(projectRoot, "home") }
|
|
3823
4500
|
});
|
|
3824
4501
|
} else {
|
|
3825
4502
|
await exec("npm", ["install"], { cwd: projectRoot });
|
|
@@ -3862,7 +4539,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3862
4539
|
} catch {
|
|
3863
4540
|
}
|
|
3864
4541
|
}
|
|
3865
|
-
if (
|
|
4542
|
+
if (existsSync10(variant.path)) {
|
|
3866
4543
|
try {
|
|
3867
4544
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
3868
4545
|
} catch {
|
|
@@ -3876,29 +4553,83 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3876
4553
|
chownMindDir(projectRoot, mindName);
|
|
3877
4554
|
return c.json({ ok: true });
|
|
3878
4555
|
});
|
|
3879
|
-
var variants_default =
|
|
4556
|
+
var variants_default = app16;
|
|
3880
4557
|
|
|
3881
|
-
// src/web/
|
|
3882
|
-
import {
|
|
3883
|
-
import {
|
|
3884
|
-
import {
|
|
3885
|
-
|
|
4558
|
+
// src/web/api/volute/channels.ts
|
|
4559
|
+
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
4560
|
+
import { Hono as Hono17 } from "hono";
|
|
4561
|
+
import { z as z6 } from "zod";
|
|
4562
|
+
var createSchema = z6.object({
|
|
4563
|
+
name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
|
|
4564
|
+
});
|
|
4565
|
+
var app17 = new Hono17().get("/", async (c) => {
|
|
4566
|
+
const user = c.get("user");
|
|
4567
|
+
const channels = await listChannels();
|
|
4568
|
+
const results = await Promise.all(
|
|
4569
|
+
channels.map(async (ch) => {
|
|
4570
|
+
const participants = await getParticipants(ch.id);
|
|
4571
|
+
const isMember = participants.some((p) => p.userId === user.id);
|
|
4572
|
+
return { ...ch, participantCount: participants.length, isMember };
|
|
4573
|
+
})
|
|
4574
|
+
);
|
|
4575
|
+
return c.json(results);
|
|
4576
|
+
}).post("/", zValidator6("json", createSchema), async (c) => {
|
|
4577
|
+
const user = c.get("user");
|
|
4578
|
+
const body = c.req.valid("json");
|
|
4579
|
+
try {
|
|
4580
|
+
const ch = await createChannel(body.name, user.id);
|
|
4581
|
+
return c.json(ch, 201);
|
|
4582
|
+
} catch (err) {
|
|
4583
|
+
const cause = err instanceof Error ? err.cause : null;
|
|
4584
|
+
if (cause && /UNIQUE/i.test(cause.extendedCode ?? cause.message ?? "")) {
|
|
4585
|
+
return c.json({ error: "Channel already exists" }, 409);
|
|
4586
|
+
}
|
|
4587
|
+
throw err;
|
|
4588
|
+
}
|
|
4589
|
+
}).post("/:name/join", async (c) => {
|
|
4590
|
+
const name = c.req.param("name");
|
|
4591
|
+
const user = c.get("user");
|
|
4592
|
+
const ch = await getChannelByName(name);
|
|
4593
|
+
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4594
|
+
await joinChannel(ch.id, user.id);
|
|
4595
|
+
return c.json({ ok: true, conversationId: ch.id });
|
|
4596
|
+
}).post("/:name/leave", async (c) => {
|
|
4597
|
+
const name = c.req.param("name");
|
|
4598
|
+
const user = c.get("user");
|
|
4599
|
+
const ch = await getChannelByName(name);
|
|
4600
|
+
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4601
|
+
await leaveChannel(ch.id, user.id);
|
|
4602
|
+
return c.json({ ok: true });
|
|
4603
|
+
}).get("/:name/members", async (c) => {
|
|
4604
|
+
const name = c.req.param("name");
|
|
4605
|
+
const ch = await getChannelByName(name);
|
|
4606
|
+
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4607
|
+
const participants = await getParticipants(ch.id);
|
|
4608
|
+
return c.json(participants);
|
|
4609
|
+
});
|
|
4610
|
+
var channels_default2 = app17;
|
|
4611
|
+
|
|
4612
|
+
// src/web/api/volute/chat.ts
|
|
4613
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
4614
|
+
import { resolve as resolve16 } from "path";
|
|
4615
|
+
import { zValidator as zValidator7 } from "@hono/zod-validator";
|
|
4616
|
+
import { Hono as Hono18 } from "hono";
|
|
3886
4617
|
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
3887
|
-
import { z as
|
|
3888
|
-
var chatSchema =
|
|
3889
|
-
message:
|
|
3890
|
-
conversationId:
|
|
3891
|
-
sender:
|
|
3892
|
-
images:
|
|
3893
|
-
|
|
3894
|
-
media_type:
|
|
3895
|
-
data:
|
|
4618
|
+
import { z as z7 } from "zod";
|
|
4619
|
+
var chatSchema = z7.object({
|
|
4620
|
+
message: z7.string().optional(),
|
|
4621
|
+
conversationId: z7.string().optional(),
|
|
4622
|
+
sender: z7.string().optional(),
|
|
4623
|
+
images: z7.array(
|
|
4624
|
+
z7.object({
|
|
4625
|
+
media_type: z7.string(),
|
|
4626
|
+
data: z7.string()
|
|
3896
4627
|
})
|
|
3897
4628
|
).optional()
|
|
3898
4629
|
});
|
|
3899
4630
|
function getDaemonUrl() {
|
|
3900
4631
|
try {
|
|
3901
|
-
const data = JSON.parse(
|
|
4632
|
+
const data = JSON.parse(readFileSync9(resolve16(voluteHome(), "daemon.json"), "utf-8"));
|
|
3902
4633
|
return `http://${daemonLoopback()}:${data.port}`;
|
|
3903
4634
|
} catch (err) {
|
|
3904
4635
|
throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3914,7 +4645,7 @@ function daemonFetchInternal(path, body) {
|
|
|
3914
4645
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
3915
4646
|
return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
|
|
3916
4647
|
}
|
|
3917
|
-
var
|
|
4648
|
+
var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
|
|
3918
4649
|
const name = c.req.param("name");
|
|
3919
4650
|
const [baseName] = name.split("@", 2);
|
|
3920
4651
|
const entry = findMind(baseName);
|
|
@@ -3975,7 +4706,7 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
|
|
|
3975
4706
|
const participants = await getParticipants(conversationId);
|
|
3976
4707
|
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
3977
4708
|
const participantNames = participants.map((p) => p.username);
|
|
3978
|
-
const { getMindManager: getMindManager2 } = await import("./mind-manager-
|
|
4709
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-Z7O7PN2O.js");
|
|
3979
4710
|
const manager = getMindManager2();
|
|
3980
4711
|
const runningMinds = mindParticipants.map((ap) => {
|
|
3981
4712
|
const mindKey = ap.username === baseName ? name : ap.username;
|
|
@@ -4020,8 +4751,8 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
|
|
|
4020
4751
|
});
|
|
4021
4752
|
daemonFetchInternal(`/api/minds/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
|
|
4022
4753
|
if (!res.ok) {
|
|
4023
|
-
const
|
|
4024
|
-
console.error(`[chat] mind ${mindName} responded ${res.status}: ${
|
|
4754
|
+
const text = await res.text().catch(() => "");
|
|
4755
|
+
console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
|
|
4025
4756
|
}
|
|
4026
4757
|
}).catch((err) => {
|
|
4027
4758
|
console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
|
|
@@ -4045,27 +4776,116 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
|
|
|
4045
4776
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
4046
4777
|
});
|
|
4047
4778
|
}, 15e3);
|
|
4048
|
-
await new Promise((
|
|
4779
|
+
await new Promise((resolve19) => {
|
|
4049
4780
|
stream.onAbort(() => {
|
|
4050
4781
|
unsubscribe();
|
|
4051
4782
|
clearInterval(keepAlive);
|
|
4052
|
-
|
|
4783
|
+
resolve19();
|
|
4053
4784
|
});
|
|
4054
4785
|
});
|
|
4055
4786
|
});
|
|
4056
4787
|
});
|
|
4057
|
-
var
|
|
4788
|
+
var unifiedChatSchema = z7.object({
|
|
4789
|
+
message: z7.string().optional(),
|
|
4790
|
+
conversationId: z7.string(),
|
|
4791
|
+
images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
|
|
4792
|
+
});
|
|
4793
|
+
var unifiedChatApp = new Hono18().post(
|
|
4794
|
+
"/chat",
|
|
4795
|
+
zValidator7("json", unifiedChatSchema),
|
|
4796
|
+
async (c) => {
|
|
4797
|
+
const user = c.get("user");
|
|
4798
|
+
const body = c.req.valid("json");
|
|
4799
|
+
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
4800
|
+
return c.json({ error: "message or images required" }, 400);
|
|
4801
|
+
}
|
|
4802
|
+
const conv = await getConversation(body.conversationId);
|
|
4803
|
+
if (!conv) return c.json({ error: "Conversation not found" }, 404);
|
|
4804
|
+
if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
|
|
4805
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4806
|
+
}
|
|
4807
|
+
const senderName = user.username;
|
|
4808
|
+
const contentBlocks = [];
|
|
4809
|
+
if (body.message) contentBlocks.push({ type: "text", text: body.message });
|
|
4810
|
+
if (body.images) {
|
|
4811
|
+
for (const img of body.images) {
|
|
4812
|
+
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
await addMessage(body.conversationId, "user", senderName, contentBlocks);
|
|
4816
|
+
const participants = await getParticipants(body.conversationId);
|
|
4817
|
+
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
4818
|
+
const participantNames = participants.map((p) => p.username);
|
|
4819
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-Z7O7PN2O.js");
|
|
4820
|
+
const manager = getMindManager2();
|
|
4821
|
+
const runningMinds = mindParticipants.map((ap) => manager.isRunning(ap.username) ? ap.username : null).filter((n) => n !== null && n !== senderName);
|
|
4822
|
+
const isDM = conv.type === "dm" && participants.length === 2;
|
|
4823
|
+
const channelEntry = {
|
|
4824
|
+
platformId: body.conversationId,
|
|
4825
|
+
platform: "volute",
|
|
4826
|
+
name: conv.title ?? void 0,
|
|
4827
|
+
type: conv.type === "channel" ? "group" : isDM ? "dm" : "group"
|
|
4828
|
+
};
|
|
4829
|
+
for (const ap of mindParticipants) {
|
|
4830
|
+
const slug = buildVoluteSlug({
|
|
4831
|
+
participants,
|
|
4832
|
+
mindUsername: ap.username,
|
|
4833
|
+
convTitle: conv.title,
|
|
4834
|
+
conversationId: conv.id,
|
|
4835
|
+
convType: conv.type,
|
|
4836
|
+
convName: conv.name
|
|
4837
|
+
});
|
|
4838
|
+
try {
|
|
4839
|
+
writeChannelEntry(ap.username, slug, channelEntry);
|
|
4840
|
+
} catch (err) {
|
|
4841
|
+
console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
for (const mindName of runningMinds) {
|
|
4845
|
+
const channel = buildVoluteSlug({
|
|
4846
|
+
participants,
|
|
4847
|
+
mindUsername: mindName,
|
|
4848
|
+
convTitle: conv.title,
|
|
4849
|
+
conversationId: body.conversationId,
|
|
4850
|
+
convType: conv.type,
|
|
4851
|
+
convName: conv.name
|
|
4852
|
+
});
|
|
4853
|
+
const typingMap = getTypingMap();
|
|
4854
|
+
const currentlyTyping = typingMap.get(channel);
|
|
4855
|
+
const payload = JSON.stringify({
|
|
4856
|
+
content: contentBlocks,
|
|
4857
|
+
channel,
|
|
4858
|
+
conversationId: body.conversationId,
|
|
4859
|
+
sender: senderName,
|
|
4860
|
+
participants: participantNames,
|
|
4861
|
+
participantCount: participants.length,
|
|
4862
|
+
isDM,
|
|
4863
|
+
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
4864
|
+
});
|
|
4865
|
+
daemonFetchInternal(`/api/minds/${encodeURIComponent(mindName)}/message`, payload).then(async (res) => {
|
|
4866
|
+
if (!res.ok) {
|
|
4867
|
+
const text = await res.text().catch(() => "");
|
|
4868
|
+
console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
|
|
4869
|
+
}
|
|
4870
|
+
}).catch((err) => {
|
|
4871
|
+
console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
|
|
4872
|
+
});
|
|
4873
|
+
}
|
|
4874
|
+
return c.json({ ok: true, conversationId: body.conversationId });
|
|
4875
|
+
}
|
|
4876
|
+
);
|
|
4877
|
+
var chat_default = app18;
|
|
4058
4878
|
|
|
4059
|
-
// src/web/
|
|
4060
|
-
import { zValidator as
|
|
4061
|
-
import { Hono as
|
|
4062
|
-
import { z as
|
|
4063
|
-
var createConvSchema =
|
|
4064
|
-
title:
|
|
4065
|
-
participantIds:
|
|
4066
|
-
participantNames:
|
|
4879
|
+
// src/web/api/volute/conversations.ts
|
|
4880
|
+
import { zValidator as zValidator8 } from "@hono/zod-validator";
|
|
4881
|
+
import { Hono as Hono19 } from "hono";
|
|
4882
|
+
import { z as z8 } from "zod";
|
|
4883
|
+
var createConvSchema = z8.object({
|
|
4884
|
+
title: z8.string().optional(),
|
|
4885
|
+
participantIds: z8.array(z8.number()).optional(),
|
|
4886
|
+
participantNames: z8.array(z8.string()).optional()
|
|
4067
4887
|
});
|
|
4068
|
-
var
|
|
4888
|
+
var app19 = new Hono19().get("/:name/conversations", async (c) => {
|
|
4069
4889
|
const name = c.req.param("name");
|
|
4070
4890
|
const user = c.get("user");
|
|
4071
4891
|
let lookupId = user.id;
|
|
@@ -4074,9 +4894,9 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
|
|
|
4074
4894
|
lookupId = mindUser.id;
|
|
4075
4895
|
}
|
|
4076
4896
|
const all = await listConversationsForUser(lookupId);
|
|
4077
|
-
const convs = all.filter((c2) => c2.mind_name === name);
|
|
4897
|
+
const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
|
|
4078
4898
|
return c.json(convs);
|
|
4079
|
-
}).post("/:name/conversations",
|
|
4899
|
+
}).post("/:name/conversations", zValidator8("json", createConvSchema), async (c) => {
|
|
4080
4900
|
const name = c.req.param("name");
|
|
4081
4901
|
const user = c.get("user");
|
|
4082
4902
|
const body = c.req.valid("json");
|
|
@@ -4150,17 +4970,18 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
|
|
|
4150
4970
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
4151
4971
|
return c.json({ ok: true });
|
|
4152
4972
|
});
|
|
4153
|
-
var conversations_default =
|
|
4973
|
+
var conversations_default = app19;
|
|
4154
4974
|
|
|
4155
|
-
// src/web/
|
|
4156
|
-
import { zValidator as
|
|
4157
|
-
import { Hono as
|
|
4158
|
-
import {
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4975
|
+
// src/web/api/volute/user-conversations.ts
|
|
4976
|
+
import { zValidator as zValidator9 } from "@hono/zod-validator";
|
|
4977
|
+
import { Hono as Hono20 } from "hono";
|
|
4978
|
+
import { streamSSE as streamSSE4 } from "hono/streaming";
|
|
4979
|
+
import { z as z9 } from "zod";
|
|
4980
|
+
var createSchema2 = z9.object({
|
|
4981
|
+
title: z9.string().optional(),
|
|
4982
|
+
participantNames: z9.array(z9.string()).min(1)
|
|
4162
4983
|
});
|
|
4163
|
-
var
|
|
4984
|
+
var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
|
|
4164
4985
|
const user = c.get("user");
|
|
4165
4986
|
const convs = await listConversationsWithParticipants(user.id);
|
|
4166
4987
|
return c.json(convs);
|
|
@@ -4172,7 +4993,7 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
|
|
|
4172
4993
|
}
|
|
4173
4994
|
const msgs = await getMessages(id);
|
|
4174
4995
|
return c.json(msgs);
|
|
4175
|
-
}).post("/",
|
|
4996
|
+
}).post("/", zValidator9("json", createSchema2), async (c) => {
|
|
4176
4997
|
const user = c.get("user");
|
|
4177
4998
|
const body = c.req.valid("json");
|
|
4178
4999
|
const participantIds = /* @__PURE__ */ new Set();
|
|
@@ -4202,6 +5023,31 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
|
|
|
4202
5023
|
participantIds: [...participantIds]
|
|
4203
5024
|
});
|
|
4204
5025
|
return c.json(conv, 201);
|
|
5026
|
+
}).get("/:id/events", async (c) => {
|
|
5027
|
+
const conversationId = c.req.param("id");
|
|
5028
|
+
const user = c.get("user");
|
|
5029
|
+
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
5030
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
5031
|
+
}
|
|
5032
|
+
return streamSSE4(c, async (stream) => {
|
|
5033
|
+
const unsubscribe = subscribe(conversationId, (event) => {
|
|
5034
|
+
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
5035
|
+
if (!stream.aborted) console.error("[chat] SSE write error:", err);
|
|
5036
|
+
});
|
|
5037
|
+
});
|
|
5038
|
+
const keepAlive = setInterval(() => {
|
|
5039
|
+
stream.writeSSE({ data: "" }).catch((err) => {
|
|
5040
|
+
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
5041
|
+
});
|
|
5042
|
+
}, 15e3);
|
|
5043
|
+
await new Promise((resolve19) => {
|
|
5044
|
+
stream.onAbort(() => {
|
|
5045
|
+
unsubscribe();
|
|
5046
|
+
clearInterval(keepAlive);
|
|
5047
|
+
resolve19();
|
|
5048
|
+
});
|
|
5049
|
+
});
|
|
5050
|
+
});
|
|
4205
5051
|
}).delete("/:id", async (c) => {
|
|
4206
5052
|
const id = c.req.param("id");
|
|
4207
5053
|
const user = c.get("user");
|
|
@@ -4209,43 +5055,44 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
|
|
|
4209
5055
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
4210
5056
|
return c.json({ ok: true });
|
|
4211
5057
|
});
|
|
4212
|
-
var user_conversations_default =
|
|
5058
|
+
var user_conversations_default = app20;
|
|
4213
5059
|
|
|
4214
5060
|
// src/web/app.ts
|
|
4215
|
-
var
|
|
4216
|
-
|
|
5061
|
+
var httpLog = logger_default.child("http");
|
|
5062
|
+
var app21 = new Hono21();
|
|
5063
|
+
app21.onError((err, c) => {
|
|
4217
5064
|
if (err instanceof HTTPException) {
|
|
4218
5065
|
return err.getResponse();
|
|
4219
5066
|
}
|
|
4220
|
-
logger_default.error("
|
|
5067
|
+
logger_default.error("unhandled error", {
|
|
4221
5068
|
path: c.req.path,
|
|
4222
5069
|
method: c.req.method,
|
|
4223
|
-
error: err.message
|
|
5070
|
+
error: err.stack ?? err.message
|
|
4224
5071
|
});
|
|
4225
5072
|
return c.json({ error: "Internal server error" }, 500);
|
|
4226
5073
|
});
|
|
4227
|
-
|
|
5074
|
+
app21.notFound((c) => {
|
|
4228
5075
|
return c.json({ error: "Not found" }, 404);
|
|
4229
5076
|
});
|
|
4230
|
-
|
|
5077
|
+
app21.use("*", async (c, next) => {
|
|
4231
5078
|
const start = Date.now();
|
|
4232
5079
|
await next();
|
|
4233
5080
|
const duration = Date.now() - start;
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
}
|
|
5081
|
+
const data = { method: c.req.method, path: c.req.path, status: c.res.status, duration };
|
|
5082
|
+
if (c.res.status >= 400) {
|
|
5083
|
+
httpLog.warn("request error", data);
|
|
5084
|
+
} else {
|
|
5085
|
+
httpLog.debug("request", data);
|
|
5086
|
+
}
|
|
4240
5087
|
});
|
|
4241
|
-
|
|
5088
|
+
app21.get("/api/health", (c) => {
|
|
4242
5089
|
let version = "unknown";
|
|
4243
5090
|
let cached = null;
|
|
4244
5091
|
try {
|
|
4245
5092
|
version = getCurrentVersion();
|
|
4246
5093
|
cached = checkForUpdateCached();
|
|
4247
5094
|
} catch (err) {
|
|
4248
|
-
logger_default.
|
|
5095
|
+
logger_default.warn("health check error", { error: err.message });
|
|
4249
5096
|
}
|
|
4250
5097
|
return c.json({
|
|
4251
5098
|
ok: true,
|
|
@@ -4253,15 +5100,18 @@ app17.get("/api/health", (c) => {
|
|
|
4253
5100
|
...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
|
|
4254
5101
|
});
|
|
4255
5102
|
});
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
5103
|
+
app21.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
5104
|
+
app21.use("/api/*", csrf());
|
|
5105
|
+
app21.use("/api/minds/*", authMiddleware);
|
|
5106
|
+
app21.use("/api/conversations/*", authMiddleware);
|
|
5107
|
+
app21.use("/api/volute/*", authMiddleware);
|
|
5108
|
+
app21.use("/api/system/*", authMiddleware);
|
|
5109
|
+
app21.use("/api/env/*", authMiddleware);
|
|
5110
|
+
app21.use("/api/prompts/*", authMiddleware);
|
|
5111
|
+
app21.use("/api/skills/*", authMiddleware);
|
|
5112
|
+
app21.route("/pages", pages_default);
|
|
5113
|
+
var routes = app21.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", env_default).route("/api/minds", mind_skills_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/prompts", prompts_default).route("/api/skills", skills_default).route("/api/conversations", user_conversations_default).route("/api/volute/channels", channels_default2).route("/api/volute", unifiedChatApp);
|
|
5114
|
+
var app_default = app21;
|
|
4265
5115
|
|
|
4266
5116
|
// src/web/server.ts
|
|
4267
5117
|
var MIME_TYPES2 = {
|
|
@@ -4278,20 +5128,20 @@ async function startServer({
|
|
|
4278
5128
|
hostname = "127.0.0.1"
|
|
4279
5129
|
}) {
|
|
4280
5130
|
let assetsDir = "";
|
|
4281
|
-
let searchDir =
|
|
5131
|
+
let searchDir = dirname2(new URL(import.meta.url).pathname);
|
|
4282
5132
|
for (let i = 0; i < 5; i++) {
|
|
4283
|
-
const candidate =
|
|
4284
|
-
if (
|
|
5133
|
+
const candidate = resolve17(searchDir, "dist", "web-assets");
|
|
5134
|
+
if (existsSync11(candidate)) {
|
|
4285
5135
|
assetsDir = candidate;
|
|
4286
5136
|
break;
|
|
4287
5137
|
}
|
|
4288
|
-
searchDir =
|
|
5138
|
+
searchDir = dirname2(searchDir);
|
|
4289
5139
|
}
|
|
4290
5140
|
if (assetsDir) {
|
|
4291
5141
|
app_default.get("*", async (c) => {
|
|
4292
5142
|
const urlPath = new URL(c.req.url).pathname;
|
|
4293
5143
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
4294
|
-
const filePath =
|
|
5144
|
+
const filePath = resolve17(assetsDir, urlPath.slice(1));
|
|
4295
5145
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
4296
5146
|
const s = await stat2(filePath).catch(() => null);
|
|
4297
5147
|
if (s?.isFile()) {
|
|
@@ -4300,7 +5150,7 @@ async function startServer({
|
|
|
4300
5150
|
const body = await readFile3(filePath);
|
|
4301
5151
|
return c.body(body, 200, { "Content-Type": mime });
|
|
4302
5152
|
}
|
|
4303
|
-
const indexPath =
|
|
5153
|
+
const indexPath = resolve17(assetsDir, "index.html");
|
|
4304
5154
|
const indexStat = await stat2(indexPath).catch(() => null);
|
|
4305
5155
|
if (indexStat?.isFile()) {
|
|
4306
5156
|
const body = await readFile3(indexPath, "utf-8");
|
|
@@ -4310,10 +5160,10 @@ async function startServer({
|
|
|
4310
5160
|
});
|
|
4311
5161
|
}
|
|
4312
5162
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
4313
|
-
await new Promise((
|
|
5163
|
+
await new Promise((resolve19, reject) => {
|
|
4314
5164
|
server.on("listening", () => {
|
|
4315
5165
|
logger_default.info("Volute UI running", { hostname, port });
|
|
4316
|
-
|
|
5166
|
+
resolve19();
|
|
4317
5167
|
});
|
|
4318
5168
|
server.on("error", (err) => {
|
|
4319
5169
|
reject(err);
|
|
@@ -4324,24 +5174,26 @@ async function startServer({
|
|
|
4324
5174
|
|
|
4325
5175
|
// src/daemon.ts
|
|
4326
5176
|
if (!process.env.VOLUTE_HOME) {
|
|
4327
|
-
process.env.VOLUTE_HOME =
|
|
5177
|
+
process.env.VOLUTE_HOME = resolve18(homedir2(), ".volute");
|
|
4328
5178
|
}
|
|
4329
5179
|
async function startDaemon(opts) {
|
|
4330
5180
|
const { port, hostname } = opts;
|
|
4331
5181
|
const myPid = String(process.pid);
|
|
4332
5182
|
const home = voluteHome();
|
|
4333
5183
|
if (!opts.foreground) {
|
|
4334
|
-
const
|
|
4335
|
-
|
|
5184
|
+
const rotatingLog = new RotatingLog(resolve18(home, "daemon.log"));
|
|
5185
|
+
logger_default.setOutput((line) => rotatingLog.write(`${line}
|
|
5186
|
+
`));
|
|
5187
|
+
const write = (...args) => rotatingLog.write(`${format(...args)}
|
|
4336
5188
|
`);
|
|
4337
|
-
console.log =
|
|
4338
|
-
console.error =
|
|
4339
|
-
console.warn =
|
|
4340
|
-
console.info =
|
|
4341
|
-
}
|
|
4342
|
-
const DAEMON_PID_PATH =
|
|
4343
|
-
const DAEMON_JSON_PATH =
|
|
4344
|
-
|
|
5189
|
+
console.log = write;
|
|
5190
|
+
console.error = write;
|
|
5191
|
+
console.warn = write;
|
|
5192
|
+
console.info = write;
|
|
5193
|
+
}
|
|
5194
|
+
const DAEMON_PID_PATH = resolve18(home, "daemon.pid");
|
|
5195
|
+
const DAEMON_JSON_PATH = resolve18(home, "daemon.json");
|
|
5196
|
+
mkdirSync8(home, { recursive: true });
|
|
4345
5197
|
migrateAgentsToMinds();
|
|
4346
5198
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
4347
5199
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
@@ -4353,13 +5205,13 @@ async function startDaemon(opts) {
|
|
|
4353
5205
|
} catch (err) {
|
|
4354
5206
|
const e = err;
|
|
4355
5207
|
if (e.code === "EADDRINUSE") {
|
|
4356
|
-
|
|
5208
|
+
logger_default.error(`port ${port} is already in use`);
|
|
4357
5209
|
process.exit(1);
|
|
4358
5210
|
}
|
|
4359
5211
|
throw err;
|
|
4360
5212
|
}
|
|
4361
|
-
|
|
4362
|
-
|
|
5213
|
+
writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
5214
|
+
writeFileSync8(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
4363
5215
|
`, {
|
|
4364
5216
|
mode: 420
|
|
4365
5217
|
});
|
|
@@ -4377,7 +5229,7 @@ async function startDaemon(opts) {
|
|
|
4377
5229
|
try {
|
|
4378
5230
|
migrateMindState(entry.name);
|
|
4379
5231
|
} catch (err) {
|
|
4380
|
-
|
|
5232
|
+
logger_default.warn(`failed to migrate state for ${entry.name}`, logger_default.errorData(err));
|
|
4381
5233
|
}
|
|
4382
5234
|
}
|
|
4383
5235
|
for (const entry of registry) {
|
|
@@ -4388,9 +5240,8 @@ async function startDaemon(opts) {
|
|
|
4388
5240
|
const dir = mindDir(entry.name);
|
|
4389
5241
|
await connectors.startConnectors(entry.name, dir, entry.port, port);
|
|
4390
5242
|
scheduler.loadSchedules(entry.name);
|
|
4391
|
-
ensureMailAddress(entry.name).catch(
|
|
4392
|
-
|
|
4393
|
-
);
|
|
5243
|
+
ensureMailAddress(entry.name).catch(() => {
|
|
5244
|
+
});
|
|
4394
5245
|
const config = readVoluteConfig(dir);
|
|
4395
5246
|
if (config?.tokenBudget) {
|
|
4396
5247
|
tokenBudget.setBudget(
|
|
@@ -4400,7 +5251,7 @@ async function startDaemon(opts) {
|
|
|
4400
5251
|
);
|
|
4401
5252
|
}
|
|
4402
5253
|
} catch (err) {
|
|
4403
|
-
|
|
5254
|
+
logger_default.error(`failed to start mind ${entry.name}`, logger_default.errorData(err));
|
|
4404
5255
|
setMindRunning(entry.name, false);
|
|
4405
5256
|
}
|
|
4406
5257
|
}
|
|
@@ -4410,22 +5261,22 @@ async function startDaemon(opts) {
|
|
|
4410
5261
|
try {
|
|
4411
5262
|
await manager.startMind(compositeKey);
|
|
4412
5263
|
} catch (err) {
|
|
4413
|
-
|
|
5264
|
+
logger_default.error(`failed to start variant ${compositeKey}`, logger_default.errorData(err));
|
|
4414
5265
|
setVariantRunning(mindName, variant.name, false);
|
|
4415
5266
|
}
|
|
4416
5267
|
}
|
|
4417
5268
|
cleanExpiredSessions().catch(() => {
|
|
4418
5269
|
});
|
|
4419
|
-
|
|
5270
|
+
logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
|
|
4420
5271
|
function cleanup() {
|
|
4421
5272
|
try {
|
|
4422
|
-
if (
|
|
5273
|
+
if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
4423
5274
|
unlinkSync2(DAEMON_PID_PATH);
|
|
4424
5275
|
}
|
|
4425
5276
|
} catch {
|
|
4426
5277
|
}
|
|
4427
5278
|
try {
|
|
4428
|
-
const data = JSON.parse(
|
|
5279
|
+
const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
|
|
4429
5280
|
if (data.token === token) {
|
|
4430
5281
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
4431
5282
|
}
|
|
@@ -4436,7 +5287,7 @@ async function startDaemon(opts) {
|
|
|
4436
5287
|
async function shutdown() {
|
|
4437
5288
|
if (shuttingDown) return;
|
|
4438
5289
|
shuttingDown = true;
|
|
4439
|
-
|
|
5290
|
+
logger_default.info("shutting down...");
|
|
4440
5291
|
scheduler.stop();
|
|
4441
5292
|
scheduler.saveState();
|
|
4442
5293
|
mailPoller.stop();
|