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.
Files changed (81) hide show
  1. package/README.md +20 -10
  2. package/dist/accept-666DIZX2.js +41 -0
  3. package/dist/api.d.ts +342 -143
  4. package/dist/{chat-MHJ3L6JQ.js → chat-KTPOR2JT.js} +18 -8
  5. package/dist/chunk-A6TUJJ3L.js +19 -0
  6. package/dist/{chunk-OQZH4PBB.js → chunk-CMILSHZD.js} +199 -277
  7. package/dist/{chunk-K5NAC55T.js → chunk-CQ7SNKNI.js} +1 -1
  8. package/dist/{chunk-PHSAT7YL.js → chunk-EHZKEMMV.js} +5 -5
  9. package/dist/{chunk-IAYBDWVG.js → chunk-FLZGS4QH.js} +145 -0
  10. package/dist/{chunk-USUXRNVD.js → chunk-J4IBNXGJ.js} +0 -2
  11. package/dist/chunk-MD4C26II.js +128 -0
  12. package/dist/{chunk-4WXYUOAK.js → chunk-NI5FFCCS.js} +8 -1
  13. package/dist/{chunk-JKOWNZ4P.js → chunk-P72MVS4R.js} +1 -40
  14. package/dist/chunk-THUUIU3E.js +232 -0
  15. package/dist/cli.js +21 -30
  16. package/dist/clock-DGCBVGYA.js +259 -0
  17. package/dist/{cloud-sync-T7M3ESC3.js → cloud-sync-KILFGV5Q.js} +7 -7
  18. package/dist/connectors/discord-bridge.js +1 -1
  19. package/dist/connectors/slack-bridge.js +1 -1
  20. package/dist/connectors/telegram-bridge.js +1 -1
  21. package/dist/{conversations-M2K4253F.js → conversations-P5BL7RMX.js} +7 -1
  22. package/dist/create-DFCAGEE5.js +70 -0
  23. package/dist/{daemon-restart-M2QTYMEG.js → daemon-restart-UHOMICXT.js} +1 -1
  24. package/dist/daemon.js +715 -661
  25. package/dist/files-M546TKVN.js +46 -0
  26. package/dist/{login-XX37I52P.js → login-BKP3AFWN.js} +7 -17
  27. package/dist/logout-IQK7FNEK.js +20 -0
  28. package/dist/{message-delivery-LDXLGERA.js → message-delivery-Q7VUMIEI.js} +11 -9
  29. package/dist/{mind-DI33C74K.js → mind-S5V6CK5W.js} +8 -13
  30. package/dist/{mind-activity-tracker-EN6XNXPF.js → mind-activity-tracker-WRHFI3YW.js} +1 -1
  31. package/dist/mind-list-UPJ75GPI.js +29 -0
  32. package/dist/{mind-manager-M6EMUW5I.js → mind-manager-P66HQDNE.js} +2 -2
  33. package/dist/mind-status-TK5AETEM.js +55 -0
  34. package/dist/{package-7WY6VKU3.js → package-OFKXNKJF.js} +1 -1
  35. package/dist/{pages-6EBS6CBR.js → pages-EUJR52AH.js} +5 -5
  36. package/dist/pages-watcher-P7QECRE2.js +21 -0
  37. package/dist/{publish-66UB2ZFY.js → publish-ZZB33WP4.js} +6 -17
  38. package/dist/{register-6B2CXTYM.js → register-CHREOMJ3.js} +5 -24
  39. package/dist/reject-LXIZFJ4Q.js +39 -0
  40. package/dist/{sandbox-TGBX22DS.js → sandbox-5BW5HPXM.js} +1 -1
  41. package/dist/{send-ZNCJDSRP.js → send-TAOEZ4NH.js} +64 -6
  42. package/dist/skills/dreaming/references/INSTALL.md +3 -17
  43. package/dist/skills/shared-files/SKILL.md +44 -0
  44. package/dist/skills/shared-files/scripts/merge.ts +72 -0
  45. package/dist/skills/shared-files/scripts/pull.ts +52 -0
  46. package/dist/skills/volute-mind/SKILL.md +48 -22
  47. package/dist/{sleep-manager-MWYHM5HV.js → sleep-manager-G4B5GW5P.js} +7 -7
  48. package/dist/{sprout-IJVVKSJ2.js → sprout-UNT7LKKE.js} +1 -1
  49. package/dist/{status-77YEPHMW.js → status-NQJYR4BG.js} +45 -1
  50. package/dist/{status-THLOBLWG.js → status-S7UUPNRW.js} +3 -13
  51. package/dist/systems-SMEFSHTA.js +60 -0
  52. package/dist/{up-NKSMXBWR.js → up-W6VAK2XE.js} +1 -1
  53. package/dist/{version-notify-5Z4MNR6M.js → version-notify-WDHRO3XD.js} +11 -11
  54. package/dist/web-assets/assets/index-BmKDnWDB.css +1 -0
  55. package/dist/web-assets/assets/index-CLJMx-GA.js +71 -0
  56. package/dist/web-assets/index.html +2 -2
  57. package/package.json +1 -1
  58. package/templates/_base/src/lib/logger.ts +10 -53
  59. package/templates/_base/src/lib/router.ts +1 -9
  60. package/templates/claude/src/lib/stream-consumer.ts +1 -4
  61. package/templates/pi/src/lib/event-handler.ts +1 -14
  62. package/dist/auth-D3OT2ARB.js +0 -37
  63. package/dist/chunk-KDGS53OS.js +0 -50
  64. package/dist/chunk-RWKVSSLY.js +0 -26
  65. package/dist/chunk-T6HKBWXZ.js +0 -23
  66. package/dist/create-D7J73A6H.js +0 -45
  67. package/dist/file-CR36YUPD.js +0 -204
  68. package/dist/log-ABYNVYJ3.js +0 -39
  69. package/dist/logout-W4KOOBIT.js +0 -18
  70. package/dist/logs-U35JR2KE.js +0 -77
  71. package/dist/merge-LNSMSAOF.js +0 -46
  72. package/dist/pull-XCHJTM5M.js +0 -39
  73. package/dist/schedule-QTJMFATP.js +0 -154
  74. package/dist/service-6LIN3F3K.js +0 -122
  75. package/dist/shared-ML5I4Q2A.js +0 -39
  76. package/dist/status-7GA4SM4Y.js +0 -35
  77. package/dist/web-assets/assets/index-CI5wgghI.css +0 -1
  78. package/dist/web-assets/assets/index-is5CvJWH.js +0 -75
  79. package/dist/{chunk-GIE6CSN5.js → chunk-DUAUMCEE.js} +0 -0
  80. package/dist/{history-XKRTAFS2.js → history-ALPTNB3I.js} +0 -0
  81. 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 = 2 * 60 * 1e3;
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-USUXRNVD.js";
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}. ${queuedInfo}",
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", "queuedInfo"],
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-MWYHM5HV.js");
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-EN6XNXPF.js").then(({ markIdle }) => markIdle(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
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 = ["volute-mind", "memory", "sessions", "notes", "dreaming"];
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
+ };