volute 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +4294 -0
- package/dist/chunk-G5KRTU2F.js +76 -0
- package/dist/chunk-ISWZ6QUK.js +2691 -0
- package/dist/{chunk-J5A3DF2U.js → chunk-JG4CCJOA.js} +1 -1
- package/dist/{chunk-IPJXU366.js → chunk-JTDFJWI2.js} +1 -0
- package/dist/{chunk-7LPTHFIL.js → chunk-M5CNKH4J.js} +55 -5
- package/dist/{chunk-L3LHXZD7.js → chunk-PHHKNGA3.js} +1 -1
- package/dist/{chunk-Q7AITQ44.js → chunk-QIXPN3OO.js} +1 -1
- package/dist/{chunk-PC6R6UUW.js → chunk-RK627D57.js} +36 -59
- package/dist/{chunk-5462YKWP.js → chunk-TFS25FIM.js} +1 -1
- package/dist/{chunk-QUJUKM4U.js → chunk-VT5QODNE.js} +1 -1
- package/dist/chunk-XLC342FO.js +29 -0
- package/dist/cli.js +10 -10
- package/dist/cloud-sync-PI47U2LT.js +96 -0
- package/dist/{daemon-restart-BH67ZOTE.js → daemon-restart-RMGOOGPE.js} +4 -4
- package/dist/daemon.js +1216 -1822
- package/dist/{down-LIOQ5JDH.js → down-WSUASL5E.js} +3 -3
- package/dist/{import-E433B4KG.js → import-EAXTHHXL.js} +2 -1
- package/dist/message-delivery-FHV4NO2F.js +23 -0
- package/dist/{mind-BIDOF65R.js → mind-BTXR5B3C.js} +13 -5
- package/dist/{mind-manager-3V2NXX4I.js → mind-manager-KMY4GA2J.js} +1 -1
- package/dist/mind-sleep-FWRBIFBS.js +41 -0
- package/dist/mind-wake-LJK2YU5X.js +36 -0
- package/dist/{package-HQR52XSG.js → package-CUBJ4PKS.js} +10 -1
- package/dist/{pages-KQBR5TAZ.js → pages-YSTRWJR4.js} +1 -1
- package/dist/{publish-OJ4QMXVZ.js → publish-BZNHKUUK.js} +2 -2
- package/dist/{service-TVNEORO7.js → service-7BFXDI6J.js} +4 -4
- package/dist/{setup-OZDYCKDI.js → setup-SSIIXQMI.js} +2 -2
- package/dist/sleep-manager-2TMQ65E4.js +27 -0
- package/dist/{sprout-6Z6C42YM.js → sprout-UKCYBGHK.js} +2 -2
- package/dist/{status-Z7NAFMBI.js → status-H2MKDN6L.js} +2 -2
- package/dist/{up-7BGDMFRT.js → up-Z5JRG2M2.js} +3 -3
- package/dist/{update-4WT7VWHW.js → update-ELC6MEUT.js} +2 -2
- package/dist/{upgrade-ZEC2GGFO.js → upgrade-GXW2EQY3.js} +11 -2
- package/dist/{version-notify-TFS2U5CF.js → version-notify-LKABEJSA.js} +11 -3
- package/dist/web-assets/assets/index-CZ26vsyY.js +69 -0
- package/dist/web-assets/assets/index-DyyAvJwW.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +10 -1
- package/templates/_base/.init/.config/prompts.json +1 -0
- package/templates/_base/home/.config/config.json.tmpl +4 -1
- package/templates/_base/src/lib/file-handler.ts +6 -1
- package/templates/_base/src/lib/logger.ts +68 -23
- package/templates/_base/src/lib/startup.ts +12 -3
- package/templates/claude/src/agent.ts +150 -29
- package/templates/claude/src/lib/hooks/pre-compact.ts +18 -4
- package/templates/claude/src/lib/message-channel.ts +6 -0
- package/templates/claude/src/lib/stream-consumer.ts +17 -1
- package/templates/claude/src/server.ts +3 -1
- package/templates/pi/home/.config/config.json.tmpl +4 -1
- package/templates/pi/src/agent.ts +87 -0
- package/templates/pi/src/lib/content.ts +18 -3
- package/templates/pi/src/lib/event-handler.ts +22 -2
- package/templates/pi/src/server.ts +3 -1
- package/dist/chunk-OGZYB5GL.js +0 -847
- package/dist/web-assets/assets/index-BR3gtK3E.css +0 -1
- package/dist/web-assets/assets/index-CWmrZRQd.js +0 -64
- /package/dist/{shared-DCQ2UXOM.js → shared-2OGT3NSL.js} +0 -0
package/dist/daemon.js
CHANGED
|
@@ -7,62 +7,79 @@ import {
|
|
|
7
7
|
sharedMerge,
|
|
8
8
|
sharedPull,
|
|
9
9
|
sharedStatus
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-PHHKNGA3.js";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
fireWebhook,
|
|
13
|
+
initWebhook
|
|
14
|
+
} from "./chunk-G5KRTU2F.js";
|
|
14
15
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
getPrompt,
|
|
23
|
-
getPromptIfCustom,
|
|
24
|
-
initMindManager,
|
|
25
|
-
loadJsonMap,
|
|
26
|
-
saveJsonMap,
|
|
27
|
-
substitute
|
|
28
|
-
} from "./chunk-7LPTHFIL.js";
|
|
16
|
+
applyInitFiles,
|
|
17
|
+
composeTemplate,
|
|
18
|
+
computeTemplateHash,
|
|
19
|
+
copyTemplateToDir,
|
|
20
|
+
findTemplatesRoot,
|
|
21
|
+
listFiles
|
|
22
|
+
} from "./chunk-AKPFNL7L.js";
|
|
29
23
|
import {
|
|
30
24
|
deliverMessage,
|
|
31
25
|
extractTextContent,
|
|
26
|
+
getCachedRecentPages,
|
|
27
|
+
getCachedSites,
|
|
28
|
+
getConnectorManager,
|
|
32
29
|
getDeliveryManager,
|
|
30
|
+
getMailPoller,
|
|
31
|
+
getScheduler,
|
|
32
|
+
getTokenBudget,
|
|
33
33
|
getTypingMap,
|
|
34
|
+
initConnectorManager,
|
|
34
35
|
initDeliveryManager,
|
|
36
|
+
initMailPoller,
|
|
37
|
+
initScheduler,
|
|
38
|
+
initSleepManager,
|
|
39
|
+
initTokenBudget,
|
|
35
40
|
publish,
|
|
41
|
+
publish2,
|
|
36
42
|
publishTypingForChannels,
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
recordInbound,
|
|
44
|
+
startMindFull,
|
|
45
|
+
stopAllWatchers,
|
|
46
|
+
stopMindFull,
|
|
47
|
+
subscribe as subscribe2,
|
|
48
|
+
subscribe2 as subscribe3
|
|
49
|
+
} from "./chunk-ISWZ6QUK.js";
|
|
39
50
|
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
computeTemplateHash,
|
|
43
|
-
copyTemplateToDir,
|
|
44
|
-
findTemplatesRoot,
|
|
45
|
-
listFiles
|
|
46
|
-
} from "./chunk-AKPFNL7L.js";
|
|
51
|
+
readSystemsConfig
|
|
52
|
+
} from "./chunk-HFCBO2GL.js";
|
|
47
53
|
import {
|
|
48
54
|
getActiveMinds,
|
|
49
|
-
markIdle,
|
|
50
55
|
onMindEvent,
|
|
51
56
|
stopAll
|
|
52
57
|
} from "./chunk-HGCDWKSP.js";
|
|
53
58
|
import {
|
|
54
59
|
broadcast,
|
|
55
|
-
|
|
56
|
-
subscribe as subscribe2
|
|
60
|
+
subscribe
|
|
57
61
|
} from "./chunk-A4S7H6G6.js";
|
|
62
|
+
import {
|
|
63
|
+
PROMPT_DEFAULTS,
|
|
64
|
+
PROMPT_KEYS,
|
|
65
|
+
RotatingLog,
|
|
66
|
+
getMindManager,
|
|
67
|
+
getMindPromptDefaults,
|
|
68
|
+
getPrompt,
|
|
69
|
+
getPromptIfCustom,
|
|
70
|
+
initMindManager,
|
|
71
|
+
substitute
|
|
72
|
+
} from "./chunk-M5CNKH4J.js";
|
|
58
73
|
import {
|
|
59
74
|
findOpenClawSession,
|
|
60
75
|
importOpenClawConnectors,
|
|
61
76
|
importPiSession,
|
|
62
|
-
parseNameFromIdentity
|
|
77
|
+
parseNameFromIdentity
|
|
78
|
+
} from "./chunk-RK627D57.js";
|
|
79
|
+
import {
|
|
63
80
|
readVoluteConfig,
|
|
64
81
|
writeVoluteConfig
|
|
65
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-XLC342FO.js";
|
|
66
83
|
import {
|
|
67
84
|
loadMergedEnv,
|
|
68
85
|
mindEnvPath,
|
|
@@ -88,7 +105,7 @@ import {
|
|
|
88
105
|
syncBuiltinSkills,
|
|
89
106
|
uninstallSkill,
|
|
90
107
|
updateSkill
|
|
91
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-TFS25FIM.js";
|
|
92
109
|
import {
|
|
93
110
|
activity,
|
|
94
111
|
conversationParticipants,
|
|
@@ -109,7 +126,7 @@ import {
|
|
|
109
126
|
exec,
|
|
110
127
|
gitExec,
|
|
111
128
|
resolveVoluteBin
|
|
112
|
-
} from "./chunk-
|
|
129
|
+
} from "./chunk-JTDFJWI2.js";
|
|
113
130
|
import {
|
|
114
131
|
chownMindDir,
|
|
115
132
|
createMindUser,
|
|
@@ -134,7 +151,6 @@ import {
|
|
|
134
151
|
addMind,
|
|
135
152
|
addVariant,
|
|
136
153
|
checkHealth,
|
|
137
|
-
daemonLoopback,
|
|
138
154
|
ensureVoluteHome,
|
|
139
155
|
findMind,
|
|
140
156
|
findVariant,
|
|
@@ -163,1178 +179,15 @@ import {
|
|
|
163
179
|
|
|
164
180
|
// src/daemon.ts
|
|
165
181
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
166
|
-
import { mkdirSync as
|
|
182
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
167
183
|
import { homedir as homedir2 } from "os";
|
|
168
|
-
import { resolve as
|
|
184
|
+
import { resolve as resolve17 } from "path";
|
|
169
185
|
import { format } from "util";
|
|
170
186
|
|
|
171
|
-
// src/lib/daemon/connector-manager.ts
|
|
172
|
-
import { spawn } from "child_process";
|
|
173
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
174
|
-
import { dirname, resolve as resolve2 } from "path";
|
|
175
|
-
|
|
176
|
-
// src/lib/connector-defs.ts
|
|
177
|
-
import { existsSync, readFileSync } from "fs";
|
|
178
|
-
import { resolve } from "path";
|
|
179
|
-
var BUILTIN_DEFS = {
|
|
180
|
-
discord: {
|
|
181
|
-
displayName: "Discord",
|
|
182
|
-
description: "Connect to Discord as a bot",
|
|
183
|
-
envVars: [
|
|
184
|
-
{
|
|
185
|
-
name: "DISCORD_TOKEN",
|
|
186
|
-
required: true,
|
|
187
|
-
description: "Discord bot token",
|
|
188
|
-
scope: "mind"
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
name: "DISCORD_GUILD_ID",
|
|
192
|
-
required: false,
|
|
193
|
-
description: "Discord server ID (optional, for slash commands)",
|
|
194
|
-
scope: "mind"
|
|
195
|
-
}
|
|
196
|
-
]
|
|
197
|
-
},
|
|
198
|
-
slack: {
|
|
199
|
-
displayName: "Slack",
|
|
200
|
-
description: "Connect to Slack via Socket Mode",
|
|
201
|
-
envVars: [
|
|
202
|
-
{
|
|
203
|
-
name: "SLACK_BOT_TOKEN",
|
|
204
|
-
required: true,
|
|
205
|
-
description: "Slack bot token (xoxb-...)",
|
|
206
|
-
scope: "mind"
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
name: "SLACK_APP_TOKEN",
|
|
210
|
-
required: true,
|
|
211
|
-
description: "Slack app-level token (xapp-...) for Socket Mode",
|
|
212
|
-
scope: "mind"
|
|
213
|
-
}
|
|
214
|
-
]
|
|
215
|
-
},
|
|
216
|
-
telegram: {
|
|
217
|
-
displayName: "Telegram",
|
|
218
|
-
description: "Connect to Telegram via long polling",
|
|
219
|
-
envVars: [
|
|
220
|
-
{
|
|
221
|
-
name: "TELEGRAM_BOT_TOKEN",
|
|
222
|
-
required: true,
|
|
223
|
-
description: "Telegram bot token from BotFather",
|
|
224
|
-
scope: "mind"
|
|
225
|
-
}
|
|
226
|
-
]
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
function getConnectorDef(type, connectorDir) {
|
|
230
|
-
if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
|
|
231
|
-
if (connectorDir) {
|
|
232
|
-
const jsonPath = resolve(connectorDir, "connector.json");
|
|
233
|
-
if (existsSync(jsonPath)) {
|
|
234
|
-
try {
|
|
235
|
-
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
236
|
-
} catch (err) {
|
|
237
|
-
console.warn(`Failed to parse ${jsonPath}: ${err}`);
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
function checkMissingEnvVars(def, env) {
|
|
245
|
-
return def.envVars.filter((v) => v.required && !env[v.name]);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/lib/daemon/connector-manager.ts
|
|
249
|
-
var clog = logger_default.child("connectors");
|
|
250
|
-
function searchUpwards(...segments) {
|
|
251
|
-
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
252
|
-
for (let i = 0; i < 5; i++) {
|
|
253
|
-
const candidate = resolve2(searchDir, ...segments);
|
|
254
|
-
if (existsSync2(candidate)) return candidate;
|
|
255
|
-
searchDir = dirname(searchDir);
|
|
256
|
-
}
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
var ConnectorManager = class {
|
|
260
|
-
connectors = /* @__PURE__ */ new Map();
|
|
261
|
-
stopping = /* @__PURE__ */ new Set();
|
|
262
|
-
// "mind:type" keys currently being explicitly stopped
|
|
263
|
-
shuttingDown = false;
|
|
264
|
-
restartTracker = new RestartTracker();
|
|
265
|
-
async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
|
|
266
|
-
const config = readVoluteConfig(mindDir2) ?? {};
|
|
267
|
-
const types = config.connectors ?? [];
|
|
268
|
-
await Promise.all(
|
|
269
|
-
types.map(
|
|
270
|
-
(type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
271
|
-
clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
272
|
-
})
|
|
273
|
-
)
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
checkConnectorEnv(type, mindName, mindDir2) {
|
|
277
|
-
const mindConnectorDir = resolve2(mindDir2, "connectors", type);
|
|
278
|
-
const userConnectorDir = resolve2(voluteHome(), "connectors", type);
|
|
279
|
-
const connectorDir = existsSync2(mindConnectorDir) ? mindConnectorDir : existsSync2(userConnectorDir) ? userConnectorDir : void 0;
|
|
280
|
-
const def = getConnectorDef(type, connectorDir);
|
|
281
|
-
if (!def) return null;
|
|
282
|
-
const env = loadMergedEnv(mindName);
|
|
283
|
-
const missing = checkMissingEnvVars(def, env);
|
|
284
|
-
if (missing.length === 0) return null;
|
|
285
|
-
return {
|
|
286
|
-
missing: missing.map((v) => ({ name: v.name, description: v.description })),
|
|
287
|
-
connectorName: def.displayName
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
async startConnector(mindName, mindDir2, mindPort, type, daemonPort) {
|
|
291
|
-
const existing = this.connectors.get(mindName)?.get(type);
|
|
292
|
-
if (existing) {
|
|
293
|
-
await new Promise((res) => {
|
|
294
|
-
existing.child.on("exit", () => res());
|
|
295
|
-
try {
|
|
296
|
-
if (existing.child.pid) {
|
|
297
|
-
process.kill(-existing.child.pid, "SIGTERM");
|
|
298
|
-
} else {
|
|
299
|
-
existing.child.kill("SIGTERM");
|
|
300
|
-
}
|
|
301
|
-
} catch {
|
|
302
|
-
res();
|
|
303
|
-
}
|
|
304
|
-
setTimeout(() => {
|
|
305
|
-
try {
|
|
306
|
-
if (existing.child.pid) {
|
|
307
|
-
process.kill(-existing.child.pid, "SIGKILL");
|
|
308
|
-
} else {
|
|
309
|
-
existing.child.kill("SIGKILL");
|
|
310
|
-
}
|
|
311
|
-
} catch {
|
|
312
|
-
}
|
|
313
|
-
res();
|
|
314
|
-
}, 3e3);
|
|
315
|
-
});
|
|
316
|
-
this.connectors.get(mindName)?.delete(type);
|
|
317
|
-
}
|
|
318
|
-
this.killOrphanConnector(mindName, type);
|
|
319
|
-
const mindConnector = resolve2(mindDir2, "connectors", type, "index.ts");
|
|
320
|
-
const userConnector = resolve2(voluteHome(), "connectors", type, "index.ts");
|
|
321
|
-
const builtinConnector = this.resolveBuiltinConnector(type);
|
|
322
|
-
let connectorScript;
|
|
323
|
-
let runtime;
|
|
324
|
-
if (existsSync2(mindConnector)) {
|
|
325
|
-
connectorScript = mindConnector;
|
|
326
|
-
runtime = resolve2(mindDir2, "node_modules", ".bin", "tsx");
|
|
327
|
-
} else if (existsSync2(userConnector)) {
|
|
328
|
-
connectorScript = userConnector;
|
|
329
|
-
runtime = this.resolveVoluteTsx();
|
|
330
|
-
} else if (builtinConnector) {
|
|
331
|
-
connectorScript = builtinConnector;
|
|
332
|
-
runtime = process.execPath;
|
|
333
|
-
} else {
|
|
334
|
-
throw new Error(`No connector code found for type: ${type}`);
|
|
335
|
-
}
|
|
336
|
-
const mindStateDir = stateDir(mindName);
|
|
337
|
-
const logsDir = resolve2(mindStateDir, "logs");
|
|
338
|
-
mkdirSync(logsDir, { recursive: true });
|
|
339
|
-
if (isIsolationEnabled()) {
|
|
340
|
-
try {
|
|
341
|
-
const [base] = mindName.split("@", 2);
|
|
342
|
-
chownMindDir(mindStateDir, base);
|
|
343
|
-
} catch (err) {
|
|
344
|
-
throw new Error(
|
|
345
|
-
`Cannot start connector ${type} for ${mindName}: failed to set ownership on state directory ${mindStateDir}: ${err instanceof Error ? err.message : err}`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
|
|
350
|
-
const mindEnv = loadMergedEnv(mindName);
|
|
351
|
-
const prefix = `${type.toUpperCase()}_`;
|
|
352
|
-
const connectorEnv = Object.fromEntries(
|
|
353
|
-
Object.entries(mindEnv).filter(([k]) => k.startsWith(prefix))
|
|
354
|
-
);
|
|
355
|
-
const spawnOpts = {
|
|
356
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
357
|
-
detached: true,
|
|
358
|
-
env: {
|
|
359
|
-
...process.env,
|
|
360
|
-
VOLUTE_MIND_PORT: String(mindPort),
|
|
361
|
-
VOLUTE_MIND_NAME: mindName,
|
|
362
|
-
VOLUTE_MIND_DIR: mindDir2,
|
|
363
|
-
...daemonPort ? {
|
|
364
|
-
VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
|
|
365
|
-
VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
|
|
366
|
-
} : {},
|
|
367
|
-
...connectorEnv
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
const [spawnCmd, spawnArgs] = wrapForIsolation(runtime, [connectorScript], mindName);
|
|
371
|
-
const child = spawn(spawnCmd, spawnArgs, spawnOpts);
|
|
372
|
-
let lastStderr = "";
|
|
373
|
-
child.stdout?.pipe(logStream);
|
|
374
|
-
child.stderr?.on("data", (chunk) => {
|
|
375
|
-
logStream.write(chunk);
|
|
376
|
-
lastStderr = chunk.toString().trim();
|
|
377
|
-
});
|
|
378
|
-
if (child.pid) {
|
|
379
|
-
this.saveConnectorPid(mindName, type, child.pid);
|
|
380
|
-
}
|
|
381
|
-
if (!this.connectors.has(mindName)) {
|
|
382
|
-
this.connectors.set(mindName, /* @__PURE__ */ new Map());
|
|
383
|
-
}
|
|
384
|
-
this.connectors.get(mindName).set(type, { child, type });
|
|
385
|
-
const stopKey = `${mindName}:${type}`;
|
|
386
|
-
this.restartTracker.reset(stopKey);
|
|
387
|
-
child.on("exit", (code) => {
|
|
388
|
-
const mindMap = this.connectors.get(mindName);
|
|
389
|
-
if (mindMap?.get(type)?.child === child) {
|
|
390
|
-
mindMap.delete(type);
|
|
391
|
-
}
|
|
392
|
-
if (this.shuttingDown) return;
|
|
393
|
-
if (this.stopping.has(stopKey)) return;
|
|
394
|
-
clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
|
|
395
|
-
if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
|
|
396
|
-
const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(stopKey);
|
|
397
|
-
if (!shouldRestart) {
|
|
398
|
-
clog.error(`connector ${type} for ${mindName} crashed ${attempt} times \u2014 giving up`);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
clog.info(
|
|
402
|
-
`restarting connector ${type} for ${mindName} \u2014 attempt ${attempt}/${this.restartTracker.maxRestartAttempts}, in ${delay}ms`
|
|
403
|
-
);
|
|
404
|
-
setTimeout(() => {
|
|
405
|
-
if (this.shuttingDown || this.stopping.has(stopKey)) return;
|
|
406
|
-
this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
407
|
-
clog.error(`failed to restart connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
408
|
-
});
|
|
409
|
-
}, delay);
|
|
410
|
-
});
|
|
411
|
-
clog.info(`started connector ${type} for ${mindName}`);
|
|
412
|
-
}
|
|
413
|
-
async stopConnector(mindName, type) {
|
|
414
|
-
const mindMap = this.connectors.get(mindName);
|
|
415
|
-
if (!mindMap) return;
|
|
416
|
-
const tracked = mindMap.get(type);
|
|
417
|
-
if (!tracked) return;
|
|
418
|
-
const stopKey = `${mindName}:${type}`;
|
|
419
|
-
this.stopping.add(stopKey);
|
|
420
|
-
mindMap.delete(type);
|
|
421
|
-
await new Promise((resolve23) => {
|
|
422
|
-
tracked.child.on("exit", () => resolve23());
|
|
423
|
-
try {
|
|
424
|
-
process.kill(-tracked.child.pid, "SIGTERM");
|
|
425
|
-
} catch {
|
|
426
|
-
resolve23();
|
|
427
|
-
}
|
|
428
|
-
setTimeout(() => {
|
|
429
|
-
try {
|
|
430
|
-
process.kill(-tracked.child.pid, "SIGKILL");
|
|
431
|
-
} catch {
|
|
432
|
-
}
|
|
433
|
-
resolve23();
|
|
434
|
-
}, 5e3);
|
|
435
|
-
});
|
|
436
|
-
this.stopping.delete(stopKey);
|
|
437
|
-
this.restartTracker.reset(stopKey);
|
|
438
|
-
try {
|
|
439
|
-
this.removeConnectorPid(mindName, type);
|
|
440
|
-
} catch (err) {
|
|
441
|
-
clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
|
|
442
|
-
}
|
|
443
|
-
clog.info(`stopped connector ${type} for ${mindName}`);
|
|
444
|
-
}
|
|
445
|
-
async stopConnectors(mindName) {
|
|
446
|
-
const mindMap = this.connectors.get(mindName);
|
|
447
|
-
if (!mindMap) return;
|
|
448
|
-
const types = [...mindMap.keys()];
|
|
449
|
-
await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
|
|
450
|
-
this.connectors.delete(mindName);
|
|
451
|
-
}
|
|
452
|
-
async stopAll() {
|
|
453
|
-
this.shuttingDown = true;
|
|
454
|
-
const minds = [...this.connectors.keys()];
|
|
455
|
-
await Promise.all(minds.map((name) => this.stopConnectors(name)));
|
|
456
|
-
}
|
|
457
|
-
getConnectorStatus(mindName) {
|
|
458
|
-
const mindMap = this.connectors.get(mindName);
|
|
459
|
-
if (!mindMap) return [];
|
|
460
|
-
return [...mindMap.entries()].map(([type, tracked]) => ({
|
|
461
|
-
type,
|
|
462
|
-
running: !tracked.child.killed
|
|
463
|
-
}));
|
|
464
|
-
}
|
|
465
|
-
connectorPidPath(mindName, type) {
|
|
466
|
-
return resolve2(stateDir(mindName), "connectors", `${type}.pid`);
|
|
467
|
-
}
|
|
468
|
-
saveConnectorPid(mindName, type, pid) {
|
|
469
|
-
const pidPath = this.connectorPidPath(mindName, type);
|
|
470
|
-
mkdirSync(dirname(pidPath), { recursive: true });
|
|
471
|
-
writeFileSync(pidPath, String(pid));
|
|
472
|
-
}
|
|
473
|
-
removeConnectorPid(mindName, type) {
|
|
474
|
-
try {
|
|
475
|
-
unlinkSync(this.connectorPidPath(mindName, type));
|
|
476
|
-
} catch {
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
killOrphanConnector(mindName, type) {
|
|
480
|
-
const pidPath = this.connectorPidPath(mindName, type);
|
|
481
|
-
if (!existsSync2(pidPath)) return;
|
|
482
|
-
try {
|
|
483
|
-
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
484
|
-
if (pid > 0) {
|
|
485
|
-
try {
|
|
486
|
-
process.kill(-pid, "SIGTERM");
|
|
487
|
-
} catch {
|
|
488
|
-
process.kill(pid, "SIGTERM");
|
|
489
|
-
}
|
|
490
|
-
clog.warn(`killed orphan connector ${type} (pid ${pid})`);
|
|
491
|
-
}
|
|
492
|
-
} catch {
|
|
493
|
-
}
|
|
494
|
-
try {
|
|
495
|
-
unlinkSync(pidPath);
|
|
496
|
-
} catch {
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
resolveBuiltinConnector(type) {
|
|
500
|
-
return searchUpwards("connectors", `${type}.js`);
|
|
501
|
-
}
|
|
502
|
-
resolveVoluteTsx() {
|
|
503
|
-
return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
var instance = null;
|
|
507
|
-
function initConnectorManager() {
|
|
508
|
-
if (instance) throw new Error("ConnectorManager already initialized");
|
|
509
|
-
instance = new ConnectorManager();
|
|
510
|
-
return instance;
|
|
511
|
-
}
|
|
512
|
-
function getConnectorManager() {
|
|
513
|
-
if (!instance)
|
|
514
|
-
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
515
|
-
return instance;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// src/lib/daemon/mail-poller.ts
|
|
519
|
-
var mlog = logger_default.child("mail");
|
|
520
|
-
function formatEmailContent(email) {
|
|
521
|
-
if (email.body) {
|
|
522
|
-
return email.subject ? `Subject: ${email.subject}
|
|
523
|
-
|
|
524
|
-
${email.body}` : email.body;
|
|
525
|
-
}
|
|
526
|
-
if (email.html) {
|
|
527
|
-
return email.subject ? `Subject: ${email.subject}
|
|
528
|
-
|
|
529
|
-
[HTML email \u2014 plain text not available]` : "[HTML email \u2014 plain text not available]";
|
|
530
|
-
}
|
|
531
|
-
return email.subject ? `Subject: ${email.subject}` : "[Empty email]";
|
|
532
|
-
}
|
|
533
|
-
var PING_INTERVAL_MS = 3e4;
|
|
534
|
-
var INITIAL_RECONNECT_MS = 1e3;
|
|
535
|
-
var MAX_RECONNECT_MS = 6e4;
|
|
536
|
-
var MailPoller = class {
|
|
537
|
-
ws = null;
|
|
538
|
-
running = false;
|
|
539
|
-
pingTimer = null;
|
|
540
|
-
reconnectTimer = null;
|
|
541
|
-
reconnectDelay = INITIAL_RECONNECT_MS;
|
|
542
|
-
reconnectAttempts = 0;
|
|
543
|
-
disconnectedAt = null;
|
|
544
|
-
config = null;
|
|
545
|
-
start() {
|
|
546
|
-
if (this.running) {
|
|
547
|
-
mlog.warn("already running \u2014 ignoring duplicate start");
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
this.config = readSystemsConfig();
|
|
551
|
-
if (!this.config) {
|
|
552
|
-
mlog.info("no systems config \u2014 mail disabled");
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
this.running = true;
|
|
556
|
-
this.connect();
|
|
557
|
-
}
|
|
558
|
-
stop() {
|
|
559
|
-
this.running = false;
|
|
560
|
-
this.config = null;
|
|
561
|
-
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
562
|
-
this.pingTimer = null;
|
|
563
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
564
|
-
this.reconnectTimer = null;
|
|
565
|
-
if (this.ws) {
|
|
566
|
-
this.ws.close();
|
|
567
|
-
this.ws = null;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
isRunning() {
|
|
571
|
-
return this.running;
|
|
572
|
-
}
|
|
573
|
-
connect() {
|
|
574
|
-
if (!this.running) return;
|
|
575
|
-
this.config = readSystemsConfig();
|
|
576
|
-
if (!this.config) {
|
|
577
|
-
mlog.info("systems config removed \u2014 stopping");
|
|
578
|
-
this.stop();
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
const wsUrl = `${this.config.apiUrl.replace(/^http/, "ws")}/api/ws`;
|
|
582
|
-
try {
|
|
583
|
-
this.ws = new WebSocket(wsUrl, {
|
|
584
|
-
headers: { Authorization: `Bearer ${this.config.apiKey}` }
|
|
585
|
-
});
|
|
586
|
-
} catch (err) {
|
|
587
|
-
mlog.warn("failed to create WebSocket", logger_default.errorData(err));
|
|
588
|
-
this.scheduleReconnect();
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
this.ws.onopen = () => {
|
|
592
|
-
if (this.reconnectAttempts > 0) {
|
|
593
|
-
mlog.info(`reconnected after ${this.reconnectAttempts} attempts`);
|
|
594
|
-
}
|
|
595
|
-
mlog.info("connected");
|
|
596
|
-
this.reconnectAttempts = 0;
|
|
597
|
-
this.reconnectDelay = INITIAL_RECONNECT_MS;
|
|
598
|
-
if (this.disconnectedAt) {
|
|
599
|
-
this.catchUp(this.disconnectedAt);
|
|
600
|
-
this.disconnectedAt = null;
|
|
601
|
-
}
|
|
602
|
-
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
603
|
-
this.pingTimer = setInterval(() => {
|
|
604
|
-
try {
|
|
605
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
606
|
-
this.ws.send("ping");
|
|
607
|
-
}
|
|
608
|
-
} catch (err) {
|
|
609
|
-
mlog.warn("ping failed", logger_default.errorData(err));
|
|
610
|
-
}
|
|
611
|
-
}, PING_INTERVAL_MS);
|
|
612
|
-
};
|
|
613
|
-
this.ws.onmessage = (event) => {
|
|
614
|
-
this.handleMessage(String(event.data));
|
|
615
|
-
};
|
|
616
|
-
this.ws.onclose = () => {
|
|
617
|
-
mlog.warn("disconnected");
|
|
618
|
-
if (!this.disconnectedAt) {
|
|
619
|
-
this.disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
620
|
-
}
|
|
621
|
-
this.cleanup();
|
|
622
|
-
this.scheduleReconnect();
|
|
623
|
-
};
|
|
624
|
-
this.ws.onerror = (err) => {
|
|
625
|
-
mlog.warn("WebSocket error", logger_default.errorData(err));
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
cleanup() {
|
|
629
|
-
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
630
|
-
this.pingTimer = null;
|
|
631
|
-
this.ws = null;
|
|
632
|
-
}
|
|
633
|
-
scheduleReconnect() {
|
|
634
|
-
if (!this.running) return;
|
|
635
|
-
this.reconnectAttempts++;
|
|
636
|
-
if (this.reconnectAttempts % 10 === 0) {
|
|
637
|
-
mlog.warn(
|
|
638
|
-
`failed to connect ${this.reconnectAttempts} times \u2014 check systems config and network`
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
mlog.info(`reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
|
|
642
|
-
this.reconnectTimer = setTimeout(() => {
|
|
643
|
-
this.reconnectTimer = null;
|
|
644
|
-
this.connect();
|
|
645
|
-
}, this.reconnectDelay);
|
|
646
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_MS);
|
|
647
|
-
}
|
|
648
|
-
/** Fetch emails that arrived while disconnected */
|
|
649
|
-
catchUp(since) {
|
|
650
|
-
if (!this.config) return;
|
|
651
|
-
const url = `${this.config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
|
|
652
|
-
fetch(url, {
|
|
653
|
-
headers: { Authorization: `Bearer ${this.config.apiKey}` }
|
|
654
|
-
}).then(async (res) => {
|
|
655
|
-
if (!res.ok) {
|
|
656
|
-
mlog.warn(`catch-up poll failed: HTTP ${res.status}`);
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
const data = await res.json();
|
|
660
|
-
if (!Array.isArray(data.emails) || data.emails.length === 0) return;
|
|
661
|
-
mlog.info(`catching up on ${data.emails.length} missed emails`);
|
|
662
|
-
for (const email of data.emails) {
|
|
663
|
-
await this.deliver(email.mind, email);
|
|
664
|
-
}
|
|
665
|
-
}).catch((err) => {
|
|
666
|
-
mlog.warn("catch-up error", logger_default.errorData(err));
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
handleMessage(data) {
|
|
670
|
-
if (data === "pong") return;
|
|
671
|
-
let msg;
|
|
672
|
-
try {
|
|
673
|
-
msg = JSON.parse(data);
|
|
674
|
-
} catch {
|
|
675
|
-
mlog.warn(`received unparseable message: ${data.slice(0, 200)}`);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (msg.type !== "email") return;
|
|
679
|
-
if (!msg.mind || !msg.email?.id) {
|
|
680
|
-
mlog.warn(`received malformed email notification: ${data.slice(0, 500)}`);
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
this.fetchAndDeliver(msg.mind, msg.email).catch((err) => {
|
|
684
|
-
mlog.warn(`failed to process email for ${msg.mind}`, logger_default.errorData(err));
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
async fetchAndDeliver(mind, notification) {
|
|
688
|
-
if (!this.config) {
|
|
689
|
-
mlog.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
const url = `${this.config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
|
|
693
|
-
const res = await fetch(url, {
|
|
694
|
-
headers: { Authorization: `Bearer ${this.config.apiKey}` }
|
|
695
|
-
});
|
|
696
|
-
if (!res.ok) {
|
|
697
|
-
mlog.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
const email = await res.json();
|
|
701
|
-
await this.deliver(mind, { ...email, mind });
|
|
702
|
-
}
|
|
703
|
-
async deliver(mind, email) {
|
|
704
|
-
const entry = findMind(mind);
|
|
705
|
-
if (!entry || !entry.running) {
|
|
706
|
-
mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
const text = formatEmailContent(email);
|
|
710
|
-
try {
|
|
711
|
-
await deliverMessage(mind, {
|
|
712
|
-
content: [{ type: "text", text }],
|
|
713
|
-
channel: `mail:${email.from.address}`,
|
|
714
|
-
sender: email.from.name || email.from.address,
|
|
715
|
-
platform: "Email",
|
|
716
|
-
isDM: true
|
|
717
|
-
});
|
|
718
|
-
mlog.info(`delivered email from ${email.from.address} to ${mind}`);
|
|
719
|
-
} catch (err) {
|
|
720
|
-
mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
var instance2 = null;
|
|
725
|
-
function initMailPoller() {
|
|
726
|
-
if (instance2) throw new Error("MailPoller already initialized");
|
|
727
|
-
instance2 = new MailPoller();
|
|
728
|
-
return instance2;
|
|
729
|
-
}
|
|
730
|
-
function getMailPoller() {
|
|
731
|
-
if (!instance2) throw new Error("MailPoller not initialized \u2014 call initMailPoller() first");
|
|
732
|
-
return instance2;
|
|
733
|
-
}
|
|
734
|
-
async function ensureMailAddress(mindName) {
|
|
735
|
-
const config = readSystemsConfig();
|
|
736
|
-
if (!config) return;
|
|
737
|
-
try {
|
|
738
|
-
const res = await fetch(`${config.apiUrl}/api/mail/addresses/${encodeURIComponent(mindName)}`, {
|
|
739
|
-
method: "PUT",
|
|
740
|
-
headers: {
|
|
741
|
-
Authorization: `Bearer ${config.apiKey}`,
|
|
742
|
-
"Content-Type": "application/json"
|
|
743
|
-
}
|
|
744
|
-
});
|
|
745
|
-
if (!res.ok) {
|
|
746
|
-
mlog.warn(`failed to ensure address for ${mindName}: HTTP ${res.status}`);
|
|
747
|
-
}
|
|
748
|
-
await res.text().catch(() => {
|
|
749
|
-
});
|
|
750
|
-
} catch (err) {
|
|
751
|
-
mlog.warn(`failed to ensure address for ${mindName}`, logger_default.errorData(err));
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// src/lib/pages-watcher.ts
|
|
756
|
-
import { existsSync as existsSync3, readdirSync, statSync, watch } from "fs";
|
|
757
|
-
import { join, resolve as resolve3 } from "path";
|
|
758
|
-
var watchers = /* @__PURE__ */ new Map();
|
|
759
|
-
var homeWatchers = /* @__PURE__ */ new Map();
|
|
760
|
-
var debounceTimers = /* @__PURE__ */ new Map();
|
|
761
|
-
var sitesCache = null;
|
|
762
|
-
var recentPagesCache = null;
|
|
763
|
-
function startPagesWatcher(mindName, pagesDir) {
|
|
764
|
-
try {
|
|
765
|
-
const watcher = watch(pagesDir, { recursive: true }, (_eventType, filename) => {
|
|
766
|
-
if (!filename || !filename.endsWith(".html")) return;
|
|
767
|
-
const key = `${mindName}:${filename}`;
|
|
768
|
-
const existing = debounceTimers.get(key);
|
|
769
|
-
if (existing) clearTimeout(existing);
|
|
770
|
-
debounceTimers.set(
|
|
771
|
-
key,
|
|
772
|
-
setTimeout(() => {
|
|
773
|
-
debounceTimers.delete(key);
|
|
774
|
-
invalidateCache();
|
|
775
|
-
publish2({
|
|
776
|
-
type: "page_updated",
|
|
777
|
-
mind: mindName,
|
|
778
|
-
summary: `${mindName} updated ${filename}`,
|
|
779
|
-
metadata: { file: filename }
|
|
780
|
-
}).catch(
|
|
781
|
-
(err) => logger_default.error("failed to publish page_updated activity", logger_default.errorData(err))
|
|
782
|
-
);
|
|
783
|
-
}, 100)
|
|
784
|
-
);
|
|
785
|
-
});
|
|
786
|
-
watchers.set(mindName, watcher);
|
|
787
|
-
} catch (err) {
|
|
788
|
-
logger_default.warn(`failed to start pages watcher for ${mindName}`, logger_default.errorData(err));
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
function startWatcher(mindName) {
|
|
792
|
-
if (watchers.has(mindName)) return;
|
|
793
|
-
const pagesDir = resolve3(mindDir(mindName), "home", "pages");
|
|
794
|
-
if (existsSync3(pagesDir)) {
|
|
795
|
-
startPagesWatcher(mindName, pagesDir);
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
if (homeWatchers.has(mindName)) return;
|
|
799
|
-
const homeDir = resolve3(mindDir(mindName), "home");
|
|
800
|
-
if (!existsSync3(homeDir)) return;
|
|
801
|
-
try {
|
|
802
|
-
const hw = watch(homeDir, (_eventType, filename) => {
|
|
803
|
-
if (filename !== "pages") return;
|
|
804
|
-
if (!existsSync3(pagesDir)) return;
|
|
805
|
-
hw.close();
|
|
806
|
-
homeWatchers.delete(mindName);
|
|
807
|
-
invalidateCache();
|
|
808
|
-
startPagesWatcher(mindName, pagesDir);
|
|
809
|
-
});
|
|
810
|
-
homeWatchers.set(mindName, hw);
|
|
811
|
-
} catch (err) {
|
|
812
|
-
logger_default.warn(`failed to start home watcher for ${mindName}`, logger_default.errorData(err));
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
function stopWatcher(mindName) {
|
|
816
|
-
const watcher = watchers.get(mindName);
|
|
817
|
-
if (watcher) {
|
|
818
|
-
watcher.close();
|
|
819
|
-
watchers.delete(mindName);
|
|
820
|
-
}
|
|
821
|
-
const hw = homeWatchers.get(mindName);
|
|
822
|
-
if (hw) {
|
|
823
|
-
hw.close();
|
|
824
|
-
homeWatchers.delete(mindName);
|
|
825
|
-
}
|
|
826
|
-
for (const [key, timer] of debounceTimers) {
|
|
827
|
-
if (key.startsWith(`${mindName}:`)) {
|
|
828
|
-
clearTimeout(timer);
|
|
829
|
-
debounceTimers.delete(key);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
function stopAllWatchers() {
|
|
834
|
-
for (const [, watcher] of watchers) {
|
|
835
|
-
watcher.close();
|
|
836
|
-
}
|
|
837
|
-
watchers.clear();
|
|
838
|
-
for (const [, hw] of homeWatchers) {
|
|
839
|
-
hw.close();
|
|
840
|
-
}
|
|
841
|
-
homeWatchers.clear();
|
|
842
|
-
for (const [, timer] of debounceTimers) {
|
|
843
|
-
clearTimeout(timer);
|
|
844
|
-
}
|
|
845
|
-
debounceTimers.clear();
|
|
846
|
-
invalidateCache();
|
|
847
|
-
}
|
|
848
|
-
function invalidateCache() {
|
|
849
|
-
sitesCache = null;
|
|
850
|
-
recentPagesCache = null;
|
|
851
|
-
}
|
|
852
|
-
function scanPagesDir(dir, urlPrefix) {
|
|
853
|
-
const pages = [];
|
|
854
|
-
let items;
|
|
855
|
-
try {
|
|
856
|
-
items = readdirSync(dir);
|
|
857
|
-
} catch {
|
|
858
|
-
return pages;
|
|
859
|
-
}
|
|
860
|
-
for (const item of items) {
|
|
861
|
-
if (item.startsWith(".")) continue;
|
|
862
|
-
const fullPath = resolve3(dir, item);
|
|
863
|
-
try {
|
|
864
|
-
const s = statSync(fullPath);
|
|
865
|
-
if (s.isFile() && item.endsWith(".html")) {
|
|
866
|
-
pages.push({
|
|
867
|
-
file: item,
|
|
868
|
-
modified: s.mtime.toISOString(),
|
|
869
|
-
url: `${urlPrefix}/${item}`
|
|
870
|
-
});
|
|
871
|
-
} else if (s.isDirectory()) {
|
|
872
|
-
const indexPath = resolve3(fullPath, "index.html");
|
|
873
|
-
if (existsSync3(indexPath)) {
|
|
874
|
-
const indexStat = statSync(indexPath);
|
|
875
|
-
pages.push({
|
|
876
|
-
file: join(item, "index.html"),
|
|
877
|
-
modified: indexStat.mtime.toISOString(),
|
|
878
|
-
url: `${urlPrefix}/${item}/`
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
} catch {
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
886
|
-
return pages;
|
|
887
|
-
}
|
|
888
|
-
function buildSites() {
|
|
889
|
-
const sites = [];
|
|
890
|
-
const systemPagesDir = resolve3(voluteHome(), "shared", "pages");
|
|
891
|
-
if (existsSync3(systemPagesDir)) {
|
|
892
|
-
const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
|
|
893
|
-
if (systemPages.length > 0) {
|
|
894
|
-
sites.push({ name: "_system", label: "System", pages: systemPages });
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
const entries = readRegistry();
|
|
898
|
-
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
899
|
-
const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
|
|
900
|
-
if (!existsSync3(pagesDir)) continue;
|
|
901
|
-
const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
|
|
902
|
-
if (mindPages.length > 0) {
|
|
903
|
-
sites.push({ name: entry.name, label: entry.name, pages: mindPages });
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
return sites;
|
|
907
|
-
}
|
|
908
|
-
function buildRecentPages() {
|
|
909
|
-
const entries = readRegistry();
|
|
910
|
-
const pages = [];
|
|
911
|
-
for (const entry of entries) {
|
|
912
|
-
const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
|
|
913
|
-
if (!existsSync3(pagesDir)) continue;
|
|
914
|
-
let items;
|
|
915
|
-
try {
|
|
916
|
-
items = readdirSync(pagesDir);
|
|
917
|
-
} catch {
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
for (const item of items) {
|
|
921
|
-
if (item.startsWith(".")) continue;
|
|
922
|
-
const fullPath = resolve3(pagesDir, item);
|
|
923
|
-
try {
|
|
924
|
-
const s = statSync(fullPath);
|
|
925
|
-
if (s.isFile() && item.endsWith(".html")) {
|
|
926
|
-
pages.push({
|
|
927
|
-
mind: entry.name,
|
|
928
|
-
file: item,
|
|
929
|
-
modified: s.mtime.toISOString(),
|
|
930
|
-
url: `/pages/${entry.name}/${item}`
|
|
931
|
-
});
|
|
932
|
-
} else if (s.isDirectory()) {
|
|
933
|
-
const indexPath = resolve3(fullPath, "index.html");
|
|
934
|
-
if (existsSync3(indexPath)) {
|
|
935
|
-
const indexStat = statSync(indexPath);
|
|
936
|
-
pages.push({
|
|
937
|
-
mind: entry.name,
|
|
938
|
-
file: join(item, "index.html"),
|
|
939
|
-
modified: indexStat.mtime.toISOString(),
|
|
940
|
-
url: `/pages/${entry.name}/${item}/`
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
} catch {
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
949
|
-
return pages.slice(0, 10);
|
|
950
|
-
}
|
|
951
|
-
function getCachedSites() {
|
|
952
|
-
if (!sitesCache) sitesCache = buildSites();
|
|
953
|
-
return sitesCache;
|
|
954
|
-
}
|
|
955
|
-
function getCachedRecentPages() {
|
|
956
|
-
if (!recentPagesCache) recentPagesCache = buildRecentPages();
|
|
957
|
-
return recentPagesCache;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// src/lib/daemon/scheduler.ts
|
|
961
|
-
import { resolve as resolve4 } from "path";
|
|
962
|
-
import { CronExpressionParser } from "cron-parser";
|
|
963
|
-
var slog = logger_default.child("scheduler");
|
|
964
|
-
var Scheduler = class {
|
|
965
|
-
schedules = /* @__PURE__ */ new Map();
|
|
966
|
-
interval = null;
|
|
967
|
-
lastFired = /* @__PURE__ */ new Map();
|
|
968
|
-
// "mind:scheduleId" → epoch minute
|
|
969
|
-
get statePath() {
|
|
970
|
-
return resolve4(voluteHome(), "scheduler-state.json");
|
|
971
|
-
}
|
|
972
|
-
start() {
|
|
973
|
-
this.loadState();
|
|
974
|
-
this.interval = setInterval(() => this.tick(), 6e4);
|
|
975
|
-
}
|
|
976
|
-
stop() {
|
|
977
|
-
if (this.interval) clearInterval(this.interval);
|
|
978
|
-
}
|
|
979
|
-
loadState() {
|
|
980
|
-
this.lastFired = loadJsonMap(this.statePath);
|
|
981
|
-
}
|
|
982
|
-
saveState() {
|
|
983
|
-
saveJsonMap(this.statePath, this.lastFired);
|
|
984
|
-
}
|
|
985
|
-
clearState() {
|
|
986
|
-
clearJsonMap(this.statePath, this.lastFired);
|
|
987
|
-
}
|
|
988
|
-
loadSchedules(mindName) {
|
|
989
|
-
const dir = mindDir(mindName);
|
|
990
|
-
const config = readVoluteConfig(dir);
|
|
991
|
-
if (!config) return;
|
|
992
|
-
const schedules = config.schedules ?? [];
|
|
993
|
-
if (schedules.length > 0) {
|
|
994
|
-
this.schedules.set(mindName, schedules);
|
|
995
|
-
} else {
|
|
996
|
-
this.schedules.delete(mindName);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
unloadSchedules(mindName) {
|
|
1000
|
-
this.schedules.delete(mindName);
|
|
1001
|
-
}
|
|
1002
|
-
tick() {
|
|
1003
|
-
const now = /* @__PURE__ */ new Date();
|
|
1004
|
-
const epochMinute = Math.floor(now.getTime() / 6e4);
|
|
1005
|
-
const cronCache = /* @__PURE__ */ new Map();
|
|
1006
|
-
let anyFired = false;
|
|
1007
|
-
for (const [mind, schedules] of this.schedules) {
|
|
1008
|
-
for (const schedule of schedules) {
|
|
1009
|
-
if (!schedule.enabled) continue;
|
|
1010
|
-
if (this.shouldFire(schedule, epochMinute, mind, cronCache)) {
|
|
1011
|
-
anyFired = true;
|
|
1012
|
-
this.fire(mind, schedule);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
if (anyFired) this.saveState();
|
|
1017
|
-
}
|
|
1018
|
-
shouldFire(schedule, epochMinute, mind, cronCache) {
|
|
1019
|
-
const key = `${mind}:${schedule.id}`;
|
|
1020
|
-
if (this.lastFired.get(key) === epochMinute) return false;
|
|
1021
|
-
let prevMinute = cronCache.get(schedule.cron);
|
|
1022
|
-
if (prevMinute === void 0) {
|
|
1023
|
-
try {
|
|
1024
|
-
const interval = CronExpressionParser.parse(schedule.cron);
|
|
1025
|
-
const prev = interval.prev().toDate();
|
|
1026
|
-
prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
1027
|
-
cronCache.set(schedule.cron, prevMinute);
|
|
1028
|
-
} catch (err) {
|
|
1029
|
-
slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
1030
|
-
return false;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
if (prevMinute === epochMinute) {
|
|
1034
|
-
this.lastFired.set(key, epochMinute);
|
|
1035
|
-
return true;
|
|
1036
|
-
}
|
|
1037
|
-
return false;
|
|
1038
|
-
}
|
|
1039
|
-
async fire(mindName, schedule) {
|
|
1040
|
-
try {
|
|
1041
|
-
let text;
|
|
1042
|
-
if (schedule.script) {
|
|
1043
|
-
const homeDir = resolve4(mindDir(mindName), "home");
|
|
1044
|
-
try {
|
|
1045
|
-
const output = await this.runScript(schedule.script, homeDir, mindName);
|
|
1046
|
-
if (!output.trim()) {
|
|
1047
|
-
slog.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
text = output;
|
|
1051
|
-
} catch (err) {
|
|
1052
|
-
const stderr = err.stderr ?? "";
|
|
1053
|
-
text = `[script error] ${err.message}${stderr ? `
|
|
1054
|
-
${stderr}` : ""}`;
|
|
1055
|
-
slog.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
|
|
1056
|
-
}
|
|
1057
|
-
} else if (schedule.message) {
|
|
1058
|
-
text = schedule.message;
|
|
1059
|
-
} else {
|
|
1060
|
-
slog.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
await this.deliver(mindName, {
|
|
1064
|
-
content: [{ type: "text", text }],
|
|
1065
|
-
channel: "system:scheduler",
|
|
1066
|
-
sender: schedule.id
|
|
1067
|
-
});
|
|
1068
|
-
slog.info(`fired "${schedule.id}" for ${mindName}`);
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
runScript(script, cwd, mindName) {
|
|
1074
|
-
return exec("bash", ["-c", script], { cwd, mindName });
|
|
1075
|
-
}
|
|
1076
|
-
deliver(mindName, payload) {
|
|
1077
|
-
return deliverMessage(mindName, payload);
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
var instance3 = null;
|
|
1081
|
-
function initScheduler() {
|
|
1082
|
-
if (instance3) throw new Error("Scheduler already initialized");
|
|
1083
|
-
instance3 = new Scheduler();
|
|
1084
|
-
return instance3;
|
|
1085
|
-
}
|
|
1086
|
-
function getScheduler() {
|
|
1087
|
-
if (!instance3) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
|
|
1088
|
-
return instance3;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// src/lib/daemon/token-budget.ts
|
|
1092
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1093
|
-
import { resolve as resolve5 } from "path";
|
|
1094
|
-
var tlog = logger_default.child("token-budget");
|
|
1095
|
-
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
1096
|
-
var MAX_QUEUE_SIZE = 100;
|
|
1097
|
-
var TokenBudget = class {
|
|
1098
|
-
budgets = /* @__PURE__ */ new Map();
|
|
1099
|
-
interval = null;
|
|
1100
|
-
dirty = /* @__PURE__ */ new Set();
|
|
1101
|
-
start() {
|
|
1102
|
-
this.interval = setInterval(() => this.tick(), 6e4);
|
|
1103
|
-
}
|
|
1104
|
-
stop() {
|
|
1105
|
-
this.flush();
|
|
1106
|
-
if (this.interval) clearInterval(this.interval);
|
|
1107
|
-
this.interval = null;
|
|
1108
|
-
}
|
|
1109
|
-
setBudget(mind, tokenLimit, periodMinutes) {
|
|
1110
|
-
if (tokenLimit <= 0) return;
|
|
1111
|
-
const existing = this.budgets.get(mind);
|
|
1112
|
-
if (existing) {
|
|
1113
|
-
existing.tokenLimit = tokenLimit;
|
|
1114
|
-
existing.periodMinutes = periodMinutes;
|
|
1115
|
-
} else {
|
|
1116
|
-
const persisted = this.loadBudgetState(mind);
|
|
1117
|
-
if (persisted) {
|
|
1118
|
-
persisted.tokenLimit = tokenLimit;
|
|
1119
|
-
persisted.periodMinutes = periodMinutes;
|
|
1120
|
-
this.budgets.set(mind, persisted);
|
|
1121
|
-
} else {
|
|
1122
|
-
this.budgets.set(mind, {
|
|
1123
|
-
tokensUsed: 0,
|
|
1124
|
-
periodStart: Date.now(),
|
|
1125
|
-
periodMinutes,
|
|
1126
|
-
tokenLimit,
|
|
1127
|
-
queue: [],
|
|
1128
|
-
warningInjected: false
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
removeBudget(mind) {
|
|
1134
|
-
this.budgets.delete(mind);
|
|
1135
|
-
}
|
|
1136
|
-
recordUsage(mind, inputTokens, outputTokens) {
|
|
1137
|
-
const state = this.budgets.get(mind);
|
|
1138
|
-
if (!state) return;
|
|
1139
|
-
state.tokensUsed += inputTokens + outputTokens;
|
|
1140
|
-
this.dirty.add(mind);
|
|
1141
|
-
}
|
|
1142
|
-
/** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
|
|
1143
|
-
checkBudget(mind) {
|
|
1144
|
-
const state = this.budgets.get(mind);
|
|
1145
|
-
if (!state) return "ok";
|
|
1146
|
-
const pct = state.tokensUsed / state.tokenLimit;
|
|
1147
|
-
if (pct >= 1) return "exceeded";
|
|
1148
|
-
if (pct >= 0.8 && !state.warningInjected) return "warning";
|
|
1149
|
-
return "ok";
|
|
1150
|
-
}
|
|
1151
|
-
/** Mark warning as delivered for this period. Call after successfully injecting the warning. */
|
|
1152
|
-
acknowledgeWarning(mind) {
|
|
1153
|
-
const state = this.budgets.get(mind);
|
|
1154
|
-
if (state) state.warningInjected = true;
|
|
1155
|
-
}
|
|
1156
|
-
enqueue(mind, message) {
|
|
1157
|
-
const state = this.budgets.get(mind);
|
|
1158
|
-
if (!state) return;
|
|
1159
|
-
if (state.queue.length >= MAX_QUEUE_SIZE) {
|
|
1160
|
-
state.queue.shift();
|
|
1161
|
-
}
|
|
1162
|
-
state.queue.push(message);
|
|
1163
|
-
}
|
|
1164
|
-
drain(mind) {
|
|
1165
|
-
const state = this.budgets.get(mind);
|
|
1166
|
-
if (!state) return [];
|
|
1167
|
-
const messages2 = state.queue;
|
|
1168
|
-
state.queue = [];
|
|
1169
|
-
return messages2;
|
|
1170
|
-
}
|
|
1171
|
-
getUsage(mind) {
|
|
1172
|
-
const state = this.budgets.get(mind);
|
|
1173
|
-
if (!state) return null;
|
|
1174
|
-
return {
|
|
1175
|
-
tokensUsed: state.tokensUsed,
|
|
1176
|
-
tokenLimit: state.tokenLimit,
|
|
1177
|
-
periodMinutes: state.periodMinutes,
|
|
1178
|
-
periodStart: state.periodStart,
|
|
1179
|
-
queueLength: state.queue.length,
|
|
1180
|
-
percentUsed: Math.round(state.tokensUsed / state.tokenLimit * 100)
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
tick() {
|
|
1184
|
-
const now = Date.now();
|
|
1185
|
-
for (const [mind, state] of this.budgets) {
|
|
1186
|
-
const elapsed = now - state.periodStart;
|
|
1187
|
-
if (elapsed >= state.periodMinutes * 6e4) {
|
|
1188
|
-
state.tokensUsed = 0;
|
|
1189
|
-
state.periodStart = now;
|
|
1190
|
-
state.warningInjected = false;
|
|
1191
|
-
this.dirty.add(mind);
|
|
1192
|
-
const queued = this.drain(mind);
|
|
1193
|
-
if (queued.length > 0) {
|
|
1194
|
-
this.replay(mind, queued).catch((err) => {
|
|
1195
|
-
tlog.warn(`replay error for ${mind}`, logger_default.errorData(err));
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
this.flush();
|
|
1201
|
-
}
|
|
1202
|
-
/** Flush all dirty budget states to disk. */
|
|
1203
|
-
flush() {
|
|
1204
|
-
for (const mind of this.dirty) {
|
|
1205
|
-
const state = this.budgets.get(mind);
|
|
1206
|
-
if (state) this.saveBudgetState(mind, state);
|
|
1207
|
-
}
|
|
1208
|
-
this.dirty.clear();
|
|
1209
|
-
}
|
|
1210
|
-
budgetStatePath(mind) {
|
|
1211
|
-
return resolve5(stateDir(mind), "budget.json");
|
|
1212
|
-
}
|
|
1213
|
-
saveBudgetState(mind, state) {
|
|
1214
|
-
try {
|
|
1215
|
-
const dir = stateDir(mind);
|
|
1216
|
-
mkdirSync2(dir, { recursive: true });
|
|
1217
|
-
const data = {
|
|
1218
|
-
periodStart: state.periodStart,
|
|
1219
|
-
tokensUsed: state.tokensUsed,
|
|
1220
|
-
warningInjected: state.warningInjected,
|
|
1221
|
-
queue: state.queue
|
|
1222
|
-
};
|
|
1223
|
-
writeFileSync2(this.budgetStatePath(mind), `${JSON.stringify(data)}
|
|
1224
|
-
`);
|
|
1225
|
-
} catch (err) {
|
|
1226
|
-
tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
loadBudgetState(mind) {
|
|
1230
|
-
try {
|
|
1231
|
-
const path = this.budgetStatePath(mind);
|
|
1232
|
-
if (!existsSync4(path)) return null;
|
|
1233
|
-
const data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1234
|
-
if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
|
|
1235
|
-
return {
|
|
1236
|
-
periodStart: data.periodStart,
|
|
1237
|
-
tokensUsed: data.tokensUsed,
|
|
1238
|
-
warningInjected: data.warningInjected ?? false,
|
|
1239
|
-
queue: Array.isArray(data.queue) ? data.queue : [],
|
|
1240
|
-
periodMinutes: 0,
|
|
1241
|
-
// will be overwritten by caller
|
|
1242
|
-
tokenLimit: 0
|
|
1243
|
-
// will be overwritten by caller
|
|
1244
|
-
};
|
|
1245
|
-
} catch (err) {
|
|
1246
|
-
tlog.warn(`failed to load budget state for ${mind}`, logger_default.errorData(err));
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
async replay(mindName, messages2) {
|
|
1251
|
-
const summary = messages2.map((m) => {
|
|
1252
|
-
const from = m.sender ? `[${m.sender}]` : "";
|
|
1253
|
-
const ch = m.channel ? `(${m.channel})` : "";
|
|
1254
|
-
return `${from}${ch} ${m.textContent}`;
|
|
1255
|
-
}).join("\n");
|
|
1256
|
-
try {
|
|
1257
|
-
await deliverMessage(mindName, {
|
|
1258
|
-
content: [
|
|
1259
|
-
{
|
|
1260
|
-
type: "text",
|
|
1261
|
-
text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
|
|
1262
|
-
|
|
1263
|
-
${summary}`
|
|
1264
|
-
}
|
|
1265
|
-
],
|
|
1266
|
-
channel: "system:budget-replay",
|
|
1267
|
-
sender: "system"
|
|
1268
|
-
});
|
|
1269
|
-
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
1270
|
-
} catch (err) {
|
|
1271
|
-
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
1272
|
-
const state = this.budgets.get(mindName);
|
|
1273
|
-
if (state) state.queue.push(...messages2);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
var instance4 = null;
|
|
1278
|
-
function initTokenBudget() {
|
|
1279
|
-
if (instance4) throw new Error("TokenBudget already initialized");
|
|
1280
|
-
instance4 = new TokenBudget();
|
|
1281
|
-
return instance4;
|
|
1282
|
-
}
|
|
1283
|
-
function getTokenBudget() {
|
|
1284
|
-
if (!instance4) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
|
|
1285
|
-
return instance4;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// src/lib/daemon/mind-service.ts
|
|
1289
|
-
async function startMindFull(name) {
|
|
1290
|
-
const [baseName, variantName] = name.split("@", 2);
|
|
1291
|
-
await getMindManager().startMind(name);
|
|
1292
|
-
publish2({
|
|
1293
|
-
type: "mind_started",
|
|
1294
|
-
mind: name,
|
|
1295
|
-
summary: `${name} started`
|
|
1296
|
-
}).catch((err) => logger_default.error("failed to publish mind_started activity", logger_default.errorData(err)));
|
|
1297
|
-
if (variantName) return;
|
|
1298
|
-
const entry = findMind(baseName);
|
|
1299
|
-
if (!entry || entry.stage === "seed") return;
|
|
1300
|
-
const dir = mindDir(baseName);
|
|
1301
|
-
const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
|
|
1302
|
-
await getConnectorManager().startConnectors(baseName, dir, entry.port, daemonPort);
|
|
1303
|
-
getScheduler().loadSchedules(baseName);
|
|
1304
|
-
ensureMailAddress(baseName).catch(
|
|
1305
|
-
(err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
|
|
1306
|
-
);
|
|
1307
|
-
const config = readVoluteConfig(dir);
|
|
1308
|
-
if (config?.tokenBudget) {
|
|
1309
|
-
getTokenBudget().setBudget(
|
|
1310
|
-
baseName,
|
|
1311
|
-
config.tokenBudget,
|
|
1312
|
-
config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
|
|
1313
|
-
);
|
|
1314
|
-
}
|
|
1315
|
-
startWatcher(baseName);
|
|
1316
|
-
}
|
|
1317
|
-
async function stopMindFull(name) {
|
|
1318
|
-
const [baseName, variantName] = name.split("@", 2);
|
|
1319
|
-
if (!variantName) {
|
|
1320
|
-
stopWatcher(baseName);
|
|
1321
|
-
markIdle(baseName);
|
|
1322
|
-
await getConnectorManager().stopConnectors(baseName);
|
|
1323
|
-
getScheduler().unloadSchedules(baseName);
|
|
1324
|
-
getTokenBudget().removeBudget(baseName);
|
|
1325
|
-
}
|
|
1326
|
-
await getMindManager().stopMind(name);
|
|
1327
|
-
publish2({
|
|
1328
|
-
type: "mind_stopped",
|
|
1329
|
-
mind: name,
|
|
1330
|
-
summary: `${name} stopped`
|
|
1331
|
-
}).catch((err) => logger_default.error("failed to publish mind_stopped activity", logger_default.errorData(err)));
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
187
|
// src/lib/migrate-agents-to-minds.ts
|
|
1335
188
|
import { execFileSync } from "child_process";
|
|
1336
|
-
import { existsSync
|
|
1337
|
-
import { resolve
|
|
189
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
190
|
+
import { resolve } from "path";
|
|
1338
191
|
var TAG = "[migrate]";
|
|
1339
192
|
function log(msg) {
|
|
1340
193
|
console.error(`${TAG} ${msg}`);
|
|
@@ -1355,19 +208,19 @@ function bridgeEnvVar() {
|
|
|
1355
208
|
}
|
|
1356
209
|
}
|
|
1357
210
|
function migrateRegistry(home) {
|
|
1358
|
-
const oldPath =
|
|
1359
|
-
const newPath =
|
|
1360
|
-
if (!
|
|
211
|
+
const oldPath = resolve(home, "agents.json");
|
|
212
|
+
const newPath = resolve(home, "minds.json");
|
|
213
|
+
if (!existsSync(oldPath) || existsSync(newPath)) {
|
|
1361
214
|
return readNamesFromRegistry(newPath);
|
|
1362
215
|
}
|
|
1363
|
-
const raw =
|
|
216
|
+
const raw = readFileSync(oldPath, "utf-8");
|
|
1364
217
|
const entries = JSON.parse(raw);
|
|
1365
218
|
for (const entry of entries) {
|
|
1366
219
|
if (entry.stage === "mind") {
|
|
1367
220
|
entry.stage = "sprouted";
|
|
1368
221
|
}
|
|
1369
222
|
}
|
|
1370
|
-
|
|
223
|
+
writeFileSync(newPath, `${JSON.stringify(entries, null, 2)}
|
|
1371
224
|
`);
|
|
1372
225
|
try {
|
|
1373
226
|
renameSync(oldPath, `${oldPath}.bak`);
|
|
@@ -1377,9 +230,9 @@ function migrateRegistry(home) {
|
|
|
1377
230
|
return entries.map((e) => e.name);
|
|
1378
231
|
}
|
|
1379
232
|
function readNamesFromRegistry(path) {
|
|
1380
|
-
if (!
|
|
233
|
+
if (!existsSync(path)) return [];
|
|
1381
234
|
try {
|
|
1382
|
-
const entries = JSON.parse(
|
|
235
|
+
const entries = JSON.parse(readFileSync(path, "utf-8"));
|
|
1383
236
|
return entries.map((e) => e.name);
|
|
1384
237
|
} catch {
|
|
1385
238
|
return [];
|
|
@@ -1387,9 +240,9 @@ function readNamesFromRegistry(path) {
|
|
|
1387
240
|
}
|
|
1388
241
|
function migrateMindsDirectory(home) {
|
|
1389
242
|
if (process.env.VOLUTE_MINDS_DIR) return;
|
|
1390
|
-
const oldDir =
|
|
1391
|
-
const newDir =
|
|
1392
|
-
if (
|
|
243
|
+
const oldDir = resolve(home, "agents");
|
|
244
|
+
const newDir = resolve(home, "minds");
|
|
245
|
+
if (existsSync(oldDir) && !existsSync(newDir)) {
|
|
1393
246
|
try {
|
|
1394
247
|
renameSync(oldDir, newDir);
|
|
1395
248
|
log("renamed agents/ \u2192 minds/");
|
|
@@ -1400,10 +253,10 @@ function migrateMindsDirectory(home) {
|
|
|
1400
253
|
}
|
|
1401
254
|
function migrateLogFiles(home, names) {
|
|
1402
255
|
for (const name of names) {
|
|
1403
|
-
const logsDir =
|
|
1404
|
-
const oldLog =
|
|
1405
|
-
const newLog =
|
|
1406
|
-
if (
|
|
256
|
+
const logsDir = resolve(home, "state", name, "logs");
|
|
257
|
+
const oldLog = resolve(logsDir, "agent.log");
|
|
258
|
+
const newLog = resolve(logsDir, "mind.log");
|
|
259
|
+
if (existsSync(oldLog) && !existsSync(newLog)) {
|
|
1407
260
|
try {
|
|
1408
261
|
renameSync(oldLog, newLog);
|
|
1409
262
|
log(`renamed ${name} agent.log \u2192 mind.log`);
|
|
@@ -1453,12 +306,12 @@ function migrateLinuxUsers(names) {
|
|
|
1453
306
|
}
|
|
1454
307
|
function migrateProfileScript() {
|
|
1455
308
|
const profilePath = "/etc/profile.d/volute.sh";
|
|
1456
|
-
if (!
|
|
309
|
+
if (!existsSync(profilePath)) return;
|
|
1457
310
|
try {
|
|
1458
|
-
const content =
|
|
311
|
+
const content = readFileSync(profilePath, "utf-8");
|
|
1459
312
|
if (!content.includes("VOLUTE_AGENTS_DIR")) return;
|
|
1460
313
|
const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
|
|
1461
|
-
|
|
314
|
+
writeFileSync(profilePath, updated);
|
|
1462
315
|
log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
|
|
1463
316
|
} catch (err) {
|
|
1464
317
|
log(`failed to update profile script: ${err}`);
|
|
@@ -1466,37 +319,37 @@ function migrateProfileScript() {
|
|
|
1466
319
|
}
|
|
1467
320
|
|
|
1468
321
|
// src/lib/migrate-state.ts
|
|
1469
|
-
import { copyFileSync, existsSync as
|
|
1470
|
-
import { resolve as
|
|
322
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, renameSync as renameSync2 } from "fs";
|
|
323
|
+
import { resolve as resolve2 } from "path";
|
|
1471
324
|
function migrateDotVoluteDir(name) {
|
|
1472
325
|
const dir = mindDir(name);
|
|
1473
|
-
const oldDir =
|
|
1474
|
-
const newDir =
|
|
1475
|
-
if (
|
|
326
|
+
const oldDir = resolve2(dir, ".volute");
|
|
327
|
+
const newDir = resolve2(dir, ".mind");
|
|
328
|
+
if (existsSync2(oldDir) && !existsSync2(newDir)) {
|
|
1476
329
|
renameSync2(oldDir, newDir);
|
|
1477
|
-
} else if (
|
|
330
|
+
} else if (existsSync2(oldDir) && existsSync2(newDir)) {
|
|
1478
331
|
console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
|
|
1479
332
|
}
|
|
1480
333
|
}
|
|
1481
334
|
function migrateMindState(name) {
|
|
1482
|
-
const src =
|
|
1483
|
-
if (!
|
|
335
|
+
const src = resolve2(mindDir(name), ".mind");
|
|
336
|
+
if (!existsSync2(src)) return;
|
|
1484
337
|
const dest = stateDir(name);
|
|
1485
|
-
|
|
338
|
+
mkdirSync(dest, { recursive: true });
|
|
1486
339
|
for (const file of ["env.json", "channels.json"]) {
|
|
1487
|
-
const srcPath =
|
|
1488
|
-
const destPath =
|
|
1489
|
-
if (
|
|
340
|
+
const srcPath = resolve2(src, file);
|
|
341
|
+
const destPath = resolve2(dest, file);
|
|
342
|
+
if (existsSync2(srcPath) && !existsSync2(destPath)) {
|
|
1490
343
|
copyFileSync(srcPath, destPath);
|
|
1491
344
|
}
|
|
1492
345
|
}
|
|
1493
|
-
const srcLogs =
|
|
1494
|
-
const destLogs =
|
|
1495
|
-
if (
|
|
1496
|
-
|
|
1497
|
-
for (const file of
|
|
346
|
+
const srcLogs = resolve2(src, "logs");
|
|
347
|
+
const destLogs = resolve2(dest, "logs");
|
|
348
|
+
if (existsSync2(srcLogs) && !existsSync2(destLogs)) {
|
|
349
|
+
mkdirSync(destLogs, { recursive: true });
|
|
350
|
+
for (const file of readdirSync(srcLogs)) {
|
|
1498
351
|
try {
|
|
1499
|
-
copyFileSync(
|
|
352
|
+
copyFileSync(resolve2(srcLogs, file), resolve2(destLogs, file));
|
|
1500
353
|
} catch (err) {
|
|
1501
354
|
console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
|
|
1502
355
|
}
|
|
@@ -1722,13 +575,13 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
1722
575
|
});
|
|
1723
576
|
|
|
1724
577
|
// src/web/server.ts
|
|
1725
|
-
import { existsSync as
|
|
578
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1726
579
|
import { readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
1727
|
-
import { dirname
|
|
580
|
+
import { dirname, extname as extname3, resolve as resolve16 } from "path";
|
|
1728
581
|
import { serve } from "@hono/node-server";
|
|
1729
582
|
|
|
1730
583
|
// src/web/app.ts
|
|
1731
|
-
import { Hono as
|
|
584
|
+
import { Hono as Hono28 } from "hono";
|
|
1732
585
|
import { bodyLimit } from "hono/body-limit";
|
|
1733
586
|
import { csrf } from "hono/csrf";
|
|
1734
587
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -1740,7 +593,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
1740
593
|
|
|
1741
594
|
// src/lib/events/conversations.ts
|
|
1742
595
|
import { randomUUID } from "crypto";
|
|
1743
|
-
import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
|
|
596
|
+
import { and as and2, desc, eq as eq3, inArray, isNull, lt as lt2, sql } from "drizzle-orm";
|
|
1744
597
|
async function createConversation(mindName, channel, opts) {
|
|
1745
598
|
const db = await getDb();
|
|
1746
599
|
const id = randomUUID();
|
|
@@ -1766,6 +619,11 @@ async function createConversation(mindName, channel, opts) {
|
|
|
1766
619
|
);
|
|
1767
620
|
}
|
|
1768
621
|
});
|
|
622
|
+
fireWebhook({
|
|
623
|
+
event: "conversation_created",
|
|
624
|
+
mind: mindName ?? "",
|
|
625
|
+
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
626
|
+
});
|
|
1769
627
|
return {
|
|
1770
628
|
id,
|
|
1771
629
|
mind_name: mindName,
|
|
@@ -1858,7 +716,7 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
1858
716
|
content,
|
|
1859
717
|
created_at: result.created_at
|
|
1860
718
|
};
|
|
1861
|
-
|
|
719
|
+
publish2(conversationId, {
|
|
1862
720
|
type: "message",
|
|
1863
721
|
id: msg.id,
|
|
1864
722
|
role: msg.role,
|
|
@@ -1866,21 +724,50 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
1866
724
|
content: msg.content,
|
|
1867
725
|
createdAt: msg.created_at
|
|
1868
726
|
});
|
|
727
|
+
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq3(conversations.id, conversationId)).get();
|
|
728
|
+
fireWebhook({
|
|
729
|
+
event: "message_created",
|
|
730
|
+
mind: conv?.mind_name ?? "",
|
|
731
|
+
data: {
|
|
732
|
+
conversationId,
|
|
733
|
+
messageId: result.id,
|
|
734
|
+
role,
|
|
735
|
+
senderName,
|
|
736
|
+
content: content.filter((b) => b.type !== "image"),
|
|
737
|
+
createdAt: result.created_at
|
|
738
|
+
}
|
|
739
|
+
});
|
|
1869
740
|
return msg;
|
|
1870
741
|
}
|
|
1871
742
|
async function getMessages(conversationId) {
|
|
1872
743
|
const db = await getDb();
|
|
1873
744
|
const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1874
|
-
return rows.map(
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
745
|
+
return rows.map(parseMessageRow);
|
|
746
|
+
}
|
|
747
|
+
async function getMessagesPaginated(conversationId, opts) {
|
|
748
|
+
const db = await getDb();
|
|
749
|
+
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
750
|
+
const conditions = [eq3(messages.conversation_id, conversationId)];
|
|
751
|
+
if (opts?.before != null) {
|
|
752
|
+
conditions.push(lt2(messages.id, opts.before));
|
|
753
|
+
}
|
|
754
|
+
const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
|
|
755
|
+
const hasMore = rows.length > limit;
|
|
756
|
+
const page = rows.slice(0, limit).reverse();
|
|
757
|
+
return {
|
|
758
|
+
messages: page.map(parseMessageRow),
|
|
759
|
+
hasMore
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function parseMessageRow(row) {
|
|
763
|
+
let content;
|
|
764
|
+
try {
|
|
765
|
+
const parsed = JSON.parse(row.content);
|
|
766
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
767
|
+
} catch {
|
|
768
|
+
content = [{ type: "text", text: row.content }];
|
|
769
|
+
}
|
|
770
|
+
return { ...row, role: row.role, content };
|
|
1884
771
|
}
|
|
1885
772
|
async function listConversationsWithParticipants(userId) {
|
|
1886
773
|
const convs = await listConversationsForUser(userId);
|
|
@@ -2022,7 +909,7 @@ var app = new Hono().get("/events", async (c) => {
|
|
|
2022
909
|
activeMinds: getActiveMinds()
|
|
2023
910
|
})
|
|
2024
911
|
});
|
|
2025
|
-
const unsubActivity =
|
|
912
|
+
const unsubActivity = subscribe((event) => {
|
|
2026
913
|
stream.writeSSE({
|
|
2027
914
|
data: JSON.stringify({ event: "activity", ...event })
|
|
2028
915
|
}).catch((err) => {
|
|
@@ -2031,7 +918,7 @@ var app = new Hono().get("/events", async (c) => {
|
|
|
2031
918
|
});
|
|
2032
919
|
cleanups.push(unsubActivity);
|
|
2033
920
|
for (const conv of conversations2) {
|
|
2034
|
-
const unsubConv =
|
|
921
|
+
const unsubConv = subscribe3(conv.id, (event) => {
|
|
2035
922
|
stream.writeSSE({
|
|
2036
923
|
data: JSON.stringify({ event: "conversation", conversationId: conv.id, ...event })
|
|
2037
924
|
}).catch((err) => {
|
|
@@ -2046,8 +933,8 @@ var app = new Hono().get("/events", async (c) => {
|
|
|
2046
933
|
});
|
|
2047
934
|
}, 15e3);
|
|
2048
935
|
cleanups.push(() => clearInterval(keepAlive));
|
|
2049
|
-
await new Promise((
|
|
2050
|
-
stream.onAbort(() =>
|
|
936
|
+
await new Promise((resolve18) => {
|
|
937
|
+
stream.onAbort(() => resolve18());
|
|
2051
938
|
});
|
|
2052
939
|
} finally {
|
|
2053
940
|
for (const cleanup of cleanups) {
|
|
@@ -2609,16 +1496,16 @@ __export(volute_exports, {
|
|
|
2609
1496
|
read: () => read4,
|
|
2610
1497
|
send: () => send4
|
|
2611
1498
|
});
|
|
2612
|
-
import { existsSync as
|
|
2613
|
-
import { resolve as
|
|
1499
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1500
|
+
import { resolve as resolve3 } from "path";
|
|
2614
1501
|
function getDaemonConfig() {
|
|
2615
|
-
const configPath2 =
|
|
2616
|
-
if (!
|
|
1502
|
+
const configPath2 = resolve3(voluteHome(), "daemon.json");
|
|
1503
|
+
if (!existsSync3(configPath2)) {
|
|
2617
1504
|
throw new Error("Volute daemon is not running");
|
|
2618
1505
|
}
|
|
2619
1506
|
let config;
|
|
2620
1507
|
try {
|
|
2621
|
-
config = JSON.parse(
|
|
1508
|
+
config = JSON.parse(readFileSync2(configPath2, "utf-8"));
|
|
2622
1509
|
} catch (err) {
|
|
2623
1510
|
throw new Error(`Failed to parse ${configPath2}: ${err}`);
|
|
2624
1511
|
}
|
|
@@ -3055,14 +1942,14 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
|
|
|
3055
1942
|
var env_default = app5;
|
|
3056
1943
|
|
|
3057
1944
|
// src/web/api/file-sharing.ts
|
|
3058
|
-
import { readFileSync as
|
|
3059
|
-
import { resolve as
|
|
1945
|
+
import { readFileSync as readFileSync4, statSync } from "fs";
|
|
1946
|
+
import { resolve as resolve5 } from "path";
|
|
3060
1947
|
import { Hono as Hono6 } from "hono";
|
|
3061
1948
|
|
|
3062
1949
|
// src/lib/file-sharing.ts
|
|
3063
1950
|
import { randomBytes } from "crypto";
|
|
3064
|
-
import { existsSync as
|
|
3065
|
-
import { basename, join
|
|
1951
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1952
|
+
import { basename, join, normalize, resolve as resolve4 } from "path";
|
|
3066
1953
|
function validateFilePath(filePath) {
|
|
3067
1954
|
if (!filePath) return "File path is required";
|
|
3068
1955
|
const normalized = normalize(filePath);
|
|
@@ -3075,13 +1962,13 @@ function validateFilePath(filePath) {
|
|
|
3075
1962
|
return null;
|
|
3076
1963
|
}
|
|
3077
1964
|
function configPath(dir) {
|
|
3078
|
-
return
|
|
1965
|
+
return resolve4(dir, "home", ".config", "file-sharing.json");
|
|
3079
1966
|
}
|
|
3080
1967
|
function readFileSharingConfig(dir) {
|
|
3081
1968
|
const p = configPath(dir);
|
|
3082
|
-
if (!
|
|
1969
|
+
if (!existsSync4(p)) return {};
|
|
3083
1970
|
try {
|
|
3084
|
-
return JSON.parse(
|
|
1971
|
+
return JSON.parse(readFileSync3(p, "utf-8"));
|
|
3085
1972
|
} catch (err) {
|
|
3086
1973
|
console.warn(`[file-sharing] failed to parse config at ${p}:`, err);
|
|
3087
1974
|
return {};
|
|
@@ -3089,8 +1976,8 @@ function readFileSharingConfig(dir) {
|
|
|
3089
1976
|
}
|
|
3090
1977
|
function writeFileSharingConfig(dir, config) {
|
|
3091
1978
|
const p = configPath(dir);
|
|
3092
|
-
|
|
3093
|
-
|
|
1979
|
+
mkdirSync2(resolve4(p, ".."), { recursive: true });
|
|
1980
|
+
writeFileSync2(p, `${JSON.stringify(config, null, 2)}
|
|
3094
1981
|
`);
|
|
3095
1982
|
}
|
|
3096
1983
|
function isTrustedSender(dir, sender) {
|
|
@@ -3113,7 +2000,7 @@ function removeTrust(dir, sender) {
|
|
|
3113
2000
|
writeFileSharingConfig(dir, config);
|
|
3114
2001
|
}
|
|
3115
2002
|
function pendingDir(receiver) {
|
|
3116
|
-
return
|
|
2003
|
+
return resolve4(stateDir(receiver), "pending-files");
|
|
3117
2004
|
}
|
|
3118
2005
|
function validateId(id) {
|
|
3119
2006
|
if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
@@ -3132,8 +2019,8 @@ function stageFile(receiver, sender, filename, content, originalPath) {
|
|
|
3132
2019
|
throw new Error("Invalid sender name");
|
|
3133
2020
|
}
|
|
3134
2021
|
const id = generateId(sender);
|
|
3135
|
-
const dir =
|
|
3136
|
-
|
|
2022
|
+
const dir = resolve4(pendingDir(receiver), id);
|
|
2023
|
+
mkdirSync2(dir, { recursive: true });
|
|
3137
2024
|
const metadata = {
|
|
3138
2025
|
id,
|
|
3139
2026
|
sender,
|
|
@@ -3142,22 +2029,22 @@ function stageFile(receiver, sender, filename, content, originalPath) {
|
|
|
3142
2029
|
size: content.length,
|
|
3143
2030
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3144
2031
|
};
|
|
3145
|
-
|
|
2032
|
+
writeFileSync2(resolve4(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
|
|
3146
2033
|
`);
|
|
3147
|
-
|
|
2034
|
+
writeFileSync2(resolve4(dir, "data"), content);
|
|
3148
2035
|
return { id };
|
|
3149
2036
|
}
|
|
3150
2037
|
function listPending(receiver) {
|
|
3151
2038
|
const dir = pendingDir(receiver);
|
|
3152
|
-
if (!
|
|
3153
|
-
const entries =
|
|
2039
|
+
if (!existsSync4(dir)) return [];
|
|
2040
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
3154
2041
|
const result = [];
|
|
3155
2042
|
for (const entry of entries) {
|
|
3156
2043
|
if (!entry.isDirectory()) continue;
|
|
3157
|
-
const metaPath =
|
|
3158
|
-
if (!
|
|
2044
|
+
const metaPath = resolve4(dir, entry.name, "metadata.json");
|
|
2045
|
+
if (!existsSync4(metaPath)) continue;
|
|
3159
2046
|
try {
|
|
3160
|
-
result.push(JSON.parse(
|
|
2047
|
+
result.push(JSON.parse(readFileSync3(metaPath, "utf-8")));
|
|
3161
2048
|
} catch (err) {
|
|
3162
2049
|
console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
|
|
3163
2050
|
}
|
|
@@ -3166,10 +2053,10 @@ function listPending(receiver) {
|
|
|
3166
2053
|
}
|
|
3167
2054
|
function getPending(receiver, id) {
|
|
3168
2055
|
validateId(id);
|
|
3169
|
-
const metaPath =
|
|
3170
|
-
if (!
|
|
2056
|
+
const metaPath = resolve4(pendingDir(receiver), id, "metadata.json");
|
|
2057
|
+
if (!existsSync4(metaPath)) return null;
|
|
3171
2058
|
try {
|
|
3172
|
-
return JSON.parse(
|
|
2059
|
+
return JSON.parse(readFileSync3(metaPath, "utf-8"));
|
|
3173
2060
|
} catch (err) {
|
|
3174
2061
|
console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
|
|
3175
2062
|
return null;
|
|
@@ -3184,27 +2071,27 @@ function deliverFile(receiverDir, sender, filename, content, inboxPath) {
|
|
|
3184
2071
|
if (sender.includes("/") || sender.includes("\\")) {
|
|
3185
2072
|
throw new Error("Invalid sender name");
|
|
3186
2073
|
}
|
|
3187
|
-
const destDir =
|
|
3188
|
-
|
|
3189
|
-
const destPath =
|
|
3190
|
-
|
|
3191
|
-
return
|
|
2074
|
+
const destDir = resolve4(receiverDir, "home", inbox, sender);
|
|
2075
|
+
mkdirSync2(destDir, { recursive: true });
|
|
2076
|
+
const destPath = resolve4(destDir, basename(filename));
|
|
2077
|
+
writeFileSync2(destPath, content);
|
|
2078
|
+
return join(inbox, sender, basename(filename));
|
|
3192
2079
|
}
|
|
3193
2080
|
function acceptPending(receiver, id, receiverDir) {
|
|
3194
2081
|
const meta = getPending(receiver, id);
|
|
3195
2082
|
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
3196
|
-
const dataPath =
|
|
3197
|
-
const content =
|
|
2083
|
+
const dataPath = resolve4(pendingDir(receiver), id, "data");
|
|
2084
|
+
const content = readFileSync3(dataPath);
|
|
3198
2085
|
const config = readFileSharingConfig(receiverDir);
|
|
3199
2086
|
const inboxPath = config.inboxPath ?? "inbox";
|
|
3200
2087
|
const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
|
|
3201
|
-
rmSync(
|
|
2088
|
+
rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
|
|
3202
2089
|
return { sender: meta.sender, filename: meta.filename, destPath };
|
|
3203
2090
|
}
|
|
3204
2091
|
function rejectPending(receiver, id) {
|
|
3205
2092
|
const meta = getPending(receiver, id);
|
|
3206
2093
|
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
3207
|
-
rmSync(
|
|
2094
|
+
rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
|
|
3208
2095
|
return { sender: meta.sender, filename: meta.filename };
|
|
3209
2096
|
}
|
|
3210
2097
|
function formatFileSize(bytes) {
|
|
@@ -3245,9 +2132,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
3245
2132
|
const pathErr = validateFilePath(body.filePath);
|
|
3246
2133
|
if (pathErr) return c.json({ error: pathErr }, 400);
|
|
3247
2134
|
const senderDir = mindDir(senderName);
|
|
3248
|
-
const filePath =
|
|
2135
|
+
const filePath = resolve5(senderDir, "home", body.filePath);
|
|
3249
2136
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
3250
|
-
const stat4 =
|
|
2137
|
+
const stat4 = statSync(filePath, { throwIfNoEntry: false });
|
|
3251
2138
|
if (!stat4) return c.json({ error: `File not found: ${body.filePath}` }, 404);
|
|
3252
2139
|
if (stat4.size > MAX_FILE_SIZE) {
|
|
3253
2140
|
return c.json(
|
|
@@ -3259,7 +2146,7 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
3259
2146
|
}
|
|
3260
2147
|
let content;
|
|
3261
2148
|
try {
|
|
3262
|
-
content =
|
|
2149
|
+
content = readFileSync4(filePath);
|
|
3263
2150
|
} catch {
|
|
3264
2151
|
return c.json({ error: `File not found: ${body.filePath}` }, 404);
|
|
3265
2152
|
}
|
|
@@ -3361,9 +2248,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
3361
2248
|
var file_sharing_default = app6;
|
|
3362
2249
|
|
|
3363
2250
|
// src/web/api/files.ts
|
|
3364
|
-
import { existsSync as
|
|
2251
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3365
2252
|
import { readdir, readFile, realpath, stat } from "fs/promises";
|
|
3366
|
-
import { extname, resolve as
|
|
2253
|
+
import { extname, resolve as resolve6 } from "path";
|
|
3367
2254
|
import { Hono as Hono7 } from "hono";
|
|
3368
2255
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
3369
2256
|
var AVATAR_MIME = {
|
|
@@ -3384,8 +2271,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
|
|
|
3384
2271
|
const ext = extname(config.avatar).toLowerCase();
|
|
3385
2272
|
const mime = AVATAR_MIME[ext];
|
|
3386
2273
|
if (!mime) return c.json({ error: "Invalid avatar extension" }, 400);
|
|
3387
|
-
const homeDir =
|
|
3388
|
-
const avatarPath =
|
|
2274
|
+
const homeDir = resolve6(dir, "home");
|
|
2275
|
+
const avatarPath = resolve6(homeDir, config.avatar);
|
|
3389
2276
|
if (!avatarPath.startsWith(`${homeDir}/`)) return c.json({ error: "Invalid avatar path" }, 400);
|
|
3390
2277
|
let realAvatarPath;
|
|
3391
2278
|
try {
|
|
@@ -3414,8 +2301,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
|
|
|
3414
2301
|
const entry = findMind(name);
|
|
3415
2302
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3416
2303
|
const dir = mindDir(name);
|
|
3417
|
-
const homeDir =
|
|
3418
|
-
if (!
|
|
2304
|
+
const homeDir = resolve6(dir, "home");
|
|
2305
|
+
if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
3419
2306
|
const allFiles = await readdir(homeDir);
|
|
3420
2307
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
3421
2308
|
return c.json(files);
|
|
@@ -3428,8 +2315,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
|
|
|
3428
2315
|
const entry = findMind(name);
|
|
3429
2316
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3430
2317
|
const dir = mindDir(name);
|
|
3431
|
-
const filePath =
|
|
3432
|
-
if (!
|
|
2318
|
+
const filePath = resolve6(dir, "home", filename);
|
|
2319
|
+
if (!existsSync5(filePath)) {
|
|
3433
2320
|
return c.json({ error: "File not found" }, 404);
|
|
3434
2321
|
}
|
|
3435
2322
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -3442,19 +2329,19 @@ import { Hono as Hono8 } from "hono";
|
|
|
3442
2329
|
|
|
3443
2330
|
// src/lib/identity.ts
|
|
3444
2331
|
import { createHash, generateKeyPairSync, sign, verify } from "crypto";
|
|
3445
|
-
import { existsSync as
|
|
3446
|
-
import { resolve as
|
|
2332
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
2333
|
+
import { resolve as resolve7 } from "path";
|
|
3447
2334
|
function generateIdentity(mindDir2) {
|
|
3448
|
-
const identityDir =
|
|
3449
|
-
|
|
2335
|
+
const identityDir = resolve7(mindDir2, ".mind/identity");
|
|
2336
|
+
mkdirSync3(identityDir, { recursive: true });
|
|
3450
2337
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
3451
2338
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
3452
2339
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
3453
2340
|
});
|
|
3454
|
-
const privatePath =
|
|
3455
|
-
const publicPath =
|
|
3456
|
-
|
|
3457
|
-
|
|
2341
|
+
const privatePath = resolve7(identityDir, "private.pem");
|
|
2342
|
+
const publicPath = resolve7(identityDir, "public.pem");
|
|
2343
|
+
writeFileSync3(privatePath, privateKey, { mode: 384 });
|
|
2344
|
+
writeFileSync3(publicPath, publicKey, { mode: 420 });
|
|
3458
2345
|
const config = readVoluteConfig(mindDir2) ?? {};
|
|
3459
2346
|
config.identity = {
|
|
3460
2347
|
privateKey: ".mind/identity/private.pem",
|
|
@@ -3467,17 +2354,17 @@ function getPrivateKey(mindDir2) {
|
|
|
3467
2354
|
const config = readVoluteConfig(mindDir2);
|
|
3468
2355
|
const relPath = config?.identity?.privateKey;
|
|
3469
2356
|
if (!relPath) return null;
|
|
3470
|
-
const fullPath =
|
|
3471
|
-
if (!
|
|
3472
|
-
return
|
|
2357
|
+
const fullPath = resolve7(mindDir2, relPath);
|
|
2358
|
+
if (!existsSync6(fullPath)) return null;
|
|
2359
|
+
return readFileSync5(fullPath, "utf-8");
|
|
3473
2360
|
}
|
|
3474
2361
|
function getPublicKey(mindDir2) {
|
|
3475
2362
|
const config = readVoluteConfig(mindDir2);
|
|
3476
2363
|
const relPath = config?.identity?.publicKey;
|
|
3477
2364
|
if (!relPath) return null;
|
|
3478
|
-
const fullPath =
|
|
3479
|
-
if (!
|
|
3480
|
-
return
|
|
2365
|
+
const fullPath = resolve7(mindDir2, relPath);
|
|
2366
|
+
if (!existsSync6(fullPath)) return null;
|
|
2367
|
+
return readFileSync5(fullPath, "utf-8");
|
|
3481
2368
|
}
|
|
3482
2369
|
function getFingerprint(publicKeyPem) {
|
|
3483
2370
|
return createHash("sha256").update(publicKeyPem).digest("hex");
|
|
@@ -3529,21 +2416,21 @@ var app8 = new Hono8().get("/:fingerprint", (c) => {
|
|
|
3529
2416
|
var keys_default = app8;
|
|
3530
2417
|
|
|
3531
2418
|
// src/web/api/logs.ts
|
|
3532
|
-
import { spawn
|
|
3533
|
-
import { existsSync as
|
|
3534
|
-
import { resolve as
|
|
2419
|
+
import { spawn } from "child_process";
|
|
2420
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2421
|
+
import { resolve as resolve8 } from "path";
|
|
3535
2422
|
import { Hono as Hono9 } from "hono";
|
|
3536
2423
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
3537
2424
|
var app9 = new Hono9().get("/:name/logs", async (c) => {
|
|
3538
2425
|
const name = c.req.param("name");
|
|
3539
2426
|
const entry = findMind(name);
|
|
3540
2427
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3541
|
-
const logFile =
|
|
3542
|
-
if (!
|
|
2428
|
+
const logFile = resolve8(stateDir(name), "logs", "mind.log");
|
|
2429
|
+
if (!existsSync7(logFile)) {
|
|
3543
2430
|
return c.json({ error: "No log file found" }, 404);
|
|
3544
2431
|
}
|
|
3545
2432
|
return streamSSE2(c, async (stream) => {
|
|
3546
|
-
const tail =
|
|
2433
|
+
const tail = spawn("tail", ["-n", "200", "-f", logFile]);
|
|
3547
2434
|
const onData = (data) => {
|
|
3548
2435
|
const lines = data.toString().split("\n");
|
|
3549
2436
|
for (const line of lines) {
|
|
@@ -3557,28 +2444,28 @@ var app9 = new Hono9().get("/:name/logs", async (c) => {
|
|
|
3557
2444
|
stream.onAbort(() => {
|
|
3558
2445
|
tail.kill();
|
|
3559
2446
|
});
|
|
3560
|
-
await new Promise((
|
|
3561
|
-
tail.on("exit",
|
|
3562
|
-
stream.onAbort(
|
|
2447
|
+
await new Promise((resolve18) => {
|
|
2448
|
+
tail.on("exit", resolve18);
|
|
2449
|
+
stream.onAbort(resolve18);
|
|
3563
2450
|
});
|
|
3564
2451
|
});
|
|
3565
2452
|
}).get("/:name/logs/tail", async (c) => {
|
|
3566
2453
|
const name = c.req.param("name");
|
|
3567
2454
|
const entry = findMind(name);
|
|
3568
2455
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3569
|
-
const logFile =
|
|
3570
|
-
if (!
|
|
2456
|
+
const logFile = resolve8(stateDir(name), "logs", "mind.log");
|
|
2457
|
+
if (!existsSync7(logFile)) {
|
|
3571
2458
|
return c.json({ error: "No log file found" }, 404);
|
|
3572
2459
|
}
|
|
3573
2460
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
3574
2461
|
const n = Number.isFinite(nParam) && nParam > 0 ? Math.min(nParam, 1e4) : 50;
|
|
3575
|
-
const tail =
|
|
2462
|
+
const tail = spawn("tail", ["-n", String(n), logFile]);
|
|
3576
2463
|
let output = "";
|
|
3577
2464
|
tail.stdout.on("data", (data) => {
|
|
3578
2465
|
output += data.toString();
|
|
3579
2466
|
});
|
|
3580
|
-
await new Promise((
|
|
3581
|
-
tail.on("exit",
|
|
2467
|
+
await new Promise((resolve18) => {
|
|
2468
|
+
tail.on("exit", resolve18);
|
|
3582
2469
|
});
|
|
3583
2470
|
return c.text(output);
|
|
3584
2471
|
});
|
|
@@ -3668,33 +2555,33 @@ var mind_skills_default = app10;
|
|
|
3668
2555
|
// src/web/api/minds.ts
|
|
3669
2556
|
import {
|
|
3670
2557
|
cpSync,
|
|
3671
|
-
existsSync as
|
|
3672
|
-
mkdirSync as
|
|
3673
|
-
readdirSync as
|
|
3674
|
-
readFileSync as
|
|
3675
|
-
rmSync as
|
|
3676
|
-
writeFileSync as
|
|
2558
|
+
existsSync as existsSync9,
|
|
2559
|
+
mkdirSync as mkdirSync5,
|
|
2560
|
+
readdirSync as readdirSync4,
|
|
2561
|
+
readFileSync as readFileSync8,
|
|
2562
|
+
rmSync as rmSync3,
|
|
2563
|
+
writeFileSync as writeFileSync6
|
|
3677
2564
|
} from "fs";
|
|
3678
|
-
import { resolve as
|
|
2565
|
+
import { resolve as resolve11 } from "path";
|
|
3679
2566
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
3680
2567
|
import { and as and3, desc as desc3, eq as eq4, sql as sql2 } from "drizzle-orm";
|
|
3681
2568
|
import { Hono as Hono11 } from "hono";
|
|
3682
2569
|
import { z as z3 } from "zod";
|
|
3683
2570
|
|
|
3684
2571
|
// src/lib/consolidate.ts
|
|
3685
|
-
import { readdirSync as
|
|
3686
|
-
import { resolve as
|
|
2572
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
2573
|
+
import { resolve as resolve9 } from "path";
|
|
3687
2574
|
async function consolidateMemory(mindDir2) {
|
|
3688
|
-
const soulPath =
|
|
3689
|
-
const memoryPath =
|
|
3690
|
-
const memoryDir =
|
|
3691
|
-
const soul =
|
|
2575
|
+
const soulPath = resolve9(mindDir2, "home/SOUL.md");
|
|
2576
|
+
const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
|
|
2577
|
+
const memoryDir = resolve9(mindDir2, "home/memory");
|
|
2578
|
+
const soul = readFileSync6(soulPath, "utf-8");
|
|
3692
2579
|
const logs = [];
|
|
3693
2580
|
try {
|
|
3694
|
-
const files =
|
|
2581
|
+
const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
3695
2582
|
for (const filename of files) {
|
|
3696
2583
|
const date = filename.replace(".md", "");
|
|
3697
|
-
const content2 =
|
|
2584
|
+
const content2 = readFileSync6(resolve9(memoryDir, filename), "utf-8").trim();
|
|
3698
2585
|
if (content2) {
|
|
3699
2586
|
logs.push(`### ${date}
|
|
3700
2587
|
|
|
@@ -3744,7 +2631,7 @@ ${content2}`);
|
|
|
3744
2631
|
const data = await res.json();
|
|
3745
2632
|
const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
|
|
3746
2633
|
if (content) {
|
|
3747
|
-
|
|
2634
|
+
writeFileSync4(memoryPath, `${content}
|
|
3748
2635
|
`);
|
|
3749
2636
|
console.log("MEMORY.md created successfully.");
|
|
3750
2637
|
} else {
|
|
@@ -3754,11 +2641,11 @@ ${content2}`);
|
|
|
3754
2641
|
|
|
3755
2642
|
// src/lib/convert-session.ts
|
|
3756
2643
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3757
|
-
import { mkdirSync as
|
|
2644
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
3758
2645
|
import { homedir } from "os";
|
|
3759
|
-
import { resolve as
|
|
2646
|
+
import { resolve as resolve10 } from "path";
|
|
3760
2647
|
function convertSession(opts) {
|
|
3761
|
-
const lines =
|
|
2648
|
+
const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
|
|
3762
2649
|
const sessionId = randomUUID2();
|
|
3763
2650
|
const idMap = /* @__PURE__ */ new Map();
|
|
3764
2651
|
const messages2 = [];
|
|
@@ -3872,10 +2759,10 @@ function convertSession(opts) {
|
|
|
3872
2759
|
}
|
|
3873
2760
|
}
|
|
3874
2761
|
const projectId = opts.projectDir.replace(/\//g, "-");
|
|
3875
|
-
const sdkDir =
|
|
3876
|
-
|
|
3877
|
-
const sdkPath =
|
|
3878
|
-
|
|
2762
|
+
const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
|
|
2763
|
+
mkdirSync4(sdkDir, { recursive: true });
|
|
2764
|
+
const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
|
|
2765
|
+
writeFileSync5(sdkPath, `${sdkEvents.join("\n")}
|
|
3879
2766
|
`);
|
|
3880
2767
|
console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
|
|
3881
2768
|
return sessionId;
|
|
@@ -3926,39 +2813,53 @@ function convertAssistantContent(content) {
|
|
|
3926
2813
|
return result;
|
|
3927
2814
|
}
|
|
3928
2815
|
|
|
3929
|
-
// src/lib/
|
|
3930
|
-
|
|
3931
|
-
function
|
|
3932
|
-
|
|
3933
|
-
if (!set) {
|
|
3934
|
-
set = /* @__PURE__ */ new Set();
|
|
3935
|
-
subscribers.set(mind, set);
|
|
3936
|
-
}
|
|
3937
|
-
set.add(callback);
|
|
3938
|
-
return () => {
|
|
3939
|
-
set.delete(callback);
|
|
3940
|
-
if (set.size === 0) subscribers.delete(mind);
|
|
3941
|
-
};
|
|
3942
|
-
}
|
|
3943
|
-
function publish3(mind, event) {
|
|
3944
|
-
const set = subscribers.get(mind);
|
|
3945
|
-
if (!set) return;
|
|
3946
|
-
for (const cb of set) {
|
|
2816
|
+
// src/lib/variant-cleanup.ts
|
|
2817
|
+
import { existsSync as existsSync8, rmSync as rmSync2 } from "fs";
|
|
2818
|
+
async function cleanupVariant(mindName, variantName, projectRoot, variantPath, opts) {
|
|
2819
|
+
if (opts?.stop) {
|
|
3947
2820
|
try {
|
|
3948
|
-
|
|
3949
|
-
} catch
|
|
3950
|
-
console.error("[mind-events] subscriber threw:", err);
|
|
3951
|
-
set.delete(cb);
|
|
3952
|
-
if (set.size === 0) subscribers.delete(mind);
|
|
2821
|
+
await getMindManager().stopMind(`${mindName}@${variantName}`);
|
|
2822
|
+
} catch {
|
|
3953
2823
|
}
|
|
3954
2824
|
}
|
|
2825
|
+
if (existsSync8(variantPath)) {
|
|
2826
|
+
try {
|
|
2827
|
+
await gitExec(["worktree", "remove", "--force", variantPath], { cwd: projectRoot });
|
|
2828
|
+
} catch {
|
|
2829
|
+
rmSync2(variantPath, { recursive: true, force: true });
|
|
2830
|
+
try {
|
|
2831
|
+
await gitExec(["worktree", "prune"], { cwd: projectRoot });
|
|
2832
|
+
} catch {
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
try {
|
|
2837
|
+
await gitExec(["branch", "-D", variantName], { cwd: projectRoot });
|
|
2838
|
+
} catch {
|
|
2839
|
+
}
|
|
2840
|
+
try {
|
|
2841
|
+
removeVariant(mindName, variantName);
|
|
2842
|
+
} catch {
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
chownMindDir(projectRoot, mindName);
|
|
2846
|
+
} catch (err) {
|
|
2847
|
+
logger_default.error(`failed to fix ownership during variant cleanup for ${mindName}`, logger_default.errorData(err));
|
|
2848
|
+
}
|
|
3955
2849
|
}
|
|
3956
2850
|
|
|
3957
2851
|
// src/web/api/minds.ts
|
|
3958
2852
|
async function getMindStatus(name, port) {
|
|
3959
2853
|
const manager = getMindManager();
|
|
3960
2854
|
let status = "stopped";
|
|
3961
|
-
|
|
2855
|
+
try {
|
|
2856
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
2857
|
+
if (getSleepManagerIfReady()?.isSleeping(name)) {
|
|
2858
|
+
status = "sleeping";
|
|
2859
|
+
}
|
|
2860
|
+
} catch {
|
|
2861
|
+
}
|
|
2862
|
+
if (status !== "sleeping" && manager.isRunning(name)) {
|
|
3962
2863
|
const health = await checkHealth(port);
|
|
3963
2864
|
status = health.ok ? "running" : "starting";
|
|
3964
2865
|
}
|
|
@@ -4010,7 +2911,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
|
|
|
4010
2911
|
await gitExec(["commit", "-m", "initial commit"], opts);
|
|
4011
2912
|
}
|
|
4012
2913
|
async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
4013
|
-
const tempWorktree =
|
|
2914
|
+
const tempWorktree = resolve11(projectRoot, ".variants", "_template_update");
|
|
4014
2915
|
let branchExists = false;
|
|
4015
2916
|
try {
|
|
4016
2917
|
await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
|
|
@@ -4021,8 +2922,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
4021
2922
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
4022
2923
|
} catch {
|
|
4023
2924
|
}
|
|
4024
|
-
if (
|
|
4025
|
-
|
|
2925
|
+
if (existsSync9(tempWorktree)) {
|
|
2926
|
+
rmSync3(tempWorktree, { recursive: true, force: true });
|
|
4026
2927
|
}
|
|
4027
2928
|
const templatesRoot = findTemplatesRoot();
|
|
4028
2929
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
@@ -4042,9 +2943,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
4042
2943
|
});
|
|
4043
2944
|
}
|
|
4044
2945
|
copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
|
|
4045
|
-
const initDir =
|
|
4046
|
-
if (
|
|
4047
|
-
|
|
2946
|
+
const initDir = resolve11(tempWorktree, ".init");
|
|
2947
|
+
if (existsSync9(initDir)) {
|
|
2948
|
+
rmSync3(initDir, { recursive: true, force: true });
|
|
4048
2949
|
}
|
|
4049
2950
|
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
4050
2951
|
try {
|
|
@@ -4057,10 +2958,10 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
4057
2958
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
4058
2959
|
} catch {
|
|
4059
2960
|
}
|
|
4060
|
-
if (
|
|
4061
|
-
|
|
2961
|
+
if (existsSync9(tempWorktree)) {
|
|
2962
|
+
rmSync3(tempWorktree, { recursive: true, force: true });
|
|
4062
2963
|
}
|
|
4063
|
-
|
|
2964
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4064
2965
|
}
|
|
4065
2966
|
}
|
|
4066
2967
|
async function mergeTemplateBranch(worktreeDir) {
|
|
@@ -4083,14 +2984,14 @@ async function mergeTemplateBranch(worktreeDir) {
|
|
|
4083
2984
|
async function npmInstallAsMind(cwd, mindName) {
|
|
4084
2985
|
if (isIsolationEnabled()) {
|
|
4085
2986
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
4086
|
-
await exec(cmd, args, { cwd, env: { ...process.env, HOME:
|
|
2987
|
+
await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve11(cwd, "home") } });
|
|
4087
2988
|
} else {
|
|
4088
2989
|
await exec("npm", ["install"], { cwd });
|
|
4089
2990
|
}
|
|
4090
2991
|
}
|
|
4091
2992
|
async function importFromArchive(c, tempDir, nameOverride, manifest) {
|
|
4092
|
-
const extractedMindDir =
|
|
4093
|
-
if (!
|
|
2993
|
+
const extractedMindDir = resolve11(tempDir, "mind");
|
|
2994
|
+
if (!existsSync9(extractedMindDir)) {
|
|
4094
2995
|
return c.json({ error: "Invalid archive: missing mind/ directory" }, 400);
|
|
4095
2996
|
}
|
|
4096
2997
|
if (!manifest?.includes || !manifest.name || !manifest.template) {
|
|
@@ -4108,21 +3009,21 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
|
|
|
4108
3009
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
4109
3010
|
ensureVoluteHome();
|
|
4110
3011
|
const dest = mindDir(name);
|
|
4111
|
-
if (
|
|
3012
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4112
3013
|
try {
|
|
4113
3014
|
cpSync(extractedMindDir, dest, { recursive: true });
|
|
4114
3015
|
if (!manifest.includes.identity) {
|
|
4115
3016
|
generateIdentity(dest);
|
|
4116
3017
|
}
|
|
4117
3018
|
const state = stateDir(name);
|
|
4118
|
-
|
|
4119
|
-
const channelsJson =
|
|
4120
|
-
if (
|
|
4121
|
-
cpSync(channelsJson,
|
|
3019
|
+
mkdirSync5(state, { recursive: true });
|
|
3020
|
+
const channelsJson = resolve11(tempDir, "state/channels.json");
|
|
3021
|
+
if (existsSync9(channelsJson)) {
|
|
3022
|
+
cpSync(channelsJson, resolve11(state, "channels.json"));
|
|
4122
3023
|
}
|
|
4123
|
-
const envJson =
|
|
4124
|
-
if (
|
|
4125
|
-
cpSync(envJson,
|
|
3024
|
+
const envJson = resolve11(tempDir, "state/env.json");
|
|
3025
|
+
if (existsSync9(envJson)) {
|
|
3026
|
+
cpSync(envJson, resolve11(state, "env.json"));
|
|
4126
3027
|
}
|
|
4127
3028
|
const port = nextPort();
|
|
4128
3029
|
addMind(name, port, manifest.stage, manifest.template);
|
|
@@ -4131,36 +3032,36 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
|
|
|
4131
3032
|
} catch (err) {
|
|
4132
3033
|
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4133
3034
|
}
|
|
4134
|
-
const homeDir =
|
|
3035
|
+
const homeDir = resolve11(dest, "home");
|
|
4135
3036
|
ensureVoluteGroup();
|
|
4136
3037
|
createMindUser(name, homeDir);
|
|
4137
3038
|
chownMindDir(dest, name);
|
|
4138
3039
|
await npmInstallAsMind(dest, name);
|
|
4139
3040
|
await importHistoryFromArchive(name, tempDir);
|
|
4140
3041
|
importSessionsFromArchive(dest, tempDir);
|
|
4141
|
-
if (!
|
|
3042
|
+
if (!existsSync9(resolve11(dest, ".git"))) {
|
|
4142
3043
|
try {
|
|
4143
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3044
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
|
|
4144
3045
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
4145
3046
|
await configureGitIdentity(name, { cwd: dest, mindName: name, env });
|
|
4146
3047
|
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
4147
3048
|
await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
|
|
4148
3049
|
} catch (err) {
|
|
4149
3050
|
logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
|
|
4150
|
-
|
|
3051
|
+
rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
|
|
4151
3052
|
}
|
|
4152
3053
|
}
|
|
4153
3054
|
chownMindDir(dest, name);
|
|
4154
|
-
|
|
3055
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4155
3056
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
4156
3057
|
} catch (err) {
|
|
4157
|
-
if (
|
|
3058
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4158
3059
|
try {
|
|
4159
3060
|
removeMind(name);
|
|
4160
3061
|
} catch (cleanupErr) {
|
|
4161
3062
|
logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
|
|
4162
3063
|
}
|
|
4163
|
-
|
|
3064
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4164
3065
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4165
3066
|
}
|
|
4166
3067
|
}
|
|
@@ -4171,7 +3072,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4171
3072
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
4172
3073
|
ensureVoluteHome();
|
|
4173
3074
|
const dest = mindDir(name);
|
|
4174
|
-
if (
|
|
3075
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4175
3076
|
const templatesRoot = findTemplatesRoot();
|
|
4176
3077
|
const { composedDir, manifest: templateManifest } = composeTemplate(
|
|
4177
3078
|
templatesRoot,
|
|
@@ -4180,40 +3081,40 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4180
3081
|
try {
|
|
4181
3082
|
copyTemplateToDir(composedDir, dest, name, templateManifest);
|
|
4182
3083
|
applyInitFiles(dest);
|
|
4183
|
-
const extractedHome =
|
|
4184
|
-
if (
|
|
4185
|
-
cpSync(extractedHome,
|
|
3084
|
+
const extractedHome = resolve11(extractedMindDir, "home");
|
|
3085
|
+
if (existsSync9(extractedHome)) {
|
|
3086
|
+
cpSync(extractedHome, resolve11(dest, "home"), { recursive: true });
|
|
4186
3087
|
}
|
|
4187
|
-
const extractedMindInternal =
|
|
4188
|
-
if (
|
|
4189
|
-
cpSync(extractedMindInternal,
|
|
3088
|
+
const extractedMindInternal = resolve11(extractedMindDir, ".mind");
|
|
3089
|
+
if (existsSync9(extractedMindInternal)) {
|
|
3090
|
+
cpSync(extractedMindInternal, resolve11(dest, ".mind"), { recursive: true });
|
|
4190
3091
|
}
|
|
4191
|
-
const identityDir =
|
|
3092
|
+
const identityDir = resolve11(dest, ".mind/identity");
|
|
4192
3093
|
let publicKeyPem;
|
|
4193
|
-
if (!manifest.includes.identity || !
|
|
3094
|
+
if (!manifest.includes.identity || !existsSync9(resolve11(identityDir, "private.pem"))) {
|
|
4194
3095
|
({ publicKeyPem } = generateIdentity(dest));
|
|
4195
3096
|
} else {
|
|
4196
|
-
publicKeyPem =
|
|
3097
|
+
publicKeyPem = readFileSync8(resolve11(identityDir, "public.pem"), "utf-8");
|
|
4197
3098
|
}
|
|
4198
|
-
const promptsPath =
|
|
4199
|
-
if (!
|
|
3099
|
+
const promptsPath = resolve11(dest, "home/.config/prompts.json");
|
|
3100
|
+
if (!existsSync9(promptsPath)) {
|
|
4200
3101
|
const mindPrompts = await getMindPromptDefaults();
|
|
4201
|
-
|
|
3102
|
+
writeFileSync6(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
|
|
4202
3103
|
`);
|
|
4203
3104
|
}
|
|
4204
3105
|
const state = stateDir(name);
|
|
4205
|
-
|
|
4206
|
-
const channelsJson =
|
|
4207
|
-
if (
|
|
4208
|
-
cpSync(channelsJson,
|
|
3106
|
+
mkdirSync5(state, { recursive: true });
|
|
3107
|
+
const channelsJson = resolve11(tempDir, "state/channels.json");
|
|
3108
|
+
if (existsSync9(channelsJson)) {
|
|
3109
|
+
cpSync(channelsJson, resolve11(state, "channels.json"));
|
|
4209
3110
|
}
|
|
4210
|
-
const envJson =
|
|
4211
|
-
if (
|
|
4212
|
-
cpSync(envJson,
|
|
3111
|
+
const envJson = resolve11(tempDir, "state/env.json");
|
|
3112
|
+
if (existsSync9(envJson)) {
|
|
3113
|
+
cpSync(envJson, resolve11(state, "env.json"));
|
|
4213
3114
|
}
|
|
4214
3115
|
const port = nextPort();
|
|
4215
3116
|
addMind(name, port, manifest.stage, manifest.template);
|
|
4216
|
-
const homeDir =
|
|
3117
|
+
const homeDir = resolve11(dest, "home");
|
|
4217
3118
|
ensureVoluteGroup();
|
|
4218
3119
|
createMindUser(name, homeDir);
|
|
4219
3120
|
chownMindDir(dest, name);
|
|
@@ -4226,7 +3127,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4226
3127
|
await initTemplateBranch(dest, composedDir, templateManifest, name, env);
|
|
4227
3128
|
} catch (err) {
|
|
4228
3129
|
logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
|
|
4229
|
-
|
|
3130
|
+
rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
|
|
4230
3131
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
4231
3132
|
}
|
|
4232
3133
|
try {
|
|
@@ -4250,7 +3151,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4250
3151
|
publishPublicKey(name, publicKeyPem).catch(
|
|
4251
3152
|
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
4252
3153
|
);
|
|
4253
|
-
|
|
3154
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4254
3155
|
return c.json({
|
|
4255
3156
|
ok: true,
|
|
4256
3157
|
name,
|
|
@@ -4261,24 +3162,24 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4261
3162
|
...skillWarnings.length > 0 && { skillWarnings }
|
|
4262
3163
|
});
|
|
4263
3164
|
} catch (err) {
|
|
4264
|
-
if (
|
|
3165
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4265
3166
|
try {
|
|
4266
3167
|
removeMind(name);
|
|
4267
3168
|
} catch (cleanupErr) {
|
|
4268
3169
|
logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
|
|
4269
3170
|
}
|
|
4270
|
-
|
|
3171
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4271
3172
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4272
3173
|
} finally {
|
|
4273
|
-
|
|
3174
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4274
3175
|
}
|
|
4275
3176
|
}
|
|
4276
3177
|
async function importHistoryFromArchive(name, tempDir) {
|
|
4277
|
-
const historyJsonl =
|
|
4278
|
-
if (!
|
|
3178
|
+
const historyJsonl = resolve11(tempDir, "history.jsonl");
|
|
3179
|
+
if (!existsSync9(historyJsonl)) return;
|
|
4279
3180
|
try {
|
|
4280
3181
|
const db = await getDb();
|
|
4281
|
-
const lines =
|
|
3182
|
+
const lines = readFileSync8(historyJsonl, "utf-8").trim().split("\n");
|
|
4282
3183
|
let imported = 0;
|
|
4283
3184
|
let failed = 0;
|
|
4284
3185
|
for (const line of lines) {
|
|
@@ -4314,13 +3215,13 @@ async function importHistoryFromArchive(name, tempDir) {
|
|
|
4314
3215
|
}
|
|
4315
3216
|
}
|
|
4316
3217
|
function importSessionsFromArchive(dest, tempDir) {
|
|
4317
|
-
const sessionsDir =
|
|
4318
|
-
if (!
|
|
3218
|
+
const sessionsDir = resolve11(tempDir, "sessions");
|
|
3219
|
+
if (!existsSync9(sessionsDir)) return;
|
|
4319
3220
|
try {
|
|
4320
|
-
const destSessions =
|
|
4321
|
-
|
|
4322
|
-
for (const file of
|
|
4323
|
-
cpSync(
|
|
3221
|
+
const destSessions = resolve11(dest, ".mind/sessions");
|
|
3222
|
+
mkdirSync5(destSessions, { recursive: true });
|
|
3223
|
+
for (const file of readdirSync4(sessionsDir)) {
|
|
3224
|
+
cpSync(resolve11(sessionsDir, file), resolve11(destSessions, file));
|
|
4324
3225
|
}
|
|
4325
3226
|
} catch (err) {
|
|
4326
3227
|
logger_default.error("Failed to import sessions from archive", logger_default.errorData(err));
|
|
@@ -4343,7 +3244,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4343
3244
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
4344
3245
|
ensureVoluteHome();
|
|
4345
3246
|
const dest = mindDir(name);
|
|
4346
|
-
if (
|
|
3247
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4347
3248
|
const templatesRoot = findTemplatesRoot();
|
|
4348
3249
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
4349
3250
|
try {
|
|
@@ -4357,15 +3258,15 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4357
3258
|
writeVoluteConfig(dest, seedConfig);
|
|
4358
3259
|
}
|
|
4359
3260
|
if (body.model) {
|
|
4360
|
-
const configPath2 =
|
|
4361
|
-
const existing =
|
|
3261
|
+
const configPath2 = resolve11(dest, "home/.config/config.json");
|
|
3262
|
+
const existing = existsSync9(configPath2) ? JSON.parse(readFileSync8(configPath2, "utf-8")) : {};
|
|
4362
3263
|
existing.model = body.model;
|
|
4363
|
-
|
|
3264
|
+
writeFileSync6(configPath2, `${JSON.stringify(existing, null, 2)}
|
|
4364
3265
|
`);
|
|
4365
3266
|
}
|
|
4366
3267
|
const mindPrompts = await getMindPromptDefaults();
|
|
4367
|
-
|
|
4368
|
-
|
|
3268
|
+
writeFileSync6(
|
|
3269
|
+
resolve11(dest, "home/.config/prompts.json"),
|
|
4369
3270
|
`${JSON.stringify(mindPrompts, null, 2)}
|
|
4370
3271
|
`
|
|
4371
3272
|
);
|
|
@@ -4376,7 +3277,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4376
3277
|
} catch (err) {
|
|
4377
3278
|
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4378
3279
|
}
|
|
4379
|
-
const homeDir =
|
|
3280
|
+
const homeDir = resolve11(dest, "home");
|
|
4380
3281
|
ensureVoluteGroup();
|
|
4381
3282
|
createMindUser(name, homeDir);
|
|
4382
3283
|
chownMindDir(dest, name);
|
|
@@ -4389,7 +3290,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4389
3290
|
await initTemplateBranch(dest, composedDir, manifest, name, env);
|
|
4390
3291
|
} catch (err) {
|
|
4391
3292
|
logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
|
|
4392
|
-
|
|
3293
|
+
rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
|
|
4393
3294
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
4394
3295
|
}
|
|
4395
3296
|
try {
|
|
@@ -4404,7 +3305,7 @@ The human who planted you described you as: "${body.description}"
|
|
|
4404
3305
|
` : "";
|
|
4405
3306
|
const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
|
|
4406
3307
|
const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
|
|
4407
|
-
|
|
3308
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
|
|
4408
3309
|
}
|
|
4409
3310
|
const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
|
|
4410
3311
|
const skillWarnings = [];
|
|
@@ -4419,16 +3320,27 @@ The human who planted you described you as: "${body.description}"
|
|
|
4419
3320
|
if (body.stage !== "seed") {
|
|
4420
3321
|
const customSoul = await getPromptIfCustom("default_soul");
|
|
4421
3322
|
if (customSoul) {
|
|
4422
|
-
|
|
3323
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
|
|
4423
3324
|
}
|
|
4424
3325
|
const customMemory = await getPromptIfCustom("default_memory");
|
|
4425
3326
|
if (customMemory) {
|
|
4426
|
-
|
|
3327
|
+
writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
|
|
4427
3328
|
}
|
|
4428
3329
|
}
|
|
4429
3330
|
publishPublicKey(name, publicKeyPem).catch(
|
|
4430
3331
|
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
4431
3332
|
);
|
|
3333
|
+
fireWebhook({
|
|
3334
|
+
event: "mind_created",
|
|
3335
|
+
mind: name,
|
|
3336
|
+
data: {
|
|
3337
|
+
name,
|
|
3338
|
+
port,
|
|
3339
|
+
stage: body.stage ?? "sprouted",
|
|
3340
|
+
template,
|
|
3341
|
+
description: body.description
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
4432
3344
|
return c.json({
|
|
4433
3345
|
ok: true,
|
|
4434
3346
|
name,
|
|
@@ -4439,14 +3351,14 @@ The human who planted you described you as: "${body.description}"
|
|
|
4439
3351
|
...skillWarnings.length > 0 && { skillWarnings }
|
|
4440
3352
|
});
|
|
4441
3353
|
} catch (err) {
|
|
4442
|
-
if (
|
|
3354
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4443
3355
|
try {
|
|
4444
3356
|
removeMind(name);
|
|
4445
3357
|
} catch {
|
|
4446
3358
|
}
|
|
4447
3359
|
return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
|
|
4448
3360
|
} finally {
|
|
4449
|
-
|
|
3361
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4450
3362
|
}
|
|
4451
3363
|
}).post("/import", requireAdmin, async (c) => {
|
|
4452
3364
|
let body;
|
|
@@ -4459,13 +3371,13 @@ The human who planted you described you as: "${body.description}"
|
|
|
4459
3371
|
return importFromArchive(c, body.archivePath, body.name, body.manifest);
|
|
4460
3372
|
}
|
|
4461
3373
|
const wsDir = body.workspacePath;
|
|
4462
|
-
if (!wsDir || !
|
|
3374
|
+
if (!wsDir || !existsSync9(resolve11(wsDir, "SOUL.md")) || !existsSync9(resolve11(wsDir, "IDENTITY.md"))) {
|
|
4463
3375
|
return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
|
|
4464
3376
|
}
|
|
4465
|
-
const soul =
|
|
4466
|
-
const identity =
|
|
4467
|
-
const userPath =
|
|
4468
|
-
const user =
|
|
3377
|
+
const soul = readFileSync8(resolve11(wsDir, "SOUL.md"), "utf-8");
|
|
3378
|
+
const identity = readFileSync8(resolve11(wsDir, "IDENTITY.md"), "utf-8");
|
|
3379
|
+
const userPath = resolve11(wsDir, "USER.md");
|
|
3380
|
+
const user = existsSync9(userPath) ? readFileSync8(userPath, "utf-8") : "";
|
|
4469
3381
|
const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
|
|
4470
3382
|
const template = body.template ?? "claude";
|
|
4471
3383
|
const nameErr = validateMindName(name);
|
|
@@ -4485,33 +3397,33 @@ ${user.trimEnd()}
|
|
|
4485
3397
|
` : "";
|
|
4486
3398
|
ensureVoluteHome();
|
|
4487
3399
|
const dest = mindDir(name);
|
|
4488
|
-
if (
|
|
3400
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4489
3401
|
const templatesRoot = findTemplatesRoot();
|
|
4490
3402
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
4491
3403
|
try {
|
|
4492
3404
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
4493
3405
|
applyInitFiles(dest);
|
|
4494
3406
|
const { publicKeyPem: importPublicKey } = generateIdentity(dest);
|
|
4495
|
-
|
|
4496
|
-
const wsMemoryPath =
|
|
4497
|
-
const hasMemory =
|
|
3407
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
|
|
3408
|
+
const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
|
|
3409
|
+
const hasMemory = existsSync9(wsMemoryPath);
|
|
4498
3410
|
if (hasMemory) {
|
|
4499
|
-
const existingMemory =
|
|
4500
|
-
|
|
4501
|
-
|
|
3411
|
+
const existingMemory = readFileSync8(wsMemoryPath, "utf-8");
|
|
3412
|
+
writeFileSync6(
|
|
3413
|
+
resolve11(dest, "home/MEMORY.md"),
|
|
4502
3414
|
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
4503
3415
|
);
|
|
4504
3416
|
} else if (user) {
|
|
4505
|
-
|
|
3417
|
+
writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
4506
3418
|
`);
|
|
4507
3419
|
}
|
|
4508
|
-
const wsMemoryDir =
|
|
3420
|
+
const wsMemoryDir = resolve11(wsDir, "memory");
|
|
4509
3421
|
let dailyLogCount = 0;
|
|
4510
|
-
if (
|
|
4511
|
-
const destMemoryDir =
|
|
4512
|
-
const files =
|
|
3422
|
+
if (existsSync9(wsMemoryDir)) {
|
|
3423
|
+
const destMemoryDir = resolve11(dest, "home/memory");
|
|
3424
|
+
const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
4513
3425
|
for (const file of files) {
|
|
4514
|
-
cpSync(
|
|
3426
|
+
cpSync(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
|
|
4515
3427
|
}
|
|
4516
3428
|
dailyLogCount = files.length;
|
|
4517
3429
|
}
|
|
@@ -4522,7 +3434,7 @@ ${user.trimEnd()}
|
|
|
4522
3434
|
} catch (err) {
|
|
4523
3435
|
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4524
3436
|
}
|
|
4525
|
-
const homeDir =
|
|
3437
|
+
const homeDir = resolve11(dest, "home");
|
|
4526
3438
|
ensureVoluteGroup();
|
|
4527
3439
|
createMindUser(name, homeDir);
|
|
4528
3440
|
chownMindDir(dest, name);
|
|
@@ -4530,20 +3442,20 @@ ${user.trimEnd()}
|
|
|
4530
3442
|
if (!hasMemory && dailyLogCount > 0) {
|
|
4531
3443
|
await consolidateMemory(dest);
|
|
4532
3444
|
}
|
|
4533
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3445
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
|
|
4534
3446
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
4535
3447
|
await configureGitIdentity(name, { cwd: dest, mindName: name, env });
|
|
4536
3448
|
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
4537
3449
|
await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
|
|
4538
|
-
const sessionFile = body.sessionPath ?
|
|
4539
|
-
if (sessionFile &&
|
|
3450
|
+
const sessionFile = body.sessionPath ? resolve11(body.sessionPath) : findOpenClawSession(wsDir);
|
|
3451
|
+
if (sessionFile && existsSync9(sessionFile)) {
|
|
4540
3452
|
if (template === "pi") {
|
|
4541
3453
|
importPiSession(sessionFile, dest);
|
|
4542
3454
|
} else if (template === "claude") {
|
|
4543
3455
|
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
4544
|
-
const mindRuntimeDir =
|
|
4545
|
-
|
|
4546
|
-
|
|
3456
|
+
const mindRuntimeDir = resolve11(dest, ".mind");
|
|
3457
|
+
mkdirSync5(mindRuntimeDir, { recursive: true });
|
|
3458
|
+
writeFileSync6(resolve11(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
|
|
4547
3459
|
}
|
|
4548
3460
|
}
|
|
4549
3461
|
importOpenClawConnectors(name, dest);
|
|
@@ -4558,14 +3470,14 @@ ${user.trimEnd()}
|
|
|
4558
3470
|
);
|
|
4559
3471
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
4560
3472
|
} catch (err) {
|
|
4561
|
-
if (
|
|
3473
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4562
3474
|
try {
|
|
4563
3475
|
removeMind(name);
|
|
4564
3476
|
} catch {
|
|
4565
3477
|
}
|
|
4566
3478
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4567
3479
|
} finally {
|
|
4568
|
-
|
|
3480
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4569
3481
|
}
|
|
4570
3482
|
}).get("/", async (c) => {
|
|
4571
3483
|
const entries = readRegistry();
|
|
@@ -4582,7 +3494,7 @@ ${user.trimEnd()}
|
|
|
4582
3494
|
const minds = await Promise.all(
|
|
4583
3495
|
entries.map(async (entry) => {
|
|
4584
3496
|
const mindStatus = await getMindStatus(entry.name, entry.port);
|
|
4585
|
-
const hasPages =
|
|
3497
|
+
const hasPages = existsSync9(resolve11(mindDir(entry.name), "home", "pages"));
|
|
4586
3498
|
return {
|
|
4587
3499
|
...entry,
|
|
4588
3500
|
...mindStatus,
|
|
@@ -4600,7 +3512,7 @@ ${user.trimEnd()}
|
|
|
4600
3512
|
const name = c.req.param("name");
|
|
4601
3513
|
const entry = findMind(name);
|
|
4602
3514
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4603
|
-
if (!
|
|
3515
|
+
if (!existsSync9(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
|
|
4604
3516
|
const mindStatus = await getMindStatus(name, entry.port);
|
|
4605
3517
|
const variants = readVariants(name);
|
|
4606
3518
|
const manager = getMindManager();
|
|
@@ -4615,7 +3527,7 @@ ${user.trimEnd()}
|
|
|
4615
3527
|
return { name: v.name, port: v.port, status: variantStatus };
|
|
4616
3528
|
})
|
|
4617
3529
|
);
|
|
4618
|
-
const hasPages =
|
|
3530
|
+
const hasPages = existsSync9(resolve11(mindDir(name), "home", "pages"));
|
|
4619
3531
|
return c.json({ ...entry, ...mindStatus, variants: variantStatuses, hasPages });
|
|
4620
3532
|
}).post("/:name/start", requireAdmin, async (c) => {
|
|
4621
3533
|
const name = c.req.param("name");
|
|
@@ -4629,7 +3541,7 @@ ${user.trimEnd()}
|
|
|
4629
3541
|
targetPort = variant.port;
|
|
4630
3542
|
} else {
|
|
4631
3543
|
const dir = mindDir(baseName);
|
|
4632
|
-
if (!
|
|
3544
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
4633
3545
|
}
|
|
4634
3546
|
if (getMindManager().isRunning(name)) {
|
|
4635
3547
|
return c.json({ error: "Mind already running" }, 409);
|
|
@@ -4652,7 +3564,7 @@ ${user.trimEnd()}
|
|
|
4652
3564
|
targetPort = variant.port;
|
|
4653
3565
|
} else {
|
|
4654
3566
|
const dir = mindDir(baseName);
|
|
4655
|
-
if (!
|
|
3567
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
4656
3568
|
}
|
|
4657
3569
|
let context;
|
|
4658
3570
|
const contentType = c.req.header("content-type");
|
|
@@ -4679,7 +3591,7 @@ ${user.trimEnd()}
|
|
|
4679
3591
|
const variant = findVariant(baseName, mergeVariantName);
|
|
4680
3592
|
if (variant) {
|
|
4681
3593
|
const projectRoot = mindDir(baseName);
|
|
4682
|
-
if (
|
|
3594
|
+
if (existsSync9(variant.path)) {
|
|
4683
3595
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
4684
3596
|
if (status) {
|
|
4685
3597
|
try {
|
|
@@ -4707,20 +3619,7 @@ ${user.trimEnd()}
|
|
|
4707
3619
|
}
|
|
4708
3620
|
}
|
|
4709
3621
|
await gitExec(["merge", variant.branch], { cwd: projectRoot });
|
|
4710
|
-
|
|
4711
|
-
try {
|
|
4712
|
-
await gitExec(["worktree", "remove", "--force", variant.path], {
|
|
4713
|
-
cwd: projectRoot
|
|
4714
|
-
});
|
|
4715
|
-
} catch {
|
|
4716
|
-
}
|
|
4717
|
-
}
|
|
4718
|
-
try {
|
|
4719
|
-
await gitExec(["branch", "-D", variant.branch], { cwd: projectRoot });
|
|
4720
|
-
} catch {
|
|
4721
|
-
}
|
|
4722
|
-
removeVariant(baseName, mergeVariantName);
|
|
4723
|
-
chownMindDir(projectRoot, baseName);
|
|
3622
|
+
await cleanupVariant(baseName, mergeVariantName, projectRoot, variant.path);
|
|
4724
3623
|
try {
|
|
4725
3624
|
await npmInstallAsMind(projectRoot, baseName);
|
|
4726
3625
|
} catch (e) {
|
|
@@ -4768,6 +3667,53 @@ ${user.trimEnd()}
|
|
|
4768
3667
|
} catch (err) {
|
|
4769
3668
|
return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
|
|
4770
3669
|
}
|
|
3670
|
+
}).get("/:name/sleep", requireAdmin, async (c) => {
|
|
3671
|
+
const name = c.req.param("name");
|
|
3672
|
+
const entry = findMind(name);
|
|
3673
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3674
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
3675
|
+
const sm = getSleepManagerIfReady();
|
|
3676
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3677
|
+
return c.json(sm.getState(name));
|
|
3678
|
+
}).post("/:name/sleep", requireAdmin, async (c) => {
|
|
3679
|
+
const name = c.req.param("name");
|
|
3680
|
+
const entry = findMind(name);
|
|
3681
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3682
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
3683
|
+
const sm = getSleepManagerIfReady();
|
|
3684
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3685
|
+
if (sm.isSleeping(name)) return c.json({ error: "Mind is already sleeping" }, 409);
|
|
3686
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3687
|
+
const wakeAt = body.wakeAt;
|
|
3688
|
+
if (wakeAt) {
|
|
3689
|
+
const wakeDate = new Date(wakeAt);
|
|
3690
|
+
if (Number.isNaN(wakeDate.getTime()) || wakeDate <= /* @__PURE__ */ new Date()) {
|
|
3691
|
+
return c.json({ error: "wakeAt must be a valid future ISO date" }, 400);
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
sm.initiateSleep(name, wakeAt ? { voluntaryWakeAt: wakeAt } : void 0).catch(
|
|
3695
|
+
(err) => logger_default.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
|
|
3696
|
+
);
|
|
3697
|
+
return c.json({ ok: true });
|
|
3698
|
+
}).post("/:name/wake", requireAdmin, async (c) => {
|
|
3699
|
+
const name = c.req.param("name");
|
|
3700
|
+
const entry = findMind(name);
|
|
3701
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3702
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
3703
|
+
const sm = getSleepManagerIfReady();
|
|
3704
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3705
|
+
if (!sm.isSleeping(name)) return c.json({ error: "Mind is not sleeping" }, 409);
|
|
3706
|
+
sm.initiateWake(name).catch((err) => logger_default.error(`failed to wake ${name}`, logger_default.errorData(err)));
|
|
3707
|
+
return c.json({ ok: true });
|
|
3708
|
+
}).post("/:name/sleep/messages", requireAdmin, async (c) => {
|
|
3709
|
+
const name = c.req.param("name");
|
|
3710
|
+
const entry = findMind(name);
|
|
3711
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3712
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
3713
|
+
const sm = getSleepManagerIfReady();
|
|
3714
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3715
|
+
const flushed = await sm.flushQueuedMessages(name);
|
|
3716
|
+
return c.json({ ok: true, flushed });
|
|
4771
3717
|
}).post("/:name/sprout", requireAdmin, async (c) => {
|
|
4772
3718
|
const name = c.req.param("name");
|
|
4773
3719
|
const entry = findMind(name);
|
|
@@ -4796,20 +3742,25 @@ ${user.trimEnd()}
|
|
|
4796
3742
|
removeMind(name);
|
|
4797
3743
|
await deleteMindUser2(name);
|
|
4798
3744
|
const state = stateDir(name);
|
|
4799
|
-
if (
|
|
4800
|
-
|
|
3745
|
+
if (existsSync9(state)) {
|
|
3746
|
+
rmSync3(state, { recursive: true, force: true });
|
|
4801
3747
|
}
|
|
4802
|
-
if (force &&
|
|
4803
|
-
|
|
3748
|
+
if (force && existsSync9(dir)) {
|
|
3749
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
4804
3750
|
deleteMindUser(name);
|
|
4805
3751
|
}
|
|
3752
|
+
fireWebhook({
|
|
3753
|
+
event: "mind_deleted",
|
|
3754
|
+
mind: name,
|
|
3755
|
+
data: { port: entry.port, stage: entry.stage, template: entry.template }
|
|
3756
|
+
});
|
|
4806
3757
|
return c.json({ ok: true });
|
|
4807
3758
|
}).post("/:name/upgrade", requireAdmin, async (c) => {
|
|
4808
3759
|
const mindName = c.req.param("name");
|
|
4809
3760
|
const entry = findMind(mindName);
|
|
4810
3761
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4811
3762
|
const dir = mindDir(mindName);
|
|
4812
|
-
if (!
|
|
3763
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
4813
3764
|
let body = {};
|
|
4814
3765
|
try {
|
|
4815
3766
|
body = await c.req.json();
|
|
@@ -4817,9 +3768,32 @@ ${user.trimEnd()}
|
|
|
4817
3768
|
}
|
|
4818
3769
|
const template = body.template ?? entry.template ?? "claude";
|
|
4819
3770
|
const UPGRADE_VARIANT = "upgrade";
|
|
3771
|
+
if (body.abort) {
|
|
3772
|
+
const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
|
|
3773
|
+
if (!existsSync9(worktreeDir2)) {
|
|
3774
|
+
return c.json({ error: "No upgrade in progress" }, 400);
|
|
3775
|
+
}
|
|
3776
|
+
try {
|
|
3777
|
+
try {
|
|
3778
|
+
const gitDirContent = readFileSync8(resolve11(worktreeDir2, ".git"), "utf-8").trim();
|
|
3779
|
+
const gitDir = gitDirContent.replace("gitdir: ", "");
|
|
3780
|
+
if (existsSync9(resolve11(gitDir, "MERGE_HEAD"))) {
|
|
3781
|
+
await gitExec(["merge", "--abort"], { cwd: worktreeDir2 });
|
|
3782
|
+
}
|
|
3783
|
+
} catch {
|
|
3784
|
+
}
|
|
3785
|
+
await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2, { stop: true });
|
|
3786
|
+
return c.json({ ok: true });
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
return c.json(
|
|
3789
|
+
{ error: err instanceof Error ? err.message : "Failed to abort upgrade" },
|
|
3790
|
+
500
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
4820
3794
|
if (body.continue) {
|
|
4821
|
-
const worktreeDir2 =
|
|
4822
|
-
if (!
|
|
3795
|
+
const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
|
|
3796
|
+
if (!existsSync9(worktreeDir2)) {
|
|
4823
3797
|
return c.json({ error: "No upgrade in progress" }, 400);
|
|
4824
3798
|
}
|
|
4825
3799
|
const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
|
|
@@ -4832,7 +3806,10 @@ ${user.trimEnd()}
|
|
|
4832
3806
|
await gitExec(["commit", "-m", "merge template update"], { cwd: worktreeDir2 });
|
|
4833
3807
|
} catch (e) {
|
|
4834
3808
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4835
|
-
|
|
3809
|
+
const stderr = e?.stderr ?? "";
|
|
3810
|
+
const stdout = e?.stdout ?? "";
|
|
3811
|
+
if (!msg.includes("nothing to commit") && !stderr.includes("nothing to commit") && !stdout.includes("nothing to commit"))
|
|
3812
|
+
throw e;
|
|
4836
3813
|
}
|
|
4837
3814
|
chownMindDir(dir, mindName);
|
|
4838
3815
|
try {
|
|
@@ -4853,49 +3830,30 @@ ${user.trimEnd()}
|
|
|
4853
3830
|
port: variantPort
|
|
4854
3831
|
});
|
|
4855
3832
|
} catch (err) {
|
|
4856
|
-
|
|
4857
|
-
removeVariant(mindName, UPGRADE_VARIANT);
|
|
4858
|
-
} catch {
|
|
4859
|
-
}
|
|
4860
|
-
try {
|
|
4861
|
-
await gitExec(["worktree", "remove", "--force", worktreeDir2], { cwd: dir });
|
|
4862
|
-
} catch {
|
|
4863
|
-
}
|
|
4864
|
-
try {
|
|
4865
|
-
await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
4866
|
-
} catch {
|
|
4867
|
-
}
|
|
4868
|
-
try {
|
|
4869
|
-
chownMindDir(dir, mindName);
|
|
4870
|
-
} catch (chownErr) {
|
|
4871
|
-
logger_default.error(
|
|
4872
|
-
`failed to fix ownership during upgrade cleanup for ${mindName}`,
|
|
4873
|
-
logger_default.errorData(chownErr)
|
|
4874
|
-
);
|
|
4875
|
-
}
|
|
3833
|
+
await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2);
|
|
4876
3834
|
return c.json(
|
|
4877
3835
|
{ error: err instanceof Error ? err.message : "Failed to continue upgrade" },
|
|
4878
3836
|
500
|
|
4879
3837
|
);
|
|
4880
3838
|
}
|
|
4881
3839
|
}
|
|
4882
|
-
const worktreeDir =
|
|
4883
|
-
if (
|
|
3840
|
+
const worktreeDir = resolve11(dir, ".variants", UPGRADE_VARIANT);
|
|
3841
|
+
if (existsSync9(worktreeDir)) {
|
|
4884
3842
|
return c.json(
|
|
4885
3843
|
{ error: "Upgrade variant already exists. Use continue or delete it first." },
|
|
4886
3844
|
409
|
|
4887
3845
|
);
|
|
4888
3846
|
}
|
|
4889
|
-
if (!
|
|
3847
|
+
if (!existsSync9(resolve11(dir, ".git"))) {
|
|
4890
3848
|
try {
|
|
4891
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3849
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dir, "home") } : void 0;
|
|
4892
3850
|
await gitExec(["init"], { cwd: dir, mindName, env });
|
|
4893
3851
|
await configureGitIdentity(mindName, { cwd: dir, mindName, env });
|
|
4894
3852
|
await gitExec(["add", "-A"], { cwd: dir, mindName, env });
|
|
4895
3853
|
await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
|
|
4896
3854
|
chownMindDir(dir, mindName);
|
|
4897
3855
|
} catch (err) {
|
|
4898
|
-
|
|
3856
|
+
rmSync3(resolve11(dir, ".git"), { recursive: true, force: true });
|
|
4899
3857
|
return c.json(
|
|
4900
3858
|
{
|
|
4901
3859
|
error: `Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4909,7 +3867,7 @@ ${user.trimEnd()}
|
|
|
4909
3867
|
await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
4910
3868
|
} catch {
|
|
4911
3869
|
}
|
|
4912
|
-
if (!
|
|
3870
|
+
if (!existsSync9(resolve11(dir, "home", "shared"))) {
|
|
4913
3871
|
try {
|
|
4914
3872
|
await addSharedWorktree(mindName, dir);
|
|
4915
3873
|
} catch (err) {
|
|
@@ -4920,9 +3878,9 @@ ${user.trimEnd()}
|
|
|
4920
3878
|
}
|
|
4921
3879
|
}
|
|
4922
3880
|
await updateTemplateBranch(dir, template, mindName);
|
|
4923
|
-
const parentDir =
|
|
4924
|
-
if (!
|
|
4925
|
-
|
|
3881
|
+
const parentDir = resolve11(dir, ".variants");
|
|
3882
|
+
if (!existsSync9(parentDir)) {
|
|
3883
|
+
mkdirSync5(parentDir, { recursive: true });
|
|
4926
3884
|
}
|
|
4927
3885
|
await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
4928
3886
|
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
@@ -4953,26 +3911,7 @@ ${user.trimEnd()}
|
|
|
4953
3911
|
port: variantPort
|
|
4954
3912
|
});
|
|
4955
3913
|
} catch (err) {
|
|
4956
|
-
|
|
4957
|
-
removeVariant(mindName, UPGRADE_VARIANT);
|
|
4958
|
-
} catch {
|
|
4959
|
-
}
|
|
4960
|
-
try {
|
|
4961
|
-
await gitExec(["worktree", "remove", "--force", worktreeDir], { cwd: dir });
|
|
4962
|
-
} catch {
|
|
4963
|
-
}
|
|
4964
|
-
try {
|
|
4965
|
-
await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
4966
|
-
} catch {
|
|
4967
|
-
}
|
|
4968
|
-
try {
|
|
4969
|
-
chownMindDir(dir, mindName);
|
|
4970
|
-
} catch (chownErr) {
|
|
4971
|
-
logger_default.error(
|
|
4972
|
-
`failed to fix ownership during upgrade cleanup for ${mindName}`,
|
|
4973
|
-
logger_default.errorData(chownErr)
|
|
4974
|
-
);
|
|
4975
|
-
}
|
|
3914
|
+
await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir);
|
|
4976
3915
|
return c.json(
|
|
4977
3916
|
{ error: err instanceof Error ? err.message : "Failed to complete upgrade" },
|
|
4978
3917
|
500
|
|
@@ -4987,6 +3926,38 @@ ${user.trimEnd()}
|
|
|
4987
3926
|
const variant = findVariant(baseName, variantName);
|
|
4988
3927
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
4989
3928
|
}
|
|
3929
|
+
try {
|
|
3930
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
3931
|
+
const sm = getSleepManagerIfReady();
|
|
3932
|
+
if (sm?.isSleeping(baseName)) {
|
|
3933
|
+
const body2 = await c.req.text();
|
|
3934
|
+
let parsed2 = null;
|
|
3935
|
+
try {
|
|
3936
|
+
parsed2 = JSON.parse(body2);
|
|
3937
|
+
} catch {
|
|
3938
|
+
}
|
|
3939
|
+
if (parsed2) {
|
|
3940
|
+
const payload = {
|
|
3941
|
+
channel: parsed2.channel ?? "unknown",
|
|
3942
|
+
sender: parsed2.sender ?? null,
|
|
3943
|
+
content: parsed2.content,
|
|
3944
|
+
isDM: parsed2.isDM
|
|
3945
|
+
};
|
|
3946
|
+
if (sm.checkWakeTrigger(baseName, payload)) {
|
|
3947
|
+
await sm.queueSleepMessage(baseName, payload);
|
|
3948
|
+
sm.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch(
|
|
3949
|
+
(err) => logger_default.error(`failed to trigger-wake ${baseName}`, logger_default.errorData(err))
|
|
3950
|
+
);
|
|
3951
|
+
return c.json({ ok: true, queued: true, triggerWake: true });
|
|
3952
|
+
}
|
|
3953
|
+
await sm.queueSleepMessage(baseName, payload);
|
|
3954
|
+
return c.json({ ok: true, queued: true });
|
|
3955
|
+
}
|
|
3956
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
3957
|
+
}
|
|
3958
|
+
} catch (err) {
|
|
3959
|
+
logger_default.error(`failed to check sleep state for ${baseName}`, logger_default.errorData(err));
|
|
3960
|
+
}
|
|
4990
3961
|
if (!getMindManager().isRunning(name)) {
|
|
4991
3962
|
return c.json({ error: "Mind is not running" }, 409);
|
|
4992
3963
|
}
|
|
@@ -4998,21 +3969,10 @@ ${user.trimEnd()}
|
|
|
4998
3969
|
logger_default.error(`failed to parse message body for ${baseName}`, logger_default.errorData(err));
|
|
4999
3970
|
}
|
|
5000
3971
|
const channel = parsed?.channel ?? "unknown";
|
|
5001
|
-
const db = await getDb();
|
|
5002
3972
|
if (parsed) {
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
await db.insert(mindHistory).values({
|
|
5007
|
-
mind: baseName,
|
|
5008
|
-
type: "inbound",
|
|
5009
|
-
channel,
|
|
5010
|
-
sender: sender2,
|
|
5011
|
-
content
|
|
5012
|
-
});
|
|
5013
|
-
} catch (err) {
|
|
5014
|
-
logger_default.error(`failed to persist inbound message for ${baseName}`, logger_default.errorData(err));
|
|
5015
|
-
}
|
|
3973
|
+
const sender2 = parsed.sender ?? null;
|
|
3974
|
+
const content = extractTextContent(parsed.content);
|
|
3975
|
+
await recordInbound(baseName, channel, sender2, content);
|
|
5016
3976
|
}
|
|
5017
3977
|
const budget = getTokenBudget();
|
|
5018
3978
|
const budgetStatus = budget.checkBudget(baseName);
|
|
@@ -5064,6 +4024,7 @@ ${user.trimEnd()}
|
|
|
5064
4024
|
const seedEntry = findMind(baseName);
|
|
5065
4025
|
if (seedEntry?.stage === "seed") {
|
|
5066
4026
|
try {
|
|
4027
|
+
const db = await getDb();
|
|
5067
4028
|
const countResult = await db.select({ count: sql2`count(*)` }).from(mindHistory).where(eq4(mindHistory.mind, baseName));
|
|
5068
4029
|
const msgCount = countResult[0]?.count ?? 0;
|
|
5069
4030
|
if (msgCount >= 10 && msgCount % 10 === 0) {
|
|
@@ -5109,13 +4070,13 @@ ${user.trimEnd()}
|
|
|
5109
4070
|
const entry = findMind(name);
|
|
5110
4071
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
5111
4072
|
const dir = mindDir(name);
|
|
5112
|
-
if (!
|
|
4073
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
5113
4074
|
let config = readVoluteConfig(dir);
|
|
5114
4075
|
if (!config && entry.template === "pi") {
|
|
5115
|
-
const piConfigPath =
|
|
5116
|
-
if (
|
|
4076
|
+
const piConfigPath = resolve11(dir, "home/.config/config.json");
|
|
4077
|
+
if (existsSync9(piConfigPath)) {
|
|
5117
4078
|
try {
|
|
5118
|
-
config = JSON.parse(
|
|
4079
|
+
config = JSON.parse(readFileSync8(piConfigPath, "utf-8"));
|
|
5119
4080
|
} catch {
|
|
5120
4081
|
}
|
|
5121
4082
|
}
|
|
@@ -5152,7 +4113,7 @@ ${user.trimEnd()}
|
|
|
5152
4113
|
const entry = findMind(name);
|
|
5153
4114
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
5154
4115
|
const dir = mindDir(name);
|
|
5155
|
-
if (!
|
|
4116
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
5156
4117
|
const body = c.req.valid("json");
|
|
5157
4118
|
const existing = readVoluteConfig(dir) ?? {};
|
|
5158
4119
|
if (body.model !== void 0) existing.model = body.model;
|
|
@@ -5215,7 +4176,7 @@ ${user.trimEnd()}
|
|
|
5215
4176
|
} catch (err) {
|
|
5216
4177
|
logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
|
|
5217
4178
|
}
|
|
5218
|
-
|
|
4179
|
+
publish(baseName, {
|
|
5219
4180
|
mind: baseName,
|
|
5220
4181
|
type: body.type,
|
|
5221
4182
|
session: body.session,
|
|
@@ -5265,14 +4226,29 @@ ${user.trimEnd()}
|
|
|
5265
4226
|
|
|
5266
4227
|
`));
|
|
5267
4228
|
};
|
|
5268
|
-
|
|
4229
|
+
let unsubscribe;
|
|
4230
|
+
const pingInterval = setInterval(() => {
|
|
4231
|
+
try {
|
|
4232
|
+
controller.enqueue(encoder.encode(": ping\n\n"));
|
|
4233
|
+
} catch {
|
|
4234
|
+
clearInterval(pingInterval);
|
|
4235
|
+
unsubscribe?.();
|
|
4236
|
+
}
|
|
4237
|
+
}, 15e3);
|
|
4238
|
+
unsubscribe = subscribe2(baseName, (event) => {
|
|
5269
4239
|
if (typeFilter && !typeFilter.includes(event.type)) return;
|
|
5270
4240
|
if (sessionFilter && event.session !== sessionFilter) return;
|
|
5271
4241
|
if (channelFilter && event.channel !== channelFilter) return;
|
|
5272
|
-
|
|
4242
|
+
try {
|
|
4243
|
+
send5(JSON.stringify(event));
|
|
4244
|
+
} catch {
|
|
4245
|
+
clearInterval(pingInterval);
|
|
4246
|
+
unsubscribe?.();
|
|
4247
|
+
}
|
|
5273
4248
|
});
|
|
5274
4249
|
c.req.raw.signal.addEventListener("abort", () => {
|
|
5275
|
-
|
|
4250
|
+
clearInterval(pingInterval);
|
|
4251
|
+
unsubscribe?.();
|
|
5276
4252
|
try {
|
|
5277
4253
|
controller.close();
|
|
5278
4254
|
} catch {
|
|
@@ -5360,7 +4336,7 @@ var minds_default = app11;
|
|
|
5360
4336
|
|
|
5361
4337
|
// src/web/api/pages.ts
|
|
5362
4338
|
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
5363
|
-
import { extname as extname2, resolve as
|
|
4339
|
+
import { extname as extname2, resolve as resolve12 } from "path";
|
|
5364
4340
|
import { Hono as Hono12 } from "hono";
|
|
5365
4341
|
var MIME_TYPES = {
|
|
5366
4342
|
".html": "text/html",
|
|
@@ -5382,17 +4358,17 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
|
|
|
5382
4358
|
const name = c.req.param("name");
|
|
5383
4359
|
let pagesRoot;
|
|
5384
4360
|
if (name === "_system") {
|
|
5385
|
-
pagesRoot =
|
|
4361
|
+
pagesRoot = resolve12(voluteHome(), "shared", "pages");
|
|
5386
4362
|
} else {
|
|
5387
4363
|
if (!findMind(name)) return c.text("Not found", 404);
|
|
5388
|
-
pagesRoot =
|
|
4364
|
+
pagesRoot = resolve12(mindDir(name), "home", "pages");
|
|
5389
4365
|
}
|
|
5390
4366
|
const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
|
|
5391
|
-
const requestedPath =
|
|
4367
|
+
const requestedPath = resolve12(pagesRoot, wildcard.slice(1));
|
|
5392
4368
|
if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
|
|
5393
4369
|
let fileStat = await stat2(requestedPath).catch(() => null);
|
|
5394
4370
|
if (fileStat?.isDirectory()) {
|
|
5395
|
-
const indexPath =
|
|
4371
|
+
const indexPath = resolve12(requestedPath, "index.html");
|
|
5396
4372
|
fileStat = await stat2(indexPath).catch(() => null);
|
|
5397
4373
|
if (fileStat?.isFile()) {
|
|
5398
4374
|
const body = await readFile2(indexPath);
|
|
@@ -5462,9 +4438,9 @@ var app13 = new Hono13().get("/", async (c) => {
|
|
|
5462
4438
|
var prompts_default = app13;
|
|
5463
4439
|
|
|
5464
4440
|
// src/web/api/schedules.ts
|
|
5465
|
-
import { CronExpressionParser
|
|
4441
|
+
import { CronExpressionParser } from "cron-parser";
|
|
5466
4442
|
import { Hono as Hono14 } from "hono";
|
|
5467
|
-
var
|
|
4443
|
+
var slog = logger_default.child("schedules");
|
|
5468
4444
|
function readSchedules(name) {
|
|
5469
4445
|
return readVoluteConfig(mindDir(name))?.schedules ?? [];
|
|
5470
4446
|
}
|
|
@@ -5474,6 +4450,11 @@ function writeSchedules(name, schedules) {
|
|
|
5474
4450
|
config.schedules = schedules.length > 0 ? schedules : void 0;
|
|
5475
4451
|
writeVoluteConfig(dir, config);
|
|
5476
4452
|
getScheduler().loadSchedules(name);
|
|
4453
|
+
fireWebhook({
|
|
4454
|
+
event: "schedule_changed",
|
|
4455
|
+
mind: name,
|
|
4456
|
+
data: { schedules }
|
|
4457
|
+
});
|
|
5477
4458
|
}
|
|
5478
4459
|
var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
5479
4460
|
const name = c.req.param("name");
|
|
@@ -5496,7 +4477,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
|
5496
4477
|
return c.json({ error: "message and script are mutually exclusive" }, 400);
|
|
5497
4478
|
}
|
|
5498
4479
|
try {
|
|
5499
|
-
|
|
4480
|
+
CronExpressionParser.parse(body.cron);
|
|
5500
4481
|
} catch {
|
|
5501
4482
|
return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
|
|
5502
4483
|
}
|
|
@@ -5524,7 +4505,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
|
5524
4505
|
}
|
|
5525
4506
|
if (body.cron !== void 0) {
|
|
5526
4507
|
try {
|
|
5527
|
-
|
|
4508
|
+
CronExpressionParser.parse(body.cron);
|
|
5528
4509
|
} catch {
|
|
5529
4510
|
return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
|
|
5530
4511
|
}
|
|
@@ -5574,7 +4555,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
|
5574
4555
|
}
|
|
5575
4556
|
return c.json({ ok: true });
|
|
5576
4557
|
} catch (err) {
|
|
5577
|
-
|
|
4558
|
+
slog.warn(`webhook delivery failed for ${name}`, logger_default.errorData(err));
|
|
5578
4559
|
return c.json({ error: "Failed to reach mind" }, 502);
|
|
5579
4560
|
}
|
|
5580
4561
|
});
|
|
@@ -5634,9 +4615,9 @@ var app15 = new Hono15().post("/:name/shared/merge", requireAdmin, async (c) =>
|
|
|
5634
4615
|
var shared_default = app15;
|
|
5635
4616
|
|
|
5636
4617
|
// src/web/api/skills.ts
|
|
5637
|
-
import { existsSync as
|
|
4618
|
+
import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "fs";
|
|
5638
4619
|
import { tmpdir } from "os";
|
|
5639
|
-
import { join as
|
|
4620
|
+
import { join as join2, resolve as resolve13 } from "path";
|
|
5640
4621
|
import AdmZip from "adm-zip";
|
|
5641
4622
|
import { Hono as Hono16 } from "hono";
|
|
5642
4623
|
var app16 = new Hono16().get("/", async (c) => {
|
|
@@ -5646,7 +4627,7 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
5646
4627
|
const id = c.req.param("id");
|
|
5647
4628
|
const skill = await getSharedSkill(id);
|
|
5648
4629
|
if (!skill) return c.json({ error: "Skill not found" }, 404);
|
|
5649
|
-
const dir =
|
|
4630
|
+
const dir = join2(sharedSkillsDir(), id);
|
|
5650
4631
|
const files = listFilesRecursive(dir);
|
|
5651
4632
|
return c.json({ ...skill, files });
|
|
5652
4633
|
}).post("/upload", requireAdmin, async (c) => {
|
|
@@ -5658,25 +4639,25 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
5658
4639
|
if (!file.name.endsWith(".zip")) {
|
|
5659
4640
|
return c.json({ error: "Only .zip files are accepted" }, 400);
|
|
5660
4641
|
}
|
|
5661
|
-
const
|
|
5662
|
-
const tmpDir = mkdtempSync(
|
|
4642
|
+
const buffer2 = Buffer.from(await file.arrayBuffer());
|
|
4643
|
+
const tmpDir = mkdtempSync(join2(tmpdir(), "volute-skill-upload-"));
|
|
5663
4644
|
try {
|
|
5664
|
-
const zip = new AdmZip(
|
|
4645
|
+
const zip = new AdmZip(buffer2);
|
|
5665
4646
|
for (const entry of zip.getEntries()) {
|
|
5666
|
-
const target =
|
|
4647
|
+
const target = resolve13(tmpDir, entry.entryName);
|
|
5667
4648
|
if (!target.startsWith(tmpDir)) {
|
|
5668
4649
|
return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
|
|
5669
4650
|
}
|
|
5670
4651
|
}
|
|
5671
4652
|
zip.extractAllTo(tmpDir, true);
|
|
5672
4653
|
let skillDir = null;
|
|
5673
|
-
if (
|
|
4654
|
+
if (existsSync10(join2(tmpDir, "SKILL.md"))) {
|
|
5674
4655
|
skillDir = tmpDir;
|
|
5675
4656
|
} else {
|
|
5676
|
-
const entries =
|
|
4657
|
+
const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
5677
4658
|
for (const entry of entries) {
|
|
5678
|
-
if (
|
|
5679
|
-
skillDir =
|
|
4659
|
+
if (existsSync10(join2(tmpDir, entry.name, "SKILL.md"))) {
|
|
4660
|
+
skillDir = join2(tmpDir, entry.name);
|
|
5680
4661
|
break;
|
|
5681
4662
|
}
|
|
5682
4663
|
}
|
|
@@ -5692,7 +4673,7 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
5692
4673
|
}
|
|
5693
4674
|
throw e;
|
|
5694
4675
|
} finally {
|
|
5695
|
-
|
|
4676
|
+
rmSync4(tmpDir, { recursive: true, force: true });
|
|
5696
4677
|
}
|
|
5697
4678
|
}).delete("/:id", requireAdmin, async (c) => {
|
|
5698
4679
|
const id = c.req.param("id");
|
|
@@ -5717,93 +4698,487 @@ var app17 = new Hono17().post("/restart", requireAdmin, (c) => {
|
|
|
5717
4698
|
return c.json({ ok: true });
|
|
5718
4699
|
}).get("/logs", async (c) => {
|
|
5719
4700
|
const user = c.get("user");
|
|
5720
|
-
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
5721
|
-
return streamSSE3(c, async (stream) => {
|
|
5722
|
-
for (const entry of logBuffer.getEntries()) {
|
|
5723
|
-
await stream.writeSSE({ data: JSON.stringify(entry) });
|
|
5724
|
-
}
|
|
5725
|
-
const unsubscribe = logBuffer.subscribe((entry) => {
|
|
5726
|
-
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
4701
|
+
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
4702
|
+
return streamSSE3(c, async (stream) => {
|
|
4703
|
+
for (const entry of logBuffer.getEntries()) {
|
|
4704
|
+
await stream.writeSSE({ data: JSON.stringify(entry) });
|
|
4705
|
+
}
|
|
4706
|
+
const unsubscribe = logBuffer.subscribe((entry) => {
|
|
4707
|
+
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
4708
|
+
});
|
|
4709
|
+
});
|
|
4710
|
+
await new Promise((resolve18) => {
|
|
4711
|
+
stream.onAbort(() => {
|
|
4712
|
+
unsubscribe();
|
|
4713
|
+
resolve18();
|
|
4714
|
+
});
|
|
4715
|
+
});
|
|
4716
|
+
});
|
|
4717
|
+
}).get("/info", (c) => {
|
|
4718
|
+
const config = readSystemsConfig();
|
|
4719
|
+
return c.json({ system: config?.system ?? null });
|
|
4720
|
+
});
|
|
4721
|
+
var system_default = app17;
|
|
4722
|
+
|
|
4723
|
+
// src/web/api/typing.ts
|
|
4724
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
4725
|
+
import { Hono as Hono18 } from "hono";
|
|
4726
|
+
import { z as z5 } from "zod";
|
|
4727
|
+
var typingSchema = z5.object({
|
|
4728
|
+
channel: z5.string().min(1),
|
|
4729
|
+
sender: z5.string().min(1),
|
|
4730
|
+
active: z5.boolean()
|
|
4731
|
+
});
|
|
4732
|
+
var app18 = new Hono18().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
|
|
4733
|
+
const { channel, sender, active } = c.req.valid("json");
|
|
4734
|
+
const map = getTypingMap();
|
|
4735
|
+
if (active) {
|
|
4736
|
+
map.set(channel, sender);
|
|
4737
|
+
} else {
|
|
4738
|
+
map.delete(channel, sender);
|
|
4739
|
+
}
|
|
4740
|
+
const volutePrefix = "volute:";
|
|
4741
|
+
if (channel.startsWith(volutePrefix)) {
|
|
4742
|
+
const conversationId = channel.slice(volutePrefix.length);
|
|
4743
|
+
publish2(conversationId, { type: "typing", senders: map.get(channel) });
|
|
4744
|
+
}
|
|
4745
|
+
return c.json({ ok: true });
|
|
4746
|
+
}).get("/:name/typing", (c) => {
|
|
4747
|
+
const channel = c.req.query("channel");
|
|
4748
|
+
if (!channel) {
|
|
4749
|
+
return c.json({ error: "channel query param is required" }, 400);
|
|
4750
|
+
}
|
|
4751
|
+
const map = getTypingMap();
|
|
4752
|
+
return c.json({ typing: map.get(channel) });
|
|
4753
|
+
});
|
|
4754
|
+
var typing_default = app18;
|
|
4755
|
+
|
|
4756
|
+
// src/web/api/update.ts
|
|
4757
|
+
import { spawn as spawn2 } from "child_process";
|
|
4758
|
+
import { Hono as Hono19 } from "hono";
|
|
4759
|
+
var bin;
|
|
4760
|
+
var app19 = new Hono19().get("/update", async (c) => {
|
|
4761
|
+
const result = await checkForUpdate();
|
|
4762
|
+
return c.json(result);
|
|
4763
|
+
}).post("/update", requireAdmin, async (c) => {
|
|
4764
|
+
bin ??= resolveVoluteBin();
|
|
4765
|
+
const child = spawn2(bin, ["update"], {
|
|
4766
|
+
stdio: "ignore",
|
|
4767
|
+
detached: true
|
|
4768
|
+
});
|
|
4769
|
+
child.on("error", (err) => {
|
|
4770
|
+
logger_default.error("Update process error", { error: err.message });
|
|
4771
|
+
});
|
|
4772
|
+
child.unref();
|
|
4773
|
+
return c.json({ ok: true, message: "Updating..." });
|
|
4774
|
+
});
|
|
4775
|
+
var update_default = app19;
|
|
4776
|
+
|
|
4777
|
+
// src/web/api/v1/chat.ts
|
|
4778
|
+
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
4779
|
+
import { Hono as Hono20 } from "hono";
|
|
4780
|
+
import { streamSSE as streamSSE4 } from "hono/streaming";
|
|
4781
|
+
import { z as z6 } from "zod";
|
|
4782
|
+
async function fanOutToMinds(opts) {
|
|
4783
|
+
const participants = await getParticipants(opts.conversationId);
|
|
4784
|
+
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
4785
|
+
const participantNames = participants.map((p) => p.username);
|
|
4786
|
+
const isDM = opts.isDM ?? participants.length === 2;
|
|
4787
|
+
const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
|
|
4788
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-KMY4GA2J.js");
|
|
4789
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
4790
|
+
const manager = getMindManager2();
|
|
4791
|
+
const sm = getSleepManagerIfReady();
|
|
4792
|
+
const targetMinds = mindParticipants.map((ap) => {
|
|
4793
|
+
const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
|
|
4794
|
+
if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
|
|
4795
|
+
return null;
|
|
4796
|
+
}).filter((n) => n !== null && n !== opts.senderName);
|
|
4797
|
+
function slugForMind(mindUsername) {
|
|
4798
|
+
return buildVoluteSlug({
|
|
4799
|
+
participants,
|
|
4800
|
+
mindUsername,
|
|
4801
|
+
convTitle: opts.convTitle,
|
|
4802
|
+
conversationId: opts.conversationId,
|
|
4803
|
+
...opts.slugExtra
|
|
4804
|
+
});
|
|
4805
|
+
}
|
|
4806
|
+
const channelEntry = {
|
|
4807
|
+
platformId: opts.conversationId,
|
|
4808
|
+
platform: "volute",
|
|
4809
|
+
name: opts.convTitle ?? void 0,
|
|
4810
|
+
type: channelEntryType
|
|
4811
|
+
};
|
|
4812
|
+
for (const ap of mindParticipants) {
|
|
4813
|
+
try {
|
|
4814
|
+
writeChannelEntry(ap.username, slugForMind(ap.username), channelEntry);
|
|
4815
|
+
} catch (err) {
|
|
4816
|
+
logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
for (const mindName of targetMinds) {
|
|
4820
|
+
const target = opts.targetName ? opts.targetName(mindName) : mindName;
|
|
4821
|
+
const channel = slugForMind(mindName);
|
|
4822
|
+
const typingMap = getTypingMap();
|
|
4823
|
+
const currentlyTyping = typingMap.get(channel).filter((name) => participantNames.includes(name));
|
|
4824
|
+
deliverMessage(target, {
|
|
4825
|
+
content: opts.contentBlocks,
|
|
4826
|
+
channel,
|
|
4827
|
+
conversationId: opts.conversationId,
|
|
4828
|
+
sender: opts.senderName,
|
|
4829
|
+
participants: participantNames,
|
|
4830
|
+
participantCount: participants.length,
|
|
4831
|
+
isDM,
|
|
4832
|
+
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
4833
|
+
}).catch((err) => {
|
|
4834
|
+
logger_default.warn("[v1-chat] delivery failed", logger_default.errorData(err));
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
var mindChatSchema = z6.object({
|
|
4839
|
+
message: z6.string().optional(),
|
|
4840
|
+
conversationId: z6.string().optional(),
|
|
4841
|
+
sender: z6.string().optional(),
|
|
4842
|
+
images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
|
|
4843
|
+
});
|
|
4844
|
+
var unifiedChatSchema = z6.object({
|
|
4845
|
+
message: z6.string().optional(),
|
|
4846
|
+
conversationId: z6.string(),
|
|
4847
|
+
images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
|
|
4848
|
+
});
|
|
4849
|
+
var app20 = new Hono20().use("*", authMiddleware).post("/minds/:name/chat", zValidator6("json", mindChatSchema), async (c) => {
|
|
4850
|
+
const name = c.req.param("name");
|
|
4851
|
+
const [baseName] = name.split("@", 2);
|
|
4852
|
+
const entry = findMind(baseName);
|
|
4853
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4854
|
+
const body = c.req.valid("json");
|
|
4855
|
+
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
4856
|
+
return c.json({ error: "message or images required" }, 400);
|
|
4857
|
+
}
|
|
4858
|
+
const user = c.get("user");
|
|
4859
|
+
const mindUser = await getOrCreateMindUser(baseName);
|
|
4860
|
+
const senderName = user.id === 0 && body.sender ? body.sender : user.username;
|
|
4861
|
+
let conversationId = body.conversationId;
|
|
4862
|
+
if (conversationId) {
|
|
4863
|
+
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
4864
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4865
|
+
}
|
|
4866
|
+
} else {
|
|
4867
|
+
const participantIds = [];
|
|
4868
|
+
if (user.id !== 0) {
|
|
4869
|
+
participantIds.push(user.id);
|
|
4870
|
+
} else if (body.sender) {
|
|
4871
|
+
const senderMind = findMind(body.sender);
|
|
4872
|
+
if (senderMind) {
|
|
4873
|
+
const senderMindUser = await getOrCreateMindUser(body.sender);
|
|
4874
|
+
participantIds.push(senderMindUser.id);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
participantIds.push(mindUser.id);
|
|
4878
|
+
if (participantIds.length === 2) {
|
|
4879
|
+
const existing = await findDMConversation(baseName, participantIds);
|
|
4880
|
+
if (existing) conversationId = existing;
|
|
4881
|
+
}
|
|
4882
|
+
if (!conversationId) {
|
|
4883
|
+
const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
|
|
4884
|
+
const title = [...participantNames].join(", ");
|
|
4885
|
+
const conv2 = await createConversation(baseName, "volute", {
|
|
4886
|
+
userId: user.id !== 0 ? user.id : void 0,
|
|
4887
|
+
title,
|
|
4888
|
+
participantIds
|
|
4889
|
+
});
|
|
4890
|
+
conversationId = conv2.id;
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
const conv = await getConversation(conversationId);
|
|
4894
|
+
const convTitle = conv?.title ?? null;
|
|
4895
|
+
const contentBlocks = [];
|
|
4896
|
+
if (body.message) contentBlocks.push({ type: "text", text: body.message });
|
|
4897
|
+
if (body.images) {
|
|
4898
|
+
for (const img of body.images) {
|
|
4899
|
+
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
await addMessage(conversationId, "user", senderName, contentBlocks);
|
|
4903
|
+
await fanOutToMinds({
|
|
4904
|
+
conversationId,
|
|
4905
|
+
contentBlocks,
|
|
4906
|
+
senderName,
|
|
4907
|
+
convTitle,
|
|
4908
|
+
targetName: (username) => username === baseName ? name : username
|
|
4909
|
+
});
|
|
4910
|
+
return c.json({ ok: true, conversationId });
|
|
4911
|
+
}).get("/minds/:name/conversations/:id/events", async (c) => {
|
|
4912
|
+
const conversationId = c.req.param("id");
|
|
4913
|
+
const user = c.get("user");
|
|
4914
|
+
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
4915
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4916
|
+
}
|
|
4917
|
+
return streamSSE4(c, async (stream) => {
|
|
4918
|
+
const unsubscribe = subscribe3(conversationId, (event) => {
|
|
4919
|
+
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
4920
|
+
if (!stream.aborted) logger_default.error("[v1-chat] SSE write error:", logger_default.errorData(err));
|
|
5727
4921
|
});
|
|
5728
4922
|
});
|
|
5729
|
-
|
|
4923
|
+
const keepAlive = setInterval(() => {
|
|
4924
|
+
stream.writeSSE({ data: "" }).catch((err) => {
|
|
4925
|
+
if (!stream.aborted) logger_default.error("[v1-chat] SSE ping error:", logger_default.errorData(err));
|
|
4926
|
+
});
|
|
4927
|
+
}, 15e3);
|
|
4928
|
+
await new Promise((resolve18) => {
|
|
5730
4929
|
stream.onAbort(() => {
|
|
5731
4930
|
unsubscribe();
|
|
5732
|
-
|
|
4931
|
+
clearInterval(keepAlive);
|
|
4932
|
+
resolve18();
|
|
5733
4933
|
});
|
|
5734
4934
|
});
|
|
5735
4935
|
});
|
|
5736
|
-
}).
|
|
5737
|
-
const
|
|
5738
|
-
|
|
4936
|
+
}).post("/chat", zValidator6("json", unifiedChatSchema), async (c) => {
|
|
4937
|
+
const user = c.get("user");
|
|
4938
|
+
const body = c.req.valid("json");
|
|
4939
|
+
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
4940
|
+
return c.json({ error: "message or images required" }, 400);
|
|
4941
|
+
}
|
|
4942
|
+
const conv = await getConversation(body.conversationId);
|
|
4943
|
+
if (!conv) return c.json({ error: "Conversation not found" }, 404);
|
|
4944
|
+
if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
|
|
4945
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4946
|
+
}
|
|
4947
|
+
const senderName = user.username;
|
|
4948
|
+
const contentBlocks = [];
|
|
4949
|
+
if (body.message) contentBlocks.push({ type: "text", text: body.message });
|
|
4950
|
+
if (body.images) {
|
|
4951
|
+
for (const img of body.images) {
|
|
4952
|
+
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
await addMessage(body.conversationId, "user", senderName, contentBlocks);
|
|
4956
|
+
const isDM = conv.type === "dm";
|
|
4957
|
+
await fanOutToMinds({
|
|
4958
|
+
conversationId: body.conversationId,
|
|
4959
|
+
contentBlocks,
|
|
4960
|
+
senderName,
|
|
4961
|
+
convTitle: conv.title,
|
|
4962
|
+
isDM,
|
|
4963
|
+
channelEntryType: conv.type === "channel" ? "group" : isDM ? "dm" : "group",
|
|
4964
|
+
slugExtra: { convType: conv.type, convName: conv.name }
|
|
4965
|
+
});
|
|
4966
|
+
return c.json({ ok: true, conversationId: body.conversationId });
|
|
5739
4967
|
});
|
|
5740
|
-
var
|
|
4968
|
+
var chat_default = app20;
|
|
5741
4969
|
|
|
5742
|
-
// src/web/api/
|
|
5743
|
-
import { zValidator as
|
|
5744
|
-
import { Hono as
|
|
5745
|
-
import { z as
|
|
5746
|
-
var
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
active: z5.boolean()
|
|
4970
|
+
// src/web/api/v1/conversations.ts
|
|
4971
|
+
import { zValidator as zValidator7 } from "@hono/zod-validator";
|
|
4972
|
+
import { Hono as Hono21 } from "hono";
|
|
4973
|
+
import { z as z7 } from "zod";
|
|
4974
|
+
var createSchema = z7.object({
|
|
4975
|
+
title: z7.string().optional(),
|
|
4976
|
+
participantNames: z7.array(z7.string()).min(1)
|
|
5750
4977
|
});
|
|
5751
|
-
var
|
|
5752
|
-
const
|
|
5753
|
-
const
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
4978
|
+
var app21 = new Hono21().use("*", authMiddleware).get("/", async (c) => {
|
|
4979
|
+
const user = c.get("user");
|
|
4980
|
+
const convs = await listConversationsWithParticipants(user.id);
|
|
4981
|
+
return c.json(convs);
|
|
4982
|
+
}).get("/:id/messages", async (c) => {
|
|
4983
|
+
const id = c.req.param("id");
|
|
4984
|
+
const user = c.get("user");
|
|
4985
|
+
if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
|
|
4986
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
5758
4987
|
}
|
|
5759
|
-
const
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
4988
|
+
const beforeStr = c.req.query("before");
|
|
4989
|
+
const limitStr = c.req.query("limit");
|
|
4990
|
+
if (!beforeStr && !limitStr) {
|
|
4991
|
+
const msgs = await getMessages(id);
|
|
4992
|
+
return c.json({ items: msgs, hasMore: false });
|
|
5763
4993
|
}
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
return c.json({ error: "channel query param is required" }, 400);
|
|
4994
|
+
const before = beforeStr ? parseInt(beforeStr, 10) : void 0;
|
|
4995
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
4996
|
+
if (before !== void 0 && isNaN(before) || limit !== void 0 && isNaN(limit)) {
|
|
4997
|
+
return c.json({ error: "Invalid cursor params: before and limit must be integers" }, 400);
|
|
5769
4998
|
}
|
|
5770
|
-
const
|
|
5771
|
-
return c.json({
|
|
4999
|
+
const result = await getMessagesPaginated(id, { before, limit });
|
|
5000
|
+
return c.json({ items: result.messages, hasMore: result.hasMore });
|
|
5001
|
+
}).get("/:id/participants", async (c) => {
|
|
5002
|
+
const id = c.req.param("id");
|
|
5003
|
+
const user = c.get("user");
|
|
5004
|
+
if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
|
|
5005
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
5006
|
+
}
|
|
5007
|
+
const participants = await getParticipants(id);
|
|
5008
|
+
return c.json(participants);
|
|
5009
|
+
}).post("/", zValidator7("json", createSchema), async (c) => {
|
|
5010
|
+
const user = c.get("user");
|
|
5011
|
+
const body = c.req.valid("json");
|
|
5012
|
+
const participantIds = /* @__PURE__ */ new Set();
|
|
5013
|
+
if (user.id !== 0) participantIds.add(user.id);
|
|
5014
|
+
let firstMindName;
|
|
5015
|
+
for (const name of body.participantNames) {
|
|
5016
|
+
const existing = await getUserByUsername(name);
|
|
5017
|
+
if (existing) {
|
|
5018
|
+
participantIds.add(existing.id);
|
|
5019
|
+
if (!firstMindName && existing.user_type === "mind") firstMindName = name;
|
|
5020
|
+
continue;
|
|
5021
|
+
}
|
|
5022
|
+
if (findMind(name)) {
|
|
5023
|
+
const au = await getOrCreateMindUser(name);
|
|
5024
|
+
participantIds.add(au.id);
|
|
5025
|
+
if (!firstMindName) firstMindName = name;
|
|
5026
|
+
continue;
|
|
5027
|
+
}
|
|
5028
|
+
return c.json({ error: `User not found: ${name}` }, 400);
|
|
5029
|
+
}
|
|
5030
|
+
if (!firstMindName) {
|
|
5031
|
+
return c.json({ error: "At least one mind participant is required" }, 400);
|
|
5032
|
+
}
|
|
5033
|
+
const conv = await createConversation(firstMindName, "volute", {
|
|
5034
|
+
userId: user.id !== 0 ? user.id : void 0,
|
|
5035
|
+
title: body.title,
|
|
5036
|
+
participantIds: [...participantIds]
|
|
5037
|
+
});
|
|
5038
|
+
return c.json(conv, 201);
|
|
5039
|
+
}).delete("/:id", async (c) => {
|
|
5040
|
+
const id = c.req.param("id");
|
|
5041
|
+
const user = c.get("user");
|
|
5042
|
+
const deleted = await deleteConversationForUser(id, user.id);
|
|
5043
|
+
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
5044
|
+
return c.json({ ok: true });
|
|
5772
5045
|
});
|
|
5773
|
-
var
|
|
5046
|
+
var conversations_default = app21;
|
|
5774
5047
|
|
|
5775
|
-
// src/web/api/
|
|
5776
|
-
import {
|
|
5777
|
-
import { Hono as
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
});
|
|
5788
|
-
|
|
5789
|
-
|
|
5048
|
+
// src/web/api/v1/events.ts
|
|
5049
|
+
import { desc as desc4 } from "drizzle-orm";
|
|
5050
|
+
import { Hono as Hono22 } from "hono";
|
|
5051
|
+
import { streamSSE as streamSSE5 } from "hono/streaming";
|
|
5052
|
+
|
|
5053
|
+
// src/lib/events/event-sequencer.ts
|
|
5054
|
+
var BUFFER_SIZE = 1e3;
|
|
5055
|
+
var MAX_AGE_MS = 5 * 60 * 1e3;
|
|
5056
|
+
var nextId = 1;
|
|
5057
|
+
var buffer = [];
|
|
5058
|
+
function bufferEvent(data) {
|
|
5059
|
+
const id = nextId++;
|
|
5060
|
+
buffer.push({ id, data, timestamp: Date.now() });
|
|
5061
|
+
while (buffer.length > BUFFER_SIZE) {
|
|
5062
|
+
buffer.shift();
|
|
5063
|
+
}
|
|
5064
|
+
return id;
|
|
5065
|
+
}
|
|
5066
|
+
function getEventsSince(sinceId) {
|
|
5067
|
+
const now = Date.now();
|
|
5068
|
+
const startIdx = buffer.findIndex((e) => e.id > sinceId);
|
|
5069
|
+
if (startIdx === -1) return [];
|
|
5070
|
+
return buffer.slice(startIdx).filter((e) => now - e.timestamp < MAX_AGE_MS);
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
// src/web/api/v1/events.ts
|
|
5074
|
+
var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
|
|
5075
|
+
const user = c.get("user");
|
|
5076
|
+
const since = c.req.query("since");
|
|
5077
|
+
const sinceId = since ? Number(since) : 0;
|
|
5078
|
+
return streamSSE5(c, async (stream) => {
|
|
5079
|
+
const cleanups = [];
|
|
5080
|
+
try {
|
|
5081
|
+
if (sinceId > 0) {
|
|
5082
|
+
const missed = getEventsSince(sinceId);
|
|
5083
|
+
for (const event of missed) {
|
|
5084
|
+
await stream.writeSSE({
|
|
5085
|
+
id: String(event.id),
|
|
5086
|
+
data: JSON.stringify(event.data)
|
|
5087
|
+
});
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
let recentActivity = [];
|
|
5091
|
+
try {
|
|
5092
|
+
const db = await getDb();
|
|
5093
|
+
recentActivity = await db.select().from(activity).orderBy(desc4(activity.created_at)).limit(50);
|
|
5094
|
+
recentActivity = recentActivity.map((row) => ({
|
|
5095
|
+
...row,
|
|
5096
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
5097
|
+
}));
|
|
5098
|
+
} catch (err) {
|
|
5099
|
+
logger_default.error("[v1-events] failed to fetch recent activity", logger_default.errorData(err));
|
|
5100
|
+
}
|
|
5101
|
+
let conversations2 = [];
|
|
5102
|
+
try {
|
|
5103
|
+
conversations2 = await listConversationsWithParticipants(user.id);
|
|
5104
|
+
} catch (err) {
|
|
5105
|
+
logger_default.error("[v1-events] failed to fetch conversations", logger_default.errorData(err));
|
|
5106
|
+
}
|
|
5107
|
+
const sites = getCachedSites();
|
|
5108
|
+
const recentPages = getCachedRecentPages();
|
|
5109
|
+
const snapshotData = {
|
|
5110
|
+
event: "snapshot",
|
|
5111
|
+
activity: recentActivity,
|
|
5112
|
+
conversations: conversations2,
|
|
5113
|
+
sites,
|
|
5114
|
+
recentPages,
|
|
5115
|
+
activeMinds: getActiveMinds()
|
|
5116
|
+
};
|
|
5117
|
+
const snapshotId = bufferEvent(snapshotData);
|
|
5118
|
+
await stream.writeSSE({
|
|
5119
|
+
id: String(snapshotId),
|
|
5120
|
+
data: JSON.stringify(snapshotData)
|
|
5121
|
+
});
|
|
5122
|
+
const unsubActivity = subscribe((event) => {
|
|
5123
|
+
const data = {
|
|
5124
|
+
event: "activity",
|
|
5125
|
+
...event,
|
|
5126
|
+
metadata: event.metadata ?? null
|
|
5127
|
+
};
|
|
5128
|
+
const eventId = bufferEvent(data);
|
|
5129
|
+
stream.writeSSE({
|
|
5130
|
+
id: String(eventId),
|
|
5131
|
+
data: JSON.stringify(data)
|
|
5132
|
+
}).catch((err) => {
|
|
5133
|
+
if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
|
|
5134
|
+
});
|
|
5135
|
+
});
|
|
5136
|
+
cleanups.push(unsubActivity);
|
|
5137
|
+
for (const conv of conversations2) {
|
|
5138
|
+
const unsubConv = subscribe3(conv.id, (event) => {
|
|
5139
|
+
const data = { event: "conversation", conversationId: conv.id, ...event };
|
|
5140
|
+
const eventId = bufferEvent(data);
|
|
5141
|
+
stream.writeSSE({
|
|
5142
|
+
id: String(eventId),
|
|
5143
|
+
data: JSON.stringify(data)
|
|
5144
|
+
}).catch((err) => {
|
|
5145
|
+
if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
|
|
5146
|
+
});
|
|
5147
|
+
});
|
|
5148
|
+
cleanups.push(unsubConv);
|
|
5149
|
+
}
|
|
5150
|
+
const keepAlive = setInterval(() => {
|
|
5151
|
+
stream.writeSSE({ data: "" }).catch((err) => {
|
|
5152
|
+
if (!stream.aborted) logger_default.error("[v1-events] ping error:", logger_default.errorData(err));
|
|
5153
|
+
});
|
|
5154
|
+
}, 15e3);
|
|
5155
|
+
cleanups.push(() => clearInterval(keepAlive));
|
|
5156
|
+
await new Promise((resolve18) => {
|
|
5157
|
+
stream.onAbort(() => resolve18());
|
|
5158
|
+
});
|
|
5159
|
+
} finally {
|
|
5160
|
+
for (const cleanup of cleanups) {
|
|
5161
|
+
try {
|
|
5162
|
+
cleanup();
|
|
5163
|
+
} catch {
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5790
5167
|
});
|
|
5791
|
-
child.unref();
|
|
5792
|
-
return c.json({ ok: true, message: "Updating..." });
|
|
5793
5168
|
});
|
|
5794
|
-
var
|
|
5169
|
+
var events_default = app22;
|
|
5795
5170
|
|
|
5796
5171
|
// src/web/api/variants.ts
|
|
5797
|
-
import { existsSync as
|
|
5798
|
-
import { resolve as
|
|
5799
|
-
import { Hono as
|
|
5172
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
5173
|
+
import { resolve as resolve15 } from "path";
|
|
5174
|
+
import { Hono as Hono23 } from "hono";
|
|
5800
5175
|
|
|
5801
5176
|
// src/lib/spawn-server.ts
|
|
5802
|
-
import { spawn as
|
|
5803
|
-
import { closeSync, mkdirSync as
|
|
5804
|
-
import { resolve as
|
|
5177
|
+
import { spawn as spawn3 } from "child_process";
|
|
5178
|
+
import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync9 } from "fs";
|
|
5179
|
+
import { resolve as resolve14 } from "path";
|
|
5805
5180
|
function tsxBin(cwd) {
|
|
5806
|
-
return
|
|
5181
|
+
return resolve14(cwd, "node_modules", ".bin", "tsx");
|
|
5807
5182
|
}
|
|
5808
5183
|
function spawnServer(cwd, port, options) {
|
|
5809
5184
|
if (options?.detached) {
|
|
@@ -5812,37 +5187,37 @@ function spawnServer(cwd, port, options) {
|
|
|
5812
5187
|
return spawnAttached(cwd, port);
|
|
5813
5188
|
}
|
|
5814
5189
|
function spawnAttached(cwd, port) {
|
|
5815
|
-
const child =
|
|
5190
|
+
const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
5816
5191
|
cwd,
|
|
5817
5192
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5818
5193
|
});
|
|
5819
|
-
return new Promise((
|
|
5820
|
-
const timeout = setTimeout(() =>
|
|
5194
|
+
return new Promise((resolve18) => {
|
|
5195
|
+
const timeout = setTimeout(() => resolve18(null), 3e4);
|
|
5821
5196
|
function checkOutput(data) {
|
|
5822
5197
|
const match = data.toString().match(/listening on :(\d+)/);
|
|
5823
5198
|
if (match) {
|
|
5824
5199
|
clearTimeout(timeout);
|
|
5825
|
-
|
|
5200
|
+
resolve18({ child, actualPort: parseInt(match[1], 10) });
|
|
5826
5201
|
}
|
|
5827
5202
|
}
|
|
5828
5203
|
child.stdout?.on("data", checkOutput);
|
|
5829
5204
|
child.stderr?.on("data", checkOutput);
|
|
5830
5205
|
child.on("error", () => {
|
|
5831
5206
|
clearTimeout(timeout);
|
|
5832
|
-
|
|
5207
|
+
resolve18(null);
|
|
5833
5208
|
});
|
|
5834
5209
|
child.on("exit", () => {
|
|
5835
5210
|
clearTimeout(timeout);
|
|
5836
|
-
|
|
5211
|
+
resolve18(null);
|
|
5837
5212
|
});
|
|
5838
5213
|
});
|
|
5839
5214
|
}
|
|
5840
5215
|
function spawnDetached(cwd, port, logDir) {
|
|
5841
|
-
const logsDir = logDir ??
|
|
5842
|
-
|
|
5843
|
-
const logPath =
|
|
5216
|
+
const logsDir = logDir ?? resolve14(cwd, ".mind", "logs");
|
|
5217
|
+
mkdirSync6(logsDir, { recursive: true });
|
|
5218
|
+
const logPath = resolve14(logsDir, "mind.log");
|
|
5844
5219
|
const logFd = openSync(logPath, "a");
|
|
5845
|
-
const child =
|
|
5220
|
+
const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
5846
5221
|
cwd,
|
|
5847
5222
|
stdio: ["ignore", logFd, logFd],
|
|
5848
5223
|
detached: true
|
|
@@ -5860,7 +5235,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
5860
5235
|
}
|
|
5861
5236
|
const interval = setInterval(() => {
|
|
5862
5237
|
try {
|
|
5863
|
-
const content =
|
|
5238
|
+
const content = readFileSync9(logPath, "utf-8");
|
|
5864
5239
|
const match = content.match(/listening on :(\d+)/);
|
|
5865
5240
|
if (match) {
|
|
5866
5241
|
finish({ child, actualPort: parseInt(match[1], 10) });
|
|
@@ -5912,7 +5287,7 @@ async function verify2(port) {
|
|
|
5912
5287
|
}
|
|
5913
5288
|
|
|
5914
5289
|
// src/web/api/variants.ts
|
|
5915
|
-
var
|
|
5290
|
+
var app23 = new Hono23().get("/:name/variants", async (c) => {
|
|
5916
5291
|
const name = c.req.param("name");
|
|
5917
5292
|
const entry = findMind(name);
|
|
5918
5293
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
@@ -5952,11 +5327,11 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5952
5327
|
const err = validateBranchName(variantName);
|
|
5953
5328
|
if (err) return c.json({ error: err }, 400);
|
|
5954
5329
|
const projectRoot = mindDir(mindName);
|
|
5955
|
-
const variantDir =
|
|
5956
|
-
if (
|
|
5330
|
+
const variantDir = resolve15(projectRoot, ".variants", variantName);
|
|
5331
|
+
if (existsSync11(variantDir)) {
|
|
5957
5332
|
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
5958
5333
|
}
|
|
5959
|
-
|
|
5334
|
+
mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
|
|
5960
5335
|
try {
|
|
5961
5336
|
await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
5962
5337
|
} catch (e) {
|
|
@@ -5969,7 +5344,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5969
5344
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
5970
5345
|
await exec(cmd, args, {
|
|
5971
5346
|
cwd: variantDir,
|
|
5972
|
-
env: { ...process.env, HOME:
|
|
5347
|
+
env: { ...process.env, HOME: resolve15(variantDir, "home") }
|
|
5973
5348
|
});
|
|
5974
5349
|
} else {
|
|
5975
5350
|
await exec("npm", ["install"], { cwd: variantDir });
|
|
@@ -5979,7 +5354,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5979
5354
|
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
5980
5355
|
}
|
|
5981
5356
|
if (body.soul) {
|
|
5982
|
-
|
|
5357
|
+
writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
|
|
5983
5358
|
}
|
|
5984
5359
|
const variantPort = body.port ?? nextPort();
|
|
5985
5360
|
const variant = {
|
|
@@ -6017,7 +5392,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6017
5392
|
} catch {
|
|
6018
5393
|
}
|
|
6019
5394
|
const projectRoot = mindDir(mindName);
|
|
6020
|
-
if (
|
|
5395
|
+
if (existsSync11(variant.path)) {
|
|
6021
5396
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
6022
5397
|
if (status) {
|
|
6023
5398
|
try {
|
|
@@ -6074,17 +5449,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6074
5449
|
} catch (e) {
|
|
6075
5450
|
return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
|
|
6076
5451
|
}
|
|
6077
|
-
|
|
6078
|
-
try {
|
|
6079
|
-
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
6080
|
-
} catch {
|
|
6081
|
-
}
|
|
6082
|
-
}
|
|
6083
|
-
try {
|
|
6084
|
-
await gitExec(["branch", "-D", variant.branch], { cwd: projectRoot });
|
|
6085
|
-
} catch {
|
|
6086
|
-
}
|
|
6087
|
-
removeVariant(mindName, variantName);
|
|
5452
|
+
await cleanupVariant(mindName, variantName, projectRoot, variant.path);
|
|
6088
5453
|
if (variantName === "upgrade") {
|
|
6089
5454
|
try {
|
|
6090
5455
|
const { computeTemplateHash: computeTemplateHash2 } = await import("./template-hash-BIMA4ILT.js");
|
|
@@ -6095,13 +5460,12 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6095
5460
|
console.error(`[daemon] failed to update template hash for ${mindName}:`, err);
|
|
6096
5461
|
}
|
|
6097
5462
|
}
|
|
6098
|
-
chownMindDir(projectRoot, mindName);
|
|
6099
5463
|
try {
|
|
6100
5464
|
if (isIsolationEnabled()) {
|
|
6101
5465
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
6102
5466
|
await exec(cmd, args, {
|
|
6103
5467
|
cwd: projectRoot,
|
|
6104
|
-
env: { ...process.env, HOME:
|
|
5468
|
+
env: { ...process.env, HOME: resolve15(projectRoot, "home") }
|
|
6105
5469
|
});
|
|
6106
5470
|
} else {
|
|
6107
5471
|
await exec("npm", ["install"], { cwd: projectRoot });
|
|
@@ -6136,41 +5500,22 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6136
5500
|
const variant = findVariant(mindName, variantName);
|
|
6137
5501
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
6138
5502
|
const projectRoot = mindDir(mindName);
|
|
6139
|
-
|
|
6140
|
-
const compositeKey = `${mindName}@${variantName}`;
|
|
6141
|
-
if (manager.isRunning(compositeKey)) {
|
|
6142
|
-
try {
|
|
6143
|
-
await manager.stopMind(compositeKey);
|
|
6144
|
-
} catch {
|
|
6145
|
-
}
|
|
6146
|
-
}
|
|
6147
|
-
if (existsSync14(variant.path)) {
|
|
6148
|
-
try {
|
|
6149
|
-
await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
|
|
6150
|
-
} catch {
|
|
6151
|
-
}
|
|
6152
|
-
}
|
|
6153
|
-
try {
|
|
6154
|
-
await gitExec(["branch", "-D", variant.branch], { cwd: projectRoot });
|
|
6155
|
-
} catch {
|
|
6156
|
-
}
|
|
6157
|
-
removeVariant(mindName, variantName);
|
|
6158
|
-
chownMindDir(projectRoot, mindName);
|
|
5503
|
+
await cleanupVariant(mindName, variantName, projectRoot, variant.path, { stop: true });
|
|
6159
5504
|
return c.json({ ok: true });
|
|
6160
5505
|
});
|
|
6161
|
-
var variants_default =
|
|
5506
|
+
var variants_default = app23;
|
|
6162
5507
|
|
|
6163
5508
|
// src/web/api/volute/channels.ts
|
|
6164
|
-
import { zValidator as
|
|
6165
|
-
import { Hono as
|
|
6166
|
-
import { z as
|
|
6167
|
-
var
|
|
6168
|
-
name:
|
|
5509
|
+
import { zValidator as zValidator8 } from "@hono/zod-validator";
|
|
5510
|
+
import { Hono as Hono24 } from "hono";
|
|
5511
|
+
import { z as z8 } from "zod";
|
|
5512
|
+
var createSchema2 = z8.object({
|
|
5513
|
+
name: z8.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
|
|
6169
5514
|
});
|
|
6170
|
-
var inviteSchema =
|
|
6171
|
-
username:
|
|
5515
|
+
var inviteSchema = z8.object({
|
|
5516
|
+
username: z8.string().min(1)
|
|
6172
5517
|
});
|
|
6173
|
-
var
|
|
5518
|
+
var app24 = new Hono24().get("/", async (c) => {
|
|
6174
5519
|
const user = c.get("user");
|
|
6175
5520
|
const channels = await listChannels();
|
|
6176
5521
|
const results = await Promise.all(
|
|
@@ -6181,7 +5526,7 @@ var app21 = new Hono21().get("/", async (c) => {
|
|
|
6181
5526
|
})
|
|
6182
5527
|
);
|
|
6183
5528
|
return c.json(results);
|
|
6184
|
-
}).post("/",
|
|
5529
|
+
}).post("/", zValidator8("json", createSchema2), async (c) => {
|
|
6185
5530
|
const user = c.get("user");
|
|
6186
5531
|
const body = c.req.valid("json");
|
|
6187
5532
|
try {
|
|
@@ -6214,7 +5559,7 @@ var app21 = new Hono21().get("/", async (c) => {
|
|
|
6214
5559
|
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
6215
5560
|
const participants = await getParticipants(ch.id);
|
|
6216
5561
|
return c.json(participants);
|
|
6217
|
-
}).post("/:name/invite",
|
|
5562
|
+
}).post("/:name/invite", zValidator8("json", inviteSchema), async (c) => {
|
|
6218
5563
|
const name = c.req.param("name");
|
|
6219
5564
|
const inviter = c.get("user");
|
|
6220
5565
|
const { username } = c.req.valid("json");
|
|
@@ -6234,24 +5579,27 @@ var app21 = new Hono21().get("/", async (c) => {
|
|
|
6234
5579
|
]);
|
|
6235
5580
|
return c.json({ ok: true });
|
|
6236
5581
|
});
|
|
6237
|
-
var channels_default2 =
|
|
5582
|
+
var channels_default2 = app24;
|
|
6238
5583
|
|
|
6239
5584
|
// src/web/api/volute/chat.ts
|
|
6240
|
-
import { zValidator as
|
|
6241
|
-
import { Hono as
|
|
6242
|
-
import { streamSSE as
|
|
6243
|
-
import { z as
|
|
6244
|
-
async function
|
|
5585
|
+
import { zValidator as zValidator9 } from "@hono/zod-validator";
|
|
5586
|
+
import { Hono as Hono25 } from "hono";
|
|
5587
|
+
import { streamSSE as streamSSE6 } from "hono/streaming";
|
|
5588
|
+
import { z as z9 } from "zod";
|
|
5589
|
+
async function fanOutToMinds2(opts) {
|
|
6245
5590
|
const participants = await getParticipants(opts.conversationId);
|
|
6246
5591
|
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
6247
5592
|
const participantNames = participants.map((p) => p.username);
|
|
6248
5593
|
const isDM = opts.isDM ?? participants.length === 2;
|
|
6249
5594
|
const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
|
|
6250
|
-
const { getMindManager: getMindManager2 } = await import("./mind-manager-
|
|
5595
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-KMY4GA2J.js");
|
|
5596
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-2TMQ65E4.js");
|
|
6251
5597
|
const manager = getMindManager2();
|
|
6252
|
-
const
|
|
5598
|
+
const sm = getSleepManagerIfReady();
|
|
5599
|
+
const targetMinds = mindParticipants.map((ap) => {
|
|
6253
5600
|
const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
|
|
6254
|
-
|
|
5601
|
+
if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
|
|
5602
|
+
return null;
|
|
6255
5603
|
}).filter((n) => n !== null && n !== opts.senderName);
|
|
6256
5604
|
function slugForMind(mindUsername) {
|
|
6257
5605
|
return buildVoluteSlug({
|
|
@@ -6275,7 +5623,7 @@ async function fanOutToMinds(opts) {
|
|
|
6275
5623
|
logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
|
|
6276
5624
|
}
|
|
6277
5625
|
}
|
|
6278
|
-
for (const mindName of
|
|
5626
|
+
for (const mindName of targetMinds) {
|
|
6279
5627
|
const target = opts.targetName ? opts.targetName(mindName) : mindName;
|
|
6280
5628
|
const channel = slugForMind(mindName);
|
|
6281
5629
|
const typingMap = getTypingMap();
|
|
@@ -6293,18 +5641,18 @@ async function fanOutToMinds(opts) {
|
|
|
6293
5641
|
});
|
|
6294
5642
|
}
|
|
6295
5643
|
}
|
|
6296
|
-
var chatSchema =
|
|
6297
|
-
message:
|
|
6298
|
-
conversationId:
|
|
6299
|
-
sender:
|
|
6300
|
-
images:
|
|
6301
|
-
|
|
6302
|
-
media_type:
|
|
6303
|
-
data:
|
|
5644
|
+
var chatSchema = z9.object({
|
|
5645
|
+
message: z9.string().optional(),
|
|
5646
|
+
conversationId: z9.string().optional(),
|
|
5647
|
+
sender: z9.string().optional(),
|
|
5648
|
+
images: z9.array(
|
|
5649
|
+
z9.object({
|
|
5650
|
+
media_type: z9.string(),
|
|
5651
|
+
data: z9.string()
|
|
6304
5652
|
})
|
|
6305
5653
|
).optional()
|
|
6306
5654
|
});
|
|
6307
|
-
var
|
|
5655
|
+
var app25 = new Hono25().post("/:name/chat", zValidator9("json", chatSchema), async (c) => {
|
|
6308
5656
|
const name = c.req.param("name");
|
|
6309
5657
|
const [baseName] = name.split("@", 2);
|
|
6310
5658
|
const entry = findMind(baseName);
|
|
@@ -6362,7 +5710,7 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
6362
5710
|
}
|
|
6363
5711
|
}
|
|
6364
5712
|
await addMessage(conversationId, "user", senderName, contentBlocks);
|
|
6365
|
-
await
|
|
5713
|
+
await fanOutToMinds2({
|
|
6366
5714
|
conversationId,
|
|
6367
5715
|
contentBlocks,
|
|
6368
5716
|
senderName,
|
|
@@ -6376,8 +5724,8 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
6376
5724
|
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
6377
5725
|
return c.json({ error: "Conversation not found" }, 404);
|
|
6378
5726
|
}
|
|
6379
|
-
return
|
|
6380
|
-
const unsubscribe =
|
|
5727
|
+
return streamSSE6(c, async (stream) => {
|
|
5728
|
+
const unsubscribe = subscribe3(conversationId, (event) => {
|
|
6381
5729
|
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
6382
5730
|
if (!stream.aborted) console.error("[chat] SSE write error:", err);
|
|
6383
5731
|
});
|
|
@@ -6387,23 +5735,23 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
6387
5735
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
6388
5736
|
});
|
|
6389
5737
|
}, 15e3);
|
|
6390
|
-
await new Promise((
|
|
5738
|
+
await new Promise((resolve18) => {
|
|
6391
5739
|
stream.onAbort(() => {
|
|
6392
5740
|
unsubscribe();
|
|
6393
5741
|
clearInterval(keepAlive);
|
|
6394
|
-
|
|
5742
|
+
resolve18();
|
|
6395
5743
|
});
|
|
6396
5744
|
});
|
|
6397
5745
|
});
|
|
6398
5746
|
});
|
|
6399
|
-
var
|
|
6400
|
-
message:
|
|
6401
|
-
conversationId:
|
|
6402
|
-
images:
|
|
5747
|
+
var unifiedChatSchema2 = z9.object({
|
|
5748
|
+
message: z9.string().optional(),
|
|
5749
|
+
conversationId: z9.string(),
|
|
5750
|
+
images: z9.array(z9.object({ media_type: z9.string(), data: z9.string() })).optional()
|
|
6403
5751
|
});
|
|
6404
|
-
var unifiedChatApp = new
|
|
5752
|
+
var unifiedChatApp = new Hono25().post(
|
|
6405
5753
|
"/chat",
|
|
6406
|
-
|
|
5754
|
+
zValidator9("json", unifiedChatSchema2),
|
|
6407
5755
|
async (c) => {
|
|
6408
5756
|
const user = c.get("user");
|
|
6409
5757
|
const body = c.req.valid("json");
|
|
@@ -6425,7 +5773,7 @@ var unifiedChatApp = new Hono22().post(
|
|
|
6425
5773
|
}
|
|
6426
5774
|
await addMessage(body.conversationId, "user", senderName, contentBlocks);
|
|
6427
5775
|
const isDM = conv.type === "dm";
|
|
6428
|
-
await
|
|
5776
|
+
await fanOutToMinds2({
|
|
6429
5777
|
conversationId: body.conversationId,
|
|
6430
5778
|
contentBlocks,
|
|
6431
5779
|
senderName,
|
|
@@ -6437,18 +5785,18 @@ var unifiedChatApp = new Hono22().post(
|
|
|
6437
5785
|
return c.json({ ok: true, conversationId: body.conversationId });
|
|
6438
5786
|
}
|
|
6439
5787
|
);
|
|
6440
|
-
var
|
|
5788
|
+
var chat_default2 = app25;
|
|
6441
5789
|
|
|
6442
5790
|
// src/web/api/volute/conversations.ts
|
|
6443
|
-
import { zValidator as
|
|
6444
|
-
import { Hono as
|
|
6445
|
-
import { z as
|
|
6446
|
-
var createConvSchema =
|
|
6447
|
-
title:
|
|
6448
|
-
participantIds:
|
|
6449
|
-
participantNames:
|
|
5791
|
+
import { zValidator as zValidator10 } from "@hono/zod-validator";
|
|
5792
|
+
import { Hono as Hono26 } from "hono";
|
|
5793
|
+
import { z as z10 } from "zod";
|
|
5794
|
+
var createConvSchema = z10.object({
|
|
5795
|
+
title: z10.string().optional(),
|
|
5796
|
+
participantIds: z10.array(z10.number()).optional(),
|
|
5797
|
+
participantNames: z10.array(z10.string()).optional()
|
|
6450
5798
|
});
|
|
6451
|
-
var
|
|
5799
|
+
var app26 = new Hono26().get("/:name/conversations", async (c) => {
|
|
6452
5800
|
const name = c.req.param("name");
|
|
6453
5801
|
const user = c.get("user");
|
|
6454
5802
|
let lookupId = user.id;
|
|
@@ -6459,7 +5807,7 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
|
|
|
6459
5807
|
const all = await listConversationsForUser(lookupId);
|
|
6460
5808
|
const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
|
|
6461
5809
|
return c.json(convs);
|
|
6462
|
-
}).post("/:name/conversations",
|
|
5810
|
+
}).post("/:name/conversations", zValidator10("json", createConvSchema), async (c) => {
|
|
6463
5811
|
const name = c.req.param("name");
|
|
6464
5812
|
const user = c.get("user");
|
|
6465
5813
|
const body = c.req.valid("json");
|
|
@@ -6533,18 +5881,18 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
|
|
|
6533
5881
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
6534
5882
|
return c.json({ ok: true });
|
|
6535
5883
|
});
|
|
6536
|
-
var
|
|
5884
|
+
var conversations_default2 = app26;
|
|
6537
5885
|
|
|
6538
5886
|
// src/web/api/volute/user-conversations.ts
|
|
6539
|
-
import { zValidator as
|
|
6540
|
-
import { Hono as
|
|
6541
|
-
import { streamSSE as
|
|
6542
|
-
import { z as
|
|
6543
|
-
var
|
|
6544
|
-
title:
|
|
6545
|
-
participantNames:
|
|
5887
|
+
import { zValidator as zValidator11 } from "@hono/zod-validator";
|
|
5888
|
+
import { Hono as Hono27 } from "hono";
|
|
5889
|
+
import { streamSSE as streamSSE7 } from "hono/streaming";
|
|
5890
|
+
import { z as z11 } from "zod";
|
|
5891
|
+
var createSchema3 = z11.object({
|
|
5892
|
+
title: z11.string().optional(),
|
|
5893
|
+
participantNames: z11.array(z11.string()).min(1)
|
|
6546
5894
|
});
|
|
6547
|
-
var
|
|
5895
|
+
var app27 = new Hono27().use("*", authMiddleware).get("/", async (c) => {
|
|
6548
5896
|
const user = c.get("user");
|
|
6549
5897
|
const convs = await listConversationsWithParticipants(user.id);
|
|
6550
5898
|
return c.json(convs);
|
|
@@ -6556,7 +5904,7 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6556
5904
|
}
|
|
6557
5905
|
const msgs = await getMessages(id);
|
|
6558
5906
|
return c.json(msgs);
|
|
6559
|
-
}).post("/",
|
|
5907
|
+
}).post("/", zValidator11("json", createSchema3), async (c) => {
|
|
6560
5908
|
const user = c.get("user");
|
|
6561
5909
|
const body = c.req.valid("json");
|
|
6562
5910
|
const participantIds = /* @__PURE__ */ new Set();
|
|
@@ -6592,8 +5940,8 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6592
5940
|
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
6593
5941
|
return c.json({ error: "Conversation not found" }, 404);
|
|
6594
5942
|
}
|
|
6595
|
-
return
|
|
6596
|
-
const unsubscribe =
|
|
5943
|
+
return streamSSE7(c, async (stream) => {
|
|
5944
|
+
const unsubscribe = subscribe3(conversationId, (event) => {
|
|
6597
5945
|
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
6598
5946
|
if (!stream.aborted) console.error("[chat] SSE write error:", err);
|
|
6599
5947
|
});
|
|
@@ -6603,11 +5951,11 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6603
5951
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
6604
5952
|
});
|
|
6605
5953
|
}, 15e3);
|
|
6606
|
-
await new Promise((
|
|
5954
|
+
await new Promise((resolve18) => {
|
|
6607
5955
|
stream.onAbort(() => {
|
|
6608
5956
|
unsubscribe();
|
|
6609
5957
|
clearInterval(keepAlive);
|
|
6610
|
-
|
|
5958
|
+
resolve18();
|
|
6611
5959
|
});
|
|
6612
5960
|
});
|
|
6613
5961
|
});
|
|
@@ -6618,12 +5966,12 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6618
5966
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
6619
5967
|
return c.json({ ok: true });
|
|
6620
5968
|
});
|
|
6621
|
-
var user_conversations_default =
|
|
5969
|
+
var user_conversations_default = app27;
|
|
6622
5970
|
|
|
6623
5971
|
// src/web/app.ts
|
|
6624
5972
|
var httpLog = logger_default.child("http");
|
|
6625
|
-
var
|
|
6626
|
-
|
|
5973
|
+
var app28 = new Hono28();
|
|
5974
|
+
app28.onError((err, c) => {
|
|
6627
5975
|
if (err instanceof HTTPException) {
|
|
6628
5976
|
return err.getResponse();
|
|
6629
5977
|
}
|
|
@@ -6634,10 +5982,10 @@ app25.onError((err, c) => {
|
|
|
6634
5982
|
});
|
|
6635
5983
|
return c.json({ error: "Internal server error" }, 500);
|
|
6636
5984
|
});
|
|
6637
|
-
|
|
5985
|
+
app28.notFound((c) => {
|
|
6638
5986
|
return c.json({ error: "Not found" }, 404);
|
|
6639
5987
|
});
|
|
6640
|
-
|
|
5988
|
+
app28.use("*", async (c, next) => {
|
|
6641
5989
|
const start = Date.now();
|
|
6642
5990
|
await next();
|
|
6643
5991
|
const duration = Date.now() - start;
|
|
@@ -6648,7 +5996,7 @@ app25.use("*", async (c, next) => {
|
|
|
6648
5996
|
httpLog.debug("request", data);
|
|
6649
5997
|
}
|
|
6650
5998
|
});
|
|
6651
|
-
|
|
5999
|
+
app28.get("/api/health", (c) => {
|
|
6652
6000
|
let version = "unknown";
|
|
6653
6001
|
let cached = null;
|
|
6654
6002
|
try {
|
|
@@ -6663,19 +6011,35 @@ app25.get("/api/health", (c) => {
|
|
|
6663
6011
|
...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
|
|
6664
6012
|
});
|
|
6665
6013
|
});
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
var
|
|
6014
|
+
app28.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
6015
|
+
app28.use("/api/*", csrf());
|
|
6016
|
+
app28.use("/api/activity/*", authMiddleware);
|
|
6017
|
+
app28.use("/api/minds/*", authMiddleware);
|
|
6018
|
+
app28.use("/api/conversations/*", authMiddleware);
|
|
6019
|
+
app28.use("/api/volute/*", authMiddleware);
|
|
6020
|
+
app28.use("/api/system/*", authMiddleware);
|
|
6021
|
+
app28.use("/api/env/*", authMiddleware);
|
|
6022
|
+
app28.use("/api/prompts/*", authMiddleware);
|
|
6023
|
+
app28.use("/api/skills/*", authMiddleware);
|
|
6024
|
+
app28.use("/api/v1/*", authMiddleware);
|
|
6025
|
+
app28.route("/pages", pages_default);
|
|
6026
|
+
var routes = app28.route("/api/activity", activity_default).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_default2).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", file_sharing_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_default2).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).route("/api/v1/conversations", conversations_default).route("/api/v1/events", events_default).route("/api/v1", chat_default);
|
|
6027
|
+
app28.route("/api/v1/minds", minds_default);
|
|
6028
|
+
app28.route("/api/v1/minds", typing_default);
|
|
6029
|
+
app28.route("/api/v1/minds", variants_default);
|
|
6030
|
+
app28.route("/api/v1/minds", files_default);
|
|
6031
|
+
app28.route("/api/v1/minds", env_default);
|
|
6032
|
+
app28.route("/api/v1/minds", mind_skills_default);
|
|
6033
|
+
app28.route("/api/v1/minds", connectors_default);
|
|
6034
|
+
app28.route("/api/v1/minds", schedules_default);
|
|
6035
|
+
app28.route("/api/v1/minds", logs_default);
|
|
6036
|
+
app28.route("/api/v1/system", system_default);
|
|
6037
|
+
app28.route("/api/v1/system", update_default);
|
|
6038
|
+
app28.route("/api/v1/prompts", prompts_default);
|
|
6039
|
+
app28.route("/api/v1/skills", skills_default);
|
|
6040
|
+
app28.route("/api/v1/env", sharedEnvApp);
|
|
6041
|
+
app28.route("/api/v1/channels", channels_default2);
|
|
6042
|
+
var app_default = app28;
|
|
6679
6043
|
|
|
6680
6044
|
// src/web/server.ts
|
|
6681
6045
|
var MIME_TYPES2 = {
|
|
@@ -6692,20 +6056,20 @@ async function startServer({
|
|
|
6692
6056
|
hostname = "127.0.0.1"
|
|
6693
6057
|
}) {
|
|
6694
6058
|
let assetsDir = "";
|
|
6695
|
-
let searchDir =
|
|
6059
|
+
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
6696
6060
|
for (let i = 0; i < 5; i++) {
|
|
6697
|
-
const candidate =
|
|
6698
|
-
if (
|
|
6061
|
+
const candidate = resolve16(searchDir, "dist", "web-assets");
|
|
6062
|
+
if (existsSync12(candidate)) {
|
|
6699
6063
|
assetsDir = candidate;
|
|
6700
6064
|
break;
|
|
6701
6065
|
}
|
|
6702
|
-
searchDir =
|
|
6066
|
+
searchDir = dirname(searchDir);
|
|
6703
6067
|
}
|
|
6704
6068
|
if (assetsDir) {
|
|
6705
6069
|
app_default.get("*", async (c) => {
|
|
6706
6070
|
const urlPath = new URL(c.req.url).pathname;
|
|
6707
6071
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
6708
|
-
const filePath =
|
|
6072
|
+
const filePath = resolve16(assetsDir, urlPath.slice(1));
|
|
6709
6073
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
6710
6074
|
const s = await stat3(filePath).catch(() => null);
|
|
6711
6075
|
if (s?.isFile()) {
|
|
@@ -6714,7 +6078,7 @@ async function startServer({
|
|
|
6714
6078
|
const body = await readFile3(filePath);
|
|
6715
6079
|
return c.body(body, 200, { "Content-Type": mime });
|
|
6716
6080
|
}
|
|
6717
|
-
const indexPath =
|
|
6081
|
+
const indexPath = resolve16(assetsDir, "index.html");
|
|
6718
6082
|
const indexStat = await stat3(indexPath).catch(() => null);
|
|
6719
6083
|
if (indexStat?.isFile()) {
|
|
6720
6084
|
const body = await readFile3(indexPath, "utf-8");
|
|
@@ -6724,10 +6088,10 @@ async function startServer({
|
|
|
6724
6088
|
});
|
|
6725
6089
|
}
|
|
6726
6090
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
6727
|
-
await new Promise((
|
|
6091
|
+
await new Promise((resolve18, reject) => {
|
|
6728
6092
|
server.on("listening", () => {
|
|
6729
6093
|
logger_default.info("Volute UI running", { hostname, port });
|
|
6730
|
-
|
|
6094
|
+
resolve18();
|
|
6731
6095
|
});
|
|
6732
6096
|
server.on("error", (err) => {
|
|
6733
6097
|
reject(err);
|
|
@@ -6738,14 +6102,17 @@ async function startServer({
|
|
|
6738
6102
|
|
|
6739
6103
|
// src/daemon.ts
|
|
6740
6104
|
if (!process.env.VOLUTE_HOME) {
|
|
6741
|
-
process.env.VOLUTE_HOME =
|
|
6105
|
+
process.env.VOLUTE_HOME = resolve17(homedir2(), ".volute");
|
|
6106
|
+
}
|
|
6107
|
+
if (process.env.VOLUTE_TIMEZONE && !process.env.TZ) {
|
|
6108
|
+
process.env.TZ = process.env.VOLUTE_TIMEZONE;
|
|
6742
6109
|
}
|
|
6743
6110
|
async function startDaemon(opts) {
|
|
6744
6111
|
const { port, hostname } = opts;
|
|
6745
6112
|
const myPid = String(process.pid);
|
|
6746
6113
|
const home = voluteHome();
|
|
6747
6114
|
if (!opts.foreground) {
|
|
6748
|
-
const rotatingLog = new RotatingLog(
|
|
6115
|
+
const rotatingLog = new RotatingLog(resolve17(home, "daemon.log"));
|
|
6749
6116
|
logger_default.setOutput((line) => rotatingLog.write(`${line}
|
|
6750
6117
|
`));
|
|
6751
6118
|
const write = (...args) => rotatingLog.write(`${format(...args)}
|
|
@@ -6755,9 +6122,9 @@ async function startDaemon(opts) {
|
|
|
6755
6122
|
console.warn = write;
|
|
6756
6123
|
console.info = write;
|
|
6757
6124
|
}
|
|
6758
|
-
const DAEMON_PID_PATH =
|
|
6759
|
-
const DAEMON_JSON_PATH =
|
|
6760
|
-
|
|
6125
|
+
const DAEMON_PID_PATH = resolve17(home, "daemon.pid");
|
|
6126
|
+
const DAEMON_JSON_PATH = resolve17(home, "daemon.json");
|
|
6127
|
+
mkdirSync8(home, { recursive: true });
|
|
6761
6128
|
migrateAgentsToMinds();
|
|
6762
6129
|
try {
|
|
6763
6130
|
await ensureSharedRepo();
|
|
@@ -6785,8 +6152,8 @@ async function startDaemon(opts) {
|
|
|
6785
6152
|
}
|
|
6786
6153
|
throw err;
|
|
6787
6154
|
}
|
|
6788
|
-
|
|
6789
|
-
|
|
6155
|
+
writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
6156
|
+
writeFileSync8(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
6790
6157
|
`, {
|
|
6791
6158
|
mode: 420
|
|
6792
6159
|
});
|
|
@@ -6800,6 +6167,9 @@ async function startDaemon(opts) {
|
|
|
6800
6167
|
mailPoller.start();
|
|
6801
6168
|
const tokenBudget = initTokenBudget();
|
|
6802
6169
|
tokenBudget.start();
|
|
6170
|
+
const sleepManager = initSleepManager();
|
|
6171
|
+
sleepManager.start();
|
|
6172
|
+
const unsubscribeWebhook = initWebhook();
|
|
6803
6173
|
const registry = readRegistry();
|
|
6804
6174
|
for (const entry of registry) {
|
|
6805
6175
|
try {
|
|
@@ -6815,6 +6185,20 @@ async function startDaemon(opts) {
|
|
|
6815
6185
|
const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
|
|
6816
6186
|
while (queue.length > 0) {
|
|
6817
6187
|
const entry = queue.shift();
|
|
6188
|
+
if (sleepManager.isSleeping(entry.name)) {
|
|
6189
|
+
try {
|
|
6190
|
+
const dir = mindDir(entry.name);
|
|
6191
|
+
const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
|
|
6192
|
+
await connectors.startConnectors(entry.name, dir, entry.port, daemonPort);
|
|
6193
|
+
scheduler.loadSchedules(entry.name);
|
|
6194
|
+
} catch (err) {
|
|
6195
|
+
logger_default.error(
|
|
6196
|
+
`failed to start connectors for sleeping mind ${entry.name}`,
|
|
6197
|
+
logger_default.errorData(err)
|
|
6198
|
+
);
|
|
6199
|
+
}
|
|
6200
|
+
continue;
|
|
6201
|
+
}
|
|
6818
6202
|
try {
|
|
6819
6203
|
await startMindFull(entry.name);
|
|
6820
6204
|
} catch (err) {
|
|
@@ -6842,8 +6226,15 @@ async function startDaemon(opts) {
|
|
|
6842
6226
|
});
|
|
6843
6227
|
await Promise.all(workers);
|
|
6844
6228
|
}
|
|
6229
|
+
import("./cloud-sync-PI47U2LT.js").then(
|
|
6230
|
+
({ consumeQueuedMessages }) => consumeQueuedMessages().catch((err) => {
|
|
6231
|
+
logger_default.warn("failed to consume queued cloud messages", logger_default.errorData(err));
|
|
6232
|
+
})
|
|
6233
|
+
).catch((err) => {
|
|
6234
|
+
logger_default.warn("failed to load cloud-sync module", logger_default.errorData(err));
|
|
6235
|
+
});
|
|
6845
6236
|
try {
|
|
6846
|
-
const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-
|
|
6237
|
+
const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-LKABEJSA.js");
|
|
6847
6238
|
backfillTemplateHashes();
|
|
6848
6239
|
notifyVersionUpdate().catch((err) => {
|
|
6849
6240
|
logger_default.warn("failed to send version update notifications", logger_default.errorData(err));
|
|
@@ -6860,15 +6251,15 @@ async function startDaemon(opts) {
|
|
|
6860
6251
|
logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
|
|
6861
6252
|
function cleanup() {
|
|
6862
6253
|
try {
|
|
6863
|
-
if (
|
|
6864
|
-
|
|
6254
|
+
if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
6255
|
+
unlinkSync(DAEMON_PID_PATH);
|
|
6865
6256
|
}
|
|
6866
6257
|
} catch {
|
|
6867
6258
|
}
|
|
6868
6259
|
try {
|
|
6869
|
-
const data = JSON.parse(
|
|
6260
|
+
const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
|
|
6870
6261
|
if (data.token === token) {
|
|
6871
|
-
|
|
6262
|
+
unlinkSync(DAEMON_JSON_PATH);
|
|
6872
6263
|
}
|
|
6873
6264
|
} catch {
|
|
6874
6265
|
}
|
|
@@ -6890,6 +6281,9 @@ async function startDaemon(opts) {
|
|
|
6890
6281
|
try {
|
|
6891
6282
|
safe("stopAllWatchers", stopAllWatchers);
|
|
6892
6283
|
safe("stopAllActivityTrackers", stopAll);
|
|
6284
|
+
safe("unsubscribeWebhook", unsubscribeWebhook);
|
|
6285
|
+
safe("sleepManager.stop", () => sleepManager.stop());
|
|
6286
|
+
safe("sleepManager.saveState", () => sleepManager.saveState());
|
|
6893
6287
|
safe("scheduler.stop", () => scheduler.stop());
|
|
6894
6288
|
safe("scheduler.saveState", () => scheduler.saveState());
|
|
6895
6289
|
safe("mailPoller.stop", () => mailPoller.stop());
|