volute 0.18.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-AYB7XAWO.js → chunk-2TJGRJ4O.js} +114 -279
- 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-GK4E7LM7.js → chunk-RHEGSQFJ.js} +1 -1
- package/dist/{chunk-MVSXRMJJ.js → chunk-SCUDS4US.js} +1 -1
- package/dist/{chunk-FW5API7X.js → chunk-UJ6GHNR7.js} +2 -2
- package/dist/{chunk-OYSZNX5I.js → chunk-VDWCHYTS.js} +1 -1
- package/dist/{chunk-6DVBMLVN.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 +33 -25
- 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-2HVTHZAT.js → daemon-restart-JMZM3QY4.js} +8 -8
- package/dist/daemon.js +1144 -1108
- 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-YUEKTJ2N.js → history-WNK3DFUM.js} +6 -6
- 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-OKLFO7UY.js → package-MYE2ZJLV.js} +5 -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-BNDTLUPM.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-2Y42P4JY.js → skill-BCVNI6TV.js} +6 -6
- 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 +19 -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-7B3BWF2U.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/0010_delivery_queue.sql +12 -0
- package/drizzle/0011_rename_human_to_brain.sql +1 -0
- package/drizzle/meta/0010_snapshot.json +7 -0
- package/drizzle/meta/0011_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +5 -3
- package/templates/_base/.init/.config/hooks/startup-context.sh +1 -1
- 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 +123 -1
- package/templates/_base/src/lib/types.ts +4 -0
- package/templates/_base/src/lib/volute-server.ts +91 -2
- package/templates/claude/src/server.ts +2 -2
- package/templates/claude/volute-template.json +1 -2
- package/templates/pi/src/agent.ts +1 -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-Z7O7PN2O.js +0 -15
- package/dist/web-assets/assets/index-CtiimdWK.css +0 -1
- package/dist/web-assets/assets/index-kt1_EcuO.js +0 -63
- /package/{templates/_base/_skills → dist/skills}/memory/SKILL.md +0 -0
package/dist/daemon.js
CHANGED
|
@@ -1,39 +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 {
|
|
13
38
|
PROMPT_DEFAULTS,
|
|
14
39
|
PROMPT_KEYS,
|
|
40
|
+
RestartTracker,
|
|
15
41
|
RotatingLog,
|
|
16
42
|
clearJsonMap,
|
|
17
|
-
conversationParticipants,
|
|
18
|
-
conversations,
|
|
19
|
-
getDb,
|
|
20
43
|
getMindManager,
|
|
21
44
|
getMindPromptDefaults,
|
|
22
45
|
getPrompt,
|
|
23
46
|
getPromptIfCustom,
|
|
24
47
|
initMindManager,
|
|
25
48
|
loadJsonMap,
|
|
26
|
-
logBuffer,
|
|
27
|
-
logger_default,
|
|
28
|
-
messages,
|
|
29
|
-
mindHistory,
|
|
30
49
|
saveJsonMap,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} from "./chunk-
|
|
50
|
+
substitute
|
|
51
|
+
} from "./chunk-2TJGRJ4O.js";
|
|
52
|
+
import {
|
|
53
|
+
logBuffer,
|
|
54
|
+
logger_default
|
|
55
|
+
} from "./chunk-YUIHSKR6.js";
|
|
56
|
+
import {
|
|
57
|
+
CHANNELS,
|
|
58
|
+
getChannelDriver
|
|
59
|
+
} from "./chunk-UJ6GHNR7.js";
|
|
37
60
|
import {
|
|
38
61
|
findOpenClawSession,
|
|
39
62
|
importOpenClawConnectors,
|
|
@@ -41,23 +64,32 @@ import {
|
|
|
41
64
|
parseNameFromIdentity,
|
|
42
65
|
readVoluteConfig,
|
|
43
66
|
writeVoluteConfig
|
|
44
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-EMQSAY3B.js";
|
|
45
68
|
import {
|
|
46
69
|
loadMergedEnv,
|
|
47
70
|
mindEnvPath,
|
|
48
71
|
readEnv,
|
|
49
72
|
sharedEnvPath,
|
|
50
73
|
writeEnv
|
|
51
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-VDWCHYTS.js";
|
|
52
75
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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";
|
|
56
88
|
import {
|
|
57
89
|
exec,
|
|
58
90
|
gitExec,
|
|
59
91
|
resolveVoluteBin
|
|
60
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-DYZGP3EW.js";
|
|
61
93
|
import {
|
|
62
94
|
chownMindDir,
|
|
63
95
|
createMindUser,
|
|
@@ -65,17 +97,16 @@ import {
|
|
|
65
97
|
ensureVoluteGroup,
|
|
66
98
|
isIsolationEnabled,
|
|
67
99
|
wrapForIsolation
|
|
68
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-OGXOMR65.js";
|
|
69
101
|
import {
|
|
70
102
|
checkForUpdate,
|
|
71
103
|
checkForUpdateCached,
|
|
72
104
|
getCurrentVersion
|
|
73
|
-
} from "./chunk-
|
|
74
|
-
import "./chunk-D424ZQGI.js";
|
|
105
|
+
} from "./chunk-SCUDS4US.js";
|
|
75
106
|
import {
|
|
76
107
|
buildVoluteSlug,
|
|
77
108
|
writeChannelEntry
|
|
78
|
-
} from "./chunk-
|
|
109
|
+
} from "./chunk-RHEGSQFJ.js";
|
|
79
110
|
import {
|
|
80
111
|
addMind,
|
|
81
112
|
addVariant,
|
|
@@ -85,6 +116,7 @@ import {
|
|
|
85
116
|
findMind,
|
|
86
117
|
findVariant,
|
|
87
118
|
getAllRunningVariants,
|
|
119
|
+
initRegistryCache,
|
|
88
120
|
mindDir,
|
|
89
121
|
nextPort,
|
|
90
122
|
readRegistry,
|
|
@@ -99,14 +131,14 @@ import {
|
|
|
99
131
|
validateBranchName,
|
|
100
132
|
validateMindName,
|
|
101
133
|
voluteHome
|
|
102
|
-
} from "./chunk-
|
|
134
|
+
} from "./chunk-EBGCNDMM.js";
|
|
103
135
|
import "./chunk-K3NQKI34.js";
|
|
104
136
|
|
|
105
137
|
// src/daemon.ts
|
|
106
138
|
import { randomBytes } from "crypto";
|
|
107
|
-
import { mkdirSync as
|
|
139
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
108
140
|
import { homedir as homedir2 } from "os";
|
|
109
|
-
import { resolve as
|
|
141
|
+
import { resolve as resolve19 } from "path";
|
|
110
142
|
import { format } from "util";
|
|
111
143
|
|
|
112
144
|
// src/lib/connector-manager.ts
|
|
@@ -197,16 +229,12 @@ function searchUpwards(...segments) {
|
|
|
197
229
|
}
|
|
198
230
|
return null;
|
|
199
231
|
}
|
|
200
|
-
var MAX_RESTART_ATTEMPTS = 5;
|
|
201
|
-
var BASE_RESTART_DELAY = 3e3;
|
|
202
|
-
var MAX_RESTART_DELAY = 6e4;
|
|
203
232
|
var ConnectorManager = class {
|
|
204
233
|
connectors = /* @__PURE__ */ new Map();
|
|
205
234
|
stopping = /* @__PURE__ */ new Set();
|
|
206
235
|
// "mind:type" keys currently being explicitly stopped
|
|
207
236
|
shuttingDown = false;
|
|
208
|
-
|
|
209
|
-
// "mind:type" -> count
|
|
237
|
+
restartTracker = new RestartTracker();
|
|
210
238
|
async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
|
|
211
239
|
const config = readVoluteConfig(mindDir2) ?? {};
|
|
212
240
|
const types = config.connectors ?? [];
|
|
@@ -319,7 +347,7 @@ var ConnectorManager = class {
|
|
|
319
347
|
}
|
|
320
348
|
this.connectors.get(mindName).set(type, { child, type });
|
|
321
349
|
const stopKey = `${mindName}:${type}`;
|
|
322
|
-
this.
|
|
350
|
+
this.restartTracker.reset(stopKey);
|
|
323
351
|
child.on("exit", (code) => {
|
|
324
352
|
const mindMap = this.connectors.get(mindName);
|
|
325
353
|
if (mindMap?.get(type)?.child === child) {
|
|
@@ -329,15 +357,13 @@ var ConnectorManager = class {
|
|
|
329
357
|
if (this.stopping.has(stopKey)) return;
|
|
330
358
|
clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
|
|
331
359
|
if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
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`);
|
|
335
363
|
return;
|
|
336
364
|
}
|
|
337
|
-
const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
|
|
338
|
-
this.restartAttempts.set(stopKey, attempts + 1);
|
|
339
365
|
clog.info(
|
|
340
|
-
`restarting connector ${type} for ${mindName} \u2014 attempt ${
|
|
366
|
+
`restarting connector ${type} for ${mindName} \u2014 attempt ${attempt}/${this.restartTracker.maxRestartAttempts}, in ${delay}ms`
|
|
341
367
|
);
|
|
342
368
|
setTimeout(() => {
|
|
343
369
|
if (this.shuttingDown || this.stopping.has(stopKey)) return;
|
|
@@ -356,23 +382,23 @@ var ConnectorManager = class {
|
|
|
356
382
|
const stopKey = `${mindName}:${type}`;
|
|
357
383
|
this.stopping.add(stopKey);
|
|
358
384
|
mindMap.delete(type);
|
|
359
|
-
await new Promise((
|
|
360
|
-
tracked.child.on("exit", () =>
|
|
385
|
+
await new Promise((resolve20) => {
|
|
386
|
+
tracked.child.on("exit", () => resolve20());
|
|
361
387
|
try {
|
|
362
388
|
tracked.child.kill("SIGTERM");
|
|
363
389
|
} catch {
|
|
364
|
-
|
|
390
|
+
resolve20();
|
|
365
391
|
}
|
|
366
392
|
setTimeout(() => {
|
|
367
393
|
try {
|
|
368
394
|
tracked.child.kill("SIGKILL");
|
|
369
395
|
} catch {
|
|
370
396
|
}
|
|
371
|
-
|
|
397
|
+
resolve20();
|
|
372
398
|
}, 5e3);
|
|
373
399
|
});
|
|
374
400
|
this.stopping.delete(stopKey);
|
|
375
|
-
this.
|
|
401
|
+
this.restartTracker.reset(stopKey);
|
|
376
402
|
try {
|
|
377
403
|
this.removeConnectorPid(mindName, type);
|
|
378
404
|
} catch (err) {
|
|
@@ -444,7 +470,8 @@ function initConnectorManager() {
|
|
|
444
470
|
return instance;
|
|
445
471
|
}
|
|
446
472
|
function getConnectorManager() {
|
|
447
|
-
if (!instance)
|
|
473
|
+
if (!instance)
|
|
474
|
+
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
448
475
|
return instance;
|
|
449
476
|
}
|
|
450
477
|
|
|
@@ -468,15 +495,13 @@ var INITIAL_RECONNECT_MS = 1e3;
|
|
|
468
495
|
var MAX_RECONNECT_MS = 6e4;
|
|
469
496
|
var MailPoller = class {
|
|
470
497
|
ws = null;
|
|
471
|
-
daemonPort = null;
|
|
472
|
-
daemonToken = null;
|
|
473
498
|
running = false;
|
|
474
499
|
pingTimer = null;
|
|
475
500
|
reconnectTimer = null;
|
|
476
501
|
reconnectDelay = INITIAL_RECONNECT_MS;
|
|
477
502
|
reconnectAttempts = 0;
|
|
478
503
|
disconnectedAt = null;
|
|
479
|
-
start(
|
|
504
|
+
start() {
|
|
480
505
|
if (this.running) {
|
|
481
506
|
mlog.warn("already running \u2014 ignoring duplicate start");
|
|
482
507
|
return;
|
|
@@ -486,8 +511,6 @@ var MailPoller = class {
|
|
|
486
511
|
mlog.info("no systems config \u2014 mail disabled");
|
|
487
512
|
return;
|
|
488
513
|
}
|
|
489
|
-
this.daemonPort = daemonPort ?? null;
|
|
490
|
-
this.daemonToken = daemonToken ?? null;
|
|
491
514
|
this.running = true;
|
|
492
515
|
this.connect();
|
|
493
516
|
}
|
|
@@ -643,51 +666,29 @@ var MailPoller = class {
|
|
|
643
666
|
mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
|
|
644
667
|
return;
|
|
645
668
|
}
|
|
646
|
-
const channel = `mail:${email.from.address}`;
|
|
647
|
-
const sender = email.from.name || email.from.address;
|
|
648
669
|
const text = formatEmailContent(email);
|
|
649
|
-
const body = JSON.stringify({
|
|
650
|
-
content: [{ type: "text", text }],
|
|
651
|
-
channel,
|
|
652
|
-
sender,
|
|
653
|
-
platform: "Email",
|
|
654
|
-
isDM: true
|
|
655
|
-
});
|
|
656
|
-
if (!this.daemonPort || !this.daemonToken) {
|
|
657
|
-
mlog.warn(`cannot deliver to ${mind}: daemon port/token not set`);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
|
|
661
|
-
const controller = new AbortController();
|
|
662
|
-
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
663
670
|
try {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
},
|
|
671
|
-
body,
|
|
672
|
-
signal: controller.signal
|
|
673
|
-
});
|
|
674
|
-
if (!res.ok) {
|
|
675
|
-
mlog.warn(`deliver to ${mind} got HTTP ${res.status}`);
|
|
676
|
-
} else {
|
|
677
|
-
mlog.info(`delivered email from ${email.from.address} to ${mind}`);
|
|
678
|
-
}
|
|
679
|
-
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
|
|
680
677
|
});
|
|
678
|
+
mlog.info(`delivered email from ${email.from.address} to ${mind}`);
|
|
681
679
|
} catch (err) {
|
|
682
680
|
mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
|
|
683
|
-
} finally {
|
|
684
|
-
clearTimeout(timeout);
|
|
685
681
|
}
|
|
686
682
|
}
|
|
687
683
|
};
|
|
688
684
|
var instance2 = null;
|
|
685
|
+
function initMailPoller() {
|
|
686
|
+
if (instance2) throw new Error("MailPoller already initialized");
|
|
687
|
+
instance2 = new MailPoller();
|
|
688
|
+
return instance2;
|
|
689
|
+
}
|
|
689
690
|
function getMailPoller() {
|
|
690
|
-
if (!instance2)
|
|
691
|
+
if (!instance2) throw new Error("MailPoller not initialized \u2014 call initMailPoller() first");
|
|
691
692
|
return instance2;
|
|
692
693
|
}
|
|
693
694
|
async function ensureMailAddress(mindName) {
|
|
@@ -846,10 +847,20 @@ function migrateProfileScript() {
|
|
|
846
847
|
}
|
|
847
848
|
|
|
848
849
|
// src/lib/migrate-state.ts
|
|
849
|
-
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";
|
|
850
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
|
+
}
|
|
851
862
|
function migrateMindState(name) {
|
|
852
|
-
const src = resolve4(mindDir(name), ".
|
|
863
|
+
const src = resolve4(mindDir(name), ".mind");
|
|
853
864
|
if (!existsSync4(src)) return;
|
|
854
865
|
const dest = stateDir(name);
|
|
855
866
|
mkdirSync2(dest, { recursive: true });
|
|
@@ -883,14 +894,10 @@ var Scheduler = class {
|
|
|
883
894
|
interval = null;
|
|
884
895
|
lastFired = /* @__PURE__ */ new Map();
|
|
885
896
|
// "mind:scheduleId" → epoch minute
|
|
886
|
-
daemonPort = null;
|
|
887
|
-
daemonToken = null;
|
|
888
897
|
get statePath() {
|
|
889
898
|
return resolve5(voluteHome(), "scheduler-state.json");
|
|
890
899
|
}
|
|
891
|
-
start(
|
|
892
|
-
this.daemonPort = daemonPort ?? null;
|
|
893
|
-
this.daemonToken = daemonToken ?? null;
|
|
900
|
+
start() {
|
|
894
901
|
this.loadState();
|
|
895
902
|
this.interval = setInterval(() => this.tick(), 6e4);
|
|
896
903
|
}
|
|
@@ -921,9 +928,6 @@ var Scheduler = class {
|
|
|
921
928
|
this.schedules.delete(mindName);
|
|
922
929
|
}
|
|
923
930
|
tick() {
|
|
924
|
-
for (const mind of this.schedules.keys()) {
|
|
925
|
-
this.loadSchedules(mind);
|
|
926
|
-
}
|
|
927
931
|
const now = /* @__PURE__ */ new Date();
|
|
928
932
|
for (const [mind, schedules] of this.schedules) {
|
|
929
933
|
for (const schedule of schedules) {
|
|
@@ -954,69 +958,39 @@ var Scheduler = class {
|
|
|
954
958
|
}
|
|
955
959
|
}
|
|
956
960
|
async fire(mindName, schedule) {
|
|
957
|
-
const entry = findMind(mindName);
|
|
958
|
-
if (!entry) return;
|
|
959
|
-
const body = JSON.stringify({
|
|
960
|
-
content: [{ type: "text", text: schedule.message }],
|
|
961
|
-
channel: "system:scheduler",
|
|
962
|
-
sender: schedule.id
|
|
963
|
-
});
|
|
964
|
-
const controller = new AbortController();
|
|
965
|
-
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
966
961
|
try {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
method: "POST",
|
|
972
|
-
headers: {
|
|
973
|
-
"Content-Type": "application/json",
|
|
974
|
-
Authorization: `Bearer ${this.daemonToken}`,
|
|
975
|
-
Origin: daemonUrl
|
|
976
|
-
},
|
|
977
|
-
body,
|
|
978
|
-
signal: controller.signal
|
|
979
|
-
});
|
|
980
|
-
} else {
|
|
981
|
-
res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
982
|
-
method: "POST",
|
|
983
|
-
headers: { "Content-Type": "application/json" },
|
|
984
|
-
body,
|
|
985
|
-
signal: controller.signal
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
if (!res.ok) {
|
|
989
|
-
slog.warn(`"${schedule.id}" for ${mindName} got HTTP ${res.status}`);
|
|
990
|
-
} else {
|
|
991
|
-
slog.info(`fired "${schedule.id}" for ${mindName}`);
|
|
992
|
-
}
|
|
993
|
-
await res.text().catch(() => {
|
|
962
|
+
await deliverMessage(mindName, {
|
|
963
|
+
content: [{ type: "text", text: schedule.message }],
|
|
964
|
+
channel: "system:scheduler",
|
|
965
|
+
sender: schedule.id
|
|
994
966
|
});
|
|
967
|
+
slog.info(`fired "${schedule.id}" for ${mindName}`);
|
|
995
968
|
} catch (err) {
|
|
996
969
|
slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
997
|
-
} finally {
|
|
998
|
-
clearTimeout(timeout);
|
|
999
970
|
}
|
|
1000
971
|
}
|
|
1001
972
|
};
|
|
1002
973
|
var instance3 = null;
|
|
974
|
+
function initScheduler() {
|
|
975
|
+
if (instance3) throw new Error("Scheduler already initialized");
|
|
976
|
+
instance3 = new Scheduler();
|
|
977
|
+
return instance3;
|
|
978
|
+
}
|
|
1003
979
|
function getScheduler() {
|
|
1004
|
-
if (!instance3)
|
|
980
|
+
if (!instance3) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
|
|
1005
981
|
return instance3;
|
|
1006
982
|
}
|
|
1007
983
|
|
|
1008
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";
|
|
1009
987
|
var tlog = logger_default.child("token-budget");
|
|
1010
988
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
1011
989
|
var MAX_QUEUE_SIZE = 100;
|
|
1012
990
|
var TokenBudget = class {
|
|
1013
991
|
budgets = /* @__PURE__ */ new Map();
|
|
1014
992
|
interval = null;
|
|
1015
|
-
|
|
1016
|
-
daemonToken = null;
|
|
1017
|
-
start(daemonPort, daemonToken) {
|
|
1018
|
-
this.daemonPort = daemonPort ?? null;
|
|
1019
|
-
this.daemonToken = daemonToken ?? null;
|
|
993
|
+
start() {
|
|
1020
994
|
this.interval = setInterval(() => this.tick(), 6e4);
|
|
1021
995
|
}
|
|
1022
996
|
stop() {
|
|
@@ -1030,14 +1004,21 @@ var TokenBudget = class {
|
|
|
1030
1004
|
existing.tokenLimit = tokenLimit;
|
|
1031
1005
|
existing.periodMinutes = periodMinutes;
|
|
1032
1006
|
} else {
|
|
1033
|
-
this.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
periodMinutes
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
+
}
|
|
1041
1022
|
}
|
|
1042
1023
|
}
|
|
1043
1024
|
removeBudget(mind) {
|
|
@@ -1047,6 +1028,7 @@ var TokenBudget = class {
|
|
|
1047
1028
|
const state = this.budgets.get(mind);
|
|
1048
1029
|
if (!state) return;
|
|
1049
1030
|
state.tokensUsed += inputTokens + outputTokens;
|
|
1031
|
+
this.saveBudgetState(mind, state);
|
|
1050
1032
|
}
|
|
1051
1033
|
/** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
|
|
1052
1034
|
checkBudget(mind) {
|
|
@@ -1097,6 +1079,7 @@ var TokenBudget = class {
|
|
|
1097
1079
|
state.tokensUsed = 0;
|
|
1098
1080
|
state.periodStart = now;
|
|
1099
1081
|
state.warningInjected = false;
|
|
1082
|
+
this.saveBudgetState(mind, state);
|
|
1100
1083
|
const queued = this.drain(mind);
|
|
1101
1084
|
if (queued.length > 0) {
|
|
1102
1085
|
this.replay(mind, queued).catch((err) => {
|
|
@@ -1106,68 +1089,117 @@ var TokenBudget = class {
|
|
|
1106
1089
|
}
|
|
1107
1090
|
}
|
|
1108
1091
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
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;
|
|
1117
1130
|
}
|
|
1131
|
+
}
|
|
1132
|
+
async replay(mindName, messages2) {
|
|
1118
1133
|
const summary = messages2.map((m) => {
|
|
1119
1134
|
const from = m.sender ? `[${m.sender}]` : "";
|
|
1120
1135
|
const ch = m.channel ? `(${m.channel})` : "";
|
|
1121
1136
|
return `${from}${ch} ${m.textContent}`;
|
|
1122
1137
|
}).join("\n");
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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:
|
|
1128
1144
|
|
|
1129
1145
|
${summary}`
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
});
|
|
1135
|
-
const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
|
|
1136
|
-
const controller = new AbortController();
|
|
1137
|
-
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
1138
|
-
try {
|
|
1139
|
-
const res = await fetch(`${daemonUrl}/api/minds/${encodeURIComponent(mindName)}/message`, {
|
|
1140
|
-
method: "POST",
|
|
1141
|
-
headers: {
|
|
1142
|
-
"Content-Type": "application/json",
|
|
1143
|
-
Authorization: `Bearer ${this.daemonToken}`,
|
|
1144
|
-
Origin: daemonUrl
|
|
1145
|
-
},
|
|
1146
|
-
body,
|
|
1147
|
-
signal: controller.signal
|
|
1148
|
-
});
|
|
1149
|
-
if (!res.ok) {
|
|
1150
|
-
tlog.warn(`replay for ${mindName} got HTTP ${res.status}`);
|
|
1151
|
-
} else {
|
|
1152
|
-
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
1153
|
-
}
|
|
1154
|
-
await res.text().catch(() => {
|
|
1146
|
+
}
|
|
1147
|
+
],
|
|
1148
|
+
channel: "system:budget-replay",
|
|
1149
|
+
sender: "system"
|
|
1155
1150
|
});
|
|
1151
|
+
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
1156
1152
|
} catch (err) {
|
|
1157
1153
|
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
1158
1154
|
const state = this.budgets.get(mindName);
|
|
1159
1155
|
if (state) state.queue.push(...messages2);
|
|
1160
|
-
} finally {
|
|
1161
|
-
clearTimeout(timeout);
|
|
1162
1156
|
}
|
|
1163
1157
|
}
|
|
1164
1158
|
};
|
|
1165
1159
|
var instance4 = null;
|
|
1160
|
+
function initTokenBudget() {
|
|
1161
|
+
if (instance4) throw new Error("TokenBudget already initialized");
|
|
1162
|
+
instance4 = new TokenBudget();
|
|
1163
|
+
return instance4;
|
|
1164
|
+
}
|
|
1166
1165
|
function getTokenBudget() {
|
|
1167
|
-
if (!instance4)
|
|
1166
|
+
if (!instance4) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
|
|
1168
1167
|
return instance4;
|
|
1169
1168
|
}
|
|
1170
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
|
+
|
|
1171
1203
|
// src/web/middleware/auth.ts
|
|
1172
1204
|
import { timingSafeEqual } from "crypto";
|
|
1173
1205
|
import { eq as eq2, lt } from "drizzle-orm";
|
|
@@ -1180,7 +1212,7 @@ import { and, count, eq } from "drizzle-orm";
|
|
|
1180
1212
|
async function createUser(username, password) {
|
|
1181
1213
|
const db = await getDb();
|
|
1182
1214
|
const hash = hashSync(password, 10);
|
|
1183
|
-
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "
|
|
1215
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
|
|
1184
1216
|
const role = value === 0 ? "admin" : "pending";
|
|
1185
1217
|
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
1186
1218
|
id: users.id,
|
|
@@ -1315,6 +1347,8 @@ function isValidDaemonToken(token) {
|
|
|
1315
1347
|
return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
|
|
1316
1348
|
}
|
|
1317
1349
|
var SESSION_MAX_AGE = 864e5;
|
|
1350
|
+
var SESSION_CACHE_TTL = 5 * 60 * 1e3;
|
|
1351
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
1318
1352
|
async function createSession(userId) {
|
|
1319
1353
|
const db = await getDb();
|
|
1320
1354
|
const sessionId = crypto.randomUUID();
|
|
@@ -1322,6 +1356,7 @@ async function createSession(userId) {
|
|
|
1322
1356
|
return sessionId;
|
|
1323
1357
|
}
|
|
1324
1358
|
async function deleteSession(sessionId) {
|
|
1359
|
+
sessionCache.delete(sessionId);
|
|
1325
1360
|
const db = await getDb();
|
|
1326
1361
|
await db.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
1327
1362
|
}
|
|
@@ -1352,30 +1387,44 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
1352
1387
|
if (authHeader?.startsWith("Bearer ")) {
|
|
1353
1388
|
const token = authHeader.slice(7);
|
|
1354
1389
|
if (token && isValidDaemonToken(token)) {
|
|
1355
|
-
c.set("user", { id: 0, username: "cli", role: "admin", user_type: "
|
|
1390
|
+
c.set("user", { id: 0, username: "cli", role: "admin", user_type: "brain" });
|
|
1356
1391
|
await next();
|
|
1357
1392
|
return;
|
|
1358
1393
|
}
|
|
1359
1394
|
}
|
|
1360
1395
|
const sessionId = getCookie(c, "volute_session");
|
|
1361
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
|
+
}
|
|
1362
1404
|
const userId = await getSessionUserId(sessionId);
|
|
1363
|
-
if (userId == null)
|
|
1405
|
+
if (userId == null) {
|
|
1406
|
+
sessionCache.delete(sessionId);
|
|
1407
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
1408
|
+
}
|
|
1364
1409
|
const user = await getUser(userId);
|
|
1365
|
-
if (!user)
|
|
1410
|
+
if (!user) {
|
|
1411
|
+
sessionCache.delete(sessionId);
|
|
1412
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
1413
|
+
}
|
|
1366
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 });
|
|
1367
1416
|
c.set("user", user);
|
|
1368
1417
|
await next();
|
|
1369
1418
|
});
|
|
1370
1419
|
|
|
1371
1420
|
// src/web/server.ts
|
|
1372
|
-
import { existsSync as
|
|
1421
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1373
1422
|
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
1374
|
-
import { dirname as
|
|
1423
|
+
import { dirname as dirname3, extname as extname2, resolve as resolve18 } from "path";
|
|
1375
1424
|
import { serve } from "@hono/node-server";
|
|
1376
1425
|
|
|
1377
1426
|
// src/web/app.ts
|
|
1378
|
-
import { Hono as
|
|
1427
|
+
import { Hono as Hono23 } from "hono";
|
|
1379
1428
|
import { bodyLimit } from "hono/body-limit";
|
|
1380
1429
|
import { csrf } from "hono/csrf";
|
|
1381
1430
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -1408,7 +1457,7 @@ var admin = new Hono().use(authMiddleware).get("/users", async (c) => {
|
|
|
1408
1457
|
await getOrCreateMindUser(mind.name);
|
|
1409
1458
|
}
|
|
1410
1459
|
const type = c.req.query("type");
|
|
1411
|
-
if (type === "
|
|
1460
|
+
if (type === "brain" || type === "mind") {
|
|
1412
1461
|
return c.json(await listUsersByType(type));
|
|
1413
1462
|
}
|
|
1414
1463
|
return c.json(await listUsers());
|
|
@@ -1713,9 +1762,9 @@ var sharedEnvApp = new Hono4().get("/", (c) => {
|
|
|
1713
1762
|
var env_default = app4;
|
|
1714
1763
|
|
|
1715
1764
|
// src/web/api/files.ts
|
|
1716
|
-
import { existsSync as
|
|
1765
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1717
1766
|
import { readdir, readFile } from "fs/promises";
|
|
1718
|
-
import { resolve as
|
|
1767
|
+
import { resolve as resolve7 } from "path";
|
|
1719
1768
|
import { Hono as Hono5 } from "hono";
|
|
1720
1769
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
1721
1770
|
var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
@@ -1723,8 +1772,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
|
1723
1772
|
const entry = findMind(name);
|
|
1724
1773
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1725
1774
|
const dir = mindDir(name);
|
|
1726
|
-
const homeDir =
|
|
1727
|
-
if (!
|
|
1775
|
+
const homeDir = resolve7(dir, "home");
|
|
1776
|
+
if (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1728
1777
|
const allFiles = await readdir(homeDir);
|
|
1729
1778
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1730
1779
|
return c.json(files);
|
|
@@ -1737,8 +1786,8 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
|
1737
1786
|
const entry = findMind(name);
|
|
1738
1787
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1739
1788
|
const dir = mindDir(name);
|
|
1740
|
-
const filePath =
|
|
1741
|
-
if (!
|
|
1789
|
+
const filePath = resolve7(dir, "home", filename);
|
|
1790
|
+
if (!existsSync6(filePath)) {
|
|
1742
1791
|
return c.json({ error: "File not found" }, 404);
|
|
1743
1792
|
}
|
|
1744
1793
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -1746,18 +1795,109 @@ var app5 = new Hono5().get("/:name/files", async (c) => {
|
|
|
1746
1795
|
});
|
|
1747
1796
|
var files_default = app5;
|
|
1748
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
|
+
|
|
1749
1889
|
// src/web/api/logs.ts
|
|
1750
1890
|
import { spawn as spawn2 } from "child_process";
|
|
1751
|
-
import { existsSync as
|
|
1752
|
-
import { resolve as
|
|
1753
|
-
import { Hono as
|
|
1891
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1892
|
+
import { resolve as resolve9 } from "path";
|
|
1893
|
+
import { Hono as Hono7 } from "hono";
|
|
1754
1894
|
import { streamSSE } from "hono/streaming";
|
|
1755
|
-
var
|
|
1895
|
+
var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
1756
1896
|
const name = c.req.param("name");
|
|
1757
1897
|
const entry = findMind(name);
|
|
1758
1898
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1759
|
-
const logFile =
|
|
1760
|
-
if (!
|
|
1899
|
+
const logFile = resolve9(stateDir(name), "logs", "mind.log");
|
|
1900
|
+
if (!existsSync8(logFile)) {
|
|
1761
1901
|
return c.json({ error: "No log file found" }, 404);
|
|
1762
1902
|
}
|
|
1763
1903
|
return streamSSE(c, async (stream) => {
|
|
@@ -1775,17 +1915,17 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
|
|
|
1775
1915
|
stream.onAbort(() => {
|
|
1776
1916
|
tail.kill();
|
|
1777
1917
|
});
|
|
1778
|
-
await new Promise((
|
|
1779
|
-
tail.on("exit",
|
|
1780
|
-
stream.onAbort(
|
|
1918
|
+
await new Promise((resolve20) => {
|
|
1919
|
+
tail.on("exit", resolve20);
|
|
1920
|
+
stream.onAbort(resolve20);
|
|
1781
1921
|
});
|
|
1782
1922
|
});
|
|
1783
1923
|
}).get("/:name/logs/tail", async (c) => {
|
|
1784
1924
|
const name = c.req.param("name");
|
|
1785
1925
|
const entry = findMind(name);
|
|
1786
1926
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
1787
|
-
const logFile =
|
|
1788
|
-
if (!
|
|
1927
|
+
const logFile = resolve9(stateDir(name), "logs", "mind.log");
|
|
1928
|
+
if (!existsSync8(logFile)) {
|
|
1789
1929
|
return c.json({ error: "No log file found" }, 404);
|
|
1790
1930
|
}
|
|
1791
1931
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
@@ -1795,314 +1935,18 @@ var app6 = new Hono6().get("/:name/logs", async (c) => {
|
|
|
1795
1935
|
tail.stdout.on("data", (data) => {
|
|
1796
1936
|
output += data.toString();
|
|
1797
1937
|
});
|
|
1798
|
-
await new Promise((
|
|
1799
|
-
tail.on("exit",
|
|
1938
|
+
await new Promise((resolve20) => {
|
|
1939
|
+
tail.on("exit", resolve20);
|
|
1800
1940
|
});
|
|
1801
1941
|
return c.text(output);
|
|
1802
1942
|
});
|
|
1803
|
-
var logs_default =
|
|
1943
|
+
var logs_default = app7;
|
|
1804
1944
|
|
|
1805
1945
|
// src/web/api/mind-skills.ts
|
|
1806
1946
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
1807
|
-
import { Hono as
|
|
1947
|
+
import { Hono as Hono8 } from "hono";
|
|
1808
1948
|
import { z as z2 } from "zod";
|
|
1809
|
-
|
|
1810
|
-
// src/lib/skills.ts
|
|
1811
|
-
import {
|
|
1812
|
-
cpSync,
|
|
1813
|
-
existsSync as existsSync7,
|
|
1814
|
-
mkdirSync as mkdirSync3,
|
|
1815
|
-
readdirSync as readdirSync2,
|
|
1816
|
-
readFileSync as readFileSync4,
|
|
1817
|
-
rmSync,
|
|
1818
|
-
writeFileSync as writeFileSync3
|
|
1819
|
-
} from "fs";
|
|
1820
|
-
import { tmpdir } from "os";
|
|
1821
|
-
import { basename, join, resolve as resolve8 } from "path";
|
|
1822
|
-
import { eq as eq3, sql } from "drizzle-orm";
|
|
1823
|
-
var VALID_SKILL_ID = /^[a-zA-Z0-9_-]+$/;
|
|
1824
|
-
function validateSkillId(id) {
|
|
1825
|
-
if (!id || !VALID_SKILL_ID.test(id)) {
|
|
1826
|
-
throw new Error(`Invalid skill ID: ${id}`);
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
function sharedSkillsDir() {
|
|
1830
|
-
return resolve8(voluteHome(), "skills");
|
|
1831
|
-
}
|
|
1832
|
-
function parseSkillMd(content) {
|
|
1833
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1834
|
-
if (!match) return { name: "", description: "" };
|
|
1835
|
-
const frontmatter = match[1];
|
|
1836
|
-
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
1837
|
-
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
1838
|
-
return {
|
|
1839
|
-
name: nameMatch?.[1].trim() ?? "",
|
|
1840
|
-
description: descMatch?.[1].trim() ?? ""
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
async function listSharedSkills() {
|
|
1844
|
-
const db = await getDb();
|
|
1845
|
-
return db.select().from(sharedSkills).all();
|
|
1846
|
-
}
|
|
1847
|
-
async function getSharedSkill(id) {
|
|
1848
|
-
const db = await getDb();
|
|
1849
|
-
return db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1850
|
-
}
|
|
1851
|
-
async function importSkillFromDir(sourceDir, author) {
|
|
1852
|
-
const skillMdPath = join(sourceDir, "SKILL.md");
|
|
1853
|
-
if (!existsSync7(skillMdPath)) {
|
|
1854
|
-
throw new Error("SKILL.md not found in source directory");
|
|
1855
|
-
}
|
|
1856
|
-
const content = readFileSync4(skillMdPath, "utf-8");
|
|
1857
|
-
const { name, description } = parseSkillMd(content);
|
|
1858
|
-
const id = basename(sourceDir);
|
|
1859
|
-
if (!id || id === "." || id === "..") {
|
|
1860
|
-
throw new Error("Invalid skill directory name");
|
|
1861
|
-
}
|
|
1862
|
-
validateSkillId(id);
|
|
1863
|
-
const destDir = join(sharedSkillsDir(), id);
|
|
1864
|
-
if (existsSync7(destDir)) rmSync(destDir, { recursive: true });
|
|
1865
|
-
mkdirSync3(destDir, { recursive: true });
|
|
1866
|
-
cpSync(sourceDir, destDir, { recursive: true });
|
|
1867
|
-
const upstreamPath = join(destDir, ".upstream.json");
|
|
1868
|
-
if (existsSync7(upstreamPath)) rmSync(upstreamPath);
|
|
1869
|
-
const db = await getDb();
|
|
1870
|
-
const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1871
|
-
const version = existing ? existing.version + 1 : 1;
|
|
1872
|
-
await db.insert(sharedSkills).values({ id, name: name || id, description, author, version }).onConflictDoUpdate({
|
|
1873
|
-
target: sharedSkills.id,
|
|
1874
|
-
set: {
|
|
1875
|
-
name: name || id,
|
|
1876
|
-
description,
|
|
1877
|
-
author,
|
|
1878
|
-
version,
|
|
1879
|
-
updated_at: sql`(datetime('now'))`
|
|
1880
|
-
}
|
|
1881
|
-
});
|
|
1882
|
-
const row = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1883
|
-
if (!row) throw new Error(`Failed to upsert shared skill: ${id}`);
|
|
1884
|
-
return row;
|
|
1885
|
-
}
|
|
1886
|
-
async function removeSharedSkill(id) {
|
|
1887
|
-
const db = await getDb();
|
|
1888
|
-
const existing = await db.select().from(sharedSkills).where(eq3(sharedSkills.id, id)).get();
|
|
1889
|
-
if (!existing) throw new Error(`Shared skill not found: ${id}`);
|
|
1890
|
-
await db.delete(sharedSkills).where(eq3(sharedSkills.id, id));
|
|
1891
|
-
const dir = join(sharedSkillsDir(), id);
|
|
1892
|
-
if (existsSync7(dir)) rmSync(dir, { recursive: true });
|
|
1893
|
-
}
|
|
1894
|
-
function mindSkillsDir(dir) {
|
|
1895
|
-
return resolve8(dir, "home", ".claude", "skills");
|
|
1896
|
-
}
|
|
1897
|
-
function readUpstream(skillDir) {
|
|
1898
|
-
const upstreamPath = join(skillDir, ".upstream.json");
|
|
1899
|
-
if (!existsSync7(upstreamPath)) return null;
|
|
1900
|
-
try {
|
|
1901
|
-
const data = JSON.parse(readFileSync4(upstreamPath, "utf-8"));
|
|
1902
|
-
if (typeof data?.source !== "string" || typeof data?.version !== "number" || typeof data?.baseCommit !== "string") {
|
|
1903
|
-
return null;
|
|
1904
|
-
}
|
|
1905
|
-
return data;
|
|
1906
|
-
} catch {
|
|
1907
|
-
return null;
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
async function installSkill(_mindName, dir, skillId) {
|
|
1911
|
-
validateSkillId(skillId);
|
|
1912
|
-
const shared = await getSharedSkill(skillId);
|
|
1913
|
-
if (!shared) throw new Error(`Shared skill not found: ${skillId}`);
|
|
1914
|
-
const sourceDir = join(sharedSkillsDir(), skillId);
|
|
1915
|
-
if (!existsSync7(sourceDir)) throw new Error(`Shared skill files not found: ${skillId}`);
|
|
1916
|
-
const destDir = join(mindSkillsDir(dir), skillId);
|
|
1917
|
-
if (existsSync7(destDir)) throw new Error(`Skill already installed: ${skillId}`);
|
|
1918
|
-
mkdirSync3(destDir, { recursive: true });
|
|
1919
|
-
cpSync(sourceDir, destDir, { recursive: true });
|
|
1920
|
-
await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
|
|
1921
|
-
await gitExec(["commit", "-m", `Install shared skill: ${skillId}`], { cwd: dir });
|
|
1922
|
-
const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
|
|
1923
|
-
const upstream = {
|
|
1924
|
-
source: skillId,
|
|
1925
|
-
version: shared.version,
|
|
1926
|
-
baseCommit: commitHash
|
|
1927
|
-
};
|
|
1928
|
-
writeFileSync3(join(destDir, ".upstream.json"), `${JSON.stringify(upstream, null, 2)}
|
|
1929
|
-
`);
|
|
1930
|
-
await gitExec(["add", join("home", ".claude", "skills", skillId, ".upstream.json")], {
|
|
1931
|
-
cwd: dir
|
|
1932
|
-
});
|
|
1933
|
-
await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
|
|
1934
|
-
}
|
|
1935
|
-
async function uninstallSkill(_mindName, dir, skillId) {
|
|
1936
|
-
validateSkillId(skillId);
|
|
1937
|
-
const skillDir = join(mindSkillsDir(dir), skillId);
|
|
1938
|
-
if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
|
|
1939
|
-
rmSync(skillDir, { recursive: true });
|
|
1940
|
-
await gitExec(["add", join("home", ".claude", "skills", skillId)], { cwd: dir });
|
|
1941
|
-
await gitExec(["commit", "-m", `Uninstall skill: ${skillId}`], { cwd: dir });
|
|
1942
|
-
}
|
|
1943
|
-
async function updateSkill(_mindName, dir, skillId) {
|
|
1944
|
-
validateSkillId(skillId);
|
|
1945
|
-
const skillDir = join(mindSkillsDir(dir), skillId);
|
|
1946
|
-
if (!existsSync7(skillDir)) throw new Error(`Skill not installed: ${skillId}`);
|
|
1947
|
-
const upstream = readUpstream(skillDir);
|
|
1948
|
-
if (!upstream) throw new Error(`No upstream tracking for skill: ${skillId}`);
|
|
1949
|
-
const shared = await getSharedSkill(upstream.source);
|
|
1950
|
-
if (!shared) throw new Error(`Shared skill no longer exists: ${upstream.source}`);
|
|
1951
|
-
if (shared.version <= upstream.version) {
|
|
1952
|
-
return { status: "up-to-date" };
|
|
1953
|
-
}
|
|
1954
|
-
const sourceDir = join(sharedSkillsDir(), upstream.source);
|
|
1955
|
-
if (!existsSync7(sourceDir)) throw new Error(`Shared skill files missing: ${upstream.source}`);
|
|
1956
|
-
const relSkillPath = join("home", ".claude", "skills", skillId);
|
|
1957
|
-
const currentFiles = listFilesRecursive(skillDir).filter((f) => f !== ".upstream.json");
|
|
1958
|
-
const newFiles = listFilesRecursive(sourceDir).filter((f) => f !== ".upstream.json");
|
|
1959
|
-
const allFiles = [.../* @__PURE__ */ new Set([...currentFiles, ...newFiles])];
|
|
1960
|
-
const conflictFiles = [];
|
|
1961
|
-
const tmpBase = join(tmpdir(), `volute-merge-${process.pid}-${Date.now()}`);
|
|
1962
|
-
mkdirSync3(tmpBase, { recursive: true });
|
|
1963
|
-
try {
|
|
1964
|
-
for (const file of allFiles) {
|
|
1965
|
-
const currentPath = join(skillDir, file);
|
|
1966
|
-
const newPath = join(sourceDir, file);
|
|
1967
|
-
const currentExists = existsSync7(currentPath);
|
|
1968
|
-
const newExists = existsSync7(newPath);
|
|
1969
|
-
if (!currentExists && newExists) {
|
|
1970
|
-
const destPath = join(skillDir, file);
|
|
1971
|
-
mkdirSync3(join(skillDir, ...file.split("/").slice(0, -1)), { recursive: true });
|
|
1972
|
-
cpSync(newPath, destPath);
|
|
1973
|
-
continue;
|
|
1974
|
-
}
|
|
1975
|
-
if (currentExists && !newExists) {
|
|
1976
|
-
let baseContent2 = null;
|
|
1977
|
-
try {
|
|
1978
|
-
baseContent2 = await gitExec(
|
|
1979
|
-
["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
|
|
1980
|
-
{ cwd: dir }
|
|
1981
|
-
);
|
|
1982
|
-
} catch {
|
|
1983
|
-
continue;
|
|
1984
|
-
}
|
|
1985
|
-
const currentContent2 = readFileSync4(currentPath, "utf-8");
|
|
1986
|
-
if (currentContent2 === baseContent2) {
|
|
1987
|
-
rmSync(currentPath);
|
|
1988
|
-
}
|
|
1989
|
-
continue;
|
|
1990
|
-
}
|
|
1991
|
-
let baseContent;
|
|
1992
|
-
try {
|
|
1993
|
-
baseContent = await gitExec(
|
|
1994
|
-
["show", `${upstream.baseCommit}:${join(relSkillPath, file)}`],
|
|
1995
|
-
{ cwd: dir }
|
|
1996
|
-
);
|
|
1997
|
-
} catch {
|
|
1998
|
-
baseContent = "";
|
|
1999
|
-
}
|
|
2000
|
-
const currentContent = readFileSync4(currentPath, "utf-8");
|
|
2001
|
-
const newContent = readFileSync4(newPath, "utf-8");
|
|
2002
|
-
if (currentContent === baseContent) {
|
|
2003
|
-
writeFileSync3(currentPath, newContent);
|
|
2004
|
-
continue;
|
|
2005
|
-
}
|
|
2006
|
-
if (newContent === baseContent) {
|
|
2007
|
-
continue;
|
|
2008
|
-
}
|
|
2009
|
-
const baseTmp = join(tmpBase, `${file}.base`);
|
|
2010
|
-
const currentTmp = join(tmpBase, `${file}.current`);
|
|
2011
|
-
const newTmp = join(tmpBase, `${file}.new`);
|
|
2012
|
-
mkdirSync3(join(tmpBase, ...file.split("/").slice(0, -1)), { recursive: true });
|
|
2013
|
-
writeFileSync3(baseTmp, baseContent);
|
|
2014
|
-
writeFileSync3(currentTmp, currentContent);
|
|
2015
|
-
writeFileSync3(newTmp, newContent);
|
|
2016
|
-
try {
|
|
2017
|
-
await exec("git", ["merge-file", currentTmp, baseTmp, newTmp]);
|
|
2018
|
-
writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
|
|
2019
|
-
} catch (e) {
|
|
2020
|
-
const exitCode = e && typeof e === "object" && "code" in e ? e.code : null;
|
|
2021
|
-
if (exitCode === 1) {
|
|
2022
|
-
writeFileSync3(currentPath, readFileSync4(currentTmp, "utf-8"));
|
|
2023
|
-
conflictFiles.push(file);
|
|
2024
|
-
} else {
|
|
2025
|
-
throw e;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
} finally {
|
|
2030
|
-
rmSync(tmpBase, { recursive: true, force: true });
|
|
2031
|
-
}
|
|
2032
|
-
if (conflictFiles.length > 0) {
|
|
2033
|
-
return { status: "conflict", conflictFiles };
|
|
2034
|
-
}
|
|
2035
|
-
const upstreamInfo = {
|
|
2036
|
-
source: upstream.source,
|
|
2037
|
-
version: shared.version,
|
|
2038
|
-
baseCommit: upstream.baseCommit
|
|
2039
|
-
// will update after commit
|
|
2040
|
-
};
|
|
2041
|
-
writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
|
|
2042
|
-
`);
|
|
2043
|
-
await gitExec(["add", relSkillPath], { cwd: dir });
|
|
2044
|
-
await gitExec(["commit", "-m", `Update skill: ${skillId} (v${shared.version})`], { cwd: dir });
|
|
2045
|
-
const commitHash = (await gitExec(["rev-parse", "HEAD"], { cwd: dir })).trim();
|
|
2046
|
-
upstreamInfo.baseCommit = commitHash;
|
|
2047
|
-
writeFileSync3(join(skillDir, ".upstream.json"), `${JSON.stringify(upstreamInfo, null, 2)}
|
|
2048
|
-
`);
|
|
2049
|
-
await gitExec(["add", join(relSkillPath, ".upstream.json")], { cwd: dir });
|
|
2050
|
-
await gitExec(["commit", "--amend", "--no-edit"], { cwd: dir });
|
|
2051
|
-
return { status: "updated" };
|
|
2052
|
-
}
|
|
2053
|
-
async function listMindSkills(dir) {
|
|
2054
|
-
const skillsDir = mindSkillsDir(dir);
|
|
2055
|
-
if (!existsSync7(skillsDir)) return [];
|
|
2056
|
-
const entries = readdirSync2(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
2057
|
-
const sharedMap = /* @__PURE__ */ new Map();
|
|
2058
|
-
for (const s of await listSharedSkills()) {
|
|
2059
|
-
sharedMap.set(s.id, s);
|
|
2060
|
-
}
|
|
2061
|
-
const results = [];
|
|
2062
|
-
for (const entry of entries) {
|
|
2063
|
-
const skillDir = join(skillsDir, entry.name);
|
|
2064
|
-
const skillMdPath = join(skillDir, "SKILL.md");
|
|
2065
|
-
let name = entry.name;
|
|
2066
|
-
let description = "";
|
|
2067
|
-
if (existsSync7(skillMdPath)) {
|
|
2068
|
-
const parsed = parseSkillMd(readFileSync4(skillMdPath, "utf-8"));
|
|
2069
|
-
if (parsed.name) name = parsed.name;
|
|
2070
|
-
description = parsed.description;
|
|
2071
|
-
}
|
|
2072
|
-
const upstream = readUpstream(skillDir);
|
|
2073
|
-
let updateAvailable = false;
|
|
2074
|
-
if (upstream) {
|
|
2075
|
-
const shared = sharedMap.get(upstream.source);
|
|
2076
|
-
if (shared && shared.version > upstream.version) {
|
|
2077
|
-
updateAvailable = true;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
results.push({ id: entry.name, name, description, upstream, updateAvailable });
|
|
2081
|
-
}
|
|
2082
|
-
return results;
|
|
2083
|
-
}
|
|
2084
|
-
async function publishSkill(mindName, dir, skillId) {
|
|
2085
|
-
const skillDir = join(mindSkillsDir(dir), skillId);
|
|
2086
|
-
if (!existsSync7(skillDir)) throw new Error(`Skill not found: ${skillId}`);
|
|
2087
|
-
const skillMdPath = join(skillDir, "SKILL.md");
|
|
2088
|
-
if (!existsSync7(skillMdPath)) throw new Error(`SKILL.md not found in ${skillId}`);
|
|
2089
|
-
return importSkillFromDir(skillDir, mindName);
|
|
2090
|
-
}
|
|
2091
|
-
function listFilesRecursive(dir, prefix = "") {
|
|
2092
|
-
const results = [];
|
|
2093
|
-
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2094
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
2095
|
-
if (entry.isDirectory()) {
|
|
2096
|
-
results.push(...listFilesRecursive(join(dir, entry.name), rel));
|
|
2097
|
-
} else {
|
|
2098
|
-
results.push(rel);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
return results;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
// src/web/api/mind-skills.ts
|
|
2105
|
-
var app7 = new Hono7().get("/:name/skills", async (c) => {
|
|
1949
|
+
var app8 = new Hono8().get("/:name/skills", async (c) => {
|
|
2106
1950
|
const name = c.req.param("name");
|
|
2107
1951
|
const entry = findMind(name);
|
|
2108
1952
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
@@ -2177,39 +2021,39 @@ var app7 = new Hono7().get("/:name/skills", async (c) => {
|
|
|
2177
2021
|
}
|
|
2178
2022
|
return c.json({ ok: true });
|
|
2179
2023
|
});
|
|
2180
|
-
var mind_skills_default =
|
|
2024
|
+
var mind_skills_default = app8;
|
|
2181
2025
|
|
|
2182
2026
|
// src/web/api/minds.ts
|
|
2183
2027
|
import {
|
|
2184
2028
|
cpSync as cpSync2,
|
|
2185
|
-
existsSync as
|
|
2186
|
-
mkdirSync as
|
|
2029
|
+
existsSync as existsSync10,
|
|
2030
|
+
mkdirSync as mkdirSync7,
|
|
2187
2031
|
readdirSync as readdirSync4,
|
|
2188
|
-
readFileSync as
|
|
2032
|
+
readFileSync as readFileSync9,
|
|
2189
2033
|
rmSync as rmSync2,
|
|
2190
|
-
statSync,
|
|
2191
|
-
writeFileSync as
|
|
2034
|
+
statSync as statSync2,
|
|
2035
|
+
writeFileSync as writeFileSync8
|
|
2192
2036
|
} from "fs";
|
|
2193
|
-
import { join as join2, resolve as
|
|
2037
|
+
import { join as join2, resolve as resolve13 } from "path";
|
|
2194
2038
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
2195
|
-
import { and as and3, desc as desc2, eq as
|
|
2196
|
-
import { Hono as
|
|
2039
|
+
import { and as and3, desc as desc2, eq as eq4, sql as sql2 } from "drizzle-orm";
|
|
2040
|
+
import { Hono as Hono9 } from "hono";
|
|
2197
2041
|
import { z as z3 } from "zod";
|
|
2198
2042
|
|
|
2199
2043
|
// src/lib/consolidate.ts
|
|
2200
|
-
import { readdirSync as
|
|
2201
|
-
import { resolve as
|
|
2044
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2045
|
+
import { resolve as resolve10 } from "path";
|
|
2202
2046
|
async function consolidateMemory(mindDir2) {
|
|
2203
|
-
const soulPath =
|
|
2204
|
-
const memoryPath =
|
|
2205
|
-
const memoryDir =
|
|
2206
|
-
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");
|
|
2207
2051
|
const logs = [];
|
|
2208
2052
|
try {
|
|
2209
|
-
const files =
|
|
2053
|
+
const files = readdirSync2(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
2210
2054
|
for (const filename of files) {
|
|
2211
2055
|
const date = filename.replace(".md", "");
|
|
2212
|
-
const content2 =
|
|
2056
|
+
const content2 = readFileSync6(resolve10(memoryDir, filename), "utf-8").trim();
|
|
2213
2057
|
if (content2) {
|
|
2214
2058
|
logs.push(`### ${date}
|
|
2215
2059
|
|
|
@@ -2259,7 +2103,7 @@ ${content2}`);
|
|
|
2259
2103
|
const data = await res.json();
|
|
2260
2104
|
const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
|
|
2261
2105
|
if (content) {
|
|
2262
|
-
|
|
2106
|
+
writeFileSync5(memoryPath, `${content}
|
|
2263
2107
|
`);
|
|
2264
2108
|
console.log("MEMORY.md created successfully.");
|
|
2265
2109
|
} else {
|
|
@@ -2269,7 +2113,7 @@ ${content2}`);
|
|
|
2269
2113
|
|
|
2270
2114
|
// src/lib/conversations.ts
|
|
2271
2115
|
import { randomUUID } from "crypto";
|
|
2272
|
-
import { and as and2, desc, eq as
|
|
2116
|
+
import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
|
|
2273
2117
|
|
|
2274
2118
|
// src/lib/conversation-events.ts
|
|
2275
2119
|
var subscribers = /* @__PURE__ */ new Map();
|
|
@@ -2339,7 +2183,7 @@ async function createConversation(mindName, channel, opts) {
|
|
|
2339
2183
|
}
|
|
2340
2184
|
async function getConversation(id) {
|
|
2341
2185
|
const db = await getDb();
|
|
2342
|
-
const row = await db.select().from(conversations).where(
|
|
2186
|
+
const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
|
|
2343
2187
|
return row ?? null;
|
|
2344
2188
|
}
|
|
2345
2189
|
async function addParticipant(conversationId, userId, role = "member") {
|
|
@@ -2354,8 +2198,8 @@ async function removeParticipant(conversationId, userId) {
|
|
|
2354
2198
|
const db = await getDb();
|
|
2355
2199
|
await db.delete(conversationParticipants).where(
|
|
2356
2200
|
and2(
|
|
2357
|
-
|
|
2358
|
-
|
|
2201
|
+
eq3(conversationParticipants.conversation_id, conversationId),
|
|
2202
|
+
eq3(conversationParticipants.user_id, userId)
|
|
2359
2203
|
)
|
|
2360
2204
|
);
|
|
2361
2205
|
}
|
|
@@ -2366,22 +2210,22 @@ async function getParticipants(conversationId) {
|
|
|
2366
2210
|
username: users.username,
|
|
2367
2211
|
userType: users.user_type,
|
|
2368
2212
|
role: conversationParticipants.role
|
|
2369
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
2213
|
+
}).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
|
|
2370
2214
|
return rows;
|
|
2371
2215
|
}
|
|
2372
2216
|
async function isParticipant(conversationId, userId) {
|
|
2373
2217
|
const db = await getDb();
|
|
2374
2218
|
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
2375
2219
|
and2(
|
|
2376
|
-
|
|
2377
|
-
|
|
2220
|
+
eq3(conversationParticipants.conversation_id, conversationId),
|
|
2221
|
+
eq3(conversationParticipants.user_id, userId)
|
|
2378
2222
|
)
|
|
2379
2223
|
).get();
|
|
2380
2224
|
return row != null;
|
|
2381
2225
|
}
|
|
2382
2226
|
async function listConversationsForUser(userId) {
|
|
2383
2227
|
const db = await getDb();
|
|
2384
|
-
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(
|
|
2228
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
|
|
2385
2229
|
if (participantRows.length === 0) return [];
|
|
2386
2230
|
const convIds = participantRows.map((r) => r.conversation_id);
|
|
2387
2231
|
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
@@ -2389,7 +2233,7 @@ async function listConversationsForUser(userId) {
|
|
|
2389
2233
|
async function isParticipantOrOwner(conversationId, userId) {
|
|
2390
2234
|
if (await isParticipant(conversationId, userId)) return true;
|
|
2391
2235
|
const db = await getDb();
|
|
2392
|
-
const row = await db.select().from(conversations).where(and2(
|
|
2236
|
+
const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
|
|
2393
2237
|
return row != null;
|
|
2394
2238
|
}
|
|
2395
2239
|
async function deleteConversationForUser(id, userId) {
|
|
@@ -2401,12 +2245,12 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
2401
2245
|
const db = await getDb();
|
|
2402
2246
|
const serialized = JSON.stringify(content);
|
|
2403
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 });
|
|
2404
|
-
await db.update(conversations).set({ updated_at:
|
|
2248
|
+
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
|
|
2405
2249
|
if (role === "user") {
|
|
2406
2250
|
const firstText = content.find((b) => b.type === "text");
|
|
2407
2251
|
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
2408
2252
|
if (title) {
|
|
2409
|
-
await db.update(conversations).set({ title }).where(and2(
|
|
2253
|
+
await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
|
|
2410
2254
|
}
|
|
2411
2255
|
}
|
|
2412
2256
|
const msg = {
|
|
@@ -2429,7 +2273,7 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
2429
2273
|
}
|
|
2430
2274
|
async function getMessages(conversationId) {
|
|
2431
2275
|
const db = await getDb();
|
|
2432
|
-
const rows = await db.select().from(messages).where(
|
|
2276
|
+
const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
2433
2277
|
return rows.map((row) => {
|
|
2434
2278
|
let content;
|
|
2435
2279
|
try {
|
|
@@ -2452,7 +2296,7 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2452
2296
|
username: users.username,
|
|
2453
2297
|
userType: users.user_type,
|
|
2454
2298
|
role: conversationParticipants.role
|
|
2455
|
-
}).from(conversationParticipants).innerJoin(users,
|
|
2299
|
+
}).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
2456
2300
|
const byConv = /* @__PURE__ */ new Map();
|
|
2457
2301
|
for (const r of rows) {
|
|
2458
2302
|
let arr = byConv.get(r.conversationId);
|
|
@@ -2469,7 +2313,7 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2469
2313
|
}
|
|
2470
2314
|
const lastMsgIds = await db.select({
|
|
2471
2315
|
conversationId: messages.conversation_id,
|
|
2472
|
-
maxId:
|
|
2316
|
+
maxId: sql`MAX(${messages.id})`
|
|
2473
2317
|
}).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
2474
2318
|
const byLastMsg = /* @__PURE__ */ new Map();
|
|
2475
2319
|
if (lastMsgIds.length > 0) {
|
|
@@ -2505,9 +2349,9 @@ async function listConversationsWithParticipants(userId) {
|
|
|
2505
2349
|
}
|
|
2506
2350
|
async function findDMConversation(mindName, participantIds) {
|
|
2507
2351
|
const db = await getDb();
|
|
2508
|
-
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(
|
|
2352
|
+
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq3(conversations.mind_name, mindName), eq3(conversations.type, "dm"))).all();
|
|
2509
2353
|
for (const conv of mindConvs) {
|
|
2510
|
-
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
2354
|
+
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
|
|
2511
2355
|
if (rows.length !== 2) continue;
|
|
2512
2356
|
const ids = new Set(rows.map((r) => r.user_id));
|
|
2513
2357
|
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
@@ -2518,7 +2362,7 @@ async function findDMConversation(mindName, participantIds) {
|
|
|
2518
2362
|
}
|
|
2519
2363
|
async function deleteConversation(id) {
|
|
2520
2364
|
const db = await getDb();
|
|
2521
|
-
await db.delete(conversations).where(
|
|
2365
|
+
await db.delete(conversations).where(eq3(conversations.id, id));
|
|
2522
2366
|
}
|
|
2523
2367
|
async function createChannel(name, creatorId) {
|
|
2524
2368
|
const participantIds = creatorId ? [creatorId] : [];
|
|
@@ -2531,12 +2375,12 @@ async function createChannel(name, creatorId) {
|
|
|
2531
2375
|
}
|
|
2532
2376
|
async function getChannelByName(name) {
|
|
2533
2377
|
const db = await getDb();
|
|
2534
|
-
const row = await db.select().from(conversations).where(and2(
|
|
2378
|
+
const row = await db.select().from(conversations).where(and2(eq3(conversations.name, name), eq3(conversations.type, "channel"))).get();
|
|
2535
2379
|
return row ?? null;
|
|
2536
2380
|
}
|
|
2537
2381
|
async function listChannels() {
|
|
2538
2382
|
const db = await getDb();
|
|
2539
|
-
return await db.select().from(conversations).where(
|
|
2383
|
+
return await db.select().from(conversations).where(eq3(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
2540
2384
|
}
|
|
2541
2385
|
async function joinChannel(conversationId, userId) {
|
|
2542
2386
|
if (await isParticipant(conversationId, userId)) return;
|
|
@@ -2548,11 +2392,11 @@ async function leaveChannel(conversationId, userId) {
|
|
|
2548
2392
|
|
|
2549
2393
|
// src/lib/convert-session.ts
|
|
2550
2394
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2551
|
-
import { mkdirSync as
|
|
2395
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
2552
2396
|
import { homedir } from "os";
|
|
2553
|
-
import { resolve as
|
|
2397
|
+
import { resolve as resolve11 } from "path";
|
|
2554
2398
|
function convertSession(opts) {
|
|
2555
|
-
const lines =
|
|
2399
|
+
const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
|
|
2556
2400
|
const sessionId = randomUUID2();
|
|
2557
2401
|
const idMap = /* @__PURE__ */ new Map();
|
|
2558
2402
|
const messages2 = [];
|
|
@@ -2666,10 +2510,10 @@ function convertSession(opts) {
|
|
|
2666
2510
|
}
|
|
2667
2511
|
}
|
|
2668
2512
|
const projectId = opts.projectDir.replace(/\//g, "-");
|
|
2669
|
-
const sdkDir =
|
|
2670
|
-
|
|
2671
|
-
const sdkPath =
|
|
2672
|
-
|
|
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")}
|
|
2673
2517
|
`);
|
|
2674
2518
|
console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
|
|
2675
2519
|
return sessionId;
|
|
@@ -2748,121 +2592,112 @@ function publish2(mind, event) {
|
|
|
2748
2592
|
}
|
|
2749
2593
|
}
|
|
2750
2594
|
|
|
2751
|
-
// src/lib/
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
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);
|
|
2615
|
+
}
|
|
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);
|
|
2647
|
+
}
|
|
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));
|
|
2777
2658
|
}
|
|
2778
2659
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
this.channels.delete(channel);
|
|
2785
|
-
}
|
|
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));
|
|
2786
2665
|
}
|
|
2787
2666
|
}
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
for (const
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
this.channels.delete(channel);
|
|
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));
|
|
2815
2693
|
}
|
|
2816
2694
|
}
|
|
2817
2695
|
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
function getTypingMap() {
|
|
2821
|
-
if (!instance5) {
|
|
2822
|
-
instance5 = new TypingMap();
|
|
2823
|
-
}
|
|
2824
|
-
return instance5;
|
|
2696
|
+
walk(dir);
|
|
2697
|
+
return results;
|
|
2825
2698
|
}
|
|
2826
2699
|
|
|
2827
2700
|
// src/web/api/minds.ts
|
|
2828
|
-
async function startMindFull(name, baseName, variantName) {
|
|
2829
|
-
await getMindManager().startMind(name);
|
|
2830
|
-
if (variantName) return;
|
|
2831
|
-
if (findMind(baseName)?.stage === "seed") return;
|
|
2832
|
-
const dir = mindDir(baseName);
|
|
2833
|
-
const entry = findMind(baseName);
|
|
2834
|
-
await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
|
|
2835
|
-
getScheduler().loadSchedules(baseName);
|
|
2836
|
-
ensureMailAddress(baseName).catch(
|
|
2837
|
-
(err) => console.error(`[mail] failed to ensure address for ${baseName}:`, err)
|
|
2838
|
-
);
|
|
2839
|
-
const config = readVoluteConfig(dir);
|
|
2840
|
-
if (config?.tokenBudget) {
|
|
2841
|
-
getTokenBudget().setBudget(
|
|
2842
|
-
baseName,
|
|
2843
|
-
config.tokenBudget,
|
|
2844
|
-
config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
|
|
2845
|
-
);
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
function extractTextContent(content) {
|
|
2849
|
-
if (typeof content === "string") return content;
|
|
2850
|
-
if (Array.isArray(content)) {
|
|
2851
|
-
return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
2852
|
-
}
|
|
2853
|
-
return JSON.stringify(content);
|
|
2854
|
-
}
|
|
2855
|
-
function getDaemonPort() {
|
|
2856
|
-
try {
|
|
2857
|
-
const data = JSON.parse(readFileSync7(resolve11(voluteHome(), "daemon.json"), "utf-8"));
|
|
2858
|
-
return data.port;
|
|
2859
|
-
} catch (err) {
|
|
2860
|
-
if (err?.code !== "ENOENT") {
|
|
2861
|
-
console.error("[daemon] failed to read daemon.json:", err);
|
|
2862
|
-
}
|
|
2863
|
-
return void 0;
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
2701
|
async function getMindStatus(name, port) {
|
|
2867
2702
|
const manager = getMindManager();
|
|
2868
2703
|
let status = "stopped";
|
|
@@ -2905,7 +2740,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
|
|
|
2905
2740
|
await gitExec(["commit", "-m", "initial commit"], opts);
|
|
2906
2741
|
}
|
|
2907
2742
|
async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
2908
|
-
const tempWorktree =
|
|
2743
|
+
const tempWorktree = resolve13(projectRoot, ".variants", "_template_update");
|
|
2909
2744
|
let branchExists = false;
|
|
2910
2745
|
try {
|
|
2911
2746
|
await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
|
|
@@ -2916,7 +2751,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2916
2751
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
2917
2752
|
} catch {
|
|
2918
2753
|
}
|
|
2919
|
-
if (
|
|
2754
|
+
if (existsSync10(tempWorktree)) {
|
|
2920
2755
|
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
2921
2756
|
}
|
|
2922
2757
|
const templatesRoot = findTemplatesRoot();
|
|
@@ -2937,8 +2772,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2937
2772
|
});
|
|
2938
2773
|
}
|
|
2939
2774
|
copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
|
|
2940
|
-
const initDir =
|
|
2941
|
-
if (
|
|
2775
|
+
const initDir = resolve13(tempWorktree, ".init");
|
|
2776
|
+
if (existsSync10(initDir)) {
|
|
2942
2777
|
rmSync2(initDir, { recursive: true, force: true });
|
|
2943
2778
|
}
|
|
2944
2779
|
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
@@ -2952,7 +2787,7 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
2952
2787
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
2953
2788
|
} catch {
|
|
2954
2789
|
}
|
|
2955
|
-
if (
|
|
2790
|
+
if (existsSync10(tempWorktree)) {
|
|
2956
2791
|
rmSync2(tempWorktree, { recursive: true, force: true });
|
|
2957
2792
|
}
|
|
2958
2793
|
rmSync2(composedDir, { recursive: true, force: true });
|
|
@@ -2978,20 +2813,125 @@ async function mergeTemplateBranch(worktreeDir) {
|
|
|
2978
2813
|
async function npmInstallAsMind(cwd, mindName) {
|
|
2979
2814
|
if (isIsolationEnabled()) {
|
|
2980
2815
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
2981
|
-
await exec(cmd, args, { cwd, env: { ...process.env, HOME:
|
|
2816
|
+
await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve13(cwd, "home") } });
|
|
2982
2817
|
} else {
|
|
2983
2818
|
await exec("npm", ["install"], { cwd });
|
|
2984
2819
|
}
|
|
2985
2820
|
}
|
|
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
|
+
}
|
|
2986
2925
|
var createMindSchema = z3.object({
|
|
2987
2926
|
name: z3.string(),
|
|
2988
2927
|
template: z3.string().optional(),
|
|
2989
2928
|
stage: z3.enum(["seed", "sprouted"]).optional(),
|
|
2990
2929
|
description: z3.string().optional(),
|
|
2991
2930
|
model: z3.string().optional(),
|
|
2992
|
-
seedSoul: z3.string().optional()
|
|
2931
|
+
seedSoul: z3.string().optional(),
|
|
2932
|
+
skills: z3.array(z3.string()).optional()
|
|
2993
2933
|
});
|
|
2994
|
-
var
|
|
2934
|
+
var app9 = new Hono9().post("/", requireAdmin, zValidator3("json", createMindSchema), async (c) => {
|
|
2995
2935
|
const body = c.req.valid("json");
|
|
2996
2936
|
const { name, template = "claude" } = body;
|
|
2997
2937
|
const nameErr = validateMindName(name);
|
|
@@ -2999,28 +2939,29 @@ var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSch
|
|
|
2999
2939
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
3000
2940
|
ensureVoluteHome();
|
|
3001
2941
|
const dest = mindDir(name);
|
|
3002
|
-
if (
|
|
2942
|
+
if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
3003
2943
|
const templatesRoot = findTemplatesRoot();
|
|
3004
2944
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
3005
2945
|
try {
|
|
3006
2946
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
3007
2947
|
applyInitFiles(dest);
|
|
2948
|
+
const { publicKeyPem } = generateIdentity(dest);
|
|
3008
2949
|
if (body.model) {
|
|
3009
|
-
const configPath =
|
|
3010
|
-
const existing =
|
|
2950
|
+
const configPath = resolve13(dest, "home/.config/config.json");
|
|
2951
|
+
const existing = existsSync10(configPath) ? JSON.parse(readFileSync9(configPath, "utf-8")) : {};
|
|
3011
2952
|
existing.model = body.model;
|
|
3012
|
-
|
|
2953
|
+
writeFileSync8(configPath, `${JSON.stringify(existing, null, 2)}
|
|
3013
2954
|
`);
|
|
3014
2955
|
}
|
|
3015
2956
|
const mindPrompts = await getMindPromptDefaults();
|
|
3016
|
-
|
|
3017
|
-
|
|
2957
|
+
writeFileSync8(
|
|
2958
|
+
resolve13(dest, "home/.config/prompts.json"),
|
|
3018
2959
|
`${JSON.stringify(mindPrompts, null, 2)}
|
|
3019
2960
|
`
|
|
3020
2961
|
);
|
|
3021
2962
|
const port = nextPort();
|
|
3022
|
-
addMind(name, port, body.stage);
|
|
3023
|
-
const homeDir =
|
|
2963
|
+
addMind(name, port, body.stage, template);
|
|
2964
|
+
const homeDir = resolve13(dest, "home");
|
|
3024
2965
|
ensureVoluteGroup();
|
|
3025
2966
|
createMindUser(name, homeDir);
|
|
3026
2967
|
chownMindDir(dest, name);
|
|
@@ -3031,10 +2972,15 @@ var app8 = new Hono8().post("/", requireAdmin, zValidator3("json", createMindSch
|
|
|
3031
2972
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
3032
2973
|
await initTemplateBranch(dest, composedDir, manifest, name, env);
|
|
3033
2974
|
} catch (err) {
|
|
3034
|
-
|
|
3035
|
-
rmSync2(
|
|
2975
|
+
logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
|
|
2976
|
+
rmSync2(resolve13(dest, ".git"), { recursive: true, force: true });
|
|
3036
2977
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
3037
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
|
+
}
|
|
3038
2984
|
chownMindDir(dest, name);
|
|
3039
2985
|
if (body.stage === "seed") {
|
|
3040
2986
|
const descLine = body.description ? `
|
|
@@ -3042,33 +2988,42 @@ The human who planted you described you as: "${body.description}"
|
|
|
3042
2988
|
` : "";
|
|
3043
2989
|
const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
|
|
3044
2990
|
const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
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}`);
|
|
3050
3001
|
}
|
|
3051
3002
|
}
|
|
3052
3003
|
if (body.stage !== "seed") {
|
|
3053
3004
|
const customSoul = await getPromptIfCustom("default_soul");
|
|
3054
3005
|
if (customSoul) {
|
|
3055
|
-
|
|
3006
|
+
writeFileSync8(resolve13(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
|
|
3056
3007
|
}
|
|
3057
3008
|
const customMemory = await getPromptIfCustom("default_memory");
|
|
3058
3009
|
if (customMemory) {
|
|
3059
|
-
|
|
3010
|
+
writeFileSync8(resolve13(dest, "home/MEMORY.md"), customMemory);
|
|
3060
3011
|
}
|
|
3061
3012
|
}
|
|
3013
|
+
publishPublicKey(name, publicKeyPem).catch(
|
|
3014
|
+
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
3015
|
+
);
|
|
3062
3016
|
return c.json({
|
|
3063
3017
|
ok: true,
|
|
3064
3018
|
name,
|
|
3065
3019
|
port,
|
|
3066
3020
|
stage: body.stage ?? "sprouted",
|
|
3067
3021
|
message: `Created mind: ${name} (port ${port})`,
|
|
3068
|
-
...gitWarning && { warning: gitWarning }
|
|
3022
|
+
...gitWarning && { warning: gitWarning },
|
|
3023
|
+
...skillWarnings.length > 0 && { skillWarnings }
|
|
3069
3024
|
});
|
|
3070
3025
|
} catch (err) {
|
|
3071
|
-
if (
|
|
3026
|
+
if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
3072
3027
|
try {
|
|
3073
3028
|
removeMind(name);
|
|
3074
3029
|
} catch {
|
|
@@ -3084,14 +3039,17 @@ The human who planted you described you as: "${body.description}"
|
|
|
3084
3039
|
} catch {
|
|
3085
3040
|
return c.json({ error: "Invalid JSON" }, 400);
|
|
3086
3041
|
}
|
|
3042
|
+
if (body.archivePath && body.manifest) {
|
|
3043
|
+
return importFromArchive(c, body.archivePath, body.name, body.manifest);
|
|
3044
|
+
}
|
|
3087
3045
|
const wsDir = body.workspacePath;
|
|
3088
|
-
if (!wsDir || !
|
|
3046
|
+
if (!wsDir || !existsSync10(resolve13(wsDir, "SOUL.md")) || !existsSync10(resolve13(wsDir, "IDENTITY.md"))) {
|
|
3089
3047
|
return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
|
|
3090
3048
|
}
|
|
3091
|
-
const soul =
|
|
3092
|
-
const identity =
|
|
3093
|
-
const userPath =
|
|
3094
|
-
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") : "";
|
|
3095
3053
|
const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
|
|
3096
3054
|
const template = body.template ?? "claude";
|
|
3097
3055
|
const nameErr = validateMindName(name);
|
|
@@ -3111,38 +3069,39 @@ ${user.trimEnd()}
|
|
|
3111
3069
|
` : "";
|
|
3112
3070
|
ensureVoluteHome();
|
|
3113
3071
|
const dest = mindDir(name);
|
|
3114
|
-
if (
|
|
3072
|
+
if (existsSync10(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
3115
3073
|
const templatesRoot = findTemplatesRoot();
|
|
3116
3074
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
3117
3075
|
try {
|
|
3118
3076
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
3119
3077
|
applyInitFiles(dest);
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
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);
|
|
3123
3082
|
if (hasMemory) {
|
|
3124
|
-
const existingMemory =
|
|
3125
|
-
|
|
3126
|
-
|
|
3083
|
+
const existingMemory = readFileSync9(wsMemoryPath, "utf-8");
|
|
3084
|
+
writeFileSync8(
|
|
3085
|
+
resolve13(dest, "home/MEMORY.md"),
|
|
3127
3086
|
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
3128
3087
|
);
|
|
3129
3088
|
} else if (user) {
|
|
3130
|
-
|
|
3089
|
+
writeFileSync8(resolve13(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
3131
3090
|
`);
|
|
3132
3091
|
}
|
|
3133
|
-
const wsMemoryDir =
|
|
3092
|
+
const wsMemoryDir = resolve13(wsDir, "memory");
|
|
3134
3093
|
let dailyLogCount = 0;
|
|
3135
|
-
if (
|
|
3136
|
-
const destMemoryDir =
|
|
3094
|
+
if (existsSync10(wsMemoryDir)) {
|
|
3095
|
+
const destMemoryDir = resolve13(dest, "home/memory");
|
|
3137
3096
|
const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
3138
3097
|
for (const file of files) {
|
|
3139
|
-
cpSync2(
|
|
3098
|
+
cpSync2(resolve13(wsMemoryDir, file), resolve13(destMemoryDir, file));
|
|
3140
3099
|
}
|
|
3141
3100
|
dailyLogCount = files.length;
|
|
3142
3101
|
}
|
|
3143
3102
|
const port = nextPort();
|
|
3144
|
-
addMind(name, port);
|
|
3145
|
-
const homeDir =
|
|
3103
|
+
addMind(name, port, void 0, template);
|
|
3104
|
+
const homeDir = resolve13(dest, "home");
|
|
3146
3105
|
ensureVoluteGroup();
|
|
3147
3106
|
createMindUser(name, homeDir);
|
|
3148
3107
|
chownMindDir(dest, name);
|
|
@@ -3150,26 +3109,34 @@ ${user.trimEnd()}
|
|
|
3150
3109
|
if (!hasMemory && dailyLogCount > 0) {
|
|
3151
3110
|
await consolidateMemory(dest);
|
|
3152
3111
|
}
|
|
3153
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3112
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve13(dest, "home") } : void 0;
|
|
3154
3113
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
3155
3114
|
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
3156
3115
|
await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
|
|
3157
|
-
const sessionFile = body.sessionPath ?
|
|
3158
|
-
if (sessionFile &&
|
|
3116
|
+
const sessionFile = body.sessionPath ? resolve13(body.sessionPath) : findOpenClawSession(wsDir);
|
|
3117
|
+
if (sessionFile && existsSync10(sessionFile)) {
|
|
3159
3118
|
if (template === "pi") {
|
|
3160
3119
|
importPiSession(sessionFile, dest);
|
|
3161
3120
|
} else if (template === "claude") {
|
|
3162
3121
|
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
3163
|
-
const
|
|
3164
|
-
|
|
3165
|
-
|
|
3122
|
+
const mindRuntimeDir = resolve13(dest, ".mind");
|
|
3123
|
+
mkdirSync7(mindRuntimeDir, { recursive: true });
|
|
3124
|
+
writeFileSync8(resolve13(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
|
|
3166
3125
|
}
|
|
3167
3126
|
}
|
|
3168
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
|
+
}
|
|
3169
3133
|
chownMindDir(dest, name);
|
|
3134
|
+
publishPublicKey(name, importPublicKey).catch(
|
|
3135
|
+
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
3136
|
+
);
|
|
3170
3137
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
3171
3138
|
} catch (err) {
|
|
3172
|
-
if (
|
|
3139
|
+
if (existsSync10(dest)) rmSync2(dest, { recursive: true, force: true });
|
|
3173
3140
|
try {
|
|
3174
3141
|
removeMind(name);
|
|
3175
3142
|
} catch {
|
|
@@ -3185,7 +3152,7 @@ ${user.trimEnd()}
|
|
|
3185
3152
|
const db = await getDb();
|
|
3186
3153
|
const lastActiveRows = await db.select({
|
|
3187
3154
|
mind: mindHistory.mind,
|
|
3188
|
-
lastActiveAt:
|
|
3155
|
+
lastActiveAt: sql2`MAX(${mindHistory.created_at})`
|
|
3189
3156
|
}).from(mindHistory).groupBy(mindHistory.mind);
|
|
3190
3157
|
lastActiveMap = new Map(lastActiveRows.map((r) => [r.mind, r.lastActiveAt]));
|
|
3191
3158
|
} catch {
|
|
@@ -3193,7 +3160,7 @@ ${user.trimEnd()}
|
|
|
3193
3160
|
const minds = await Promise.all(
|
|
3194
3161
|
entries.map(async (entry) => {
|
|
3195
3162
|
const { status, channels } = await getMindStatus(entry.name, entry.port);
|
|
3196
|
-
const hasPages =
|
|
3163
|
+
const hasPages = existsSync10(resolve13(mindDir(entry.name), "home", "pages"));
|
|
3197
3164
|
return {
|
|
3198
3165
|
...entry,
|
|
3199
3166
|
status,
|
|
@@ -3208,8 +3175,8 @@ ${user.trimEnd()}
|
|
|
3208
3175
|
const entries = readRegistry();
|
|
3209
3176
|
const pages = [];
|
|
3210
3177
|
for (const entry of entries) {
|
|
3211
|
-
const pagesDir =
|
|
3212
|
-
if (!
|
|
3178
|
+
const pagesDir = resolve13(mindDir(entry.name), "home", "pages");
|
|
3179
|
+
if (!existsSync10(pagesDir)) continue;
|
|
3213
3180
|
let items;
|
|
3214
3181
|
try {
|
|
3215
3182
|
items = readdirSync4(pagesDir);
|
|
@@ -3218,9 +3185,9 @@ ${user.trimEnd()}
|
|
|
3218
3185
|
continue;
|
|
3219
3186
|
}
|
|
3220
3187
|
for (const item of items) {
|
|
3221
|
-
const fullPath =
|
|
3188
|
+
const fullPath = resolve13(pagesDir, item);
|
|
3222
3189
|
try {
|
|
3223
|
-
const s =
|
|
3190
|
+
const s = statSync2(fullPath);
|
|
3224
3191
|
if (s.isFile()) {
|
|
3225
3192
|
pages.push({
|
|
3226
3193
|
mind: entry.name,
|
|
@@ -3229,9 +3196,9 @@ ${user.trimEnd()}
|
|
|
3229
3196
|
url: `/pages/${entry.name}/${item}`
|
|
3230
3197
|
});
|
|
3231
3198
|
} else if (s.isDirectory()) {
|
|
3232
|
-
const indexPath =
|
|
3233
|
-
if (
|
|
3234
|
-
const indexStat =
|
|
3199
|
+
const indexPath = resolve13(fullPath, "index.html");
|
|
3200
|
+
if (existsSync10(indexPath)) {
|
|
3201
|
+
const indexStat = statSync2(indexPath);
|
|
3235
3202
|
pages.push({
|
|
3236
3203
|
mind: entry.name,
|
|
3237
3204
|
file: join2(item, "index.html"),
|
|
@@ -3255,7 +3222,7 @@ ${user.trimEnd()}
|
|
|
3255
3222
|
const name = c.req.param("name");
|
|
3256
3223
|
const entry = findMind(name);
|
|
3257
3224
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3258
|
-
if (!
|
|
3225
|
+
if (!existsSync10(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
|
|
3259
3226
|
const { status, channels } = await getMindStatus(name, entry.port);
|
|
3260
3227
|
const variants = readVariants(name);
|
|
3261
3228
|
const manager = getMindManager();
|
|
@@ -3270,7 +3237,7 @@ ${user.trimEnd()}
|
|
|
3270
3237
|
return { name: v.name, port: v.port, status: variantStatus };
|
|
3271
3238
|
})
|
|
3272
3239
|
);
|
|
3273
|
-
const hasPages =
|
|
3240
|
+
const hasPages = existsSync10(resolve13(mindDir(name), "home", "pages"));
|
|
3274
3241
|
return c.json({ ...entry, status, channels, variants: variantStatuses, hasPages });
|
|
3275
3242
|
}).post("/:name/start", requireAdmin, async (c) => {
|
|
3276
3243
|
const name = c.req.param("name");
|
|
@@ -3282,13 +3249,13 @@ ${user.trimEnd()}
|
|
|
3282
3249
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
3283
3250
|
} else {
|
|
3284
3251
|
const dir = mindDir(baseName);
|
|
3285
|
-
if (!
|
|
3252
|
+
if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
3286
3253
|
}
|
|
3287
3254
|
if (getMindManager().isRunning(name)) {
|
|
3288
3255
|
return c.json({ error: "Mind already running" }, 409);
|
|
3289
3256
|
}
|
|
3290
3257
|
try {
|
|
3291
|
-
await startMindFull(name
|
|
3258
|
+
await startMindFull(name);
|
|
3292
3259
|
return c.json({ ok: true });
|
|
3293
3260
|
} catch (err) {
|
|
3294
3261
|
return c.json({ error: err instanceof Error ? err.message : "Failed to start mind" }, 500);
|
|
@@ -3303,7 +3270,7 @@ ${user.trimEnd()}
|
|
|
3303
3270
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
3304
3271
|
} else {
|
|
3305
3272
|
const dir = mindDir(baseName);
|
|
3306
|
-
if (!
|
|
3273
|
+
if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
3307
3274
|
}
|
|
3308
3275
|
let context;
|
|
3309
3276
|
const contentType = c.req.header("content-type");
|
|
@@ -3312,17 +3279,13 @@ ${user.trimEnd()}
|
|
|
3312
3279
|
const body = await c.req.json();
|
|
3313
3280
|
if (body?.context) context = body.context;
|
|
3314
3281
|
} catch (err) {
|
|
3315
|
-
|
|
3282
|
+
logger_default.error(`failed to parse restart context for ${name}`, logger_default.errorData(err));
|
|
3316
3283
|
}
|
|
3317
3284
|
}
|
|
3318
3285
|
const manager = getMindManager();
|
|
3319
3286
|
try {
|
|
3320
3287
|
if (manager.isRunning(name)) {
|
|
3321
|
-
|
|
3322
|
-
await getConnectorManager().stopConnectors(baseName);
|
|
3323
|
-
getTokenBudget().removeBudget(baseName);
|
|
3324
|
-
}
|
|
3325
|
-
await manager.stopMind(name);
|
|
3288
|
+
await stopMindFull(name);
|
|
3326
3289
|
}
|
|
3327
3290
|
if (context?.type === "merge" && context.name && !variantName) {
|
|
3328
3291
|
const mergeVariantName = String(context.name);
|
|
@@ -3330,11 +3293,11 @@ ${user.trimEnd()}
|
|
|
3330
3293
|
if (branchErr) {
|
|
3331
3294
|
return c.json({ error: `Invalid variant name: ${branchErr}` }, 400);
|
|
3332
3295
|
}
|
|
3333
|
-
|
|
3296
|
+
logger_default.error(`merging variant for ${baseName}: ${mergeVariantName}`);
|
|
3334
3297
|
const variant = findVariant(baseName, mergeVariantName);
|
|
3335
3298
|
if (variant) {
|
|
3336
3299
|
const projectRoot = mindDir(baseName);
|
|
3337
|
-
if (
|
|
3300
|
+
if (existsSync10(variant.path)) {
|
|
3338
3301
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
3339
3302
|
if (status) {
|
|
3340
3303
|
try {
|
|
@@ -3343,9 +3306,9 @@ ${user.trimEnd()}
|
|
|
3343
3306
|
cwd: variant.path
|
|
3344
3307
|
});
|
|
3345
3308
|
} catch (e) {
|
|
3346
|
-
|
|
3347
|
-
`
|
|
3348
|
-
e
|
|
3309
|
+
logger_default.error(
|
|
3310
|
+
`failed to auto-commit variant worktree for ${baseName}`,
|
|
3311
|
+
logger_default.errorData(e)
|
|
3349
3312
|
);
|
|
3350
3313
|
}
|
|
3351
3314
|
}
|
|
@@ -3358,11 +3321,11 @@ ${user.trimEnd()}
|
|
|
3358
3321
|
cwd: projectRoot
|
|
3359
3322
|
});
|
|
3360
3323
|
} catch (e) {
|
|
3361
|
-
|
|
3324
|
+
logger_default.error(`failed to auto-commit main worktree for ${baseName}`, logger_default.errorData(e));
|
|
3362
3325
|
}
|
|
3363
3326
|
}
|
|
3364
3327
|
await gitExec(["merge", variant.branch], { cwd: projectRoot });
|
|
3365
|
-
if (
|
|
3328
|
+
if (existsSync10(variant.path)) {
|
|
3366
3329
|
try {
|
|
3367
3330
|
await gitExec(["worktree", "remove", "--force", variant.path], {
|
|
3368
3331
|
cwd: projectRoot
|
|
@@ -3379,7 +3342,7 @@ ${user.trimEnd()}
|
|
|
3379
3342
|
try {
|
|
3380
3343
|
await npmInstallAsMind(projectRoot, baseName);
|
|
3381
3344
|
} catch (e) {
|
|
3382
|
-
|
|
3345
|
+
logger_default.error(`npm install failed after merge for ${baseName}`, logger_default.errorData(e));
|
|
3383
3346
|
}
|
|
3384
3347
|
}
|
|
3385
3348
|
}
|
|
@@ -3389,17 +3352,17 @@ ${user.trimEnd()}
|
|
|
3389
3352
|
if (context?.type === "sprouted" && !variantName) {
|
|
3390
3353
|
try {
|
|
3391
3354
|
const db = await getDb();
|
|
3392
|
-
const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(
|
|
3355
|
+
const activeConvs = await db.select({ id: conversations.id }).from(conversations).where(eq4(conversations.mind_name, baseName)).all();
|
|
3393
3356
|
for (const conv of activeConvs) {
|
|
3394
3357
|
await addMessage(conv.id, "assistant", "system", [
|
|
3395
3358
|
{ type: "text", text: "[seed has sprouted]" }
|
|
3396
3359
|
]);
|
|
3397
3360
|
}
|
|
3398
3361
|
} catch (err) {
|
|
3399
|
-
|
|
3362
|
+
logger_default.error(`failed to inject sprouted message for ${baseName}`, logger_default.errorData(err));
|
|
3400
3363
|
}
|
|
3401
3364
|
}
|
|
3402
|
-
await startMindFull(name
|
|
3365
|
+
await startMindFull(name);
|
|
3403
3366
|
return c.json({ ok: true });
|
|
3404
3367
|
} catch (err) {
|
|
3405
3368
|
return c.json({ error: err instanceof Error ? err.message : "Failed to restart mind" }, 500);
|
|
@@ -3418,12 +3381,7 @@ ${user.trimEnd()}
|
|
|
3418
3381
|
return c.json({ error: "Mind is not running" }, 409);
|
|
3419
3382
|
}
|
|
3420
3383
|
try {
|
|
3421
|
-
|
|
3422
|
-
await getConnectorManager().stopConnectors(baseName);
|
|
3423
|
-
getScheduler().unloadSchedules(baseName);
|
|
3424
|
-
getTokenBudget().removeBudget(baseName);
|
|
3425
|
-
}
|
|
3426
|
-
await manager.stopMind(name);
|
|
3384
|
+
await stopMindFull(name);
|
|
3427
3385
|
return c.json({ ok: true });
|
|
3428
3386
|
} catch (err) {
|
|
3429
3387
|
return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
|
|
@@ -3445,18 +3403,21 @@ ${user.trimEnd()}
|
|
|
3445
3403
|
const force = c.req.query("force") === "true";
|
|
3446
3404
|
const manager = getMindManager();
|
|
3447
3405
|
if (manager.isRunning(name)) {
|
|
3448
|
-
await
|
|
3449
|
-
getTokenBudget().removeBudget(name);
|
|
3450
|
-
await manager.stopMind(name);
|
|
3406
|
+
await stopMindFull(name);
|
|
3451
3407
|
}
|
|
3452
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
|
+
}
|
|
3453
3414
|
removeMind(name);
|
|
3454
3415
|
await deleteMindUser2(name);
|
|
3455
3416
|
const state = stateDir(name);
|
|
3456
|
-
if (
|
|
3417
|
+
if (existsSync10(state)) {
|
|
3457
3418
|
rmSync2(state, { recursive: true, force: true });
|
|
3458
3419
|
}
|
|
3459
|
-
if (force &&
|
|
3420
|
+
if (force && existsSync10(dir)) {
|
|
3460
3421
|
rmSync2(dir, { recursive: true, force: true });
|
|
3461
3422
|
deleteMindUser(name);
|
|
3462
3423
|
}
|
|
@@ -3466,17 +3427,17 @@ ${user.trimEnd()}
|
|
|
3466
3427
|
const entry = findMind(mindName);
|
|
3467
3428
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3468
3429
|
const dir = mindDir(mindName);
|
|
3469
|
-
if (!
|
|
3430
|
+
if (!existsSync10(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
3470
3431
|
let body = {};
|
|
3471
3432
|
try {
|
|
3472
3433
|
body = await c.req.json();
|
|
3473
3434
|
} catch {
|
|
3474
3435
|
}
|
|
3475
|
-
const template = body.template ?? "claude";
|
|
3436
|
+
const template = body.template ?? entry.template ?? "claude";
|
|
3476
3437
|
const UPGRADE_VARIANT = "upgrade";
|
|
3477
3438
|
if (body.continue) {
|
|
3478
|
-
const worktreeDir2 =
|
|
3479
|
-
if (!
|
|
3439
|
+
const worktreeDir2 = resolve13(dir, ".variants", UPGRADE_VARIANT);
|
|
3440
|
+
if (!existsSync10(worktreeDir2)) {
|
|
3480
3441
|
return c.json({ error: "No upgrade in progress" }, 400);
|
|
3481
3442
|
}
|
|
3482
3443
|
const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
|
|
@@ -3525,9 +3486,9 @@ ${user.trimEnd()}
|
|
|
3525
3486
|
try {
|
|
3526
3487
|
chownMindDir(dir, mindName);
|
|
3527
3488
|
} catch (chownErr) {
|
|
3528
|
-
|
|
3529
|
-
`
|
|
3530
|
-
chownErr
|
|
3489
|
+
logger_default.error(
|
|
3490
|
+
`failed to fix ownership during upgrade cleanup for ${mindName}`,
|
|
3491
|
+
logger_default.errorData(chownErr)
|
|
3531
3492
|
);
|
|
3532
3493
|
}
|
|
3533
3494
|
return c.json(
|
|
@@ -3536,8 +3497,8 @@ ${user.trimEnd()}
|
|
|
3536
3497
|
);
|
|
3537
3498
|
}
|
|
3538
3499
|
}
|
|
3539
|
-
const worktreeDir =
|
|
3540
|
-
if (
|
|
3500
|
+
const worktreeDir = resolve13(dir, ".variants", UPGRADE_VARIANT);
|
|
3501
|
+
if (existsSync10(worktreeDir)) {
|
|
3541
3502
|
return c.json(
|
|
3542
3503
|
{ error: "Upgrade variant already exists. Use continue or delete it first." },
|
|
3543
3504
|
409
|
|
@@ -3548,10 +3509,20 @@ ${user.trimEnd()}
|
|
|
3548
3509
|
await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
3549
3510
|
} catch {
|
|
3550
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
|
+
}
|
|
3551
3522
|
await updateTemplateBranch(dir, template, mindName);
|
|
3552
|
-
const parentDir =
|
|
3553
|
-
if (!
|
|
3554
|
-
|
|
3523
|
+
const parentDir = resolve13(dir, ".variants");
|
|
3524
|
+
if (!existsSync10(parentDir)) {
|
|
3525
|
+
mkdirSync7(parentDir, { recursive: true });
|
|
3555
3526
|
}
|
|
3556
3527
|
await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
3557
3528
|
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
@@ -3597,9 +3568,9 @@ ${user.trimEnd()}
|
|
|
3597
3568
|
try {
|
|
3598
3569
|
chownMindDir(dir, mindName);
|
|
3599
3570
|
} catch (chownErr) {
|
|
3600
|
-
|
|
3601
|
-
`
|
|
3602
|
-
chownErr
|
|
3571
|
+
logger_default.error(
|
|
3572
|
+
`failed to fix ownership during upgrade cleanup for ${mindName}`,
|
|
3573
|
+
logger_default.errorData(chownErr)
|
|
3603
3574
|
);
|
|
3604
3575
|
}
|
|
3605
3576
|
return c.json(
|
|
@@ -3612,11 +3583,9 @@ ${user.trimEnd()}
|
|
|
3612
3583
|
const [baseName, variantName] = name.split("@", 2);
|
|
3613
3584
|
const entry = findMind(baseName);
|
|
3614
3585
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3615
|
-
let port = entry.port;
|
|
3616
3586
|
if (variantName) {
|
|
3617
3587
|
const variant = findVariant(baseName, variantName);
|
|
3618
3588
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
3619
|
-
port = variant.port;
|
|
3620
3589
|
}
|
|
3621
3590
|
if (!getMindManager().isRunning(name)) {
|
|
3622
3591
|
return c.json({ error: "Mind is not running" }, 409);
|
|
@@ -3626,7 +3595,7 @@ ${user.trimEnd()}
|
|
|
3626
3595
|
try {
|
|
3627
3596
|
parsed = JSON.parse(body);
|
|
3628
3597
|
} catch (err) {
|
|
3629
|
-
|
|
3598
|
+
logger_default.error(`failed to parse message body for ${baseName}`, logger_default.errorData(err));
|
|
3630
3599
|
}
|
|
3631
3600
|
const channel = parsed?.channel ?? "unknown";
|
|
3632
3601
|
const db = await getDb();
|
|
@@ -3642,7 +3611,7 @@ ${user.trimEnd()}
|
|
|
3642
3611
|
content
|
|
3643
3612
|
});
|
|
3644
3613
|
} catch (err) {
|
|
3645
|
-
|
|
3614
|
+
logger_default.error(`failed to persist inbound message for ${baseName}`, logger_default.errorData(err));
|
|
3646
3615
|
}
|
|
3647
3616
|
}
|
|
3648
3617
|
const budget = getTokenBudget();
|
|
@@ -3656,16 +3625,31 @@ ${user.trimEnd()}
|
|
|
3656
3625
|
});
|
|
3657
3626
|
return c.json({ error: "Token budget exceeded \u2014 message queued for next period" }, 429);
|
|
3658
3627
|
}
|
|
3628
|
+
if (!parsed) return c.json({ error: "Invalid JSON" }, 400);
|
|
3659
3629
|
const typingMap = getTypingMap();
|
|
3660
|
-
const sender = parsed
|
|
3630
|
+
const sender = parsed.sender ?? "";
|
|
3661
3631
|
if (sender) typingMap.delete(channel, sender);
|
|
3662
3632
|
const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
|
|
3663
|
-
|
|
3664
|
-
if (parsed && currentlyTyping.length > 0) {
|
|
3633
|
+
if (currentlyTyping.length > 0) {
|
|
3665
3634
|
parsed.typing = currentlyTyping;
|
|
3666
|
-
forwardBody = JSON.stringify(parsed);
|
|
3667
3635
|
}
|
|
3668
|
-
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") {
|
|
3669
3653
|
const usage = budget.getUsage(baseName);
|
|
3670
3654
|
const pct = usage?.percentUsed ?? 80;
|
|
3671
3655
|
const warningText = `
|
|
@@ -3676,12 +3660,11 @@ ${user.trimEnd()}
|
|
|
3676
3660
|
parsed.content = [...parsed.content, { type: "text", text: warningText }];
|
|
3677
3661
|
}
|
|
3678
3662
|
budget.acknowledgeWarning(baseName);
|
|
3679
|
-
forwardBody = JSON.stringify(parsed);
|
|
3680
3663
|
}
|
|
3681
3664
|
const seedEntry = findMind(baseName);
|
|
3682
|
-
if (seedEntry?.stage === "seed"
|
|
3665
|
+
if (seedEntry?.stage === "seed") {
|
|
3683
3666
|
try {
|
|
3684
|
-
const countResult = await db.select({ count:
|
|
3667
|
+
const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
|
|
3685
3668
|
const msgCount = countResult[0]?.count ?? 0;
|
|
3686
3669
|
if (msgCount >= 10 && msgCount % 10 === 0) {
|
|
3687
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.]";
|
|
@@ -3690,27 +3673,29 @@ ${user.trimEnd()}
|
|
|
3690
3673
|
} else if (Array.isArray(parsed.content)) {
|
|
3691
3674
|
parsed.content = [...parsed.content, { type: "text", text: nudge }];
|
|
3692
3675
|
}
|
|
3693
|
-
forwardBody = JSON.stringify(parsed);
|
|
3694
3676
|
}
|
|
3695
3677
|
} catch (err) {
|
|
3696
|
-
|
|
3697
|
-
}
|
|
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;
|
|
3698
3696
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
|
|
3702
|
-
fetch(`http://127.0.0.1:${port}/message`, {
|
|
3703
|
-
method: "POST",
|
|
3704
|
-
headers: { "Content-Type": "application/json" },
|
|
3705
|
-
body: forwardBody
|
|
3706
|
-
}).then(async (res) => {
|
|
3707
|
-
if (!res.ok) {
|
|
3708
|
-
const text = await res.text().catch(() => "");
|
|
3709
|
-
console.error(`[daemon] mind ${name} responded with ${res.status}: ${text}`);
|
|
3710
|
-
}
|
|
3711
|
-
}).catch((err) => {
|
|
3712
|
-
console.error(`[daemon] mind ${name} unreachable on port ${port}:`, err);
|
|
3713
|
-
typingMap.delete(channel, baseName);
|
|
3697
|
+
getDeliveryManager().routeAndDeliver(name, deliveryPayload).catch((err) => {
|
|
3698
|
+
logger_default.error(`delivery failed for ${name}`, logger_default.errorData(err));
|
|
3714
3699
|
});
|
|
3715
3700
|
return c.json({ ok: true });
|
|
3716
3701
|
}).get("/:name/budget", async (c) => {
|
|
@@ -3719,6 +3704,19 @@ ${user.trimEnd()}
|
|
|
3719
3704
|
const usage = getTokenBudget().getUsage(baseName);
|
|
3720
3705
|
if (!usage) return c.json({ error: "No budget configured" }, 404);
|
|
3721
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
|
+
}
|
|
3722
3720
|
}).post("/:name/events", async (c) => {
|
|
3723
3721
|
const name = c.req.param("name");
|
|
3724
3722
|
const [baseName] = name.split("@", 2);
|
|
@@ -3743,7 +3741,7 @@ ${user.trimEnd()}
|
|
|
3743
3741
|
metadata: body.metadata ? JSON.stringify(body.metadata) : null
|
|
3744
3742
|
});
|
|
3745
3743
|
} catch (err) {
|
|
3746
|
-
|
|
3744
|
+
logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
|
|
3747
3745
|
}
|
|
3748
3746
|
publish2(baseName, {
|
|
3749
3747
|
mind: baseName,
|
|
@@ -3754,12 +3752,22 @@ ${user.trimEnd()}
|
|
|
3754
3752
|
content: body.content,
|
|
3755
3753
|
metadata: body.metadata
|
|
3756
3754
|
});
|
|
3755
|
+
if ((body.type === "text" || body.type === "outbound") && body.channel) {
|
|
3756
|
+
getTypingMap().delete(body.channel, baseName);
|
|
3757
|
+
}
|
|
3757
3758
|
if (body.type === "done") {
|
|
3758
3759
|
if (body.channel) {
|
|
3759
3760
|
getTypingMap().delete(body.channel, baseName);
|
|
3760
3761
|
} else {
|
|
3761
3762
|
getTypingMap().deleteSender(baseName);
|
|
3762
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
|
+
}
|
|
3763
3771
|
}
|
|
3764
3772
|
if (body.type === "usage" && body.metadata) {
|
|
3765
3773
|
const inputTokens = body.metadata.input_tokens ?? 0;
|
|
@@ -3827,7 +3835,7 @@ ${user.trimEnd()}
|
|
|
3827
3835
|
content: body.content
|
|
3828
3836
|
});
|
|
3829
3837
|
} catch (err) {
|
|
3830
|
-
|
|
3838
|
+
logger_default.error(`failed to persist external send for ${baseName}`, logger_default.errorData(err));
|
|
3831
3839
|
return c.json({ error: "Failed to persist" }, 500);
|
|
3832
3840
|
}
|
|
3833
3841
|
return c.json({ ok: true });
|
|
@@ -3836,16 +3844,16 @@ ${user.trimEnd()}
|
|
|
3836
3844
|
const db = await getDb();
|
|
3837
3845
|
const rows = await db.select({
|
|
3838
3846
|
session: mindHistory.session,
|
|
3839
|
-
started_at:
|
|
3840
|
-
event_count:
|
|
3841
|
-
message_count:
|
|
3842
|
-
tool_count:
|
|
3843
|
-
}).from(mindHistory).where(and3(
|
|
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`);
|
|
3844
3852
|
return c.json(rows);
|
|
3845
3853
|
}).get("/:name/history/channels", async (c) => {
|
|
3846
3854
|
const name = c.req.param("name");
|
|
3847
3855
|
const db = await getDb();
|
|
3848
|
-
const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(
|
|
3856
|
+
const rows = await db.selectDistinct({ channel: mindHistory.channel }).from(mindHistory).where(eq4(mindHistory.mind, name));
|
|
3849
3857
|
return c.json(rows.map((r) => r.channel));
|
|
3850
3858
|
}).get("/:name/history", async (c) => {
|
|
3851
3859
|
const name = c.req.param("name");
|
|
@@ -3855,25 +3863,25 @@ ${user.trimEnd()}
|
|
|
3855
3863
|
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
3856
3864
|
const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
|
|
3857
3865
|
const db = await getDb();
|
|
3858
|
-
const conditions = [
|
|
3866
|
+
const conditions = [eq4(mindHistory.mind, name)];
|
|
3859
3867
|
if (channel) {
|
|
3860
|
-
conditions.push(
|
|
3868
|
+
conditions.push(eq4(mindHistory.channel, channel));
|
|
3861
3869
|
}
|
|
3862
3870
|
if (session) {
|
|
3863
|
-
conditions.push(
|
|
3871
|
+
conditions.push(eq4(mindHistory.session, session));
|
|
3864
3872
|
}
|
|
3865
3873
|
if (!full) {
|
|
3866
|
-
conditions.push(
|
|
3874
|
+
conditions.push(sql2`${mindHistory.type} IN ('inbound', 'outbound')`);
|
|
3867
3875
|
}
|
|
3868
3876
|
const rows = await db.select().from(mindHistory).where(and3(...conditions)).orderBy(desc2(mindHistory.created_at)).limit(limit).offset(offset);
|
|
3869
3877
|
return c.json(rows);
|
|
3870
3878
|
});
|
|
3871
|
-
var minds_default =
|
|
3879
|
+
var minds_default = app9;
|
|
3872
3880
|
|
|
3873
3881
|
// src/web/api/pages.ts
|
|
3874
3882
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
3875
|
-
import { extname, resolve as
|
|
3876
|
-
import { Hono as
|
|
3883
|
+
import { extname, resolve as resolve14 } from "path";
|
|
3884
|
+
import { Hono as Hono10 } from "hono";
|
|
3877
3885
|
var MIME_TYPES = {
|
|
3878
3886
|
".html": "text/html",
|
|
3879
3887
|
".js": "application/javascript",
|
|
@@ -3890,16 +3898,16 @@ var MIME_TYPES = {
|
|
|
3890
3898
|
".txt": "text/plain",
|
|
3891
3899
|
".xml": "application/xml"
|
|
3892
3900
|
};
|
|
3893
|
-
var
|
|
3901
|
+
var app10 = new Hono10().get("/:name/*", async (c) => {
|
|
3894
3902
|
const name = c.req.param("name");
|
|
3895
3903
|
if (!findMind(name)) return c.text("Not found", 404);
|
|
3896
|
-
const pagesRoot =
|
|
3904
|
+
const pagesRoot = resolve14(mindDir(name), "home", "pages");
|
|
3897
3905
|
const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
|
|
3898
|
-
const requestedPath =
|
|
3906
|
+
const requestedPath = resolve14(pagesRoot, wildcard.slice(1));
|
|
3899
3907
|
if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
|
|
3900
3908
|
let fileStat = await stat(requestedPath).catch(() => null);
|
|
3901
3909
|
if (fileStat?.isDirectory()) {
|
|
3902
|
-
const indexPath =
|
|
3910
|
+
const indexPath = resolve14(requestedPath, "index.html");
|
|
3903
3911
|
fileStat = await stat(indexPath).catch(() => null);
|
|
3904
3912
|
if (fileStat?.isFile()) {
|
|
3905
3913
|
const body = await readFile2(indexPath);
|
|
@@ -3915,14 +3923,14 @@ var app9 = new Hono9().get("/:name/*", async (c) => {
|
|
|
3915
3923
|
}
|
|
3916
3924
|
return c.text("Not found", 404);
|
|
3917
3925
|
});
|
|
3918
|
-
var pages_default =
|
|
3926
|
+
var pages_default = app10;
|
|
3919
3927
|
|
|
3920
3928
|
// src/web/api/prompts.ts
|
|
3921
3929
|
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
3922
|
-
import { eq as
|
|
3923
|
-
import { Hono as
|
|
3930
|
+
import { eq as eq5, sql as sql3 } from "drizzle-orm";
|
|
3931
|
+
import { Hono as Hono11 } from "hono";
|
|
3924
3932
|
import { z as z4 } from "zod";
|
|
3925
|
-
var
|
|
3933
|
+
var app11 = new Hono11().get("/", async (c) => {
|
|
3926
3934
|
let rows;
|
|
3927
3935
|
try {
|
|
3928
3936
|
const db = await getDb();
|
|
@@ -3952,9 +3960,9 @@ var app10 = new Hono10().get("/", async (c) => {
|
|
|
3952
3960
|
}
|
|
3953
3961
|
const { content } = c.req.valid("json");
|
|
3954
3962
|
const db = await getDb();
|
|
3955
|
-
await db.insert(systemPrompts).values({ key, content, updated_at:
|
|
3963
|
+
await db.insert(systemPrompts).values({ key, content, updated_at: sql3`(datetime('now'))` }).onConflictDoUpdate({
|
|
3956
3964
|
target: systemPrompts.key,
|
|
3957
|
-
set: { content, updated_at:
|
|
3965
|
+
set: { content, updated_at: sql3`(datetime('now'))` }
|
|
3958
3966
|
});
|
|
3959
3967
|
return c.json({ ok: true });
|
|
3960
3968
|
}).delete("/:key", requireAdmin, async (c) => {
|
|
@@ -3963,13 +3971,13 @@ var app10 = new Hono10().get("/", async (c) => {
|
|
|
3963
3971
|
return c.json({ error: "Unknown prompt key" }, 404);
|
|
3964
3972
|
}
|
|
3965
3973
|
const db = await getDb();
|
|
3966
|
-
await db.delete(systemPrompts).where(
|
|
3974
|
+
await db.delete(systemPrompts).where(eq5(systemPrompts.key, key));
|
|
3967
3975
|
return c.json({ ok: true });
|
|
3968
3976
|
});
|
|
3969
|
-
var prompts_default =
|
|
3977
|
+
var prompts_default = app11;
|
|
3970
3978
|
|
|
3971
3979
|
// src/web/api/schedules.ts
|
|
3972
|
-
import { Hono as
|
|
3980
|
+
import { Hono as Hono12 } from "hono";
|
|
3973
3981
|
function readSchedules(name) {
|
|
3974
3982
|
return readVoluteConfig(mindDir(name))?.schedules ?? [];
|
|
3975
3983
|
}
|
|
@@ -3980,7 +3988,7 @@ function writeSchedules(name, schedules) {
|
|
|
3980
3988
|
writeVoluteConfig(dir, config);
|
|
3981
3989
|
getScheduler().loadSchedules(name);
|
|
3982
3990
|
}
|
|
3983
|
-
var
|
|
3991
|
+
var app12 = new Hono12().get("/:name/schedules", (c) => {
|
|
3984
3992
|
const name = c.req.param("name");
|
|
3985
3993
|
if (!findMind(name)) return c.json({ error: "Mind not found" }, 404);
|
|
3986
3994
|
return c.json(readSchedules(name));
|
|
@@ -4051,15 +4059,68 @@ var app11 = new Hono11().get("/:name/schedules", (c) => {
|
|
|
4051
4059
|
return c.json({ error: "Failed to reach mind" }, 502);
|
|
4052
4060
|
}
|
|
4053
4061
|
});
|
|
4054
|
-
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;
|
|
4055
4116
|
|
|
4056
4117
|
// src/web/api/skills.ts
|
|
4057
|
-
import { existsSync as
|
|
4118
|
+
import { existsSync as existsSync11, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync3 } from "fs";
|
|
4058
4119
|
import { tmpdir as tmpdir2 } from "os";
|
|
4059
|
-
import { join as join3, resolve as
|
|
4120
|
+
import { join as join3, resolve as resolve15 } from "path";
|
|
4060
4121
|
import AdmZip from "adm-zip";
|
|
4061
|
-
import { Hono as
|
|
4062
|
-
var
|
|
4122
|
+
import { Hono as Hono14 } from "hono";
|
|
4123
|
+
var app14 = new Hono14().get("/", async (c) => {
|
|
4063
4124
|
const skills = await listSharedSkills();
|
|
4064
4125
|
return c.json(skills);
|
|
4065
4126
|
}).get("/:id", async (c) => {
|
|
@@ -4083,19 +4144,19 @@ var app12 = new Hono12().get("/", async (c) => {
|
|
|
4083
4144
|
try {
|
|
4084
4145
|
const zip = new AdmZip(buffer);
|
|
4085
4146
|
for (const entry of zip.getEntries()) {
|
|
4086
|
-
const target =
|
|
4147
|
+
const target = resolve15(tmpDir, entry.entryName);
|
|
4087
4148
|
if (!target.startsWith(tmpDir)) {
|
|
4088
4149
|
return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
|
|
4089
4150
|
}
|
|
4090
4151
|
}
|
|
4091
4152
|
zip.extractAllTo(tmpDir, true);
|
|
4092
4153
|
let skillDir = null;
|
|
4093
|
-
if (
|
|
4154
|
+
if (existsSync11(join3(tmpDir, "SKILL.md"))) {
|
|
4094
4155
|
skillDir = tmpDir;
|
|
4095
4156
|
} else {
|
|
4096
4157
|
const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
4097
4158
|
for (const entry of entries) {
|
|
4098
|
-
if (
|
|
4159
|
+
if (existsSync11(join3(tmpDir, entry.name, "SKILL.md"))) {
|
|
4099
4160
|
skillDir = join3(tmpDir, entry.name);
|
|
4100
4161
|
break;
|
|
4101
4162
|
}
|
|
@@ -4124,12 +4185,12 @@ var app12 = new Hono12().get("/", async (c) => {
|
|
|
4124
4185
|
}
|
|
4125
4186
|
return c.json({ ok: true });
|
|
4126
4187
|
});
|
|
4127
|
-
var skills_default =
|
|
4188
|
+
var skills_default = app14;
|
|
4128
4189
|
|
|
4129
4190
|
// src/web/api/system.ts
|
|
4130
|
-
import { Hono as
|
|
4191
|
+
import { Hono as Hono15 } from "hono";
|
|
4131
4192
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4132
|
-
var
|
|
4193
|
+
var app15 = new Hono15().post("/restart", requireAdmin, (c) => {
|
|
4133
4194
|
setTimeout(() => process.exit(1), 200);
|
|
4134
4195
|
return c.json({ ok: true });
|
|
4135
4196
|
}).post("/stop", requireAdmin, (c) => {
|
|
@@ -4146,10 +4207,10 @@ var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
|
|
|
4146
4207
|
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
4147
4208
|
});
|
|
4148
4209
|
});
|
|
4149
|
-
await new Promise((
|
|
4210
|
+
await new Promise((resolve20) => {
|
|
4150
4211
|
stream.onAbort(() => {
|
|
4151
4212
|
unsubscribe();
|
|
4152
|
-
|
|
4213
|
+
resolve20();
|
|
4153
4214
|
});
|
|
4154
4215
|
});
|
|
4155
4216
|
});
|
|
@@ -4157,18 +4218,18 @@ var app13 = new Hono13().post("/restart", requireAdmin, (c) => {
|
|
|
4157
4218
|
const config = readSystemsConfig();
|
|
4158
4219
|
return c.json({ system: config?.system ?? null });
|
|
4159
4220
|
});
|
|
4160
|
-
var system_default =
|
|
4221
|
+
var system_default = app15;
|
|
4161
4222
|
|
|
4162
4223
|
// src/web/api/typing.ts
|
|
4163
4224
|
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
4164
|
-
import { Hono as
|
|
4225
|
+
import { Hono as Hono16 } from "hono";
|
|
4165
4226
|
import { z as z5 } from "zod";
|
|
4166
4227
|
var typingSchema = z5.object({
|
|
4167
4228
|
channel: z5.string().min(1),
|
|
4168
4229
|
sender: z5.string().min(1),
|
|
4169
4230
|
active: z5.boolean()
|
|
4170
4231
|
});
|
|
4171
|
-
var
|
|
4232
|
+
var app16 = new Hono16().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
|
|
4172
4233
|
const { channel, sender, active } = c.req.valid("json");
|
|
4173
4234
|
const map = getTypingMap();
|
|
4174
4235
|
if (active) {
|
|
@@ -4185,13 +4246,13 @@ var app14 = new Hono14().post("/:name/typing", zValidator5("json", typingSchema)
|
|
|
4185
4246
|
const map = getTypingMap();
|
|
4186
4247
|
return c.json({ typing: map.get(channel) });
|
|
4187
4248
|
});
|
|
4188
|
-
var typing_default =
|
|
4249
|
+
var typing_default = app16;
|
|
4189
4250
|
|
|
4190
4251
|
// src/web/api/update.ts
|
|
4191
4252
|
import { spawn as spawn3 } from "child_process";
|
|
4192
|
-
import { Hono as
|
|
4253
|
+
import { Hono as Hono17 } from "hono";
|
|
4193
4254
|
var bin;
|
|
4194
|
-
var
|
|
4255
|
+
var app17 = new Hono17().get("/update", async (c) => {
|
|
4195
4256
|
const result = await checkForUpdate();
|
|
4196
4257
|
return c.json(result);
|
|
4197
4258
|
}).post("/update", requireAdmin, async (c) => {
|
|
@@ -4206,19 +4267,19 @@ var app15 = new Hono15().get("/update", async (c) => {
|
|
|
4206
4267
|
child.unref();
|
|
4207
4268
|
return c.json({ ok: true, message: "Updating..." });
|
|
4208
4269
|
});
|
|
4209
|
-
var update_default =
|
|
4270
|
+
var update_default = app17;
|
|
4210
4271
|
|
|
4211
4272
|
// src/web/api/variants.ts
|
|
4212
|
-
import { existsSync as
|
|
4213
|
-
import { resolve as
|
|
4214
|
-
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";
|
|
4215
4276
|
|
|
4216
4277
|
// src/lib/spawn-server.ts
|
|
4217
4278
|
import { spawn as spawn4 } from "child_process";
|
|
4218
|
-
import { closeSync, mkdirSync as
|
|
4219
|
-
import { resolve as
|
|
4279
|
+
import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync10 } from "fs";
|
|
4280
|
+
import { resolve as resolve16 } from "path";
|
|
4220
4281
|
function tsxBin(cwd) {
|
|
4221
|
-
return
|
|
4282
|
+
return resolve16(cwd, "node_modules", ".bin", "tsx");
|
|
4222
4283
|
}
|
|
4223
4284
|
function spawnServer(cwd, port, options) {
|
|
4224
4285
|
if (options?.detached) {
|
|
@@ -4231,31 +4292,31 @@ function spawnAttached(cwd, port) {
|
|
|
4231
4292
|
cwd,
|
|
4232
4293
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4233
4294
|
});
|
|
4234
|
-
return new Promise((
|
|
4235
|
-
const timeout = setTimeout(() =>
|
|
4295
|
+
return new Promise((resolve20) => {
|
|
4296
|
+
const timeout = setTimeout(() => resolve20(null), 3e4);
|
|
4236
4297
|
function checkOutput(data) {
|
|
4237
4298
|
const match = data.toString().match(/listening on :(\d+)/);
|
|
4238
4299
|
if (match) {
|
|
4239
4300
|
clearTimeout(timeout);
|
|
4240
|
-
|
|
4301
|
+
resolve20({ child, actualPort: parseInt(match[1], 10) });
|
|
4241
4302
|
}
|
|
4242
4303
|
}
|
|
4243
4304
|
child.stdout?.on("data", checkOutput);
|
|
4244
4305
|
child.stderr?.on("data", checkOutput);
|
|
4245
4306
|
child.on("error", () => {
|
|
4246
4307
|
clearTimeout(timeout);
|
|
4247
|
-
|
|
4308
|
+
resolve20(null);
|
|
4248
4309
|
});
|
|
4249
4310
|
child.on("exit", () => {
|
|
4250
4311
|
clearTimeout(timeout);
|
|
4251
|
-
|
|
4312
|
+
resolve20(null);
|
|
4252
4313
|
});
|
|
4253
4314
|
});
|
|
4254
4315
|
}
|
|
4255
4316
|
function spawnDetached(cwd, port, logDir) {
|
|
4256
|
-
const logsDir = logDir ??
|
|
4257
|
-
|
|
4258
|
-
const logPath =
|
|
4317
|
+
const logsDir = logDir ?? resolve16(cwd, ".mind", "logs");
|
|
4318
|
+
mkdirSync8(logsDir, { recursive: true });
|
|
4319
|
+
const logPath = resolve16(logsDir, "mind.log");
|
|
4259
4320
|
const logFd = openSync(logPath, "a");
|
|
4260
4321
|
const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
4261
4322
|
cwd,
|
|
@@ -4275,7 +4336,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
4275
4336
|
}
|
|
4276
4337
|
const interval = setInterval(() => {
|
|
4277
4338
|
try {
|
|
4278
|
-
const content =
|
|
4339
|
+
const content = readFileSync10(logPath, "utf-8");
|
|
4279
4340
|
const match = content.match(/listening on :(\d+)/);
|
|
4280
4341
|
if (match) {
|
|
4281
4342
|
finish({ child, actualPort: parseInt(match[1], 10) });
|
|
@@ -4290,7 +4351,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
4290
4351
|
}
|
|
4291
4352
|
|
|
4292
4353
|
// src/lib/verify.ts
|
|
4293
|
-
async function
|
|
4354
|
+
async function verify2(port) {
|
|
4294
4355
|
const health = await checkHealth(port);
|
|
4295
4356
|
if (!health.ok) {
|
|
4296
4357
|
console.error(" Health check: failed");
|
|
@@ -4327,7 +4388,7 @@ async function verify(port) {
|
|
|
4327
4388
|
}
|
|
4328
4389
|
|
|
4329
4390
|
// src/web/api/variants.ts
|
|
4330
|
-
var
|
|
4391
|
+
var app18 = new Hono18().get("/:name/variants", async (c) => {
|
|
4331
4392
|
const name = c.req.param("name");
|
|
4332
4393
|
const entry = findMind(name);
|
|
4333
4394
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
@@ -4357,11 +4418,11 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4357
4418
|
const err = validateBranchName(variantName);
|
|
4358
4419
|
if (err) return c.json({ error: err }, 400);
|
|
4359
4420
|
const projectRoot = mindDir(mindName);
|
|
4360
|
-
const variantDir =
|
|
4361
|
-
if (
|
|
4421
|
+
const variantDir = resolve17(projectRoot, ".variants", variantName);
|
|
4422
|
+
if (existsSync12(variantDir)) {
|
|
4362
4423
|
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
4363
4424
|
}
|
|
4364
|
-
|
|
4425
|
+
mkdirSync9(resolve17(projectRoot, ".variants"), { recursive: true });
|
|
4365
4426
|
try {
|
|
4366
4427
|
await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
4367
4428
|
} catch (e) {
|
|
@@ -4374,7 +4435,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4374
4435
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
4375
4436
|
await exec(cmd, args, {
|
|
4376
4437
|
cwd: variantDir,
|
|
4377
|
-
env: { ...process.env, HOME:
|
|
4438
|
+
env: { ...process.env, HOME: resolve17(variantDir, "home") }
|
|
4378
4439
|
});
|
|
4379
4440
|
} else {
|
|
4380
4441
|
await exec("npm", ["install"], { cwd: variantDir });
|
|
@@ -4384,7 +4445,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4384
4445
|
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
4385
4446
|
}
|
|
4386
4447
|
if (body.soul) {
|
|
4387
|
-
|
|
4448
|
+
writeFileSync9(resolve17(variantDir, "home/SOUL.md"), body.soul);
|
|
4388
4449
|
}
|
|
4389
4450
|
const variantPort = body.port ?? nextPort();
|
|
4390
4451
|
const variant = {
|
|
@@ -4422,7 +4483,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4422
4483
|
} catch {
|
|
4423
4484
|
}
|
|
4424
4485
|
const projectRoot = mindDir(mindName);
|
|
4425
|
-
if (
|
|
4486
|
+
if (existsSync12(variant.path)) {
|
|
4426
4487
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
4427
4488
|
if (status) {
|
|
4428
4489
|
try {
|
|
@@ -4448,7 +4509,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4448
4509
|
500
|
|
4449
4510
|
);
|
|
4450
4511
|
}
|
|
4451
|
-
const verified = await
|
|
4512
|
+
const verified = await verify2(result.actualPort);
|
|
4452
4513
|
try {
|
|
4453
4514
|
process.kill(result.child.pid);
|
|
4454
4515
|
} catch {
|
|
@@ -4479,7 +4540,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4479
4540
|
} catch (e) {
|
|
4480
4541
|
return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
|
|
4481
4542
|
}
|
|
4482
|
-
if (
|
|
4543
|
+
if (existsSync12(variant.path)) {
|
|
4483
4544
|
try {
|
|
4484
4545
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
4485
4546
|
} catch {
|
|
@@ -4496,7 +4557,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4496
4557
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
4497
4558
|
await exec(cmd, args, {
|
|
4498
4559
|
cwd: projectRoot,
|
|
4499
|
-
env: { ...process.env, HOME:
|
|
4560
|
+
env: { ...process.env, HOME: resolve17(projectRoot, "home") }
|
|
4500
4561
|
});
|
|
4501
4562
|
} else {
|
|
4502
4563
|
await exec("npm", ["install"], { cwd: projectRoot });
|
|
@@ -4539,7 +4600,7 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4539
4600
|
} catch {
|
|
4540
4601
|
}
|
|
4541
4602
|
}
|
|
4542
|
-
if (
|
|
4603
|
+
if (existsSync12(variant.path)) {
|
|
4543
4604
|
try {
|
|
4544
4605
|
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
4545
4606
|
} catch {
|
|
@@ -4553,16 +4614,19 @@ var app16 = new Hono16().get("/:name/variants", async (c) => {
|
|
|
4553
4614
|
chownMindDir(projectRoot, mindName);
|
|
4554
4615
|
return c.json({ ok: true });
|
|
4555
4616
|
});
|
|
4556
|
-
var variants_default =
|
|
4617
|
+
var variants_default = app18;
|
|
4557
4618
|
|
|
4558
4619
|
// src/web/api/volute/channels.ts
|
|
4559
4620
|
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
4560
|
-
import { Hono as
|
|
4621
|
+
import { Hono as Hono19 } from "hono";
|
|
4561
4622
|
import { z as z6 } from "zod";
|
|
4562
4623
|
var createSchema = z6.object({
|
|
4563
4624
|
name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
|
|
4564
4625
|
});
|
|
4565
|
-
var
|
|
4626
|
+
var inviteSchema = z6.object({
|
|
4627
|
+
username: z6.string().min(1)
|
|
4628
|
+
});
|
|
4629
|
+
var app19 = new Hono19().get("/", async (c) => {
|
|
4566
4630
|
const user = c.get("user");
|
|
4567
4631
|
const channels = await listChannels();
|
|
4568
4632
|
const results = await Promise.all(
|
|
@@ -4606,16 +4670,85 @@ var app17 = new Hono17().get("/", async (c) => {
|
|
|
4606
4670
|
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
4607
4671
|
const participants = await getParticipants(ch.id);
|
|
4608
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 });
|
|
4609
4692
|
});
|
|
4610
|
-
var channels_default2 =
|
|
4693
|
+
var channels_default2 = app19;
|
|
4611
4694
|
|
|
4612
4695
|
// src/web/api/volute/chat.ts
|
|
4613
|
-
import { readFileSync as readFileSync9 } from "fs";
|
|
4614
|
-
import { resolve as resolve16 } from "path";
|
|
4615
4696
|
import { zValidator as zValidator7 } from "@hono/zod-validator";
|
|
4616
|
-
import { Hono as
|
|
4697
|
+
import { Hono as Hono20 } from "hono";
|
|
4617
4698
|
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
4618
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
|
|
4726
|
+
};
|
|
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
|
+
}
|
|
4751
|
+
}
|
|
4619
4752
|
var chatSchema = z7.object({
|
|
4620
4753
|
message: z7.string().optional(),
|
|
4621
4754
|
conversationId: z7.string().optional(),
|
|
@@ -4627,25 +4760,7 @@ var chatSchema = z7.object({
|
|
|
4627
4760
|
})
|
|
4628
4761
|
).optional()
|
|
4629
4762
|
});
|
|
4630
|
-
|
|
4631
|
-
try {
|
|
4632
|
-
const data = JSON.parse(readFileSync9(resolve16(voluteHome(), "daemon.json"), "utf-8"));
|
|
4633
|
-
return `http://${daemonLoopback()}:${data.port}`;
|
|
4634
|
-
} catch (err) {
|
|
4635
|
-
throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
|
|
4636
|
-
}
|
|
4637
|
-
}
|
|
4638
|
-
function daemonFetchInternal(path, body) {
|
|
4639
|
-
const daemonUrl = getDaemonUrl();
|
|
4640
|
-
const token = process.env.VOLUTE_DAEMON_TOKEN;
|
|
4641
|
-
const headers = {
|
|
4642
|
-
"Content-Type": "application/json",
|
|
4643
|
-
Origin: daemonUrl
|
|
4644
|
-
};
|
|
4645
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
4646
|
-
return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
|
|
4647
|
-
}
|
|
4648
|
-
var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
|
|
4763
|
+
var app20 = new Hono20().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
|
|
4649
4764
|
const name = c.req.param("name");
|
|
4650
4765
|
const [baseName] = name.split("@", 2);
|
|
4651
4766
|
const entry = findMind(baseName);
|
|
@@ -4681,8 +4796,8 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
4681
4796
|
}
|
|
4682
4797
|
}
|
|
4683
4798
|
if (!conversationId) {
|
|
4684
|
-
const
|
|
4685
|
-
const title = [...
|
|
4799
|
+
const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
|
|
4800
|
+
const title = [...participantNames].join(", ");
|
|
4686
4801
|
const conv2 = await createConversation(baseName, "volute", {
|
|
4687
4802
|
userId: user.id !== 0 ? user.id : void 0,
|
|
4688
4803
|
title,
|
|
@@ -4692,7 +4807,7 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
4692
4807
|
}
|
|
4693
4808
|
}
|
|
4694
4809
|
const conv = await getConversation(conversationId);
|
|
4695
|
-
const convTitle = conv?.title;
|
|
4810
|
+
const convTitle = conv?.title ?? null;
|
|
4696
4811
|
const contentBlocks = [];
|
|
4697
4812
|
if (body.message) {
|
|
4698
4813
|
contentBlocks.push({ type: "text", text: body.message });
|
|
@@ -4703,61 +4818,13 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
4703
4818
|
}
|
|
4704
4819
|
}
|
|
4705
4820
|
await addMessage(conversationId, "user", senderName, contentBlocks);
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
return manager.isRunning(mindKey) ? ap.username : null;
|
|
4714
|
-
}).filter((n) => n !== null && n !== senderName);
|
|
4715
|
-
const isDM = participants.length === 2;
|
|
4716
|
-
function channelForMind(mindUsername) {
|
|
4717
|
-
return buildVoluteSlug({
|
|
4718
|
-
participants,
|
|
4719
|
-
mindUsername,
|
|
4720
|
-
convTitle,
|
|
4721
|
-
conversationId
|
|
4722
|
-
});
|
|
4723
|
-
}
|
|
4724
|
-
const channelEntry = {
|
|
4725
|
-
platformId: conversationId,
|
|
4726
|
-
platform: "volute",
|
|
4727
|
-
name: convTitle ?? void 0,
|
|
4728
|
-
type: isDM ? "dm" : "group"
|
|
4729
|
-
};
|
|
4730
|
-
for (const ap of mindParticipants) {
|
|
4731
|
-
try {
|
|
4732
|
-
writeChannelEntry(ap.username, channelForMind(ap.username), channelEntry);
|
|
4733
|
-
} catch (err) {
|
|
4734
|
-
console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
|
|
4735
|
-
}
|
|
4736
|
-
}
|
|
4737
|
-
for (const mindName of runningMinds) {
|
|
4738
|
-
const targetName = mindName === baseName ? name : mindName;
|
|
4739
|
-
const channel = channelForMind(mindName);
|
|
4740
|
-
const typingMap = getTypingMap();
|
|
4741
|
-
const currentlyTyping = typingMap.get(channel);
|
|
4742
|
-
const payload = JSON.stringify({
|
|
4743
|
-
content: contentBlocks,
|
|
4744
|
-
channel,
|
|
4745
|
-
conversationId,
|
|
4746
|
-
sender: senderName,
|
|
4747
|
-
participants: participantNames,
|
|
4748
|
-
participantCount: participants.length,
|
|
4749
|
-
isDM,
|
|
4750
|
-
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
4751
|
-
});
|
|
4752
|
-
daemonFetchInternal(`/api/minds/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
|
|
4753
|
-
if (!res.ok) {
|
|
4754
|
-
const text = await res.text().catch(() => "");
|
|
4755
|
-
console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
|
|
4756
|
-
}
|
|
4757
|
-
}).catch((err) => {
|
|
4758
|
-
console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
|
|
4759
|
-
});
|
|
4760
|
-
}
|
|
4821
|
+
await fanOutToMinds({
|
|
4822
|
+
conversationId,
|
|
4823
|
+
contentBlocks,
|
|
4824
|
+
senderName,
|
|
4825
|
+
convTitle,
|
|
4826
|
+
targetName: (username) => username === baseName ? name : username
|
|
4827
|
+
});
|
|
4761
4828
|
return c.json({ ok: true, conversationId });
|
|
4762
4829
|
}).get("/:name/conversations/:id/events", async (c) => {
|
|
4763
4830
|
const conversationId = c.req.param("id");
|
|
@@ -4776,11 +4843,11 @@ var app18 = new Hono18().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
4776
4843
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
4777
4844
|
});
|
|
4778
4845
|
}, 15e3);
|
|
4779
|
-
await new Promise((
|
|
4846
|
+
await new Promise((resolve20) => {
|
|
4780
4847
|
stream.onAbort(() => {
|
|
4781
4848
|
unsubscribe();
|
|
4782
4849
|
clearInterval(keepAlive);
|
|
4783
|
-
|
|
4850
|
+
resolve20();
|
|
4784
4851
|
});
|
|
4785
4852
|
});
|
|
4786
4853
|
});
|
|
@@ -4790,7 +4857,7 @@ var unifiedChatSchema = z7.object({
|
|
|
4790
4857
|
conversationId: z7.string(),
|
|
4791
4858
|
images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
|
|
4792
4859
|
});
|
|
4793
|
-
var unifiedChatApp = new
|
|
4860
|
+
var unifiedChatApp = new Hono20().post(
|
|
4794
4861
|
"/chat",
|
|
4795
4862
|
zValidator7("json", unifiedChatSchema),
|
|
4796
4863
|
async (c) => {
|
|
@@ -4813,79 +4880,31 @@ var unifiedChatApp = new Hono18().post(
|
|
|
4813
4880
|
}
|
|
4814
4881
|
}
|
|
4815
4882
|
await addMessage(body.conversationId, "user", senderName, contentBlocks);
|
|
4816
|
-
const
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
name: conv.title ?? void 0,
|
|
4827
|
-
type: conv.type === "channel" ? "group" : isDM ? "dm" : "group"
|
|
4828
|
-
};
|
|
4829
|
-
for (const ap of mindParticipants) {
|
|
4830
|
-
const slug = buildVoluteSlug({
|
|
4831
|
-
participants,
|
|
4832
|
-
mindUsername: ap.username,
|
|
4833
|
-
convTitle: conv.title,
|
|
4834
|
-
conversationId: conv.id,
|
|
4835
|
-
convType: conv.type,
|
|
4836
|
-
convName: conv.name
|
|
4837
|
-
});
|
|
4838
|
-
try {
|
|
4839
|
-
writeChannelEntry(ap.username, slug, channelEntry);
|
|
4840
|
-
} catch (err) {
|
|
4841
|
-
console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
|
|
4842
|
-
}
|
|
4843
|
-
}
|
|
4844
|
-
for (const mindName of runningMinds) {
|
|
4845
|
-
const channel = buildVoluteSlug({
|
|
4846
|
-
participants,
|
|
4847
|
-
mindUsername: mindName,
|
|
4848
|
-
convTitle: conv.title,
|
|
4849
|
-
conversationId: body.conversationId,
|
|
4850
|
-
convType: conv.type,
|
|
4851
|
-
convName: conv.name
|
|
4852
|
-
});
|
|
4853
|
-
const typingMap = getTypingMap();
|
|
4854
|
-
const currentlyTyping = typingMap.get(channel);
|
|
4855
|
-
const payload = JSON.stringify({
|
|
4856
|
-
content: contentBlocks,
|
|
4857
|
-
channel,
|
|
4858
|
-
conversationId: body.conversationId,
|
|
4859
|
-
sender: senderName,
|
|
4860
|
-
participants: participantNames,
|
|
4861
|
-
participantCount: participants.length,
|
|
4862
|
-
isDM,
|
|
4863
|
-
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
4864
|
-
});
|
|
4865
|
-
daemonFetchInternal(`/api/minds/${encodeURIComponent(mindName)}/message`, payload).then(async (res) => {
|
|
4866
|
-
if (!res.ok) {
|
|
4867
|
-
const text = await res.text().catch(() => "");
|
|
4868
|
-
console.error(`[chat] mind ${mindName} responded ${res.status}: ${text}`);
|
|
4869
|
-
}
|
|
4870
|
-
}).catch((err) => {
|
|
4871
|
-
console.error(`[chat] mind ${mindName} unreachable via daemon:`, err);
|
|
4872
|
-
});
|
|
4873
|
-
}
|
|
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
|
+
});
|
|
4874
4893
|
return c.json({ ok: true, conversationId: body.conversationId });
|
|
4875
4894
|
}
|
|
4876
4895
|
);
|
|
4877
|
-
var chat_default =
|
|
4896
|
+
var chat_default = app20;
|
|
4878
4897
|
|
|
4879
4898
|
// src/web/api/volute/conversations.ts
|
|
4880
4899
|
import { zValidator as zValidator8 } from "@hono/zod-validator";
|
|
4881
|
-
import { Hono as
|
|
4900
|
+
import { Hono as Hono21 } from "hono";
|
|
4882
4901
|
import { z as z8 } from "zod";
|
|
4883
4902
|
var createConvSchema = z8.object({
|
|
4884
4903
|
title: z8.string().optional(),
|
|
4885
4904
|
participantIds: z8.array(z8.number()).optional(),
|
|
4886
4905
|
participantNames: z8.array(z8.string()).optional()
|
|
4887
4906
|
});
|
|
4888
|
-
var
|
|
4907
|
+
var app21 = new Hono21().get("/:name/conversations", async (c) => {
|
|
4889
4908
|
const name = c.req.param("name");
|
|
4890
4909
|
const user = c.get("user");
|
|
4891
4910
|
let lookupId = user.id;
|
|
@@ -4970,18 +4989,18 @@ var app19 = new Hono19().get("/:name/conversations", async (c) => {
|
|
|
4970
4989
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
4971
4990
|
return c.json({ ok: true });
|
|
4972
4991
|
});
|
|
4973
|
-
var conversations_default =
|
|
4992
|
+
var conversations_default = app21;
|
|
4974
4993
|
|
|
4975
4994
|
// src/web/api/volute/user-conversations.ts
|
|
4976
4995
|
import { zValidator as zValidator9 } from "@hono/zod-validator";
|
|
4977
|
-
import { Hono as
|
|
4996
|
+
import { Hono as Hono22 } from "hono";
|
|
4978
4997
|
import { streamSSE as streamSSE4 } from "hono/streaming";
|
|
4979
4998
|
import { z as z9 } from "zod";
|
|
4980
4999
|
var createSchema2 = z9.object({
|
|
4981
5000
|
title: z9.string().optional(),
|
|
4982
5001
|
participantNames: z9.array(z9.string()).min(1)
|
|
4983
5002
|
});
|
|
4984
|
-
var
|
|
5003
|
+
var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
|
|
4985
5004
|
const user = c.get("user");
|
|
4986
5005
|
const convs = await listConversationsWithParticipants(user.id);
|
|
4987
5006
|
return c.json(convs);
|
|
@@ -5040,11 +5059,11 @@ var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
|
|
|
5040
5059
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
5041
5060
|
});
|
|
5042
5061
|
}, 15e3);
|
|
5043
|
-
await new Promise((
|
|
5062
|
+
await new Promise((resolve20) => {
|
|
5044
5063
|
stream.onAbort(() => {
|
|
5045
5064
|
unsubscribe();
|
|
5046
5065
|
clearInterval(keepAlive);
|
|
5047
|
-
|
|
5066
|
+
resolve20();
|
|
5048
5067
|
});
|
|
5049
5068
|
});
|
|
5050
5069
|
});
|
|
@@ -5055,12 +5074,12 @@ var app20 = new Hono20().use("*", authMiddleware).get("/", async (c) => {
|
|
|
5055
5074
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
5056
5075
|
return c.json({ ok: true });
|
|
5057
5076
|
});
|
|
5058
|
-
var user_conversations_default =
|
|
5077
|
+
var user_conversations_default = app22;
|
|
5059
5078
|
|
|
5060
5079
|
// src/web/app.ts
|
|
5061
5080
|
var httpLog = logger_default.child("http");
|
|
5062
|
-
var
|
|
5063
|
-
|
|
5081
|
+
var app23 = new Hono23();
|
|
5082
|
+
app23.onError((err, c) => {
|
|
5064
5083
|
if (err instanceof HTTPException) {
|
|
5065
5084
|
return err.getResponse();
|
|
5066
5085
|
}
|
|
@@ -5071,10 +5090,10 @@ app21.onError((err, c) => {
|
|
|
5071
5090
|
});
|
|
5072
5091
|
return c.json({ error: "Internal server error" }, 500);
|
|
5073
5092
|
});
|
|
5074
|
-
|
|
5093
|
+
app23.notFound((c) => {
|
|
5075
5094
|
return c.json({ error: "Not found" }, 404);
|
|
5076
5095
|
});
|
|
5077
|
-
|
|
5096
|
+
app23.use("*", async (c, next) => {
|
|
5078
5097
|
const start = Date.now();
|
|
5079
5098
|
await next();
|
|
5080
5099
|
const duration = Date.now() - start;
|
|
@@ -5085,7 +5104,7 @@ app21.use("*", async (c, next) => {
|
|
|
5085
5104
|
httpLog.debug("request", data);
|
|
5086
5105
|
}
|
|
5087
5106
|
});
|
|
5088
|
-
|
|
5107
|
+
app23.get("/api/health", (c) => {
|
|
5089
5108
|
let version = "unknown";
|
|
5090
5109
|
let cached = null;
|
|
5091
5110
|
try {
|
|
@@ -5100,18 +5119,18 @@ app21.get("/api/health", (c) => {
|
|
|
5100
5119
|
...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
|
|
5101
5120
|
});
|
|
5102
5121
|
});
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
var routes =
|
|
5114
|
-
var app_default =
|
|
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;
|
|
5115
5134
|
|
|
5116
5135
|
// src/web/server.ts
|
|
5117
5136
|
var MIME_TYPES2 = {
|
|
@@ -5128,20 +5147,20 @@ async function startServer({
|
|
|
5128
5147
|
hostname = "127.0.0.1"
|
|
5129
5148
|
}) {
|
|
5130
5149
|
let assetsDir = "";
|
|
5131
|
-
let searchDir =
|
|
5150
|
+
let searchDir = dirname3(new URL(import.meta.url).pathname);
|
|
5132
5151
|
for (let i = 0; i < 5; i++) {
|
|
5133
|
-
const candidate =
|
|
5134
|
-
if (
|
|
5152
|
+
const candidate = resolve18(searchDir, "dist", "web-assets");
|
|
5153
|
+
if (existsSync13(candidate)) {
|
|
5135
5154
|
assetsDir = candidate;
|
|
5136
5155
|
break;
|
|
5137
5156
|
}
|
|
5138
|
-
searchDir =
|
|
5157
|
+
searchDir = dirname3(searchDir);
|
|
5139
5158
|
}
|
|
5140
5159
|
if (assetsDir) {
|
|
5141
5160
|
app_default.get("*", async (c) => {
|
|
5142
5161
|
const urlPath = new URL(c.req.url).pathname;
|
|
5143
5162
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
5144
|
-
const filePath =
|
|
5163
|
+
const filePath = resolve18(assetsDir, urlPath.slice(1));
|
|
5145
5164
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
5146
5165
|
const s = await stat2(filePath).catch(() => null);
|
|
5147
5166
|
if (s?.isFile()) {
|
|
@@ -5150,7 +5169,7 @@ async function startServer({
|
|
|
5150
5169
|
const body = await readFile3(filePath);
|
|
5151
5170
|
return c.body(body, 200, { "Content-Type": mime });
|
|
5152
5171
|
}
|
|
5153
|
-
const indexPath =
|
|
5172
|
+
const indexPath = resolve18(assetsDir, "index.html");
|
|
5154
5173
|
const indexStat = await stat2(indexPath).catch(() => null);
|
|
5155
5174
|
if (indexStat?.isFile()) {
|
|
5156
5175
|
const body = await readFile3(indexPath, "utf-8");
|
|
@@ -5160,10 +5179,10 @@ async function startServer({
|
|
|
5160
5179
|
});
|
|
5161
5180
|
}
|
|
5162
5181
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
5163
|
-
await new Promise((
|
|
5182
|
+
await new Promise((resolve20, reject) => {
|
|
5164
5183
|
server.on("listening", () => {
|
|
5165
5184
|
logger_default.info("Volute UI running", { hostname, port });
|
|
5166
|
-
|
|
5185
|
+
resolve20();
|
|
5167
5186
|
});
|
|
5168
5187
|
server.on("error", (err) => {
|
|
5169
5188
|
reject(err);
|
|
@@ -5174,14 +5193,14 @@ async function startServer({
|
|
|
5174
5193
|
|
|
5175
5194
|
// src/daemon.ts
|
|
5176
5195
|
if (!process.env.VOLUTE_HOME) {
|
|
5177
|
-
process.env.VOLUTE_HOME =
|
|
5196
|
+
process.env.VOLUTE_HOME = resolve19(homedir2(), ".volute");
|
|
5178
5197
|
}
|
|
5179
5198
|
async function startDaemon(opts) {
|
|
5180
5199
|
const { port, hostname } = opts;
|
|
5181
5200
|
const myPid = String(process.pid);
|
|
5182
5201
|
const home = voluteHome();
|
|
5183
5202
|
if (!opts.foreground) {
|
|
5184
|
-
const rotatingLog = new RotatingLog(
|
|
5203
|
+
const rotatingLog = new RotatingLog(resolve19(home, "daemon.log"));
|
|
5185
5204
|
logger_default.setOutput((line) => rotatingLog.write(`${line}
|
|
5186
5205
|
`));
|
|
5187
5206
|
const write = (...args) => rotatingLog.write(`${format(...args)}
|
|
@@ -5191,10 +5210,21 @@ async function startDaemon(opts) {
|
|
|
5191
5210
|
console.warn = write;
|
|
5192
5211
|
console.info = write;
|
|
5193
5212
|
}
|
|
5194
|
-
const DAEMON_PID_PATH =
|
|
5195
|
-
const DAEMON_JSON_PATH =
|
|
5196
|
-
|
|
5213
|
+
const DAEMON_PID_PATH = resolve19(home, "daemon.pid");
|
|
5214
|
+
const DAEMON_JSON_PATH = resolve19(home, "daemon.json");
|
|
5215
|
+
mkdirSync10(home, { recursive: true });
|
|
5197
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
|
+
}
|
|
5198
5228
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
5199
5229
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
5200
5230
|
process.env.VOLUTE_DAEMON_PORT = String(port);
|
|
@@ -5210,73 +5240,78 @@ async function startDaemon(opts) {
|
|
|
5210
5240
|
}
|
|
5211
5241
|
throw err;
|
|
5212
5242
|
}
|
|
5213
|
-
|
|
5214
|
-
|
|
5243
|
+
writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
5244
|
+
writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
5215
5245
|
`, {
|
|
5216
5246
|
mode: 420
|
|
5217
5247
|
});
|
|
5248
|
+
const delivery = initDeliveryManager();
|
|
5218
5249
|
const manager = initMindManager();
|
|
5219
5250
|
manager.loadCrashAttempts();
|
|
5220
5251
|
const connectors = initConnectorManager();
|
|
5221
|
-
const scheduler =
|
|
5222
|
-
scheduler.start(
|
|
5223
|
-
const mailPoller =
|
|
5224
|
-
mailPoller.start(
|
|
5225
|
-
const tokenBudget =
|
|
5226
|
-
tokenBudget.start(
|
|
5252
|
+
const scheduler = initScheduler();
|
|
5253
|
+
scheduler.start();
|
|
5254
|
+
const mailPoller = initMailPoller();
|
|
5255
|
+
mailPoller.start();
|
|
5256
|
+
const tokenBudget = initTokenBudget();
|
|
5257
|
+
tokenBudget.start();
|
|
5227
5258
|
const registry = readRegistry();
|
|
5228
5259
|
for (const entry of registry) {
|
|
5229
5260
|
try {
|
|
5261
|
+
migrateDotVoluteDir(entry.name);
|
|
5230
5262
|
migrateMindState(entry.name);
|
|
5231
5263
|
} catch (err) {
|
|
5232
5264
|
logger_default.warn(`failed to migrate state for ${entry.name}`, logger_default.errorData(err));
|
|
5233
5265
|
}
|
|
5234
5266
|
}
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
tokenBudget.setBudget(
|
|
5248
|
-
entry.name,
|
|
5249
|
-
config.tokenBudget,
|
|
5250
|
-
config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
|
|
5251
|
-
);
|
|
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
|
+
}
|
|
5252
5279
|
}
|
|
5253
|
-
}
|
|
5254
|
-
|
|
5255
|
-
setMindRunning(entry.name, false);
|
|
5256
|
-
}
|
|
5280
|
+
});
|
|
5281
|
+
await Promise.all(workers);
|
|
5257
5282
|
}
|
|
5258
5283
|
const runningVariants = getAllRunningVariants();
|
|
5259
|
-
|
|
5260
|
-
const
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
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);
|
|
5267
5299
|
}
|
|
5300
|
+
delivery.restoreFromDb().catch((err) => {
|
|
5301
|
+
logger_default.warn("failed to restore delivery queue", logger_default.errorData(err));
|
|
5302
|
+
});
|
|
5268
5303
|
cleanExpiredSessions().catch(() => {
|
|
5269
5304
|
});
|
|
5270
5305
|
logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
|
|
5271
5306
|
function cleanup() {
|
|
5272
5307
|
try {
|
|
5273
|
-
if (
|
|
5308
|
+
if (readFileSync11(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
5274
5309
|
unlinkSync2(DAEMON_PID_PATH);
|
|
5275
5310
|
}
|
|
5276
5311
|
} catch {
|
|
5277
5312
|
}
|
|
5278
5313
|
try {
|
|
5279
|
-
const data = JSON.parse(
|
|
5314
|
+
const data = JSON.parse(readFileSync11(DAEMON_JSON_PATH, "utf-8"));
|
|
5280
5315
|
if (data.token === token) {
|
|
5281
5316
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
5282
5317
|
}
|
|
@@ -5292,6 +5327,7 @@ async function startDaemon(opts) {
|
|
|
5292
5327
|
scheduler.saveState();
|
|
5293
5328
|
mailPoller.stop();
|
|
5294
5329
|
tokenBudget.stop();
|
|
5330
|
+
delivery.dispose();
|
|
5295
5331
|
await connectors.stopAll();
|
|
5296
5332
|
await manager.stopAll();
|
|
5297
5333
|
manager.clearCrashAttempts();
|