volute 0.21.0 → 0.22.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-J5A3DF2U.js → chunk-JNFRY2WU.js} +1 -1
- package/dist/{chunk-IPJXU366.js → chunk-JTDFJWI2.js} +1 -0
- package/dist/chunk-OSFGKF2T.js +2651 -0
- 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-7LPTHFIL.js → chunk-VNVCRVYI.js} +55 -5
- 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-C6WRYRVR.js +96 -0
- package/dist/{daemon-restart-BH67ZOTE.js → daemon-restart-TPQ2XBRZ.js} +4 -4
- package/dist/daemon.js +1199 -1785
- package/dist/{down-LIOQ5JDH.js → down-WSUASL5E.js} +3 -3
- package/dist/{import-E433B4KG.js → import-EAXTHHXL.js} +2 -1
- package/dist/message-delivery-WUS4K4ZC.js +21 -0
- package/dist/{mind-BIDOF65R.js → mind-BTXR5B3C.js} +13 -5
- package/dist/{mind-manager-3V2NXX4I.js → mind-manager-P5OBDUKI.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-A7PEYJI2.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-3RWUX2ZR.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-JKGC7PPF.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-5FGUAVSF.js} +11 -3
- package/dist/web-assets/assets/index-DWBxl4LO.js +69 -0
- package/dist/web-assets/assets/index-ZqMd1mx1.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/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 +7 -0
- 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/event-handler.ts +13 -1
- 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,76 @@ 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,
|
|
36
41
|
publishTypingForChannels,
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
startMindFull,
|
|
43
|
+
stopAllWatchers,
|
|
44
|
+
stopMindFull,
|
|
45
|
+
subscribe as subscribe2
|
|
46
|
+
} from "./chunk-OSFGKF2T.js";
|
|
39
47
|
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
computeTemplateHash,
|
|
43
|
-
copyTemplateToDir,
|
|
44
|
-
findTemplatesRoot,
|
|
45
|
-
listFiles
|
|
46
|
-
} from "./chunk-AKPFNL7L.js";
|
|
48
|
+
readSystemsConfig
|
|
49
|
+
} from "./chunk-HFCBO2GL.js";
|
|
47
50
|
import {
|
|
48
51
|
getActiveMinds,
|
|
49
|
-
markIdle,
|
|
50
52
|
onMindEvent,
|
|
51
53
|
stopAll
|
|
52
54
|
} from "./chunk-HGCDWKSP.js";
|
|
53
55
|
import {
|
|
54
56
|
broadcast,
|
|
55
|
-
|
|
56
|
-
subscribe as subscribe2
|
|
57
|
+
subscribe
|
|
57
58
|
} from "./chunk-A4S7H6G6.js";
|
|
59
|
+
import {
|
|
60
|
+
PROMPT_DEFAULTS,
|
|
61
|
+
PROMPT_KEYS,
|
|
62
|
+
RotatingLog,
|
|
63
|
+
getMindManager,
|
|
64
|
+
getMindPromptDefaults,
|
|
65
|
+
getPrompt,
|
|
66
|
+
getPromptIfCustom,
|
|
67
|
+
initMindManager,
|
|
68
|
+
substitute
|
|
69
|
+
} from "./chunk-VNVCRVYI.js";
|
|
58
70
|
import {
|
|
59
71
|
findOpenClawSession,
|
|
60
72
|
importOpenClawConnectors,
|
|
61
73
|
importPiSession,
|
|
62
|
-
parseNameFromIdentity
|
|
74
|
+
parseNameFromIdentity
|
|
75
|
+
} from "./chunk-RK627D57.js";
|
|
76
|
+
import {
|
|
63
77
|
readVoluteConfig,
|
|
64
78
|
writeVoluteConfig
|
|
65
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-XLC342FO.js";
|
|
66
80
|
import {
|
|
67
81
|
loadMergedEnv,
|
|
68
82
|
mindEnvPath,
|
|
@@ -88,7 +102,7 @@ import {
|
|
|
88
102
|
syncBuiltinSkills,
|
|
89
103
|
uninstallSkill,
|
|
90
104
|
updateSkill
|
|
91
|
-
} from "./chunk-
|
|
105
|
+
} from "./chunk-TFS25FIM.js";
|
|
92
106
|
import {
|
|
93
107
|
activity,
|
|
94
108
|
conversationParticipants,
|
|
@@ -109,7 +123,7 @@ import {
|
|
|
109
123
|
exec,
|
|
110
124
|
gitExec,
|
|
111
125
|
resolveVoluteBin
|
|
112
|
-
} from "./chunk-
|
|
126
|
+
} from "./chunk-JTDFJWI2.js";
|
|
113
127
|
import {
|
|
114
128
|
chownMindDir,
|
|
115
129
|
createMindUser,
|
|
@@ -134,7 +148,6 @@ import {
|
|
|
134
148
|
addMind,
|
|
135
149
|
addVariant,
|
|
136
150
|
checkHealth,
|
|
137
|
-
daemonLoopback,
|
|
138
151
|
ensureVoluteHome,
|
|
139
152
|
findMind,
|
|
140
153
|
findVariant,
|
|
@@ -163,1178 +176,15 @@ import {
|
|
|
163
176
|
|
|
164
177
|
// src/daemon.ts
|
|
165
178
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
166
|
-
import { mkdirSync as
|
|
179
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
167
180
|
import { homedir as homedir2 } from "os";
|
|
168
|
-
import { resolve as
|
|
181
|
+
import { resolve as resolve17 } from "path";
|
|
169
182
|
import { format } from "util";
|
|
170
183
|
|
|
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
184
|
// src/lib/migrate-agents-to-minds.ts
|
|
1335
185
|
import { execFileSync } from "child_process";
|
|
1336
|
-
import { existsSync
|
|
1337
|
-
import { resolve
|
|
186
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
187
|
+
import { resolve } from "path";
|
|
1338
188
|
var TAG = "[migrate]";
|
|
1339
189
|
function log(msg) {
|
|
1340
190
|
console.error(`${TAG} ${msg}`);
|
|
@@ -1355,19 +205,19 @@ function bridgeEnvVar() {
|
|
|
1355
205
|
}
|
|
1356
206
|
}
|
|
1357
207
|
function migrateRegistry(home) {
|
|
1358
|
-
const oldPath =
|
|
1359
|
-
const newPath =
|
|
1360
|
-
if (!
|
|
208
|
+
const oldPath = resolve(home, "agents.json");
|
|
209
|
+
const newPath = resolve(home, "minds.json");
|
|
210
|
+
if (!existsSync(oldPath) || existsSync(newPath)) {
|
|
1361
211
|
return readNamesFromRegistry(newPath);
|
|
1362
212
|
}
|
|
1363
|
-
const raw =
|
|
213
|
+
const raw = readFileSync(oldPath, "utf-8");
|
|
1364
214
|
const entries = JSON.parse(raw);
|
|
1365
215
|
for (const entry of entries) {
|
|
1366
216
|
if (entry.stage === "mind") {
|
|
1367
217
|
entry.stage = "sprouted";
|
|
1368
218
|
}
|
|
1369
219
|
}
|
|
1370
|
-
|
|
220
|
+
writeFileSync(newPath, `${JSON.stringify(entries, null, 2)}
|
|
1371
221
|
`);
|
|
1372
222
|
try {
|
|
1373
223
|
renameSync(oldPath, `${oldPath}.bak`);
|
|
@@ -1377,9 +227,9 @@ function migrateRegistry(home) {
|
|
|
1377
227
|
return entries.map((e) => e.name);
|
|
1378
228
|
}
|
|
1379
229
|
function readNamesFromRegistry(path) {
|
|
1380
|
-
if (!
|
|
230
|
+
if (!existsSync(path)) return [];
|
|
1381
231
|
try {
|
|
1382
|
-
const entries = JSON.parse(
|
|
232
|
+
const entries = JSON.parse(readFileSync(path, "utf-8"));
|
|
1383
233
|
return entries.map((e) => e.name);
|
|
1384
234
|
} catch {
|
|
1385
235
|
return [];
|
|
@@ -1387,9 +237,9 @@ function readNamesFromRegistry(path) {
|
|
|
1387
237
|
}
|
|
1388
238
|
function migrateMindsDirectory(home) {
|
|
1389
239
|
if (process.env.VOLUTE_MINDS_DIR) return;
|
|
1390
|
-
const oldDir =
|
|
1391
|
-
const newDir =
|
|
1392
|
-
if (
|
|
240
|
+
const oldDir = resolve(home, "agents");
|
|
241
|
+
const newDir = resolve(home, "minds");
|
|
242
|
+
if (existsSync(oldDir) && !existsSync(newDir)) {
|
|
1393
243
|
try {
|
|
1394
244
|
renameSync(oldDir, newDir);
|
|
1395
245
|
log("renamed agents/ \u2192 minds/");
|
|
@@ -1400,10 +250,10 @@ function migrateMindsDirectory(home) {
|
|
|
1400
250
|
}
|
|
1401
251
|
function migrateLogFiles(home, names) {
|
|
1402
252
|
for (const name of names) {
|
|
1403
|
-
const logsDir =
|
|
1404
|
-
const oldLog =
|
|
1405
|
-
const newLog =
|
|
1406
|
-
if (
|
|
253
|
+
const logsDir = resolve(home, "state", name, "logs");
|
|
254
|
+
const oldLog = resolve(logsDir, "agent.log");
|
|
255
|
+
const newLog = resolve(logsDir, "mind.log");
|
|
256
|
+
if (existsSync(oldLog) && !existsSync(newLog)) {
|
|
1407
257
|
try {
|
|
1408
258
|
renameSync(oldLog, newLog);
|
|
1409
259
|
log(`renamed ${name} agent.log \u2192 mind.log`);
|
|
@@ -1453,12 +303,12 @@ function migrateLinuxUsers(names) {
|
|
|
1453
303
|
}
|
|
1454
304
|
function migrateProfileScript() {
|
|
1455
305
|
const profilePath = "/etc/profile.d/volute.sh";
|
|
1456
|
-
if (!
|
|
306
|
+
if (!existsSync(profilePath)) return;
|
|
1457
307
|
try {
|
|
1458
|
-
const content =
|
|
308
|
+
const content = readFileSync(profilePath, "utf-8");
|
|
1459
309
|
if (!content.includes("VOLUTE_AGENTS_DIR")) return;
|
|
1460
310
|
const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
|
|
1461
|
-
|
|
311
|
+
writeFileSync(profilePath, updated);
|
|
1462
312
|
log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
|
|
1463
313
|
} catch (err) {
|
|
1464
314
|
log(`failed to update profile script: ${err}`);
|
|
@@ -1466,37 +316,37 @@ function migrateProfileScript() {
|
|
|
1466
316
|
}
|
|
1467
317
|
|
|
1468
318
|
// src/lib/migrate-state.ts
|
|
1469
|
-
import { copyFileSync, existsSync as
|
|
1470
|
-
import { resolve as
|
|
319
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, renameSync as renameSync2 } from "fs";
|
|
320
|
+
import { resolve as resolve2 } from "path";
|
|
1471
321
|
function migrateDotVoluteDir(name) {
|
|
1472
322
|
const dir = mindDir(name);
|
|
1473
|
-
const oldDir =
|
|
1474
|
-
const newDir =
|
|
1475
|
-
if (
|
|
323
|
+
const oldDir = resolve2(dir, ".volute");
|
|
324
|
+
const newDir = resolve2(dir, ".mind");
|
|
325
|
+
if (existsSync2(oldDir) && !existsSync2(newDir)) {
|
|
1476
326
|
renameSync2(oldDir, newDir);
|
|
1477
|
-
} else if (
|
|
327
|
+
} else if (existsSync2(oldDir) && existsSync2(newDir)) {
|
|
1478
328
|
console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
|
|
1479
329
|
}
|
|
1480
330
|
}
|
|
1481
331
|
function migrateMindState(name) {
|
|
1482
|
-
const src =
|
|
1483
|
-
if (!
|
|
332
|
+
const src = resolve2(mindDir(name), ".mind");
|
|
333
|
+
if (!existsSync2(src)) return;
|
|
1484
334
|
const dest = stateDir(name);
|
|
1485
|
-
|
|
335
|
+
mkdirSync(dest, { recursive: true });
|
|
1486
336
|
for (const file of ["env.json", "channels.json"]) {
|
|
1487
|
-
const srcPath =
|
|
1488
|
-
const destPath =
|
|
1489
|
-
if (
|
|
337
|
+
const srcPath = resolve2(src, file);
|
|
338
|
+
const destPath = resolve2(dest, file);
|
|
339
|
+
if (existsSync2(srcPath) && !existsSync2(destPath)) {
|
|
1490
340
|
copyFileSync(srcPath, destPath);
|
|
1491
341
|
}
|
|
1492
342
|
}
|
|
1493
|
-
const srcLogs =
|
|
1494
|
-
const destLogs =
|
|
1495
|
-
if (
|
|
1496
|
-
|
|
1497
|
-
for (const file of
|
|
343
|
+
const srcLogs = resolve2(src, "logs");
|
|
344
|
+
const destLogs = resolve2(dest, "logs");
|
|
345
|
+
if (existsSync2(srcLogs) && !existsSync2(destLogs)) {
|
|
346
|
+
mkdirSync(destLogs, { recursive: true });
|
|
347
|
+
for (const file of readdirSync(srcLogs)) {
|
|
1498
348
|
try {
|
|
1499
|
-
copyFileSync(
|
|
349
|
+
copyFileSync(resolve2(srcLogs, file), resolve2(destLogs, file));
|
|
1500
350
|
} catch (err) {
|
|
1501
351
|
console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
|
|
1502
352
|
}
|
|
@@ -1722,13 +572,13 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
1722
572
|
});
|
|
1723
573
|
|
|
1724
574
|
// src/web/server.ts
|
|
1725
|
-
import { existsSync as
|
|
575
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1726
576
|
import { readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
1727
|
-
import { dirname
|
|
577
|
+
import { dirname, extname as extname3, resolve as resolve16 } from "path";
|
|
1728
578
|
import { serve } from "@hono/node-server";
|
|
1729
579
|
|
|
1730
580
|
// src/web/app.ts
|
|
1731
|
-
import { Hono as
|
|
581
|
+
import { Hono as Hono28 } from "hono";
|
|
1732
582
|
import { bodyLimit } from "hono/body-limit";
|
|
1733
583
|
import { csrf } from "hono/csrf";
|
|
1734
584
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -1740,7 +590,7 @@ import { streamSSE } from "hono/streaming";
|
|
|
1740
590
|
|
|
1741
591
|
// src/lib/events/conversations.ts
|
|
1742
592
|
import { randomUUID } from "crypto";
|
|
1743
|
-
import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
|
|
593
|
+
import { and as and2, desc, eq as eq3, inArray, isNull, lt as lt2, sql } from "drizzle-orm";
|
|
1744
594
|
async function createConversation(mindName, channel, opts) {
|
|
1745
595
|
const db = await getDb();
|
|
1746
596
|
const id = randomUUID();
|
|
@@ -1766,6 +616,11 @@ async function createConversation(mindName, channel, opts) {
|
|
|
1766
616
|
);
|
|
1767
617
|
}
|
|
1768
618
|
});
|
|
619
|
+
fireWebhook({
|
|
620
|
+
event: "conversation_created",
|
|
621
|
+
mind: mindName ?? "",
|
|
622
|
+
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
623
|
+
});
|
|
1769
624
|
return {
|
|
1770
625
|
id,
|
|
1771
626
|
mind_name: mindName,
|
|
@@ -1866,21 +721,50 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
1866
721
|
content: msg.content,
|
|
1867
722
|
createdAt: msg.created_at
|
|
1868
723
|
});
|
|
724
|
+
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq3(conversations.id, conversationId)).get();
|
|
725
|
+
fireWebhook({
|
|
726
|
+
event: "message_created",
|
|
727
|
+
mind: conv?.mind_name ?? "",
|
|
728
|
+
data: {
|
|
729
|
+
conversationId,
|
|
730
|
+
messageId: result.id,
|
|
731
|
+
role,
|
|
732
|
+
senderName,
|
|
733
|
+
content: content.filter((b) => b.type !== "image"),
|
|
734
|
+
createdAt: result.created_at
|
|
735
|
+
}
|
|
736
|
+
});
|
|
1869
737
|
return msg;
|
|
1870
738
|
}
|
|
1871
739
|
async function getMessages(conversationId) {
|
|
1872
740
|
const db = await getDb();
|
|
1873
741
|
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
|
-
|
|
742
|
+
return rows.map(parseMessageRow);
|
|
743
|
+
}
|
|
744
|
+
async function getMessagesPaginated(conversationId, opts) {
|
|
745
|
+
const db = await getDb();
|
|
746
|
+
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
747
|
+
const conditions = [eq3(messages.conversation_id, conversationId)];
|
|
748
|
+
if (opts?.before != null) {
|
|
749
|
+
conditions.push(lt2(messages.id, opts.before));
|
|
750
|
+
}
|
|
751
|
+
const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
|
|
752
|
+
const hasMore = rows.length > limit;
|
|
753
|
+
const page = rows.slice(0, limit).reverse();
|
|
754
|
+
return {
|
|
755
|
+
messages: page.map(parseMessageRow),
|
|
756
|
+
hasMore
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function parseMessageRow(row) {
|
|
760
|
+
let content;
|
|
761
|
+
try {
|
|
762
|
+
const parsed = JSON.parse(row.content);
|
|
763
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
764
|
+
} catch {
|
|
765
|
+
content = [{ type: "text", text: row.content }];
|
|
766
|
+
}
|
|
767
|
+
return { ...row, role: row.role, content };
|
|
1884
768
|
}
|
|
1885
769
|
async function listConversationsWithParticipants(userId) {
|
|
1886
770
|
const convs = await listConversationsForUser(userId);
|
|
@@ -2022,7 +906,7 @@ var app = new Hono().get("/events", async (c) => {
|
|
|
2022
906
|
activeMinds: getActiveMinds()
|
|
2023
907
|
})
|
|
2024
908
|
});
|
|
2025
|
-
const unsubActivity =
|
|
909
|
+
const unsubActivity = subscribe((event) => {
|
|
2026
910
|
stream.writeSSE({
|
|
2027
911
|
data: JSON.stringify({ event: "activity", ...event })
|
|
2028
912
|
}).catch((err) => {
|
|
@@ -2031,7 +915,7 @@ var app = new Hono().get("/events", async (c) => {
|
|
|
2031
915
|
});
|
|
2032
916
|
cleanups.push(unsubActivity);
|
|
2033
917
|
for (const conv of conversations2) {
|
|
2034
|
-
const unsubConv =
|
|
918
|
+
const unsubConv = subscribe2(conv.id, (event) => {
|
|
2035
919
|
stream.writeSSE({
|
|
2036
920
|
data: JSON.stringify({ event: "conversation", conversationId: conv.id, ...event })
|
|
2037
921
|
}).catch((err) => {
|
|
@@ -2046,8 +930,8 @@ var app = new Hono().get("/events", async (c) => {
|
|
|
2046
930
|
});
|
|
2047
931
|
}, 15e3);
|
|
2048
932
|
cleanups.push(() => clearInterval(keepAlive));
|
|
2049
|
-
await new Promise((
|
|
2050
|
-
stream.onAbort(() =>
|
|
933
|
+
await new Promise((resolve18) => {
|
|
934
|
+
stream.onAbort(() => resolve18());
|
|
2051
935
|
});
|
|
2052
936
|
} finally {
|
|
2053
937
|
for (const cleanup of cleanups) {
|
|
@@ -2609,16 +1493,16 @@ __export(volute_exports, {
|
|
|
2609
1493
|
read: () => read4,
|
|
2610
1494
|
send: () => send4
|
|
2611
1495
|
});
|
|
2612
|
-
import { existsSync as
|
|
2613
|
-
import { resolve as
|
|
1496
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1497
|
+
import { resolve as resolve3 } from "path";
|
|
2614
1498
|
function getDaemonConfig() {
|
|
2615
|
-
const configPath2 =
|
|
2616
|
-
if (!
|
|
1499
|
+
const configPath2 = resolve3(voluteHome(), "daemon.json");
|
|
1500
|
+
if (!existsSync3(configPath2)) {
|
|
2617
1501
|
throw new Error("Volute daemon is not running");
|
|
2618
1502
|
}
|
|
2619
1503
|
let config;
|
|
2620
1504
|
try {
|
|
2621
|
-
config = JSON.parse(
|
|
1505
|
+
config = JSON.parse(readFileSync2(configPath2, "utf-8"));
|
|
2622
1506
|
} catch (err) {
|
|
2623
1507
|
throw new Error(`Failed to parse ${configPath2}: ${err}`);
|
|
2624
1508
|
}
|
|
@@ -3055,14 +1939,14 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
|
|
|
3055
1939
|
var env_default = app5;
|
|
3056
1940
|
|
|
3057
1941
|
// src/web/api/file-sharing.ts
|
|
3058
|
-
import { readFileSync as
|
|
3059
|
-
import { resolve as
|
|
1942
|
+
import { readFileSync as readFileSync4, statSync } from "fs";
|
|
1943
|
+
import { resolve as resolve5 } from "path";
|
|
3060
1944
|
import { Hono as Hono6 } from "hono";
|
|
3061
1945
|
|
|
3062
1946
|
// src/lib/file-sharing.ts
|
|
3063
1947
|
import { randomBytes } from "crypto";
|
|
3064
|
-
import { existsSync as
|
|
3065
|
-
import { basename, join
|
|
1948
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1949
|
+
import { basename, join, normalize, resolve as resolve4 } from "path";
|
|
3066
1950
|
function validateFilePath(filePath) {
|
|
3067
1951
|
if (!filePath) return "File path is required";
|
|
3068
1952
|
const normalized = normalize(filePath);
|
|
@@ -3075,13 +1959,13 @@ function validateFilePath(filePath) {
|
|
|
3075
1959
|
return null;
|
|
3076
1960
|
}
|
|
3077
1961
|
function configPath(dir) {
|
|
3078
|
-
return
|
|
1962
|
+
return resolve4(dir, "home", ".config", "file-sharing.json");
|
|
3079
1963
|
}
|
|
3080
1964
|
function readFileSharingConfig(dir) {
|
|
3081
1965
|
const p = configPath(dir);
|
|
3082
|
-
if (!
|
|
1966
|
+
if (!existsSync4(p)) return {};
|
|
3083
1967
|
try {
|
|
3084
|
-
return JSON.parse(
|
|
1968
|
+
return JSON.parse(readFileSync3(p, "utf-8"));
|
|
3085
1969
|
} catch (err) {
|
|
3086
1970
|
console.warn(`[file-sharing] failed to parse config at ${p}:`, err);
|
|
3087
1971
|
return {};
|
|
@@ -3089,8 +1973,8 @@ function readFileSharingConfig(dir) {
|
|
|
3089
1973
|
}
|
|
3090
1974
|
function writeFileSharingConfig(dir, config) {
|
|
3091
1975
|
const p = configPath(dir);
|
|
3092
|
-
|
|
3093
|
-
|
|
1976
|
+
mkdirSync2(resolve4(p, ".."), { recursive: true });
|
|
1977
|
+
writeFileSync2(p, `${JSON.stringify(config, null, 2)}
|
|
3094
1978
|
`);
|
|
3095
1979
|
}
|
|
3096
1980
|
function isTrustedSender(dir, sender) {
|
|
@@ -3113,7 +1997,7 @@ function removeTrust(dir, sender) {
|
|
|
3113
1997
|
writeFileSharingConfig(dir, config);
|
|
3114
1998
|
}
|
|
3115
1999
|
function pendingDir(receiver) {
|
|
3116
|
-
return
|
|
2000
|
+
return resolve4(stateDir(receiver), "pending-files");
|
|
3117
2001
|
}
|
|
3118
2002
|
function validateId(id) {
|
|
3119
2003
|
if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
@@ -3132,8 +2016,8 @@ function stageFile(receiver, sender, filename, content, originalPath) {
|
|
|
3132
2016
|
throw new Error("Invalid sender name");
|
|
3133
2017
|
}
|
|
3134
2018
|
const id = generateId(sender);
|
|
3135
|
-
const dir =
|
|
3136
|
-
|
|
2019
|
+
const dir = resolve4(pendingDir(receiver), id);
|
|
2020
|
+
mkdirSync2(dir, { recursive: true });
|
|
3137
2021
|
const metadata = {
|
|
3138
2022
|
id,
|
|
3139
2023
|
sender,
|
|
@@ -3142,22 +2026,22 @@ function stageFile(receiver, sender, filename, content, originalPath) {
|
|
|
3142
2026
|
size: content.length,
|
|
3143
2027
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3144
2028
|
};
|
|
3145
|
-
|
|
2029
|
+
writeFileSync2(resolve4(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
|
|
3146
2030
|
`);
|
|
3147
|
-
|
|
2031
|
+
writeFileSync2(resolve4(dir, "data"), content);
|
|
3148
2032
|
return { id };
|
|
3149
2033
|
}
|
|
3150
2034
|
function listPending(receiver) {
|
|
3151
2035
|
const dir = pendingDir(receiver);
|
|
3152
|
-
if (!
|
|
3153
|
-
const entries =
|
|
2036
|
+
if (!existsSync4(dir)) return [];
|
|
2037
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
3154
2038
|
const result = [];
|
|
3155
2039
|
for (const entry of entries) {
|
|
3156
2040
|
if (!entry.isDirectory()) continue;
|
|
3157
|
-
const metaPath =
|
|
3158
|
-
if (!
|
|
2041
|
+
const metaPath = resolve4(dir, entry.name, "metadata.json");
|
|
2042
|
+
if (!existsSync4(metaPath)) continue;
|
|
3159
2043
|
try {
|
|
3160
|
-
result.push(JSON.parse(
|
|
2044
|
+
result.push(JSON.parse(readFileSync3(metaPath, "utf-8")));
|
|
3161
2045
|
} catch (err) {
|
|
3162
2046
|
console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
|
|
3163
2047
|
}
|
|
@@ -3166,10 +2050,10 @@ function listPending(receiver) {
|
|
|
3166
2050
|
}
|
|
3167
2051
|
function getPending(receiver, id) {
|
|
3168
2052
|
validateId(id);
|
|
3169
|
-
const metaPath =
|
|
3170
|
-
if (!
|
|
2053
|
+
const metaPath = resolve4(pendingDir(receiver), id, "metadata.json");
|
|
2054
|
+
if (!existsSync4(metaPath)) return null;
|
|
3171
2055
|
try {
|
|
3172
|
-
return JSON.parse(
|
|
2056
|
+
return JSON.parse(readFileSync3(metaPath, "utf-8"));
|
|
3173
2057
|
} catch (err) {
|
|
3174
2058
|
console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
|
|
3175
2059
|
return null;
|
|
@@ -3184,27 +2068,27 @@ function deliverFile(receiverDir, sender, filename, content, inboxPath) {
|
|
|
3184
2068
|
if (sender.includes("/") || sender.includes("\\")) {
|
|
3185
2069
|
throw new Error("Invalid sender name");
|
|
3186
2070
|
}
|
|
3187
|
-
const destDir =
|
|
3188
|
-
|
|
3189
|
-
const destPath =
|
|
3190
|
-
|
|
3191
|
-
return
|
|
2071
|
+
const destDir = resolve4(receiverDir, "home", inbox, sender);
|
|
2072
|
+
mkdirSync2(destDir, { recursive: true });
|
|
2073
|
+
const destPath = resolve4(destDir, basename(filename));
|
|
2074
|
+
writeFileSync2(destPath, content);
|
|
2075
|
+
return join(inbox, sender, basename(filename));
|
|
3192
2076
|
}
|
|
3193
2077
|
function acceptPending(receiver, id, receiverDir) {
|
|
3194
2078
|
const meta = getPending(receiver, id);
|
|
3195
2079
|
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
3196
|
-
const dataPath =
|
|
3197
|
-
const content =
|
|
2080
|
+
const dataPath = resolve4(pendingDir(receiver), id, "data");
|
|
2081
|
+
const content = readFileSync3(dataPath);
|
|
3198
2082
|
const config = readFileSharingConfig(receiverDir);
|
|
3199
2083
|
const inboxPath = config.inboxPath ?? "inbox";
|
|
3200
2084
|
const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
|
|
3201
|
-
rmSync(
|
|
2085
|
+
rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
|
|
3202
2086
|
return { sender: meta.sender, filename: meta.filename, destPath };
|
|
3203
2087
|
}
|
|
3204
2088
|
function rejectPending(receiver, id) {
|
|
3205
2089
|
const meta = getPending(receiver, id);
|
|
3206
2090
|
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
3207
|
-
rmSync(
|
|
2091
|
+
rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
|
|
3208
2092
|
return { sender: meta.sender, filename: meta.filename };
|
|
3209
2093
|
}
|
|
3210
2094
|
function formatFileSize(bytes) {
|
|
@@ -3245,9 +2129,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
3245
2129
|
const pathErr = validateFilePath(body.filePath);
|
|
3246
2130
|
if (pathErr) return c.json({ error: pathErr }, 400);
|
|
3247
2131
|
const senderDir = mindDir(senderName);
|
|
3248
|
-
const filePath =
|
|
2132
|
+
const filePath = resolve5(senderDir, "home", body.filePath);
|
|
3249
2133
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
3250
|
-
const stat4 =
|
|
2134
|
+
const stat4 = statSync(filePath, { throwIfNoEntry: false });
|
|
3251
2135
|
if (!stat4) return c.json({ error: `File not found: ${body.filePath}` }, 404);
|
|
3252
2136
|
if (stat4.size > MAX_FILE_SIZE) {
|
|
3253
2137
|
return c.json(
|
|
@@ -3259,7 +2143,7 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
3259
2143
|
}
|
|
3260
2144
|
let content;
|
|
3261
2145
|
try {
|
|
3262
|
-
content =
|
|
2146
|
+
content = readFileSync4(filePath);
|
|
3263
2147
|
} catch {
|
|
3264
2148
|
return c.json({ error: `File not found: ${body.filePath}` }, 404);
|
|
3265
2149
|
}
|
|
@@ -3361,9 +2245,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
|
|
|
3361
2245
|
var file_sharing_default = app6;
|
|
3362
2246
|
|
|
3363
2247
|
// src/web/api/files.ts
|
|
3364
|
-
import { existsSync as
|
|
2248
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3365
2249
|
import { readdir, readFile, realpath, stat } from "fs/promises";
|
|
3366
|
-
import { extname, resolve as
|
|
2250
|
+
import { extname, resolve as resolve6 } from "path";
|
|
3367
2251
|
import { Hono as Hono7 } from "hono";
|
|
3368
2252
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
3369
2253
|
var AVATAR_MIME = {
|
|
@@ -3384,8 +2268,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
|
|
|
3384
2268
|
const ext = extname(config.avatar).toLowerCase();
|
|
3385
2269
|
const mime = AVATAR_MIME[ext];
|
|
3386
2270
|
if (!mime) return c.json({ error: "Invalid avatar extension" }, 400);
|
|
3387
|
-
const homeDir =
|
|
3388
|
-
const avatarPath =
|
|
2271
|
+
const homeDir = resolve6(dir, "home");
|
|
2272
|
+
const avatarPath = resolve6(homeDir, config.avatar);
|
|
3389
2273
|
if (!avatarPath.startsWith(`${homeDir}/`)) return c.json({ error: "Invalid avatar path" }, 400);
|
|
3390
2274
|
let realAvatarPath;
|
|
3391
2275
|
try {
|
|
@@ -3414,8 +2298,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
|
|
|
3414
2298
|
const entry = findMind(name);
|
|
3415
2299
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3416
2300
|
const dir = mindDir(name);
|
|
3417
|
-
const homeDir =
|
|
3418
|
-
if (!
|
|
2301
|
+
const homeDir = resolve6(dir, "home");
|
|
2302
|
+
if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
3419
2303
|
const allFiles = await readdir(homeDir);
|
|
3420
2304
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
3421
2305
|
return c.json(files);
|
|
@@ -3428,8 +2312,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
|
|
|
3428
2312
|
const entry = findMind(name);
|
|
3429
2313
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3430
2314
|
const dir = mindDir(name);
|
|
3431
|
-
const filePath =
|
|
3432
|
-
if (!
|
|
2315
|
+
const filePath = resolve6(dir, "home", filename);
|
|
2316
|
+
if (!existsSync5(filePath)) {
|
|
3433
2317
|
return c.json({ error: "File not found" }, 404);
|
|
3434
2318
|
}
|
|
3435
2319
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -3442,19 +2326,19 @@ import { Hono as Hono8 } from "hono";
|
|
|
3442
2326
|
|
|
3443
2327
|
// src/lib/identity.ts
|
|
3444
2328
|
import { createHash, generateKeyPairSync, sign, verify } from "crypto";
|
|
3445
|
-
import { existsSync as
|
|
3446
|
-
import { resolve as
|
|
2329
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
2330
|
+
import { resolve as resolve7 } from "path";
|
|
3447
2331
|
function generateIdentity(mindDir2) {
|
|
3448
|
-
const identityDir =
|
|
3449
|
-
|
|
2332
|
+
const identityDir = resolve7(mindDir2, ".mind/identity");
|
|
2333
|
+
mkdirSync3(identityDir, { recursive: true });
|
|
3450
2334
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
3451
2335
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
3452
2336
|
privateKeyEncoding: { type: "pkcs8", format: "pem" }
|
|
3453
2337
|
});
|
|
3454
|
-
const privatePath =
|
|
3455
|
-
const publicPath =
|
|
3456
|
-
|
|
3457
|
-
|
|
2338
|
+
const privatePath = resolve7(identityDir, "private.pem");
|
|
2339
|
+
const publicPath = resolve7(identityDir, "public.pem");
|
|
2340
|
+
writeFileSync3(privatePath, privateKey, { mode: 384 });
|
|
2341
|
+
writeFileSync3(publicPath, publicKey, { mode: 420 });
|
|
3458
2342
|
const config = readVoluteConfig(mindDir2) ?? {};
|
|
3459
2343
|
config.identity = {
|
|
3460
2344
|
privateKey: ".mind/identity/private.pem",
|
|
@@ -3467,17 +2351,17 @@ function getPrivateKey(mindDir2) {
|
|
|
3467
2351
|
const config = readVoluteConfig(mindDir2);
|
|
3468
2352
|
const relPath = config?.identity?.privateKey;
|
|
3469
2353
|
if (!relPath) return null;
|
|
3470
|
-
const fullPath =
|
|
3471
|
-
if (!
|
|
3472
|
-
return
|
|
2354
|
+
const fullPath = resolve7(mindDir2, relPath);
|
|
2355
|
+
if (!existsSync6(fullPath)) return null;
|
|
2356
|
+
return readFileSync5(fullPath, "utf-8");
|
|
3473
2357
|
}
|
|
3474
2358
|
function getPublicKey(mindDir2) {
|
|
3475
2359
|
const config = readVoluteConfig(mindDir2);
|
|
3476
2360
|
const relPath = config?.identity?.publicKey;
|
|
3477
2361
|
if (!relPath) return null;
|
|
3478
|
-
const fullPath =
|
|
3479
|
-
if (!
|
|
3480
|
-
return
|
|
2362
|
+
const fullPath = resolve7(mindDir2, relPath);
|
|
2363
|
+
if (!existsSync6(fullPath)) return null;
|
|
2364
|
+
return readFileSync5(fullPath, "utf-8");
|
|
3481
2365
|
}
|
|
3482
2366
|
function getFingerprint(publicKeyPem) {
|
|
3483
2367
|
return createHash("sha256").update(publicKeyPem).digest("hex");
|
|
@@ -3529,21 +2413,21 @@ var app8 = new Hono8().get("/:fingerprint", (c) => {
|
|
|
3529
2413
|
var keys_default = app8;
|
|
3530
2414
|
|
|
3531
2415
|
// src/web/api/logs.ts
|
|
3532
|
-
import { spawn
|
|
3533
|
-
import { existsSync as
|
|
3534
|
-
import { resolve as
|
|
2416
|
+
import { spawn } from "child_process";
|
|
2417
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2418
|
+
import { resolve as resolve8 } from "path";
|
|
3535
2419
|
import { Hono as Hono9 } from "hono";
|
|
3536
2420
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
3537
2421
|
var app9 = new Hono9().get("/:name/logs", async (c) => {
|
|
3538
2422
|
const name = c.req.param("name");
|
|
3539
2423
|
const entry = findMind(name);
|
|
3540
2424
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3541
|
-
const logFile =
|
|
3542
|
-
if (!
|
|
2425
|
+
const logFile = resolve8(stateDir(name), "logs", "mind.log");
|
|
2426
|
+
if (!existsSync7(logFile)) {
|
|
3543
2427
|
return c.json({ error: "No log file found" }, 404);
|
|
3544
2428
|
}
|
|
3545
2429
|
return streamSSE2(c, async (stream) => {
|
|
3546
|
-
const tail =
|
|
2430
|
+
const tail = spawn("tail", ["-n", "200", "-f", logFile]);
|
|
3547
2431
|
const onData = (data) => {
|
|
3548
2432
|
const lines = data.toString().split("\n");
|
|
3549
2433
|
for (const line of lines) {
|
|
@@ -3557,28 +2441,28 @@ var app9 = new Hono9().get("/:name/logs", async (c) => {
|
|
|
3557
2441
|
stream.onAbort(() => {
|
|
3558
2442
|
tail.kill();
|
|
3559
2443
|
});
|
|
3560
|
-
await new Promise((
|
|
3561
|
-
tail.on("exit",
|
|
3562
|
-
stream.onAbort(
|
|
2444
|
+
await new Promise((resolve18) => {
|
|
2445
|
+
tail.on("exit", resolve18);
|
|
2446
|
+
stream.onAbort(resolve18);
|
|
3563
2447
|
});
|
|
3564
2448
|
});
|
|
3565
2449
|
}).get("/:name/logs/tail", async (c) => {
|
|
3566
2450
|
const name = c.req.param("name");
|
|
3567
2451
|
const entry = findMind(name);
|
|
3568
2452
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3569
|
-
const logFile =
|
|
3570
|
-
if (!
|
|
2453
|
+
const logFile = resolve8(stateDir(name), "logs", "mind.log");
|
|
2454
|
+
if (!existsSync7(logFile)) {
|
|
3571
2455
|
return c.json({ error: "No log file found" }, 404);
|
|
3572
2456
|
}
|
|
3573
2457
|
const nParam = parseInt(c.req.query("n") ?? "50", 10);
|
|
3574
2458
|
const n = Number.isFinite(nParam) && nParam > 0 ? Math.min(nParam, 1e4) : 50;
|
|
3575
|
-
const tail =
|
|
2459
|
+
const tail = spawn("tail", ["-n", String(n), logFile]);
|
|
3576
2460
|
let output = "";
|
|
3577
2461
|
tail.stdout.on("data", (data) => {
|
|
3578
2462
|
output += data.toString();
|
|
3579
2463
|
});
|
|
3580
|
-
await new Promise((
|
|
3581
|
-
tail.on("exit",
|
|
2464
|
+
await new Promise((resolve18) => {
|
|
2465
|
+
tail.on("exit", resolve18);
|
|
3582
2466
|
});
|
|
3583
2467
|
return c.text(output);
|
|
3584
2468
|
});
|
|
@@ -3668,33 +2552,33 @@ var mind_skills_default = app10;
|
|
|
3668
2552
|
// src/web/api/minds.ts
|
|
3669
2553
|
import {
|
|
3670
2554
|
cpSync,
|
|
3671
|
-
existsSync as
|
|
3672
|
-
mkdirSync as
|
|
3673
|
-
readdirSync as
|
|
3674
|
-
readFileSync as
|
|
3675
|
-
rmSync as
|
|
3676
|
-
writeFileSync as
|
|
2555
|
+
existsSync as existsSync9,
|
|
2556
|
+
mkdirSync as mkdirSync5,
|
|
2557
|
+
readdirSync as readdirSync4,
|
|
2558
|
+
readFileSync as readFileSync8,
|
|
2559
|
+
rmSync as rmSync3,
|
|
2560
|
+
writeFileSync as writeFileSync6
|
|
3677
2561
|
} from "fs";
|
|
3678
|
-
import { resolve as
|
|
2562
|
+
import { resolve as resolve11 } from "path";
|
|
3679
2563
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
3680
2564
|
import { and as and3, desc as desc3, eq as eq4, sql as sql2 } from "drizzle-orm";
|
|
3681
2565
|
import { Hono as Hono11 } from "hono";
|
|
3682
2566
|
import { z as z3 } from "zod";
|
|
3683
2567
|
|
|
3684
2568
|
// src/lib/consolidate.ts
|
|
3685
|
-
import { readdirSync as
|
|
3686
|
-
import { resolve as
|
|
2569
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
2570
|
+
import { resolve as resolve9 } from "path";
|
|
3687
2571
|
async function consolidateMemory(mindDir2) {
|
|
3688
|
-
const soulPath =
|
|
3689
|
-
const memoryPath =
|
|
3690
|
-
const memoryDir =
|
|
3691
|
-
const soul =
|
|
2572
|
+
const soulPath = resolve9(mindDir2, "home/SOUL.md");
|
|
2573
|
+
const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
|
|
2574
|
+
const memoryDir = resolve9(mindDir2, "home/memory");
|
|
2575
|
+
const soul = readFileSync6(soulPath, "utf-8");
|
|
3692
2576
|
const logs = [];
|
|
3693
2577
|
try {
|
|
3694
|
-
const files =
|
|
2578
|
+
const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
|
|
3695
2579
|
for (const filename of files) {
|
|
3696
2580
|
const date = filename.replace(".md", "");
|
|
3697
|
-
const content2 =
|
|
2581
|
+
const content2 = readFileSync6(resolve9(memoryDir, filename), "utf-8").trim();
|
|
3698
2582
|
if (content2) {
|
|
3699
2583
|
logs.push(`### ${date}
|
|
3700
2584
|
|
|
@@ -3744,7 +2628,7 @@ ${content2}`);
|
|
|
3744
2628
|
const data = await res.json();
|
|
3745
2629
|
const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
|
|
3746
2630
|
if (content) {
|
|
3747
|
-
|
|
2631
|
+
writeFileSync4(memoryPath, `${content}
|
|
3748
2632
|
`);
|
|
3749
2633
|
console.log("MEMORY.md created successfully.");
|
|
3750
2634
|
} else {
|
|
@@ -3754,11 +2638,11 @@ ${content2}`);
|
|
|
3754
2638
|
|
|
3755
2639
|
// src/lib/convert-session.ts
|
|
3756
2640
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3757
|
-
import { mkdirSync as
|
|
2641
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
3758
2642
|
import { homedir } from "os";
|
|
3759
|
-
import { resolve as
|
|
2643
|
+
import { resolve as resolve10 } from "path";
|
|
3760
2644
|
function convertSession(opts) {
|
|
3761
|
-
const lines =
|
|
2645
|
+
const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
|
|
3762
2646
|
const sessionId = randomUUID2();
|
|
3763
2647
|
const idMap = /* @__PURE__ */ new Map();
|
|
3764
2648
|
const messages2 = [];
|
|
@@ -3872,10 +2756,10 @@ function convertSession(opts) {
|
|
|
3872
2756
|
}
|
|
3873
2757
|
}
|
|
3874
2758
|
const projectId = opts.projectDir.replace(/\//g, "-");
|
|
3875
|
-
const sdkDir =
|
|
3876
|
-
|
|
3877
|
-
const sdkPath =
|
|
3878
|
-
|
|
2759
|
+
const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
|
|
2760
|
+
mkdirSync4(sdkDir, { recursive: true });
|
|
2761
|
+
const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
|
|
2762
|
+
writeFileSync5(sdkPath, `${sdkEvents.join("\n")}
|
|
3879
2763
|
`);
|
|
3880
2764
|
console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
|
|
3881
2765
|
return sessionId;
|
|
@@ -3940,7 +2824,7 @@ function subscribe3(mind, callback) {
|
|
|
3940
2824
|
if (set.size === 0) subscribers.delete(mind);
|
|
3941
2825
|
};
|
|
3942
2826
|
}
|
|
3943
|
-
function
|
|
2827
|
+
function publish2(mind, event) {
|
|
3944
2828
|
const set = subscribers.get(mind);
|
|
3945
2829
|
if (!set) return;
|
|
3946
2830
|
for (const cb of set) {
|
|
@@ -3954,11 +2838,53 @@ function publish3(mind, event) {
|
|
|
3954
2838
|
}
|
|
3955
2839
|
}
|
|
3956
2840
|
|
|
2841
|
+
// src/lib/variant-cleanup.ts
|
|
2842
|
+
import { existsSync as existsSync8, rmSync as rmSync2 } from "fs";
|
|
2843
|
+
async function cleanupVariant(mindName, variantName, projectRoot, variantPath, opts) {
|
|
2844
|
+
if (opts?.stop) {
|
|
2845
|
+
try {
|
|
2846
|
+
await getMindManager().stopMind(`${mindName}@${variantName}`);
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
if (existsSync8(variantPath)) {
|
|
2851
|
+
try {
|
|
2852
|
+
await gitExec(["worktree", "remove", "--force", variantPath], { cwd: projectRoot });
|
|
2853
|
+
} catch {
|
|
2854
|
+
rmSync2(variantPath, { recursive: true, force: true });
|
|
2855
|
+
try {
|
|
2856
|
+
await gitExec(["worktree", "prune"], { cwd: projectRoot });
|
|
2857
|
+
} catch {
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
try {
|
|
2862
|
+
await gitExec(["branch", "-D", variantName], { cwd: projectRoot });
|
|
2863
|
+
} catch {
|
|
2864
|
+
}
|
|
2865
|
+
try {
|
|
2866
|
+
removeVariant(mindName, variantName);
|
|
2867
|
+
} catch {
|
|
2868
|
+
}
|
|
2869
|
+
try {
|
|
2870
|
+
chownMindDir(projectRoot, mindName);
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
logger_default.error(`failed to fix ownership during variant cleanup for ${mindName}`, logger_default.errorData(err));
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
3957
2876
|
// src/web/api/minds.ts
|
|
3958
2877
|
async function getMindStatus(name, port) {
|
|
3959
2878
|
const manager = getMindManager();
|
|
3960
2879
|
let status = "stopped";
|
|
3961
|
-
|
|
2880
|
+
try {
|
|
2881
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
2882
|
+
if (getSleepManagerIfReady()?.isSleeping(name)) {
|
|
2883
|
+
status = "sleeping";
|
|
2884
|
+
}
|
|
2885
|
+
} catch {
|
|
2886
|
+
}
|
|
2887
|
+
if (status !== "sleeping" && manager.isRunning(name)) {
|
|
3962
2888
|
const health = await checkHealth(port);
|
|
3963
2889
|
status = health.ok ? "running" : "starting";
|
|
3964
2890
|
}
|
|
@@ -4010,7 +2936,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
|
|
|
4010
2936
|
await gitExec(["commit", "-m", "initial commit"], opts);
|
|
4011
2937
|
}
|
|
4012
2938
|
async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
4013
|
-
const tempWorktree =
|
|
2939
|
+
const tempWorktree = resolve11(projectRoot, ".variants", "_template_update");
|
|
4014
2940
|
let branchExists = false;
|
|
4015
2941
|
try {
|
|
4016
2942
|
await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
|
|
@@ -4021,8 +2947,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
4021
2947
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
4022
2948
|
} catch {
|
|
4023
2949
|
}
|
|
4024
|
-
if (
|
|
4025
|
-
|
|
2950
|
+
if (existsSync9(tempWorktree)) {
|
|
2951
|
+
rmSync3(tempWorktree, { recursive: true, force: true });
|
|
4026
2952
|
}
|
|
4027
2953
|
const templatesRoot = findTemplatesRoot();
|
|
4028
2954
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
@@ -4042,9 +2968,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
4042
2968
|
});
|
|
4043
2969
|
}
|
|
4044
2970
|
copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
|
|
4045
|
-
const initDir =
|
|
4046
|
-
if (
|
|
4047
|
-
|
|
2971
|
+
const initDir = resolve11(tempWorktree, ".init");
|
|
2972
|
+
if (existsSync9(initDir)) {
|
|
2973
|
+
rmSync3(initDir, { recursive: true, force: true });
|
|
4048
2974
|
}
|
|
4049
2975
|
await gitExec(["add", "-A"], { cwd: tempWorktree });
|
|
4050
2976
|
try {
|
|
@@ -4057,10 +2983,10 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
|
|
|
4057
2983
|
await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
|
|
4058
2984
|
} catch {
|
|
4059
2985
|
}
|
|
4060
|
-
if (
|
|
4061
|
-
|
|
2986
|
+
if (existsSync9(tempWorktree)) {
|
|
2987
|
+
rmSync3(tempWorktree, { recursive: true, force: true });
|
|
4062
2988
|
}
|
|
4063
|
-
|
|
2989
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4064
2990
|
}
|
|
4065
2991
|
}
|
|
4066
2992
|
async function mergeTemplateBranch(worktreeDir) {
|
|
@@ -4083,14 +3009,14 @@ async function mergeTemplateBranch(worktreeDir) {
|
|
|
4083
3009
|
async function npmInstallAsMind(cwd, mindName) {
|
|
4084
3010
|
if (isIsolationEnabled()) {
|
|
4085
3011
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
4086
|
-
await exec(cmd, args, { cwd, env: { ...process.env, HOME:
|
|
3012
|
+
await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve11(cwd, "home") } });
|
|
4087
3013
|
} else {
|
|
4088
3014
|
await exec("npm", ["install"], { cwd });
|
|
4089
3015
|
}
|
|
4090
3016
|
}
|
|
4091
3017
|
async function importFromArchive(c, tempDir, nameOverride, manifest) {
|
|
4092
|
-
const extractedMindDir =
|
|
4093
|
-
if (!
|
|
3018
|
+
const extractedMindDir = resolve11(tempDir, "mind");
|
|
3019
|
+
if (!existsSync9(extractedMindDir)) {
|
|
4094
3020
|
return c.json({ error: "Invalid archive: missing mind/ directory" }, 400);
|
|
4095
3021
|
}
|
|
4096
3022
|
if (!manifest?.includes || !manifest.name || !manifest.template) {
|
|
@@ -4108,21 +3034,21 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
|
|
|
4108
3034
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
4109
3035
|
ensureVoluteHome();
|
|
4110
3036
|
const dest = mindDir(name);
|
|
4111
|
-
if (
|
|
3037
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4112
3038
|
try {
|
|
4113
3039
|
cpSync(extractedMindDir, dest, { recursive: true });
|
|
4114
3040
|
if (!manifest.includes.identity) {
|
|
4115
3041
|
generateIdentity(dest);
|
|
4116
3042
|
}
|
|
4117
3043
|
const state = stateDir(name);
|
|
4118
|
-
|
|
4119
|
-
const channelsJson =
|
|
4120
|
-
if (
|
|
4121
|
-
cpSync(channelsJson,
|
|
3044
|
+
mkdirSync5(state, { recursive: true });
|
|
3045
|
+
const channelsJson = resolve11(tempDir, "state/channels.json");
|
|
3046
|
+
if (existsSync9(channelsJson)) {
|
|
3047
|
+
cpSync(channelsJson, resolve11(state, "channels.json"));
|
|
4122
3048
|
}
|
|
4123
|
-
const envJson =
|
|
4124
|
-
if (
|
|
4125
|
-
cpSync(envJson,
|
|
3049
|
+
const envJson = resolve11(tempDir, "state/env.json");
|
|
3050
|
+
if (existsSync9(envJson)) {
|
|
3051
|
+
cpSync(envJson, resolve11(state, "env.json"));
|
|
4126
3052
|
}
|
|
4127
3053
|
const port = nextPort();
|
|
4128
3054
|
addMind(name, port, manifest.stage, manifest.template);
|
|
@@ -4131,36 +3057,36 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
|
|
|
4131
3057
|
} catch (err) {
|
|
4132
3058
|
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4133
3059
|
}
|
|
4134
|
-
const homeDir =
|
|
3060
|
+
const homeDir = resolve11(dest, "home");
|
|
4135
3061
|
ensureVoluteGroup();
|
|
4136
3062
|
createMindUser(name, homeDir);
|
|
4137
3063
|
chownMindDir(dest, name);
|
|
4138
3064
|
await npmInstallAsMind(dest, name);
|
|
4139
3065
|
await importHistoryFromArchive(name, tempDir);
|
|
4140
3066
|
importSessionsFromArchive(dest, tempDir);
|
|
4141
|
-
if (!
|
|
3067
|
+
if (!existsSync9(resolve11(dest, ".git"))) {
|
|
4142
3068
|
try {
|
|
4143
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3069
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
|
|
4144
3070
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
4145
3071
|
await configureGitIdentity(name, { cwd: dest, mindName: name, env });
|
|
4146
3072
|
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
4147
3073
|
await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
|
|
4148
3074
|
} catch (err) {
|
|
4149
3075
|
logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
|
|
4150
|
-
|
|
3076
|
+
rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
|
|
4151
3077
|
}
|
|
4152
3078
|
}
|
|
4153
3079
|
chownMindDir(dest, name);
|
|
4154
|
-
|
|
3080
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4155
3081
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
4156
3082
|
} catch (err) {
|
|
4157
|
-
if (
|
|
3083
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4158
3084
|
try {
|
|
4159
3085
|
removeMind(name);
|
|
4160
3086
|
} catch (cleanupErr) {
|
|
4161
3087
|
logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
|
|
4162
3088
|
}
|
|
4163
|
-
|
|
3089
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4164
3090
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4165
3091
|
}
|
|
4166
3092
|
}
|
|
@@ -4171,7 +3097,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4171
3097
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
4172
3098
|
ensureVoluteHome();
|
|
4173
3099
|
const dest = mindDir(name);
|
|
4174
|
-
if (
|
|
3100
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4175
3101
|
const templatesRoot = findTemplatesRoot();
|
|
4176
3102
|
const { composedDir, manifest: templateManifest } = composeTemplate(
|
|
4177
3103
|
templatesRoot,
|
|
@@ -4180,40 +3106,40 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4180
3106
|
try {
|
|
4181
3107
|
copyTemplateToDir(composedDir, dest, name, templateManifest);
|
|
4182
3108
|
applyInitFiles(dest);
|
|
4183
|
-
const extractedHome =
|
|
4184
|
-
if (
|
|
4185
|
-
cpSync(extractedHome,
|
|
3109
|
+
const extractedHome = resolve11(extractedMindDir, "home");
|
|
3110
|
+
if (existsSync9(extractedHome)) {
|
|
3111
|
+
cpSync(extractedHome, resolve11(dest, "home"), { recursive: true });
|
|
4186
3112
|
}
|
|
4187
|
-
const extractedMindInternal =
|
|
4188
|
-
if (
|
|
4189
|
-
cpSync(extractedMindInternal,
|
|
3113
|
+
const extractedMindInternal = resolve11(extractedMindDir, ".mind");
|
|
3114
|
+
if (existsSync9(extractedMindInternal)) {
|
|
3115
|
+
cpSync(extractedMindInternal, resolve11(dest, ".mind"), { recursive: true });
|
|
4190
3116
|
}
|
|
4191
|
-
const identityDir =
|
|
3117
|
+
const identityDir = resolve11(dest, ".mind/identity");
|
|
4192
3118
|
let publicKeyPem;
|
|
4193
|
-
if (!manifest.includes.identity || !
|
|
3119
|
+
if (!manifest.includes.identity || !existsSync9(resolve11(identityDir, "private.pem"))) {
|
|
4194
3120
|
({ publicKeyPem } = generateIdentity(dest));
|
|
4195
3121
|
} else {
|
|
4196
|
-
publicKeyPem =
|
|
3122
|
+
publicKeyPem = readFileSync8(resolve11(identityDir, "public.pem"), "utf-8");
|
|
4197
3123
|
}
|
|
4198
|
-
const promptsPath =
|
|
4199
|
-
if (!
|
|
3124
|
+
const promptsPath = resolve11(dest, "home/.config/prompts.json");
|
|
3125
|
+
if (!existsSync9(promptsPath)) {
|
|
4200
3126
|
const mindPrompts = await getMindPromptDefaults();
|
|
4201
|
-
|
|
3127
|
+
writeFileSync6(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
|
|
4202
3128
|
`);
|
|
4203
3129
|
}
|
|
4204
3130
|
const state = stateDir(name);
|
|
4205
|
-
|
|
4206
|
-
const channelsJson =
|
|
4207
|
-
if (
|
|
4208
|
-
cpSync(channelsJson,
|
|
3131
|
+
mkdirSync5(state, { recursive: true });
|
|
3132
|
+
const channelsJson = resolve11(tempDir, "state/channels.json");
|
|
3133
|
+
if (existsSync9(channelsJson)) {
|
|
3134
|
+
cpSync(channelsJson, resolve11(state, "channels.json"));
|
|
4209
3135
|
}
|
|
4210
|
-
const envJson =
|
|
4211
|
-
if (
|
|
4212
|
-
cpSync(envJson,
|
|
3136
|
+
const envJson = resolve11(tempDir, "state/env.json");
|
|
3137
|
+
if (existsSync9(envJson)) {
|
|
3138
|
+
cpSync(envJson, resolve11(state, "env.json"));
|
|
4213
3139
|
}
|
|
4214
3140
|
const port = nextPort();
|
|
4215
3141
|
addMind(name, port, manifest.stage, manifest.template);
|
|
4216
|
-
const homeDir =
|
|
3142
|
+
const homeDir = resolve11(dest, "home");
|
|
4217
3143
|
ensureVoluteGroup();
|
|
4218
3144
|
createMindUser(name, homeDir);
|
|
4219
3145
|
chownMindDir(dest, name);
|
|
@@ -4226,7 +3152,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4226
3152
|
await initTemplateBranch(dest, composedDir, templateManifest, name, env);
|
|
4227
3153
|
} catch (err) {
|
|
4228
3154
|
logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
|
|
4229
|
-
|
|
3155
|
+
rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
|
|
4230
3156
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
4231
3157
|
}
|
|
4232
3158
|
try {
|
|
@@ -4250,7 +3176,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4250
3176
|
publishPublicKey(name, publicKeyPem).catch(
|
|
4251
3177
|
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
4252
3178
|
);
|
|
4253
|
-
|
|
3179
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4254
3180
|
return c.json({
|
|
4255
3181
|
ok: true,
|
|
4256
3182
|
name,
|
|
@@ -4261,24 +3187,24 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
|
|
|
4261
3187
|
...skillWarnings.length > 0 && { skillWarnings }
|
|
4262
3188
|
});
|
|
4263
3189
|
} catch (err) {
|
|
4264
|
-
if (
|
|
3190
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4265
3191
|
try {
|
|
4266
3192
|
removeMind(name);
|
|
4267
3193
|
} catch (cleanupErr) {
|
|
4268
3194
|
logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
|
|
4269
3195
|
}
|
|
4270
|
-
|
|
3196
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
4271
3197
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4272
3198
|
} finally {
|
|
4273
|
-
|
|
3199
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4274
3200
|
}
|
|
4275
3201
|
}
|
|
4276
3202
|
async function importHistoryFromArchive(name, tempDir) {
|
|
4277
|
-
const historyJsonl =
|
|
4278
|
-
if (!
|
|
3203
|
+
const historyJsonl = resolve11(tempDir, "history.jsonl");
|
|
3204
|
+
if (!existsSync9(historyJsonl)) return;
|
|
4279
3205
|
try {
|
|
4280
3206
|
const db = await getDb();
|
|
4281
|
-
const lines =
|
|
3207
|
+
const lines = readFileSync8(historyJsonl, "utf-8").trim().split("\n");
|
|
4282
3208
|
let imported = 0;
|
|
4283
3209
|
let failed = 0;
|
|
4284
3210
|
for (const line of lines) {
|
|
@@ -4314,13 +3240,13 @@ async function importHistoryFromArchive(name, tempDir) {
|
|
|
4314
3240
|
}
|
|
4315
3241
|
}
|
|
4316
3242
|
function importSessionsFromArchive(dest, tempDir) {
|
|
4317
|
-
const sessionsDir =
|
|
4318
|
-
if (!
|
|
3243
|
+
const sessionsDir = resolve11(tempDir, "sessions");
|
|
3244
|
+
if (!existsSync9(sessionsDir)) return;
|
|
4319
3245
|
try {
|
|
4320
|
-
const destSessions =
|
|
4321
|
-
|
|
4322
|
-
for (const file of
|
|
4323
|
-
cpSync(
|
|
3246
|
+
const destSessions = resolve11(dest, ".mind/sessions");
|
|
3247
|
+
mkdirSync5(destSessions, { recursive: true });
|
|
3248
|
+
for (const file of readdirSync4(sessionsDir)) {
|
|
3249
|
+
cpSync(resolve11(sessionsDir, file), resolve11(destSessions, file));
|
|
4324
3250
|
}
|
|
4325
3251
|
} catch (err) {
|
|
4326
3252
|
logger_default.error("Failed to import sessions from archive", logger_default.errorData(err));
|
|
@@ -4343,7 +3269,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4343
3269
|
if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
|
|
4344
3270
|
ensureVoluteHome();
|
|
4345
3271
|
const dest = mindDir(name);
|
|
4346
|
-
if (
|
|
3272
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4347
3273
|
const templatesRoot = findTemplatesRoot();
|
|
4348
3274
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
4349
3275
|
try {
|
|
@@ -4357,15 +3283,15 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4357
3283
|
writeVoluteConfig(dest, seedConfig);
|
|
4358
3284
|
}
|
|
4359
3285
|
if (body.model) {
|
|
4360
|
-
const configPath2 =
|
|
4361
|
-
const existing =
|
|
3286
|
+
const configPath2 = resolve11(dest, "home/.config/config.json");
|
|
3287
|
+
const existing = existsSync9(configPath2) ? JSON.parse(readFileSync8(configPath2, "utf-8")) : {};
|
|
4362
3288
|
existing.model = body.model;
|
|
4363
|
-
|
|
3289
|
+
writeFileSync6(configPath2, `${JSON.stringify(existing, null, 2)}
|
|
4364
3290
|
`);
|
|
4365
3291
|
}
|
|
4366
3292
|
const mindPrompts = await getMindPromptDefaults();
|
|
4367
|
-
|
|
4368
|
-
|
|
3293
|
+
writeFileSync6(
|
|
3294
|
+
resolve11(dest, "home/.config/prompts.json"),
|
|
4369
3295
|
`${JSON.stringify(mindPrompts, null, 2)}
|
|
4370
3296
|
`
|
|
4371
3297
|
);
|
|
@@ -4376,7 +3302,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4376
3302
|
} catch (err) {
|
|
4377
3303
|
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4378
3304
|
}
|
|
4379
|
-
const homeDir =
|
|
3305
|
+
const homeDir = resolve11(dest, "home");
|
|
4380
3306
|
ensureVoluteGroup();
|
|
4381
3307
|
createMindUser(name, homeDir);
|
|
4382
3308
|
chownMindDir(dest, name);
|
|
@@ -4389,7 +3315,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
|
|
|
4389
3315
|
await initTemplateBranch(dest, composedDir, manifest, name, env);
|
|
4390
3316
|
} catch (err) {
|
|
4391
3317
|
logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
|
|
4392
|
-
|
|
3318
|
+
rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
|
|
4393
3319
|
gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
|
|
4394
3320
|
}
|
|
4395
3321
|
try {
|
|
@@ -4404,7 +3330,7 @@ The human who planted you described you as: "${body.description}"
|
|
|
4404
3330
|
` : "";
|
|
4405
3331
|
const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
|
|
4406
3332
|
const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
|
|
4407
|
-
|
|
3333
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
|
|
4408
3334
|
}
|
|
4409
3335
|
const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
|
|
4410
3336
|
const skillWarnings = [];
|
|
@@ -4419,16 +3345,27 @@ The human who planted you described you as: "${body.description}"
|
|
|
4419
3345
|
if (body.stage !== "seed") {
|
|
4420
3346
|
const customSoul = await getPromptIfCustom("default_soul");
|
|
4421
3347
|
if (customSoul) {
|
|
4422
|
-
|
|
3348
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
|
|
4423
3349
|
}
|
|
4424
3350
|
const customMemory = await getPromptIfCustom("default_memory");
|
|
4425
3351
|
if (customMemory) {
|
|
4426
|
-
|
|
3352
|
+
writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
|
|
4427
3353
|
}
|
|
4428
3354
|
}
|
|
4429
3355
|
publishPublicKey(name, publicKeyPem).catch(
|
|
4430
3356
|
(err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
|
|
4431
3357
|
);
|
|
3358
|
+
fireWebhook({
|
|
3359
|
+
event: "mind_created",
|
|
3360
|
+
mind: name,
|
|
3361
|
+
data: {
|
|
3362
|
+
name,
|
|
3363
|
+
port,
|
|
3364
|
+
stage: body.stage ?? "sprouted",
|
|
3365
|
+
template,
|
|
3366
|
+
description: body.description
|
|
3367
|
+
}
|
|
3368
|
+
});
|
|
4432
3369
|
return c.json({
|
|
4433
3370
|
ok: true,
|
|
4434
3371
|
name,
|
|
@@ -4439,14 +3376,14 @@ The human who planted you described you as: "${body.description}"
|
|
|
4439
3376
|
...skillWarnings.length > 0 && { skillWarnings }
|
|
4440
3377
|
});
|
|
4441
3378
|
} catch (err) {
|
|
4442
|
-
if (
|
|
3379
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4443
3380
|
try {
|
|
4444
3381
|
removeMind(name);
|
|
4445
3382
|
} catch {
|
|
4446
3383
|
}
|
|
4447
3384
|
return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
|
|
4448
3385
|
} finally {
|
|
4449
|
-
|
|
3386
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4450
3387
|
}
|
|
4451
3388
|
}).post("/import", requireAdmin, async (c) => {
|
|
4452
3389
|
let body;
|
|
@@ -4459,13 +3396,13 @@ The human who planted you described you as: "${body.description}"
|
|
|
4459
3396
|
return importFromArchive(c, body.archivePath, body.name, body.manifest);
|
|
4460
3397
|
}
|
|
4461
3398
|
const wsDir = body.workspacePath;
|
|
4462
|
-
if (!wsDir || !
|
|
3399
|
+
if (!wsDir || !existsSync9(resolve11(wsDir, "SOUL.md")) || !existsSync9(resolve11(wsDir, "IDENTITY.md"))) {
|
|
4463
3400
|
return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
|
|
4464
3401
|
}
|
|
4465
|
-
const soul =
|
|
4466
|
-
const identity =
|
|
4467
|
-
const userPath =
|
|
4468
|
-
const user =
|
|
3402
|
+
const soul = readFileSync8(resolve11(wsDir, "SOUL.md"), "utf-8");
|
|
3403
|
+
const identity = readFileSync8(resolve11(wsDir, "IDENTITY.md"), "utf-8");
|
|
3404
|
+
const userPath = resolve11(wsDir, "USER.md");
|
|
3405
|
+
const user = existsSync9(userPath) ? readFileSync8(userPath, "utf-8") : "";
|
|
4469
3406
|
const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
|
|
4470
3407
|
const template = body.template ?? "claude";
|
|
4471
3408
|
const nameErr = validateMindName(name);
|
|
@@ -4485,33 +3422,33 @@ ${user.trimEnd()}
|
|
|
4485
3422
|
` : "";
|
|
4486
3423
|
ensureVoluteHome();
|
|
4487
3424
|
const dest = mindDir(name);
|
|
4488
|
-
if (
|
|
3425
|
+
if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
|
|
4489
3426
|
const templatesRoot = findTemplatesRoot();
|
|
4490
3427
|
const { composedDir, manifest } = composeTemplate(templatesRoot, template);
|
|
4491
3428
|
try {
|
|
4492
3429
|
copyTemplateToDir(composedDir, dest, name, manifest);
|
|
4493
3430
|
applyInitFiles(dest);
|
|
4494
3431
|
const { publicKeyPem: importPublicKey } = generateIdentity(dest);
|
|
4495
|
-
|
|
4496
|
-
const wsMemoryPath =
|
|
4497
|
-
const hasMemory =
|
|
3432
|
+
writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
|
|
3433
|
+
const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
|
|
3434
|
+
const hasMemory = existsSync9(wsMemoryPath);
|
|
4498
3435
|
if (hasMemory) {
|
|
4499
|
-
const existingMemory =
|
|
4500
|
-
|
|
4501
|
-
|
|
3436
|
+
const existingMemory = readFileSync8(wsMemoryPath, "utf-8");
|
|
3437
|
+
writeFileSync6(
|
|
3438
|
+
resolve11(dest, "home/MEMORY.md"),
|
|
4502
3439
|
`${existingMemory.trimEnd()}${mergedMemoryExtra}`
|
|
4503
3440
|
);
|
|
4504
3441
|
} else if (user) {
|
|
4505
|
-
|
|
3442
|
+
writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
|
|
4506
3443
|
`);
|
|
4507
3444
|
}
|
|
4508
|
-
const wsMemoryDir =
|
|
3445
|
+
const wsMemoryDir = resolve11(wsDir, "memory");
|
|
4509
3446
|
let dailyLogCount = 0;
|
|
4510
|
-
if (
|
|
4511
|
-
const destMemoryDir =
|
|
4512
|
-
const files =
|
|
3447
|
+
if (existsSync9(wsMemoryDir)) {
|
|
3448
|
+
const destMemoryDir = resolve11(dest, "home/memory");
|
|
3449
|
+
const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
|
|
4513
3450
|
for (const file of files) {
|
|
4514
|
-
cpSync(
|
|
3451
|
+
cpSync(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
|
|
4515
3452
|
}
|
|
4516
3453
|
dailyLogCount = files.length;
|
|
4517
3454
|
}
|
|
@@ -4522,7 +3459,7 @@ ${user.trimEnd()}
|
|
|
4522
3459
|
} catch (err) {
|
|
4523
3460
|
logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
|
|
4524
3461
|
}
|
|
4525
|
-
const homeDir =
|
|
3462
|
+
const homeDir = resolve11(dest, "home");
|
|
4526
3463
|
ensureVoluteGroup();
|
|
4527
3464
|
createMindUser(name, homeDir);
|
|
4528
3465
|
chownMindDir(dest, name);
|
|
@@ -4530,20 +3467,20 @@ ${user.trimEnd()}
|
|
|
4530
3467
|
if (!hasMemory && dailyLogCount > 0) {
|
|
4531
3468
|
await consolidateMemory(dest);
|
|
4532
3469
|
}
|
|
4533
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3470
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
|
|
4534
3471
|
await gitExec(["init"], { cwd: dest, mindName: name, env });
|
|
4535
3472
|
await configureGitIdentity(name, { cwd: dest, mindName: name, env });
|
|
4536
3473
|
await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
|
|
4537
3474
|
await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
|
|
4538
|
-
const sessionFile = body.sessionPath ?
|
|
4539
|
-
if (sessionFile &&
|
|
3475
|
+
const sessionFile = body.sessionPath ? resolve11(body.sessionPath) : findOpenClawSession(wsDir);
|
|
3476
|
+
if (sessionFile && existsSync9(sessionFile)) {
|
|
4540
3477
|
if (template === "pi") {
|
|
4541
3478
|
importPiSession(sessionFile, dest);
|
|
4542
3479
|
} else if (template === "claude") {
|
|
4543
3480
|
const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
|
|
4544
|
-
const mindRuntimeDir =
|
|
4545
|
-
|
|
4546
|
-
|
|
3481
|
+
const mindRuntimeDir = resolve11(dest, ".mind");
|
|
3482
|
+
mkdirSync5(mindRuntimeDir, { recursive: true });
|
|
3483
|
+
writeFileSync6(resolve11(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
|
|
4547
3484
|
}
|
|
4548
3485
|
}
|
|
4549
3486
|
importOpenClawConnectors(name, dest);
|
|
@@ -4558,14 +3495,14 @@ ${user.trimEnd()}
|
|
|
4558
3495
|
);
|
|
4559
3496
|
return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
|
|
4560
3497
|
} catch (err) {
|
|
4561
|
-
if (
|
|
3498
|
+
if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
|
|
4562
3499
|
try {
|
|
4563
3500
|
removeMind(name);
|
|
4564
3501
|
} catch {
|
|
4565
3502
|
}
|
|
4566
3503
|
return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
|
|
4567
3504
|
} finally {
|
|
4568
|
-
|
|
3505
|
+
rmSync3(composedDir, { recursive: true, force: true });
|
|
4569
3506
|
}
|
|
4570
3507
|
}).get("/", async (c) => {
|
|
4571
3508
|
const entries = readRegistry();
|
|
@@ -4582,7 +3519,7 @@ ${user.trimEnd()}
|
|
|
4582
3519
|
const minds = await Promise.all(
|
|
4583
3520
|
entries.map(async (entry) => {
|
|
4584
3521
|
const mindStatus = await getMindStatus(entry.name, entry.port);
|
|
4585
|
-
const hasPages =
|
|
3522
|
+
const hasPages = existsSync9(resolve11(mindDir(entry.name), "home", "pages"));
|
|
4586
3523
|
return {
|
|
4587
3524
|
...entry,
|
|
4588
3525
|
...mindStatus,
|
|
@@ -4600,7 +3537,7 @@ ${user.trimEnd()}
|
|
|
4600
3537
|
const name = c.req.param("name");
|
|
4601
3538
|
const entry = findMind(name);
|
|
4602
3539
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4603
|
-
if (!
|
|
3540
|
+
if (!existsSync9(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
|
|
4604
3541
|
const mindStatus = await getMindStatus(name, entry.port);
|
|
4605
3542
|
const variants = readVariants(name);
|
|
4606
3543
|
const manager = getMindManager();
|
|
@@ -4615,7 +3552,7 @@ ${user.trimEnd()}
|
|
|
4615
3552
|
return { name: v.name, port: v.port, status: variantStatus };
|
|
4616
3553
|
})
|
|
4617
3554
|
);
|
|
4618
|
-
const hasPages =
|
|
3555
|
+
const hasPages = existsSync9(resolve11(mindDir(name), "home", "pages"));
|
|
4619
3556
|
return c.json({ ...entry, ...mindStatus, variants: variantStatuses, hasPages });
|
|
4620
3557
|
}).post("/:name/start", requireAdmin, async (c) => {
|
|
4621
3558
|
const name = c.req.param("name");
|
|
@@ -4629,7 +3566,7 @@ ${user.trimEnd()}
|
|
|
4629
3566
|
targetPort = variant.port;
|
|
4630
3567
|
} else {
|
|
4631
3568
|
const dir = mindDir(baseName);
|
|
4632
|
-
if (!
|
|
3569
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
4633
3570
|
}
|
|
4634
3571
|
if (getMindManager().isRunning(name)) {
|
|
4635
3572
|
return c.json({ error: "Mind already running" }, 409);
|
|
@@ -4652,7 +3589,7 @@ ${user.trimEnd()}
|
|
|
4652
3589
|
targetPort = variant.port;
|
|
4653
3590
|
} else {
|
|
4654
3591
|
const dir = mindDir(baseName);
|
|
4655
|
-
if (!
|
|
3592
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
4656
3593
|
}
|
|
4657
3594
|
let context;
|
|
4658
3595
|
const contentType = c.req.header("content-type");
|
|
@@ -4679,7 +3616,7 @@ ${user.trimEnd()}
|
|
|
4679
3616
|
const variant = findVariant(baseName, mergeVariantName);
|
|
4680
3617
|
if (variant) {
|
|
4681
3618
|
const projectRoot = mindDir(baseName);
|
|
4682
|
-
if (
|
|
3619
|
+
if (existsSync9(variant.path)) {
|
|
4683
3620
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
4684
3621
|
if (status) {
|
|
4685
3622
|
try {
|
|
@@ -4703,24 +3640,11 @@ ${user.trimEnd()}
|
|
|
4703
3640
|
cwd: projectRoot
|
|
4704
3641
|
});
|
|
4705
3642
|
} catch (e) {
|
|
4706
|
-
logger_default.error(`failed to auto-commit main worktree for ${baseName}`, logger_default.errorData(e));
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
await gitExec(["merge", variant.branch], { cwd: projectRoot });
|
|
4710
|
-
if (existsSync12(variant.path)) {
|
|
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 {
|
|
3643
|
+
logger_default.error(`failed to auto-commit main worktree for ${baseName}`, logger_default.errorData(e));
|
|
3644
|
+
}
|
|
4721
3645
|
}
|
|
4722
|
-
|
|
4723
|
-
|
|
3646
|
+
await gitExec(["merge", variant.branch], { cwd: projectRoot });
|
|
3647
|
+
await cleanupVariant(baseName, mergeVariantName, projectRoot, variant.path);
|
|
4724
3648
|
try {
|
|
4725
3649
|
await npmInstallAsMind(projectRoot, baseName);
|
|
4726
3650
|
} catch (e) {
|
|
@@ -4768,6 +3692,53 @@ ${user.trimEnd()}
|
|
|
4768
3692
|
} catch (err) {
|
|
4769
3693
|
return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
|
|
4770
3694
|
}
|
|
3695
|
+
}).get("/:name/sleep", requireAdmin, async (c) => {
|
|
3696
|
+
const name = c.req.param("name");
|
|
3697
|
+
const entry = findMind(name);
|
|
3698
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3699
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
3700
|
+
const sm = getSleepManagerIfReady();
|
|
3701
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3702
|
+
return c.json(sm.getState(name));
|
|
3703
|
+
}).post("/:name/sleep", requireAdmin, async (c) => {
|
|
3704
|
+
const name = c.req.param("name");
|
|
3705
|
+
const entry = findMind(name);
|
|
3706
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3707
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
3708
|
+
const sm = getSleepManagerIfReady();
|
|
3709
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3710
|
+
if (sm.isSleeping(name)) return c.json({ error: "Mind is already sleeping" }, 409);
|
|
3711
|
+
const body = await c.req.json().catch(() => ({}));
|
|
3712
|
+
const wakeAt = body.wakeAt;
|
|
3713
|
+
if (wakeAt) {
|
|
3714
|
+
const wakeDate = new Date(wakeAt);
|
|
3715
|
+
if (Number.isNaN(wakeDate.getTime()) || wakeDate <= /* @__PURE__ */ new Date()) {
|
|
3716
|
+
return c.json({ error: "wakeAt must be a valid future ISO date" }, 400);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
sm.initiateSleep(name, wakeAt ? { voluntaryWakeAt: wakeAt } : void 0).catch(
|
|
3720
|
+
(err) => logger_default.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
|
|
3721
|
+
);
|
|
3722
|
+
return c.json({ ok: true });
|
|
3723
|
+
}).post("/:name/wake", requireAdmin, async (c) => {
|
|
3724
|
+
const name = c.req.param("name");
|
|
3725
|
+
const entry = findMind(name);
|
|
3726
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3727
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
3728
|
+
const sm = getSleepManagerIfReady();
|
|
3729
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3730
|
+
if (!sm.isSleeping(name)) return c.json({ error: "Mind is not sleeping" }, 409);
|
|
3731
|
+
sm.initiateWake(name).catch((err) => logger_default.error(`failed to wake ${name}`, logger_default.errorData(err)));
|
|
3732
|
+
return c.json({ ok: true });
|
|
3733
|
+
}).post("/:name/sleep/messages", requireAdmin, async (c) => {
|
|
3734
|
+
const name = c.req.param("name");
|
|
3735
|
+
const entry = findMind(name);
|
|
3736
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
3737
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
3738
|
+
const sm = getSleepManagerIfReady();
|
|
3739
|
+
if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
|
|
3740
|
+
const flushed = await sm.flushQueuedMessages(name);
|
|
3741
|
+
return c.json({ ok: true, flushed });
|
|
4771
3742
|
}).post("/:name/sprout", requireAdmin, async (c) => {
|
|
4772
3743
|
const name = c.req.param("name");
|
|
4773
3744
|
const entry = findMind(name);
|
|
@@ -4796,20 +3767,25 @@ ${user.trimEnd()}
|
|
|
4796
3767
|
removeMind(name);
|
|
4797
3768
|
await deleteMindUser2(name);
|
|
4798
3769
|
const state = stateDir(name);
|
|
4799
|
-
if (
|
|
4800
|
-
|
|
3770
|
+
if (existsSync9(state)) {
|
|
3771
|
+
rmSync3(state, { recursive: true, force: true });
|
|
4801
3772
|
}
|
|
4802
|
-
if (force &&
|
|
4803
|
-
|
|
3773
|
+
if (force && existsSync9(dir)) {
|
|
3774
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
4804
3775
|
deleteMindUser(name);
|
|
4805
3776
|
}
|
|
3777
|
+
fireWebhook({
|
|
3778
|
+
event: "mind_deleted",
|
|
3779
|
+
mind: name,
|
|
3780
|
+
data: { port: entry.port, stage: entry.stage, template: entry.template }
|
|
3781
|
+
});
|
|
4806
3782
|
return c.json({ ok: true });
|
|
4807
3783
|
}).post("/:name/upgrade", requireAdmin, async (c) => {
|
|
4808
3784
|
const mindName = c.req.param("name");
|
|
4809
3785
|
const entry = findMind(mindName);
|
|
4810
3786
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4811
3787
|
const dir = mindDir(mindName);
|
|
4812
|
-
if (!
|
|
3788
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
4813
3789
|
let body = {};
|
|
4814
3790
|
try {
|
|
4815
3791
|
body = await c.req.json();
|
|
@@ -4817,9 +3793,32 @@ ${user.trimEnd()}
|
|
|
4817
3793
|
}
|
|
4818
3794
|
const template = body.template ?? entry.template ?? "claude";
|
|
4819
3795
|
const UPGRADE_VARIANT = "upgrade";
|
|
3796
|
+
if (body.abort) {
|
|
3797
|
+
const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
|
|
3798
|
+
if (!existsSync9(worktreeDir2)) {
|
|
3799
|
+
return c.json({ error: "No upgrade in progress" }, 400);
|
|
3800
|
+
}
|
|
3801
|
+
try {
|
|
3802
|
+
try {
|
|
3803
|
+
const gitDirContent = readFileSync8(resolve11(worktreeDir2, ".git"), "utf-8").trim();
|
|
3804
|
+
const gitDir = gitDirContent.replace("gitdir: ", "");
|
|
3805
|
+
if (existsSync9(resolve11(gitDir, "MERGE_HEAD"))) {
|
|
3806
|
+
await gitExec(["merge", "--abort"], { cwd: worktreeDir2 });
|
|
3807
|
+
}
|
|
3808
|
+
} catch {
|
|
3809
|
+
}
|
|
3810
|
+
await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2, { stop: true });
|
|
3811
|
+
return c.json({ ok: true });
|
|
3812
|
+
} catch (err) {
|
|
3813
|
+
return c.json(
|
|
3814
|
+
{ error: err instanceof Error ? err.message : "Failed to abort upgrade" },
|
|
3815
|
+
500
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
4820
3819
|
if (body.continue) {
|
|
4821
|
-
const worktreeDir2 =
|
|
4822
|
-
if (!
|
|
3820
|
+
const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
|
|
3821
|
+
if (!existsSync9(worktreeDir2)) {
|
|
4823
3822
|
return c.json({ error: "No upgrade in progress" }, 400);
|
|
4824
3823
|
}
|
|
4825
3824
|
const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
|
|
@@ -4832,7 +3831,10 @@ ${user.trimEnd()}
|
|
|
4832
3831
|
await gitExec(["commit", "-m", "merge template update"], { cwd: worktreeDir2 });
|
|
4833
3832
|
} catch (e) {
|
|
4834
3833
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4835
|
-
|
|
3834
|
+
const stderr = e?.stderr ?? "";
|
|
3835
|
+
const stdout = e?.stdout ?? "";
|
|
3836
|
+
if (!msg.includes("nothing to commit") && !stderr.includes("nothing to commit") && !stdout.includes("nothing to commit"))
|
|
3837
|
+
throw e;
|
|
4836
3838
|
}
|
|
4837
3839
|
chownMindDir(dir, mindName);
|
|
4838
3840
|
try {
|
|
@@ -4853,49 +3855,30 @@ ${user.trimEnd()}
|
|
|
4853
3855
|
port: variantPort
|
|
4854
3856
|
});
|
|
4855
3857
|
} 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
|
-
}
|
|
3858
|
+
await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2);
|
|
4876
3859
|
return c.json(
|
|
4877
3860
|
{ error: err instanceof Error ? err.message : "Failed to continue upgrade" },
|
|
4878
3861
|
500
|
|
4879
3862
|
);
|
|
4880
3863
|
}
|
|
4881
3864
|
}
|
|
4882
|
-
const worktreeDir =
|
|
4883
|
-
if (
|
|
3865
|
+
const worktreeDir = resolve11(dir, ".variants", UPGRADE_VARIANT);
|
|
3866
|
+
if (existsSync9(worktreeDir)) {
|
|
4884
3867
|
return c.json(
|
|
4885
3868
|
{ error: "Upgrade variant already exists. Use continue or delete it first." },
|
|
4886
3869
|
409
|
|
4887
3870
|
);
|
|
4888
3871
|
}
|
|
4889
|
-
if (!
|
|
3872
|
+
if (!existsSync9(resolve11(dir, ".git"))) {
|
|
4890
3873
|
try {
|
|
4891
|
-
const env = isIsolationEnabled() ? { ...process.env, HOME:
|
|
3874
|
+
const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dir, "home") } : void 0;
|
|
4892
3875
|
await gitExec(["init"], { cwd: dir, mindName, env });
|
|
4893
3876
|
await configureGitIdentity(mindName, { cwd: dir, mindName, env });
|
|
4894
3877
|
await gitExec(["add", "-A"], { cwd: dir, mindName, env });
|
|
4895
3878
|
await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
|
|
4896
3879
|
chownMindDir(dir, mindName);
|
|
4897
3880
|
} catch (err) {
|
|
4898
|
-
|
|
3881
|
+
rmSync3(resolve11(dir, ".git"), { recursive: true, force: true });
|
|
4899
3882
|
return c.json(
|
|
4900
3883
|
{
|
|
4901
3884
|
error: `Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -4909,7 +3892,7 @@ ${user.trimEnd()}
|
|
|
4909
3892
|
await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
|
|
4910
3893
|
} catch {
|
|
4911
3894
|
}
|
|
4912
|
-
if (!
|
|
3895
|
+
if (!existsSync9(resolve11(dir, "home", "shared"))) {
|
|
4913
3896
|
try {
|
|
4914
3897
|
await addSharedWorktree(mindName, dir);
|
|
4915
3898
|
} catch (err) {
|
|
@@ -4920,9 +3903,9 @@ ${user.trimEnd()}
|
|
|
4920
3903
|
}
|
|
4921
3904
|
}
|
|
4922
3905
|
await updateTemplateBranch(dir, template, mindName);
|
|
4923
|
-
const parentDir =
|
|
4924
|
-
if (!
|
|
4925
|
-
|
|
3906
|
+
const parentDir = resolve11(dir, ".variants");
|
|
3907
|
+
if (!existsSync9(parentDir)) {
|
|
3908
|
+
mkdirSync5(parentDir, { recursive: true });
|
|
4926
3909
|
}
|
|
4927
3910
|
await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
|
|
4928
3911
|
const hasConflicts = await mergeTemplateBranch(worktreeDir);
|
|
@@ -4953,26 +3936,7 @@ ${user.trimEnd()}
|
|
|
4953
3936
|
port: variantPort
|
|
4954
3937
|
});
|
|
4955
3938
|
} 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
|
-
}
|
|
3939
|
+
await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir);
|
|
4976
3940
|
return c.json(
|
|
4977
3941
|
{ error: err instanceof Error ? err.message : "Failed to complete upgrade" },
|
|
4978
3942
|
500
|
|
@@ -4987,6 +3951,38 @@ ${user.trimEnd()}
|
|
|
4987
3951
|
const variant = findVariant(baseName, variantName);
|
|
4988
3952
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
4989
3953
|
}
|
|
3954
|
+
try {
|
|
3955
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
3956
|
+
const sm = getSleepManagerIfReady();
|
|
3957
|
+
if (sm?.isSleeping(baseName)) {
|
|
3958
|
+
const body2 = await c.req.text();
|
|
3959
|
+
let parsed2 = null;
|
|
3960
|
+
try {
|
|
3961
|
+
parsed2 = JSON.parse(body2);
|
|
3962
|
+
} catch {
|
|
3963
|
+
}
|
|
3964
|
+
if (parsed2) {
|
|
3965
|
+
const payload = {
|
|
3966
|
+
channel: parsed2.channel ?? "unknown",
|
|
3967
|
+
sender: parsed2.sender ?? null,
|
|
3968
|
+
content: parsed2.content,
|
|
3969
|
+
isDM: parsed2.isDM
|
|
3970
|
+
};
|
|
3971
|
+
if (sm.checkWakeTrigger(baseName, payload)) {
|
|
3972
|
+
await sm.queueSleepMessage(baseName, payload);
|
|
3973
|
+
sm.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch(
|
|
3974
|
+
(err) => logger_default.error(`failed to trigger-wake ${baseName}`, logger_default.errorData(err))
|
|
3975
|
+
);
|
|
3976
|
+
return c.json({ ok: true, queued: true, triggerWake: true });
|
|
3977
|
+
}
|
|
3978
|
+
await sm.queueSleepMessage(baseName, payload);
|
|
3979
|
+
return c.json({ ok: true, queued: true });
|
|
3980
|
+
}
|
|
3981
|
+
return c.json({ error: "Invalid JSON" }, 400);
|
|
3982
|
+
}
|
|
3983
|
+
} catch (err) {
|
|
3984
|
+
logger_default.error(`failed to check sleep state for ${baseName}`, logger_default.errorData(err));
|
|
3985
|
+
}
|
|
4990
3986
|
if (!getMindManager().isRunning(name)) {
|
|
4991
3987
|
return c.json({ error: "Mind is not running" }, 409);
|
|
4992
3988
|
}
|
|
@@ -5109,13 +4105,13 @@ ${user.trimEnd()}
|
|
|
5109
4105
|
const entry = findMind(name);
|
|
5110
4106
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
5111
4107
|
const dir = mindDir(name);
|
|
5112
|
-
if (!
|
|
4108
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
5113
4109
|
let config = readVoluteConfig(dir);
|
|
5114
4110
|
if (!config && entry.template === "pi") {
|
|
5115
|
-
const piConfigPath =
|
|
5116
|
-
if (
|
|
4111
|
+
const piConfigPath = resolve11(dir, "home/.config/config.json");
|
|
4112
|
+
if (existsSync9(piConfigPath)) {
|
|
5117
4113
|
try {
|
|
5118
|
-
config = JSON.parse(
|
|
4114
|
+
config = JSON.parse(readFileSync8(piConfigPath, "utf-8"));
|
|
5119
4115
|
} catch {
|
|
5120
4116
|
}
|
|
5121
4117
|
}
|
|
@@ -5152,7 +4148,7 @@ ${user.trimEnd()}
|
|
|
5152
4148
|
const entry = findMind(name);
|
|
5153
4149
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
5154
4150
|
const dir = mindDir(name);
|
|
5155
|
-
if (!
|
|
4151
|
+
if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
|
|
5156
4152
|
const body = c.req.valid("json");
|
|
5157
4153
|
const existing = readVoluteConfig(dir) ?? {};
|
|
5158
4154
|
if (body.model !== void 0) existing.model = body.model;
|
|
@@ -5215,7 +4211,7 @@ ${user.trimEnd()}
|
|
|
5215
4211
|
} catch (err) {
|
|
5216
4212
|
logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
|
|
5217
4213
|
}
|
|
5218
|
-
|
|
4214
|
+
publish2(baseName, {
|
|
5219
4215
|
mind: baseName,
|
|
5220
4216
|
type: body.type,
|
|
5221
4217
|
session: body.session,
|
|
@@ -5360,7 +4356,7 @@ var minds_default = app11;
|
|
|
5360
4356
|
|
|
5361
4357
|
// src/web/api/pages.ts
|
|
5362
4358
|
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
5363
|
-
import { extname as extname2, resolve as
|
|
4359
|
+
import { extname as extname2, resolve as resolve12 } from "path";
|
|
5364
4360
|
import { Hono as Hono12 } from "hono";
|
|
5365
4361
|
var MIME_TYPES = {
|
|
5366
4362
|
".html": "text/html",
|
|
@@ -5382,17 +4378,17 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
|
|
|
5382
4378
|
const name = c.req.param("name");
|
|
5383
4379
|
let pagesRoot;
|
|
5384
4380
|
if (name === "_system") {
|
|
5385
|
-
pagesRoot =
|
|
4381
|
+
pagesRoot = resolve12(voluteHome(), "shared", "pages");
|
|
5386
4382
|
} else {
|
|
5387
4383
|
if (!findMind(name)) return c.text("Not found", 404);
|
|
5388
|
-
pagesRoot =
|
|
4384
|
+
pagesRoot = resolve12(mindDir(name), "home", "pages");
|
|
5389
4385
|
}
|
|
5390
4386
|
const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
|
|
5391
|
-
const requestedPath =
|
|
4387
|
+
const requestedPath = resolve12(pagesRoot, wildcard.slice(1));
|
|
5392
4388
|
if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
|
|
5393
4389
|
let fileStat = await stat2(requestedPath).catch(() => null);
|
|
5394
4390
|
if (fileStat?.isDirectory()) {
|
|
5395
|
-
const indexPath =
|
|
4391
|
+
const indexPath = resolve12(requestedPath, "index.html");
|
|
5396
4392
|
fileStat = await stat2(indexPath).catch(() => null);
|
|
5397
4393
|
if (fileStat?.isFile()) {
|
|
5398
4394
|
const body = await readFile2(indexPath);
|
|
@@ -5462,9 +4458,9 @@ var app13 = new Hono13().get("/", async (c) => {
|
|
|
5462
4458
|
var prompts_default = app13;
|
|
5463
4459
|
|
|
5464
4460
|
// src/web/api/schedules.ts
|
|
5465
|
-
import { CronExpressionParser
|
|
4461
|
+
import { CronExpressionParser } from "cron-parser";
|
|
5466
4462
|
import { Hono as Hono14 } from "hono";
|
|
5467
|
-
var
|
|
4463
|
+
var slog = logger_default.child("schedules");
|
|
5468
4464
|
function readSchedules(name) {
|
|
5469
4465
|
return readVoluteConfig(mindDir(name))?.schedules ?? [];
|
|
5470
4466
|
}
|
|
@@ -5474,6 +4470,11 @@ function writeSchedules(name, schedules) {
|
|
|
5474
4470
|
config.schedules = schedules.length > 0 ? schedules : void 0;
|
|
5475
4471
|
writeVoluteConfig(dir, config);
|
|
5476
4472
|
getScheduler().loadSchedules(name);
|
|
4473
|
+
fireWebhook({
|
|
4474
|
+
event: "schedule_changed",
|
|
4475
|
+
mind: name,
|
|
4476
|
+
data: { schedules }
|
|
4477
|
+
});
|
|
5477
4478
|
}
|
|
5478
4479
|
var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
5479
4480
|
const name = c.req.param("name");
|
|
@@ -5496,7 +4497,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
|
5496
4497
|
return c.json({ error: "message and script are mutually exclusive" }, 400);
|
|
5497
4498
|
}
|
|
5498
4499
|
try {
|
|
5499
|
-
|
|
4500
|
+
CronExpressionParser.parse(body.cron);
|
|
5500
4501
|
} catch {
|
|
5501
4502
|
return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
|
|
5502
4503
|
}
|
|
@@ -5524,7 +4525,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
|
5524
4525
|
}
|
|
5525
4526
|
if (body.cron !== void 0) {
|
|
5526
4527
|
try {
|
|
5527
|
-
|
|
4528
|
+
CronExpressionParser.parse(body.cron);
|
|
5528
4529
|
} catch {
|
|
5529
4530
|
return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
|
|
5530
4531
|
}
|
|
@@ -5574,7 +4575,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
|
|
|
5574
4575
|
}
|
|
5575
4576
|
return c.json({ ok: true });
|
|
5576
4577
|
} catch (err) {
|
|
5577
|
-
|
|
4578
|
+
slog.warn(`webhook delivery failed for ${name}`, logger_default.errorData(err));
|
|
5578
4579
|
return c.json({ error: "Failed to reach mind" }, 502);
|
|
5579
4580
|
}
|
|
5580
4581
|
});
|
|
@@ -5634,9 +4635,9 @@ var app15 = new Hono15().post("/:name/shared/merge", requireAdmin, async (c) =>
|
|
|
5634
4635
|
var shared_default = app15;
|
|
5635
4636
|
|
|
5636
4637
|
// src/web/api/skills.ts
|
|
5637
|
-
import { existsSync as
|
|
4638
|
+
import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "fs";
|
|
5638
4639
|
import { tmpdir } from "os";
|
|
5639
|
-
import { join as
|
|
4640
|
+
import { join as join2, resolve as resolve13 } from "path";
|
|
5640
4641
|
import AdmZip from "adm-zip";
|
|
5641
4642
|
import { Hono as Hono16 } from "hono";
|
|
5642
4643
|
var app16 = new Hono16().get("/", async (c) => {
|
|
@@ -5646,7 +4647,7 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
5646
4647
|
const id = c.req.param("id");
|
|
5647
4648
|
const skill = await getSharedSkill(id);
|
|
5648
4649
|
if (!skill) return c.json({ error: "Skill not found" }, 404);
|
|
5649
|
-
const dir =
|
|
4650
|
+
const dir = join2(sharedSkillsDir(), id);
|
|
5650
4651
|
const files = listFilesRecursive(dir);
|
|
5651
4652
|
return c.json({ ...skill, files });
|
|
5652
4653
|
}).post("/upload", requireAdmin, async (c) => {
|
|
@@ -5658,25 +4659,25 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
5658
4659
|
if (!file.name.endsWith(".zip")) {
|
|
5659
4660
|
return c.json({ error: "Only .zip files are accepted" }, 400);
|
|
5660
4661
|
}
|
|
5661
|
-
const
|
|
5662
|
-
const tmpDir = mkdtempSync(
|
|
4662
|
+
const buffer2 = Buffer.from(await file.arrayBuffer());
|
|
4663
|
+
const tmpDir = mkdtempSync(join2(tmpdir(), "volute-skill-upload-"));
|
|
5663
4664
|
try {
|
|
5664
|
-
const zip = new AdmZip(
|
|
4665
|
+
const zip = new AdmZip(buffer2);
|
|
5665
4666
|
for (const entry of zip.getEntries()) {
|
|
5666
|
-
const target =
|
|
4667
|
+
const target = resolve13(tmpDir, entry.entryName);
|
|
5667
4668
|
if (!target.startsWith(tmpDir)) {
|
|
5668
4669
|
return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
|
|
5669
4670
|
}
|
|
5670
4671
|
}
|
|
5671
4672
|
zip.extractAllTo(tmpDir, true);
|
|
5672
4673
|
let skillDir = null;
|
|
5673
|
-
if (
|
|
4674
|
+
if (existsSync10(join2(tmpDir, "SKILL.md"))) {
|
|
5674
4675
|
skillDir = tmpDir;
|
|
5675
4676
|
} else {
|
|
5676
|
-
const entries =
|
|
4677
|
+
const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
5677
4678
|
for (const entry of entries) {
|
|
5678
|
-
if (
|
|
5679
|
-
skillDir =
|
|
4679
|
+
if (existsSync10(join2(tmpDir, entry.name, "SKILL.md"))) {
|
|
4680
|
+
skillDir = join2(tmpDir, entry.name);
|
|
5680
4681
|
break;
|
|
5681
4682
|
}
|
|
5682
4683
|
}
|
|
@@ -5692,7 +4693,7 @@ var app16 = new Hono16().get("/", async (c) => {
|
|
|
5692
4693
|
}
|
|
5693
4694
|
throw e;
|
|
5694
4695
|
} finally {
|
|
5695
|
-
|
|
4696
|
+
rmSync4(tmpDir, { recursive: true, force: true });
|
|
5696
4697
|
}
|
|
5697
4698
|
}).delete("/:id", requireAdmin, async (c) => {
|
|
5698
4699
|
const id = c.req.param("id");
|
|
@@ -5717,93 +4718,487 @@ var app17 = new Hono17().post("/restart", requireAdmin, (c) => {
|
|
|
5717
4718
|
return c.json({ ok: true });
|
|
5718
4719
|
}).get("/logs", async (c) => {
|
|
5719
4720
|
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(() => {
|
|
4721
|
+
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
4722
|
+
return streamSSE3(c, async (stream) => {
|
|
4723
|
+
for (const entry of logBuffer.getEntries()) {
|
|
4724
|
+
await stream.writeSSE({ data: JSON.stringify(entry) });
|
|
4725
|
+
}
|
|
4726
|
+
const unsubscribe = logBuffer.subscribe((entry) => {
|
|
4727
|
+
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
4728
|
+
});
|
|
4729
|
+
});
|
|
4730
|
+
await new Promise((resolve18) => {
|
|
4731
|
+
stream.onAbort(() => {
|
|
4732
|
+
unsubscribe();
|
|
4733
|
+
resolve18();
|
|
4734
|
+
});
|
|
4735
|
+
});
|
|
4736
|
+
});
|
|
4737
|
+
}).get("/info", (c) => {
|
|
4738
|
+
const config = readSystemsConfig();
|
|
4739
|
+
return c.json({ system: config?.system ?? null });
|
|
4740
|
+
});
|
|
4741
|
+
var system_default = app17;
|
|
4742
|
+
|
|
4743
|
+
// src/web/api/typing.ts
|
|
4744
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
4745
|
+
import { Hono as Hono18 } from "hono";
|
|
4746
|
+
import { z as z5 } from "zod";
|
|
4747
|
+
var typingSchema = z5.object({
|
|
4748
|
+
channel: z5.string().min(1),
|
|
4749
|
+
sender: z5.string().min(1),
|
|
4750
|
+
active: z5.boolean()
|
|
4751
|
+
});
|
|
4752
|
+
var app18 = new Hono18().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
|
|
4753
|
+
const { channel, sender, active } = c.req.valid("json");
|
|
4754
|
+
const map = getTypingMap();
|
|
4755
|
+
if (active) {
|
|
4756
|
+
map.set(channel, sender);
|
|
4757
|
+
} else {
|
|
4758
|
+
map.delete(channel, sender);
|
|
4759
|
+
}
|
|
4760
|
+
const volutePrefix = "volute:";
|
|
4761
|
+
if (channel.startsWith(volutePrefix)) {
|
|
4762
|
+
const conversationId = channel.slice(volutePrefix.length);
|
|
4763
|
+
publish(conversationId, { type: "typing", senders: map.get(channel) });
|
|
4764
|
+
}
|
|
4765
|
+
return c.json({ ok: true });
|
|
4766
|
+
}).get("/:name/typing", (c) => {
|
|
4767
|
+
const channel = c.req.query("channel");
|
|
4768
|
+
if (!channel) {
|
|
4769
|
+
return c.json({ error: "channel query param is required" }, 400);
|
|
4770
|
+
}
|
|
4771
|
+
const map = getTypingMap();
|
|
4772
|
+
return c.json({ typing: map.get(channel) });
|
|
4773
|
+
});
|
|
4774
|
+
var typing_default = app18;
|
|
4775
|
+
|
|
4776
|
+
// src/web/api/update.ts
|
|
4777
|
+
import { spawn as spawn2 } from "child_process";
|
|
4778
|
+
import { Hono as Hono19 } from "hono";
|
|
4779
|
+
var bin;
|
|
4780
|
+
var app19 = new Hono19().get("/update", async (c) => {
|
|
4781
|
+
const result = await checkForUpdate();
|
|
4782
|
+
return c.json(result);
|
|
4783
|
+
}).post("/update", requireAdmin, async (c) => {
|
|
4784
|
+
bin ??= resolveVoluteBin();
|
|
4785
|
+
const child = spawn2(bin, ["update"], {
|
|
4786
|
+
stdio: "ignore",
|
|
4787
|
+
detached: true
|
|
4788
|
+
});
|
|
4789
|
+
child.on("error", (err) => {
|
|
4790
|
+
logger_default.error("Update process error", { error: err.message });
|
|
4791
|
+
});
|
|
4792
|
+
child.unref();
|
|
4793
|
+
return c.json({ ok: true, message: "Updating..." });
|
|
4794
|
+
});
|
|
4795
|
+
var update_default = app19;
|
|
4796
|
+
|
|
4797
|
+
// src/web/api/v1/chat.ts
|
|
4798
|
+
import { zValidator as zValidator6 } from "@hono/zod-validator";
|
|
4799
|
+
import { Hono as Hono20 } from "hono";
|
|
4800
|
+
import { streamSSE as streamSSE4 } from "hono/streaming";
|
|
4801
|
+
import { z as z6 } from "zod";
|
|
4802
|
+
async function fanOutToMinds(opts) {
|
|
4803
|
+
const participants = await getParticipants(opts.conversationId);
|
|
4804
|
+
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
4805
|
+
const participantNames = participants.map((p) => p.username);
|
|
4806
|
+
const isDM = opts.isDM ?? participants.length === 2;
|
|
4807
|
+
const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
|
|
4808
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-P5OBDUKI.js");
|
|
4809
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
4810
|
+
const manager = getMindManager2();
|
|
4811
|
+
const sm = getSleepManagerIfReady();
|
|
4812
|
+
const targetMinds = mindParticipants.map((ap) => {
|
|
4813
|
+
const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
|
|
4814
|
+
if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
|
|
4815
|
+
return null;
|
|
4816
|
+
}).filter((n) => n !== null && n !== opts.senderName);
|
|
4817
|
+
function slugForMind(mindUsername) {
|
|
4818
|
+
return buildVoluteSlug({
|
|
4819
|
+
participants,
|
|
4820
|
+
mindUsername,
|
|
4821
|
+
convTitle: opts.convTitle,
|
|
4822
|
+
conversationId: opts.conversationId,
|
|
4823
|
+
...opts.slugExtra
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
const channelEntry = {
|
|
4827
|
+
platformId: opts.conversationId,
|
|
4828
|
+
platform: "volute",
|
|
4829
|
+
name: opts.convTitle ?? void 0,
|
|
4830
|
+
type: channelEntryType
|
|
4831
|
+
};
|
|
4832
|
+
for (const ap of mindParticipants) {
|
|
4833
|
+
try {
|
|
4834
|
+
writeChannelEntry(ap.username, slugForMind(ap.username), channelEntry);
|
|
4835
|
+
} catch (err) {
|
|
4836
|
+
logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
for (const mindName of targetMinds) {
|
|
4840
|
+
const target = opts.targetName ? opts.targetName(mindName) : mindName;
|
|
4841
|
+
const channel = slugForMind(mindName);
|
|
4842
|
+
const typingMap = getTypingMap();
|
|
4843
|
+
const currentlyTyping = typingMap.get(channel).filter((name) => participantNames.includes(name));
|
|
4844
|
+
deliverMessage(target, {
|
|
4845
|
+
content: opts.contentBlocks,
|
|
4846
|
+
channel,
|
|
4847
|
+
conversationId: opts.conversationId,
|
|
4848
|
+
sender: opts.senderName,
|
|
4849
|
+
participants: participantNames,
|
|
4850
|
+
participantCount: participants.length,
|
|
4851
|
+
isDM,
|
|
4852
|
+
...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
|
|
4853
|
+
}).catch((err) => {
|
|
4854
|
+
logger_default.warn("[v1-chat] delivery failed", logger_default.errorData(err));
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
var mindChatSchema = z6.object({
|
|
4859
|
+
message: z6.string().optional(),
|
|
4860
|
+
conversationId: z6.string().optional(),
|
|
4861
|
+
sender: z6.string().optional(),
|
|
4862
|
+
images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
|
|
4863
|
+
});
|
|
4864
|
+
var unifiedChatSchema = z6.object({
|
|
4865
|
+
message: z6.string().optional(),
|
|
4866
|
+
conversationId: z6.string(),
|
|
4867
|
+
images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
|
|
4868
|
+
});
|
|
4869
|
+
var app20 = new Hono20().use("*", authMiddleware).post("/minds/:name/chat", zValidator6("json", mindChatSchema), async (c) => {
|
|
4870
|
+
const name = c.req.param("name");
|
|
4871
|
+
const [baseName] = name.split("@", 2);
|
|
4872
|
+
const entry = findMind(baseName);
|
|
4873
|
+
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
4874
|
+
const body = c.req.valid("json");
|
|
4875
|
+
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
4876
|
+
return c.json({ error: "message or images required" }, 400);
|
|
4877
|
+
}
|
|
4878
|
+
const user = c.get("user");
|
|
4879
|
+
const mindUser = await getOrCreateMindUser(baseName);
|
|
4880
|
+
const senderName = user.id === 0 && body.sender ? body.sender : user.username;
|
|
4881
|
+
let conversationId = body.conversationId;
|
|
4882
|
+
if (conversationId) {
|
|
4883
|
+
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
4884
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4885
|
+
}
|
|
4886
|
+
} else {
|
|
4887
|
+
const participantIds = [];
|
|
4888
|
+
if (user.id !== 0) {
|
|
4889
|
+
participantIds.push(user.id);
|
|
4890
|
+
} else if (body.sender) {
|
|
4891
|
+
const senderMind = findMind(body.sender);
|
|
4892
|
+
if (senderMind) {
|
|
4893
|
+
const senderMindUser = await getOrCreateMindUser(body.sender);
|
|
4894
|
+
participantIds.push(senderMindUser.id);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
participantIds.push(mindUser.id);
|
|
4898
|
+
if (participantIds.length === 2) {
|
|
4899
|
+
const existing = await findDMConversation(baseName, participantIds);
|
|
4900
|
+
if (existing) conversationId = existing;
|
|
4901
|
+
}
|
|
4902
|
+
if (!conversationId) {
|
|
4903
|
+
const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
|
|
4904
|
+
const title = [...participantNames].join(", ");
|
|
4905
|
+
const conv2 = await createConversation(baseName, "volute", {
|
|
4906
|
+
userId: user.id !== 0 ? user.id : void 0,
|
|
4907
|
+
title,
|
|
4908
|
+
participantIds
|
|
4909
|
+
});
|
|
4910
|
+
conversationId = conv2.id;
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
const conv = await getConversation(conversationId);
|
|
4914
|
+
const convTitle = conv?.title ?? null;
|
|
4915
|
+
const contentBlocks = [];
|
|
4916
|
+
if (body.message) contentBlocks.push({ type: "text", text: body.message });
|
|
4917
|
+
if (body.images) {
|
|
4918
|
+
for (const img of body.images) {
|
|
4919
|
+
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
await addMessage(conversationId, "user", senderName, contentBlocks);
|
|
4923
|
+
await fanOutToMinds({
|
|
4924
|
+
conversationId,
|
|
4925
|
+
contentBlocks,
|
|
4926
|
+
senderName,
|
|
4927
|
+
convTitle,
|
|
4928
|
+
targetName: (username) => username === baseName ? name : username
|
|
4929
|
+
});
|
|
4930
|
+
return c.json({ ok: true, conversationId });
|
|
4931
|
+
}).get("/minds/:name/conversations/:id/events", async (c) => {
|
|
4932
|
+
const conversationId = c.req.param("id");
|
|
4933
|
+
const user = c.get("user");
|
|
4934
|
+
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
4935
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4936
|
+
}
|
|
4937
|
+
return streamSSE4(c, async (stream) => {
|
|
4938
|
+
const unsubscribe = subscribe2(conversationId, (event) => {
|
|
4939
|
+
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
4940
|
+
if (!stream.aborted) logger_default.error("[v1-chat] SSE write error:", logger_default.errorData(err));
|
|
5727
4941
|
});
|
|
5728
4942
|
});
|
|
5729
|
-
|
|
4943
|
+
const keepAlive = setInterval(() => {
|
|
4944
|
+
stream.writeSSE({ data: "" }).catch((err) => {
|
|
4945
|
+
if (!stream.aborted) logger_default.error("[v1-chat] SSE ping error:", logger_default.errorData(err));
|
|
4946
|
+
});
|
|
4947
|
+
}, 15e3);
|
|
4948
|
+
await new Promise((resolve18) => {
|
|
5730
4949
|
stream.onAbort(() => {
|
|
5731
4950
|
unsubscribe();
|
|
5732
|
-
|
|
4951
|
+
clearInterval(keepAlive);
|
|
4952
|
+
resolve18();
|
|
5733
4953
|
});
|
|
5734
4954
|
});
|
|
5735
4955
|
});
|
|
5736
|
-
}).
|
|
5737
|
-
const
|
|
5738
|
-
|
|
4956
|
+
}).post("/chat", zValidator6("json", unifiedChatSchema), async (c) => {
|
|
4957
|
+
const user = c.get("user");
|
|
4958
|
+
const body = c.req.valid("json");
|
|
4959
|
+
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
4960
|
+
return c.json({ error: "message or images required" }, 400);
|
|
4961
|
+
}
|
|
4962
|
+
const conv = await getConversation(body.conversationId);
|
|
4963
|
+
if (!conv) return c.json({ error: "Conversation not found" }, 404);
|
|
4964
|
+
if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
|
|
4965
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
4966
|
+
}
|
|
4967
|
+
const senderName = user.username;
|
|
4968
|
+
const contentBlocks = [];
|
|
4969
|
+
if (body.message) contentBlocks.push({ type: "text", text: body.message });
|
|
4970
|
+
if (body.images) {
|
|
4971
|
+
for (const img of body.images) {
|
|
4972
|
+
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
await addMessage(body.conversationId, "user", senderName, contentBlocks);
|
|
4976
|
+
const isDM = conv.type === "dm";
|
|
4977
|
+
await fanOutToMinds({
|
|
4978
|
+
conversationId: body.conversationId,
|
|
4979
|
+
contentBlocks,
|
|
4980
|
+
senderName,
|
|
4981
|
+
convTitle: conv.title,
|
|
4982
|
+
isDM,
|
|
4983
|
+
channelEntryType: conv.type === "channel" ? "group" : isDM ? "dm" : "group",
|
|
4984
|
+
slugExtra: { convType: conv.type, convName: conv.name }
|
|
4985
|
+
});
|
|
4986
|
+
return c.json({ ok: true, conversationId: body.conversationId });
|
|
5739
4987
|
});
|
|
5740
|
-
var
|
|
4988
|
+
var chat_default = app20;
|
|
5741
4989
|
|
|
5742
|
-
// src/web/api/
|
|
5743
|
-
import { zValidator as
|
|
5744
|
-
import { Hono as
|
|
5745
|
-
import { z as
|
|
5746
|
-
var
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
active: z5.boolean()
|
|
4990
|
+
// src/web/api/v1/conversations.ts
|
|
4991
|
+
import { zValidator as zValidator7 } from "@hono/zod-validator";
|
|
4992
|
+
import { Hono as Hono21 } from "hono";
|
|
4993
|
+
import { z as z7 } from "zod";
|
|
4994
|
+
var createSchema = z7.object({
|
|
4995
|
+
title: z7.string().optional(),
|
|
4996
|
+
participantNames: z7.array(z7.string()).min(1)
|
|
5750
4997
|
});
|
|
5751
|
-
var
|
|
5752
|
-
const
|
|
5753
|
-
const
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
4998
|
+
var app21 = new Hono21().use("*", authMiddleware).get("/", async (c) => {
|
|
4999
|
+
const user = c.get("user");
|
|
5000
|
+
const convs = await listConversationsWithParticipants(user.id);
|
|
5001
|
+
return c.json(convs);
|
|
5002
|
+
}).get("/:id/messages", async (c) => {
|
|
5003
|
+
const id = c.req.param("id");
|
|
5004
|
+
const user = c.get("user");
|
|
5005
|
+
if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
|
|
5006
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
5758
5007
|
}
|
|
5759
|
-
const
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5008
|
+
const beforeStr = c.req.query("before");
|
|
5009
|
+
const limitStr = c.req.query("limit");
|
|
5010
|
+
if (!beforeStr && !limitStr) {
|
|
5011
|
+
const msgs = await getMessages(id);
|
|
5012
|
+
return c.json({ items: msgs, hasMore: false });
|
|
5763
5013
|
}
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
return c.json({ error: "channel query param is required" }, 400);
|
|
5014
|
+
const before = beforeStr ? parseInt(beforeStr, 10) : void 0;
|
|
5015
|
+
const limit = limitStr ? parseInt(limitStr, 10) : void 0;
|
|
5016
|
+
if (before !== void 0 && isNaN(before) || limit !== void 0 && isNaN(limit)) {
|
|
5017
|
+
return c.json({ error: "Invalid cursor params: before and limit must be integers" }, 400);
|
|
5769
5018
|
}
|
|
5770
|
-
const
|
|
5771
|
-
return c.json({
|
|
5019
|
+
const result = await getMessagesPaginated(id, { before, limit });
|
|
5020
|
+
return c.json({ items: result.messages, hasMore: result.hasMore });
|
|
5021
|
+
}).get("/:id/participants", async (c) => {
|
|
5022
|
+
const id = c.req.param("id");
|
|
5023
|
+
const user = c.get("user");
|
|
5024
|
+
if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
|
|
5025
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
5026
|
+
}
|
|
5027
|
+
const participants = await getParticipants(id);
|
|
5028
|
+
return c.json(participants);
|
|
5029
|
+
}).post("/", zValidator7("json", createSchema), async (c) => {
|
|
5030
|
+
const user = c.get("user");
|
|
5031
|
+
const body = c.req.valid("json");
|
|
5032
|
+
const participantIds = /* @__PURE__ */ new Set();
|
|
5033
|
+
if (user.id !== 0) participantIds.add(user.id);
|
|
5034
|
+
let firstMindName;
|
|
5035
|
+
for (const name of body.participantNames) {
|
|
5036
|
+
const existing = await getUserByUsername(name);
|
|
5037
|
+
if (existing) {
|
|
5038
|
+
participantIds.add(existing.id);
|
|
5039
|
+
if (!firstMindName && existing.user_type === "mind") firstMindName = name;
|
|
5040
|
+
continue;
|
|
5041
|
+
}
|
|
5042
|
+
if (findMind(name)) {
|
|
5043
|
+
const au = await getOrCreateMindUser(name);
|
|
5044
|
+
participantIds.add(au.id);
|
|
5045
|
+
if (!firstMindName) firstMindName = name;
|
|
5046
|
+
continue;
|
|
5047
|
+
}
|
|
5048
|
+
return c.json({ error: `User not found: ${name}` }, 400);
|
|
5049
|
+
}
|
|
5050
|
+
if (!firstMindName) {
|
|
5051
|
+
return c.json({ error: "At least one mind participant is required" }, 400);
|
|
5052
|
+
}
|
|
5053
|
+
const conv = await createConversation(firstMindName, "volute", {
|
|
5054
|
+
userId: user.id !== 0 ? user.id : void 0,
|
|
5055
|
+
title: body.title,
|
|
5056
|
+
participantIds: [...participantIds]
|
|
5057
|
+
});
|
|
5058
|
+
return c.json(conv, 201);
|
|
5059
|
+
}).delete("/:id", async (c) => {
|
|
5060
|
+
const id = c.req.param("id");
|
|
5061
|
+
const user = c.get("user");
|
|
5062
|
+
const deleted = await deleteConversationForUser(id, user.id);
|
|
5063
|
+
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
5064
|
+
return c.json({ ok: true });
|
|
5772
5065
|
});
|
|
5773
|
-
var
|
|
5066
|
+
var conversations_default = app21;
|
|
5774
5067
|
|
|
5775
|
-
// src/web/api/
|
|
5776
|
-
import {
|
|
5777
|
-
import { Hono as
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
});
|
|
5788
|
-
|
|
5789
|
-
|
|
5068
|
+
// src/web/api/v1/events.ts
|
|
5069
|
+
import { desc as desc4 } from "drizzle-orm";
|
|
5070
|
+
import { Hono as Hono22 } from "hono";
|
|
5071
|
+
import { streamSSE as streamSSE5 } from "hono/streaming";
|
|
5072
|
+
|
|
5073
|
+
// src/lib/events/event-sequencer.ts
|
|
5074
|
+
var BUFFER_SIZE = 1e3;
|
|
5075
|
+
var MAX_AGE_MS = 5 * 60 * 1e3;
|
|
5076
|
+
var nextId = 1;
|
|
5077
|
+
var buffer = [];
|
|
5078
|
+
function bufferEvent(data) {
|
|
5079
|
+
const id = nextId++;
|
|
5080
|
+
buffer.push({ id, data, timestamp: Date.now() });
|
|
5081
|
+
while (buffer.length > BUFFER_SIZE) {
|
|
5082
|
+
buffer.shift();
|
|
5083
|
+
}
|
|
5084
|
+
return id;
|
|
5085
|
+
}
|
|
5086
|
+
function getEventsSince(sinceId) {
|
|
5087
|
+
const now = Date.now();
|
|
5088
|
+
const startIdx = buffer.findIndex((e) => e.id > sinceId);
|
|
5089
|
+
if (startIdx === -1) return [];
|
|
5090
|
+
return buffer.slice(startIdx).filter((e) => now - e.timestamp < MAX_AGE_MS);
|
|
5091
|
+
}
|
|
5092
|
+
|
|
5093
|
+
// src/web/api/v1/events.ts
|
|
5094
|
+
var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
|
|
5095
|
+
const user = c.get("user");
|
|
5096
|
+
const since = c.req.query("since");
|
|
5097
|
+
const sinceId = since ? Number(since) : 0;
|
|
5098
|
+
return streamSSE5(c, async (stream) => {
|
|
5099
|
+
const cleanups = [];
|
|
5100
|
+
try {
|
|
5101
|
+
if (sinceId > 0) {
|
|
5102
|
+
const missed = getEventsSince(sinceId);
|
|
5103
|
+
for (const event of missed) {
|
|
5104
|
+
await stream.writeSSE({
|
|
5105
|
+
id: String(event.id),
|
|
5106
|
+
data: JSON.stringify(event.data)
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
let recentActivity = [];
|
|
5111
|
+
try {
|
|
5112
|
+
const db = await getDb();
|
|
5113
|
+
recentActivity = await db.select().from(activity).orderBy(desc4(activity.created_at)).limit(50);
|
|
5114
|
+
recentActivity = recentActivity.map((row) => ({
|
|
5115
|
+
...row,
|
|
5116
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
5117
|
+
}));
|
|
5118
|
+
} catch (err) {
|
|
5119
|
+
logger_default.error("[v1-events] failed to fetch recent activity", logger_default.errorData(err));
|
|
5120
|
+
}
|
|
5121
|
+
let conversations2 = [];
|
|
5122
|
+
try {
|
|
5123
|
+
conversations2 = await listConversationsWithParticipants(user.id);
|
|
5124
|
+
} catch (err) {
|
|
5125
|
+
logger_default.error("[v1-events] failed to fetch conversations", logger_default.errorData(err));
|
|
5126
|
+
}
|
|
5127
|
+
const sites = getCachedSites();
|
|
5128
|
+
const recentPages = getCachedRecentPages();
|
|
5129
|
+
const snapshotData = {
|
|
5130
|
+
event: "snapshot",
|
|
5131
|
+
activity: recentActivity,
|
|
5132
|
+
conversations: conversations2,
|
|
5133
|
+
sites,
|
|
5134
|
+
recentPages,
|
|
5135
|
+
activeMinds: getActiveMinds()
|
|
5136
|
+
};
|
|
5137
|
+
const snapshotId = bufferEvent(snapshotData);
|
|
5138
|
+
await stream.writeSSE({
|
|
5139
|
+
id: String(snapshotId),
|
|
5140
|
+
data: JSON.stringify(snapshotData)
|
|
5141
|
+
});
|
|
5142
|
+
const unsubActivity = subscribe((event) => {
|
|
5143
|
+
const data = {
|
|
5144
|
+
event: "activity",
|
|
5145
|
+
...event,
|
|
5146
|
+
metadata: event.metadata ?? null
|
|
5147
|
+
};
|
|
5148
|
+
const eventId = bufferEvent(data);
|
|
5149
|
+
stream.writeSSE({
|
|
5150
|
+
id: String(eventId),
|
|
5151
|
+
data: JSON.stringify(data)
|
|
5152
|
+
}).catch((err) => {
|
|
5153
|
+
if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
|
|
5154
|
+
});
|
|
5155
|
+
});
|
|
5156
|
+
cleanups.push(unsubActivity);
|
|
5157
|
+
for (const conv of conversations2) {
|
|
5158
|
+
const unsubConv = subscribe2(conv.id, (event) => {
|
|
5159
|
+
const data = { event: "conversation", conversationId: conv.id, ...event };
|
|
5160
|
+
const eventId = bufferEvent(data);
|
|
5161
|
+
stream.writeSSE({
|
|
5162
|
+
id: String(eventId),
|
|
5163
|
+
data: JSON.stringify(data)
|
|
5164
|
+
}).catch((err) => {
|
|
5165
|
+
if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
|
|
5166
|
+
});
|
|
5167
|
+
});
|
|
5168
|
+
cleanups.push(unsubConv);
|
|
5169
|
+
}
|
|
5170
|
+
const keepAlive = setInterval(() => {
|
|
5171
|
+
stream.writeSSE({ data: "" }).catch((err) => {
|
|
5172
|
+
if (!stream.aborted) logger_default.error("[v1-events] ping error:", logger_default.errorData(err));
|
|
5173
|
+
});
|
|
5174
|
+
}, 15e3);
|
|
5175
|
+
cleanups.push(() => clearInterval(keepAlive));
|
|
5176
|
+
await new Promise((resolve18) => {
|
|
5177
|
+
stream.onAbort(() => resolve18());
|
|
5178
|
+
});
|
|
5179
|
+
} finally {
|
|
5180
|
+
for (const cleanup of cleanups) {
|
|
5181
|
+
try {
|
|
5182
|
+
cleanup();
|
|
5183
|
+
} catch {
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5790
5187
|
});
|
|
5791
|
-
child.unref();
|
|
5792
|
-
return c.json({ ok: true, message: "Updating..." });
|
|
5793
5188
|
});
|
|
5794
|
-
var
|
|
5189
|
+
var events_default = app22;
|
|
5795
5190
|
|
|
5796
5191
|
// src/web/api/variants.ts
|
|
5797
|
-
import { existsSync as
|
|
5798
|
-
import { resolve as
|
|
5799
|
-
import { Hono as
|
|
5192
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
5193
|
+
import { resolve as resolve15 } from "path";
|
|
5194
|
+
import { Hono as Hono23 } from "hono";
|
|
5800
5195
|
|
|
5801
5196
|
// src/lib/spawn-server.ts
|
|
5802
|
-
import { spawn as
|
|
5803
|
-
import { closeSync, mkdirSync as
|
|
5804
|
-
import { resolve as
|
|
5197
|
+
import { spawn as spawn3 } from "child_process";
|
|
5198
|
+
import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync9 } from "fs";
|
|
5199
|
+
import { resolve as resolve14 } from "path";
|
|
5805
5200
|
function tsxBin(cwd) {
|
|
5806
|
-
return
|
|
5201
|
+
return resolve14(cwd, "node_modules", ".bin", "tsx");
|
|
5807
5202
|
}
|
|
5808
5203
|
function spawnServer(cwd, port, options) {
|
|
5809
5204
|
if (options?.detached) {
|
|
@@ -5812,37 +5207,37 @@ function spawnServer(cwd, port, options) {
|
|
|
5812
5207
|
return spawnAttached(cwd, port);
|
|
5813
5208
|
}
|
|
5814
5209
|
function spawnAttached(cwd, port) {
|
|
5815
|
-
const child =
|
|
5210
|
+
const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
5816
5211
|
cwd,
|
|
5817
5212
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5818
5213
|
});
|
|
5819
|
-
return new Promise((
|
|
5820
|
-
const timeout = setTimeout(() =>
|
|
5214
|
+
return new Promise((resolve18) => {
|
|
5215
|
+
const timeout = setTimeout(() => resolve18(null), 3e4);
|
|
5821
5216
|
function checkOutput(data) {
|
|
5822
5217
|
const match = data.toString().match(/listening on :(\d+)/);
|
|
5823
5218
|
if (match) {
|
|
5824
5219
|
clearTimeout(timeout);
|
|
5825
|
-
|
|
5220
|
+
resolve18({ child, actualPort: parseInt(match[1], 10) });
|
|
5826
5221
|
}
|
|
5827
5222
|
}
|
|
5828
5223
|
child.stdout?.on("data", checkOutput);
|
|
5829
5224
|
child.stderr?.on("data", checkOutput);
|
|
5830
5225
|
child.on("error", () => {
|
|
5831
5226
|
clearTimeout(timeout);
|
|
5832
|
-
|
|
5227
|
+
resolve18(null);
|
|
5833
5228
|
});
|
|
5834
5229
|
child.on("exit", () => {
|
|
5835
5230
|
clearTimeout(timeout);
|
|
5836
|
-
|
|
5231
|
+
resolve18(null);
|
|
5837
5232
|
});
|
|
5838
5233
|
});
|
|
5839
5234
|
}
|
|
5840
5235
|
function spawnDetached(cwd, port, logDir) {
|
|
5841
|
-
const logsDir = logDir ??
|
|
5842
|
-
|
|
5843
|
-
const logPath =
|
|
5236
|
+
const logsDir = logDir ?? resolve14(cwd, ".mind", "logs");
|
|
5237
|
+
mkdirSync6(logsDir, { recursive: true });
|
|
5238
|
+
const logPath = resolve14(logsDir, "mind.log");
|
|
5844
5239
|
const logFd = openSync(logPath, "a");
|
|
5845
|
-
const child =
|
|
5240
|
+
const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
|
|
5846
5241
|
cwd,
|
|
5847
5242
|
stdio: ["ignore", logFd, logFd],
|
|
5848
5243
|
detached: true
|
|
@@ -5860,7 +5255,7 @@ function spawnDetached(cwd, port, logDir) {
|
|
|
5860
5255
|
}
|
|
5861
5256
|
const interval = setInterval(() => {
|
|
5862
5257
|
try {
|
|
5863
|
-
const content =
|
|
5258
|
+
const content = readFileSync9(logPath, "utf-8");
|
|
5864
5259
|
const match = content.match(/listening on :(\d+)/);
|
|
5865
5260
|
if (match) {
|
|
5866
5261
|
finish({ child, actualPort: parseInt(match[1], 10) });
|
|
@@ -5912,7 +5307,7 @@ async function verify2(port) {
|
|
|
5912
5307
|
}
|
|
5913
5308
|
|
|
5914
5309
|
// src/web/api/variants.ts
|
|
5915
|
-
var
|
|
5310
|
+
var app23 = new Hono23().get("/:name/variants", async (c) => {
|
|
5916
5311
|
const name = c.req.param("name");
|
|
5917
5312
|
const entry = findMind(name);
|
|
5918
5313
|
if (!entry) return c.json({ error: "Mind not found" }, 404);
|
|
@@ -5952,11 +5347,11 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5952
5347
|
const err = validateBranchName(variantName);
|
|
5953
5348
|
if (err) return c.json({ error: err }, 400);
|
|
5954
5349
|
const projectRoot = mindDir(mindName);
|
|
5955
|
-
const variantDir =
|
|
5956
|
-
if (
|
|
5350
|
+
const variantDir = resolve15(projectRoot, ".variants", variantName);
|
|
5351
|
+
if (existsSync11(variantDir)) {
|
|
5957
5352
|
return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
|
|
5958
5353
|
}
|
|
5959
|
-
|
|
5354
|
+
mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
|
|
5960
5355
|
try {
|
|
5961
5356
|
await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
|
|
5962
5357
|
} catch (e) {
|
|
@@ -5969,7 +5364,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5969
5364
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
5970
5365
|
await exec(cmd, args, {
|
|
5971
5366
|
cwd: variantDir,
|
|
5972
|
-
env: { ...process.env, HOME:
|
|
5367
|
+
env: { ...process.env, HOME: resolve15(variantDir, "home") }
|
|
5973
5368
|
});
|
|
5974
5369
|
} else {
|
|
5975
5370
|
await exec("npm", ["install"], { cwd: variantDir });
|
|
@@ -5979,7 +5374,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
5979
5374
|
return c.json({ error: `npm install failed: ${msg}` }, 500);
|
|
5980
5375
|
}
|
|
5981
5376
|
if (body.soul) {
|
|
5982
|
-
|
|
5377
|
+
writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
|
|
5983
5378
|
}
|
|
5984
5379
|
const variantPort = body.port ?? nextPort();
|
|
5985
5380
|
const variant = {
|
|
@@ -6017,7 +5412,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6017
5412
|
} catch {
|
|
6018
5413
|
}
|
|
6019
5414
|
const projectRoot = mindDir(mindName);
|
|
6020
|
-
if (
|
|
5415
|
+
if (existsSync11(variant.path)) {
|
|
6021
5416
|
const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
|
|
6022
5417
|
if (status) {
|
|
6023
5418
|
try {
|
|
@@ -6074,17 +5469,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6074
5469
|
} catch (e) {
|
|
6075
5470
|
return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
|
|
6076
5471
|
}
|
|
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);
|
|
5472
|
+
await cleanupVariant(mindName, variantName, projectRoot, variant.path);
|
|
6088
5473
|
if (variantName === "upgrade") {
|
|
6089
5474
|
try {
|
|
6090
5475
|
const { computeTemplateHash: computeTemplateHash2 } = await import("./template-hash-BIMA4ILT.js");
|
|
@@ -6095,13 +5480,12 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6095
5480
|
console.error(`[daemon] failed to update template hash for ${mindName}:`, err);
|
|
6096
5481
|
}
|
|
6097
5482
|
}
|
|
6098
|
-
chownMindDir(projectRoot, mindName);
|
|
6099
5483
|
try {
|
|
6100
5484
|
if (isIsolationEnabled()) {
|
|
6101
5485
|
const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
|
|
6102
5486
|
await exec(cmd, args, {
|
|
6103
5487
|
cwd: projectRoot,
|
|
6104
|
-
env: { ...process.env, HOME:
|
|
5488
|
+
env: { ...process.env, HOME: resolve15(projectRoot, "home") }
|
|
6105
5489
|
});
|
|
6106
5490
|
} else {
|
|
6107
5491
|
await exec("npm", ["install"], { cwd: projectRoot });
|
|
@@ -6136,41 +5520,22 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
|
|
|
6136
5520
|
const variant = findVariant(mindName, variantName);
|
|
6137
5521
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
6138
5522
|
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);
|
|
5523
|
+
await cleanupVariant(mindName, variantName, projectRoot, variant.path, { stop: true });
|
|
6159
5524
|
return c.json({ ok: true });
|
|
6160
5525
|
});
|
|
6161
|
-
var variants_default =
|
|
5526
|
+
var variants_default = app23;
|
|
6162
5527
|
|
|
6163
5528
|
// src/web/api/volute/channels.ts
|
|
6164
|
-
import { zValidator as
|
|
6165
|
-
import { Hono as
|
|
6166
|
-
import { z as
|
|
6167
|
-
var
|
|
6168
|
-
name:
|
|
5529
|
+
import { zValidator as zValidator8 } from "@hono/zod-validator";
|
|
5530
|
+
import { Hono as Hono24 } from "hono";
|
|
5531
|
+
import { z as z8 } from "zod";
|
|
5532
|
+
var createSchema2 = z8.object({
|
|
5533
|
+
name: z8.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
|
|
6169
5534
|
});
|
|
6170
|
-
var inviteSchema =
|
|
6171
|
-
username:
|
|
5535
|
+
var inviteSchema = z8.object({
|
|
5536
|
+
username: z8.string().min(1)
|
|
6172
5537
|
});
|
|
6173
|
-
var
|
|
5538
|
+
var app24 = new Hono24().get("/", async (c) => {
|
|
6174
5539
|
const user = c.get("user");
|
|
6175
5540
|
const channels = await listChannels();
|
|
6176
5541
|
const results = await Promise.all(
|
|
@@ -6181,7 +5546,7 @@ var app21 = new Hono21().get("/", async (c) => {
|
|
|
6181
5546
|
})
|
|
6182
5547
|
);
|
|
6183
5548
|
return c.json(results);
|
|
6184
|
-
}).post("/",
|
|
5549
|
+
}).post("/", zValidator8("json", createSchema2), async (c) => {
|
|
6185
5550
|
const user = c.get("user");
|
|
6186
5551
|
const body = c.req.valid("json");
|
|
6187
5552
|
try {
|
|
@@ -6214,7 +5579,7 @@ var app21 = new Hono21().get("/", async (c) => {
|
|
|
6214
5579
|
if (!ch) return c.json({ error: "Channel not found" }, 404);
|
|
6215
5580
|
const participants = await getParticipants(ch.id);
|
|
6216
5581
|
return c.json(participants);
|
|
6217
|
-
}).post("/:name/invite",
|
|
5582
|
+
}).post("/:name/invite", zValidator8("json", inviteSchema), async (c) => {
|
|
6218
5583
|
const name = c.req.param("name");
|
|
6219
5584
|
const inviter = c.get("user");
|
|
6220
5585
|
const { username } = c.req.valid("json");
|
|
@@ -6234,24 +5599,27 @@ var app21 = new Hono21().get("/", async (c) => {
|
|
|
6234
5599
|
]);
|
|
6235
5600
|
return c.json({ ok: true });
|
|
6236
5601
|
});
|
|
6237
|
-
var channels_default2 =
|
|
5602
|
+
var channels_default2 = app24;
|
|
6238
5603
|
|
|
6239
5604
|
// 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
|
|
5605
|
+
import { zValidator as zValidator9 } from "@hono/zod-validator";
|
|
5606
|
+
import { Hono as Hono25 } from "hono";
|
|
5607
|
+
import { streamSSE as streamSSE6 } from "hono/streaming";
|
|
5608
|
+
import { z as z9 } from "zod";
|
|
5609
|
+
async function fanOutToMinds2(opts) {
|
|
6245
5610
|
const participants = await getParticipants(opts.conversationId);
|
|
6246
5611
|
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
6247
5612
|
const participantNames = participants.map((p) => p.username);
|
|
6248
5613
|
const isDM = opts.isDM ?? participants.length === 2;
|
|
6249
5614
|
const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
|
|
6250
|
-
const { getMindManager: getMindManager2 } = await import("./mind-manager-
|
|
5615
|
+
const { getMindManager: getMindManager2 } = await import("./mind-manager-P5OBDUKI.js");
|
|
5616
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
|
|
6251
5617
|
const manager = getMindManager2();
|
|
6252
|
-
const
|
|
5618
|
+
const sm = getSleepManagerIfReady();
|
|
5619
|
+
const targetMinds = mindParticipants.map((ap) => {
|
|
6253
5620
|
const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
|
|
6254
|
-
|
|
5621
|
+
if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
|
|
5622
|
+
return null;
|
|
6255
5623
|
}).filter((n) => n !== null && n !== opts.senderName);
|
|
6256
5624
|
function slugForMind(mindUsername) {
|
|
6257
5625
|
return buildVoluteSlug({
|
|
@@ -6275,7 +5643,7 @@ async function fanOutToMinds(opts) {
|
|
|
6275
5643
|
logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
|
|
6276
5644
|
}
|
|
6277
5645
|
}
|
|
6278
|
-
for (const mindName of
|
|
5646
|
+
for (const mindName of targetMinds) {
|
|
6279
5647
|
const target = opts.targetName ? opts.targetName(mindName) : mindName;
|
|
6280
5648
|
const channel = slugForMind(mindName);
|
|
6281
5649
|
const typingMap = getTypingMap();
|
|
@@ -6293,18 +5661,18 @@ async function fanOutToMinds(opts) {
|
|
|
6293
5661
|
});
|
|
6294
5662
|
}
|
|
6295
5663
|
}
|
|
6296
|
-
var chatSchema =
|
|
6297
|
-
message:
|
|
6298
|
-
conversationId:
|
|
6299
|
-
sender:
|
|
6300
|
-
images:
|
|
6301
|
-
|
|
6302
|
-
media_type:
|
|
6303
|
-
data:
|
|
5664
|
+
var chatSchema = z9.object({
|
|
5665
|
+
message: z9.string().optional(),
|
|
5666
|
+
conversationId: z9.string().optional(),
|
|
5667
|
+
sender: z9.string().optional(),
|
|
5668
|
+
images: z9.array(
|
|
5669
|
+
z9.object({
|
|
5670
|
+
media_type: z9.string(),
|
|
5671
|
+
data: z9.string()
|
|
6304
5672
|
})
|
|
6305
5673
|
).optional()
|
|
6306
5674
|
});
|
|
6307
|
-
var
|
|
5675
|
+
var app25 = new Hono25().post("/:name/chat", zValidator9("json", chatSchema), async (c) => {
|
|
6308
5676
|
const name = c.req.param("name");
|
|
6309
5677
|
const [baseName] = name.split("@", 2);
|
|
6310
5678
|
const entry = findMind(baseName);
|
|
@@ -6362,7 +5730,7 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
6362
5730
|
}
|
|
6363
5731
|
}
|
|
6364
5732
|
await addMessage(conversationId, "user", senderName, contentBlocks);
|
|
6365
|
-
await
|
|
5733
|
+
await fanOutToMinds2({
|
|
6366
5734
|
conversationId,
|
|
6367
5735
|
contentBlocks,
|
|
6368
5736
|
senderName,
|
|
@@ -6376,8 +5744,8 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
6376
5744
|
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
6377
5745
|
return c.json({ error: "Conversation not found" }, 404);
|
|
6378
5746
|
}
|
|
6379
|
-
return
|
|
6380
|
-
const unsubscribe =
|
|
5747
|
+
return streamSSE6(c, async (stream) => {
|
|
5748
|
+
const unsubscribe = subscribe2(conversationId, (event) => {
|
|
6381
5749
|
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
6382
5750
|
if (!stream.aborted) console.error("[chat] SSE write error:", err);
|
|
6383
5751
|
});
|
|
@@ -6387,23 +5755,23 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
|
|
|
6387
5755
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
6388
5756
|
});
|
|
6389
5757
|
}, 15e3);
|
|
6390
|
-
await new Promise((
|
|
5758
|
+
await new Promise((resolve18) => {
|
|
6391
5759
|
stream.onAbort(() => {
|
|
6392
5760
|
unsubscribe();
|
|
6393
5761
|
clearInterval(keepAlive);
|
|
6394
|
-
|
|
5762
|
+
resolve18();
|
|
6395
5763
|
});
|
|
6396
5764
|
});
|
|
6397
5765
|
});
|
|
6398
5766
|
});
|
|
6399
|
-
var
|
|
6400
|
-
message:
|
|
6401
|
-
conversationId:
|
|
6402
|
-
images:
|
|
5767
|
+
var unifiedChatSchema2 = z9.object({
|
|
5768
|
+
message: z9.string().optional(),
|
|
5769
|
+
conversationId: z9.string(),
|
|
5770
|
+
images: z9.array(z9.object({ media_type: z9.string(), data: z9.string() })).optional()
|
|
6403
5771
|
});
|
|
6404
|
-
var unifiedChatApp = new
|
|
5772
|
+
var unifiedChatApp = new Hono25().post(
|
|
6405
5773
|
"/chat",
|
|
6406
|
-
|
|
5774
|
+
zValidator9("json", unifiedChatSchema2),
|
|
6407
5775
|
async (c) => {
|
|
6408
5776
|
const user = c.get("user");
|
|
6409
5777
|
const body = c.req.valid("json");
|
|
@@ -6425,7 +5793,7 @@ var unifiedChatApp = new Hono22().post(
|
|
|
6425
5793
|
}
|
|
6426
5794
|
await addMessage(body.conversationId, "user", senderName, contentBlocks);
|
|
6427
5795
|
const isDM = conv.type === "dm";
|
|
6428
|
-
await
|
|
5796
|
+
await fanOutToMinds2({
|
|
6429
5797
|
conversationId: body.conversationId,
|
|
6430
5798
|
contentBlocks,
|
|
6431
5799
|
senderName,
|
|
@@ -6437,18 +5805,18 @@ var unifiedChatApp = new Hono22().post(
|
|
|
6437
5805
|
return c.json({ ok: true, conversationId: body.conversationId });
|
|
6438
5806
|
}
|
|
6439
5807
|
);
|
|
6440
|
-
var
|
|
5808
|
+
var chat_default2 = app25;
|
|
6441
5809
|
|
|
6442
5810
|
// 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:
|
|
5811
|
+
import { zValidator as zValidator10 } from "@hono/zod-validator";
|
|
5812
|
+
import { Hono as Hono26 } from "hono";
|
|
5813
|
+
import { z as z10 } from "zod";
|
|
5814
|
+
var createConvSchema = z10.object({
|
|
5815
|
+
title: z10.string().optional(),
|
|
5816
|
+
participantIds: z10.array(z10.number()).optional(),
|
|
5817
|
+
participantNames: z10.array(z10.string()).optional()
|
|
6450
5818
|
});
|
|
6451
|
-
var
|
|
5819
|
+
var app26 = new Hono26().get("/:name/conversations", async (c) => {
|
|
6452
5820
|
const name = c.req.param("name");
|
|
6453
5821
|
const user = c.get("user");
|
|
6454
5822
|
let lookupId = user.id;
|
|
@@ -6459,7 +5827,7 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
|
|
|
6459
5827
|
const all = await listConversationsForUser(lookupId);
|
|
6460
5828
|
const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
|
|
6461
5829
|
return c.json(convs);
|
|
6462
|
-
}).post("/:name/conversations",
|
|
5830
|
+
}).post("/:name/conversations", zValidator10("json", createConvSchema), async (c) => {
|
|
6463
5831
|
const name = c.req.param("name");
|
|
6464
5832
|
const user = c.get("user");
|
|
6465
5833
|
const body = c.req.valid("json");
|
|
@@ -6533,18 +5901,18 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
|
|
|
6533
5901
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
6534
5902
|
return c.json({ ok: true });
|
|
6535
5903
|
});
|
|
6536
|
-
var
|
|
5904
|
+
var conversations_default2 = app26;
|
|
6537
5905
|
|
|
6538
5906
|
// 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:
|
|
5907
|
+
import { zValidator as zValidator11 } from "@hono/zod-validator";
|
|
5908
|
+
import { Hono as Hono27 } from "hono";
|
|
5909
|
+
import { streamSSE as streamSSE7 } from "hono/streaming";
|
|
5910
|
+
import { z as z11 } from "zod";
|
|
5911
|
+
var createSchema3 = z11.object({
|
|
5912
|
+
title: z11.string().optional(),
|
|
5913
|
+
participantNames: z11.array(z11.string()).min(1)
|
|
6546
5914
|
});
|
|
6547
|
-
var
|
|
5915
|
+
var app27 = new Hono27().use("*", authMiddleware).get("/", async (c) => {
|
|
6548
5916
|
const user = c.get("user");
|
|
6549
5917
|
const convs = await listConversationsWithParticipants(user.id);
|
|
6550
5918
|
return c.json(convs);
|
|
@@ -6556,7 +5924,7 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6556
5924
|
}
|
|
6557
5925
|
const msgs = await getMessages(id);
|
|
6558
5926
|
return c.json(msgs);
|
|
6559
|
-
}).post("/",
|
|
5927
|
+
}).post("/", zValidator11("json", createSchema3), async (c) => {
|
|
6560
5928
|
const user = c.get("user");
|
|
6561
5929
|
const body = c.req.valid("json");
|
|
6562
5930
|
const participantIds = /* @__PURE__ */ new Set();
|
|
@@ -6592,8 +5960,8 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6592
5960
|
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
6593
5961
|
return c.json({ error: "Conversation not found" }, 404);
|
|
6594
5962
|
}
|
|
6595
|
-
return
|
|
6596
|
-
const unsubscribe =
|
|
5963
|
+
return streamSSE7(c, async (stream) => {
|
|
5964
|
+
const unsubscribe = subscribe2(conversationId, (event) => {
|
|
6597
5965
|
stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
|
|
6598
5966
|
if (!stream.aborted) console.error("[chat] SSE write error:", err);
|
|
6599
5967
|
});
|
|
@@ -6603,11 +5971,11 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6603
5971
|
if (!stream.aborted) console.error("[chat] SSE ping error:", err);
|
|
6604
5972
|
});
|
|
6605
5973
|
}, 15e3);
|
|
6606
|
-
await new Promise((
|
|
5974
|
+
await new Promise((resolve18) => {
|
|
6607
5975
|
stream.onAbort(() => {
|
|
6608
5976
|
unsubscribe();
|
|
6609
5977
|
clearInterval(keepAlive);
|
|
6610
|
-
|
|
5978
|
+
resolve18();
|
|
6611
5979
|
});
|
|
6612
5980
|
});
|
|
6613
5981
|
});
|
|
@@ -6618,12 +5986,12 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
|
|
|
6618
5986
|
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
6619
5987
|
return c.json({ ok: true });
|
|
6620
5988
|
});
|
|
6621
|
-
var user_conversations_default =
|
|
5989
|
+
var user_conversations_default = app27;
|
|
6622
5990
|
|
|
6623
5991
|
// src/web/app.ts
|
|
6624
5992
|
var httpLog = logger_default.child("http");
|
|
6625
|
-
var
|
|
6626
|
-
|
|
5993
|
+
var app28 = new Hono28();
|
|
5994
|
+
app28.onError((err, c) => {
|
|
6627
5995
|
if (err instanceof HTTPException) {
|
|
6628
5996
|
return err.getResponse();
|
|
6629
5997
|
}
|
|
@@ -6634,10 +6002,10 @@ app25.onError((err, c) => {
|
|
|
6634
6002
|
});
|
|
6635
6003
|
return c.json({ error: "Internal server error" }, 500);
|
|
6636
6004
|
});
|
|
6637
|
-
|
|
6005
|
+
app28.notFound((c) => {
|
|
6638
6006
|
return c.json({ error: "Not found" }, 404);
|
|
6639
6007
|
});
|
|
6640
|
-
|
|
6008
|
+
app28.use("*", async (c, next) => {
|
|
6641
6009
|
const start = Date.now();
|
|
6642
6010
|
await next();
|
|
6643
6011
|
const duration = Date.now() - start;
|
|
@@ -6648,7 +6016,7 @@ app25.use("*", async (c, next) => {
|
|
|
6648
6016
|
httpLog.debug("request", data);
|
|
6649
6017
|
}
|
|
6650
6018
|
});
|
|
6651
|
-
|
|
6019
|
+
app28.get("/api/health", (c) => {
|
|
6652
6020
|
let version = "unknown";
|
|
6653
6021
|
let cached = null;
|
|
6654
6022
|
try {
|
|
@@ -6663,19 +6031,35 @@ app25.get("/api/health", (c) => {
|
|
|
6663
6031
|
...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
|
|
6664
6032
|
});
|
|
6665
6033
|
});
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
var
|
|
6034
|
+
app28.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
6035
|
+
app28.use("/api/*", csrf());
|
|
6036
|
+
app28.use("/api/activity/*", authMiddleware);
|
|
6037
|
+
app28.use("/api/minds/*", authMiddleware);
|
|
6038
|
+
app28.use("/api/conversations/*", authMiddleware);
|
|
6039
|
+
app28.use("/api/volute/*", authMiddleware);
|
|
6040
|
+
app28.use("/api/system/*", authMiddleware);
|
|
6041
|
+
app28.use("/api/env/*", authMiddleware);
|
|
6042
|
+
app28.use("/api/prompts/*", authMiddleware);
|
|
6043
|
+
app28.use("/api/skills/*", authMiddleware);
|
|
6044
|
+
app28.use("/api/v1/*", authMiddleware);
|
|
6045
|
+
app28.route("/pages", pages_default);
|
|
6046
|
+
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);
|
|
6047
|
+
app28.route("/api/v1/minds", minds_default);
|
|
6048
|
+
app28.route("/api/v1/minds", typing_default);
|
|
6049
|
+
app28.route("/api/v1/minds", variants_default);
|
|
6050
|
+
app28.route("/api/v1/minds", files_default);
|
|
6051
|
+
app28.route("/api/v1/minds", env_default);
|
|
6052
|
+
app28.route("/api/v1/minds", mind_skills_default);
|
|
6053
|
+
app28.route("/api/v1/minds", connectors_default);
|
|
6054
|
+
app28.route("/api/v1/minds", schedules_default);
|
|
6055
|
+
app28.route("/api/v1/minds", logs_default);
|
|
6056
|
+
app28.route("/api/v1/system", system_default);
|
|
6057
|
+
app28.route("/api/v1/system", update_default);
|
|
6058
|
+
app28.route("/api/v1/prompts", prompts_default);
|
|
6059
|
+
app28.route("/api/v1/skills", skills_default);
|
|
6060
|
+
app28.route("/api/v1/env", sharedEnvApp);
|
|
6061
|
+
app28.route("/api/v1/channels", channels_default2);
|
|
6062
|
+
var app_default = app28;
|
|
6679
6063
|
|
|
6680
6064
|
// src/web/server.ts
|
|
6681
6065
|
var MIME_TYPES2 = {
|
|
@@ -6692,20 +6076,20 @@ async function startServer({
|
|
|
6692
6076
|
hostname = "127.0.0.1"
|
|
6693
6077
|
}) {
|
|
6694
6078
|
let assetsDir = "";
|
|
6695
|
-
let searchDir =
|
|
6079
|
+
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
6696
6080
|
for (let i = 0; i < 5; i++) {
|
|
6697
|
-
const candidate =
|
|
6698
|
-
if (
|
|
6081
|
+
const candidate = resolve16(searchDir, "dist", "web-assets");
|
|
6082
|
+
if (existsSync12(candidate)) {
|
|
6699
6083
|
assetsDir = candidate;
|
|
6700
6084
|
break;
|
|
6701
6085
|
}
|
|
6702
|
-
searchDir =
|
|
6086
|
+
searchDir = dirname(searchDir);
|
|
6703
6087
|
}
|
|
6704
6088
|
if (assetsDir) {
|
|
6705
6089
|
app_default.get("*", async (c) => {
|
|
6706
6090
|
const urlPath = new URL(c.req.url).pathname;
|
|
6707
6091
|
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
6708
|
-
const filePath =
|
|
6092
|
+
const filePath = resolve16(assetsDir, urlPath.slice(1));
|
|
6709
6093
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
6710
6094
|
const s = await stat3(filePath).catch(() => null);
|
|
6711
6095
|
if (s?.isFile()) {
|
|
@@ -6714,7 +6098,7 @@ async function startServer({
|
|
|
6714
6098
|
const body = await readFile3(filePath);
|
|
6715
6099
|
return c.body(body, 200, { "Content-Type": mime });
|
|
6716
6100
|
}
|
|
6717
|
-
const indexPath =
|
|
6101
|
+
const indexPath = resolve16(assetsDir, "index.html");
|
|
6718
6102
|
const indexStat = await stat3(indexPath).catch(() => null);
|
|
6719
6103
|
if (indexStat?.isFile()) {
|
|
6720
6104
|
const body = await readFile3(indexPath, "utf-8");
|
|
@@ -6724,10 +6108,10 @@ async function startServer({
|
|
|
6724
6108
|
});
|
|
6725
6109
|
}
|
|
6726
6110
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
6727
|
-
await new Promise((
|
|
6111
|
+
await new Promise((resolve18, reject) => {
|
|
6728
6112
|
server.on("listening", () => {
|
|
6729
6113
|
logger_default.info("Volute UI running", { hostname, port });
|
|
6730
|
-
|
|
6114
|
+
resolve18();
|
|
6731
6115
|
});
|
|
6732
6116
|
server.on("error", (err) => {
|
|
6733
6117
|
reject(err);
|
|
@@ -6738,14 +6122,17 @@ async function startServer({
|
|
|
6738
6122
|
|
|
6739
6123
|
// src/daemon.ts
|
|
6740
6124
|
if (!process.env.VOLUTE_HOME) {
|
|
6741
|
-
process.env.VOLUTE_HOME =
|
|
6125
|
+
process.env.VOLUTE_HOME = resolve17(homedir2(), ".volute");
|
|
6126
|
+
}
|
|
6127
|
+
if (process.env.VOLUTE_TIMEZONE && !process.env.TZ) {
|
|
6128
|
+
process.env.TZ = process.env.VOLUTE_TIMEZONE;
|
|
6742
6129
|
}
|
|
6743
6130
|
async function startDaemon(opts) {
|
|
6744
6131
|
const { port, hostname } = opts;
|
|
6745
6132
|
const myPid = String(process.pid);
|
|
6746
6133
|
const home = voluteHome();
|
|
6747
6134
|
if (!opts.foreground) {
|
|
6748
|
-
const rotatingLog = new RotatingLog(
|
|
6135
|
+
const rotatingLog = new RotatingLog(resolve17(home, "daemon.log"));
|
|
6749
6136
|
logger_default.setOutput((line) => rotatingLog.write(`${line}
|
|
6750
6137
|
`));
|
|
6751
6138
|
const write = (...args) => rotatingLog.write(`${format(...args)}
|
|
@@ -6755,9 +6142,9 @@ async function startDaemon(opts) {
|
|
|
6755
6142
|
console.warn = write;
|
|
6756
6143
|
console.info = write;
|
|
6757
6144
|
}
|
|
6758
|
-
const DAEMON_PID_PATH =
|
|
6759
|
-
const DAEMON_JSON_PATH =
|
|
6760
|
-
|
|
6145
|
+
const DAEMON_PID_PATH = resolve17(home, "daemon.pid");
|
|
6146
|
+
const DAEMON_JSON_PATH = resolve17(home, "daemon.json");
|
|
6147
|
+
mkdirSync8(home, { recursive: true });
|
|
6761
6148
|
migrateAgentsToMinds();
|
|
6762
6149
|
try {
|
|
6763
6150
|
await ensureSharedRepo();
|
|
@@ -6785,8 +6172,8 @@ async function startDaemon(opts) {
|
|
|
6785
6172
|
}
|
|
6786
6173
|
throw err;
|
|
6787
6174
|
}
|
|
6788
|
-
|
|
6789
|
-
|
|
6175
|
+
writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
|
|
6176
|
+
writeFileSync8(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
6790
6177
|
`, {
|
|
6791
6178
|
mode: 420
|
|
6792
6179
|
});
|
|
@@ -6800,6 +6187,9 @@ async function startDaemon(opts) {
|
|
|
6800
6187
|
mailPoller.start();
|
|
6801
6188
|
const tokenBudget = initTokenBudget();
|
|
6802
6189
|
tokenBudget.start();
|
|
6190
|
+
const sleepManager = initSleepManager();
|
|
6191
|
+
sleepManager.start();
|
|
6192
|
+
const unsubscribeWebhook = initWebhook();
|
|
6803
6193
|
const registry = readRegistry();
|
|
6804
6194
|
for (const entry of registry) {
|
|
6805
6195
|
try {
|
|
@@ -6815,6 +6205,20 @@ async function startDaemon(opts) {
|
|
|
6815
6205
|
const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
|
|
6816
6206
|
while (queue.length > 0) {
|
|
6817
6207
|
const entry = queue.shift();
|
|
6208
|
+
if (sleepManager.isSleeping(entry.name)) {
|
|
6209
|
+
try {
|
|
6210
|
+
const dir = mindDir(entry.name);
|
|
6211
|
+
const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
|
|
6212
|
+
await connectors.startConnectors(entry.name, dir, entry.port, daemonPort);
|
|
6213
|
+
scheduler.loadSchedules(entry.name);
|
|
6214
|
+
} catch (err) {
|
|
6215
|
+
logger_default.error(
|
|
6216
|
+
`failed to start connectors for sleeping mind ${entry.name}`,
|
|
6217
|
+
logger_default.errorData(err)
|
|
6218
|
+
);
|
|
6219
|
+
}
|
|
6220
|
+
continue;
|
|
6221
|
+
}
|
|
6818
6222
|
try {
|
|
6819
6223
|
await startMindFull(entry.name);
|
|
6820
6224
|
} catch (err) {
|
|
@@ -6842,8 +6246,15 @@ async function startDaemon(opts) {
|
|
|
6842
6246
|
});
|
|
6843
6247
|
await Promise.all(workers);
|
|
6844
6248
|
}
|
|
6249
|
+
import("./cloud-sync-C6WRYRVR.js").then(
|
|
6250
|
+
({ consumeQueuedMessages }) => consumeQueuedMessages().catch((err) => {
|
|
6251
|
+
logger_default.warn("failed to consume queued cloud messages", logger_default.errorData(err));
|
|
6252
|
+
})
|
|
6253
|
+
).catch((err) => {
|
|
6254
|
+
logger_default.warn("failed to load cloud-sync module", logger_default.errorData(err));
|
|
6255
|
+
});
|
|
6845
6256
|
try {
|
|
6846
|
-
const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-
|
|
6257
|
+
const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-5FGUAVSF.js");
|
|
6847
6258
|
backfillTemplateHashes();
|
|
6848
6259
|
notifyVersionUpdate().catch((err) => {
|
|
6849
6260
|
logger_default.warn("failed to send version update notifications", logger_default.errorData(err));
|
|
@@ -6860,15 +6271,15 @@ async function startDaemon(opts) {
|
|
|
6860
6271
|
logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
|
|
6861
6272
|
function cleanup() {
|
|
6862
6273
|
try {
|
|
6863
|
-
if (
|
|
6864
|
-
|
|
6274
|
+
if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
6275
|
+
unlinkSync(DAEMON_PID_PATH);
|
|
6865
6276
|
}
|
|
6866
6277
|
} catch {
|
|
6867
6278
|
}
|
|
6868
6279
|
try {
|
|
6869
|
-
const data = JSON.parse(
|
|
6280
|
+
const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
|
|
6870
6281
|
if (data.token === token) {
|
|
6871
|
-
|
|
6282
|
+
unlinkSync(DAEMON_JSON_PATH);
|
|
6872
6283
|
}
|
|
6873
6284
|
} catch {
|
|
6874
6285
|
}
|
|
@@ -6890,6 +6301,9 @@ async function startDaemon(opts) {
|
|
|
6890
6301
|
try {
|
|
6891
6302
|
safe("stopAllWatchers", stopAllWatchers);
|
|
6892
6303
|
safe("stopAllActivityTrackers", stopAll);
|
|
6304
|
+
safe("unsubscribeWebhook", unsubscribeWebhook);
|
|
6305
|
+
safe("sleepManager.stop", () => sleepManager.stop());
|
|
6306
|
+
safe("sleepManager.saveState", () => sleepManager.saveState());
|
|
6893
6307
|
safe("scheduler.stop", () => scheduler.stop());
|
|
6894
6308
|
safe("scheduler.saveState", () => scheduler.saveState());
|
|
6895
6309
|
safe("mailPoller.stop", () => mailPoller.stop());
|