volute 0.43.0 → 0.44.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 (108) hide show
  1. package/dist/{activity-events-UISXY4Y5.js → activity-events-OMCSM7XW.js} +3 -3
  2. package/dist/{ai-service-FBCCEYF5.js → ai-service-7PHSRDMO.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/{chat-SDSX5TVV.js → chat-I4EDDIQC.js} +2 -2
  7. package/dist/{chunk-AD46AYZQ.js → chunk-2BONPXAF.js} +1 -1
  8. package/dist/{chunk-PGPLCJPK.js → chunk-3ERIL673.js} +1 -1
  9. package/dist/{chunk-C6ESRQZ7.js → chunk-3GYVW2X3.js} +1 -1
  10. package/dist/{chunk-SPLPKBES.js → chunk-5AHYQHNT.js} +4 -8
  11. package/dist/{chunk-XDTKJNW5.js → chunk-5GT7URXJ.js} +1 -1
  12. package/dist/{chunk-65OLHBCL.js → chunk-7FT24JBQ.js} +8 -8
  13. package/dist/{chunk-ZO7IQJKO.js → chunk-7KWANSLX.js} +3 -3
  14. package/dist/{chunk-EFL5UOY4.js → chunk-CYI534DJ.js} +3 -3
  15. package/dist/{chunk-FZ5TH5PF.js → chunk-EQZD4YU3.js} +8 -8
  16. package/dist/{chunk-57LBQ7JX.js → chunk-FHWUQZXE.js} +1 -1
  17. package/dist/{chunk-K4EHBHPE.js → chunk-FZMEVCWC.js} +3 -3
  18. package/dist/{chunk-YAMO2U3N.js → chunk-H7IYFRAL.js} +1 -1
  19. package/dist/{chunk-M3PGGSBH.js → chunk-HVU45COX.js} +1 -1
  20. package/dist/{chunk-FSSUANZJ.js → chunk-I2Y2YHIO.js} +5 -5
  21. package/dist/{chunk-O3DXAR44.js → chunk-IEWZ7VD7.js} +2 -2
  22. package/dist/{chunk-YSV3VJQK.js → chunk-J47TIYHA.js} +1 -1
  23. package/dist/{chunk-QHG4OMZL.js → chunk-KDLYIECM.js} +37 -32
  24. package/dist/{chunk-EE7SZJON.js → chunk-KP3KWNG6.js} +2 -2
  25. package/dist/{chunk-JVK6QQIC.js → chunk-LAXUUY7O.js} +2 -2
  26. package/dist/{chunk-3WLWKOGM.js → chunk-NF75HPNR.js} +5 -5
  27. package/dist/{chunk-PF2AB7VJ.js → chunk-OMETFX5H.js} +321 -204
  28. package/dist/{chunk-ONM2AZFL.js → chunk-PNV24SEP.js} +1 -1
  29. package/dist/{chunk-ATEX3ZPG.js → chunk-Q2XBKQ2D.js} +2 -2
  30. package/dist/{chunk-GZJFC76M.js → chunk-QMJJWTNL.js} +1 -1
  31. package/dist/chunk-SQL7M4HJ.js +120 -0
  32. package/dist/{chunk-2HIVYZDB.js → chunk-UMI4UIJG.js} +1 -1
  33. package/dist/{chunk-7G2EEYXX.js → chunk-ZMIMSIIT.js} +2 -2
  34. package/dist/{chunk-L3IV23ZG.js → chunk-ZV7MFISX.js} +2 -0
  35. package/dist/cli.js +47 -22
  36. package/dist/{cloud-sync-FRYNWKCR.js → cloud-sync-6WJ7WFYD.js} +16 -16
  37. package/dist/{conversations-67WWW25C.js → conversations-WAJYZKOC.js} +4 -4
  38. package/dist/{create-7W3LV3J7.js → create-YZIV5OJC.js} +1 -1
  39. package/dist/{daemon-restart-5GVZ5V3X.js → daemon-restart-OUJVINXV.js} +7 -7
  40. package/dist/daemon.js +297 -294
  41. package/dist/{db-CZV62BOQ.js → db-JFLCJCUG.js} +2 -2
  42. package/dist/{db-IJL6B26S.js → db-LZTLA7XC.js} +1 -1
  43. package/dist/delivery-manager-TEVOHX5W.js +32 -0
  44. package/dist/{delivery-router-HXBHYHJM.js → delivery-router-GODSIS62.js} +3 -3
  45. package/dist/{down-RRNYIGCU.js → down-XCAZXWLB.js} +5 -5
  46. package/dist/echo-text-T7ZA5EYD.js +32 -0
  47. package/dist/{exec-SJK7VUTC.js → exec-4GWHAHS3.js} +3 -3
  48. package/dist/{export-7LPCIB5J.js → export-ID7YUSLZ.js} +3 -3
  49. package/dist/{extensions-76EGTMFY.js → extensions-3QNDGW5E.js} +16 -16
  50. package/dist/{files-LV7VAKPC.js → files-BQ2WB5KC.js} +3 -3
  51. package/dist/{import-XPECCRQ7.js → import-SKID37QX.js} +6 -6
  52. package/dist/{isolation-5AYEY55P.js → isolation-5MB3J73F.js} +3 -3
  53. package/dist/{login-KJSN4JDA.js → login-LM4NF4ZB.js} +2 -2
  54. package/dist/{logout-TMJGUQV2.js → logout-CTJVIZGA.js} +2 -2
  55. package/dist/message-delivery-2EA5LLMD.js +34 -0
  56. package/dist/{mind-QEWCR7FA.js → mind-MI7Z6KM2.js} +5 -5
  57. package/dist/{mind-activity-tracker-JHAT6456.js → mind-activity-tracker-BGHJE7XA.js} +4 -4
  58. package/dist/mind-service-S3SY7HAM.js +36 -0
  59. package/dist/{package-ZOI2KAYC.js → package-5B4XDTJH.js} +1 -1
  60. package/dist/{registry-7LQHTRUX.js → registry-NUJAGLQJ.js} +2 -2
  61. package/dist/{sandbox-SJH3XOPZ.js → sandbox-IOPWWILM.js} +4 -4
  62. package/dist/scheduler-CXWQ6Z4E.js +30 -0
  63. package/dist/{schema-LRUTFOER.js → schema-PVNG3PQM.js} +1 -1
  64. package/dist/{seed-KOG6KJAO.js → seed-UUPVQWFB.js} +1 -1
  65. package/dist/{seed-cmd-GJFENEJH.js → seed-cmd-7XPKDQUC.js} +2 -2
  66. package/dist/{seed-create-YXO6R4KE.js → seed-create-IZ4M5ESO.js} +3 -3
  67. package/dist/{seed-sprout-BBU64QXP.js → seed-sprout-POJXHJWV.js} +7 -7
  68. package/dist/{send-ZBZWIH5V.js → send-O6W53VBM.js} +37 -42
  69. package/dist/{service-4EVZKGSA.js → service-AQVPUANO.js} +4 -4
  70. package/dist/{service-install-SHRM6NHK.js → service-install-4QNUDKLM.js} +5 -5
  71. package/dist/{setup-354PDHUF.js → setup-5GCO3CUB.js} +7 -7
  72. package/dist/{setup-25MMJ3J4.js → setup-MLRKNP7Y.js} +3 -3
  73. package/dist/{skills-DPI3WZPQ.js → skills-775C7QFF.js} +5 -5
  74. package/dist/sleep-manager-HINI6Y46.js +34 -0
  75. package/dist/{spirit-JQQHUVWR.js → spirit-IK5OYWMV.js} +7 -7
  76. package/dist/{sprout-4WAYBUVV.js → sprout-ZY5QGOEL.js} +1 -1
  77. package/dist/{src-O4PRLMKM.js → src-JAOVXO5W.js} +1 -0
  78. package/dist/{src-TJ7RBU5H.js → src-XFY3XDD6.js} +24 -19
  79. package/dist/{src-L2Z7X5UT.js → src-YH6PQ7LJ.js} +264 -52
  80. package/dist/{status-WTU2WHXY.js → status-E5DQNCAO.js} +5 -5
  81. package/dist/system-chat-4DQZOEZI.js +34 -0
  82. package/dist/{tailscale-Q7ZJL7S7.js → tailscale-QAWVKQKU.js} +2 -2
  83. package/dist/{turn-tracker-2GMKJQTN.js → turn-tracker-RWZMY7M6.js} +5 -5
  84. package/dist/{up-7RSBEKW6.js → up-FLCDR2UB.js} +6 -6
  85. package/dist/{update-EJ7Q77ZX.js → update-7TYVH5FM.js} +5 -5
  86. package/dist/{update-check-ACYYNKTW.js → update-check-VSCSJ7KX.js} +3 -3
  87. package/dist/{version-notify-DCAIXXZB.js → version-notify-B67N35A3.js} +17 -17
  88. package/dist/web-assets/assets/{index-DTDmEfPU.js → index-Domoz5Cg.js} +8 -8
  89. package/dist/web-assets/assets/index-dJdNzAPn.css +1 -0
  90. package/dist/web-assets/ext-theme.css +5 -0
  91. package/dist/web-assets/index.html +2 -2
  92. package/package.json +1 -1
  93. package/packages/extensions/notes/dist/ui/assets/{index-BbgANhFm.js → index-D0-DgYyj.js} +1 -1
  94. package/packages/extensions/notes/dist/ui/index.html +1 -1
  95. package/packages/extensions/notes/skills/notes/SKILL.md +31 -3
  96. package/packages/extensions/pages/dist/ui/assets/{index-Dxk-DYrf.js → index-CQjEQg1r.js} +1 -1
  97. package/packages/extensions/pages/dist/ui/index.html +1 -1
  98. package/templates/_base/.init/.local/hooks/startup-context.ts +25 -0
  99. package/templates/claude/src/agent.ts +16 -2
  100. package/templates/claude/src/lib/message-channel.ts +28 -6
  101. package/dist/delivery-manager-5J2BE22P.js +0 -32
  102. package/dist/echo-text-XKBJM6RV.js +0 -32
  103. package/dist/message-delivery-WJBZFALY.js +0 -34
  104. package/dist/mind-service-ZBVUMJ5H.js +0 -36
  105. package/dist/scheduler-BR6C6B3E.js +0 -30
  106. package/dist/sleep-manager-BVU5D2AE.js +0 -34
  107. package/dist/system-chat-KMQIP7VH.js +0 -34
  108. package/dist/web-assets/assets/index-D7udpBCx.css +0 -1
@@ -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-OMETFX5H.js";
9
+ import "./chunk-5GT7URXJ.js";
10
+ import "./chunk-7FT24JBQ.js";
11
+ import "./chunk-QLWG3C2Z.js";
12
+ import "./chunk-I2Y2YHIO.js";
13
+ import "./chunk-FZMEVCWC.js";
14
+ import "./chunk-CU6OFXMM.js";
15
+ import "./chunk-3GYVW2X3.js";
16
+ import "./chunk-HVU45COX.js";
17
+ import "./chunk-5AHYQHNT.js";
18
+ import "./chunk-NF75HPNR.js";
19
+ import "./chunk-3ERIL673.js";
20
+ import "./chunk-J47TIYHA.js";
21
+ import "./chunk-PNV24SEP.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
@@ -13,12 +13,12 @@ import {
13
13
  sweepWedgedTurns,
14
14
  takeErrored,
15
15
  trackToolUse
16
- } from "./chunk-3WLWKOGM.js";
17
- import "./chunk-YSV3VJQK.js";
18
- import "./chunk-ONM2AZFL.js";
16
+ } from "./chunk-NF75HPNR.js";
17
+ import "./chunk-J47TIYHA.js";
18
+ import "./chunk-PNV24SEP.js";
19
19
  import "./chunk-T2TP6ZC6.js";
20
- import "./chunk-YAMO2U3N.js";
21
- import "./chunk-L3IV23ZG.js";
20
+ import "./chunk-H7IYFRAL.js";
21
+ import "./chunk-ZV7MFISX.js";
22
22
  import "./chunk-K3NQKI34.js";
23
23
  export {
24
24
  assignSession,
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  run
4
- } from "./chunk-ZO7IQJKO.js";
4
+ } from "./chunk-7KWANSLX.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-PNV24SEP.js";
12
+ import "./chunk-H7IYFRAL.js";
13
+ import "./chunk-ZV7MFISX.js";
14
14
  import "./chunk-K3NQKI34.js";
15
15
  export {
16
16
  readGlobalConfig,
@@ -7,23 +7,23 @@ import {
7
7
  checkForUpdate,
8
8
  getResolvedVersion,
9
9
  resolveVoluteInstall
10
- } from "./chunk-AD46AYZQ.js";
10
+ } from "./chunk-2BONPXAF.js";
11
11
  import {
12
12
  getServiceMode,
13
13
  modeLabel,
14
14
  pollHealth,
15
15
  readDaemonConfig,
16
16
  restartService
17
- } from "./chunk-ATEX3ZPG.js";
17
+ } from "./chunk-Q2XBKQ2D.js";
18
18
  import {
19
19
  exec,
20
20
  execInherit,
21
21
  resolveVoluteBin
22
- } from "./chunk-PGPLCJPK.js";
22
+ } from "./chunk-3ERIL673.js";
23
23
  import {
24
24
  voluteSystemDir
25
- } from "./chunk-YAMO2U3N.js";
26
- import "./chunk-L3IV23ZG.js";
25
+ } from "./chunk-H7IYFRAL.js";
26
+ import "./chunk-ZV7MFISX.js";
27
27
  import "./chunk-K3NQKI34.js";
28
28
 
29
29
  // src/commands/update.ts
@@ -8,9 +8,9 @@ import {
8
8
  getResolvedVersion,
9
9
  isNewer,
10
10
  resolveVoluteInstall
11
- } from "./chunk-AD46AYZQ.js";
12
- import "./chunk-YAMO2U3N.js";
13
- import "./chunk-L3IV23ZG.js";
11
+ } from "./chunk-2BONPXAF.js";
12
+ import "./chunk-H7IYFRAL.js";
13
+ import "./chunk-ZV7MFISX.js";
14
14
  import "./chunk-K3NQKI34.js";
15
15
  export {
16
16
  analyzeInstallPath,
@@ -1,28 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getCurrentVersion
4
- } from "./chunk-AD46AYZQ.js";
4
+ } from "./chunk-2BONPXAF.js";
5
5
  import {
6
6
  computeTemplateHash
7
7
  } from "./chunk-QMJCNZ6C.js";
8
8
  import {
9
9
  sendSystemMessage
10
- } from "./chunk-PF2AB7VJ.js";
11
- import "./chunk-XDTKJNW5.js";
12
- import "./chunk-65OLHBCL.js";
10
+ } from "./chunk-OMETFX5H.js";
11
+ import "./chunk-5GT7URXJ.js";
12
+ import "./chunk-7FT24JBQ.js";
13
13
  import "./chunk-QLWG3C2Z.js";
14
- import "./chunk-FSSUANZJ.js";
15
- import "./chunk-K4EHBHPE.js";
14
+ import "./chunk-I2Y2YHIO.js";
15
+ import "./chunk-FZMEVCWC.js";
16
16
  import "./chunk-CU6OFXMM.js";
17
- import "./chunk-C6ESRQZ7.js";
18
- import "./chunk-M3PGGSBH.js";
19
- import "./chunk-SPLPKBES.js";
20
- import "./chunk-3WLWKOGM.js";
21
- import "./chunk-PGPLCJPK.js";
22
- import "./chunk-YSV3VJQK.js";
23
- import "./chunk-ONM2AZFL.js";
24
- import "./chunk-EFL5UOY4.js";
25
- import "./chunk-7G2EEYXX.js";
17
+ import "./chunk-3GYVW2X3.js";
18
+ import "./chunk-HVU45COX.js";
19
+ import "./chunk-5AHYQHNT.js";
20
+ import "./chunk-NF75HPNR.js";
21
+ import "./chunk-3ERIL673.js";
22
+ import "./chunk-J47TIYHA.js";
23
+ import "./chunk-PNV24SEP.js";
24
+ import "./chunk-CYI534DJ.js";
25
+ import "./chunk-ZMIMSIIT.js";
26
26
  import {
27
27
  logger_default
28
28
  } from "./chunk-T2TP6ZC6.js";
@@ -30,8 +30,8 @@ import {
30
30
  readRegistry,
31
31
  setMindTemplateHash,
32
32
  voluteSystemDir
33
- } from "./chunk-YAMO2U3N.js";
34
- import "./chunk-L3IV23ZG.js";
33
+ } from "./chunk-H7IYFRAL.js";
34
+ import "./chunk-ZV7MFISX.js";
35
35
  import "./chunk-K3NQKI34.js";
36
36
 
37
37
  // packages/daemon/src/lib/version-notify.ts