volute 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -10
- package/dist/accept-666DIZX2.js +41 -0
- package/dist/api.d.ts +342 -143
- package/dist/{chat-MHJ3L6JQ.js → chat-KTPOR2JT.js} +18 -8
- package/dist/chunk-A6TUJJ3L.js +19 -0
- package/dist/{chunk-OQZH4PBB.js → chunk-CMILSHZD.js} +199 -277
- package/dist/{chunk-K5NAC55T.js → chunk-CQ7SNKNI.js} +1 -1
- package/dist/{chunk-PHSAT7YL.js → chunk-EHZKEMMV.js} +5 -5
- package/dist/{chunk-IAYBDWVG.js → chunk-FLZGS4QH.js} +145 -0
- package/dist/{chunk-USUXRNVD.js → chunk-J4IBNXGJ.js} +0 -2
- package/dist/chunk-MD4C26II.js +128 -0
- package/dist/{chunk-4WXYUOAK.js → chunk-NI5FFCCS.js} +8 -1
- package/dist/{chunk-JKOWNZ4P.js → chunk-P72MVS4R.js} +1 -40
- package/dist/chunk-THUUIU3E.js +232 -0
- package/dist/cli.js +21 -30
- package/dist/clock-DGCBVGYA.js +259 -0
- package/dist/{cloud-sync-T7M3ESC3.js → cloud-sync-KILFGV5Q.js} +7 -7
- package/dist/connectors/discord-bridge.js +1 -1
- package/dist/connectors/slack-bridge.js +1 -1
- package/dist/connectors/telegram-bridge.js +1 -1
- package/dist/{conversations-M2K4253F.js → conversations-P5BL7RMX.js} +7 -1
- package/dist/create-DFCAGEE5.js +70 -0
- package/dist/{daemon-restart-M2QTYMEG.js → daemon-restart-UHOMICXT.js} +1 -1
- package/dist/daemon.js +715 -661
- package/dist/files-M546TKVN.js +46 -0
- package/dist/{login-XX37I52P.js → login-BKP3AFWN.js} +7 -17
- package/dist/logout-IQK7FNEK.js +20 -0
- package/dist/{message-delivery-LDXLGERA.js → message-delivery-Q7VUMIEI.js} +11 -9
- package/dist/{mind-DI33C74K.js → mind-S5V6CK5W.js} +8 -13
- package/dist/{mind-activity-tracker-EN6XNXPF.js → mind-activity-tracker-WRHFI3YW.js} +1 -1
- package/dist/mind-list-UPJ75GPI.js +29 -0
- package/dist/{mind-manager-M6EMUW5I.js → mind-manager-P66HQDNE.js} +2 -2
- package/dist/mind-status-TK5AETEM.js +55 -0
- package/dist/{package-7WY6VKU3.js → package-OFKXNKJF.js} +1 -1
- package/dist/{pages-6EBS6CBR.js → pages-EUJR52AH.js} +5 -5
- package/dist/pages-watcher-P7QECRE2.js +21 -0
- package/dist/{publish-66UB2ZFY.js → publish-ZZB33WP4.js} +6 -17
- package/dist/{register-6B2CXTYM.js → register-CHREOMJ3.js} +5 -24
- package/dist/reject-LXIZFJ4Q.js +39 -0
- package/dist/{sandbox-TGBX22DS.js → sandbox-5BW5HPXM.js} +1 -1
- package/dist/{send-ZNCJDSRP.js → send-TAOEZ4NH.js} +64 -6
- package/dist/skills/dreaming/references/INSTALL.md +3 -17
- package/dist/skills/shared-files/SKILL.md +44 -0
- package/dist/skills/shared-files/scripts/merge.ts +72 -0
- package/dist/skills/shared-files/scripts/pull.ts +52 -0
- package/dist/skills/volute-mind/SKILL.md +48 -22
- package/dist/{sleep-manager-MWYHM5HV.js → sleep-manager-G4B5GW5P.js} +7 -7
- package/dist/{sprout-IJVVKSJ2.js → sprout-UNT7LKKE.js} +1 -1
- package/dist/{status-77YEPHMW.js → status-NQJYR4BG.js} +45 -1
- package/dist/{status-THLOBLWG.js → status-S7UUPNRW.js} +3 -13
- package/dist/systems-SMEFSHTA.js +60 -0
- package/dist/{up-NKSMXBWR.js → up-W6VAK2XE.js} +1 -1
- package/dist/{version-notify-5Z4MNR6M.js → version-notify-WDHRO3XD.js} +11 -11
- package/dist/web-assets/assets/index-BmKDnWDB.css +1 -0
- package/dist/web-assets/assets/index-CLJMx-GA.js +71 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +1 -1
- package/templates/_base/src/lib/logger.ts +10 -53
- package/templates/_base/src/lib/router.ts +1 -9
- package/templates/claude/src/lib/stream-consumer.ts +1 -4
- package/templates/pi/src/lib/event-handler.ts +1 -14
- package/dist/auth-D3OT2ARB.js +0 -37
- package/dist/chunk-KDGS53OS.js +0 -50
- package/dist/chunk-RWKVSSLY.js +0 -26
- package/dist/chunk-T6HKBWXZ.js +0 -23
- package/dist/create-D7J73A6H.js +0 -45
- package/dist/file-CR36YUPD.js +0 -204
- package/dist/log-ABYNVYJ3.js +0 -39
- package/dist/logout-W4KOOBIT.js +0 -18
- package/dist/logs-U35JR2KE.js +0 -77
- package/dist/merge-LNSMSAOF.js +0 -46
- package/dist/pull-XCHJTM5M.js +0 -39
- package/dist/schedule-QTJMFATP.js +0 -154
- package/dist/service-6LIN3F3K.js +0 -122
- package/dist/shared-ML5I4Q2A.js +0 -39
- package/dist/status-7GA4SM4Y.js +0 -35
- package/dist/web-assets/assets/index-CI5wgghI.css +0 -1
- package/dist/web-assets/assets/index-is5CvJWH.js +0 -75
- package/dist/{chunk-GIE6CSN5.js → chunk-DUAUMCEE.js} +0 -0
- package/dist/{history-XKRTAFS2.js → history-ALPTNB3I.js} +0 -0
- package/dist/{setup-JG4QAEBV.js → setup-RXYVGGT7.js} +3 -3
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./chunk-YUIHSKR6.js";
|
|
8
8
|
|
|
9
9
|
// src/lib/events/mind-activity-tracker.ts
|
|
10
|
-
var IDLE_TIMEOUT_MS =
|
|
10
|
+
var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
11
11
|
var minds = /* @__PURE__ */ new Map();
|
|
12
12
|
function getState(mind) {
|
|
13
13
|
let state = minds.get(mind);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
isSandboxEnabled,
|
|
4
4
|
wrapForSandbox
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-J4IBNXGJ.js";
|
|
6
6
|
import {
|
|
7
7
|
loadMergedEnv
|
|
8
8
|
} from "./chunk-2WPW7OT6.js";
|
|
@@ -172,9 +172,9 @@ To reject, delete \${filePath}`,
|
|
|
172
172
|
category: "mind"
|
|
173
173
|
},
|
|
174
174
|
pre_sleep: {
|
|
175
|
-
content: "Time to rest. You have this turn to wind down however feels right \u2014 reflect on your day, update your journal or memory, finish any threads of thought, or simply settle.\n\nYour current session will be archived and a fresh one will begin when you wake. Anything in session context that isn't saved to files will be lost.\n\nYou'll wake at ${wakeTime}.
|
|
175
|
+
content: "Time to rest. You have this turn to wind down however feels right \u2014 reflect on your day, update your journal or memory, finish any threads of thought, or simply settle.\n\nYour current session will be archived and a fresh one will begin when you wake. Anything in session context that isn't saved to files will be lost.\n\nYou'll wake at ${wakeTime}.",
|
|
176
176
|
description: "Pre-sleep message sent before stopping the mind",
|
|
177
|
-
variables: ["wakeTime"
|
|
177
|
+
variables: ["wakeTime"],
|
|
178
178
|
category: "system"
|
|
179
179
|
},
|
|
180
180
|
wake_summary: {
|
|
@@ -568,7 +568,7 @@ var MindManager = class {
|
|
|
568
568
|
if (this.shuttingDown || this.stopping.has(name)) return;
|
|
569
569
|
mlog.error(`mind ${name} exited with code ${code}`);
|
|
570
570
|
try {
|
|
571
|
-
const { getSleepManagerIfReady } = await import("./sleep-manager-
|
|
571
|
+
const { getSleepManagerIfReady } = await import("./sleep-manager-G4B5GW5P.js");
|
|
572
572
|
const sleepState = getSleepManagerIfReady()?.getState(name);
|
|
573
573
|
if (sleepState?.sleeping) {
|
|
574
574
|
mlog.info(`${name} is sleeping \u2014 skipping crash recovery`);
|
|
@@ -577,7 +577,7 @@ var MindManager = class {
|
|
|
577
577
|
} catch (err) {
|
|
578
578
|
mlog.warn(`failed to check sleep state for ${name}`, logger_default.errorData(err));
|
|
579
579
|
}
|
|
580
|
-
import("./mind-activity-tracker-
|
|
580
|
+
import("./mind-activity-tracker-WRHFI3YW.js").then(({ markIdle }) => markIdle(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
|
|
581
581
|
import("./activity-events-BBIEA2F4.js").then(
|
|
582
582
|
({ publish }) => publish({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
|
|
583
583
|
).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
|
|
@@ -109,6 +109,53 @@ function publish(conversationId, event) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// src/lib/events/conversations.ts
|
|
112
|
+
async function migrateGroupDMsToChannels() {
|
|
113
|
+
const db = await getDb();
|
|
114
|
+
await db.transaction(async (tx) => {
|
|
115
|
+
const overloadedDMs = await tx.select({
|
|
116
|
+
conversationId: conversationParticipants.conversation_id,
|
|
117
|
+
count: sql`COUNT(*)`
|
|
118
|
+
}).from(conversationParticipants).innerJoin(conversations, eq(conversationParticipants.conversation_id, conversations.id)).where(eq(conversations.type, "dm")).groupBy(conversationParticipants.conversation_id).having(sql`COUNT(*) > 2`);
|
|
119
|
+
const dmIds = overloadedDMs.map((r) => r.conversationId);
|
|
120
|
+
await tx.update(conversations).set({
|
|
121
|
+
type: "channel",
|
|
122
|
+
name: sql`COALESCE(${conversations.name}, ${conversations.title})`
|
|
123
|
+
}).where(eq(conversations.type, "group"));
|
|
124
|
+
if (dmIds.length > 0) {
|
|
125
|
+
await tx.update(conversations).set({
|
|
126
|
+
type: "channel",
|
|
127
|
+
name: sql`COALESCE(${conversations.name}, ${conversations.title})`
|
|
128
|
+
}).where(inArray(conversations.id, dmIds));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
await migrateChannelEntryTypes();
|
|
132
|
+
}
|
|
133
|
+
async function migrateChannelEntryTypes() {
|
|
134
|
+
const { existsSync, readdirSync, readFileSync, writeFileSync } = await import("fs");
|
|
135
|
+
const { join } = await import("path");
|
|
136
|
+
const home = join((await import("os")).homedir(), ".volute", "state");
|
|
137
|
+
if (!existsSync(home)) return;
|
|
138
|
+
for (const name of readdirSync(home)) {
|
|
139
|
+
const filePath = join(home, name, "channels.json");
|
|
140
|
+
if (!existsSync(filePath)) continue;
|
|
141
|
+
try {
|
|
142
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
143
|
+
const map = JSON.parse(raw);
|
|
144
|
+
let changed = false;
|
|
145
|
+
for (const entry of Object.values(map)) {
|
|
146
|
+
if (entry.type === "group") {
|
|
147
|
+
entry.type = "channel";
|
|
148
|
+
changed = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (changed) {
|
|
152
|
+
writeFileSync(filePath, `${JSON.stringify(map, null, 2)}
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
112
159
|
async function createConversation(mindName, channel, opts) {
|
|
113
160
|
const db = await getDb();
|
|
114
161
|
const id = randomUUID();
|
|
@@ -380,6 +427,101 @@ async function findDMConversation(mindName, participantIds) {
|
|
|
380
427
|
}
|
|
381
428
|
return null;
|
|
382
429
|
}
|
|
430
|
+
async function listConversationsForMind(mindName) {
|
|
431
|
+
const db = await getDb();
|
|
432
|
+
const byName = await db.select().from(conversations).where(eq(conversations.mind_name, mindName)).orderBy(desc(conversations.updated_at)).all();
|
|
433
|
+
const mindUser = await db.select({ id: users.id }).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
434
|
+
const byNameIds = new Set(byName.map((c) => c.id));
|
|
435
|
+
let byParticipation = [];
|
|
436
|
+
if (mindUser) {
|
|
437
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq(conversationParticipants.user_id, mindUser.id)).all();
|
|
438
|
+
const extraIds = participantRows.map((r) => r.conversation_id).filter((id) => !byNameIds.has(id));
|
|
439
|
+
if (extraIds.length > 0) {
|
|
440
|
+
byParticipation = await db.select().from(conversations).where(inArray(conversations.id, extraIds)).orderBy(desc(conversations.updated_at)).all();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const convs = [...byName, ...byParticipation].sort(
|
|
444
|
+
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
|
445
|
+
);
|
|
446
|
+
if (convs.length === 0) return [];
|
|
447
|
+
const convIds = convs.map((c) => c.id);
|
|
448
|
+
const rows = await db.select({
|
|
449
|
+
conversationId: conversationParticipants.conversation_id,
|
|
450
|
+
userId: users.id,
|
|
451
|
+
username: users.username,
|
|
452
|
+
userType: users.user_type,
|
|
453
|
+
role: conversationParticipants.role,
|
|
454
|
+
displayName: users.display_name,
|
|
455
|
+
description: users.description,
|
|
456
|
+
avatar: users.avatar
|
|
457
|
+
}).from(conversationParticipants).innerJoin(users, eq(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
458
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const r of rows) {
|
|
460
|
+
let arr = byConv.get(r.conversationId);
|
|
461
|
+
if (!arr) {
|
|
462
|
+
arr = [];
|
|
463
|
+
byConv.set(r.conversationId, arr);
|
|
464
|
+
}
|
|
465
|
+
arr.push({
|
|
466
|
+
userId: r.userId,
|
|
467
|
+
username: r.username,
|
|
468
|
+
userType: r.userType,
|
|
469
|
+
role: r.role,
|
|
470
|
+
displayName: r.displayName,
|
|
471
|
+
description: r.description,
|
|
472
|
+
avatar: r.avatar
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const lastMsgIds = await db.select({
|
|
476
|
+
conversationId: messages.conversation_id,
|
|
477
|
+
maxId: sql`MAX(${messages.id})`
|
|
478
|
+
}).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
479
|
+
const byLastMsg = /* @__PURE__ */ new Map();
|
|
480
|
+
if (lastMsgIds.length > 0) {
|
|
481
|
+
const msgRows = await db.select().from(messages).where(
|
|
482
|
+
inArray(
|
|
483
|
+
messages.id,
|
|
484
|
+
lastMsgIds.map((r) => r.maxId)
|
|
485
|
+
)
|
|
486
|
+
);
|
|
487
|
+
for (const m of msgRows) {
|
|
488
|
+
let text = "";
|
|
489
|
+
try {
|
|
490
|
+
const parsed = JSON.parse(m.content);
|
|
491
|
+
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
492
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
493
|
+
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
494
|
+
} catch {
|
|
495
|
+
text = m.content;
|
|
496
|
+
}
|
|
497
|
+
byLastMsg.set(m.conversation_id, {
|
|
498
|
+
role: m.role,
|
|
499
|
+
senderName: m.sender_name,
|
|
500
|
+
text,
|
|
501
|
+
createdAt: m.created_at
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return convs.map((c) => ({
|
|
506
|
+
...c,
|
|
507
|
+
participants: byConv.get(c.id) ?? [],
|
|
508
|
+
lastMessage: byLastMsg.get(c.id)
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
async function isConversationForMind(mindName, conversationId) {
|
|
512
|
+
const db = await getDb();
|
|
513
|
+
const byName = await db.select({ id: conversations.id }).from(conversations).where(and(eq(conversations.id, conversationId), eq(conversations.mind_name, mindName))).get();
|
|
514
|
+
if (byName) return true;
|
|
515
|
+
const mindUser = await db.select({ id: users.id }).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
516
|
+
if (!mindUser) return false;
|
|
517
|
+
const participant = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(
|
|
518
|
+
and(
|
|
519
|
+
eq(conversationParticipants.conversation_id, conversationId),
|
|
520
|
+
eq(conversationParticipants.user_id, mindUser.id)
|
|
521
|
+
)
|
|
522
|
+
).get();
|
|
523
|
+
return !!participant;
|
|
524
|
+
}
|
|
383
525
|
async function deleteConversation(id) {
|
|
384
526
|
const db = await getDb();
|
|
385
527
|
await db.delete(conversations).where(eq(conversations.id, id));
|
|
@@ -451,6 +593,7 @@ export {
|
|
|
451
593
|
initWebhook,
|
|
452
594
|
subscribe2 as subscribe,
|
|
453
595
|
publish,
|
|
596
|
+
migrateGroupDMsToChannels,
|
|
454
597
|
createConversation,
|
|
455
598
|
getOrCreateConversation,
|
|
456
599
|
getConversation,
|
|
@@ -466,6 +609,8 @@ export {
|
|
|
466
609
|
getMessagesPaginated,
|
|
467
610
|
listConversationsWithParticipants,
|
|
468
611
|
findDMConversation,
|
|
612
|
+
listConversationsForMind,
|
|
613
|
+
isConversationForMind,
|
|
469
614
|
deleteConversation,
|
|
470
615
|
createChannel,
|
|
471
616
|
getChannelByName,
|
|
@@ -55,8 +55,6 @@ async function buildDenyRead(mindName, mindDir) {
|
|
|
55
55
|
const userVoluteHome = voluteUserHome();
|
|
56
56
|
if (userVoluteHome !== home) {
|
|
57
57
|
deny.push(userVoluteHome);
|
|
58
|
-
} else {
|
|
59
|
-
deny.push(resolve(home, "systems.json"));
|
|
60
58
|
}
|
|
61
59
|
try {
|
|
62
60
|
const entries = await readRegistry();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
stateDir
|
|
4
|
+
} from "./chunk-H7OZRFJB.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/file-sharing.ts
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
9
|
+
import { basename, join, normalize, resolve } from "path";
|
|
10
|
+
function validateFilePath(filePath) {
|
|
11
|
+
if (!filePath) return "File path is required";
|
|
12
|
+
const normalized = normalize(filePath);
|
|
13
|
+
if (normalized.startsWith("/") || normalized.startsWith("\\")) {
|
|
14
|
+
return "Absolute paths are not allowed";
|
|
15
|
+
}
|
|
16
|
+
if (normalized.includes("..")) {
|
|
17
|
+
return "Path traversal (..) is not allowed";
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function pendingDir(receiver) {
|
|
22
|
+
return resolve(stateDir(receiver), "pending-files");
|
|
23
|
+
}
|
|
24
|
+
function validateId(id) {
|
|
25
|
+
if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
|
|
26
|
+
throw new Error("Invalid pending file id");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function generateId(sender) {
|
|
30
|
+
const ts = Date.now();
|
|
31
|
+
const rand = randomBytes(2).toString("hex");
|
|
32
|
+
return `${sender}-${ts}-${rand}`;
|
|
33
|
+
}
|
|
34
|
+
function stageFile(receiver, sender, filename, content, originalPath) {
|
|
35
|
+
const err = validateFilePath(filename);
|
|
36
|
+
if (err) throw new Error(err);
|
|
37
|
+
if (sender.includes("/") || sender.includes("\\")) {
|
|
38
|
+
throw new Error("Invalid sender name");
|
|
39
|
+
}
|
|
40
|
+
const id = generateId(sender);
|
|
41
|
+
const dir = resolve(pendingDir(receiver), id);
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
const metadata = {
|
|
44
|
+
id,
|
|
45
|
+
sender,
|
|
46
|
+
filename: basename(filename),
|
|
47
|
+
originalPath,
|
|
48
|
+
size: content.length,
|
|
49
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
50
|
+
};
|
|
51
|
+
writeFileSync(resolve(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
|
|
52
|
+
`);
|
|
53
|
+
writeFileSync(resolve(dir, "data"), content);
|
|
54
|
+
return { id };
|
|
55
|
+
}
|
|
56
|
+
function listPending(receiver) {
|
|
57
|
+
const dir = pendingDir(receiver);
|
|
58
|
+
if (!existsSync(dir)) return [];
|
|
59
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
60
|
+
const result = [];
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.isDirectory()) continue;
|
|
63
|
+
const metaPath = resolve(dir, entry.name, "metadata.json");
|
|
64
|
+
if (!existsSync(metaPath)) continue;
|
|
65
|
+
try {
|
|
66
|
+
result.push(JSON.parse(readFileSync(metaPath, "utf-8")));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
72
|
+
}
|
|
73
|
+
function getPending(receiver, id) {
|
|
74
|
+
validateId(id);
|
|
75
|
+
const metaPath = resolve(pendingDir(receiver), id, "metadata.json");
|
|
76
|
+
if (!existsSync(metaPath)) return null;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function deliverFile(receiverDir, sender, filename, content, inboxPath) {
|
|
85
|
+
const err = validateFilePath(filename);
|
|
86
|
+
if (err) throw new Error(err);
|
|
87
|
+
const inbox = inboxPath ?? "inbox";
|
|
88
|
+
const inboxErr = validateFilePath(inbox);
|
|
89
|
+
if (inboxErr) throw new Error(`Invalid inboxPath: ${inboxErr}`);
|
|
90
|
+
if (sender.includes("/") || sender.includes("\\")) {
|
|
91
|
+
throw new Error("Invalid sender name");
|
|
92
|
+
}
|
|
93
|
+
const destDir = resolve(receiverDir, "home", inbox, sender);
|
|
94
|
+
mkdirSync(destDir, { recursive: true });
|
|
95
|
+
const destPath = resolve(destDir, basename(filename));
|
|
96
|
+
writeFileSync(destPath, content);
|
|
97
|
+
return join(inbox, sender, basename(filename));
|
|
98
|
+
}
|
|
99
|
+
function acceptPending(receiver, id, receiverDir, dest) {
|
|
100
|
+
const meta = getPending(receiver, id);
|
|
101
|
+
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
102
|
+
const dataPath = resolve(pendingDir(receiver), id, "data");
|
|
103
|
+
const content = readFileSync(dataPath);
|
|
104
|
+
const inboxPath = dest ?? "inbox";
|
|
105
|
+
const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
|
|
106
|
+
rmSync(resolve(pendingDir(receiver), id), { recursive: true });
|
|
107
|
+
return { sender: meta.sender, filename: meta.filename, destPath };
|
|
108
|
+
}
|
|
109
|
+
function rejectPending(receiver, id) {
|
|
110
|
+
const meta = getPending(receiver, id);
|
|
111
|
+
if (!meta) throw new Error(`Pending file not found: ${id}`);
|
|
112
|
+
rmSync(resolve(pendingDir(receiver), id), { recursive: true });
|
|
113
|
+
return { sender: meta.sender, filename: meta.filename };
|
|
114
|
+
}
|
|
115
|
+
function formatFileSize(bytes) {
|
|
116
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
117
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
118
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
validateFilePath,
|
|
123
|
+
stageFile,
|
|
124
|
+
listPending,
|
|
125
|
+
acceptPending,
|
|
126
|
+
rejectPending,
|
|
127
|
+
formatFileSize
|
|
128
|
+
};
|
|
@@ -28,7 +28,14 @@ import { basename, dirname, join, resolve } from "path";
|
|
|
28
28
|
import { eq, sql } from "drizzle-orm";
|
|
29
29
|
var VALID_SKILL_ID = /^[a-zA-Z0-9_-]+$/;
|
|
30
30
|
var SEED_SKILLS = ["orientation", "memory"];
|
|
31
|
-
var STANDARD_SKILLS = [
|
|
31
|
+
var STANDARD_SKILLS = [
|
|
32
|
+
"volute-mind",
|
|
33
|
+
"memory",
|
|
34
|
+
"sessions",
|
|
35
|
+
"notes",
|
|
36
|
+
"dreaming",
|
|
37
|
+
"shared-files"
|
|
38
|
+
];
|
|
32
39
|
function validateSkillId(id) {
|
|
33
40
|
if (!id || !VALID_SKILL_ID.test(id)) {
|
|
34
41
|
throw new Error(`Invalid skill ID: ${id}`);
|
|
@@ -178,50 +178,11 @@ async function sharedMerge(mindName, mindDir, message) {
|
|
|
178
178
|
return { ok: true };
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
|
-
async function sharedPull(mindName, mindDir) {
|
|
182
|
-
return withSharedLock(async () => {
|
|
183
|
-
const worktreePath = resolve(mindDir, "home", "shared");
|
|
184
|
-
const status = (await gitExec(["status", "--porcelain"], { cwd: worktreePath })).trim();
|
|
185
|
-
if (status) {
|
|
186
|
-
await gitExec(["add", "-A"], { cwd: worktreePath });
|
|
187
|
-
await gitExec(
|
|
188
|
-
["commit", "--author", `${mindName} <${mindName}@volute>`, "-m", `wip: ${mindName}`],
|
|
189
|
-
{ cwd: worktreePath }
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
try {
|
|
193
|
-
await gitExec(["rebase", "main"], { cwd: worktreePath });
|
|
194
|
-
rechownWorktree(worktreePath, mindName);
|
|
195
|
-
return { ok: true };
|
|
196
|
-
} catch {
|
|
197
|
-
try {
|
|
198
|
-
await gitExec(["rebase", "--abort"], { cwd: worktreePath });
|
|
199
|
-
} catch {
|
|
200
|
-
return {
|
|
201
|
-
ok: false,
|
|
202
|
-
message: "Rebase failed and abort failed \u2014 shared worktree may need manual repair"
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
return { ok: false, message: "Rebase failed \u2014 conflicts with main" };
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
async function sharedLog(limit = 20) {
|
|
210
|
-
const dir = sharedDir();
|
|
211
|
-
return gitExec(["log", "--oneline", "-n", String(limit), "main"], { cwd: dir });
|
|
212
|
-
}
|
|
213
|
-
async function sharedStatus(mindName) {
|
|
214
|
-
const dir = sharedDir();
|
|
215
|
-
return gitExec(["diff", `main...${mindName}`, "--stat"], { cwd: dir });
|
|
216
|
-
}
|
|
217
181
|
|
|
218
182
|
export {
|
|
219
183
|
sharedDir,
|
|
220
184
|
ensureSharedRepo,
|
|
221
185
|
addSharedWorktree,
|
|
222
186
|
removeSharedWorktree,
|
|
223
|
-
sharedMerge
|
|
224
|
-
sharedPull,
|
|
225
|
-
sharedLog,
|
|
226
|
-
sharedStatus
|
|
187
|
+
sharedMerge
|
|
227
188
|
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
publish
|
|
4
|
+
} from "./chunk-VIVMW2H2.js";
|
|
5
|
+
import {
|
|
6
|
+
logger_default
|
|
7
|
+
} from "./chunk-YUIHSKR6.js";
|
|
8
|
+
import {
|
|
9
|
+
mindDir,
|
|
10
|
+
readRegistry,
|
|
11
|
+
voluteHome
|
|
12
|
+
} from "./chunk-H7OZRFJB.js";
|
|
13
|
+
|
|
14
|
+
// src/lib/pages-watcher.ts
|
|
15
|
+
import { existsSync, readdirSync, statSync, watch } from "fs";
|
|
16
|
+
import { join, resolve } from "path";
|
|
17
|
+
var watchers = /* @__PURE__ */ new Map();
|
|
18
|
+
var homeWatchers = /* @__PURE__ */ new Map();
|
|
19
|
+
var debounceTimers = /* @__PURE__ */ new Map();
|
|
20
|
+
var sitesCache = null;
|
|
21
|
+
var recentPagesCache = null;
|
|
22
|
+
function startPagesWatcher(mindName, pagesDir) {
|
|
23
|
+
try {
|
|
24
|
+
const watcher = watch(pagesDir, { recursive: true }, (_eventType, filename) => {
|
|
25
|
+
if (!filename || !filename.endsWith(".html")) return;
|
|
26
|
+
const key = `${mindName}:${filename}`;
|
|
27
|
+
const existing = debounceTimers.get(key);
|
|
28
|
+
if (existing) clearTimeout(existing);
|
|
29
|
+
debounceTimers.set(
|
|
30
|
+
key,
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
debounceTimers.delete(key);
|
|
33
|
+
invalidateCache();
|
|
34
|
+
publish({
|
|
35
|
+
type: "page_updated",
|
|
36
|
+
mind: mindName,
|
|
37
|
+
summary: `${mindName} updated ${filename}`,
|
|
38
|
+
metadata: { file: filename }
|
|
39
|
+
}).catch(
|
|
40
|
+
(err) => logger_default.error("failed to publish page_updated activity", logger_default.errorData(err))
|
|
41
|
+
);
|
|
42
|
+
}, 100)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
watchers.set(mindName, watcher);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
logger_default.warn(`failed to start pages watcher for ${mindName}`, logger_default.errorData(err));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function startSystemWatcher() {
|
|
51
|
+
if (watchers.has("_system")) return;
|
|
52
|
+
const systemPagesDir = resolve(voluteHome(), "shared", "pages");
|
|
53
|
+
if (!existsSync(systemPagesDir)) return;
|
|
54
|
+
startPagesWatcher("_system", systemPagesDir);
|
|
55
|
+
}
|
|
56
|
+
function startWatcher(mindName) {
|
|
57
|
+
if (watchers.has(mindName)) return;
|
|
58
|
+
const pagesDir = resolve(mindDir(mindName), "home", "public", "pages");
|
|
59
|
+
if (existsSync(pagesDir)) {
|
|
60
|
+
startPagesWatcher(mindName, pagesDir);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (homeWatchers.has(mindName)) return;
|
|
64
|
+
const publicDir = resolve(mindDir(mindName), "home", "public");
|
|
65
|
+
if (!existsSync(publicDir)) return;
|
|
66
|
+
try {
|
|
67
|
+
const hw = watch(publicDir, (_eventType, filename) => {
|
|
68
|
+
if (filename !== "pages") return;
|
|
69
|
+
if (!existsSync(pagesDir)) return;
|
|
70
|
+
hw.close();
|
|
71
|
+
homeWatchers.delete(mindName);
|
|
72
|
+
invalidateCache();
|
|
73
|
+
startPagesWatcher(mindName, pagesDir);
|
|
74
|
+
});
|
|
75
|
+
homeWatchers.set(mindName, hw);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger_default.warn(`failed to start home watcher for ${mindName}`, logger_default.errorData(err));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function stopWatcher(mindName) {
|
|
81
|
+
const watcher = watchers.get(mindName);
|
|
82
|
+
if (watcher) {
|
|
83
|
+
watcher.close();
|
|
84
|
+
watchers.delete(mindName);
|
|
85
|
+
}
|
|
86
|
+
const hw = homeWatchers.get(mindName);
|
|
87
|
+
if (hw) {
|
|
88
|
+
hw.close();
|
|
89
|
+
homeWatchers.delete(mindName);
|
|
90
|
+
}
|
|
91
|
+
for (const [key, timer] of debounceTimers) {
|
|
92
|
+
if (key.startsWith(`${mindName}:`)) {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
debounceTimers.delete(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function stopAllWatchers() {
|
|
99
|
+
for (const [, watcher] of watchers) {
|
|
100
|
+
watcher.close();
|
|
101
|
+
}
|
|
102
|
+
watchers.clear();
|
|
103
|
+
for (const [, hw] of homeWatchers) {
|
|
104
|
+
hw.close();
|
|
105
|
+
}
|
|
106
|
+
homeWatchers.clear();
|
|
107
|
+
for (const [, timer] of debounceTimers) {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
debounceTimers.clear();
|
|
111
|
+
invalidateCache();
|
|
112
|
+
}
|
|
113
|
+
function invalidateCache() {
|
|
114
|
+
sitesCache = null;
|
|
115
|
+
recentPagesCache = null;
|
|
116
|
+
}
|
|
117
|
+
function scanPagesDir(dir, urlPrefix) {
|
|
118
|
+
const pages = [];
|
|
119
|
+
let items;
|
|
120
|
+
try {
|
|
121
|
+
items = readdirSync(dir);
|
|
122
|
+
} catch {
|
|
123
|
+
return pages;
|
|
124
|
+
}
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
if (item.startsWith(".")) continue;
|
|
127
|
+
const fullPath = resolve(dir, item);
|
|
128
|
+
try {
|
|
129
|
+
const s = statSync(fullPath);
|
|
130
|
+
if (s.isFile() && item.endsWith(".html")) {
|
|
131
|
+
pages.push({
|
|
132
|
+
file: item,
|
|
133
|
+
modified: s.mtime.toISOString(),
|
|
134
|
+
url: `${urlPrefix}/${item}`
|
|
135
|
+
});
|
|
136
|
+
} else if (s.isDirectory()) {
|
|
137
|
+
const indexPath = resolve(fullPath, "index.html");
|
|
138
|
+
if (existsSync(indexPath)) {
|
|
139
|
+
const indexStat = statSync(indexPath);
|
|
140
|
+
pages.push({
|
|
141
|
+
file: join(item, "index.html"),
|
|
142
|
+
modified: indexStat.mtime.toISOString(),
|
|
143
|
+
url: `${urlPrefix}/${item}/`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
151
|
+
return pages;
|
|
152
|
+
}
|
|
153
|
+
async function buildSites() {
|
|
154
|
+
const sites = [];
|
|
155
|
+
const systemPagesDir = resolve(voluteHome(), "shared", "pages");
|
|
156
|
+
if (existsSync(systemPagesDir)) {
|
|
157
|
+
const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
|
|
158
|
+
if (systemPages.length > 0) {
|
|
159
|
+
sites.push({ name: "_system", label: "System", pages: systemPages });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const entries = await readRegistry();
|
|
163
|
+
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
164
|
+
const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
|
|
165
|
+
if (!existsSync(pagesDir)) continue;
|
|
166
|
+
const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
|
|
167
|
+
if (mindPages.length > 0) {
|
|
168
|
+
sites.push({ name: entry.name, label: entry.name, pages: mindPages });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return sites;
|
|
172
|
+
}
|
|
173
|
+
async function buildRecentPages() {
|
|
174
|
+
const entries = await readRegistry();
|
|
175
|
+
const pages = [];
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
|
|
178
|
+
if (!existsSync(pagesDir)) continue;
|
|
179
|
+
let items;
|
|
180
|
+
try {
|
|
181
|
+
items = readdirSync(pagesDir);
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
for (const item of items) {
|
|
186
|
+
if (item.startsWith(".")) continue;
|
|
187
|
+
const fullPath = resolve(pagesDir, item);
|
|
188
|
+
try {
|
|
189
|
+
const s = statSync(fullPath);
|
|
190
|
+
if (s.isFile() && item.endsWith(".html")) {
|
|
191
|
+
pages.push({
|
|
192
|
+
mind: entry.name,
|
|
193
|
+
file: item,
|
|
194
|
+
modified: s.mtime.toISOString(),
|
|
195
|
+
url: `/pages/${entry.name}/${item}`
|
|
196
|
+
});
|
|
197
|
+
} else if (s.isDirectory()) {
|
|
198
|
+
const indexPath = resolve(fullPath, "index.html");
|
|
199
|
+
if (existsSync(indexPath)) {
|
|
200
|
+
const indexStat = statSync(indexPath);
|
|
201
|
+
pages.push({
|
|
202
|
+
mind: entry.name,
|
|
203
|
+
file: join(item, "index.html"),
|
|
204
|
+
modified: indexStat.mtime.toISOString(),
|
|
205
|
+
url: `/pages/${entry.name}/${item}/`
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
214
|
+
return pages.slice(0, 10);
|
|
215
|
+
}
|
|
216
|
+
async function getCachedSites() {
|
|
217
|
+
if (!sitesCache) sitesCache = await buildSites();
|
|
218
|
+
return sitesCache;
|
|
219
|
+
}
|
|
220
|
+
async function getCachedRecentPages() {
|
|
221
|
+
if (!recentPagesCache) recentPagesCache = await buildRecentPages();
|
|
222
|
+
return recentPagesCache;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
startSystemWatcher,
|
|
227
|
+
startWatcher,
|
|
228
|
+
stopWatcher,
|
|
229
|
+
stopAllWatchers,
|
|
230
|
+
getCachedSites,
|
|
231
|
+
getCachedRecentPages
|
|
232
|
+
};
|