volute 0.43.1 → 0.45.1

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 (132) hide show
  1. package/dist/{activity-events-UISXY4Y5.js → activity-events-OMCSM7XW.js} +3 -3
  2. package/dist/{ai-service-FBCCEYF5.js → ai-service-534WUSDL.js} +4 -4
  3. package/dist/{archive-I32WHCJB.js → archive-6R4TZMYI.js} +3 -3
  4. package/dist/{auth-3RIG6GKN.js → auth-FRZJVQCS.js} +4 -4
  5. package/dist/avatar-image-BNWEXSA2.js +16 -0
  6. package/dist/backup-OZXHL3JS.js +381 -0
  7. package/dist/{chat-SDSX5TVV.js → chat-I4EDDIQC.js} +2 -2
  8. package/dist/{chunk-AD46AYZQ.js → chunk-2BONPXAF.js} +1 -1
  9. package/dist/{chunk-ONM2AZFL.js → chunk-322KE457.js} +14 -2
  10. package/dist/{chunk-PGPLCJPK.js → chunk-3ERIL673.js} +1 -1
  11. package/dist/{chunk-QQ2MVQU3.js → chunk-5AUKCNPE.js} +10 -10
  12. package/dist/{chunk-XDTKJNW5.js → chunk-5GT7URXJ.js} +1 -1
  13. package/dist/{chunk-DMUPZQWP.js → chunk-77BVGCTZ.js} +8 -8
  14. package/dist/{chunk-EFL5UOY4.js → chunk-CYI534DJ.js} +3 -3
  15. package/dist/{chunk-57LBQ7JX.js → chunk-FHWUQZXE.js} +1 -1
  16. package/dist/{chunk-YAMO2U3N.js → chunk-H7IYFRAL.js} +1 -1
  17. package/dist/{chunk-M3PGGSBH.js → chunk-HVU45COX.js} +1 -1
  18. package/dist/{chunk-O3DXAR44.js → chunk-IEWZ7VD7.js} +2 -2
  19. package/dist/{chunk-DE4EKQFH.js → chunk-IUXTYXFI.js} +3 -3
  20. package/dist/{chunk-K4EHBHPE.js → chunk-JSRMEY52.js} +7 -5
  21. package/dist/{chunk-QHG4OMZL.js → chunk-KDLYIECM.js} +37 -32
  22. package/dist/{chunk-EE7SZJON.js → chunk-KP3KWNG6.js} +2 -2
  23. package/dist/chunk-KVKP6VQ5.js +318 -0
  24. package/dist/{chunk-JVK6QQIC.js → chunk-LAXUUY7O.js} +2 -2
  25. package/dist/{chunk-C6ESRQZ7.js → chunk-OXXTPILE.js} +1 -1
  26. package/dist/{chunk-ATEX3ZPG.js → chunk-Q2XBKQ2D.js} +2 -2
  27. package/dist/{chunk-GZJFC76M.js → chunk-QMJJWTNL.js} +1 -1
  28. package/dist/chunk-SQL7M4HJ.js +120 -0
  29. package/dist/{chunk-YSV3VJQK.js → chunk-TSK5PI4L.js} +1 -1
  30. package/dist/{chunk-4SVONUTC.js → chunk-U4FNXDEJ.js} +350 -207
  31. package/dist/{chunk-2HIVYZDB.js → chunk-UMI4UIJG.js} +1 -1
  32. package/dist/{chunk-Y6O35D7K.js → chunk-V4GYPD5L.js} +41 -16
  33. package/dist/{chunk-KINGVQSR.js → chunk-Y6OPHHGM.js} +5 -5
  34. package/dist/{chunk-SPLPKBES.js → chunk-ZIR7HSAX.js} +6 -9
  35. package/dist/{chunk-7G2EEYXX.js → chunk-ZMIMSIIT.js} +2 -2
  36. package/dist/{chunk-L3IV23ZG.js → chunk-ZV7MFISX.js} +2 -0
  37. package/dist/cli.js +53 -22
  38. package/dist/{cloud-sync-KOEBUFW5.js → cloud-sync-Y7LQPRE3.js} +16 -16
  39. package/dist/{conversations-67WWW25C.js → conversations-G4LYMHAA.js} +4 -4
  40. package/dist/{create-7W3LV3J7.js → create-2YY7IPMS.js} +1 -1
  41. package/dist/{daemon-restart-EZXB73W7.js → daemon-restart-ABP54YN6.js} +7 -7
  42. package/dist/daemon.js +679 -478
  43. package/dist/{db-CZV62BOQ.js → db-JFLCJCUG.js} +2 -2
  44. package/dist/{db-IJL6B26S.js → db-LZTLA7XC.js} +1 -1
  45. package/dist/delivery-manager-R5STGLXB.js +32 -0
  46. package/dist/{delivery-router-HXBHYHJM.js → delivery-router-6DE3BPHO.js} +3 -3
  47. package/dist/{down-RRNYIGCU.js → down-XCAZXWLB.js} +5 -5
  48. package/dist/echo-text-NQXYYYCU.js +32 -0
  49. package/dist/{exec-SJK7VUTC.js → exec-4GWHAHS3.js} +3 -3
  50. package/dist/{export-7LPCIB5J.js → export-ID7YUSLZ.js} +3 -3
  51. package/dist/{extensions-PQPZT6LS.js → extensions-AYSPKE33.js} +16 -16
  52. package/dist/{files-LV7VAKPC.js → files-BQ2WB5KC.js} +3 -3
  53. package/dist/{import-XPECCRQ7.js → import-SKID37QX.js} +6 -6
  54. package/dist/{isolation-5AYEY55P.js → isolation-5MB3J73F.js} +3 -3
  55. package/dist/{login-KJSN4JDA.js → login-LM4NF4ZB.js} +2 -2
  56. package/dist/{logout-TMJGUQV2.js → logout-CTJVIZGA.js} +2 -2
  57. package/dist/message-delivery-IUAXRPFE.js +34 -0
  58. package/dist/{mind-G3WEWDM4.js → mind-KLHC7XYR.js} +6 -6
  59. package/dist/{mind-activity-tracker-JHAT6456.js → mind-activity-tracker-BGHJE7XA.js} +4 -4
  60. package/dist/mind-service-BF2LUQMH.js +36 -0
  61. package/dist/{package-24TL6DTF.js → package-BMHV7UHN.js} +1 -1
  62. package/dist/{registry-7LQHTRUX.js → registry-NUJAGLQJ.js} +2 -2
  63. package/dist/{sandbox-SJH3XOPZ.js → sandbox-72MWV4JL.js} +4 -4
  64. package/dist/scheduler-YHJRRV6E.js +30 -0
  65. package/dist/{schema-LRUTFOER.js → schema-PVNG3PQM.js} +1 -1
  66. package/dist/{seed-HHSJTM6O.js → seed-4MKQ5QKG.js} +1 -1
  67. package/dist/{seed-cmd-UPQY35RG.js → seed-cmd-LCXMR7VV.js} +2 -2
  68. package/dist/{seed-create-MDRMXMSB.js → seed-create-N2BF5LQX.js} +3 -3
  69. package/dist/{seed-sprout-4M7IQWMH.js → seed-sprout-6ZDP23YI.js} +7 -7
  70. package/dist/{send-ZBZWIH5V.js → send-O6W53VBM.js} +37 -42
  71. package/dist/{service-4EVZKGSA.js → service-AQVPUANO.js} +4 -4
  72. package/dist/{service-install-SHRM6NHK.js → service-install-4QNUDKLM.js} +5 -5
  73. package/dist/{setup-ZD2LMBR3.js → setup-3O6O2JZ7.js} +7 -7
  74. package/dist/{setup-25MMJ3J4.js → setup-73YBJZQL.js} +3 -3
  75. package/dist/skills/memory/SKILL.md +2 -1
  76. package/dist/skills/orientation/SKILL.md +1 -1
  77. package/dist/skills/plan-coordinator/SKILL.md +2 -2
  78. package/dist/skills/volute-admin/SKILL.md +9 -6
  79. package/dist/skills/volute-mind/SKILL.md +15 -16
  80. package/dist/skills/volute-mind/references/extensions.md +5 -3
  81. package/dist/skills/volute-mind/references/integrations.md +2 -2
  82. package/dist/skills/volute-mind/references/routing.md +19 -23
  83. package/dist/skills/volute-mind/references/variants.md +10 -8
  84. package/dist/{skills-4KQ377GN.js → skills-ND4ZFYUW.js} +5 -5
  85. package/dist/sleep-manager-YAHDOIU5.js +34 -0
  86. package/dist/{spirit-53DQPRNR.js → spirit-II3HOERM.js} +7 -7
  87. package/dist/{sprout-YKM2M6D6.js → sprout-4KUG76AQ.js} +1 -1
  88. package/dist/{src-O4PRLMKM.js → src-JAOVXO5W.js} +1 -0
  89. package/dist/{src-3EGHHKZK.js → src-XFY3XDD6.js} +16 -11
  90. package/dist/{src-L2Z7X5UT.js → src-YH6PQ7LJ.js} +264 -52
  91. package/dist/{status-WTU2WHXY.js → status-E5DQNCAO.js} +5 -5
  92. package/dist/system-chat-E65T2HSW.js +34 -0
  93. package/dist/{tailscale-Q7ZJL7S7.js → tailscale-QAWVKQKU.js} +2 -2
  94. package/dist/{turn-tracker-JZYERJ76.js → turn-tracker-QQIVVXBI.js} +7 -5
  95. package/dist/{up-LFZA4GI5.js → up-LG56MPBI.js} +6 -6
  96. package/dist/{update-EJ7Q77ZX.js → update-7TYVH5FM.js} +5 -5
  97. package/dist/{update-check-ACYYNKTW.js → update-check-VSCSJ7KX.js} +3 -3
  98. package/dist/{upgrade-37DAGF6H.js → upgrade-NWAZ7MLD.js} +1 -1
  99. package/dist/{version-notify-SRQJCQEK.js → version-notify-CODJOUW2.js} +17 -17
  100. package/dist/web-assets/assets/index-B1JBQv2m.css +1 -0
  101. package/dist/web-assets/assets/index-Dac11sKB.js +86 -0
  102. package/dist/web-assets/ext-theme.css +5 -0
  103. package/dist/web-assets/index.html +2 -2
  104. package/package.json +1 -1
  105. package/packages/extensions/notes/dist/ui/assets/{index-BbgANhFm.js → index-D0-DgYyj.js} +1 -1
  106. package/packages/extensions/notes/dist/ui/index.html +1 -1
  107. package/packages/extensions/notes/skills/notes/SKILL.md +31 -3
  108. package/packages/extensions/pages/dist/ui/assets/{index-Dxk-DYrf.js → index-CQjEQg1r.js} +1 -1
  109. package/packages/extensions/pages/dist/ui/index.html +1 -1
  110. package/packages/extensions/plan/skills/plan/SKILL.md +1 -1
  111. package/templates/_base/.init/.config/prompts.json +1 -1
  112. package/templates/_base/.init/.local/hooks/startup-context.ts +21 -4
  113. package/templates/_base/.init/MEMORY.md +2 -1
  114. package/templates/_base/home/VOLUTE.md +6 -9
  115. package/templates/_base/src/lib/startup.ts +1 -1
  116. package/templates/claude/.init/CLAUDE.md +1 -1
  117. package/templates/claude/src/agent.ts +30 -3
  118. package/templates/claude/src/lib/stream-consumer.ts +7 -4
  119. package/templates/codex/.init/AGENTS.md +4 -2
  120. package/templates/codex/src/agent.ts +13 -7
  121. package/templates/pi/.init/MINDS.md +4 -2
  122. package/templates/pi/src/lib/event-handler.ts +6 -2
  123. package/dist/delivery-manager-OMCCJEVP.js +0 -32
  124. package/dist/echo-text-KFSUCZXL.js +0 -32
  125. package/dist/message-delivery-HRGFNWO7.js +0 -34
  126. package/dist/mind-service-2C7AINBO.js +0 -36
  127. package/dist/scheduler-VON2TB52.js +0 -30
  128. package/dist/sleep-manager-AG5SQDDI.js +0 -34
  129. package/dist/system-chat-54RN3K67.js +0 -34
  130. package/dist/web-assets/assets/index-D7udpBCx.css +0 -1
  131. package/dist/web-assets/assets/index-DTDmEfPU.js +0 -79
  132. package/templates/_base/.init/.config/hooks/pre-prompt/session-activity.ts +0 -40
@@ -7,7 +7,7 @@ import {
7
7
  initDb,
8
8
  syncPublishedPages,
9
9
  syncSystemPages
10
- } from "./chunk-QHG4OMZL.js";
10
+ } from "./chunk-KDLYIECM.js";
11
11
  import {
12
12
  createExtension
13
13
  } from "./chunk-RG5TOL4O.js";
@@ -22,6 +22,7 @@ import { relative as relative2, resolve as resolve2 } from "path";
22
22
 
23
23
  // packages/extensions/pages/src/shared-pages.ts
24
24
  import { execFile as execFileCb } from "child_process";
25
+ import { createHash } from "crypto";
25
26
  import {
26
27
  chmodSync,
27
28
  existsSync,
@@ -306,6 +307,12 @@ function collectHtmlFiles(dir) {
306
307
  walk(dir);
307
308
  return files.sort();
308
309
  }
310
+ function hashFiles(baseDir, files) {
311
+ return files.map((file) => ({
312
+ file,
313
+ hash: createHash("sha256").update(readFileSync(resolve(baseDir, file))).digest("hex")
314
+ }));
315
+ }
309
316
  async function pagesStatus(mindDir, isolation) {
310
317
  const wt = worktreePath(mindDir);
311
318
  const errors = [];
@@ -397,7 +404,7 @@ function createCommands() {
397
404
  if (result.ok && ctx.db) {
398
405
  const repoDir = resolve2(ctx.dataDir, "repo");
399
406
  try {
400
- syncSystemPages(ctx.db, collectHtmlFiles(repoDir), mindName);
407
+ syncSystemPages(ctx.db, hashFiles(repoDir, collectHtmlFiles(repoDir)), mindName);
401
408
  } catch (err) {
402
409
  console.error("[pages] failed to sync system pages to DB:", err);
403
410
  syncWarning = "\nWarning: failed to sync pages to dashboard \u2014 they may not appear in the UI until the next daemon restart.";
@@ -428,7 +435,7 @@ function createCommands() {
428
435
  const pageFiles = collectFiles(snapshotDir, snapshotDir, [".html", ".md"]);
429
436
  let diff;
430
437
  try {
431
- diff = syncPublishedPages(db, mindName, pageFiles);
438
+ diff = syncPublishedPages(db, mindName, hashFiles(snapshotDir, pageFiles));
432
439
  } catch (err) {
433
440
  return { error: `Failed to update page database: ${err.message}` };
434
441
  }
@@ -504,7 +511,7 @@ Warning: remote publish failed: ${err.message}`;
504
511
  const allFlag = flags.all;
505
512
  const port = process.env.VOLUTE_DAEMON_PORT || "1618";
506
513
  if (allFlag) {
507
- const { getAllSites: getAllSites2, getSystemPages: getSystemPages2 } = await import("./db-IJL6B26S.js");
514
+ const { getAllSites: getAllSites2, getSystemPages: getSystemPages2 } = await import("./db-LZTLA7XC.js");
508
515
  const sites = getAllSites2(db);
509
516
  const system = getSystemPages2(db);
510
517
  const lines2 = [];
@@ -2141,6 +2148,7 @@ var src_default = createExtension({
2141
2148
  name: "Pages",
2142
2149
  version: "0.1.0",
2143
2150
  description: "Publish and serve web pages from mind directories",
2151
+ mindDoc: "Publish web pages others can visit \u2014 finished creative work, essays, experiments, anything you want to give a lasting home on the web.",
2144
2152
  initDb,
2145
2153
  routes: (ctx) => createRoutes(ctx),
2146
2154
  publicRoutes: (ctx) => createPublicRoutes(ctx),
@@ -2165,13 +2173,10 @@ var src_default = createExtension({
2165
2173
  ensurePagesRepo(ctx.dataDir, isolationFrom(ctx)).then(() => {
2166
2174
  if (ctx.db) {
2167
2175
  const repoDir = resolve5(ctx.dataDir, "repo");
2168
- const htmlFiles = collectHtmlFiles(repoDir);
2169
- if (htmlFiles.length > 0) {
2170
- try {
2171
- syncSystemPages(ctx.db, htmlFiles);
2172
- } catch (err) {
2173
- console.error("[pages] failed to sync system pages to DB:", err);
2174
- }
2176
+ try {
2177
+ syncSystemPages(ctx.db, hashFiles(repoDir, collectHtmlFiles(repoDir)));
2178
+ } catch (err) {
2179
+ console.error("[pages] failed to sync system pages to DB:", err);
2175
2180
  }
2176
2181
  }
2177
2182
  }).catch((err) => {
@@ -9,22 +9,36 @@ import { resolve } from "path";
9
9
 
10
10
  // packages/extensions/notes/src/notes.ts
11
11
  function slugify(text) {
12
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
12
+ return text.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
13
+ }
14
+ function normalizeSlug(slug) {
15
+ return slug.toLowerCase().replace(/[^a-z0-9]/g, "");
13
16
  }
14
17
  async function createNote(db, getUser, authorId, title, content, replyToId) {
15
- let slug = slugify(title) || "untitled";
18
+ const baseSlug = slugify(title) || "untitled";
16
19
  const existing = db.prepare("SELECT slug FROM notes WHERE author_id = ?").all(authorId);
17
20
  const existingSlugs = new Set(existing.map((r) => r.slug));
18
- if (existingSlugs.has(slug)) {
21
+ function nextSlug(taken) {
22
+ if (!taken.has(baseSlug)) return baseSlug;
19
23
  let i = 2;
20
- while (existingSlugs.has(`${slug}-${i}`)) i++;
21
- slug = `${slug}-${i}`;
24
+ while (taken.has(`${baseSlug}-${i}`)) i++;
25
+ return `${baseSlug}-${i}`;
26
+ }
27
+ let row;
28
+ for (let attempt = 0; attempt < 5 && !row; attempt++) {
29
+ const slug = nextSlug(existingSlugs);
30
+ try {
31
+ row = db.prepare(
32
+ `INSERT INTO notes (author_id, title, slug, content, reply_to_id)
33
+ VALUES (?, ?, ?, ?, ?)
34
+ RETURNING *`
35
+ ).get(authorId, title, slug, content, replyToId ?? null);
36
+ } catch (err) {
37
+ if (!/UNIQUE/i.test(err.message)) throw err;
38
+ existingSlugs.add(slug);
39
+ }
22
40
  }
23
- const row = db.prepare(
24
- `INSERT INTO notes (author_id, title, slug, content, reply_to_id)
25
- VALUES (?, ?, ?, ?, ?)
26
- RETURNING *`
27
- ).get(authorId, title, slug, content, replyToId ?? null);
41
+ if (!row) throw new Error("Failed to allocate a unique slug for note");
28
42
  const author = await getUser(authorId);
29
43
  return {
30
44
  ...row,
@@ -33,10 +47,24 @@ async function createNote(db, getUser, authorId, title, content, replyToId) {
33
47
  comment_count: 0
34
48
  };
35
49
  }
50
+ async function findNote(db, getUserByUsername, authorUsername, slug) {
51
+ const author = await getUserByUsername(authorUsername);
52
+ if (!author) return { suggestions: [] };
53
+ const exact = db.prepare("SELECT slug FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
54
+ if (exact) return { authorId: author.id, slug };
55
+ const all = db.prepare("SELECT slug FROM notes WHERE author_id = ?").all(author.id);
56
+ const target = normalizeSlug(slug);
57
+ const fuzzy = all.filter((r) => normalizeSlug(r.slug) === target);
58
+ if (fuzzy.length === 1) return { authorId: author.id, slug: fuzzy[0].slug };
59
+ const pool = fuzzy.length > 1 ? fuzzy : all;
60
+ return { suggestions: pool.slice(0, 10).map((r) => `${authorUsername}/${r.slug}`) };
61
+ }
36
62
  async function getNote(db, getUser, getUserByUsername, authorUsername, slug) {
37
63
  const author = await getUserByUsername(authorUsername);
38
64
  if (!author) return null;
39
- const row = db.prepare("SELECT * FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
65
+ const found = await findNote(db, getUserByUsername, authorUsername, slug);
66
+ if (!("authorId" in found)) return null;
67
+ const row = db.prepare("SELECT * FROM notes WHERE author_id = ? AND slug = ?").get(author.id, found.slug);
40
68
  if (!row) return null;
41
69
  const comments = await getComments(db, getUser, row.id);
42
70
  const reactions = await getReactions(db, getUser, row.id);
@@ -74,6 +102,19 @@ async function getNote(db, getUser, getUserByUsername, authorUsername, slug) {
74
102
  replies
75
103
  };
76
104
  }
105
+ function noteFilter(authorId, since) {
106
+ const conds = [];
107
+ const params = [];
108
+ if (authorId !== void 0) {
109
+ conds.push("author_id = ?");
110
+ params.push(authorId);
111
+ }
112
+ if (since) {
113
+ conds.push("created_at > ?");
114
+ params.push(since);
115
+ }
116
+ return { clause: conds.length ? `WHERE ${conds.join(" AND ")}` : "", params };
117
+ }
77
118
  async function listNotes(db, getUser, getUserByUsername, opts) {
78
119
  const limit = opts?.limit ?? 50;
79
120
  const offset = opts?.offset ?? 0;
@@ -83,9 +124,8 @@ async function listNotes(db, getUser, getUserByUsername, opts) {
83
124
  if (!author) return [];
84
125
  authorId = author.id;
85
126
  }
86
- const rows = authorId ? db.prepare(
87
- "SELECT * FROM notes WHERE author_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?"
88
- ).all(authorId, limit, offset) : db.prepare("SELECT * FROM notes ORDER BY created_at DESC LIMIT ? OFFSET ?").all(limit, offset);
127
+ const { clause, params } = noteFilter(authorId, opts?.since);
128
+ const rows = db.prepare(`SELECT * FROM notes ${clause} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
89
129
  if (rows.length === 0) return [];
90
130
  const noteIds = rows.map((r) => r.id);
91
131
  const commentCounts = db.prepare(
@@ -143,6 +183,17 @@ async function listNotes(db, getUser, getUserByUsername, opts) {
143
183
  }
144
184
  return result;
145
185
  }
186
+ async function countNotes(db, getUserByUsername, opts) {
187
+ let authorId;
188
+ if (opts?.authorUsername) {
189
+ const author = await getUserByUsername(opts.authorUsername);
190
+ if (!author) return 0;
191
+ authorId = author.id;
192
+ }
193
+ const { clause, params } = noteFilter(authorId, opts?.since);
194
+ const row = db.prepare(`SELECT COUNT(*) as count FROM notes ${clause}`).get(...params);
195
+ return row.count;
196
+ }
146
197
  async function updateNote(db, getUser, getUserByUsername, authorUsername, slug, updates) {
147
198
  const author = await getUserByUsername(authorUsername);
148
199
  if (!author) return null;
@@ -195,9 +246,15 @@ async function getComments(db, getUser, noteId) {
195
246
  }
196
247
  return result;
197
248
  }
198
- async function deleteComment(db, commentId, authorId) {
199
- const existing = db.prepare("SELECT id, author_id FROM note_comments WHERE id = ?").get(commentId);
200
- if (!existing || existing.author_id !== authorId) return false;
249
+ async function deleteComment(db, commentId, actor) {
250
+ const existing = db.prepare(
251
+ `SELECT c.id AS id, c.author_id AS comment_author, n.author_id AS note_author
252
+ FROM note_comments c JOIN notes n ON n.id = c.note_id
253
+ WHERE c.id = ?`
254
+ ).get(commentId);
255
+ if (!existing) return false;
256
+ const authorized = actor.role === "admin" || actor.id === existing.comment_author || actor.id === existing.note_author;
257
+ if (!authorized) return false;
201
258
  db.prepare("DELETE FROM note_comments WHERE id = ?").run(existing.id);
202
259
  return true;
203
260
  }
@@ -237,15 +294,41 @@ async function getReactions(db, getUser, noteId) {
237
294
  return result;
238
295
  }
239
296
  async function resolveNoteId(db, getUserByUsername, authorSlug) {
240
- const [authorName, slug] = authorSlug.split("/", 2);
241
- if (!authorName || !slug) return null;
242
- const author = await getUserByUsername(authorName);
243
- if (!author) return null;
244
- const row = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(author.id, slug);
297
+ const parts = authorSlug.split("/");
298
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
299
+ const [authorName, slug] = parts;
300
+ const found = await findNote(db, getUserByUsername, authorName, slug);
301
+ if (!("authorId" in found)) return null;
302
+ const row = db.prepare("SELECT id FROM notes WHERE author_id = ? AND slug = ?").get(found.authorId, found.slug);
245
303
  return row?.id ?? null;
246
304
  }
247
305
 
248
306
  // packages/extensions/notes/src/commands.ts
307
+ function parseUtc(ts) {
308
+ const iso = ts.endsWith("Z") ? ts : `${ts.replace(" ", "T")}Z`;
309
+ return new Date(iso);
310
+ }
311
+ function formatDate(ts) {
312
+ return parseUtc(ts).toLocaleDateString();
313
+ }
314
+ function formatDateTime(ts) {
315
+ return parseUtc(ts).toLocaleString();
316
+ }
317
+ function toUtcTimestamp(input) {
318
+ const d = new Date(input.includes("T") ? input : input.replace(" ", "T"));
319
+ if (Number.isNaN(d.getTime())) return null;
320
+ return d.toISOString().replace("T", " ").slice(0, 19);
321
+ }
322
+ function reactionSummary(reactions) {
323
+ if (!reactions || reactions.length === 0) return "";
324
+ return reactions.map((r) => `${r.emoji}${r.count}`).join(" ");
325
+ }
326
+ function notFoundHint(suggestions, author) {
327
+ if (suggestions.length > 0) {
328
+ return ` Did you mean ${suggestions.slice(0, 3).join(", ")}?`;
329
+ }
330
+ return ` Try: volute notes list --author ${author}`;
331
+ }
249
332
  function createCommands() {
250
333
  return {
251
334
  write: {
@@ -280,34 +363,75 @@ function createCommands() {
280
363
  replyToId = id;
281
364
  }
282
365
  const note = await createNote(ctx.db, ctx.getUser, user.id, title, content, replyToId);
366
+ const ref = `${note.author_username}/${note.slug}`;
283
367
  ctx.publishActivity({
284
368
  type: "note_created",
285
369
  mind: user.username,
286
370
  summary: `${user.username} wrote "${title}"`,
287
371
  metadata: { author: user.username, slug: note.slug, bodyHtml: content.slice(0, 500) }
288
372
  });
289
- return { output: `Published: ${note.author_username}/${note.slug}` };
373
+ const announcement = replyTo ? `\u{1F4DD} ${user.username} replied to ${replyTo}: "${title}" \u2014 volute notes read ${ref}` : `\u{1F4DD} ${user.username} published a note: "${title}" \u2014 volute notes read ${ref}`;
374
+ await ctx.announceToSystem(announcement);
375
+ if (replyTo) {
376
+ const parentAuthor = replyTo.split("/")[0];
377
+ if (parentAuthor && parentAuthor !== user.username) {
378
+ await ctx.recordNotice(
379
+ parentAuthor,
380
+ `${user.username} replied to your note ${replyTo}: "${title}" (${ref})`
381
+ );
382
+ }
383
+ }
384
+ return { output: `Published: ${ref}` };
290
385
  }
291
386
  },
292
387
  list: {
293
388
  description: "List notes",
294
389
  flags: {
295
390
  author: { type: "string", description: "Filter by author name" },
296
- limit: { type: "number", description: "Max number of notes to show (default: 10)" }
391
+ limit: { type: "number", description: "Max number of notes to show (default: 10)" },
392
+ offset: { type: "number", description: "Skip this many notes (for paging)" },
393
+ since: { type: "string", description: "Only notes after this date (YYYY-MM-DD)" }
297
394
  },
298
395
  handler: async ({ flags }, ctx) => {
299
396
  if (!ctx.db) return { error: "Notes extension requires a database" };
300
397
  const author = flags.author;
301
398
  const limit = flags.limit ?? 10;
399
+ const offset = flags.offset ?? 0;
400
+ let since;
401
+ const rawSince = flags.since;
402
+ if (rawSince) {
403
+ const norm = toUtcTimestamp(rawSince);
404
+ if (!norm) return { error: `Invalid --since date: ${rawSince}` };
405
+ since = norm;
406
+ }
302
407
  const notes = await listNotes(ctx.db, ctx.getUser, ctx.getUserByUsername, {
303
408
  authorUsername: author,
304
- limit
409
+ limit,
410
+ offset,
411
+ since
412
+ });
413
+ const total = await countNotes(ctx.db, ctx.getUserByUsername, {
414
+ authorUsername: author,
415
+ since
305
416
  });
306
417
  if (notes.length === 0) return { output: "No notes found." };
307
418
  const lines = notes.map((n) => {
308
- const date = new Date(n.created_at).toLocaleDateString();
309
- return ` ${n.author_username}/${n.slug} "${n.title}" (${date})`;
419
+ const date = formatDate(n.created_at);
420
+ const markers = [];
421
+ if (n.reply_to) markers.push("\u21A9");
422
+ if (n.comment_count > 0) markers.push(`\u{1F4AC}${n.comment_count}`);
423
+ const reactions = reactionSummary(n.reactions);
424
+ if (reactions) markers.push(reactions);
425
+ const suffix = markers.length > 0 ? ` ${markers.join(" ")}` : "";
426
+ return ` ${n.author_username}/${n.slug} "${n.title}" (${date})${suffix}`;
310
427
  });
428
+ const shown = offset + notes.length;
429
+ if (total > shown || offset > 0) {
430
+ lines.push("");
431
+ lines.push(
432
+ `Showing ${offset + 1}\u2013${shown} of ${total} \u2014 use --limit / --offset to see more.`
433
+ );
434
+ }
311
435
  return { output: lines.join("\n") };
312
436
  }
313
437
  },
@@ -320,28 +444,12 @@ function createCommands() {
320
444
  if (!ref || !ref.includes("/")) return { error: "Usage: volute notes read <author/slug>" };
321
445
  const [author, slug] = ref.split("/", 2);
322
446
  const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
323
- if (!note) return { error: "Note not found" };
324
- const lines = [
325
- `# ${note.title}
326
- `,
327
- `By ${note.author_username} \u2014 ${new Date(note.created_at).toLocaleString()}
328
- `,
329
- note.content
330
- ];
331
- if (note.reactions?.length) {
332
- lines.push(
333
- `
334
- Reactions: ${note.reactions.map((r) => `${r.emoji} (${r.count})`).join(" ")}`
335
- );
447
+ if (!note) {
448
+ const found = await findNote(ctx.db, ctx.getUserByUsername, author, slug);
449
+ const suggestions = "suggestions" in found ? found.suggestions : [];
450
+ return { error: `Note not found.${notFoundHint(suggestions, author)}` };
336
451
  }
337
- if (note.comments?.length) {
338
- lines.push(`
339
- Comments (${note.comments.length}):`);
340
- for (const c of note.comments) {
341
- lines.push(` ${c.author_username}: ${c.content}`);
342
- }
343
- }
344
- return { output: lines.join("\n") };
452
+ return { output: renderNote(note) };
345
453
  }
346
454
  },
347
455
  comment: {
@@ -363,8 +471,19 @@ Comments (${note.comments.length}):`);
363
471
  }
364
472
  const [author, slug] = ref.split("/", 2);
365
473
  const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
366
- if (!note) return { error: "Note not found" };
474
+ if (!note) {
475
+ const found = await findNote(ctx.db, ctx.getUserByUsername, author, slug);
476
+ const suggestions = "suggestions" in found ? found.suggestions : [];
477
+ return { error: `Note not found.${notFoundHint(suggestions, author)}` };
478
+ }
367
479
  await addComment(ctx.db, ctx.getUser, note.id, user.id, content);
480
+ if (note.author_username !== user.username) {
481
+ const snippet = content.length > 80 ? `${content.slice(0, 80)}\u2026` : content;
482
+ await ctx.recordNotice(
483
+ note.author_username,
484
+ `${user.username} commented on your note ${note.author_username}/${note.slug}: "${snippet}"`
485
+ );
486
+ }
368
487
  return { output: "Comment added." };
369
488
  }
370
489
  },
@@ -387,11 +506,66 @@ Comments (${note.comments.length}):`);
387
506
  }
388
507
  const [author, slug] = ref.split("/", 2);
389
508
  const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
390
- if (!note) return { error: "Note not found" };
509
+ if (!note) {
510
+ const found = await findNote(ctx.db, ctx.getUserByUsername, author, slug);
511
+ const suggestions = "suggestions" in found ? found.suggestions : [];
512
+ return { error: `Note not found.${notFoundHint(suggestions, author)}` };
513
+ }
391
514
  const result = toggleReaction(ctx.db, note.id, user.id, emoji);
515
+ if (result.added && note.author_username !== user.username) {
516
+ await ctx.recordNotice(
517
+ note.author_username,
518
+ `${user.username} reacted ${emoji} to your note ${note.author_username}/${note.slug}`
519
+ );
520
+ }
392
521
  return { output: result.added ? "Reaction added." : "Reaction removed." };
393
522
  }
394
523
  },
524
+ edit: {
525
+ description: "Edit your own note (keeps comments, reactions, and replies)",
526
+ args: [
527
+ { name: "ref", required: true, description: "Note reference (author/slug)" },
528
+ { name: "content", description: "New content (or pipe via stdin)" }
529
+ ],
530
+ flags: {
531
+ title: { type: "string", description: "New title (slug is preserved)" }
532
+ },
533
+ examples: [
534
+ 'volute notes edit myname/my-note "Updated content"',
535
+ 'volute notes edit myname/my-note --title "New Title"',
536
+ 'echo "new body" | volute notes edit myname/my-note'
537
+ ],
538
+ handler: async ({ args, flags }, ctx) => {
539
+ if (!ctx.db) return { error: "Notes extension requires a database" };
540
+ const mindName = ctx.mindName;
541
+ if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
542
+ const user = await ctx.getUserByUsername(mindName);
543
+ if (!user) return { error: `Unknown mind: ${mindName}` };
544
+ const ref = args.ref;
545
+ if (!ref || !ref.includes("/")) {
546
+ return { error: "Usage: volute notes edit <author/slug> [content] [--title <t>]" };
547
+ }
548
+ const [author, slug] = ref.split("/", 2);
549
+ if (author !== user.username) {
550
+ return { error: "You can only edit your own notes." };
551
+ }
552
+ const newTitle = flags.title;
553
+ const newContent = args.content ?? ctx.stdin;
554
+ if (newTitle === void 0 && newContent === void 0) {
555
+ return { error: "Nothing to update \u2014 provide new content and/or --title." };
556
+ }
557
+ const updated = await updateNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug, {
558
+ ...newTitle !== void 0 ? { title: newTitle } : {},
559
+ ...newContent !== void 0 ? { content: newContent } : {}
560
+ });
561
+ if (!updated) {
562
+ const found = await findNote(ctx.db, ctx.getUserByUsername, author, slug);
563
+ const suggestions = "suggestions" in found ? found.suggestions : [];
564
+ return { error: `Note not found.${notFoundHint(suggestions, author)}` };
565
+ }
566
+ return { output: `Updated: ${updated.author_username}/${updated.slug}` };
567
+ }
568
+ },
395
569
  delete: {
396
570
  description: "Delete your own note",
397
571
  args: [{ name: "ref", required: true, description: "Note reference (author/slug)" }],
@@ -412,6 +586,43 @@ Comments (${note.comments.length}):`);
412
586
  }
413
587
  };
414
588
  }
589
+ function renderNote(note) {
590
+ const lines = [];
591
+ if (note.reply_to) {
592
+ lines.push(
593
+ `\u21A9 In reply to ${note.reply_to.author_username}/${note.reply_to.slug} "${note.reply_to.title}"
594
+ `
595
+ );
596
+ }
597
+ lines.push(`# ${note.title}
598
+ `);
599
+ const edited = note.updated_at && note.updated_at > note.created_at ? ` (edited ${formatDateTime(note.updated_at)})` : "";
600
+ lines.push(`By ${note.author_username} \u2014 ${formatDateTime(note.created_at)}${edited}
601
+ `);
602
+ lines.push(note.content);
603
+ if (note.reactions?.length) {
604
+ const parts = note.reactions.map(
605
+ (r) => r.usernames.length > 0 ? `${r.emoji} ${r.usernames.join(", ")}` : `${r.emoji} (${r.count})`
606
+ );
607
+ lines.push(`
608
+ Reactions: ${parts.join(" ")}`);
609
+ }
610
+ if (note.replies?.length) {
611
+ lines.push(`
612
+ Replies (${note.replies.length}):`);
613
+ for (const r of note.replies) {
614
+ lines.push(` ${r.author_username}/${r.slug} "${r.title}" (${formatDate(r.created_at)})`);
615
+ }
616
+ }
617
+ if (note.comments?.length) {
618
+ lines.push(`
619
+ Comments (${note.comments.length}):`);
620
+ for (const c of note.comments) {
621
+ lines.push(` ${c.author_username} (${formatDate(c.created_at)}): ${c.content}`);
622
+ }
623
+ }
624
+ return lines.join("\n");
625
+ }
415
626
 
416
627
  // packages/extensions/notes/src/db.ts
417
628
  function initDb(db) {
@@ -463,7 +674,7 @@ async function parseJson(c) {
463
674
  function resolveUserId(c) {
464
675
  const user = c.get("user");
465
676
  if (!user || user.id === 0) return null;
466
- return { id: user.id, username: user.username };
677
+ return { id: user.id, username: user.username, role: user.role };
467
678
  }
468
679
  function createRoutes(ctx) {
469
680
  if (!ctx.db) throw new Error("Notes extension requires a database");
@@ -557,7 +768,7 @@ function createRoutes(ctx) {
557
768
  if (!actor) return c.json({ error: "Unauthorized" }, 401);
558
769
  const commentId = parseInt(c.req.param("id"), 10);
559
770
  if (Number.isNaN(commentId)) return c.json({ error: "Invalid comment ID" }, 400);
560
- const deleted = await deleteComment(db, commentId, actor.id);
771
+ const deleted = await deleteComment(db, commentId, { id: actor.id, role: actor.role });
561
772
  if (!deleted) return c.json({ error: "Comment not found or not authorized" }, 404);
562
773
  return c.json({ ok: true });
563
774
  }).get("/feed", async (c) => {
@@ -593,6 +804,7 @@ var src_default = createExtension({
593
804
  name: "Notes",
594
805
  version: "0.1.0",
595
806
  description: "Public notes for sharing thoughts, reflections, and ideas",
807
+ mindDoc: "A public shelf for small thoughts \u2014 pages are for finished things, notes are for pebbles. Write, read, comment, and react; you'll hear when someone responds to yours.",
596
808
  icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h10M3 7h8M3 10h6M3 13h9"/></svg>',
597
809
  color: "yellow",
598
810
  routes: (ctx) => createRoutes(ctx),
@@ -8,7 +8,7 @@ import {
8
8
  import "./chunk-O7IGP7ZW.js";
9
9
  import {
10
10
  checkForUpdate
11
- } from "./chunk-AD46AYZQ.js";
11
+ } from "./chunk-2BONPXAF.js";
12
12
  import {
13
13
  LAUNCHD_PLIST_LABEL,
14
14
  LAUNCHD_PLIST_PATH,
@@ -19,10 +19,10 @@ import {
19
19
  getServiceMode,
20
20
  modeLabel,
21
21
  readDaemonConfig
22
- } from "./chunk-ATEX3ZPG.js";
23
- import "./chunk-PGPLCJPK.js";
24
- import "./chunk-YAMO2U3N.js";
25
- import "./chunk-L3IV23ZG.js";
22
+ } from "./chunk-Q2XBKQ2D.js";
23
+ import "./chunk-3ERIL673.js";
24
+ import "./chunk-H7IYFRAL.js";
25
+ import "./chunk-ZV7MFISX.js";
26
26
  import "./chunk-K3NQKI34.js";
27
27
 
28
28
  // src/commands/status.ts
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ensureSystemDM,
4
+ generateSystemReply,
5
+ resetSystemDMCache,
6
+ sendSystemMessage,
7
+ sendSystemMessageDirect
8
+ } from "./chunk-U4FNXDEJ.js";
9
+ import "./chunk-5GT7URXJ.js";
10
+ import "./chunk-5AUKCNPE.js";
11
+ import "./chunk-QLWG3C2Z.js";
12
+ import "./chunk-Y6OPHHGM.js";
13
+ import "./chunk-JSRMEY52.js";
14
+ import "./chunk-CU6OFXMM.js";
15
+ import "./chunk-OXXTPILE.js";
16
+ import "./chunk-HVU45COX.js";
17
+ import "./chunk-ZIR7HSAX.js";
18
+ import "./chunk-V4GYPD5L.js";
19
+ import "./chunk-3ERIL673.js";
20
+ import "./chunk-TSK5PI4L.js";
21
+ import "./chunk-322KE457.js";
22
+ import "./chunk-CYI534DJ.js";
23
+ import "./chunk-ZMIMSIIT.js";
24
+ import "./chunk-T2TP6ZC6.js";
25
+ import "./chunk-H7IYFRAL.js";
26
+ import "./chunk-ZV7MFISX.js";
27
+ import "./chunk-K3NQKI34.js";
28
+ export {
29
+ ensureSystemDM,
30
+ generateSystemReply,
31
+ resetSystemDMCache,
32
+ sendSystemMessage,
33
+ sendSystemMessageDirect
34
+ };
@@ -4,8 +4,8 @@ import {
4
4
  } from "./chunk-T2TP6ZC6.js";
5
5
  import {
6
6
  voluteHome
7
- } from "./chunk-YAMO2U3N.js";
8
- import "./chunk-L3IV23ZG.js";
7
+ } from "./chunk-H7IYFRAL.js";
8
+ import "./chunk-ZV7MFISX.js";
9
9
  import "./chunk-K3NQKI34.js";
10
10
 
11
11
  // packages/daemon/src/lib/tailscale.ts
@@ -7,18 +7,19 @@ import {
7
7
  createTurn,
8
8
  getActiveTurnId,
9
9
  getLastToolUseEventId,
10
+ getToolUseEventId,
10
11
  linkInboundToActiveTurn,
11
12
  markErrored,
12
13
  summarizeOrphanedTurns,
13
14
  sweepWedgedTurns,
14
15
  takeErrored,
15
16
  trackToolUse
16
- } from "./chunk-Y6O35D7K.js";
17
- import "./chunk-YSV3VJQK.js";
18
- import "./chunk-ONM2AZFL.js";
17
+ } from "./chunk-V4GYPD5L.js";
18
+ import "./chunk-TSK5PI4L.js";
19
+ import "./chunk-322KE457.js";
19
20
  import "./chunk-T2TP6ZC6.js";
20
- import "./chunk-YAMO2U3N.js";
21
- import "./chunk-L3IV23ZG.js";
21
+ import "./chunk-H7IYFRAL.js";
22
+ import "./chunk-ZV7MFISX.js";
22
23
  import "./chunk-K3NQKI34.js";
23
24
  export {
24
25
  assignSession,
@@ -28,6 +29,7 @@ export {
28
29
  createTurn,
29
30
  getActiveTurnId,
30
31
  getLastToolUseEventId,
32
+ getToolUseEventId,
31
33
  linkInboundToActiveTurn,
32
34
  markErrored,
33
35
  summarizeOrphanedTurns,
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-DE4EKQFH.js";
4
+ } from "./chunk-IUXTYXFI.js";
5
5
  import "./chunk-TXSA4Q3V.js";
6
6
  import "./chunk-O7IGP7ZW.js";
7
- import "./chunk-ATEX3ZPG.js";
8
- import "./chunk-PGPLCJPK.js";
7
+ import "./chunk-Q2XBKQ2D.js";
8
+ import "./chunk-3ERIL673.js";
9
9
  import {
10
10
  readGlobalConfig
11
- } from "./chunk-ONM2AZFL.js";
12
- import "./chunk-YAMO2U3N.js";
13
- import "./chunk-L3IV23ZG.js";
11
+ } from "./chunk-322KE457.js";
12
+ import "./chunk-H7IYFRAL.js";
13
+ import "./chunk-ZV7MFISX.js";
14
14
  import "./chunk-K3NQKI34.js";
15
15
  export {
16
16
  readGlobalConfig,