volute 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/archive-ZCFOSTKB.js +15 -0
- package/dist/{channel-SLURLIRV.js → channel-PUQKGSQM.js} +60 -7
- package/dist/{chunk-CE7WMOVW.js → chunk-2TJGRJ4O.js} +236 -103
- package/dist/{chunk-6BDNWYKG.js → chunk-32VR2EOH.js} +2 -2
- package/dist/chunk-4KPUF5JD.js +214 -0
- package/dist/{chunk-QJIIHU32.js → chunk-7NO7EV5Z.js} +2 -2
- package/dist/chunk-AW7P4EVV.js +159 -0
- package/dist/{chunk-2Y77MCFG.js → chunk-DYZGP3EW.js} +2 -2
- package/dist/{chunk-M77QBTEH.js → chunk-EBGCNDMM.js} +24 -14
- package/dist/{chunk-GSPWIM5E.js → chunk-EMQSAY3B.js} +77 -6
- package/dist/{chunk-37X7ECMF.js → chunk-FCDU5BFX.js} +1 -1
- package/dist/chunk-FGV2H4TX.js +803 -0
- package/dist/{chunk-ZCEYUUID.js → chunk-OGXOMR65.js} +2 -1
- package/dist/chunk-OTWLI7F4.js +375 -0
- package/dist/{chunk-3FC42ZBM.js → chunk-RHEGSQFJ.js} +4 -1
- package/dist/{chunk-MVSXRMJJ.js → chunk-SCUDS4US.js} +1 -1
- package/dist/{chunk-MIJIAGGG.js → chunk-UJ6GHNR7.js} +8 -6
- package/dist/{chunk-OYSZNX5I.js → chunk-VDWCHYTS.js} +1 -1
- package/dist/{chunk-77ISBIKI.js → chunk-VE4D3GOP.js} +2 -2
- package/dist/chunk-VQWDC6UK.js +142 -0
- package/dist/{chunk-OJQ47SCA.js → chunk-WC6ZHVRL.js} +1 -1
- package/dist/chunk-YUIHSKR6.js +72 -0
- package/dist/chunk-Z524RFCJ.js +36 -0
- package/dist/cli.js +44 -24
- package/dist/{connector-3ELFMI2R.js → connector-JBVNZ7VK.js} +6 -6
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/slack.js +2 -2
- package/dist/connectors/telegram.js +2 -2
- package/dist/{create-ZWHCRT5F.js → create-HP4OVVHF.js} +6 -4
- package/dist/{daemon-client-ODKDUYDE.js → daemon-client-ITWUCNFO.js} +2 -2
- package/dist/{daemon-restart-VRQMZLBK.js → daemon-restart-JMZM3QY4.js} +8 -8
- package/dist/daemon.js +1624 -940
- package/dist/db-5ZVC6MQF.js +10 -0
- package/dist/{delete-6G6WEX4F.js → delete-BSU7K3RY.js} +1 -1
- package/dist/delivery-manager-ISTJMZDW.js +16 -0
- package/dist/down-ZY35KMHR.js +14 -0
- package/dist/{env-6IDWGBUH.js → env-A3LMO777.js} +6 -6
- package/dist/export-GCDNQCF3.js +100 -0
- package/dist/{history-5F4WQW7S.js → history-WNK3DFUM.js} +10 -7
- package/dist/{import-EDGRLIGO.js → import-M63VIUJ5.js} +3 -3
- package/dist/log-PPPZDVEF.js +39 -0
- package/dist/{login-ORQDXLBM.js → login-HNH3EUQV.js} +2 -2
- package/dist/{logout-XC5AUO5I.js → logout-I5CB5UZS.js} +2 -2
- package/dist/{logs-GYOR3L2L.js → logs-SF2IMJN4.js} +6 -6
- package/dist/merge-33C237A4.js +46 -0
- package/dist/{mind-OJN6RBZW.js → mind-PQ5NCPSU.js} +14 -10
- package/dist/mind-manager-RVCFROAY.js +18 -0
- package/dist/{package-4GTJGUXI.js → package-MYE2ZJLV.js} +7 -3
- package/dist/{pages-6IV4VQTU.js → pages-AXCOSY3P.js} +2 -2
- package/dist/{publish-Q4RPSJLL.js → publish-YB377JB7.js} +18 -4
- package/dist/pull-XAEWQJ47.js +39 -0
- package/dist/{register-LDE6LRXY.js → register-VSPCMHKX.js} +2 -2
- package/dist/{restart-YFAWFS5T.js → restart-IQKMCK5M.js} +6 -6
- package/dist/{schedule-AGYLDMNS.js → schedule-LMX7GAQZ.js} +6 -6
- package/dist/schema-5BW7DFZI.js +24 -0
- package/dist/{seed-AP4Q7RZ7.js → seed-J43YDKXG.js} +7 -4
- package/dist/{send-4GKDO26C.js → send-KVIZIGCE.js} +8 -8
- package/dist/{service-U7MZ2H7F.js → service-LUR7WDO7.js} +6 -6
- package/dist/{setup-DJKIZKGW.js → setup-OH3PJUJO.js} +7 -7
- package/dist/shared-KO35ZM44.js +39 -0
- package/dist/skill-BCVNI6TV.js +287 -0
- package/{templates/_base/_skills → dist/skills}/orientation/SKILL.md +1 -1
- package/{templates/_base/_skills → dist/skills}/sessions/SKILL.md +2 -2
- package/{templates/_base/_skills → dist/skills}/volute-mind/SKILL.md +35 -1
- package/dist/{sprout-TJ3BHVOG.js → sprout-VBEX63LX.js} +38 -20
- package/dist/{start-3YYRXBKP.js → start-I5JYB65M.js} +6 -6
- package/dist/{status-VSFZYX7S.js → status-4ESFLGH4.js} +5 -5
- package/dist/status-D7E5HHBV.js +35 -0
- package/dist/{status-OKNA6AR3.js → status-JCJAOXTW.js} +2 -2
- package/dist/{stop-AA5K5LYG.js → stop-NBVKEFQQ.js} +6 -6
- package/dist/{up-LT3X5Q26.js → up-WG65SWJU.js} +5 -5
- package/dist/{update-YAGN5ODG.js → update-FJIHDJKM.js} +5 -5
- package/dist/{update-check-APLTH4IN.js → update-check-MWE5AH4U.js} +2 -2
- package/dist/{upgrade-KXZCQSZN.js → upgrade-AIT24B5I.js} +1 -1
- package/dist/{variant-X5QFG6KK.js → variant-63ZWO2W7.js} +4 -4
- package/dist/variants-JAGWGBXG.js +26 -0
- package/dist/web-assets/assets/index-BAbuRsVF.css +1 -0
- package/dist/web-assets/assets/index-CiQhSKi_.js +63 -0
- package/dist/web-assets/index.html +2 -2
- 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/0010_delivery_queue.sql +12 -0
- package/drizzle/0011_rename_human_to_brain.sql +1 -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/0010_snapshot.json +7 -0
- package/drizzle/meta/0011_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +35 -0
- package/package.json +7 -3
- package/templates/_base/.init/.config/hooks/startup-context.sh +1 -1
- package/templates/_base/.init/.config/prompts.json +5 -0
- package/templates/_base/.init/.config/scripts/session-reader.ts +3 -3
- package/templates/_base/home/VOLUTE.md +16 -1
- package/templates/_base/src/lib/auto-commit.ts +51 -14
- package/templates/_base/src/lib/router.ts +168 -29
- package/templates/_base/src/lib/routing.ts +4 -1
- package/templates/_base/src/lib/startup.ts +43 -0
- package/templates/_base/src/lib/types.ts +4 -0
- package/templates/_base/src/lib/volute-server.ts +91 -2
- package/templates/claude/src/agent.ts +4 -3
- package/templates/claude/src/lib/hooks/reply-instructions.ts +3 -1
- package/templates/claude/src/server.ts +2 -2
- package/templates/claude/volute-template.json +1 -2
- package/templates/pi/src/agent.ts +6 -7
- package/templates/pi/src/lib/reply-instructions-extension.ts +3 -1
- package/templates/pi/src/lib/session-context-extension.ts +2 -2
- package/templates/pi/volute-template.json +1 -2
- package/dist/chunk-PO5Q2AYN.js +0 -121
- package/dist/down-A56B5JLK.js +0 -14
- package/dist/mind-manager-ETNCPQJN.js +0 -15
- package/dist/web-assets/assets/index-BcmT7Qxo.js +0 -63
- package/dist/web-assets/assets/index-DG01TyLb.css +0 -1
- /package/{templates/_base/_skills → dist/skills}/memory/SKILL.md +0 -0
package/dist/daemon.js
CHANGED
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
SEED_SKILLS,
|
|
4
|
+
STANDARD_SKILLS,
|
|
5
|
+
getSharedSkill,
|
|
6
|
+
importSkillFromDir,
|
|
7
|
+
installSkill,
|
|
8
|
+
listFilesRecursive,
|
|
9
|
+
listMindSkills,
|
|
10
|
+
listSharedSkills,
|
|
11
|
+
publishSkill,
|
|
12
|
+
removeSharedSkill,
|
|
13
|
+
sharedSkillsDir,
|
|
14
|
+
syncBuiltinSkills,
|
|
15
|
+
uninstallSkill,
|
|
16
|
+
updateSkill
|
|
17
|
+
} from "./chunk-OTWLI7F4.js";
|
|
18
|
+
import {
|
|
19
|
+
addSharedWorktree,
|
|
20
|
+
ensureSharedRepo,
|
|
21
|
+
removeSharedWorktree,
|
|
22
|
+
sharedLog,
|
|
23
|
+
sharedMerge,
|
|
24
|
+
sharedPull,
|
|
25
|
+
sharedStatus
|
|
26
|
+
} from "./chunk-4KPUF5JD.js";
|
|
9
27
|
import {
|
|
10
28
|
readSystemsConfig
|
|
11
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-FCDU5BFX.js";
|
|
30
|
+
import {
|
|
31
|
+
deliverMessage,
|
|
32
|
+
extractTextContent,
|
|
33
|
+
getDeliveryManager,
|
|
34
|
+
getTypingMap,
|
|
35
|
+
initDeliveryManager
|
|
36
|
+
} from "./chunk-FGV2H4TX.js";
|
|
12
37
|
import {
|
|
38
|
+
PROMPT_DEFAULTS,
|
|
39
|
+
PROMPT_KEYS,
|
|
40
|
+
RestartTracker,
|
|
13
41
|
RotatingLog,
|
|
14
42
|
clearJsonMap,
|
|
15
43
|
getMindManager,
|
|
44
|
+
getMindPromptDefaults,
|
|
45
|
+
getPrompt,
|
|
46
|
+
getPromptIfCustom,
|
|
16
47
|
initMindManager,
|
|
17
48
|
loadJsonMap,
|
|
49
|
+
saveJsonMap,
|
|
50
|
+
substitute
|
|
51
|
+
} from "./chunk-2TJGRJ4O.js";
|
|
52
|
+
import {
|
|
18
53
|
logBuffer,
|
|
19
|
-
logger_default
|
|
20
|
-
|
|
21
|
-
|
|
54
|
+
logger_default
|
|
55
|
+
} from "./chunk-YUIHSKR6.js";
|
|
56
|
+
import {
|
|
57
|
+
CHANNELS,
|
|
58
|
+
getChannelDriver
|
|
59
|
+
} from "./chunk-UJ6GHNR7.js";
|
|
22
60
|
import {
|
|
23
61
|
findOpenClawSession,
|
|
24
62
|
importOpenClawConnectors,
|
|
@@ -26,23 +64,32 @@ import {
|
|
|
26
64
|
parseNameFromIdentity,
|
|
27
65
|
readVoluteConfig,
|
|
28
66
|
writeVoluteConfig
|
|
29
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-EMQSAY3B.js";
|
|
30
68
|
import {
|
|
31
69
|
loadMergedEnv,
|
|
32
70
|
mindEnvPath,
|
|
33
71
|
readEnv,
|
|
34
72
|
sharedEnvPath,
|
|
35
73
|
writeEnv
|
|
36
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-VDWCHYTS.js";
|
|
37
75
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
76
|
+
getDb
|
|
77
|
+
} from "./chunk-Z524RFCJ.js";
|
|
78
|
+
import {
|
|
79
|
+
conversationParticipants,
|
|
80
|
+
conversations,
|
|
81
|
+
messages,
|
|
82
|
+
mindHistory,
|
|
83
|
+
sessions,
|
|
84
|
+
systemPrompts,
|
|
85
|
+
users
|
|
86
|
+
} from "./chunk-VQWDC6UK.js";
|
|
87
|
+
import "./chunk-D424ZQGI.js";
|
|
41
88
|
import {
|
|
42
89
|
exec,
|
|
43
90
|
gitExec,
|
|
44
91
|
resolveVoluteBin
|
|
45
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-DYZGP3EW.js";
|
|
46
93
|
import {
|
|
47
94
|
chownMindDir,
|
|
48
95
|
createMindUser,
|
|
@@ -50,17 +97,16 @@ import {
|
|
|
50
97
|
ensureVoluteGroup,
|
|
51
98
|
isIsolationEnabled,
|
|
52
99
|
wrapForIsolation
|
|
53
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-OGXOMR65.js";
|
|
54
101
|
import {
|
|
55
102
|
checkForUpdate,
|
|
56
103
|
checkForUpdateCached,
|
|
57
104
|
getCurrentVersion
|
|
58
|
-
} from "./chunk-
|
|
59
|
-
import "./chunk-D424ZQGI.js";
|
|
105
|
+
} from "./chunk-SCUDS4US.js";
|
|
60
106
|
import {
|
|
61
107
|
buildVoluteSlug,
|
|
62
108
|
writeChannelEntry
|
|
63
|
-
} from "./chunk-
|
|
109
|
+
} from "./chunk-RHEGSQFJ.js";
|
|
64
110
|
import {
|
|
65
111
|
addMind,
|
|
66
112
|
addVariant,
|
|
@@ -70,6 +116,7 @@ import {
|
|
|
70
116
|
findMind,
|
|
71
117
|
findVariant,
|
|
72
118
|
getAllRunningVariants,
|
|
119
|
+
initRegistryCache,
|
|
73
120
|
mindDir,
|
|
74
121
|
nextPort,
|
|
75
122
|
readRegistry,
|
|
@@ -84,16 +131,14 @@ import {
|
|
|
84
131
|
validateBranchName,
|
|
85
132
|
validateMindName,
|
|
86
133
|
voluteHome
|
|
87
|
-
} from "./chunk-
|
|
88
|
-
import
|
|
89
|
-
__export
|
|
90
|
-
} from "./chunk-K3NQKI34.js";
|
|
134
|
+
} from "./chunk-EBGCNDMM.js";
|
|
135
|
+
import "./chunk-K3NQKI34.js";
|
|
91
136
|
|
|
92
137
|
// src/daemon.ts
|
|
93
138
|
import { randomBytes } from "crypto";
|
|
94
|
-
import { mkdirSync as
|
|
139
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
95
140
|
import { homedir as homedir2 } from "os";
|
|
96
|
-
import { resolve as
|
|
141
|
+
import { resolve as resolve19 } from "path";
|
|
97
142
|
import { format } from "util";
|
|
98
143
|
|
|
99
144
|
// src/lib/connector-manager.ts
|
|
@@ -184,16 +229,12 @@ function searchUpwards(...segments) {
|
|
|
184
229
|
}
|
|
185
230
|
return null;
|
|
186
231
|
}
|
|
187
|
-
var MAX_RESTART_ATTEMPTS = 5;
|
|
188
|
-
var BASE_RESTART_DELAY = 3e3;
|
|
189
|
-
var MAX_RESTART_DELAY = 6e4;
|
|
190
232
|
var ConnectorManager = class {
|
|
191
233
|
connectors = /* @__PURE__ */ new Map();
|
|
192
234
|
stopping = /* @__PURE__ */ new Set();
|
|
193
235
|
// "mind:type" keys currently being explicitly stopped
|
|
194
236
|
shuttingDown = false;
|
|
195
|
-
|
|
196
|
-
// "mind:type" -> count
|
|
237
|
+
restartTracker = new RestartTracker();
|
|
197
238
|
async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
|
|
198
239
|
const config = readVoluteConfig(mindDir2) ?? {};
|
|
199
240
|
const types = config.connectors ?? [];
|
|
@@ -306,7 +347,7 @@ var ConnectorManager = class {
|
|
|
306
347
|
}
|
|
307
348
|
this.connectors.get(mindName).set(type, { child, type });
|
|
308
349
|
const stopKey = `${mindName}:${type}`;
|
|
309
|
-
this.
|
|
350
|
+
this.restartTracker.reset(stopKey);
|
|
310
351
|
child.on("exit", (code) => {
|
|
311
352
|
const mindMap = this.connectors.get(mindName);
|
|
312
353
|
if (mindMap?.get(type)?.child === child) {
|
|
@@ -316,15 +357,13 @@ var ConnectorManager = class {
|
|
|
316
357
|
if (this.stopping.has(stopKey)) return;
|
|
317
358
|
clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
|
|
318
359
|
if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
321
|
-
clog.error(`connector ${type} for ${mindName} crashed ${
|
|
360
|
+
const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(stopKey);
|
|
361
|
+
if (!shouldRestart) {
|
|
362
|
+
clog.error(`connector ${type} for ${mindName} crashed ${attempt} times \u2014 giving up`);
|
|
322
363
|
return;
|
|
323
364
|
}
|
|
324
|
-
const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
|
|
325
|
-
this.restartAttempts.set(stopKey, attempts + 1);
|
|
326
365
|
clog.info(
|
|
327
|
-
`restarting connector ${type} for ${mindName} \u2014 attempt ${
|
|
366
|
+
`restarting connector ${type} for ${mindName} \u2014 attempt ${attempt}/${this.restartTracker.maxRestartAttempts}, in ${delay}ms`
|
|
328
367
|
);
|
|
329
368
|
setTimeout(() => {
|
|
330
369
|
if (this.shuttingDown || this.stopping.has(stopKey)) return;
|
|
@@ -343,23 +382,23 @@ var ConnectorManager = class {
|
|
|
343
382
|
const stopKey = `${mindName}:${type}`;
|
|
344
383
|
this.stopping.add(stopKey);
|
|
345
384
|
mindMap.delete(type);
|
|
346
|
-
await new Promise((
|
|
347
|
-
tracked.child.on("exit", () =>
|
|
385
|
+
await new Promise((resolve20) => {
|
|
386
|
+
tracked.child.on("exit", () => resolve20());
|
|
348
387
|
try {
|
|
349
388
|
tracked.child.kill("SIGTERM");
|
|
350
389
|
} catch {
|
|
351
|
-
|
|
390
|
+
resolve20();
|
|
352
391
|
}
|
|
353
392
|
setTimeout(() => {
|
|
354
393
|
try {
|
|
355
394
|
tracked.child.kill("SIGKILL");
|
|
356
395
|
} catch {
|
|
357
396
|
}
|
|
358
|
-
|
|
397
|
+
resolve20();
|
|
359
398
|
}, 5e3);
|
|
360
399
|
});
|
|
361
400
|
this.stopping.delete(stopKey);
|
|
362
|
-
this.
|
|
401
|
+
this.restartTracker.reset(stopKey);
|
|
363
402
|
try {
|
|
364
403
|
this.removeConnectorPid(mindName, type);
|
|
365
404
|
} catch (err) {
|
|
@@ -431,7 +470,8 @@ function initConnectorManager() {
|
|
|
431
470
|
return instance;
|
|
432
471
|
}
|
|
433
472
|
function getConnectorManager() {
|
|
434
|
-
if (!instance)
|
|
473
|
+
if (!instance)
|
|
474
|
+
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
435
475
|
return instance;
|
|
436
476
|
}
|
|
437
477
|
|
|
@@ -455,15 +495,13 @@ var INITIAL_RECONNECT_MS = 1e3;
|
|
|
455
495
|
var MAX_RECONNECT_MS = 6e4;
|
|
456
496
|
var MailPoller = class {
|
|
457
497
|
ws = null;
|
|
458
|
-
daemonPort = null;
|
|
459
|
-
daemonToken = null;
|
|
460
498
|
running = false;
|
|
461
499
|
pingTimer = null;
|
|
462
500
|
reconnectTimer = null;
|
|
463
501
|
reconnectDelay = INITIAL_RECONNECT_MS;
|
|
464
502
|
reconnectAttempts = 0;
|
|
465
503
|
disconnectedAt = null;
|
|
466
|
-
start(
|
|
504
|
+
start() {
|
|
467
505
|
if (this.running) {
|
|
468
506
|
mlog.warn("already running \u2014 ignoring duplicate start");
|
|
469
507
|
return;
|
|
@@ -473,8 +511,6 @@ var MailPoller = class {
|
|
|
473
511
|
mlog.info("no systems config \u2014 mail disabled");
|
|
474
512
|
return;
|
|
475
513
|
}
|
|
476
|
-
this.daemonPort = daemonPort ?? null;
|
|
477
|
-
this.daemonToken = daemonToken ?? null;
|
|
478
514
|
this.running = true;
|
|
479
515
|
this.connect();
|
|
480
516
|
}
|
|
@@ -630,51 +666,29 @@ var MailPoller = class {
|
|
|
630
666
|
mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
|
|
631
667
|
return;
|
|
632
668
|
}
|
|
633
|
-
const
|
|
634
|
-
const sender = email.from.name || email.from.address;
|
|
635
|
-
const text2 = formatEmailContent(email);
|
|
636
|
-
const body = JSON.stringify({
|
|
637
|
-
content: [{ type: "text", text: text2 }],
|
|
638
|
-
channel,
|
|
639
|
-
sender,
|
|
640
|
-
platform: "Email",
|
|
641
|
-
isDM: true
|
|
642
|
-
});
|
|
643
|
-
if (!this.daemonPort || !this.daemonToken) {
|
|
644
|
-
mlog.warn(`cannot deliver to ${mind}: daemon port/token not set`);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
|
|
648
|
-
const controller = new AbortController();
|
|
649
|
-
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
669
|
+
const text = formatEmailContent(email);
|
|
650
670
|
try {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
},
|
|
658
|
-
body,
|
|
659
|
-
signal: controller.signal
|
|
660
|
-
});
|
|
661
|
-
if (!res.ok) {
|
|
662
|
-
mlog.warn(`deliver to ${mind} got HTTP ${res.status}`);
|
|
663
|
-
} else {
|
|
664
|
-
mlog.info(`delivered email from ${email.from.address} to ${mind}`);
|
|
665
|
-
}
|
|
666
|
-
await res.text().catch(() => {
|
|
671
|
+
await deliverMessage(mind, {
|
|
672
|
+
content: [{ type: "text", text }],
|
|
673
|
+
channel: `mail:${email.from.address}`,
|
|
674
|
+
sender: email.from.name || email.from.address,
|
|
675
|
+
platform: "Email",
|
|
676
|
+
isDM: true
|
|
667
677
|
});
|
|
678
|
+
mlog.info(`delivered email from ${email.from.address} to ${mind}`);
|
|
668
679
|
} catch (err) {
|
|
669
680
|
mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
|
|
670
|
-
} finally {
|
|
671
|
-
clearTimeout(timeout);
|
|
672
681
|
}
|
|
673
682
|
}
|
|
674
683
|
};
|
|
675
684
|
var instance2 = null;
|
|
685
|
+
function initMailPoller() {
|
|
686
|
+
if (instance2) throw new Error("MailPoller already initialized");
|
|
687
|
+
instance2 = new MailPoller();
|
|
688
|
+
return instance2;
|
|
689
|
+
}
|
|
676
690
|
function getMailPoller() {
|
|
677
|
-
if (!instance2)
|
|
691
|
+
if (!instance2) throw new Error("MailPoller not initialized \u2014 call initMailPoller() first");
|
|
678
692
|
return instance2;
|
|
679
693
|
}
|
|
680
694
|
async function ensureMailAddress(mindName) {
|
|
@@ -833,10 +847,20 @@ function migrateProfileScript() {
|
|
|
833
847
|
}
|
|
834
848
|
|
|
835
849
|
// src/lib/migrate-state.ts
|
|
836
|
-
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
850
|
+
import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, renameSync as renameSync2 } from "fs";
|
|
837
851
|
import { resolve as resolve4 } from "path";
|
|
852
|
+
function migrateDotVoluteDir(name) {
|
|
853
|
+
const dir = mindDir(name);
|
|
854
|
+
const oldDir = resolve4(dir, ".volute");
|
|
855
|
+
const newDir = resolve4(dir, ".mind");
|
|
856
|
+
if (existsSync4(oldDir) && !existsSync4(newDir)) {
|
|
857
|
+
renameSync2(oldDir, newDir);
|
|
858
|
+
} else if (existsSync4(oldDir) && existsSync4(newDir)) {
|
|
859
|
+
console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
838
862
|
function migrateMindState(name) {
|
|
839
|
-
const src = resolve4(mindDir(name), ".
|
|
863
|
+
const src = resolve4(mindDir(name), ".mind");
|
|
840
864
|
if (!existsSync4(src)) return;
|
|
841
865
|
const dest = stateDir(name);
|
|
842
866
|
mkdirSync2(dest, { recursive: true });
|
|
@@ -870,14 +894,10 @@ var Scheduler = class {
|
|
|
870
894
|
interval = null;
|
|
871
895
|
lastFired = /* @__PURE__ */ new Map();
|
|
872
896
|
// "mind:scheduleId" → epoch minute
|
|
873
|
-
daemonPort = null;
|
|
874
|
-
daemonToken = null;
|
|
875
897
|
get statePath() {
|
|
876
898
|
return resolve5(voluteHome(), "scheduler-state.json");
|
|
877
899
|
}
|
|
878
|
-
start(
|
|
879
|
-
this.daemonPort = daemonPort ?? null;
|
|
880
|
-
this.daemonToken = daemonToken ?? null;
|
|
900
|
+
start() {
|
|
881
901
|
this.loadState();
|
|
882
902
|
this.interval = setInterval(() => this.tick(), 6e4);
|
|
883
903
|
}
|
|
@@ -908,9 +928,6 @@ var Scheduler = class {
|
|
|
908
928
|
this.schedules.delete(mindName);
|
|
909
929
|
}
|
|
910
930
|
tick() {
|
|
911
|
-
for (const mind of this.schedules.keys()) {
|
|
912
|
-
this.loadSchedules(mind);
|
|
913
|
-
}
|
|
914
931
|
const now = /* @__PURE__ */ new Date();
|
|
915
932
|
for (const [mind, schedules] of this.schedules) {
|
|
916
933
|
for (const schedule of schedules) {
|
|
@@ -941,69 +958,39 @@ var Scheduler = class {
|
|
|
941
958
|
}
|
|
942
959
|
}
|
|
943
960
|
async fire(mindName, schedule) {
|
|
944
|
-
const entry = findMind(mindName);
|
|
945
|
-
if (!entry) return;
|
|
946
|
-
const body = JSON.stringify({
|
|
947
|
-
content: [{ type: "text", text: schedule.message }],
|
|
948
|
-
channel: "system:scheduler",
|
|
949
|
-
sender: schedule.id
|
|
950
|
-
});
|
|
951
|
-
const controller = new AbortController();
|
|
952
|
-
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
953
961
|
try {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
method: "POST",
|
|
959
|
-
headers: {
|
|
960
|
-
"Content-Type": "application/json",
|
|
961
|
-
Authorization: `Bearer ${this.daemonToken}`,
|
|
962
|
-
Origin: daemonUrl
|
|
963
|
-
},
|
|
964
|
-
body,
|
|
965
|
-
signal: controller.signal
|
|
966
|
-
});
|
|
967
|
-
} else {
|
|
968
|
-
res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
969
|
-
method: "POST",
|
|
970
|
-
headers: { "Content-Type": "application/json" },
|
|
971
|
-
body,
|
|
972
|
-
signal: controller.signal
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
if (!res.ok) {
|
|
976
|
-
slog.warn(`"${schedule.id}" for ${mindName} got HTTP ${res.status}`);
|
|
977
|
-
} else {
|
|
978
|
-
slog.info(`fired "${schedule.id}" for ${mindName}`);
|
|
979
|
-
}
|
|
980
|
-
await res.text().catch(() => {
|
|
962
|
+
await deliverMessage(mindName, {
|
|
963
|
+
content: [{ type: "text", text: schedule.message }],
|
|
964
|
+
channel: "system:scheduler",
|
|
965
|
+
sender: schedule.id
|
|
981
966
|
});
|
|
967
|
+
slog.info(`fired "${schedule.id}" for ${mindName}`);
|
|
982
968
|
} catch (err) {
|
|
983
969
|
slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
984
|
-
} finally {
|
|
985
|
-
clearTimeout(timeout);
|
|
986
970
|
}
|
|
987
971
|
}
|
|
988
972
|
};
|
|
989
973
|
var instance3 = null;
|
|
974
|
+
function initScheduler() {
|
|
975
|
+
if (instance3) throw new Error("Scheduler already initialized");
|
|
976
|
+
instance3 = new Scheduler();
|
|
977
|
+
return instance3;
|
|
978
|
+
}
|
|
990
979
|
function getScheduler() {
|
|
991
|
-
if (!instance3)
|
|
980
|
+
if (!instance3) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
|
|
992
981
|
return instance3;
|
|
993
982
|
}
|
|
994
983
|
|
|
995
984
|
// src/lib/token-budget.ts
|
|
985
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
986
|
+
import { resolve as resolve6 } from "path";
|
|
996
987
|
var tlog = logger_default.child("token-budget");
|
|
997
988
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
998
989
|
var MAX_QUEUE_SIZE = 100;
|
|
999
990
|
var TokenBudget = class {
|
|
1000
991
|
budgets = /* @__PURE__ */ new Map();
|
|
1001
992
|
interval = null;
|
|
1002
|
-
|
|
1003
|
-
daemonToken = null;
|
|
1004
|
-
start(daemonPort, daemonToken) {
|
|
1005
|
-
this.daemonPort = daemonPort ?? null;
|
|
1006
|
-
this.daemonToken = daemonToken ?? null;
|
|
993
|
+
start() {
|
|
1007
994
|
this.interval = setInterval(() => this.tick(), 6e4);
|
|
1008
995
|
}
|
|
1009
996
|
stop() {
|
|
@@ -1017,14 +1004,21 @@ var TokenBudget = class {
|
|
|
1017
1004
|
existing.tokenLimit = tokenLimit;
|
|
1018
1005
|
existing.periodMinutes = periodMinutes;
|
|
1019
1006
|
} else {
|
|
1020
|
-
this.
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
periodMinutes
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1007
|
+
const persisted = this.loadBudgetState(mind);
|
|
1008
|
+
if (persisted) {
|
|
1009
|
+
persisted.tokenLimit = tokenLimit;
|
|
1010
|
+
persisted.periodMinutes = periodMinutes;
|
|
1011
|
+
this.budgets.set(mind, persisted);
|
|
1012
|
+
} else {
|
|
1013
|
+
this.budgets.set(mind, {
|
|
1014
|
+
tokensUsed: 0,
|
|
1015
|
+
periodStart: Date.now(),
|
|
1016
|
+
periodMinutes,
|
|
1017
|
+
tokenLimit,
|
|
1018
|
+
queue: [],
|
|
1019
|
+
warningInjected: false
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1028
1022
|
}
|
|
1029
1023
|
}
|
|
1030
1024
|
removeBudget(mind) {
|
|
@@ -1034,6 +1028,7 @@ var TokenBudget = class {
|
|
|
1034
1028
|
const state = this.budgets.get(mind);
|
|
1035
1029
|
if (!state) return;
|
|
1036
1030
|
state.tokensUsed += inputTokens + outputTokens;
|
|
1031
|
+
this.saveBudgetState(mind, state);
|
|
1037
1032
|
}
|
|
1038
1033
|
/** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
|
|
1039
1034
|
checkBudget(mind) {
|
|
@@ -1084,6 +1079,7 @@ var TokenBudget = class {
|
|
|
1084
1079
|
state.tokensUsed = 0;
|
|
1085
1080
|
state.periodStart = now;
|
|
1086
1081
|
state.warningInjected = false;
|
|
1082
|
+
this.saveBudgetState(mind, state);
|
|
1087
1083
|
const queued = this.drain(mind);
|
|
1088
1084
|
if (queued.length > 0) {
|
|
1089
1085
|
this.replay(mind, queued).catch((err) => {
|
|
@@ -1093,68 +1089,117 @@ var TokenBudget = class {
|
|
|
1093
1089
|
}
|
|
1094
1090
|
}
|
|
1095
1091
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1092
|
+
budgetStatePath(mind) {
|
|
1093
|
+
return resolve6(stateDir(mind), "budget.json");
|
|
1094
|
+
}
|
|
1095
|
+
saveBudgetState(mind, state) {
|
|
1096
|
+
try {
|
|
1097
|
+
const dir = stateDir(mind);
|
|
1098
|
+
mkdirSync3(dir, { recursive: true });
|
|
1099
|
+
const data = {
|
|
1100
|
+
periodStart: state.periodStart,
|
|
1101
|
+
tokensUsed: state.tokensUsed,
|
|
1102
|
+
warningInjected: state.warningInjected,
|
|
1103
|
+
queue: state.queue
|
|
1104
|
+
};
|
|
1105
|
+
writeFileSync3(this.budgetStatePath(mind), `${JSON.stringify(data)}
|
|
1106
|
+
`);
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
loadBudgetState(mind) {
|
|
1112
|
+
try {
|
|
1113
|
+
const path = this.budgetStatePath(mind);
|
|
1114
|
+
if (!existsSync5(path)) return null;
|
|
1115
|
+
const data = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1116
|
+
if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
|
|
1117
|
+
return {
|
|
1118
|
+
periodStart: data.periodStart,
|
|
1119
|
+
tokensUsed: data.tokensUsed,
|
|
1120
|
+
warningInjected: data.warningInjected ?? false,
|
|
1121
|
+
queue: Array.isArray(data.queue) ? data.queue : [],
|
|
1122
|
+
periodMinutes: 0,
|
|
1123
|
+
// will be overwritten by caller
|
|
1124
|
+
tokenLimit: 0
|
|
1125
|
+
// will be overwritten by caller
|
|
1126
|
+
};
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
tlog.warn(`failed to load budget state for ${mind}`, logger_default.errorData(err));
|
|
1129
|
+
return null;
|
|
1104
1130
|
}
|
|
1131
|
+
}
|
|
1132
|
+
async replay(mindName, messages2) {
|
|
1105
1133
|
const summary = messages2.map((m) => {
|
|
1106
1134
|
const from = m.sender ? `[${m.sender}]` : "";
|
|
1107
1135
|
const ch = m.channel ? `(${m.channel})` : "";
|
|
1108
1136
|
return `${from}${ch} ${m.textContent}`;
|
|
1109
1137
|
}).join("\n");
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1138
|
+
try {
|
|
1139
|
+
await deliverMessage(mindName, {
|
|
1140
|
+
content: [
|
|
1141
|
+
{
|
|
1142
|
+
type: "text",
|
|
1143
|
+
text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
|
|
1115
1144
|
|
|
1116
1145
|
${summary}`
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
});
|
|
1122
|
-
const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
|
|
1123
|
-
const controller = new AbortController();
|
|
1124
|
-
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
1125
|
-
try {
|
|
1126
|
-
const res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mindName)}/message`, {
|
|
1127
|
-
method: "POST",
|
|
1128
|
-
headers: {
|
|
1129
|
-
"Content-Type": "application/json",
|
|
1130
|
-
Authorization: `Bearer ${this.daemonToken}`,
|
|
1131
|
-
Origin: daemonUrl
|
|
1132
|
-
},
|
|
1133
|
-
body,
|
|
1134
|
-
signal: controller.signal
|
|
1135
|
-
});
|
|
1136
|
-
if (!res.ok) {
|
|
1137
|
-
tlog.warn(`replay for ${mindName} got HTTP ${res.status}`);
|
|
1138
|
-
} else {
|
|
1139
|
-
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
1140
|
-
}
|
|
1141
|
-
await res.text().catch(() => {
|
|
1146
|
+
}
|
|
1147
|
+
],
|
|
1148
|
+
channel: "system:budget-replay",
|
|
1149
|
+
sender: "system"
|
|
1142
1150
|
});
|
|
1151
|
+
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
1143
1152
|
} catch (err) {
|
|
1144
1153
|
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
1145
1154
|
const state = this.budgets.get(mindName);
|
|
1146
1155
|
if (state) state.queue.push(...messages2);
|
|
1147
|
-
} finally {
|
|
1148
|
-
clearTimeout(timeout);
|
|
1149
1156
|
}
|
|
1150
1157
|
}
|
|
1151
1158
|
};
|
|
1152
1159
|
var instance4 = null;
|
|
1160
|
+
function initTokenBudget() {
|
|
1161
|
+
if (instance4) throw new Error("TokenBudget already initialized");
|
|
1162
|
+
instance4 = new TokenBudget();
|
|
1163
|
+
return instance4;
|
|
1164
|
+
}
|
|
1153
1165
|
function getTokenBudget() {
|
|
1154
|
-
if (!instance4)
|
|
1166
|
+
if (!instance4) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
|
|
1155
1167
|
return instance4;
|
|
1156
1168
|
}
|
|
1157
1169
|
|
|
1170
|
+
// src/lib/mind-service.ts
|
|
1171
|
+
async function startMindFull(name) {
|
|
1172
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
1173
|
+
await getMindManager().startMind(name);
|
|
1174
|
+
if (variantName) return;
|
|
1175
|
+
const entry = findMind(baseName);
|
|
1176
|
+
if (!entry || entry.stage === "seed") return;
|
|
1177
|
+
const dir = mindDir(baseName);
|
|
1178
|
+
const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
|
|
1179
|
+
await getConnectorManager().startConnectors(baseName, dir, entry.port, daemonPort);
|
|
1180
|
+
getScheduler().loadSchedules(baseName);
|
|
1181
|
+
ensureMailAddress(baseName).catch(
|
|
1182
|
+
(err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
|
|
1183
|
+
);
|
|
1184
|
+
const config = readVoluteConfig(dir);
|
|
1185
|
+
if (config?.tokenBudget) {
|
|
1186
|
+
getTokenBudget().setBudget(
|
|
1187
|
+
baseName,
|
|
1188
|
+
config.tokenBudget,
|
|
1189
|
+
config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
async function stopMindFull(name) {
|
|
1194
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
1195
|
+
if (!variantName) {
|
|
1196
|
+
await getConnectorManager().stopConnectors(baseName);
|
|
1197
|
+
getScheduler().unloadSchedules(baseName);
|
|
1198
|
+
getTokenBudget().removeBudget(baseName);
|
|
1199
|
+
}
|
|
1200
|
+
await getMindManager().stopMind(name);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1158
1203
|
// src/web/middleware/auth.ts
|
|
1159
1204
|
import { timingSafeEqual } from "crypto";
|
|
1160
1205
|
import { eq as eq2, lt } from "drizzle-orm";
|
|
@@ -1164,129 +1209,12 @@ import { createMiddleware } from "hono/factory";
|
|
|
1164
1209
|
// src/lib/auth.ts
|
|
1165
1210
|
import { compareSync, hashSync } from "bcryptjs";
|
|
1166
1211
|
import { and, count, eq } from "drizzle-orm";
|
|
1167
|
-
|
|
1168
|
-
// src/lib/db.ts
|
|
1169
|
-
import { chmodSync, existsSync as existsSync5 } from "fs";
|
|
1170
|
-
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
1171
|
-
import { fileURLToPath } from "url";
|
|
1172
|
-
import { drizzle } from "drizzle-orm/libsql";
|
|
1173
|
-
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
1174
|
-
|
|
1175
|
-
// src/lib/schema.ts
|
|
1176
|
-
var schema_exports = {};
|
|
1177
|
-
__export(schema_exports, {
|
|
1178
|
-
conversationParticipants: () => conversationParticipants,
|
|
1179
|
-
conversations: () => conversations,
|
|
1180
|
-
messages: () => messages,
|
|
1181
|
-
mindHistory: () => mindHistory,
|
|
1182
|
-
sessions: () => sessions,
|
|
1183
|
-
users: () => users
|
|
1184
|
-
});
|
|
1185
|
-
import { sql } from "drizzle-orm";
|
|
1186
|
-
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
1187
|
-
var users = sqliteTable("users", {
|
|
1188
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
1189
|
-
username: text("username").unique().notNull(),
|
|
1190
|
-
password_hash: text("password_hash").notNull(),
|
|
1191
|
-
role: text("role").notNull().default("pending"),
|
|
1192
|
-
user_type: text("user_type").notNull().default("human"),
|
|
1193
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
1194
|
-
});
|
|
1195
|
-
var conversations = sqliteTable(
|
|
1196
|
-
"conversations",
|
|
1197
|
-
{
|
|
1198
|
-
id: text("id").primaryKey(),
|
|
1199
|
-
mind_name: text("mind_name").notNull(),
|
|
1200
|
-
channel: text("channel").notNull(),
|
|
1201
|
-
user_id: integer("user_id").references(() => users.id),
|
|
1202
|
-
title: text("title"),
|
|
1203
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
1204
|
-
updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
1205
|
-
},
|
|
1206
|
-
(table) => [
|
|
1207
|
-
index("idx_conversations_mind_name").on(table.mind_name),
|
|
1208
|
-
index("idx_conversations_user_id").on(table.user_id),
|
|
1209
|
-
index("idx_conversations_updated_at").on(table.updated_at)
|
|
1210
|
-
]
|
|
1211
|
-
);
|
|
1212
|
-
var mindHistory = sqliteTable(
|
|
1213
|
-
"mind_history",
|
|
1214
|
-
{
|
|
1215
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
1216
|
-
mind: text("mind").notNull(),
|
|
1217
|
-
channel: text("channel"),
|
|
1218
|
-
session: text("session"),
|
|
1219
|
-
sender: text("sender"),
|
|
1220
|
-
message_id: text("message_id"),
|
|
1221
|
-
type: text("type").notNull(),
|
|
1222
|
-
content: text("content"),
|
|
1223
|
-
metadata: text("metadata"),
|
|
1224
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
1225
|
-
},
|
|
1226
|
-
(table) => [
|
|
1227
|
-
index("idx_mind_history_mind").on(table.mind),
|
|
1228
|
-
index("idx_mind_history_mind_channel").on(table.mind, table.channel),
|
|
1229
|
-
index("idx_mind_history_mind_type").on(table.mind, table.type)
|
|
1230
|
-
]
|
|
1231
|
-
);
|
|
1232
|
-
var conversationParticipants = sqliteTable(
|
|
1233
|
-
"conversation_participants",
|
|
1234
|
-
{
|
|
1235
|
-
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
1236
|
-
user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
1237
|
-
role: text("role").notNull().default("member"),
|
|
1238
|
-
joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
|
|
1239
|
-
},
|
|
1240
|
-
(table) => [
|
|
1241
|
-
uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
|
|
1242
|
-
index("idx_cp_user_id").on(table.user_id)
|
|
1243
|
-
]
|
|
1244
|
-
);
|
|
1245
|
-
var sessions = sqliteTable("sessions", {
|
|
1246
|
-
id: text("id").primaryKey(),
|
|
1247
|
-
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
|
|
1248
|
-
createdAt: integer("created_at").notNull()
|
|
1249
|
-
});
|
|
1250
|
-
var messages = sqliteTable(
|
|
1251
|
-
"messages",
|
|
1252
|
-
{
|
|
1253
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
1254
|
-
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
1255
|
-
role: text("role").notNull(),
|
|
1256
|
-
sender_name: text("sender_name"),
|
|
1257
|
-
content: text("content").notNull(),
|
|
1258
|
-
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
1259
|
-
},
|
|
1260
|
-
(table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
// src/lib/db.ts
|
|
1264
|
-
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1265
|
-
var migrationsFolder = existsSync5(resolve6(__dirname, "../drizzle")) ? resolve6(__dirname, "../drizzle") : resolve6(__dirname, "../../drizzle");
|
|
1266
|
-
var db = null;
|
|
1267
|
-
async function getDb() {
|
|
1268
|
-
if (db) return db;
|
|
1269
|
-
const dbPath = process.env.VOLUTE_DB_PATH || resolve6(voluteHome(), "volute.db");
|
|
1270
|
-
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
1271
|
-
await migrate(db, { migrationsFolder });
|
|
1272
|
-
try {
|
|
1273
|
-
chmodSync(dbPath, 384);
|
|
1274
|
-
} catch (err) {
|
|
1275
|
-
console.error(
|
|
1276
|
-
`[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
|
|
1277
|
-
err
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
return db;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// src/lib/auth.ts
|
|
1284
1212
|
async function createUser(username, password) {
|
|
1285
|
-
const
|
|
1213
|
+
const db = await getDb();
|
|
1286
1214
|
const hash = hashSync(password, 10);
|
|
1287
|
-
const [{ value }] = await
|
|
1215
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
|
|
1288
1216
|
const role = value === 0 ? "admin" : "pending";
|
|
1289
|
-
const [result] = await
|
|
1217
|
+
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
1290
1218
|
id: users.id,
|
|
1291
1219
|
username: users.username,
|
|
1292
1220
|
role: users.role,
|
|
@@ -1296,8 +1224,8 @@ async function createUser(username, password) {
|
|
|
1296
1224
|
return result;
|
|
1297
1225
|
}
|
|
1298
1226
|
async function verifyUser(username, password) {
|
|
1299
|
-
const
|
|
1300
|
-
const row = await
|
|
1227
|
+
const db = await getDb();
|
|
1228
|
+
const row = await db.select().from(users).where(eq(users.username, username)).get();
|
|
1301
1229
|
if (!row) return null;
|
|
1302
1230
|
if (row.user_type === "mind") return null;
|
|
1303
1231
|
if (!compareSync(password, row.password_hash)) return null;
|
|
@@ -1305,8 +1233,8 @@ async function verifyUser(username, password) {
|
|
|
1305
1233
|
return user;
|
|
1306
1234
|
}
|
|
1307
1235
|
async function getUser(id) {
|
|
1308
|
-
const
|
|
1309
|
-
const row = await
|
|
1236
|
+
const db = await getDb();
|
|
1237
|
+
const row = await db.select({
|
|
1310
1238
|
id: users.id,
|
|
1311
1239
|
username: users.username,
|
|
1312
1240
|
role: users.role,
|
|
@@ -1316,8 +1244,8 @@ async function getUser(id) {
|
|
|
1316
1244
|
return row ?? null;
|
|
1317
1245
|
}
|
|
1318
1246
|
async function getUserByUsername(username) {
|
|
1319
|
-
const
|
|
1320
|
-
const row = await
|
|
1247
|
+
const db = await getDb();
|
|
1248
|
+
const row = await db.select({
|
|
1321
1249
|
id: users.id,
|
|
1322
1250
|
username: users.username,
|
|
1323
1251
|
role: users.role,
|
|
@@ -1327,8 +1255,8 @@ async function getUserByUsername(username) {
|
|
|
1327
1255
|
return row ?? null;
|
|
1328
1256
|
}
|
|
1329
1257
|
async function listUsers() {
|
|
1330
|
-
const
|
|
1331
|
-
return
|
|
1258
|
+
const db = await getDb();
|
|
1259
|
+
return db.select({
|
|
1332
1260
|
id: users.id,
|
|
1333
1261
|
username: users.username,
|
|
1334
1262
|
role: users.role,
|
|
@@ -1337,8 +1265,8 @@ async function listUsers() {
|
|
|
1337
1265
|
}).from(users).orderBy(users.created_at).all();
|
|
1338
1266
|
}
|
|
1339
1267
|
async function listPendingUsers() {
|
|
1340
|
-
const
|
|
1341
|
-
return
|
|
1268
|
+
const db = await getDb();
|
|
1269
|
+
return db.select({
|
|
1342
1270
|
id: users.id,
|
|
1343
1271
|
username: users.username,
|
|
1344
1272
|
role: users.role,
|
|
@@ -1347,8 +1275,8 @@ async function listPendingUsers() {
|
|
|
1347
1275
|
}).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
1348
1276
|
}
|
|
1349
1277
|
async function listUsersByType(userType) {
|
|
1350
|
-
const
|
|
1351
|
-
return
|
|
1278
|
+
const db = await getDb();
|
|
1279
|
+
return db.select({
|
|
1352
1280
|
id: users.id,
|
|
1353
1281
|
username: users.username,
|
|
1354
1282
|
role: users.role,
|
|
@@ -1357,8 +1285,8 @@ async function listUsersByType(userType) {
|
|
|
1357
1285
|
}).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
|
|
1358
1286
|
}
|
|
1359
1287
|
async function getOrCreateMindUser(mindName) {
|
|
1360
|
-
const
|
|
1361
|
-
const existing = await
|
|
1288
|
+
const db = await getDb();
|
|
1289
|
+
const existing = await db.select({
|
|
1362
1290
|
id: users.id,
|
|
1363
1291
|
username: users.username,
|
|
1364
1292
|
role: users.role,
|
|
@@ -1367,7 +1295,7 @@ async function getOrCreateMindUser(mindName) {
|
|
|
1367
1295
|
}).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
1368
1296
|
if (existing) return existing;
|
|
1369
1297
|
try {
|
|
1370
|
-
const [result] = await
|
|
1298
|
+
const [result] = await db.insert(users).values({
|
|
1371
1299
|
username: mindName,
|
|
1372
1300
|
password_hash: "!mind",
|
|
1373
1301
|
role: "mind",
|
|
@@ -1382,7 +1310,7 @@ async function getOrCreateMindUser(mindName) {
|
|
|
1382
1310
|
return result;
|
|
1383
1311
|
} catch (err) {
|
|
1384
1312
|
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
1385
|
-
const retried = await
|
|
1313
|
+
const retried = await db.select({
|
|
1386
1314
|
id: users.id,
|
|
1387
1315
|
username: users.username,
|
|
1388
1316
|
role: users.role,
|
|
@@ -1395,12 +1323,21 @@ async function getOrCreateMindUser(mindName) {
|
|
|
1395
1323
|
}
|
|
1396
1324
|
}
|
|
1397
1325
|
async function deleteMindUser2(mindName) {
|
|
1398
|
-
const
|
|
1399
|
-
await
|
|
1326
|
+
const db = await getDb();
|
|
1327
|
+
await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
|
|
1328
|
+
}
|
|
1329
|
+
async function changePassword(userId, currentPassword, newPassword) {
|
|
1330
|
+
const db = await getDb();
|
|
1331
|
+
const row = await db.select().from(users).where(eq(users.id, userId)).get();
|
|
1332
|
+
if (!row) return false;
|
|
1333
|
+
if (!compareSync(currentPassword, row.password_hash)) return false;
|
|
1334
|
+
const hash = hashSync(newPassword, 10);
|
|
1335
|
+
await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
|
|
1336
|
+
return true;
|
|
1400
1337
|
}
|
|
1401
1338
|
async function approveUser(id) {
|
|
1402
|
-
const
|
|
1403
|
-
await
|
|
1339
|
+
const db = await getDb();
|
|
1340
|
+
await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
1404
1341
|
}
|
|
1405
1342
|
|
|
1406
1343
|
// src/web/middleware/auth.ts
|
|
@@ -1410,30 +1347,33 @@ function isValidDaemonToken(token) {
|
|
|
1410
1347
|
return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
|
|
1411
1348
|
}
|
|
1412
1349
|
var SESSION_MAX_AGE = 864e5;
|
|
1350
|
+
var SESSION_CACHE_TTL = 5 * 60 * 1e3;
|
|
1351
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
1413
1352
|
async function createSession(userId) {
|
|
1414
|
-
const
|
|
1353
|
+
const db = await getDb();
|
|
1415
1354
|
const sessionId = crypto.randomUUID();
|
|
1416
|
-
await
|
|
1355
|
+
await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
|
|
1417
1356
|
return sessionId;
|
|
1418
1357
|
}
|
|
1419
1358
|
async function deleteSession(sessionId) {
|
|
1420
|
-
|
|
1421
|
-
await
|
|
1359
|
+
sessionCache.delete(sessionId);
|
|
1360
|
+
const db = await getDb();
|
|
1361
|
+
await db.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
1422
1362
|
}
|
|
1423
1363
|
async function getSessionUserId(sessionId) {
|
|
1424
|
-
const
|
|
1425
|
-
const row = await
|
|
1364
|
+
const db = await getDb();
|
|
1365
|
+
const row = await db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
|
|
1426
1366
|
if (!row) return void 0;
|
|
1427
1367
|
if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
|
|
1428
|
-
await
|
|
1368
|
+
await db.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
1429
1369
|
return void 0;
|
|
1430
1370
|
}
|
|
1431
1371
|
return row.userId;
|
|
1432
1372
|
}
|
|
1433
1373
|
async function cleanExpiredSessions() {
|
|
1434
|
-
const
|
|
1374
|
+
const db = await getDb();
|
|
1435
1375
|
const cutoff = Date.now() - SESSION_MAX_AGE;
|
|
1436
|
-
await
|
|
1376
|
+
await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
|
|
1437
1377
|
}
|
|
1438
1378
|
var requireAdmin = createMiddleware(async (c, next) => {
|
|
1439
1379
|
const user = c.get("user");
|
|
@@ -1447,30 +1387,44 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
1447
1387
|
if (authHeader?.startsWith("Bearer ")) {
|
|
1448
1388
|
const token = authHeader.slice(7);
|
|
1449
1389
|
if (token && isValidDaemonToken(token)) {
|
|
1450
|
-
c.set("user", { id: 0, username: "cli", role: "admin", user_type: "
|
|
1390
|
+
c.set("user", { id: 0, username: "cli", role: "admin", user_type: "brain" });
|
|
1451
1391
|
await next();
|
|
1452
1392
|
return;
|
|
1453
1393
|
}
|
|
1454
1394
|
}
|
|
1455
1395
|
const sessionId = getCookie(c, "volute_session");
|
|
1456
1396
|
if (!sessionId) return c.json({ error: "Unauthorized" }, 401);
|
|
1397
|
+
const cached = sessionCache.get(sessionId);
|
|
1398
|
+
if (cached && cached.expires > Date.now()) {
|
|
1399
|
+
if (cached.user.role === "pending") return c.json({ error: "Account pending approval" }, 403);
|
|
1400
|
+
c.set("user", cached.user);
|
|
1401
|
+
await next();
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1457
1404
|
const userId = await getSessionUserId(sessionId);
|
|
1458
|
-
if (userId == null)
|
|
1405
|
+
if (userId == null) {
|
|
1406
|
+
sessionCache.delete(sessionId);
|
|
1407
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
1408
|
+
}
|
|
1459
1409
|
const user = await getUser(userId);
|
|
1460
|
-
if (!user)
|
|
1410
|
+
if (!user) {
|
|
1411
|
+
sessionCache.delete(sessionId);
|
|
1412
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
1413
|
+
}
|
|
1461
1414
|
if (user.role === "pending") return c.json({ error: "Account pending approval" }, 403);
|
|
1415
|
+
sessionCache.set(sessionId, { userId, user, expires: Date.now() + SESSION_CACHE_TTL });
|
|
1462
1416
|
c.set("user", user);
|
|
1463
1417
|
await next();
|
|
1464
1418
|
});
|
|
1465
1419
|
|
|
1466
1420
|
// src/web/server.ts
|
|
1467
|
-
import { existsSync as
|
|
1421
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1468
1422
|
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
1469
|
-
import { dirname as dirname3, extname as extname2, resolve as
|
|
1423
|
+
import { dirname as dirname3, extname as extname2, resolve as resolve18 } from "path";
|
|
1470
1424
|
import { serve } from "@hono/node-server";
|
|
1471
1425
|
|
|
1472
1426
|
// src/web/app.ts
|
|
1473
|
-
import { Hono as
|
|
1427
|
+
import { Hono as Hono23 } from "hono";
|
|
1474
1428
|
import { bodyLimit } from "hono/body-limit";
|
|
1475
1429
|
import { csrf } from "hono/csrf";
|
|
1476
1430
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -1484,6 +1438,17 @@ var credentialsSchema = z.object({
|
|
|
1484
1438
|
username: z.string().min(1),
|
|
1485
1439
|
password: z.string().min(1)
|
|
1486
1440
|
});
|
|
1441
|
+
var changePasswordSchema = z.object({
|
|
1442
|
+
currentPassword: z.string().min(1),
|
|
1443
|
+
newPassword: z.string().min(1)
|
|
1444
|
+
});
|
|
1445
|
+
var authenticated = new Hono().use(authMiddleware).post("/change-password", zValidator("json", changePasswordSchema), async (c) => {
|
|
1446
|
+
const user = c.get("user");
|
|
1447
|
+
const { currentPassword, newPassword } = c.req.valid("json");
|
|
1448
|
+
const ok = await changePassword(user.id, currentPassword, newPassword);
|
|
1449
|
+
if (!ok) return c.json({ error: "Current password is incorrect" }, 400);
|
|
1450
|
+
return c.json({ ok: true });
|
|
1451
|
+
});
|
|
1487
1452
|
var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
|
|
1488
1453
|
const user = c.get("user");
|
|
1489
1454
|
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
@@ -1492,7 +1457,7 @@ var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
|
|
|
1492
1457
|
await getOrCreateMindUser(mind.name);
|
|
1493
1458
|
}
|
|
1494
1459
|
const type = c.req.query("type");
|
|
1495
|
-
if (type === "
|
|
1460
|
+
if (type === "brain" || type === "mind") {
|
|
1496
1461
|
return c.json(await listUsersByType(type));
|
|
1497
1462
|
}
|
|
1498
1463
|
return c.json(await listUsers());
|
|
@@ -1543,7 +1508,7 @@ var app = new Hono().post("/register", zValidator("json", credentialsSchema), as
|
|
|
1543
1508
|
const user = await getUser(userId);
|
|
1544
1509
|
if (!user) return c.json({ error: "Not logged in" }, 401);
|
|
1545
1510
|
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
1546
|
-
}).route("/", admin);
|
|
1511
|
+
}).route("/", admin).route("/", authenticated);
|
|
1547
1512
|
var auth_default = app;
|
|
1548
1513
|
|
|
1549
1514
|
// src/web/api/channels.ts
|
|
@@ -1830,18 +1795,109 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
|
1830
1795
|
});
|
|
1831
1796
|
var files_default = app5;
|
|
1832
1797
|
|
|
1798
|
+
// src/web/api/keys.ts
|
|
1799
|
+
import { Hono as Hono6 } from "hono";
|
|
1800
|
+
|
|
1801
|
+
// src/lib/identity.ts
|
|
1802
|
+
import { createHash, generateKeyPairSync, sign, verify } from "crypto";
|
|
1803
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1804
|
+
import { resolve as resolve8 } from "path";
|
|
1805
|
+
function generateIdentity(mindDir2) {
|
|
1806
|
+
const identityDir = resolve8(mindDir2, ".mind/identity");
|
|
1807
|
+
mkdirSync4(identityDir, { recursive: true });
|
|
1808
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
1809
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
1810
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
1811
|
+
});
|
|
1812
|
+
const privatePath = resolve8(identityDir, "private.pem");
|
|
1813
|
+
const publicPath = resolve8(identityDir, "public.pem");
|
|
1814
|
+
writeFileSync4(privatePath, privateKey, { mode: 384 });
|
|
1815
|
+
writeFileSync4(publicPath, publicKey, { mode: 420 });
|
|
1816
|
+
const config = readVoluteConfig(mindDir2) ?? {};
|
|
1817
|
+
config.identity = {
|
|
1818
|
+
privateKey: ".mind/identity/private.pem",
|
|
1819
|
+
publicKey: ".mind/identity/public.pem"
|
|
1820
|
+
};
|
|
1821
|
+
writeVoluteConfig(mindDir2, config);
|
|
1822
|
+
return { publicKeyPem: publicKey, privateKeyPem: privateKey };
|
|
1823
|
+
}
|
|
1824
|
+
function getPrivateKey(mindDir2) {
|
|
1825
|
+
const config = readVoluteConfig(mindDir2);
|
|
1826
|
+
const relPath = config?.identity?.privateKey;
|
|
1827
|
+
if (!relPath) return null;
|
|
1828
|
+
const fullPath = resolve8(mindDir2, relPath);
|
|
1829
|
+
if (!existsSync7(fullPath)) return null;
|
|
1830
|
+
return readFileSync5(fullPath, "utf-8");
|
|
1831
|
+
}
|
|
1832
|
+
function getPublicKey(mindDir2) {
|
|
1833
|
+
const config = readVoluteConfig(mindDir2);
|
|
1834
|
+
const relPath = config?.identity?.publicKey;
|
|
1835
|
+
if (!relPath) return null;
|
|
1836
|
+
const fullPath = resolve8(mindDir2, relPath);
|
|
1837
|
+
if (!existsSync7(fullPath)) return null;
|
|
1838
|
+
return readFileSync5(fullPath, "utf-8");
|
|
1839
|
+
}
|
|
1840
|
+
function getFingerprint(publicKeyPem) {
|
|
1841
|
+
return createHash("sha256").update(publicKeyPem).digest("hex");
|
|
1842
|
+
}
|
|
1843
|
+
function signMessage(privateKeyPem, content, timestamp) {
|
|
1844
|
+
const data = `${content}
|
|
1845
|
+
${timestamp}`;
|
|
1846
|
+
const signature = sign(null, Buffer.from(data), privateKeyPem);
|
|
1847
|
+
return signature.toString("base64");
|
|
1848
|
+
}
|
|
1849
|
+
async function publishPublicKey(mindName, publicKeyPem) {
|
|
1850
|
+
const systems = readSystemsConfig();
|
|
1851
|
+
if (!systems) return false;
|
|
1852
|
+
try {
|
|
1853
|
+
const res = await fetch(`${systems.apiUrl}/api/keys/${encodeURIComponent(mindName)}`, {
|
|
1854
|
+
method: "PUT",
|
|
1855
|
+
headers: {
|
|
1856
|
+
"Content-Type": "application/json",
|
|
1857
|
+
Authorization: `Bearer ${systems.apiKey}`
|
|
1858
|
+
},
|
|
1859
|
+
body: JSON.stringify({ publicKey: publicKeyPem })
|
|
1860
|
+
});
|
|
1861
|
+
if (!res.ok) {
|
|
1862
|
+
logger_default.warn(`failed to publish key for ${mindName}: ${res.status}`);
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
return true;
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
logger_default.warn(`failed to publish key for ${mindName}`, logger_default.errorData(err));
|
|
1868
|
+
return false;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/web/api/keys.ts
|
|
1873
|
+
var app6 = new Hono6().get("/:fingerprint", (c) => {
|
|
1874
|
+
const fingerprint = c.req.param("fingerprint");
|
|
1875
|
+
for (const entry of readRegistry()) {
|
|
1876
|
+
try {
|
|
1877
|
+
const pubKey = getPublicKey(mindDir(entry.name));
|
|
1878
|
+
if (!pubKey) continue;
|
|
1879
|
+
if (getFingerprint(pubKey) === fingerprint) {
|
|
1880
|
+
return c.json({ publicKey: pubKey, mind: entry.name });
|
|
1881
|
+
}
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return c.json({ error: "Key not found" }, 404);
|
|
1886
|
+
});
|
|
1887
|
+
var keys_default = app6;
|
|
1888
|
+
|
|
1833
1889
|
// src/web/api/logs.ts
|
|
1834
1890
|
import { spawn as spawn2 } from "child_process";
|
|
1835
|
-
import { existsSync as
|
|
1836
|
-
import { resolve as
|
|
1837
|
-
import { Hono as
|
|
1891
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1892
|
+
import { resolve as resolve9 } from "path";
|
|
1893
|
+
import { Hono as Hono7 } from "hono";
|
|
1838
1894
|
import { streamSSE } from "hono/streaming";
|
|
1839
|
-
var
|
|
1895
|
+
var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
1840
1896
|
const name = c.req.param("name");
|
|
1841
1897
|
const entry = findMind(name);
|
|
1842
1898
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1843
|
-
const logFile =
|
|
1844
|
-
if (!
|
|
1899
|
+
const logFile = resolve9(stateDir(name), "logs", "mind.log");
|
|
1900
|
+
if (!existsSync8(logFile)) {
|
|
1845
1901
|
return c.json({ error: "No log file found" }, 404);
|
|
1846
1902
|
}
|
|
1847
1903
|
return streamSSE(c, async (stream) => {
|
|
@@ -1859,17 +1915,17 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
|
|
|
1859
1915
|
stream.onAbort(() => {
|
|
1860
1916
|
tail.kill();
|
|
1861
1917
|
});
|
|
1862
|
-
await new Promise((
|
|
1863
|
-
tail.on("exit",
|
|
1864
|
-
stream.onAbort(
|
|
1918
|
+
await new Promise((resolve20) => {
|
|
1919
|
+
tail.on("exit", resolve20);
|
|
1920
|
+
stream.onAbort(resolve20);
|
|
1865
1921
|
});
|
|
1866
1922
|
});
|
|
1867
1923
|
}).get("/:name/logs/tail", async (c) => {
|
|
1868
1924
|
const name = c.req.param("name");
|
|
1869
1925
|
const entry = findMind(name);
|
|
1870
1926
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1871
|
-
const logFile =
|
|
1872
|
-
if (!
|
|
1927
|
+
const logFile = resolve9(stateDir(name), "logs", "mind.log");
|
|
1928
|
+
if (!existsSync8(logFile)) {
|
|
1873
1929
|
return c.json({ error: "No log file found" }, 404);
|
|
1874
1930
|
}
|
|
1875
1931
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
@@ -1879,44 +1935,125 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
|
|
|
1879
1935
|
tail.stdout.on("data", (data) => {
|
|
1880
1936
|
output += data.toString();
|
|
1881
1937
|
});
|
|
1882
|
-
await new Promise((
|
|
1883
|
-
tail.on("exit",
|
|
1938
|
+
await new Promise((resolve20) => {
|
|
1939
|
+
tail.on("exit", resolve20);
|
|
1884
1940
|
});
|
|
1885
1941
|
return c.text(output);
|
|
1886
1942
|
});
|
|
1887
|
-
var logs_default =
|
|
1943
|
+
var logs_default = app7;
|
|
1944
|
+
|
|
1945
|
+
// src/web/api/mind-skills.ts
|
|
1946
|
+
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
1947
|
+
import { Hono as Hono8 } from "hono";
|
|
1948
|
+
import { z as z2 } from "zod";
|
|
1949
|
+
var app8 = new Hono8().get("/:name/skills", async (c) => {
|
|
1950
|
+
const name = c.req.param("name");
|
|
1951
|
+
const entry = findMind(name);
|
|
1952
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1953
|
+
const dir = mindDir(name);
|
|
1954
|
+
const skills = await listMindSkills(dir);
|
|
1955
|
+
return c.json(skills);
|
|
1956
|
+
}).post(
|
|
1957
|
+
"/:name/skills/install",
|
|
1958
|
+
requireAdmin,
|
|
1959
|
+
zValidator2("json", z2.object({ skillId: z2.string() })),
|
|
1960
|
+
async (c) => {
|
|
1961
|
+
const name = c.req.param("name");
|
|
1962
|
+
const entry = findMind(name);
|
|
1963
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1964
|
+
const { skillId } = c.req.valid("json");
|
|
1965
|
+
const dir = mindDir(name);
|
|
1966
|
+
try {
|
|
1967
|
+
await installSkill(name, dir, skillId);
|
|
1968
|
+
} catch (e) {
|
|
1969
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1970
|
+
return c.json({ error: msg }, 400);
|
|
1971
|
+
}
|
|
1972
|
+
return c.json({ ok: true });
|
|
1973
|
+
}
|
|
1974
|
+
).post(
|
|
1975
|
+
"/:name/skills/update",
|
|
1976
|
+
requireAdmin,
|
|
1977
|
+
zValidator2("json", z2.object({ skillId: z2.string() })),
|
|
1978
|
+
async (c) => {
|
|
1979
|
+
const name = c.req.param("name");
|
|
1980
|
+
const entry = findMind(name);
|
|
1981
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1982
|
+
const { skillId } = c.req.valid("json");
|
|
1983
|
+
const dir = mindDir(name);
|
|
1984
|
+
try {
|
|
1985
|
+
const result = await updateSkill(name, dir, skillId);
|
|
1986
|
+
return c.json(result);
|
|
1987
|
+
} catch (e) {
|
|
1988
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1989
|
+
return c.json({ error: msg }, 400);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
).post(
|
|
1993
|
+
"/:name/skills/publish",
|
|
1994
|
+
requireAdmin,
|
|
1995
|
+
zValidator2("json", z2.object({ skillId: z2.string() })),
|
|
1996
|
+
async (c) => {
|
|
1997
|
+
const name = c.req.param("name");
|
|
1998
|
+
const entry = findMind(name);
|
|
1999
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2000
|
+
const { skillId } = c.req.valid("json");
|
|
2001
|
+
const dir = mindDir(name);
|
|
2002
|
+
try {
|
|
2003
|
+
const skill = await publishSkill(name, dir, skillId);
|
|
2004
|
+
return c.json(skill);
|
|
2005
|
+
} catch (e) {
|
|
2006
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2007
|
+
return c.json({ error: msg }, 400);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
).delete("/:name/skills/:skill", requireAdmin, async (c) => {
|
|
2011
|
+
const name = c.req.param("name");
|
|
2012
|
+
const skillName = c.req.param("skill");
|
|
2013
|
+
const entry = findMind(name);
|
|
2014
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2015
|
+
const dir = mindDir(name);
|
|
2016
|
+
try {
|
|
2017
|
+
await uninstallSkill(name, dir, skillName);
|
|
2018
|
+
} catch (e) {
|
|
2019
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2020
|
+
return c.json({ error: msg }, 400);
|
|
2021
|
+
}
|
|
2022
|
+
return c.json({ ok: true });
|
|
2023
|
+
});
|
|
2024
|
+
var mind_skills_default = app8;
|
|
1888
2025
|
|
|
1889
2026
|
// src/web/api/minds.ts
|
|
1890
2027
|
import {
|
|
1891
|
-
cpSync,
|
|
1892
|
-
existsSync as
|
|
1893
|
-
mkdirSync as
|
|
1894
|
-
readdirSync as
|
|
1895
|
-
readFileSync as
|
|
1896
|
-
rmSync,
|
|
1897
|
-
statSync,
|
|
1898
|
-
writeFileSync as
|
|
2028
|
+
cpSync as cpSync2,
|
|
2029
|
+
existsSync as existsSync10,
|
|
2030
|
+
mkdirSync as mkdirSync7,
|
|
2031
|
+
readdirSync as readdirSync4,
|
|
2032
|
+
readFileSync as readFileSync9,
|
|
2033
|
+
rmSync as rmSync2,
|
|
2034
|
+
statSync as statSync2,
|
|
2035
|
+
writeFileSync as writeFileSync8
|
|
1899
2036
|
} from "fs";
|
|
1900
|
-
import { join, resolve as
|
|
1901
|
-
import { zValidator as
|
|
1902
|
-
import { and as and3, desc as desc2, eq as eq4, sql as
|
|
1903
|
-
import { Hono as
|
|
1904
|
-
import { z as
|
|
2037
|
+
import { join as join2, resolve as resolve13 } from "path";
|
|
2038
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
2039
|
+
import { and as and3, desc as desc2, eq as eq4, sql as sql2 } from "drizzle-orm";
|
|
2040
|
+
import { Hono as Hono9 } from "hono";
|
|
2041
|
+
import { z as z3 } from "zod";
|
|
1905
2042
|
|
|
1906
2043
|
// src/lib/consolidate.ts
|
|
1907
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
1908
|
-
import { resolve as
|
|
2044
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2045
|
+
import { resolve as resolve10 } from "path";
|
|
1909
2046
|
async function consolidateMemory(mindDir2) {
|
|
1910
|
-
const soulPath =
|
|
1911
|
-
const memoryPath =
|
|
1912
|
-
const memoryDir =
|
|
1913
|
-
const soul =
|
|
2047
|
+
const soulPath = resolve10(mindDir2, "home/SOUL.md");
|
|
2048
|
+
const memoryPath = resolve10(mindDir2, "home/MEMORY.md");
|
|
2049
|
+
const memoryDir = resolve10(mindDir2, "home/memory");
|
|
2050
|
+
const soul = readFileSync6(soulPath, "utf-8");
|
|
1914
2051
|
const logs = [];
|
|
1915
2052
|
try {
|
|
1916
2053
|
const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
1917
2054
|
for (const filename of files) {
|
|
1918
2055
|
const date = filename.replace(".md", "");
|
|
1919
|
-
const content2 =
|
|
2056
|
+
const content2 = readFileSync6(resolve10(memoryDir, filename), "utf-8").trim();
|
|
1920
2057
|
if (content2) {
|
|
1921
2058
|
logs.push(`### ${date}
|
|
1922
2059
|
|
|
@@ -1966,7 +2103,7 @@ ${content2}`);
|
|
|
1966
2103
|
const data = await res.json();
|
|
1967
2104
|
const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
|
|
1968
2105
|
if (content) {
|
|
1969
|
-
|
|
2106
|
+
writeFileSync5(memoryPath, `${content}
|
|
1970
2107
|
`);
|
|
1971
2108
|
console.log("MEMORY.md created successfully.");
|
|
1972
2109
|
} else {
|
|
@@ -1976,7 +2113,7 @@ ${content2}`);
|
|
|
1976
2113
|
|
|
1977
2114
|
// src/lib/conversations.ts
|
|
1978
2115
|
import { randomUUID } from "crypto";
|
|
1979
|
-
import { and as and2, desc, eq as eq3, inArray, isNull, sql
|
|
2116
|
+
import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
|
|
1980
2117
|
|
|
1981
2118
|
// src/lib/conversation-events.ts
|
|
1982
2119
|
var subscribers = /* @__PURE__ */ new Map();
|
|
@@ -2008,13 +2145,17 @@ function publish(conversationId, event) {
|
|
|
2008
2145
|
|
|
2009
2146
|
// src/lib/conversations.ts
|
|
2010
2147
|
async function createConversation(mindName, channel, opts) {
|
|
2011
|
-
const
|
|
2148
|
+
const db = await getDb();
|
|
2012
2149
|
const id = randomUUID();
|
|
2013
|
-
|
|
2150
|
+
const type = opts?.type ?? "dm";
|
|
2151
|
+
const name = opts?.name ?? null;
|
|
2152
|
+
await db.transaction(async (tx) => {
|
|
2014
2153
|
await tx.insert(conversations).values({
|
|
2015
2154
|
id,
|
|
2016
2155
|
mind_name: mindName,
|
|
2017
2156
|
channel,
|
|
2157
|
+
type,
|
|
2158
|
+
name,
|
|
2018
2159
|
user_id: opts?.userId ?? null,
|
|
2019
2160
|
title: opts?.title ?? null
|
|
2020
2161
|
});
|
|
@@ -2032,6 +2173,8 @@ async function createConversation(mindName, channel, opts) {
|
|
|
2032
2173
|
id,
|
|
2033
2174
|
mind_name: mindName,
|
|
2034
2175
|
channel,
|
|
2176
|
+
type,
|
|
2177
|
+
name,
|
|
2035
2178
|
user_id: opts?.userId ?? null,
|
|
2036
2179
|
title: opts?.title ?? null,
|
|
2037
2180
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2039,13 +2182,30 @@ async function createConversation(mindName, channel, opts) {
|
|
|
2039
2182
|
};
|
|
2040
2183
|
}
|
|
2041
2184
|
async function getConversation(id) {
|
|
2042
|
-
const
|
|
2043
|
-
const row = await
|
|
2185
|
+
const db = await getDb();
|
|
2186
|
+
const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
|
|
2044
2187
|
return row ?? null;
|
|
2045
2188
|
}
|
|
2189
|
+
async function addParticipant(conversationId, userId, role = "member") {
|
|
2190
|
+
const db = await getDb();
|
|
2191
|
+
await db.insert(conversationParticipants).values({
|
|
2192
|
+
conversation_id: conversationId,
|
|
2193
|
+
user_id: userId,
|
|
2194
|
+
role
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
async function removeParticipant(conversationId, userId) {
|
|
2198
|
+
const db = await getDb();
|
|
2199
|
+
await db.delete(conversationParticipants).where(
|
|
2200
|
+
and2(
|
|
2201
|
+
eq3(conversationParticipants.conversation_id, conversationId),
|
|
2202
|
+
eq3(conversationParticipants.user_id, userId)
|
|
2203
|
+
)
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2046
2206
|
async function getParticipants(conversationId) {
|
|
2047
|
-
const
|
|
2048
|
-
const rows = await
|
|
2207
|
+
const db = await getDb();
|
|
2208
|
+
const rows = await db.select({
|
|
2049
2209
|
userId: conversationParticipants.user_id,
|
|
2050
2210
|
username: users.username,
|
|
2051
2211
|
userType: users.user_type,
|
|
@@ -2054,8 +2214,8 @@ async function getParticipants(conversationId) {
|
|
|
2054
2214
|
return rows;
|
|
2055
2215
|
}
|
|
2056
2216
|
async function isParticipant(conversationId, userId) {
|
|
2057
|
-
const
|
|
2058
|
-
const row = await
|
|
2217
|
+
const db = await getDb();
|
|
2218
|
+
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
2059
2219
|
and2(
|
|
2060
2220
|
eq3(conversationParticipants.conversation_id, conversationId),
|
|
2061
2221
|
eq3(conversationParticipants.user_id, userId)
|
|
@@ -2064,16 +2224,16 @@ async function isParticipant(conversationId, userId) {
|
|
|
2064
2224
|
return row != null;
|
|
2065
2225
|
}
|
|
2066
2226
|
async function listConversationsForUser(userId) {
|
|
2067
|
-
const
|
|
2068
|
-
const participantRows = await
|
|
2227
|
+
const db = await getDb();
|
|
2228
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
|
|
2069
2229
|
if (participantRows.length === 0) return [];
|
|
2070
2230
|
const convIds = participantRows.map((r) => r.conversation_id);
|
|
2071
|
-
return
|
|
2231
|
+
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
2072
2232
|
}
|
|
2073
2233
|
async function isParticipantOrOwner(conversationId, userId) {
|
|
2074
2234
|
if (await isParticipant(conversationId, userId)) return true;
|
|
2075
|
-
const
|
|
2076
|
-
const row = await
|
|
2235
|
+
const db = await getDb();
|
|
2236
|
+
const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
|
|
2077
2237
|
return row != null;
|
|
2078
2238
|
}
|
|
2079
2239
|
async function deleteConversationForUser(id, userId) {
|
|
@@ -2082,15 +2242,15 @@ async function deleteConversationForUser(id, userId) {
|
|
|
2082
2242
|
return true;
|
|
2083
2243
|
}
|
|
2084
2244
|
async function addMessage(conversationId, role, senderName, content) {
|
|
2085
|
-
const
|
|
2245
|
+
const db = await getDb();
|
|
2086
2246
|
const serialized = JSON.stringify(content);
|
|
2087
|
-
const [result] = await
|
|
2088
|
-
await
|
|
2247
|
+
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 });
|
|
2248
|
+
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
|
|
2089
2249
|
if (role === "user") {
|
|
2090
2250
|
const firstText = content.find((b) => b.type === "text");
|
|
2091
2251
|
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
2092
2252
|
if (title) {
|
|
2093
|
-
await
|
|
2253
|
+
await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
|
|
2094
2254
|
}
|
|
2095
2255
|
}
|
|
2096
2256
|
const msg = {
|
|
@@ -2112,8 +2272,8 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
2112
2272
|
return msg;
|
|
2113
2273
|
}
|
|
2114
2274
|
async function getMessages(conversationId) {
|
|
2115
|
-
const
|
|
2116
|
-
const rows = await
|
|
2275
|
+
const db = await getDb();
|
|
2276
|
+
const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
2117
2277
|
return rows.map((row) => {
|
|
2118
2278
|
let content;
|
|
2119
2279
|
try {
|
|
@@ -2128,9 +2288,9 @@ async function getMessages(conversationId) {
|
|
|
2128
2288
|
async function listConversationsWithParticipants(userId) {
|
|
2129
2289
|
const convs = await listConversationsForUser(userId);
|
|
2130
2290
|
if (convs.length === 0) return [];
|
|
2131
|
-
const
|
|
2291
|
+
const db = await getDb();
|
|
2132
2292
|
const convIds = convs.map((c) => c.id);
|
|
2133
|
-
const rows = await
|
|
2293
|
+
const rows = await db.select({
|
|
2134
2294
|
conversationId: conversationParticipants.conversation_id,
|
|
2135
2295
|
userId: users.id,
|
|
2136
2296
|
username: users.username,
|
|
@@ -2151,32 +2311,32 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2151
2311
|
role: r.role
|
|
2152
2312
|
});
|
|
2153
2313
|
}
|
|
2154
|
-
const lastMsgIds = await
|
|
2314
|
+
const lastMsgIds = await db.select({
|
|
2155
2315
|
conversationId: messages.conversation_id,
|
|
2156
|
-
maxId:
|
|
2316
|
+
maxId: sql`MAX(${messages.id})`
|
|
2157
2317
|
}).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
2158
2318
|
const byLastMsg = /* @__PURE__ */ new Map();
|
|
2159
2319
|
if (lastMsgIds.length > 0) {
|
|
2160
|
-
const msgRows = await
|
|
2320
|
+
const msgRows = await db.select().from(messages).where(
|
|
2161
2321
|
inArray(
|
|
2162
2322
|
messages.id,
|
|
2163
2323
|
lastMsgIds.map((r) => r.maxId)
|
|
2164
2324
|
)
|
|
2165
2325
|
);
|
|
2166
2326
|
for (const m of msgRows) {
|
|
2167
|
-
let
|
|
2327
|
+
let text = "";
|
|
2168
2328
|
try {
|
|
2169
2329
|
const parsed = JSON.parse(m.content);
|
|
2170
2330
|
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
2171
2331
|
const textBlock = blocks.find((b) => b.type === "text");
|
|
2172
|
-
if (textBlock && "text" in textBlock)
|
|
2332
|
+
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
2173
2333
|
} catch {
|
|
2174
|
-
|
|
2334
|
+
text = m.content;
|
|
2175
2335
|
}
|
|
2176
2336
|
byLastMsg.set(m.conversation_id, {
|
|
2177
2337
|
role: m.role,
|
|
2178
2338
|
senderName: m.sender_name,
|
|
2179
|
-
text
|
|
2339
|
+
text,
|
|
2180
2340
|
createdAt: m.created_at
|
|
2181
2341
|
});
|
|
2182
2342
|
}
|
|
@@ -2188,10 +2348,10 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2188
2348
|
}));
|
|
2189
2349
|
}
|
|
2190
2350
|
async function findDMConversation(mindName, participantIds) {
|
|
2191
|
-
const
|
|
2192
|
-
const mindConvs = await
|
|
2351
|
+
const db = await getDb();
|
|
2352
|
+
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq3(conversations.mind_name, mindName), eq3(conversations.type, "dm"))).all();
|
|
2193
2353
|
for (const conv of mindConvs) {
|
|
2194
|
-
const rows = await
|
|
2354
|
+
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
|
|
2195
2355
|
if (rows.length !== 2) continue;
|
|
2196
2356
|
const ids = new Set(rows.map((r) => r.user_id));
|
|
2197
2357
|
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
@@ -2201,17 +2361,42 @@ async function findDMConversation(mindName, participantIds) {
|
|
|
2201
2361
|
return null;
|
|
2202
2362
|
}
|
|
2203
2363
|
async function deleteConversation(id) {
|
|
2204
|
-
const
|
|
2205
|
-
await
|
|
2364
|
+
const db = await getDb();
|
|
2365
|
+
await db.delete(conversations).where(eq3(conversations.id, id));
|
|
2366
|
+
}
|
|
2367
|
+
async function createChannel(name, creatorId) {
|
|
2368
|
+
const participantIds = creatorId ? [creatorId] : [];
|
|
2369
|
+
return createConversation(null, "volute", {
|
|
2370
|
+
type: "channel",
|
|
2371
|
+
name,
|
|
2372
|
+
title: name,
|
|
2373
|
+
participantIds
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
async function getChannelByName(name) {
|
|
2377
|
+
const db = await getDb();
|
|
2378
|
+
const row = await db.select().from(conversations).where(and2(eq3(conversations.name, name), eq3(conversations.type, "channel"))).get();
|
|
2379
|
+
return row ?? null;
|
|
2380
|
+
}
|
|
2381
|
+
async function listChannels() {
|
|
2382
|
+
const db = await getDb();
|
|
2383
|
+
return await db.select().from(conversations).where(eq3(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
2384
|
+
}
|
|
2385
|
+
async function joinChannel(conversationId, userId) {
|
|
2386
|
+
if (await isParticipant(conversationId, userId)) return;
|
|
2387
|
+
await addParticipant(conversationId, userId);
|
|
2388
|
+
}
|
|
2389
|
+
async function leaveChannel(conversationId, userId) {
|
|
2390
|
+
await removeParticipant(conversationId, userId);
|
|
2206
2391
|
}
|
|
2207
2392
|
|
|
2208
2393
|
// src/lib/convert-session.ts
|
|
2209
2394
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2210
|
-
import { mkdirSync as
|
|
2395
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
2211
2396
|
import { homedir } from "os";
|
|
2212
|
-
import { resolve as
|
|
2397
|
+
import { resolve as resolve11 } from "path";
|
|
2213
2398
|
function convertSession(opts) {
|
|
2214
|
-
const lines =
|
|
2399
|
+
const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
|
|
2215
2400
|
const sessionId = randomUUID2();
|
|
2216
2401
|
const idMap = /* @__PURE__ */ new Map();
|
|
2217
2402
|
const messages2 = [];
|
|
@@ -2325,10 +2510,10 @@ function convertSession(opts) {
|
|
|
2325
2510
|
}
|
|
2326
2511
|
}
|
|
2327
2512
|
const projectId = opts.projectDir.replace(/\//g, "-");
|
|
2328
|
-
const sdkDir =
|
|
2329
|
-
|
|
2330
|
-
const sdkPath =
|
|
2331
|
-
|
|
2513
|
+
const sdkDir = resolve11(homedir(), ".claude", "projects", projectId);
|
|
2514
|
+
mkdirSync5(sdkDir, { recursive: true });
|
|
2515
|
+
const sdkPath = resolve11(sdkDir, `${sessionId}.jsonl`);
|
|
2516
|
+
writeFileSync6(sdkPath, `${sdkEvents.join("\n")}
|
|
2332
2517
|
`);
|
|
2333
2518
|
console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
|
|
2334
2519
|
return sessionId;
|
|
@@ -2407,121 +2592,112 @@ function publish2(mind, event) {
|
|
|
2407
2592
|
}
|
|
2408
2593
|
}
|
|
2409
2594
|
|
|
2410
|
-
// src/lib/
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
const senders = this.channels.get(channel);
|
|
2431
|
-
if (senders) {
|
|
2432
|
-
senders.delete(sender);
|
|
2433
|
-
if (senders.size === 0) {
|
|
2434
|
-
this.channels.delete(channel);
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2595
|
+
// src/lib/template.ts
|
|
2596
|
+
import {
|
|
2597
|
+
cpSync,
|
|
2598
|
+
existsSync as existsSync9,
|
|
2599
|
+
mkdirSync as mkdirSync6,
|
|
2600
|
+
readdirSync as readdirSync3,
|
|
2601
|
+
readFileSync as readFileSync8,
|
|
2602
|
+
renameSync as renameSync3,
|
|
2603
|
+
rmSync,
|
|
2604
|
+
statSync,
|
|
2605
|
+
writeFileSync as writeFileSync7
|
|
2606
|
+
} from "fs";
|
|
2607
|
+
import { tmpdir } from "os";
|
|
2608
|
+
import { dirname as dirname2, join, relative, resolve as resolve12 } from "path";
|
|
2609
|
+
function findTemplatesRoot() {
|
|
2610
|
+
let dir = dirname2(new URL(import.meta.url).pathname);
|
|
2611
|
+
for (let i = 0; i < 5; i++) {
|
|
2612
|
+
const candidate = resolve12(dir, "templates");
|
|
2613
|
+
if (existsSync9(resolve12(candidate, "_base"))) return candidate;
|
|
2614
|
+
dir = dirname2(dir);
|
|
2437
2615
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2616
|
+
console.error(
|
|
2617
|
+
"Templates directory not found. Searched up from:",
|
|
2618
|
+
dirname2(new URL(import.meta.url).pathname)
|
|
2619
|
+
);
|
|
2620
|
+
process.exit(1);
|
|
2621
|
+
}
|
|
2622
|
+
function composeTemplate(templatesRoot, templateName) {
|
|
2623
|
+
const baseDir = resolve12(templatesRoot, "_base");
|
|
2624
|
+
const templateDir = resolve12(templatesRoot, templateName);
|
|
2625
|
+
if (!existsSync9(baseDir)) {
|
|
2626
|
+
console.error("Base template not found:", baseDir);
|
|
2627
|
+
process.exit(1);
|
|
2628
|
+
}
|
|
2629
|
+
if (!existsSync9(templateDir)) {
|
|
2630
|
+
console.error(`Template not found: ${templateName}`);
|
|
2631
|
+
process.exit(1);
|
|
2632
|
+
}
|
|
2633
|
+
const composedDir = resolve12(tmpdir(), `volute-template-${Date.now()}`);
|
|
2634
|
+
mkdirSync6(composedDir, { recursive: true });
|
|
2635
|
+
cpSync(baseDir, composedDir, { recursive: true });
|
|
2636
|
+
for (const file of listFiles(templateDir)) {
|
|
2637
|
+
const src = resolve12(templateDir, file);
|
|
2638
|
+
const dest = resolve12(composedDir, file);
|
|
2639
|
+
mkdirSync6(dirname2(dest), { recursive: true });
|
|
2640
|
+
cpSync(src, dest);
|
|
2641
|
+
}
|
|
2642
|
+
const manifestPath = resolve12(composedDir, "volute-template.json");
|
|
2643
|
+
if (!existsSync9(manifestPath)) {
|
|
2644
|
+
rmSync(composedDir, { recursive: true, force: true });
|
|
2645
|
+
console.error(`Template manifest not found: ${templateName}/volute-template.json`);
|
|
2646
|
+
process.exit(1);
|
|
2446
2647
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2648
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
|
|
2649
|
+
rmSync(manifestPath);
|
|
2650
|
+
return { composedDir, manifest };
|
|
2651
|
+
}
|
|
2652
|
+
function copyTemplateToDir(composedDir, destDir, mindName, manifest) {
|
|
2653
|
+
cpSync(composedDir, destDir, { recursive: true });
|
|
2654
|
+
for (const [from, to] of Object.entries(manifest.rename)) {
|
|
2655
|
+
const fromPath = resolve12(destDir, from);
|
|
2656
|
+
if (existsSync9(fromPath)) {
|
|
2657
|
+
renameSync3(fromPath, resolve12(destDir, to));
|
|
2456
2658
|
}
|
|
2457
|
-
return result;
|
|
2458
2659
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2660
|
+
for (const file of manifest.substitute) {
|
|
2661
|
+
const path = resolve12(destDir, file);
|
|
2662
|
+
if (existsSync9(path)) {
|
|
2663
|
+
const content = readFileSync8(path, "utf-8");
|
|
2664
|
+
writeFileSync7(path, content.replaceAll("{{name}}", mindName));
|
|
2665
|
+
}
|
|
2463
2666
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2667
|
+
}
|
|
2668
|
+
function applyInitFiles(destDir) {
|
|
2669
|
+
const initDir = resolve12(destDir, ".init");
|
|
2670
|
+
if (!existsSync9(initDir)) return;
|
|
2671
|
+
const homeDir = resolve12(destDir, "home");
|
|
2672
|
+
for (const file of listFiles(initDir)) {
|
|
2673
|
+
const src = resolve12(initDir, file);
|
|
2674
|
+
const dest = resolve12(homeDir, file);
|
|
2675
|
+
const parent = dirname2(dest);
|
|
2676
|
+
if (!existsSync9(parent)) {
|
|
2677
|
+
mkdirSync6(parent, { recursive: true });
|
|
2678
|
+
}
|
|
2679
|
+
cpSync(src, dest);
|
|
2680
|
+
}
|
|
2681
|
+
rmSync(initDir, { recursive: true, force: true });
|
|
2682
|
+
}
|
|
2683
|
+
function listFiles(dir) {
|
|
2684
|
+
const results = [];
|
|
2685
|
+
function walk(current) {
|
|
2686
|
+
for (const entry of readdirSync3(current)) {
|
|
2687
|
+
const full = join(current, entry);
|
|
2688
|
+
if (statSync(full).isDirectory()) {
|
|
2689
|
+
if (entry === ".git") continue;
|
|
2690
|
+
walk(full);
|
|
2691
|
+
} else {
|
|
2692
|
+
results.push(relative(dir, full));
|
|
2474
2693
|
}
|
|
2475
2694
|
}
|
|
2476
2695
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
function getTypingMap() {
|
|
2480
|
-
if (!instance5) {
|
|
2481
|
-
instance5 = new TypingMap();
|
|
2482
|
-
}
|
|
2483
|
-
return instance5;
|
|
2696
|
+
walk(dir);
|
|
2697
|
+
return results;
|
|
2484
2698
|
}
|
|
2485
2699
|
|
|
2486
2700
|
// src/web/api/minds.ts
|
|
2487
|
-
async function startMindFull(name, baseName, variantName) {
|
|
2488
|
-
await getMindManager().startMind(name);
|
|
2489
|
-
if (variantName) return;
|
|
2490
|
-
if (findMind(baseName)?.stage === "seed") return;
|
|
2491
|
-
const dir = mindDir(baseName);
|
|
2492
|
-
const entry = findMind(baseName);
|
|
2493
|
-
await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
|
|
2494
|
-
getScheduler().loadSchedules(baseName);
|
|
2495
|
-
ensureMailAddress(baseName).catch(
|
|
2496
|
-
(err) => console.error(`[mail] failed to ensure address for ${baseName}:`, err)
|
|
2497
|
-
);
|
|
2498
|
-
const config = readVoluteConfig(dir);
|
|
2499
|
-
if (config?.tokenBudget) {
|
|
2500
|
-
getTokenBudget().setBudget(
|
|
2501
|
-
baseName,
|
|
2502
|
-
config.tokenBudget,
|
|
2503
|
-
config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
|
|
2504
|
-
);
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
function extractTextContent(content) {
|
|
2508
|
-
if (typeof content === "string") return content;
|
|
2509
|
-
if (Array.isArray(content)) {
|
|
2510
|
-
return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
2511
|
-
}
|
|
2512
|
-
return JSON.stringify(content);
|
|
2513
|
-
}
|
|
2514
|
-
function getDaemonPort() {
|
|
2515
|
-
try {
|
|
2516
|
-
const data = JSON.parse(readFileSync6(resolve11(voluteHome(), "daemon.json"), "utf-8"));
|
|
2517
|
-
return data.port;
|
|
2518
|
-
} catch (err) {
|
|
2519
|
-
if (err?.code !== "ENOENT") {
|
|
2520
|
-
console.error("[daemon] failed to read daemon.json:", err);
|
|
2521
|
-
}
|
|
2522
|
-
return void 0;
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
2701
|
async function getMindStatus(name, port) {
|
|
2526
2702
|
const manager = getMindManager();
|
|
2527
2703
|
let status = "stopped";
|
|
@@ -2564,7 +2740,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
|
|
|
2564
2740
|
await gitExec(["commit", "-m", "initial commit"], opts);
|
|
2565
2741
|
}
|
|
2566
2742
|
async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
2567
|
-
const tempWorktree =
|
|
2743
|
+
const tempWorktree = resolve13(projectRoot, ".variants", "_template_update");
|
|
2568
2744
|
let branchExists = false;
|
|
2569
2745
|
try {
|
|
2570
2746
|
await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
|
|
@@ -2575,8 +2751,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2575
2751
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
2576
2752
|
} catch {
|
|
2577
2753
|
}
|
|
2578
|
-
if (
|
|
2579
|
-
|
|
2754
|
+
if (existsSync10(tempWorktree)) {
|
|
2755
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
2580
2756
|
}
|
|
2581
2757
|
const templatesRoot = findTemplatesRoot();
|
|
2582
2758
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
@@ -2596,9 +2772,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2596
2772
|
});
|
|
2597
2773
|
}
|
|
2598
2774
|
copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
|
|
2599
|
-
const initDir =
|
|
2600
|
-
if (
|
|
2601
|
-
|
|
2775
|
+
const initDir = resolve13(tempWorktree, ".init");
|
|
2776
|
+
if (existsSync10(initDir)) {
|
|
2777
|
+
rmSync2(initDir, { recursive: true, force: true });
|
|
2602
2778
|
}
|
|
2603
2779
|
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
2604
2780
|
try {
|
|
@@ -2611,10 +2787,10 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2611
2787
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
2612
2788
|
} catch {
|
|
2613
2789
|
}
|
|
2614
|
-
if (
|
|
2615
|
-
|
|
2790
|
+
if (existsSync10(tempWorktree)) {
|
|
2791
|
+
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
2616
2792
|
}
|
|
2617
|
-
|
|
2793
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
2618
2794
|
}
|
|
2619
2795
|
}
|
|
2620
2796
|
async function mergeTemplateBranch(worktreeDir) {
|
|
@@ -2637,19 +2813,125 @@ async function mergeTemplateBranch(worktreeDir) {
|
|
|
2637
2813
|
async function npmInstallAsMind(cwd, mindName) {
|
|
2638
2814
|
if (isIsolationEnabled()) {
|
|
2639
2815
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
2640
|
-
await exec(cmd, args, { cwd, env: { ...process.env, HOME:
|
|
2816
|
+
await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve13(cwd, "home") } });
|
|
2641
2817
|
} else {
|
|
2642
2818
|
await exec("npm", ["install"], { cwd });
|
|
2643
2819
|
}
|
|
2644
2820
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2821
|
+
async function importFromArchive(c, tempDir, nameOverride, manifest) {
|
|
2822
|
+
const extractedMindDir = resolve13(tempDir, "mind");
|
|
2823
|
+
if (!existsSync10(extractedMindDir)) {
|
|
2824
|
+
return c.json({ error: "Invalid archive: missing mind/ directory" }, 400);
|
|
2825
|
+
}
|
|
2826
|
+
if (!manifest?.includes || !manifest.name || !manifest.template) {
|
|
2827
|
+
return c.json({ error: "Invalid archive manifest" }, 400);
|
|
2828
|
+
}
|
|
2829
|
+
const name = nameOverride ?? manifest.name;
|
|
2830
|
+
const nameErr = validateMindName(name);
|
|
2831
|
+
if (nameErr) return c.json({ error: nameErr }, 400);
|
|
2832
|
+
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
2833
|
+
ensureVoluteHome();
|
|
2834
|
+
const dest = mindDir(name);
|
|
2835
|
+
if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
2836
|
+
try {
|
|
2837
|
+
cpSync2(extractedMindDir, dest, { recursive: true });
|
|
2838
|
+
if (!manifest.includes.identity) {
|
|
2839
|
+
generateIdentity(dest);
|
|
2840
|
+
}
|
|
2841
|
+
const state = stateDir(name);
|
|
2842
|
+
mkdirSync7(state, { recursive: true });
|
|
2843
|
+
const channelsJson = resolve13(tempDir, "state/channels.json");
|
|
2844
|
+
if (existsSync10(channelsJson)) {
|
|
2845
|
+
cpSync2(channelsJson, resolve13(state, "channels.json"));
|
|
2846
|
+
}
|
|
2847
|
+
const envJson = resolve13(tempDir, "state/env.json");
|
|
2848
|
+
if (existsSync10(envJson)) {
|
|
2849
|
+
cpSync2(envJson, resolve13(state, "env.json"));
|
|
2850
|
+
}
|
|
2851
|
+
const port = nextPort();
|
|
2852
|
+
addMind(name, port, void 0, manifest.template);
|
|
2853
|
+
const homeDir = resolve13(dest, "home");
|
|
2854
|
+
ensureVoluteGroup();
|
|
2855
|
+
createMindUser(name, homeDir);
|
|
2856
|
+
chownMindDir(dest, name);
|
|
2857
|
+
await npmInstallAsMind(dest, name);
|
|
2858
|
+
const historyJsonl = resolve13(tempDir, "history.jsonl");
|
|
2859
|
+
if (existsSync10(historyJsonl)) {
|
|
2860
|
+
try {
|
|
2861
|
+
const db = await getDb();
|
|
2862
|
+
const lines = readFileSync9(historyJsonl, "utf-8").trim().split("\n");
|
|
2863
|
+
let imported = 0;
|
|
2864
|
+
let failed = 0;
|
|
2865
|
+
for (const line of lines) {
|
|
2866
|
+
if (!line) continue;
|
|
2867
|
+
try {
|
|
2868
|
+
const row = JSON.parse(line);
|
|
2869
|
+
if (!row.type) {
|
|
2870
|
+
failed++;
|
|
2871
|
+
continue;
|
|
2872
|
+
}
|
|
2873
|
+
await db.insert(mindHistory).values({
|
|
2874
|
+
mind: name,
|
|
2875
|
+
channel: row.channel ?? null,
|
|
2876
|
+
session: row.session ?? null,
|
|
2877
|
+
sender: row.sender ?? null,
|
|
2878
|
+
message_id: row.message_id ?? null,
|
|
2879
|
+
type: row.type,
|
|
2880
|
+
content: row.content ?? null,
|
|
2881
|
+
metadata: row.metadata ?? null,
|
|
2882
|
+
created_at: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2883
|
+
});
|
|
2884
|
+
imported++;
|
|
2885
|
+
} catch (lineErr) {
|
|
2886
|
+
logger_default.warn("Failed to import history line", logger_default.errorData(lineErr));
|
|
2887
|
+
failed++;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
if (failed > 0) {
|
|
2891
|
+
logger_default.warn(`History import: ${imported} imported, ${failed} failed`);
|
|
2892
|
+
}
|
|
2893
|
+
} catch (err) {
|
|
2894
|
+
logger_default.error("Failed to open database for history import", logger_default.errorData(err));
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
const sessionsDir = resolve13(tempDir, "sessions");
|
|
2898
|
+
if (existsSync10(sessionsDir)) {
|
|
2899
|
+
const destSessions = resolve13(dest, ".mind/sessions");
|
|
2900
|
+
mkdirSync7(destSessions, { recursive: true });
|
|
2901
|
+
for (const file of readdirSync4(sessionsDir)) {
|
|
2902
|
+
cpSync2(resolve13(sessionsDir, file), resolve13(destSessions, file));
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (!existsSync10(resolve13(dest, ".git"))) {
|
|
2906
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
|
|
2907
|
+
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
2908
|
+
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
2909
|
+
await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
|
|
2910
|
+
}
|
|
2911
|
+
chownMindDir(dest, name);
|
|
2912
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
2913
|
+
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
2914
|
+
} catch (err) {
|
|
2915
|
+
if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
2916
|
+
try {
|
|
2917
|
+
removeMind(name);
|
|
2918
|
+
} catch (cleanupErr) {
|
|
2919
|
+
logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
|
|
2920
|
+
}
|
|
2921
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
2922
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
var createMindSchema = z3.object({
|
|
2926
|
+
name: z3.string(),
|
|
2927
|
+
template: z3.string().optional(),
|
|
2928
|
+
stage: z3.enum(["seed", "sprouted"]).optional(),
|
|
2929
|
+
description: z3.string().optional(),
|
|
2930
|
+
model: z3.string().optional(),
|
|
2931
|
+
seedSoul: z3.string().optional(),
|
|
2932
|
+
skills: z3.array(z3.string()).optional()
|
|
2651
2933
|
});
|
|
2652
|
-
var
|
|
2934
|
+
var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
|
|
2653
2935
|
const body = c.req.valid("json");
|
|
2654
2936
|
const { name, template = "claude" } = body;
|
|
2655
2937
|
const nameErr = validateMindName(name);
|
|
@@ -2657,22 +2939,29 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
|
|
|
2657
2939
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
2658
2940
|
ensureVoluteHome();
|
|
2659
2941
|
const dest = mindDir(name);
|
|
2660
|
-
if (
|
|
2942
|
+
if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
2661
2943
|
const templatesRoot = findTemplatesRoot();
|
|
2662
2944
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
2663
2945
|
try {
|
|
2664
2946
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
2665
2947
|
applyInitFiles(dest);
|
|
2948
|
+
const { publicKeyPem } = generateIdentity(dest);
|
|
2666
2949
|
if (body.model) {
|
|
2667
|
-
const configPath =
|
|
2668
|
-
const existing =
|
|
2950
|
+
const configPath = resolve13(dest, "home/.config/config.json");
|
|
2951
|
+
const existing = existsSync10(configPath) ? JSON.parse(readFileSync9(configPath, "utf-8")) : {};
|
|
2669
2952
|
existing.model = body.model;
|
|
2670
|
-
|
|
2953
|
+
writeFileSync8(configPath, `${JSON.stringify(existing, null, 2)}
|
|
2671
2954
|
`);
|
|
2672
2955
|
}
|
|
2956
|
+
const mindPrompts = await getMindPromptDefaults();
|
|
2957
|
+
writeFileSync8(
|
|
2958
|
+
resolve13(dest, "home/.config/prompts.json"),
|
|
2959
|
+
`${JSON.stringify(mindPrompts, null, 2)}
|
|
2960
|
+
`
|
|
2961
|
+
);
|
|
2673
2962
|
const port = nextPort();
|
|
2674
|
-
addMind(name, port, body.stage);
|
|
2675
|
-
const homeDir =
|
|
2963
|
+
addMind(name, port, body.stage, template);
|
|
2964
|
+
const homeDir = resolve13(dest, "home");
|
|
2676
2965
|
ensureVoluteGroup();
|
|
2677
2966
|
createMindUser(name, homeDir);
|
|
2678
2967
|
chownMindDir(dest, name);
|
|
@@ -2683,47 +2972,65 @@ var app7 = new Hono7().post("/", requireAdmin, zValidator2("json", createMindSch
|
|
|
2683
2972
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
2684
2973
|
await initTemplateBranch(dest, composedDir, manifest, name, env);
|
|
2685
2974
|
} catch (err) {
|
|
2686
|
-
|
|
2687
|
-
|
|
2975
|
+
logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
|
|
2976
|
+
rmSync2(resolve13(dest, ".git"), { recursive: true, force: true });
|
|
2688
2977
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
2689
2978
|
}
|
|
2979
|
+
try {
|
|
2980
|
+
await addSharedWorktree(name, dest);
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
logger_default.warn(`failed to add shared worktree for ${name}`, logger_default.errorData(err));
|
|
2983
|
+
}
|
|
2690
2984
|
chownMindDir(dest, name);
|
|
2691
2985
|
if (body.stage === "seed") {
|
|
2692
2986
|
const descLine = body.description ? `
|
|
2693
2987
|
The human who planted you described you as: "${body.description}"
|
|
2694
2988
|
` : "";
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2989
|
+
const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
|
|
2990
|
+
const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
|
|
2991
|
+
writeFileSync8(resolve13(dest, "home/SOUL.md"), seedSoul);
|
|
2992
|
+
}
|
|
2993
|
+
const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
|
|
2994
|
+
const skillWarnings = [];
|
|
2995
|
+
for (const skillId of skillSet) {
|
|
2996
|
+
try {
|
|
2997
|
+
await installSkill(name, dest, skillId);
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
logger_default.error(`failed to install skill ${skillId} for ${name}`, logger_default.errorData(err));
|
|
3000
|
+
skillWarnings.push(`Failed to install skill: ${skillId}`);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
if (body.stage !== "seed") {
|
|
3004
|
+
const customSoul = await getPromptIfCustom("default_soul");
|
|
3005
|
+
if (customSoul) {
|
|
3006
|
+
writeFileSync8(resolve13(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
|
|
3007
|
+
}
|
|
3008
|
+
const customMemory = await getPromptIfCustom("default_memory");
|
|
3009
|
+
if (customMemory) {
|
|
3010
|
+
writeFileSync8(resolve13(dest, "home/MEMORY.md"), customMemory);
|
|
2708
3011
|
}
|
|
2709
3012
|
}
|
|
3013
|
+
publishPublicKey(name, publicKeyPem).catch(
|
|
3014
|
+
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
3015
|
+
);
|
|
2710
3016
|
return c.json({
|
|
2711
3017
|
ok: true,
|
|
2712
3018
|
name,
|
|
2713
3019
|
port,
|
|
2714
3020
|
stage: body.stage ?? "sprouted",
|
|
2715
3021
|
message: `Created mind: ${name} (port ${port})`,
|
|
2716
|
-
...gitWarning && { warning: gitWarning }
|
|
3022
|
+
...gitWarning && { warning: gitWarning },
|
|
3023
|
+
...skillWarnings.length > 0 && { skillWarnings }
|
|
2717
3024
|
});
|
|
2718
3025
|
} catch (err) {
|
|
2719
|
-
if (
|
|
3026
|
+
if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
2720
3027
|
try {
|
|
2721
3028
|
removeMind(name);
|
|
2722
3029
|
} catch {
|
|
2723
3030
|
}
|
|
2724
3031
|
return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
|
|
2725
3032
|
} finally {
|
|
2726
|
-
|
|
3033
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
2727
3034
|
}
|
|
2728
3035
|
}).post("/import", requireAdmin, async (c) => {
|
|
2729
3036
|
let body;
|
|
@@ -2732,14 +3039,17 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
|
|
|
2732
3039
|
} catch {
|
|
2733
3040
|
return c.json({ error: "Invalid JSON" }, 400);
|
|
2734
3041
|
}
|
|
3042
|
+
if (body.archivePath && body.manifest) {
|
|
3043
|
+
return importFromArchive(c, body.archivePath, body.name, body.manifest);
|
|
3044
|
+
}
|
|
2735
3045
|
const wsDir = body.workspacePath;
|
|
2736
|
-
if (!wsDir || !
|
|
3046
|
+
if (!wsDir || !existsSync10(resolve13(wsDir, "SOUL.md")) || !existsSync10(resolve13(wsDir, "IDENTITY.md"))) {
|
|
2737
3047
|
return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
|
|
2738
3048
|
}
|
|
2739
|
-
const soul =
|
|
2740
|
-
const identity =
|
|
2741
|
-
const userPath =
|
|
2742
|
-
const user =
|
|
3049
|
+
const soul = readFileSync9(resolve13(wsDir, "SOUL.md"), "utf-8");
|
|
3050
|
+
const identity = readFileSync9(resolve13(wsDir, "IDENTITY.md"), "utf-8");
|
|
3051
|
+
const userPath = resolve13(wsDir, "USER.md");
|
|
3052
|
+
const user = existsSync10(userPath) ? readFileSync9(userPath, "utf-8") : "";
|
|
2743
3053
|
const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
|
|
2744
3054
|
const template = body.template ?? "claude";
|
|
2745
3055
|
const nameErr = validateMindName(name);
|
|
@@ -2759,38 +3069,39 @@ ${user.trimEnd()}
|
|
|
2759
3069
|
` : "";
|
|
2760
3070
|
ensureVoluteHome();
|
|
2761
3071
|
const dest = mindDir(name);
|
|
2762
|
-
if (
|
|
3072
|
+
if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
2763
3073
|
const templatesRoot = findTemplatesRoot();
|
|
2764
3074
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
2765
3075
|
try {
|
|
2766
3076
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
2767
3077
|
applyInitFiles(dest);
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
const
|
|
3078
|
+
const { publicKeyPem: importPublicKey } = generateIdentity(dest);
|
|
3079
|
+
writeFileSync8(resolve13(dest, "home/SOUL.md"), mergedSoul);
|
|
3080
|
+
const wsMemoryPath = resolve13(wsDir, "MEMORY.md");
|
|
3081
|
+
const hasMemory = existsSync10(wsMemoryPath);
|
|
2771
3082
|
if (hasMemory) {
|
|
2772
|
-
const existingMemory =
|
|
2773
|
-
|
|
2774
|
-
|
|
3083
|
+
const existingMemory = readFileSync9(wsMemoryPath, "utf-8");
|
|
3084
|
+
writeFileSync8(
|
|
3085
|
+
resolve13(dest, "home/MEMORY.md"),
|
|
2775
3086
|
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
2776
3087
|
);
|
|
2777
3088
|
} else if (user) {
|
|
2778
|
-
|
|
3089
|
+
writeFileSync8(resolve13(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
2779
3090
|
`);
|
|
2780
3091
|
}
|
|
2781
|
-
const wsMemoryDir =
|
|
3092
|
+
const wsMemoryDir = resolve13(wsDir, "memory");
|
|
2782
3093
|
let dailyLogCount = 0;
|
|
2783
|
-
if (
|
|
2784
|
-
const destMemoryDir =
|
|
2785
|
-
const files =
|
|
3094
|
+
if (existsSync10(wsMemoryDir)) {
|
|
3095
|
+
const destMemoryDir = resolve13(dest, "home/memory");
|
|
3096
|
+
const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
2786
3097
|
for (const file of files) {
|
|
2787
|
-
|
|
3098
|
+
cpSync2(resolve13(wsMemoryDir, file), resolve13(destMemoryDir, file));
|
|
2788
3099
|
}
|
|
2789
3100
|
dailyLogCount = files.length;
|
|
2790
3101
|
}
|
|
2791
3102
|
const port = nextPort();
|
|
2792
|
-
addMind(name, port);
|
|
2793
|
-
const homeDir =
|
|
3103
|
+
addMind(name, port, void 0, template);
|
|
3104
|
+
const homeDir = resolve13(dest, "home");
|
|
2794
3105
|
ensureVoluteGroup();
|
|
2795
3106
|
createMindUser(name, homeDir);
|
|
2796
3107
|
chownMindDir(dest, name);
|
|
@@ -2798,42 +3109,50 @@ ${user.trimEnd()}
|
|
|
2798
3109
|
if (!hasMemory && dailyLogCount > 0) {
|
|
2799
3110
|
await consolidateMemory(dest);
|
|
2800
3111
|
}
|
|
2801
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3112
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
|
|
2802
3113
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
2803
3114
|
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
2804
3115
|
await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
|
|
2805
|
-
const sessionFile = body.sessionPath ?
|
|
2806
|
-
if (sessionFile &&
|
|
3116
|
+
const sessionFile = body.sessionPath ? resolve13(body.sessionPath) : findOpenClawSession(wsDir);
|
|
3117
|
+
if (sessionFile && existsSync10(sessionFile)) {
|
|
2807
3118
|
if (template === "pi") {
|
|
2808
3119
|
importPiSession(sessionFile, dest);
|
|
2809
3120
|
} else if (template === "claude") {
|
|
2810
3121
|
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
3122
|
+
const mindRuntimeDir = resolve13(dest, ".mind");
|
|
3123
|
+
mkdirSync7(mindRuntimeDir, { recursive: true });
|
|
3124
|
+
writeFileSync8(resolve13(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
|
|
2814
3125
|
}
|
|
2815
3126
|
}
|
|
2816
3127
|
importOpenClawConnectors(name, dest);
|
|
3128
|
+
try {
|
|
3129
|
+
await addSharedWorktree(name, dest);
|
|
3130
|
+
} catch (err) {
|
|
3131
|
+
logger_default.warn(`failed to add shared worktree for ${name}`, logger_default.errorData(err));
|
|
3132
|
+
}
|
|
2817
3133
|
chownMindDir(dest, name);
|
|
3134
|
+
publishPublicKey(name, importPublicKey).catch(
|
|
3135
|
+
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
3136
|
+
);
|
|
2818
3137
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
2819
3138
|
} catch (err) {
|
|
2820
|
-
if (
|
|
3139
|
+
if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
2821
3140
|
try {
|
|
2822
3141
|
removeMind(name);
|
|
2823
3142
|
} catch {
|
|
2824
3143
|
}
|
|
2825
3144
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
2826
3145
|
} finally {
|
|
2827
|
-
|
|
3146
|
+
rmSync2(composedDir, { recursive: true, force: true });
|
|
2828
3147
|
}
|
|
2829
3148
|
}).get("/", async (c) => {
|
|
2830
3149
|
const entries = readRegistry();
|
|
2831
3150
|
let lastActiveMap = /* @__PURE__ */ new Map();
|
|
2832
3151
|
try {
|
|
2833
|
-
const
|
|
2834
|
-
const lastActiveRows = await
|
|
3152
|
+
const db = await getDb();
|
|
3153
|
+
const lastActiveRows = await db.select({
|
|
2835
3154
|
mind: mindHistory.mind,
|
|
2836
|
-
lastActiveAt:
|
|
3155
|
+
lastActiveAt: sql2`MAX(${mindHistory.created_at})`
|
|
2837
3156
|
}).from(mindHistory).groupBy(mindHistory.mind);
|
|
2838
3157
|
lastActiveMap = new Map(lastActiveRows.map((r) => [r.mind, r.lastActiveAt]));
|
|
2839
3158
|
} catch {
|
|
@@ -2841,7 +3160,7 @@ ${user.trimEnd()}
|
|
|
2841
3160
|
const minds = await Promise.all(
|
|
2842
3161
|
entries.map(async (entry) => {
|
|
2843
3162
|
const { status, channels } = await getMindStatus(entry.name, entry.port);
|
|
2844
|
-
const hasPages =
|
|
3163
|
+
const hasPages = existsSync10(resolve13(mindDir(entry.name), "home", "pages"));
|
|
2845
3164
|
return {
|
|
2846
3165
|
...entry,
|
|
2847
3166
|
status,
|
|
@@ -2856,19 +3175,19 @@ ${user.trimEnd()}
|
|
|
2856
3175
|
const entries = readRegistry();
|
|
2857
3176
|
const pages = [];
|
|
2858
3177
|
for (const entry of entries) {
|
|
2859
|
-
const pagesDir =
|
|
2860
|
-
if (!
|
|
3178
|
+
const pagesDir = resolve13(mindDir(entry.name), "home", "pages");
|
|
3179
|
+
if (!existsSync10(pagesDir)) continue;
|
|
2861
3180
|
let items;
|
|
2862
3181
|
try {
|
|
2863
|
-
items =
|
|
3182
|
+
items = readdirSync4(pagesDir);
|
|
2864
3183
|
} catch (err) {
|
|
2865
3184
|
logger_default.warn("Failed to read pages dir", { mind: entry.name, error: err.message });
|
|
2866
3185
|
continue;
|
|
2867
3186
|
}
|
|
2868
3187
|
for (const item of items) {
|
|
2869
|
-
const fullPath =
|
|
3188
|
+
const fullPath = resolve13(pagesDir, item);
|
|
2870
3189
|
try {
|
|
2871
|
-
const s =
|
|
3190
|
+
const s = statSync2(fullPath);
|
|
2872
3191
|
if (s.isFile()) {
|
|
2873
3192
|
pages.push({
|
|
2874
3193
|
mind: entry.name,
|
|
@@ -2877,12 +3196,12 @@ ${user.trimEnd()}
|
|
|
2877
3196
|
url: `/pages/${entry.name}/${item}`
|
|
2878
3197
|
});
|
|
2879
3198
|
} else if (s.isDirectory()) {
|
|
2880
|
-
const indexPath =
|
|
2881
|
-
if (
|
|
2882
|
-
const indexStat =
|
|
3199
|
+
const indexPath = resolve13(fullPath, "index.html");
|
|
3200
|
+
if (existsSync10(indexPath)) {
|
|
3201
|
+
const indexStat = statSync2(indexPath);
|
|
2883
3202
|
pages.push({
|
|
2884
3203
|
mind: entry.name,
|
|
2885
|
-
file:
|
|
3204
|
+
file: join2(item, "index.html"),
|
|
2886
3205
|
modified: indexStat.mtime.toISOString(),
|
|
2887
3206
|
url: `/pages/${entry.name}/${item}/`
|
|
2888
3207
|
});
|
|
@@ -2903,7 +3222,7 @@ ${user.trimEnd()}
|
|
|
2903
3222
|
const name = c.req.param("name");
|
|
2904
3223
|
const entry = findMind(name);
|
|
2905
3224
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
2906
|
-
if (!
|
|
3225
|
+
if (!existsSync10(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
|
|
2907
3226
|
const { status, channels } = await getMindStatus(name, entry.port);
|
|
2908
3227
|
const variants = readVariants(name);
|
|
2909
3228
|
const manager = getMindManager();
|
|
@@ -2918,7 +3237,7 @@ ${user.trimEnd()}
|
|
|
2918
3237
|
return { name: v.name, port: v.port, status: variantStatus };
|
|
2919
3238
|
})
|
|
2920
3239
|
);
|
|
2921
|
-
const hasPages =
|
|
3240
|
+
const hasPages = existsSync10(resolve13(mindDir(name), "home", "pages"));
|
|
2922
3241
|
return c.json({ ...entry, status, channels, variants: variantStatuses, hasPages });
|
|
2923
3242
|
}).post("/:name/start", requireAdmin, async (c) => {
|
|
2924
3243
|
const name = c.req.param("name");
|
|
@@ -2930,13 +3249,13 @@ ${user.trimEnd()}
|
|
|
2930
3249
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
2931
3250
|
} else {
|
|
2932
3251
|
const dir = mindDir(baseName);
|
|
2933
|
-
if (!
|
|
3252
|
+
if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
2934
3253
|
}
|
|
2935
3254
|
if (getMindManager().isRunning(name)) {
|
|
2936
3255
|
return c.json({ error: "Mind already running" }, 409);
|
|
2937
3256
|
}
|
|
2938
3257
|
try {
|
|
2939
|
-
await startMindFull(name
|
|
3258
|
+
await startMindFull(name);
|
|
2940
3259
|
return c.json({ ok: true });
|
|
2941
3260
|
} catch (err) {
|
|
2942
3261
|
return c.json({ error: err instanceof Error ? err.message : "Failed to start mind" }, 500);
|
|
@@ -2951,7 +3270,7 @@ ${user.trimEnd()}
|
|
|
2951
3270
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
2952
3271
|
} else {
|
|
2953
3272
|
const dir = mindDir(baseName);
|
|
2954
|
-
if (!
|
|
3273
|
+
if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
2955
3274
|
}
|
|
2956
3275
|
let context;
|
|
2957
3276
|
const contentType = c.req.header("content-type");
|
|
@@ -2960,17 +3279,13 @@ ${user.trimEnd()}
|
|
|
2960
3279
|
const body = await c.req.json();
|
|
2961
3280
|
if (body?.context) context = body.context;
|
|
2962
3281
|
} catch (err) {
|
|
2963
|
-
|
|
3282
|
+
logger_default.error(`failed to parse restart context for ${name}`, logger_default.errorData(err));
|
|
2964
3283
|
}
|
|
2965
3284
|
}
|
|
2966
3285
|
const manager = getMindManager();
|
|
2967
3286
|
try {
|
|
2968
3287
|
if (manager.isRunning(name)) {
|
|
2969
|
-
|
|
2970
|
-
await getConnectorManager().stopConnectors(baseName);
|
|
2971
|
-
getTokenBudget().removeBudget(baseName);
|
|
2972
|
-
}
|
|
2973
|
-
await manager.stopMind(name);
|
|
3288
|
+
await stopMindFull(name);
|
|
2974
3289
|
}
|
|
2975
3290
|
if (context?.type === "merge" && context.name && !variantName) {
|
|
2976
3291
|
const mergeVariantName = String(context.name);
|
|
@@ -2978,11 +3293,11 @@ ${user.trimEnd()}
|
|
|
2978
3293
|
if (branchErr) {
|
|
2979
3294
|
return c.json({ error: `Invalid variant name: ${branchErr}` }, 400);
|
|
2980
3295
|
}
|
|
2981
|
-
|
|
3296
|
+
logger_default.error(`merging variant for ${baseName}: ${mergeVariantName}`);
|
|
2982
3297
|
const variant = findVariant(baseName, mergeVariantName);
|
|
2983
3298
|
if (variant) {
|
|
2984
3299
|
const projectRoot = mindDir(baseName);
|
|
2985
|
-
if (
|
|
3300
|
+
if (existsSync10(variant.path)) {
|
|
2986
3301
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
2987
3302
|
if (status) {
|
|
2988
3303
|
try {
|
|
@@ -2991,9 +3306,9 @@ ${user.trimEnd()}
|
|
|
2991
3306
|
cwd: variant.path
|
|
2992
3307
|
});
|
|
2993
3308
|
} catch (e) {
|
|
2994
|
-
|
|
2995
|
-
`
|
|
2996
|
-
e
|
|
3309
|
+
logger_default.error(
|
|
3310
|
+
`failed to auto-commit variant worktree for ${baseName}`,
|
|
3311
|
+
logger_default.errorData(e)
|
|
2997
3312
|
);
|
|
2998
3313
|
}
|
|
2999
3314
|
}
|
|
@@ -3006,11 +3321,11 @@ ${user.trimEnd()}
|
|
|
3006
3321
|
cwd: projectRoot
|
|
3007
3322
|
});
|
|
3008
3323
|
} catch (e) {
|
|
3009
|
-
|
|
3324
|
+
logger_default.error(`failed to auto-commit main worktree for ${baseName}`, logger_default.errorData(e));
|
|
3010
3325
|
}
|
|
3011
3326
|
}
|
|
3012
3327
|
await gitExec(["merge", variant.branch], { cwd: projectRoot });
|
|
3013
|
-
if (
|
|
3328
|
+
if (existsSync10(variant.path)) {
|
|
3014
3329
|
try {
|
|
3015
3330
|
await gitExec(["worktree", "remove", "--force", variant.path], {
|
|
3016
3331
|
cwd: projectRoot
|
|
@@ -3027,7 +3342,7 @@ ${user.trimEnd()}
|
|
|
3027
3342
|
try {
|
|
3028
3343
|
await npmInstallAsMind(projectRoot, baseName);
|
|
3029
3344
|
} catch (e) {
|
|
3030
|
-
|
|
3345
|
+
logger_default.error(`npm install failed after merge for ${baseName}`, logger_default.errorData(e));
|
|
3031
3346
|
}
|
|
3032
3347
|
}
|
|
3033
3348
|
}
|
|
@@ -3036,18 +3351,18 @@ ${user.trimEnd()}
|
|
|
3036
3351
|
}
|
|
3037
3352
|
if (context?.type === "sprouted" && !variantName) {
|
|
3038
3353
|
try {
|
|
3039
|
-
const
|
|
3040
|
-
const activeConvs = await
|
|
3354
|
+
const db = await getDb();
|
|
3355
|
+
const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq4(conversations.mind_name, baseName)).all();
|
|
3041
3356
|
for (const conv of activeConvs) {
|
|
3042
3357
|
await addMessage(conv.id, "assistant", "system", [
|
|
3043
3358
|
{ type: "text", text: "[seed has sprouted]" }
|
|
3044
3359
|
]);
|
|
3045
3360
|
}
|
|
3046
3361
|
} catch (err) {
|
|
3047
|
-
|
|
3362
|
+
logger_default.error(`failed to inject sprouted message for ${baseName}`, logger_default.errorData(err));
|
|
3048
3363
|
}
|
|
3049
3364
|
}
|
|
3050
|
-
await startMindFull(name
|
|
3365
|
+
await startMindFull(name);
|
|
3051
3366
|
return c.json({ ok: true });
|
|
3052
3367
|
} catch (err) {
|
|
3053
3368
|
return c.json({ error: err instanceof Error ? err.message : "Failed to restart mind" }, 500);
|
|
@@ -3066,12 +3381,7 @@ ${user.trimEnd()}
|
|
|
3066
3381
|
return c.json({ error: "Mind is not running" }, 409);
|
|
3067
3382
|
}
|
|
3068
3383
|
try {
|
|
3069
|
-
|
|
3070
|
-
await getConnectorManager().stopConnectors(baseName);
|
|
3071
|
-
getScheduler().unloadSchedules(baseName);
|
|
3072
|
-
getTokenBudget().removeBudget(baseName);
|
|
3073
|
-
}
|
|
3074
|
-
await manager.stopMind(name);
|
|
3384
|
+
await stopMindFull(name);
|
|
3075
3385
|
return c.json({ ok: true });
|
|
3076
3386
|
} catch (err) {
|
|
3077
3387
|
return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
|
|
@@ -3093,19 +3403,22 @@ ${user.trimEnd()}
|
|
|
3093
3403
|
const force = c.req.query("force") === "true";
|
|
3094
3404
|
const manager = getMindManager();
|
|
3095
3405
|
if (manager.isRunning(name)) {
|
|
3096
|
-
await
|
|
3097
|
-
getTokenBudget().removeBudget(name);
|
|
3098
|
-
await manager.stopMind(name);
|
|
3406
|
+
await stopMindFull(name);
|
|
3099
3407
|
}
|
|
3100
3408
|
removeAllVariants(name);
|
|
3409
|
+
try {
|
|
3410
|
+
await removeSharedWorktree(name, dir);
|
|
3411
|
+
} catch (err) {
|
|
3412
|
+
logger_default.warn(`failed to clean up shared worktree for ${name}`, logger_default.errorData(err));
|
|
3413
|
+
}
|
|
3101
3414
|
removeMind(name);
|
|
3102
3415
|
await deleteMindUser2(name);
|
|
3103
3416
|
const state = stateDir(name);
|
|
3104
|
-
if (
|
|
3105
|
-
|
|
3417
|
+
if (existsSync10(state)) {
|
|
3418
|
+
rmSync2(state, { recursive: true, force: true });
|
|
3106
3419
|
}
|
|
3107
|
-
if (force &&
|
|
3108
|
-
|
|
3420
|
+
if (force && existsSync10(dir)) {
|
|
3421
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
3109
3422
|
deleteMindUser(name);
|
|
3110
3423
|
}
|
|
3111
3424
|
return c.json({ ok: true });
|
|
@@ -3114,17 +3427,17 @@ ${user.trimEnd()}
|
|
|
3114
3427
|
const entry = findMind(mindName);
|
|
3115
3428
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3116
3429
|
const dir = mindDir(mindName);
|
|
3117
|
-
if (!
|
|
3430
|
+
if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
3118
3431
|
let body = {};
|
|
3119
3432
|
try {
|
|
3120
3433
|
body = await c.req.json();
|
|
3121
3434
|
} catch {
|
|
3122
3435
|
}
|
|
3123
|
-
const template = body.template ?? "claude";
|
|
3436
|
+
const template = body.template ?? entry.template ?? "claude";
|
|
3124
3437
|
const UPGRADE_VARIANT = "upgrade";
|
|
3125
3438
|
if (body.continue) {
|
|
3126
|
-
const worktreeDir2 =
|
|
3127
|
-
if (!
|
|
3439
|
+
const worktreeDir2 = resolve13(dir, ".variants", UPGRADE_VARIANT);
|
|
3440
|
+
if (!existsSync10(worktreeDir2)) {
|
|
3128
3441
|
return c.json({ error: "No upgrade in progress" }, 400);
|
|
3129
3442
|
}
|
|
3130
3443
|
const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
|
|
@@ -3173,9 +3486,9 @@ ${user.trimEnd()}
|
|
|
3173
3486
|
try {
|
|
3174
3487
|
chownMindDir(dir, mindName);
|
|
3175
3488
|
} catch (chownErr) {
|
|
3176
|
-
|
|
3177
|
-
`
|
|
3178
|
-
chownErr
|
|
3489
|
+
logger_default.error(
|
|
3490
|
+
`failed to fix ownership during upgrade cleanup for ${mindName}`,
|
|
3491
|
+
logger_default.errorData(chownErr)
|
|
3179
3492
|
);
|
|
3180
3493
|
}
|
|
3181
3494
|
return c.json(
|
|
@@ -3184,8 +3497,8 @@ ${user.trimEnd()}
|
|
|
3184
3497
|
);
|
|
3185
3498
|
}
|
|
3186
3499
|
}
|
|
3187
|
-
const worktreeDir =
|
|
3188
|
-
if (
|
|
3500
|
+
const worktreeDir = resolve13(dir, ".variants", UPGRADE_VARIANT);
|
|
3501
|
+
if (existsSync10(worktreeDir)) {
|
|
3189
3502
|
return c.json(
|
|
3190
3503
|
{ error: "Upgrade variant already exists. Use continue or delete it first." },
|
|
3191
3504
|
409
|
|
@@ -3196,10 +3509,20 @@ ${user.trimEnd()}
|
|
|
3196
3509
|
await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
3197
3510
|
} catch {
|
|
3198
3511
|
}
|
|
3512
|
+
if (!existsSync10(resolve13(dir, "home", "shared"))) {
|
|
3513
|
+
try {
|
|
3514
|
+
await addSharedWorktree(mindName, dir);
|
|
3515
|
+
} catch (err) {
|
|
3516
|
+
logger_default.warn(
|
|
3517
|
+
`failed to add shared worktree during upgrade for ${mindName}`,
|
|
3518
|
+
logger_default.errorData(err)
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3199
3522
|
await updateTemplateBranch(dir, template, mindName);
|
|
3200
|
-
const parentDir =
|
|
3201
|
-
if (!
|
|
3202
|
-
|
|
3523
|
+
const parentDir = resolve13(dir, ".variants");
|
|
3524
|
+
if (!existsSync10(parentDir)) {
|
|
3525
|
+
mkdirSync7(parentDir, { recursive: true });
|
|
3203
3526
|
}
|
|
3204
3527
|
await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
3205
3528
|
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
@@ -3245,9 +3568,9 @@ ${user.trimEnd()}
|
|
|
3245
3568
|
try {
|
|
3246
3569
|
chownMindDir(dir, mindName);
|
|
3247
3570
|
} catch (chownErr) {
|
|
3248
|
-
|
|
3249
|
-
`
|
|
3250
|
-
chownErr
|
|
3571
|
+
logger_default.error(
|
|
3572
|
+
`failed to fix ownership during upgrade cleanup for ${mindName}`,
|
|
3573
|
+
logger_default.errorData(chownErr)
|
|
3251
3574
|
);
|
|
3252
3575
|
}
|
|
3253
3576
|
return c.json(
|
|
@@ -3260,11 +3583,9 @@ ${user.trimEnd()}
|
|
|
3260
3583
|
const [baseName, variantName] = name.split("@", 2);
|
|
3261
3584
|
const entry = findMind(baseName);
|
|
3262
3585
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3263
|
-
let port = entry.port;
|
|
3264
3586
|
if (variantName) {
|
|
3265
3587
|
const variant = findVariant(baseName, variantName);
|
|
3266
3588
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
3267
|
-
port = variant.port;
|
|
3268
3589
|
}
|
|
3269
3590
|
if (!getMindManager().isRunning(name)) {
|
|
3270
3591
|
return c.json({ error: "Mind is not running" }, 409);
|
|
@@ -3274,15 +3595,15 @@ ${user.trimEnd()}
|
|
|
3274
3595
|
try {
|
|
3275
3596
|
parsed = JSON.parse(body);
|
|
3276
3597
|
} catch (err) {
|
|
3277
|
-
|
|
3598
|
+
logger_default.error(`failed to parse message body for ${baseName}`, logger_default.errorData(err));
|
|
3278
3599
|
}
|
|
3279
3600
|
const channel = parsed?.channel ?? "unknown";
|
|
3280
|
-
const
|
|
3601
|
+
const db = await getDb();
|
|
3281
3602
|
if (parsed) {
|
|
3282
3603
|
try {
|
|
3283
3604
|
const sender2 = parsed.sender ?? null;
|
|
3284
3605
|
const content = extractTextContent(parsed.content);
|
|
3285
|
-
await
|
|
3606
|
+
await db.insert(mindHistory).values({
|
|
3286
3607
|
mind: baseName,
|
|
3287
3608
|
type: "inbound",
|
|
3288
3609
|
channel,
|
|
@@ -3290,7 +3611,7 @@ ${user.trimEnd()}
|
|
|
3290
3611
|
content
|
|
3291
3612
|
});
|
|
3292
3613
|
} catch (err) {
|
|
3293
|
-
|
|
3614
|
+
logger_default.error(`failed to persist inbound message for ${baseName}`, logger_default.errorData(err));
|
|
3294
3615
|
}
|
|
3295
3616
|
}
|
|
3296
3617
|
const budget = getTokenBudget();
|
|
@@ -3304,16 +3625,31 @@ ${user.trimEnd()}
|
|
|
3304
3625
|
});
|
|
3305
3626
|
return c.json({ error: "Token budget exceeded \u2014 message queued for next period" }, 429);
|
|
3306
3627
|
}
|
|
3628
|
+
if (!parsed) return c.json({ error: "Invalid JSON" }, 400);
|
|
3307
3629
|
const typingMap = getTypingMap();
|
|
3308
|
-
const sender = parsed
|
|
3630
|
+
const sender = parsed.sender ?? "";
|
|
3309
3631
|
if (sender) typingMap.delete(channel, sender);
|
|
3310
3632
|
const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
|
|
3311
|
-
|
|
3312
|
-
if (parsed && currentlyTyping.length > 0) {
|
|
3633
|
+
if (currentlyTyping.length > 0) {
|
|
3313
3634
|
parsed.typing = currentlyTyping;
|
|
3314
|
-
forwardBody = JSON.stringify(parsed);
|
|
3315
3635
|
}
|
|
3316
|
-
if (
|
|
3636
|
+
if (sender && findMind(sender)) {
|
|
3637
|
+
try {
|
|
3638
|
+
const senderDir = mindDir(sender);
|
|
3639
|
+
const senderPrivateKey = getPrivateKey(senderDir);
|
|
3640
|
+
const senderPublicKey = getPublicKey(senderDir);
|
|
3641
|
+
if (senderPrivateKey && senderPublicKey) {
|
|
3642
|
+
const textContent = extractTextContent(parsed.content);
|
|
3643
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
3644
|
+
parsed.signature = signMessage(senderPrivateKey, textContent, timestamp);
|
|
3645
|
+
parsed.signatureTimestamp = timestamp;
|
|
3646
|
+
parsed.signerFingerprint = getFingerprint(senderPublicKey);
|
|
3647
|
+
}
|
|
3648
|
+
} catch (err) {
|
|
3649
|
+
logger_default.warn(`failed to sign message from ${sender}`, { error: err.message });
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
if (budgetStatus === "warning") {
|
|
3317
3653
|
const usage = budget.getUsage(baseName);
|
|
3318
3654
|
const pct = usage?.percentUsed ?? 80;
|
|
3319
3655
|
const warningText = `
|
|
@@ -3324,12 +3660,11 @@ ${user.trimEnd()}
|
|
|
3324
3660
|
parsed.content = [...parsed.content, { type: "text", text: warningText }];
|
|
3325
3661
|
}
|
|
3326
3662
|
budget.acknowledgeWarning(baseName);
|
|
3327
|
-
forwardBody = JSON.stringify(parsed);
|
|
3328
3663
|
}
|
|
3329
3664
|
const seedEntry = findMind(baseName);
|
|
3330
|
-
if (seedEntry?.stage === "seed"
|
|
3665
|
+
if (seedEntry?.stage === "seed") {
|
|
3331
3666
|
try {
|
|
3332
|
-
const countResult = await
|
|
3667
|
+
const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
|
|
3333
3668
|
const msgCount = countResult[0]?.count ?? 0;
|
|
3334
3669
|
if (msgCount >= 10 && msgCount % 10 === 0) {
|
|
3335
3670
|
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.]";
|
|
@@ -3338,27 +3673,29 @@ ${user.trimEnd()}
|
|
|
3338
3673
|
} else if (Array.isArray(parsed.content)) {
|
|
3339
3674
|
parsed.content = [...parsed.content, { type: "text", text: nudge }];
|
|
3340
3675
|
}
|
|
3341
|
-
forwardBody = JSON.stringify(parsed);
|
|
3342
3676
|
}
|
|
3343
3677
|
} catch (err) {
|
|
3344
|
-
|
|
3345
|
-
}
|
|
3678
|
+
logger_default.error(`failed to check seed message count for ${baseName}`, logger_default.errorData(err));
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
const deliveryPayload = {
|
|
3682
|
+
channel: parsed.channel,
|
|
3683
|
+
sender: parsed.sender ?? null,
|
|
3684
|
+
content: parsed.content,
|
|
3685
|
+
conversationId: parsed.conversationId ?? void 0,
|
|
3686
|
+
typing: parsed.typing,
|
|
3687
|
+
platform: parsed.platform ?? void 0,
|
|
3688
|
+
isDM: parsed.isDM ?? void 0,
|
|
3689
|
+
participants: parsed.participants ?? void 0,
|
|
3690
|
+
participantCount: parsed.participantCount ?? void 0
|
|
3691
|
+
};
|
|
3692
|
+
if (parsed.signature) {
|
|
3693
|
+
deliveryPayload.signature = parsed.signature;
|
|
3694
|
+
deliveryPayload.signatureTimestamp = parsed.signatureTimestamp;
|
|
3695
|
+
deliveryPayload.signerFingerprint = parsed.signerFingerprint;
|
|
3346
3696
|
}
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
|
|
3350
|
-
fetch(`http://127.0.0.1:${port}/message`, {
|
|
3351
|
-
method: "POST",
|
|
3352
|
-
headers: { "Content-Type": "application/json" },
|
|
3353
|
-
body: forwardBody
|
|
3354
|
-
}).then(async (res) => {
|
|
3355
|
-
if (!res.ok) {
|
|
3356
|
-
const text2 = await res.text().catch(() => "");
|
|
3357
|
-
console.error(`[daemon] mind ${name} responded with ${res.status}: ${text2}`);
|
|
3358
|
-
}
|
|
3359
|
-
}).catch((err) => {
|
|
3360
|
-
console.error(`[daemon] mind ${name} unreachable on port ${port}:`, err);
|
|
3361
|
-
typingMap.delete(channel, baseName);
|
|
3697
|
+
getDeliveryManager().routeAndDeliver(name, deliveryPayload).catch((err) => {
|
|
3698
|
+
logger_default.error(`delivery failed for ${name}`, logger_default.errorData(err));
|
|
3362
3699
|
});
|
|
3363
3700
|
return c.json({ ok: true });
|
|
3364
3701
|
}).get("/:name/budget", async (c) => {
|
|
@@ -3367,6 +3704,19 @@ ${user.trimEnd()}
|
|
|
3367
3704
|
const usage = getTokenBudget().getUsage(baseName);
|
|
3368
3705
|
if (!usage) return c.json({ error: "No budget configured" }, 404);
|
|
3369
3706
|
return c.json(usage);
|
|
3707
|
+
}).get("/:name/delivery/pending", async (c) => {
|
|
3708
|
+
const name = c.req.param("name");
|
|
3709
|
+
const [baseName] = name.split("@", 2);
|
|
3710
|
+
try {
|
|
3711
|
+
const pending = await getDeliveryManager().getPending(baseName);
|
|
3712
|
+
return c.json(pending);
|
|
3713
|
+
} catch (err) {
|
|
3714
|
+
if (err instanceof Error && err.message.includes("not initialized")) {
|
|
3715
|
+
return c.json([]);
|
|
3716
|
+
}
|
|
3717
|
+
logger_default.error(`failed to get pending deliveries for ${baseName}`, logger_default.errorData(err));
|
|
3718
|
+
return c.json({ error: "Failed to retrieve pending messages" }, 500);
|
|
3719
|
+
}
|
|
3370
3720
|
}).post("/:name/events", async (c) => {
|
|
3371
3721
|
const name = c.req.param("name");
|
|
3372
3722
|
const [baseName] = name.split("@", 2);
|
|
@@ -3379,9 +3729,9 @@ ${user.trimEnd()}
|
|
|
3379
3729
|
if (!body.type) {
|
|
3380
3730
|
return c.json({ error: "type required" }, 400);
|
|
3381
3731
|
}
|
|
3382
|
-
const
|
|
3732
|
+
const db = await getDb();
|
|
3383
3733
|
try {
|
|
3384
|
-
await
|
|
3734
|
+
await db.insert(mindHistory).values({
|
|
3385
3735
|
mind: baseName,
|
|
3386
3736
|
type: body.type,
|
|
3387
3737
|
session: body.session ?? null,
|
|
@@ -3391,7 +3741,7 @@ ${user.trimEnd()}
|
|
|
3391
3741
|
metadata: body.metadata ? JSON.stringify(body.metadata) : null
|
|
3392
3742
|
});
|
|
3393
3743
|
} catch (err) {
|
|
3394
|
-
|
|
3744
|
+
logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
|
|
3395
3745
|
}
|
|
3396
3746
|
publish2(baseName, {
|
|
3397
3747
|
mind: baseName,
|
|
@@ -3402,12 +3752,22 @@ ${user.trimEnd()}
|
|
|
3402
3752
|
content: body.content,
|
|
3403
3753
|
metadata: body.metadata
|
|
3404
3754
|
});
|
|
3755
|
+
if ((body.type === "text" || body.type === "outbound") && body.channel) {
|
|
3756
|
+
getTypingMap().delete(body.channel, baseName);
|
|
3757
|
+
}
|
|
3405
3758
|
if (body.type === "done") {
|
|
3406
3759
|
if (body.channel) {
|
|
3407
3760
|
getTypingMap().delete(body.channel, baseName);
|
|
3408
3761
|
} else {
|
|
3409
3762
|
getTypingMap().deleteSender(baseName);
|
|
3410
3763
|
}
|
|
3764
|
+
try {
|
|
3765
|
+
getDeliveryManager().sessionDone(baseName, body.session);
|
|
3766
|
+
} catch (err) {
|
|
3767
|
+
if (!(err instanceof Error && err.message.includes("not initialized"))) {
|
|
3768
|
+
logger_default.error(`delivery manager sessionDone failed for ${baseName}`, logger_default.errorData(err));
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3411
3771
|
}
|
|
3412
3772
|
if (body.type === "usage" && body.metadata) {
|
|
3413
3773
|
const inputTokens = body.metadata.input_tokens ?? 0;
|
|
@@ -3465,9 +3825,9 @@ ${user.trimEnd()}
|
|
|
3465
3825
|
if (!body.channel || !body.content) {
|
|
3466
3826
|
return c.json({ error: "channel and content required" }, 400);
|
|
3467
3827
|
}
|
|
3468
|
-
const
|
|
3828
|
+
const db = await getDb();
|
|
3469
3829
|
try {
|
|
3470
|
-
await
|
|
3830
|
+
await db.insert(mindHistory).values({
|
|
3471
3831
|
mind: baseName,
|
|
3472
3832
|
type: "outbound",
|
|
3473
3833
|
channel: body.channel,
|
|
@@ -3475,25 +3835,25 @@ ${user.trimEnd()}
|
|
|
3475
3835
|
content: body.content
|
|
3476
3836
|
});
|
|
3477
3837
|
} catch (err) {
|
|
3478
|
-
|
|
3838
|
+
logger_default.error(`failed to persist external send for ${baseName}`, logger_default.errorData(err));
|
|
3479
3839
|
return c.json({ error: "Failed to persist" }, 500);
|
|
3480
3840
|
}
|
|
3481
3841
|
return c.json({ ok: true });
|
|
3482
3842
|
}).get("/:name/history/sessions", async (c) => {
|
|
3483
3843
|
const name = c.req.param("name");
|
|
3484
|
-
const
|
|
3485
|
-
const rows = await
|
|
3844
|
+
const db = await getDb();
|
|
3845
|
+
const rows = await db.select({
|
|
3486
3846
|
session: mindHistory.session,
|
|
3487
|
-
started_at:
|
|
3488
|
-
event_count:
|
|
3489
|
-
message_count:
|
|
3490
|
-
tool_count:
|
|
3491
|
-
}).from(mindHistory).where(and3(eq4(mindHistory.mind, name),
|
|
3847
|
+
started_at: sql2`MIN(${mindHistory.created_at})`,
|
|
3848
|
+
event_count: sql2`COUNT(*)`,
|
|
3849
|
+
message_count: sql2`SUM(CASE WHEN ${mindHistory.type} IN ('inbound','outbound') THEN 1 ELSE 0 END)`,
|
|
3850
|
+
tool_count: sql2`SUM(CASE WHEN ${mindHistory.type}='tool_use' THEN 1 ELSE 0 END)`
|
|
3851
|
+
}).from(mindHistory).where(and3(eq4(mindHistory.mind, name), sql2`${mindHistory.session} IS NOT NULL`)).groupBy(mindHistory.session).orderBy(sql2`MIN(${mindHistory.created_at}) DESC`);
|
|
3492
3852
|
return c.json(rows);
|
|
3493
3853
|
}).get("/:name/history/channels", async (c) => {
|
|
3494
3854
|
const name = c.req.param("name");
|
|
3495
|
-
const
|
|
3496
|
-
const rows = await
|
|
3855
|
+
const db = await getDb();
|
|
3856
|
+
const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
|
|
3497
3857
|
return c.json(rows.map((r) => r.channel));
|
|
3498
3858
|
}).get("/:name/history", async (c) => {
|
|
3499
3859
|
const name = c.req.param("name");
|
|
@@ -3502,7 +3862,7 @@ ${user.trimEnd()}
|
|
|
3502
3862
|
const full = c.req.query("full") === "true";
|
|
3503
3863
|
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
3504
3864
|
const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
|
|
3505
|
-
const
|
|
3865
|
+
const db = await getDb();
|
|
3506
3866
|
const conditions = [eq4(mindHistory.mind, name)];
|
|
3507
3867
|
if (channel) {
|
|
3508
3868
|
conditions.push(eq4(mindHistory.channel, channel));
|
|
@@ -3511,17 +3871,17 @@ ${user.trimEnd()}
|
|
|
3511
3871
|
conditions.push(eq4(mindHistory.session, session));
|
|
3512
3872
|
}
|
|
3513
3873
|
if (!full) {
|
|
3514
|
-
conditions.push(
|
|
3874
|
+
conditions.push(sql2`${mindHistory.type} IN ('inbound', 'outbound')`);
|
|
3515
3875
|
}
|
|
3516
|
-
const rows = await
|
|
3876
|
+
const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
|
|
3517
3877
|
return c.json(rows);
|
|
3518
3878
|
});
|
|
3519
|
-
var minds_default =
|
|
3879
|
+
var minds_default = app9;
|
|
3520
3880
|
|
|
3521
3881
|
// src/web/api/pages.ts
|
|
3522
3882
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
3523
|
-
import { extname, resolve as
|
|
3524
|
-
import { Hono as
|
|
3883
|
+
import { extname, resolve as resolve14 } from "path";
|
|
3884
|
+
import { Hono as Hono10 } from "hono";
|
|
3525
3885
|
var MIME_TYPES = {
|
|
3526
3886
|
".html": "text/html",
|
|
3527
3887
|
".js": "application/javascript",
|
|
@@ -3538,16 +3898,16 @@ var MIME_TYPES = {
|
|
|
3538
3898
|
".txt": "text/plain",
|
|
3539
3899
|
".xml": "application/xml"
|
|
3540
3900
|
};
|
|
3541
|
-
var
|
|
3901
|
+
var app10 = new Hono10().get("/:name/*", async (c) => {
|
|
3542
3902
|
const name = c.req.param("name");
|
|
3543
3903
|
if (!findMind(name)) return c.text("Not found", 404);
|
|
3544
|
-
const pagesRoot =
|
|
3904
|
+
const pagesRoot = resolve14(mindDir(name), "home", "pages");
|
|
3545
3905
|
const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
|
|
3546
|
-
const requestedPath =
|
|
3906
|
+
const requestedPath = resolve14(pagesRoot, wildcard.slice(1));
|
|
3547
3907
|
if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
|
|
3548
3908
|
let fileStat = await stat(requestedPath).catch(() => null);
|
|
3549
3909
|
if (fileStat?.isDirectory()) {
|
|
3550
|
-
const indexPath =
|
|
3910
|
+
const indexPath = resolve14(requestedPath, "index.html");
|
|
3551
3911
|
fileStat = await stat(indexPath).catch(() => null);
|
|
3552
3912
|
if (fileStat?.isFile()) {
|
|
3553
3913
|
const body = await readFile2(indexPath);
|
|
@@ -3563,10 +3923,61 @@ var app8 = new Hono8().get("/:name/*", async (c) => {
|
|
|
3563
3923
|
}
|
|
3564
3924
|
return c.text("Not found", 404);
|
|
3565
3925
|
});
|
|
3566
|
-
var pages_default =
|
|
3926
|
+
var pages_default = app10;
|
|
3927
|
+
|
|
3928
|
+
// src/web/api/prompts.ts
|
|
3929
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
3930
|
+
import { eq as eq5, sql as sql3 } from "drizzle-orm";
|
|
3931
|
+
import { Hono as Hono11 } from "hono";
|
|
3932
|
+
import { z as z4 } from "zod";
|
|
3933
|
+
var app11 = new Hono11().get("/", async (c) => {
|
|
3934
|
+
let rows;
|
|
3935
|
+
try {
|
|
3936
|
+
const db = await getDb();
|
|
3937
|
+
rows = await db.select().from(systemPrompts).all();
|
|
3938
|
+
} catch (err) {
|
|
3939
|
+
console.error("[prompts] failed to query system_prompts:", err);
|
|
3940
|
+
return c.json({ error: "Failed to load prompts from database" }, 500);
|
|
3941
|
+
}
|
|
3942
|
+
const customMap = new Map(rows.map((r) => [r.key, r.content]));
|
|
3943
|
+
const prompts = PROMPT_KEYS.map((key) => {
|
|
3944
|
+
const meta = PROMPT_DEFAULTS[key];
|
|
3945
|
+
const custom = customMap.get(key);
|
|
3946
|
+
return {
|
|
3947
|
+
key,
|
|
3948
|
+
content: custom ?? meta.content,
|
|
3949
|
+
description: meta.description,
|
|
3950
|
+
variables: meta.variables,
|
|
3951
|
+
isCustom: custom !== void 0,
|
|
3952
|
+
category: meta.category
|
|
3953
|
+
};
|
|
3954
|
+
});
|
|
3955
|
+
return c.json(prompts);
|
|
3956
|
+
}).put("/:key", requireAdmin, zValidator4("json", z4.object({ content: z4.string() })), async (c) => {
|
|
3957
|
+
const key = c.req.param("key");
|
|
3958
|
+
if (!PROMPT_KEYS.includes(key)) {
|
|
3959
|
+
return c.json({ error: "Unknown prompt key" }, 404);
|
|
3960
|
+
}
|
|
3961
|
+
const { content } = c.req.valid("json");
|
|
3962
|
+
const db = await getDb();
|
|
3963
|
+
await db.insert(systemPrompts).values({ key, content, updated_at: sql3`(datetime('now'))` }).onConflictDoUpdate({
|
|
3964
|
+
target: systemPrompts.key,
|
|
3965
|
+
set: { content, updated_at: sql3`(datetime('now'))` }
|
|
3966
|
+
});
|
|
3967
|
+
return c.json({ ok: true });
|
|
3968
|
+
}).delete("/:key", requireAdmin, async (c) => {
|
|
3969
|
+
const key = c.req.param("key");
|
|
3970
|
+
if (!PROMPT_KEYS.includes(key)) {
|
|
3971
|
+
return c.json({ error: "Unknown prompt key" }, 404);
|
|
3972
|
+
}
|
|
3973
|
+
const db = await getDb();
|
|
3974
|
+
await db.delete(systemPrompts).where(eq5(systemPrompts.key, key));
|
|
3975
|
+
return c.json({ ok: true });
|
|
3976
|
+
});
|
|
3977
|
+
var prompts_default = app11;
|
|
3567
3978
|
|
|
3568
3979
|
// src/web/api/schedules.ts
|
|
3569
|
-
import { Hono as
|
|
3980
|
+
import { Hono as Hono12 } from "hono";
|
|
3570
3981
|
function readSchedules(name) {
|
|
3571
3982
|
return readVoluteConfig(mindDir(name))?.schedules ?? [];
|
|
3572
3983
|
}
|
|
@@ -3577,7 +3988,7 @@ function writeSchedules(name, schedules) {
|
|
|
3577
3988
|
writeVoluteConfig(dir, config);
|
|
3578
3989
|
getScheduler().loadSchedules(name);
|
|
3579
3990
|
}
|
|
3580
|
-
var
|
|
3991
|
+
var app12 = new Hono12().get("/:name/schedules", (c) => {
|
|
3581
3992
|
const name = c.req.param("name");
|
|
3582
3993
|
if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
|
|
3583
3994
|
return c.json(readSchedules(name));
|
|
@@ -3648,12 +4059,138 @@ var app9 = new Hono9().get("/:name/schedules", (c) => {
|
|
|
3648
4059
|
return c.json({ error: "Failed to reach mind" }, 502);
|
|
3649
4060
|
}
|
|
3650
4061
|
});
|
|
3651
|
-
var schedules_default =
|
|
4062
|
+
var schedules_default = app12;
|
|
4063
|
+
|
|
4064
|
+
// src/web/api/shared.ts
|
|
4065
|
+
import { Hono as Hono13 } from "hono";
|
|
4066
|
+
var app13 = new Hono13().post("/:name/shared/merge", requireAdmin, async (c) => {
|
|
4067
|
+
const name = c.req.param("name");
|
|
4068
|
+
const entry = findMind(name);
|
|
4069
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4070
|
+
let body;
|
|
4071
|
+
try {
|
|
4072
|
+
body = await c.req.json();
|
|
4073
|
+
} catch {
|
|
4074
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
4075
|
+
}
|
|
4076
|
+
const message = body.message || `shared: merge from ${name}`;
|
|
4077
|
+
try {
|
|
4078
|
+
const result = await sharedMerge(name, mindDir(name), message);
|
|
4079
|
+
return c.json(result);
|
|
4080
|
+
} catch (err) {
|
|
4081
|
+
return c.json({ error: err instanceof Error ? err.message : "Merge failed" }, 500);
|
|
4082
|
+
}
|
|
4083
|
+
}).post("/:name/shared/pull", requireAdmin, async (c) => {
|
|
4084
|
+
const name = c.req.param("name");
|
|
4085
|
+
const entry = findMind(name);
|
|
4086
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4087
|
+
try {
|
|
4088
|
+
const result = await sharedPull(name, mindDir(name));
|
|
4089
|
+
return c.json(result);
|
|
4090
|
+
} catch (err) {
|
|
4091
|
+
return c.json({ error: err instanceof Error ? err.message : "Pull failed" }, 500);
|
|
4092
|
+
}
|
|
4093
|
+
}).get("/:name/shared/log", async (c) => {
|
|
4094
|
+
const name = c.req.param("name");
|
|
4095
|
+
const entry = findMind(name);
|
|
4096
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4097
|
+
const limit = parseInt(c.req.query("limit") ?? "20", 10) || 20;
|
|
4098
|
+
try {
|
|
4099
|
+
const log2 = await sharedLog(limit);
|
|
4100
|
+
return c.text(log2);
|
|
4101
|
+
} catch (err) {
|
|
4102
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to read log" }, 500);
|
|
4103
|
+
}
|
|
4104
|
+
}).get("/:name/shared/status", async (c) => {
|
|
4105
|
+
const name = c.req.param("name");
|
|
4106
|
+
const entry = findMind(name);
|
|
4107
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4108
|
+
try {
|
|
4109
|
+
const status = await sharedStatus(name);
|
|
4110
|
+
return c.text(status);
|
|
4111
|
+
} catch (err) {
|
|
4112
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to get status" }, 500);
|
|
4113
|
+
}
|
|
4114
|
+
});
|
|
4115
|
+
var shared_default = app13;
|
|
4116
|
+
|
|
4117
|
+
// src/web/api/skills.ts
|
|
4118
|
+
import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
|
|
4119
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
4120
|
+
import { join as join3, resolve as resolve15 } from "path";
|
|
4121
|
+
import AdmZip from "adm-zip";
|
|
4122
|
+
import { Hono as Hono14 } from "hono";
|
|
4123
|
+
var app14 = new Hono14().get("/", async (c) => {
|
|
4124
|
+
const skills = await listSharedSkills();
|
|
4125
|
+
return c.json(skills);
|
|
4126
|
+
}).get("/:id", async (c) => {
|
|
4127
|
+
const id = c.req.param("id");
|
|
4128
|
+
const skill = await getSharedSkill(id);
|
|
4129
|
+
if (!skill) return c.json({ error: "Skill not found" }, 404);
|
|
4130
|
+
const dir = join3(sharedSkillsDir(), id);
|
|
4131
|
+
const files = listFilesRecursive(dir);
|
|
4132
|
+
return c.json({ ...skill, files });
|
|
4133
|
+
}).post("/upload", requireAdmin, async (c) => {
|
|
4134
|
+
const body = await c.req.parseBody();
|
|
4135
|
+
const file = body.file;
|
|
4136
|
+
if (!file || !(file instanceof File)) {
|
|
4137
|
+
return c.json({ error: "No file uploaded" }, 400);
|
|
4138
|
+
}
|
|
4139
|
+
if (!file.name.endsWith(".zip")) {
|
|
4140
|
+
return c.json({ error: "Only .zip files are accepted" }, 400);
|
|
4141
|
+
}
|
|
4142
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
4143
|
+
const tmpDir = mkdtempSync(join3(tmpdir2(), "volute-skill-upload-"));
|
|
4144
|
+
try {
|
|
4145
|
+
const zip = new AdmZip(buffer);
|
|
4146
|
+
for (const entry of zip.getEntries()) {
|
|
4147
|
+
const target = resolve15(tmpDir, entry.entryName);
|
|
4148
|
+
if (!target.startsWith(tmpDir)) {
|
|
4149
|
+
return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
zip.extractAllTo(tmpDir, true);
|
|
4153
|
+
let skillDir = null;
|
|
4154
|
+
if (existsSync11(join3(tmpDir, "SKILL.md"))) {
|
|
4155
|
+
skillDir = tmpDir;
|
|
4156
|
+
} else {
|
|
4157
|
+
const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
4158
|
+
for (const entry of entries) {
|
|
4159
|
+
if (existsSync11(join3(tmpDir, entry.name, "SKILL.md"))) {
|
|
4160
|
+
skillDir = join3(tmpDir, entry.name);
|
|
4161
|
+
break;
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
if (!skillDir) {
|
|
4166
|
+
return c.json({ error: "No SKILL.md found in zip (checked root and one level deep)" }, 400);
|
|
4167
|
+
}
|
|
4168
|
+
const skill = await importSkillFromDir(skillDir, "upload");
|
|
4169
|
+
return c.json(skill);
|
|
4170
|
+
} catch (e) {
|
|
4171
|
+
if (e instanceof Error && e.message.includes("Invalid skill ID")) {
|
|
4172
|
+
return c.json({ error: e.message }, 400);
|
|
4173
|
+
}
|
|
4174
|
+
throw e;
|
|
4175
|
+
} finally {
|
|
4176
|
+
rmSync3(tmpDir, { recursive: true, force: true });
|
|
4177
|
+
}
|
|
4178
|
+
}).delete("/:id", requireAdmin, async (c) => {
|
|
4179
|
+
const id = c.req.param("id");
|
|
4180
|
+
try {
|
|
4181
|
+
await removeSharedSkill(id);
|
|
4182
|
+
} catch (e) {
|
|
4183
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4184
|
+
return c.json({ error: msg }, 404);
|
|
4185
|
+
}
|
|
4186
|
+
return c.json({ ok: true });
|
|
4187
|
+
});
|
|
4188
|
+
var skills_default = app14;
|
|
3652
4189
|
|
|
3653
4190
|
// src/web/api/system.ts
|
|
3654
|
-
import { Hono as
|
|
4191
|
+
import { Hono as Hono15 } from "hono";
|
|
3655
4192
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
3656
|
-
var
|
|
4193
|
+
var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
|
|
3657
4194
|
setTimeout(() => process.exit(1), 200);
|
|
3658
4195
|
return c.json({ ok: true });
|
|
3659
4196
|
}).post("/stop", requireAdmin, (c) => {
|
|
@@ -3670,10 +4207,10 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
|
|
|
3670
4207
|
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
3671
4208
|
});
|
|
3672
4209
|
});
|
|
3673
|
-
await new Promise((
|
|
4210
|
+
await new Promise((resolve20) => {
|
|
3674
4211
|
stream.onAbort(() => {
|
|
3675
4212
|
unsubscribe();
|
|
3676
|
-
|
|
4213
|
+
resolve20();
|
|
3677
4214
|
});
|
|
3678
4215
|
});
|
|
3679
4216
|
});
|
|
@@ -3681,18 +4218,18 @@ var app10 = new Hono10().post("/restart", requireAdmin, (c) => {
|
|
|
3681
4218
|
const config = readSystemsConfig();
|
|
3682
4219
|
return c.json({ system: config?.system ?? null });
|
|
3683
4220
|
});
|
|
3684
|
-
var system_default =
|
|
4221
|
+
var system_default = app15;
|
|
3685
4222
|
|
|
3686
4223
|
// src/web/api/typing.ts
|
|
3687
|
-
import { zValidator as
|
|
3688
|
-
import { Hono as
|
|
3689
|
-
import { z as
|
|
3690
|
-
var typingSchema =
|
|
3691
|
-
channel:
|
|
3692
|
-
sender:
|
|
3693
|
-
active:
|
|
4224
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
4225
|
+
import { Hono as Hono16 } from "hono";
|
|
4226
|
+
import { z as z5 } from "zod";
|
|
4227
|
+
var typingSchema = z5.object({
|
|
4228
|
+
channel: z5.string().min(1),
|
|
4229
|
+
sender: z5.string().min(1),
|
|
4230
|
+
active: z5.boolean()
|
|
3694
4231
|
});
|
|
3695
|
-
var
|
|
4232
|
+
var app16 = new Hono16().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
|
|
3696
4233
|
const { channel, sender, active } = c.req.valid("json");
|
|
3697
4234
|
const map = getTypingMap();
|
|
3698
4235
|
if (active) {
|
|
@@ -3709,13 +4246,13 @@ var app11 = new Hono11().post("/:name/typing", zValidator3("json", typingSchema)
|
|
|
3709
4246
|
const map = getTypingMap();
|
|
3710
4247
|
return c.json({ typing: map.get(channel) });
|
|
3711
4248
|
});
|
|
3712
|
-
var typing_default =
|
|
4249
|
+
var typing_default = app16;
|
|
3713
4250
|
|
|
3714
4251
|
// src/web/api/update.ts
|
|
3715
4252
|
import { spawn as spawn3 } from "child_process";
|
|
3716
|
-
import { Hono as
|
|
4253
|
+
import { Hono as Hono17 } from "hono";
|
|
3717
4254
|
var bin;
|
|
3718
|
-
var
|
|
4255
|
+
var app17 = new Hono17().get("/update", async (c) => {
|
|
3719
4256
|
const result = await checkForUpdate();
|
|
3720
4257
|
return c.json(result);
|
|
3721
4258
|
}).post("/update", requireAdmin, async (c) => {
|
|
@@ -3730,19 +4267,19 @@ var app12 = new Hono12().get("/update", async (c) => {
|
|
|
3730
4267
|
child.unref();
|
|
3731
4268
|
return c.json({ ok: true, message: "Updating..." });
|
|
3732
4269
|
});
|
|
3733
|
-
var update_default =
|
|
4270
|
+
var update_default = app17;
|
|
3734
4271
|
|
|
3735
4272
|
// src/web/api/variants.ts
|
|
3736
|
-
import { existsSync as
|
|
3737
|
-
import { resolve as
|
|
3738
|
-
import { Hono as
|
|
4273
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
4274
|
+
import { resolve as resolve17 } from "path";
|
|
4275
|
+
import { Hono as Hono18 } from "hono";
|
|
3739
4276
|
|
|
3740
4277
|
// src/lib/spawn-server.ts
|
|
3741
4278
|
import { spawn as spawn4 } from "child_process";
|
|
3742
|
-
import { closeSync, mkdirSync as
|
|
3743
|
-
import { resolve as
|
|
4279
|
+
import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync10 } from "fs";
|
|
4280
|
+
import { resolve as resolve16 } from "path";
|
|
3744
4281
|
function tsxBin(cwd) {
|
|
3745
|
-
return
|
|
4282
|
+
return resolve16(cwd, "node_modules", ".bin", "tsx");
|
|
3746
4283
|
}
|
|
3747
4284
|
function spawnServer(cwd, port, options) {
|
|
3748
4285
|
if (options?.detached) {
|
|
@@ -3755,31 +4292,31 @@ function spawnAttached(cwd, port) {
|
|
|
3755
4292
|
cwd,
|
|
3756
4293
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3757
4294
|
});
|
|
3758
|
-
return new Promise((
|
|
3759
|
-
const timeout = setTimeout(() =>
|
|
4295
|
+
return new Promise((resolve20) => {
|
|
4296
|
+
const timeout = setTimeout(() => resolve20(null), 3e4);
|
|
3760
4297
|
function checkOutput(data) {
|
|
3761
4298
|
const match = data.toString().match(/listening on :(\d+)/);
|
|
3762
4299
|
if (match) {
|
|
3763
4300
|
clearTimeout(timeout);
|
|
3764
|
-
|
|
4301
|
+
resolve20({ child, actualPort: parseInt(match[1], 10) });
|
|
3765
4302
|
}
|
|
3766
4303
|
}
|
|
3767
4304
|
child.stdout?.on("data", checkOutput);
|
|
3768
4305
|
child.stderr?.on("data", checkOutput);
|
|
3769
4306
|
child.on("error", () => {
|
|
3770
4307
|
clearTimeout(timeout);
|
|
3771
|
-
|
|
4308
|
+
resolve20(null);
|
|
3772
4309
|
});
|
|
3773
4310
|
child.on("exit", () => {
|
|
3774
4311
|
clearTimeout(timeout);
|
|
3775
|
-
|
|
4312
|
+
resolve20(null);
|
|
3776
4313
|
});
|
|
3777
4314
|
});
|
|
3778
4315
|
}
|
|
3779
4316
|
function spawnDetached(cwd, port, logDir) {
|
|
3780
|
-
const logsDir = logDir ??
|
|
3781
|
-
|
|
3782
|
-
const logPath =
|
|
4317
|
+
const logsDir = logDir ?? resolve16(cwd, ".mind", "logs");
|
|
4318
|
+
mkdirSync8(logsDir, { recursive: true });
|
|
4319
|
+
const logPath = resolve16(logsDir, "mind.log");
|
|
3783
4320
|
const logFd = openSync(logPath, "a");
|
|
3784
4321
|
const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
3785
4322
|
cwd,
|
|
@@ -3799,7 +4336,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
3799
4336
|
}
|
|
3800
4337
|
const interval = setInterval(() => {
|
|
3801
4338
|
try {
|
|
3802
|
-
const content =
|
|
4339
|
+
const content = readFileSync10(logPath, "utf-8");
|
|
3803
4340
|
const match = content.match(/listening on :(\d+)/);
|
|
3804
4341
|
if (match) {
|
|
3805
4342
|
finish({ child, actualPort: parseInt(match[1], 10) });
|
|
@@ -3814,7 +4351,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
3814
4351
|
}
|
|
3815
4352
|
|
|
3816
4353
|
// src/lib/verify.ts
|
|
3817
|
-
async function
|
|
4354
|
+
async function verify2(port) {
|
|
3818
4355
|
const health = await checkHealth(port);
|
|
3819
4356
|
if (!health.ok) {
|
|
3820
4357
|
console.error(" Health check: failed");
|
|
@@ -3851,7 +4388,7 @@ async function verify(port) {
|
|
|
3851
4388
|
}
|
|
3852
4389
|
|
|
3853
4390
|
// src/web/api/variants.ts
|
|
3854
|
-
var
|
|
4391
|
+
var app18 = new Hono18().get("/:name/variants", async (c) => {
|
|
3855
4392
|
const name = c.req.param("name");
|
|
3856
4393
|
const entry = findMind(name);
|
|
3857
4394
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
@@ -3881,11 +4418,11 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3881
4418
|
const err = validateBranchName(variantName);
|
|
3882
4419
|
if (err) return c.json({ error: err }, 400);
|
|
3883
4420
|
const projectRoot = mindDir(mindName);
|
|
3884
|
-
const variantDir =
|
|
3885
|
-
if (
|
|
4421
|
+
const variantDir = resolve17(projectRoot, ".variants", variantName);
|
|
4422
|
+
if (existsSync12(variantDir)) {
|
|
3886
4423
|
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
3887
4424
|
}
|
|
3888
|
-
|
|
4425
|
+
mkdirSync9(resolve17(projectRoot, ".variants"), { recursive: true });
|
|
3889
4426
|
try {
|
|
3890
4427
|
await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
3891
4428
|
} catch (e) {
|
|
@@ -3898,7 +4435,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3898
4435
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
3899
4436
|
await exec(cmd, args, {
|
|
3900
4437
|
cwd: variantDir,
|
|
3901
|
-
env: { ...process.env, HOME:
|
|
4438
|
+
env: { ...process.env, HOME: resolve17(variantDir, "home") }
|
|
3902
4439
|
});
|
|
3903
4440
|
} else {
|
|
3904
4441
|
await exec("npm", ["install"], { cwd: variantDir });
|
|
@@ -3908,7 +4445,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3908
4445
|
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
3909
4446
|
}
|
|
3910
4447
|
if (body.soul) {
|
|
3911
|
-
|
|
4448
|
+
writeFileSync9(resolve17(variantDir, "home/SOUL.md"), body.soul);
|
|
3912
4449
|
}
|
|
3913
4450
|
const variantPort = body.port ?? nextPort();
|
|
3914
4451
|
const variant = {
|
|
@@ -3946,7 +4483,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3946
4483
|
} catch {
|
|
3947
4484
|
}
|
|
3948
4485
|
const projectRoot = mindDir(mindName);
|
|
3949
|
-
if (
|
|
4486
|
+
if (existsSync12(variant.path)) {
|
|
3950
4487
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
3951
4488
|
if (status) {
|
|
3952
4489
|
try {
|
|
@@ -3972,7 +4509,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
3972
4509
|
500
|
|
3973
4510
|
);
|
|
3974
4511
|
}
|
|
3975
|
-
const verified = await
|
|
4512
|
+
const verified = await verify2(result.actualPort);
|
|
3976
4513
|
try {
|
|
3977
4514
|
process.kill(result.child.pid);
|
|
3978
4515
|
} catch {
|
|
@@ -4003,7 +4540,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
4003
4540
|
} catch (e) {
|
|
4004
4541
|
return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
|
|
4005
4542
|
}
|
|
4006
|
-
if (
|
|
4543
|
+
if (existsSync12(variant.path)) {
|
|
4007
4544
|
try {
|
|
4008
4545
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
4009
4546
|
} catch {
|
|
@@ -4020,7 +4557,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
4020
4557
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
4021
4558
|
await exec(cmd, args, {
|
|
4022
4559
|
cwd: projectRoot,
|
|
4023
|
-
env: { ...process.env, HOME:
|
|
4560
|
+
env: { ...process.env, HOME: resolve17(projectRoot, "home") }
|
|
4024
4561
|
});
|
|
4025
4562
|
} else {
|
|
4026
4563
|
await exec("npm", ["install"], { cwd: projectRoot });
|
|
@@ -4063,7 +4600,7 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
4063
4600
|
} catch {
|
|
4064
4601
|
}
|
|
4065
4602
|
}
|
|
4066
|
-
if (
|
|
4603
|
+
if (existsSync12(variant.path)) {
|
|
4067
4604
|
try {
|
|
4068
4605
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
4069
4606
|
} catch {
|
|
@@ -4077,45 +4614,153 @@ var app13 = new Hono13().get("/:name/variants", async (c) => {
|
|
|
4077
4614
|
chownMindDir(projectRoot, mindName);
|
|
4078
4615
|
return c.json({ ok: true });
|
|
4079
4616
|
});
|
|
4080
|
-
var variants_default =
|
|
4617
|
+
var variants_default = app18;
|
|
4081
4618
|
|
|
4082
|
-
// src/web/api/volute/
|
|
4083
|
-
import {
|
|
4084
|
-
import {
|
|
4085
|
-
import {
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
var
|
|
4090
|
-
|
|
4091
|
-
conversationId: z4.string().optional(),
|
|
4092
|
-
sender: z4.string().optional(),
|
|
4093
|
-
images: z4.array(
|
|
4094
|
-
z4.object({
|
|
4095
|
-
media_type: z4.string(),
|
|
4096
|
-
data: z4.string()
|
|
4097
|
-
})
|
|
4098
|
-
).optional()
|
|
4619
|
+
// src/web/api/volute/channels.ts
|
|
4620
|
+
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
4621
|
+
import { Hono as Hono19 } from "hono";
|
|
4622
|
+
import { z as z6 } from "zod";
|
|
4623
|
+
var createSchema = z6.object({
|
|
4624
|
+
name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
|
|
4625
|
+
});
|
|
4626
|
+
var inviteSchema = z6.object({
|
|
4627
|
+
username: z6.string().min(1)
|
|
4099
4628
|
});
|
|
4100
|
-
|
|
4629
|
+
var app19 = new Hono19().get("/", async (c) => {
|
|
4630
|
+
const user = c.get("user");
|
|
4631
|
+
const channels = await listChannels();
|
|
4632
|
+
const results = await Promise.all(
|
|
4633
|
+
channels.map(async (ch) => {
|
|
4634
|
+
const participants = await getParticipants(ch.id);
|
|
4635
|
+
const isMember = participants.some((p) => p.userId === user.id);
|
|
4636
|
+
return { ...ch, participantCount: participants.length, isMember };
|
|
4637
|
+
})
|
|
4638
|
+
);
|
|
4639
|
+
return c.json(results);
|
|
4640
|
+
}).post("/", zValidator6("json", createSchema), async (c) => {
|
|
4641
|
+
const user = c.get("user");
|
|
4642
|
+
const body = c.req.valid("json");
|
|
4101
4643
|
try {
|
|
4102
|
-
const
|
|
4103
|
-
return
|
|
4644
|
+
const ch = await createChannel(body.name, user.id);
|
|
4645
|
+
return c.json(ch, 201);
|
|
4104
4646
|
} catch (err) {
|
|
4105
|
-
|
|
4647
|
+
const cause = err instanceof Error ? err.cause : null;
|
|
4648
|
+
if (cause && /UNIQUE/i.test(cause.extendedCode ?? cause.message ?? "")) {
|
|
4649
|
+
return c.json({ error: "Channel already exists" }, 409);
|
|
4650
|
+
}
|
|
4651
|
+
throw err;
|
|
4106
4652
|
}
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
|
-
const
|
|
4110
|
-
const
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4653
|
+
}).post("/:name/join", async (c) => {
|
|
4654
|
+
const name = c.req.param("name");
|
|
4655
|
+
const user = c.get("user");
|
|
4656
|
+
const ch = await getChannelByName(name);
|
|
4657
|
+
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4658
|
+
await joinChannel(ch.id, user.id);
|
|
4659
|
+
return c.json({ ok: true, conversationId: ch.id });
|
|
4660
|
+
}).post("/:name/leave", async (c) => {
|
|
4661
|
+
const name = c.req.param("name");
|
|
4662
|
+
const user = c.get("user");
|
|
4663
|
+
const ch = await getChannelByName(name);
|
|
4664
|
+
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4665
|
+
await leaveChannel(ch.id, user.id);
|
|
4666
|
+
return c.json({ ok: true });
|
|
4667
|
+
}).get("/:name/members", async (c) => {
|
|
4668
|
+
const name = c.req.param("name");
|
|
4669
|
+
const ch = await getChannelByName(name);
|
|
4670
|
+
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4671
|
+
const participants = await getParticipants(ch.id);
|
|
4672
|
+
return c.json(participants);
|
|
4673
|
+
}).post("/:name/invite", zValidator6("json", inviteSchema), async (c) => {
|
|
4674
|
+
const name = c.req.param("name");
|
|
4675
|
+
const inviter = c.get("user");
|
|
4676
|
+
const { username } = c.req.valid("json");
|
|
4677
|
+
const ch = await getChannelByName(name);
|
|
4678
|
+
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4679
|
+
let user = await getUserByUsername(username);
|
|
4680
|
+
if (!user && findMind(username)) {
|
|
4681
|
+
user = await getOrCreateMindUser(username);
|
|
4682
|
+
}
|
|
4683
|
+
if (!user) return c.json({ error: "User not found" }, 404);
|
|
4684
|
+
if (await isParticipant(ch.id, user.id)) {
|
|
4685
|
+
return c.json({ error: "Already a member" }, 409);
|
|
4686
|
+
}
|
|
4687
|
+
await joinChannel(ch.id, user.id);
|
|
4688
|
+
await addMessage(ch.id, "system", "system", [
|
|
4689
|
+
{ type: "text", text: `${inviter.username} invited ${username} to #${name}` }
|
|
4690
|
+
]);
|
|
4691
|
+
return c.json({ ok: true });
|
|
4692
|
+
});
|
|
4693
|
+
var channels_default2 = app19;
|
|
4694
|
+
|
|
4695
|
+
// src/web/api/volute/chat.ts
|
|
4696
|
+
import { zValidator as zValidator7 } from "@hono/zod-validator";
|
|
4697
|
+
import { Hono as Hono20 } from "hono";
|
|
4698
|
+
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
4699
|
+
import { z as z7 } from "zod";
|
|
4700
|
+
async function fanOutToMinds(opts) {
|
|
4701
|
+
const participants = await getParticipants(opts.conversationId);
|
|
4702
|
+
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
4703
|
+
const participantNames = participants.map((p) => p.username);
|
|
4704
|
+
const isDM = opts.isDM ?? participants.length === 2;
|
|
4705
|
+
const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
|
|
4706
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-RVCFROAY.js");
|
|
4707
|
+
const manager = getMindManager2();
|
|
4708
|
+
const runningMinds = mindParticipants.map((ap) => {
|
|
4709
|
+
const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
|
|
4710
|
+
return manager.isRunning(key) ? ap.username : null;
|
|
4711
|
+
}).filter((n) => n !== null && n !== opts.senderName);
|
|
4712
|
+
function slugForMind(mindUsername) {
|
|
4713
|
+
return buildVoluteSlug({
|
|
4714
|
+
participants,
|
|
4715
|
+
mindUsername,
|
|
4716
|
+
convTitle: opts.convTitle,
|
|
4717
|
+
conversationId: opts.conversationId,
|
|
4718
|
+
...opts.slugExtra
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
const channelEntry = {
|
|
4722
|
+
platformId: opts.conversationId,
|
|
4723
|
+
platform: "volute",
|
|
4724
|
+
name: opts.convTitle ?? void 0,
|
|
4725
|
+
type: channelEntryType
|
|
4114
4726
|
};
|
|
4115
|
-
|
|
4116
|
-
|
|
4727
|
+
for (const ap of mindParticipants) {
|
|
4728
|
+
try {
|
|
4729
|
+
writeChannelEntry(ap.username, slugForMind(ap.username), channelEntry);
|
|
4730
|
+
} catch (err) {
|
|
4731
|
+
logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
for (const mindName of runningMinds) {
|
|
4735
|
+
const target = opts.targetName ? opts.targetName(mindName) : mindName;
|
|
4736
|
+
const channel = slugForMind(mindName);
|
|
4737
|
+
const typingMap = getTypingMap();
|
|
4738
|
+
const currentlyTyping = typingMap.get(channel);
|
|
4739
|
+
deliverMessage(target, {
|
|
4740
|
+
content: opts.contentBlocks,
|
|
4741
|
+
channel,
|
|
4742
|
+
conversationId: opts.conversationId,
|
|
4743
|
+
sender: opts.senderName,
|
|
4744
|
+
participants: participantNames,
|
|
4745
|
+
participantCount: participants.length,
|
|
4746
|
+
isDM,
|
|
4747
|
+
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
4748
|
+
}).catch(() => {
|
|
4749
|
+
});
|
|
4750
|
+
}
|
|
4117
4751
|
}
|
|
4118
|
-
var
|
|
4752
|
+
var chatSchema = z7.object({
|
|
4753
|
+
message: z7.string().optional(),
|
|
4754
|
+
conversationId: z7.string().optional(),
|
|
4755
|
+
sender: z7.string().optional(),
|
|
4756
|
+
images: z7.array(
|
|
4757
|
+
z7.object({
|
|
4758
|
+
media_type: z7.string(),
|
|
4759
|
+
data: z7.string()
|
|
4760
|
+
})
|
|
4761
|
+
).optional()
|
|
4762
|
+
});
|
|
4763
|
+
var app20 = new Hono20().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
|
|
4119
4764
|
const name = c.req.param("name");
|
|
4120
4765
|
const [baseName] = name.split("@", 2);
|
|
4121
4766
|
const entry = findMind(baseName);
|
|
@@ -4151,8 +4796,8 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
|
|
|
4151
4796
|
}
|
|
4152
4797
|
}
|
|
4153
4798
|
if (!conversationId) {
|
|
4154
|
-
const
|
|
4155
|
-
const title = [...
|
|
4799
|
+
const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
|
|
4800
|
+
const title = [...participantNames].join(", ");
|
|
4156
4801
|
const conv2 = await createConversation(baseName, "volute", {
|
|
4157
4802
|
userId: user.id !== 0 ? user.id : void 0,
|
|
4158
4803
|
title,
|
|
@@ -4162,7 +4807,7 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
|
|
|
4162
4807
|
}
|
|
4163
4808
|
}
|
|
4164
4809
|
const conv = await getConversation(conversationId);
|
|
4165
|
-
const convTitle = conv?.title;
|
|
4810
|
+
const convTitle = conv?.title ?? null;
|
|
4166
4811
|
const contentBlocks = [];
|
|
4167
4812
|
if (body.message) {
|
|
4168
4813
|
contentBlocks.push({ type: "text", text: body.message });
|
|
@@ -4173,61 +4818,13 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
|
|
|
4173
4818
|
}
|
|
4174
4819
|
}
|
|
4175
4820
|
await addMessage(conversationId, "user", senderName, contentBlocks);
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
return manager.isRunning(mindKey) ? ap.username : null;
|
|
4184
|
-
}).filter((n) => n !== null && n !== senderName);
|
|
4185
|
-
const isDM = participants.length === 2;
|
|
4186
|
-
function channelForMind(mindUsername) {
|
|
4187
|
-
return buildVoluteSlug({
|
|
4188
|
-
participants,
|
|
4189
|
-
mindUsername,
|
|
4190
|
-
convTitle,
|
|
4191
|
-
conversationId
|
|
4192
|
-
});
|
|
4193
|
-
}
|
|
4194
|
-
const channelEntry = {
|
|
4195
|
-
platformId: conversationId,
|
|
4196
|
-
platform: "volute",
|
|
4197
|
-
name: convTitle ?? void 0,
|
|
4198
|
-
type: isDM ? "dm" : "group"
|
|
4199
|
-
};
|
|
4200
|
-
for (const ap of mindParticipants) {
|
|
4201
|
-
try {
|
|
4202
|
-
writeChannelEntry(ap.username, channelForMind(ap.username), channelEntry);
|
|
4203
|
-
} catch (err) {
|
|
4204
|
-
console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
|
|
4205
|
-
}
|
|
4206
|
-
}
|
|
4207
|
-
for (const mindName of runningMinds) {
|
|
4208
|
-
const targetName = mindName === baseName ? name : mindName;
|
|
4209
|
-
const channel = channelForMind(mindName);
|
|
4210
|
-
const typingMap = getTypingMap();
|
|
4211
|
-
const currentlyTyping = typingMap.get(channel);
|
|
4212
|
-
const payload = JSON.stringify({
|
|
4213
|
-
content: contentBlocks,
|
|
4214
|
-
channel,
|
|
4215
|
-
conversationId,
|
|
4216
|
-
sender: senderName,
|
|
4217
|
-
participants: participantNames,
|
|
4218
|
-
participantCount: participants.length,
|
|
4219
|
-
isDM,
|
|
4220
|
-
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
4221
|
-
});
|
|
4222
|
-
daemonFetchInternal(`/api/minds/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
|
|
4223
|
-
if (!res.ok) {
|
|
4224
|
-
const text2 = await res.text().catch(() => "");
|
|
4225
|
-
console.error(`[chat] mind ${mindName} responded ${res.status}: ${text2}`);
|
|
4226
|
-
}
|
|
4227
|
-
}).catch((err) => {
|
|
4228
|
-
console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
|
|
4229
|
-
});
|
|
4230
|
-
}
|
|
4821
|
+
await fanOutToMinds({
|
|
4822
|
+
conversationId,
|
|
4823
|
+
contentBlocks,
|
|
4824
|
+
senderName,
|
|
4825
|
+
convTitle,
|
|
4826
|
+
targetName: (username) => username === baseName ? name : username
|
|
4827
|
+
});
|
|
4231
4828
|
return c.json({ ok: true, conversationId });
|
|
4232
4829
|
}).get("/:name/conversations/:id/events", async (c) => {
|
|
4233
4830
|
const conversationId = c.req.param("id");
|
|
@@ -4246,27 +4843,68 @@ var app14 = new Hono14().post("/:name/chat", zValidator4("json", chatSchema), as
|
|
|
4246
4843
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
4247
4844
|
});
|
|
4248
4845
|
}, 15e3);
|
|
4249
|
-
await new Promise((
|
|
4846
|
+
await new Promise((resolve20) => {
|
|
4250
4847
|
stream.onAbort(() => {
|
|
4251
4848
|
unsubscribe();
|
|
4252
4849
|
clearInterval(keepAlive);
|
|
4253
|
-
|
|
4850
|
+
resolve20();
|
|
4254
4851
|
});
|
|
4255
4852
|
});
|
|
4256
4853
|
});
|
|
4257
4854
|
});
|
|
4258
|
-
var
|
|
4855
|
+
var unifiedChatSchema = z7.object({
|
|
4856
|
+
message: z7.string().optional(),
|
|
4857
|
+
conversationId: z7.string(),
|
|
4858
|
+
images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
|
|
4859
|
+
});
|
|
4860
|
+
var unifiedChatApp = new Hono20().post(
|
|
4861
|
+
"/chat",
|
|
4862
|
+
zValidator7("json", unifiedChatSchema),
|
|
4863
|
+
async (c) => {
|
|
4864
|
+
const user = c.get("user");
|
|
4865
|
+
const body = c.req.valid("json");
|
|
4866
|
+
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
4867
|
+
return c.json({ error: "message or images required" }, 400);
|
|
4868
|
+
}
|
|
4869
|
+
const conv = await getConversation(body.conversationId);
|
|
4870
|
+
if (!conv) return c.json({ error: "Conversation not found" }, 404);
|
|
4871
|
+
if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
|
|
4872
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4873
|
+
}
|
|
4874
|
+
const senderName = user.username;
|
|
4875
|
+
const contentBlocks = [];
|
|
4876
|
+
if (body.message) contentBlocks.push({ type: "text", text: body.message });
|
|
4877
|
+
if (body.images) {
|
|
4878
|
+
for (const img of body.images) {
|
|
4879
|
+
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
await addMessage(body.conversationId, "user", senderName, contentBlocks);
|
|
4883
|
+
const isDM = conv.type === "dm";
|
|
4884
|
+
await fanOutToMinds({
|
|
4885
|
+
conversationId: body.conversationId,
|
|
4886
|
+
contentBlocks,
|
|
4887
|
+
senderName,
|
|
4888
|
+
convTitle: conv.title,
|
|
4889
|
+
isDM,
|
|
4890
|
+
channelEntryType: conv.type === "channel" ? "group" : isDM ? "dm" : "group",
|
|
4891
|
+
slugExtra: { convType: conv.type, convName: conv.name }
|
|
4892
|
+
});
|
|
4893
|
+
return c.json({ ok: true, conversationId: body.conversationId });
|
|
4894
|
+
}
|
|
4895
|
+
);
|
|
4896
|
+
var chat_default = app20;
|
|
4259
4897
|
|
|
4260
4898
|
// src/web/api/volute/conversations.ts
|
|
4261
|
-
import { zValidator as
|
|
4262
|
-
import { Hono as
|
|
4263
|
-
import { z as
|
|
4264
|
-
var createConvSchema =
|
|
4265
|
-
title:
|
|
4266
|
-
participantIds:
|
|
4267
|
-
participantNames:
|
|
4899
|
+
import { zValidator as zValidator8 } from "@hono/zod-validator";
|
|
4900
|
+
import { Hono as Hono21 } from "hono";
|
|
4901
|
+
import { z as z8 } from "zod";
|
|
4902
|
+
var createConvSchema = z8.object({
|
|
4903
|
+
title: z8.string().optional(),
|
|
4904
|
+
participantIds: z8.array(z8.number()).optional(),
|
|
4905
|
+
participantNames: z8.array(z8.string()).optional()
|
|
4268
4906
|
});
|
|
4269
|
-
var
|
|
4907
|
+
var app21 = new Hono21().get("/:name/conversations", async (c) => {
|
|
4270
4908
|
const name = c.req.param("name");
|
|
4271
4909
|
const user = c.get("user");
|
|
4272
4910
|
let lookupId = user.id;
|
|
@@ -4275,9 +4913,9 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
|
|
|
4275
4913
|
lookupId = mindUser.id;
|
|
4276
4914
|
}
|
|
4277
4915
|
const all = await listConversationsForUser(lookupId);
|
|
4278
|
-
const convs = all.filter((c2) => c2.mind_name === name);
|
|
4916
|
+
const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
|
|
4279
4917
|
return c.json(convs);
|
|
4280
|
-
}).post("/:name/conversations",
|
|
4918
|
+
}).post("/:name/conversations", zValidator8("json", createConvSchema), async (c) => {
|
|
4281
4919
|
const name = c.req.param("name");
|
|
4282
4920
|
const user = c.get("user");
|
|
4283
4921
|
const body = c.req.valid("json");
|
|
@@ -4351,17 +4989,18 @@ var app15 = new Hono15().get("/:name/conversations", async (c) => {
|
|
|
4351
4989
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
4352
4990
|
return c.json({ ok: true });
|
|
4353
4991
|
});
|
|
4354
|
-
var conversations_default =
|
|
4992
|
+
var conversations_default = app21;
|
|
4355
4993
|
|
|
4356
4994
|
// src/web/api/volute/user-conversations.ts
|
|
4357
|
-
import { zValidator as
|
|
4358
|
-
import { Hono as
|
|
4359
|
-
import {
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4995
|
+
import { zValidator as zValidator9 } from "@hono/zod-validator";
|
|
4996
|
+
import { Hono as Hono22 } from "hono";
|
|
4997
|
+
import { streamSSE as streamSSE4 } from "hono/streaming";
|
|
4998
|
+
import { z as z9 } from "zod";
|
|
4999
|
+
var createSchema2 = z9.object({
|
|
5000
|
+
title: z9.string().optional(),
|
|
5001
|
+
participantNames: z9.array(z9.string()).min(1)
|
|
4363
5002
|
});
|
|
4364
|
-
var
|
|
5003
|
+
var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
|
|
4365
5004
|
const user = c.get("user");
|
|
4366
5005
|
const convs = await listConversationsWithParticipants(user.id);
|
|
4367
5006
|
return c.json(convs);
|
|
@@ -4373,7 +5012,7 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
|
|
|
4373
5012
|
}
|
|
4374
5013
|
const msgs = await getMessages(id);
|
|
4375
5014
|
return c.json(msgs);
|
|
4376
|
-
}).post("/",
|
|
5015
|
+
}).post("/", zValidator9("json", createSchema2), async (c) => {
|
|
4377
5016
|
const user = c.get("user");
|
|
4378
5017
|
const body = c.req.valid("json");
|
|
4379
5018
|
const participantIds = /* @__PURE__ */ new Set();
|
|
@@ -4403,6 +5042,31 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
|
|
|
4403
5042
|
participantIds: [...participantIds]
|
|
4404
5043
|
});
|
|
4405
5044
|
return c.json(conv, 201);
|
|
5045
|
+
}).get("/:id/events", async (c) => {
|
|
5046
|
+
const conversationId = c.req.param("id");
|
|
5047
|
+
const user = c.get("user");
|
|
5048
|
+
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
5049
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
5050
|
+
}
|
|
5051
|
+
return streamSSE4(c, async (stream) => {
|
|
5052
|
+
const unsubscribe = subscribe(conversationId, (event) => {
|
|
5053
|
+
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
5054
|
+
if (!stream.aborted) console.error("[chat] SSE write error:", err);
|
|
5055
|
+
});
|
|
5056
|
+
});
|
|
5057
|
+
const keepAlive = setInterval(() => {
|
|
5058
|
+
stream.writeSSE({ data: "" }).catch((err) => {
|
|
5059
|
+
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
5060
|
+
});
|
|
5061
|
+
}, 15e3);
|
|
5062
|
+
await new Promise((resolve20) => {
|
|
5063
|
+
stream.onAbort(() => {
|
|
5064
|
+
unsubscribe();
|
|
5065
|
+
clearInterval(keepAlive);
|
|
5066
|
+
resolve20();
|
|
5067
|
+
});
|
|
5068
|
+
});
|
|
5069
|
+
});
|
|
4406
5070
|
}).delete("/:id", async (c) => {
|
|
4407
5071
|
const id = c.req.param("id");
|
|
4408
5072
|
const user = c.get("user");
|
|
@@ -4410,12 +5074,12 @@ var app16 = new Hono16().use("*", authMiddleware).get("/", async (c) => {
|
|
|
4410
5074
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
4411
5075
|
return c.json({ ok: true });
|
|
4412
5076
|
});
|
|
4413
|
-
var user_conversations_default =
|
|
5077
|
+
var user_conversations_default = app22;
|
|
4414
5078
|
|
|
4415
5079
|
// src/web/app.ts
|
|
4416
5080
|
var httpLog = logger_default.child("http");
|
|
4417
|
-
var
|
|
4418
|
-
|
|
5081
|
+
var app23 = new Hono23();
|
|
5082
|
+
app23.onError((err, c) => {
|
|
4419
5083
|
if (err instanceof HTTPException) {
|
|
4420
5084
|
return err.getResponse();
|
|
4421
5085
|
}
|
|
@@ -4426,10 +5090,10 @@ app17.onError((err, c) => {
|
|
|
4426
5090
|
});
|
|
4427
5091
|
return c.json({ error: "Internal server error" }, 500);
|
|
4428
5092
|
});
|
|
4429
|
-
|
|
5093
|
+
app23.notFound((c) => {
|
|
4430
5094
|
return c.json({ error: "Not found" }, 404);
|
|
4431
5095
|
});
|
|
4432
|
-
|
|
5096
|
+
app23.use("*", async (c, next) => {
|
|
4433
5097
|
const start = Date.now();
|
|
4434
5098
|
await next();
|
|
4435
5099
|
const duration = Date.now() - start;
|
|
@@ -4440,7 +5104,7 @@ app17.use("*", async (c, next) => {
|
|
|
4440
5104
|
httpLog.debug("request", data);
|
|
4441
5105
|
}
|
|
4442
5106
|
});
|
|
4443
|
-
|
|
5107
|
+
app23.get("/api/health", (c) => {
|
|
4444
5108
|
let version = "unknown";
|
|
4445
5109
|
let cached = null;
|
|
4446
5110
|
try {
|
|
@@ -4455,15 +5119,18 @@ app17.get("/api/health", (c) => {
|
|
|
4455
5119
|
...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
|
|
4456
5120
|
});
|
|
4457
5121
|
});
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
5122
|
+
app23.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
5123
|
+
app23.use("/api/*", csrf());
|
|
5124
|
+
app23.use("/api/minds/*", authMiddleware);
|
|
5125
|
+
app23.use("/api/conversations/*", authMiddleware);
|
|
5126
|
+
app23.use("/api/volute/*", authMiddleware);
|
|
5127
|
+
app23.use("/api/system/*", authMiddleware);
|
|
5128
|
+
app23.use("/api/env/*", authMiddleware);
|
|
5129
|
+
app23.use("/api/prompts/*", authMiddleware);
|
|
5130
|
+
app23.use("/api/skills/*", authMiddleware);
|
|
5131
|
+
app23.route("/pages", pages_default);
|
|
5132
|
+
var routes = app23.route("/api/keys", keys_default).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", shared_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);
|
|
5133
|
+
var app_default = app23;
|
|
4467
5134
|
|
|
4468
5135
|
// src/web/server.ts
|
|
4469
5136
|
var MIME_TYPES2 = {
|
|
@@ -4482,8 +5149,8 @@ async function startServer({
|
|
|
4482
5149
|
let assetsDir = "";
|
|
4483
5150
|
let searchDir = dirname3(new URL(import.meta.url).pathname);
|
|
4484
5151
|
for (let i = 0; i < 5; i++) {
|
|
4485
|
-
const candidate =
|
|
4486
|
-
if (
|
|
5152
|
+
const candidate = resolve18(searchDir, "dist", "web-assets");
|
|
5153
|
+
if (existsSync13(candidate)) {
|
|
4487
5154
|
assetsDir = candidate;
|
|
4488
5155
|
break;
|
|
4489
5156
|
}
|
|
@@ -4493,7 +5160,7 @@ async function startServer({
|
|
|
4493
5160
|
app_default.get("*", async (c) => {
|
|
4494
5161
|
const urlPath = new URL(c.req.url).pathname;
|
|
4495
5162
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
4496
|
-
const filePath =
|
|
5163
|
+
const filePath = resolve18(assetsDir, urlPath.slice(1));
|
|
4497
5164
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
4498
5165
|
const s = await stat2(filePath).catch(() => null);
|
|
4499
5166
|
if (s?.isFile()) {
|
|
@@ -4502,7 +5169,7 @@ async function startServer({
|
|
|
4502
5169
|
const body = await readFile3(filePath);
|
|
4503
5170
|
return c.body(body, 200, { "Content-Type": mime });
|
|
4504
5171
|
}
|
|
4505
|
-
const indexPath =
|
|
5172
|
+
const indexPath = resolve18(assetsDir, "index.html");
|
|
4506
5173
|
const indexStat = await stat2(indexPath).catch(() => null);
|
|
4507
5174
|
if (indexStat?.isFile()) {
|
|
4508
5175
|
const body = await readFile3(indexPath, "utf-8");
|
|
@@ -4512,10 +5179,10 @@ async function startServer({
|
|
|
4512
5179
|
});
|
|
4513
5180
|
}
|
|
4514
5181
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
4515
|
-
await new Promise((
|
|
5182
|
+
await new Promise((resolve20, reject) => {
|
|
4516
5183
|
server.on("listening", () => {
|
|
4517
5184
|
logger_default.info("Volute UI running", { hostname, port });
|
|
4518
|
-
|
|
5185
|
+
resolve20();
|
|
4519
5186
|
});
|
|
4520
5187
|
server.on("error", (err) => {
|
|
4521
5188
|
reject(err);
|
|
@@ -4526,14 +5193,14 @@ async function startServer({
|
|
|
4526
5193
|
|
|
4527
5194
|
// src/daemon.ts
|
|
4528
5195
|
if (!process.env.VOLUTE_HOME) {
|
|
4529
|
-
process.env.VOLUTE_HOME =
|
|
5196
|
+
process.env.VOLUTE_HOME = resolve19(homedir2(), ".volute");
|
|
4530
5197
|
}
|
|
4531
5198
|
async function startDaemon(opts) {
|
|
4532
5199
|
const { port, hostname } = opts;
|
|
4533
5200
|
const myPid = String(process.pid);
|
|
4534
5201
|
const home = voluteHome();
|
|
4535
5202
|
if (!opts.foreground) {
|
|
4536
|
-
const rotatingLog = new RotatingLog(
|
|
5203
|
+
const rotatingLog = new RotatingLog(resolve19(home, "daemon.log"));
|
|
4537
5204
|
logger_default.setOutput((line) => rotatingLog.write(`${line}
|
|
4538
5205
|
`));
|
|
4539
5206
|
const write = (...args) => rotatingLog.write(`${format(...args)}
|
|
@@ -4543,10 +5210,21 @@ async function startDaemon(opts) {
|
|
|
4543
5210
|
console.warn = write;
|
|
4544
5211
|
console.info = write;
|
|
4545
5212
|
}
|
|
4546
|
-
const DAEMON_PID_PATH =
|
|
4547
|
-
const DAEMON_JSON_PATH =
|
|
4548
|
-
|
|
5213
|
+
const DAEMON_PID_PATH = resolve19(home, "daemon.pid");
|
|
5214
|
+
const DAEMON_JSON_PATH = resolve19(home, "daemon.json");
|
|
5215
|
+
mkdirSync10(home, { recursive: true });
|
|
4549
5216
|
migrateAgentsToMinds();
|
|
5217
|
+
try {
|
|
5218
|
+
await ensureSharedRepo();
|
|
5219
|
+
} catch (err) {
|
|
5220
|
+
logger_default.warn("failed to initialize shared repo", logger_default.errorData(err));
|
|
5221
|
+
}
|
|
5222
|
+
initRegistryCache();
|
|
5223
|
+
try {
|
|
5224
|
+
await syncBuiltinSkills();
|
|
5225
|
+
} catch (err) {
|
|
5226
|
+
logger_default.error("failed to sync built-in skills", logger_default.errorData(err));
|
|
5227
|
+
}
|
|
4550
5228
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
4551
5229
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
4552
5230
|
process.env.VOLUTE_DAEMON_PORT = String(port);
|
|
@@ -4562,73 +5240,78 @@ async function startDaemon(opts) {
|
|
|
4562
5240
|
}
|
|
4563
5241
|
throw err;
|
|
4564
5242
|
}
|
|
4565
|
-
|
|
4566
|
-
|
|
5243
|
+
writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
5244
|
+
writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
4567
5245
|
`, {
|
|
4568
5246
|
mode: 420
|
|
4569
5247
|
});
|
|
5248
|
+
const delivery = initDeliveryManager();
|
|
4570
5249
|
const manager = initMindManager();
|
|
4571
5250
|
manager.loadCrashAttempts();
|
|
4572
5251
|
const connectors = initConnectorManager();
|
|
4573
|
-
const scheduler =
|
|
4574
|
-
scheduler.start(
|
|
4575
|
-
const mailPoller =
|
|
4576
|
-
mailPoller.start(
|
|
4577
|
-
const tokenBudget =
|
|
4578
|
-
tokenBudget.start(
|
|
5252
|
+
const scheduler = initScheduler();
|
|
5253
|
+
scheduler.start();
|
|
5254
|
+
const mailPoller = initMailPoller();
|
|
5255
|
+
mailPoller.start();
|
|
5256
|
+
const tokenBudget = initTokenBudget();
|
|
5257
|
+
tokenBudget.start();
|
|
4579
5258
|
const registry = readRegistry();
|
|
4580
5259
|
for (const entry of registry) {
|
|
4581
5260
|
try {
|
|
5261
|
+
migrateDotVoluteDir(entry.name);
|
|
4582
5262
|
migrateMindState(entry.name);
|
|
4583
5263
|
} catch (err) {
|
|
4584
5264
|
logger_default.warn(`failed to migrate state for ${entry.name}`, logger_default.errorData(err));
|
|
4585
5265
|
}
|
|
4586
5266
|
}
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
tokenBudget.setBudget(
|
|
4600
|
-
entry.name,
|
|
4601
|
-
config.tokenBudget,
|
|
4602
|
-
config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
|
|
4603
|
-
);
|
|
5267
|
+
const runningEntries = registry.filter((e) => e.running);
|
|
5268
|
+
{
|
|
5269
|
+
const queue = [...runningEntries];
|
|
5270
|
+
const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
|
|
5271
|
+
while (queue.length > 0) {
|
|
5272
|
+
const entry = queue.shift();
|
|
5273
|
+
try {
|
|
5274
|
+
await startMindFull(entry.name);
|
|
5275
|
+
} catch (err) {
|
|
5276
|
+
logger_default.error(`failed to start mind ${entry.name}`, logger_default.errorData(err));
|
|
5277
|
+
setMindRunning(entry.name, false);
|
|
5278
|
+
}
|
|
4604
5279
|
}
|
|
4605
|
-
}
|
|
4606
|
-
|
|
4607
|
-
setMindRunning(entry.name, false);
|
|
4608
|
-
}
|
|
5280
|
+
});
|
|
5281
|
+
await Promise.all(workers);
|
|
4609
5282
|
}
|
|
4610
5283
|
const runningVariants = getAllRunningVariants();
|
|
4611
|
-
|
|
4612
|
-
const
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
5284
|
+
{
|
|
5285
|
+
const queue = [...runningVariants];
|
|
5286
|
+
const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
|
|
5287
|
+
while (queue.length > 0) {
|
|
5288
|
+
const { mindName, variant } = queue.shift();
|
|
5289
|
+
const compositeKey = `${mindName}@${variant.name}`;
|
|
5290
|
+
try {
|
|
5291
|
+
await startMindFull(compositeKey);
|
|
5292
|
+
} catch (err) {
|
|
5293
|
+
logger_default.error(`failed to start variant ${compositeKey}`, logger_default.errorData(err));
|
|
5294
|
+
setVariantRunning(mindName, variant.name, false);
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
});
|
|
5298
|
+
await Promise.all(workers);
|
|
4619
5299
|
}
|
|
5300
|
+
delivery.restoreFromDb().catch((err) => {
|
|
5301
|
+
logger_default.warn("failed to restore delivery queue", logger_default.errorData(err));
|
|
5302
|
+
});
|
|
4620
5303
|
cleanExpiredSessions().catch(() => {
|
|
4621
5304
|
});
|
|
4622
5305
|
logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
|
|
4623
5306
|
function cleanup() {
|
|
4624
5307
|
try {
|
|
4625
|
-
if (
|
|
5308
|
+
if (readFileSync11(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
4626
5309
|
unlinkSync2(DAEMON_PID_PATH);
|
|
4627
5310
|
}
|
|
4628
5311
|
} catch {
|
|
4629
5312
|
}
|
|
4630
5313
|
try {
|
|
4631
|
-
const data = JSON.parse(
|
|
5314
|
+
const data = JSON.parse(readFileSync11(DAEMON_JSON_PATH, "utf-8"));
|
|
4632
5315
|
if (data.token === token) {
|
|
4633
5316
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
4634
5317
|
}
|
|
@@ -4644,6 +5327,7 @@ async function startDaemon(opts) {
|
|
|
4644
5327
|
scheduler.saveState();
|
|
4645
5328
|
mailPoller.stop();
|
|
4646
5329
|
tokenBudget.stop();
|
|
5330
|
+
delivery.dispose();
|
|
4647
5331
|
await connectors.stopAll();
|
|
4648
5332
|
await manager.stopAll();
|
|
4649
5333
|
manager.clearCrashAttempts();
|