volute 0.30.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{accept-E3PAH3QJ.js → accept-GAKQ3MEH.js} +4 -4
- package/dist/{activity-events-BKBPPUBP.js → activity-events-T5ZRCVAL.js} +2 -2
- package/dist/{ai-service-VAJT5UBS.js → ai-service-UWUPM4T6.js} +5 -3
- package/dist/api.d.ts +238 -111
- package/dist/{archive-WWDBWYN2.js → archive-YBNSJYZZ.js} +2 -2
- package/dist/auth-T5AW2USD.js +43 -0
- package/dist/{bridge-RO37CUFM.js → bridge-4AJ3EY26.js} +4 -4
- package/dist/{chat-TCUNPFGO.js → chat-7YLT7FI3.js} +8 -8
- package/dist/{chunk-EFVHR7KH.js → chunk-4OUOFS23.js} +24 -5
- package/dist/{chunk-G3GBKZGG.js → chunk-57OKQMP3.js} +54 -2
- package/dist/chunk-6QIUN46C.js +38 -0
- package/dist/{chunk-FSM45XD5.js → chunk-AAO77TZX.js} +1 -1
- package/dist/{chunk-DTC6EH5I.js → chunk-BC3P3QCK.js} +1 -1
- package/dist/{chunk-P7VFDSSG.js → chunk-BNC43CSY.js} +2 -2
- package/dist/{chunk-QVAQ5454.js → chunk-BWKIHH7B.js} +3080 -2099
- package/dist/{chunk-IKHDUZRH.js → chunk-DAXJKPHZ.js} +2 -2
- package/dist/{chunk-P27RV5WM.js → chunk-EKDWA7E4.js} +3 -1
- package/dist/{chunk-S5LR3XYJ.js → chunk-EMPFLFTG.js} +1 -1
- package/dist/{chunk-2C2VXEBB.js → chunk-FAHDKPEH.js} +18 -56
- package/dist/{chunk-W5OOPLNP.js → chunk-HDKY4TWU.js} +3 -3
- package/dist/{chunk-EFP3PE6C.js → chunk-HR5JKIDG.js} +2 -12
- package/dist/{chunk-JGFRDMR6.js → chunk-LX6T3GKQ.js} +1 -1
- package/dist/{chunk-2NDZC3S7.js → chunk-NOWVQ7AL.js} +447 -299
- package/dist/{chunk-ZWKTUQEL.js → chunk-NV3TYNWX.js} +1 -1
- package/dist/{chunk-NSBFETWP.js → chunk-PNQCXLSV.js} +7 -26
- package/dist/chunk-R5QJBZZG.js +175 -0
- package/dist/{chunk-MDPCSXZ4.js → chunk-S2TZLSDH.js} +4 -4
- package/dist/{chunk-FXHXHI2A.js → chunk-SNVPRRT7.js} +3 -6
- package/dist/{chunk-UPA6COHU.js → chunk-WRS3B556.js} +5 -5
- package/dist/{chunk-HHTXM4JT.js → chunk-X62AXPR7.js} +36 -4
- package/dist/cli.js +74 -23
- package/dist/{clock-G3ALCMLJ.js → clock-LJCG426D.js} +10 -8
- package/dist/{cloud-sync-JV4LJOK3.js → cloud-sync-O3LXIRN6.js} +13 -13
- package/dist/{conversations-7KVQV7EZ.js → conversations-RKKGP5IA.js} +7 -7
- package/dist/{create-VQSQHJQW.js → create-TL623TFC.js} +1 -1
- package/dist/{create-JTLS7GX3.js → create-WUTIIRI2.js} +4 -4
- package/dist/daemon-client-CVGM25DM.js +11 -0
- package/dist/{daemon-restart-4JGBHEJ4.js → daemon-restart-EZP7XH3V.js} +6 -7
- package/dist/daemon.js +1879 -1727
- package/dist/{db-HMFPIRO2.js → db-SW5PL6QA.js} +1 -1
- package/dist/{delete-JESHKE7F.js → delete-Z6HAG35F.js} +1 -1
- package/dist/down-TS4XQBA4.js +13 -0
- package/dist/{env-CLXXT7M2.js → env-NHESNNSP.js} +4 -4
- package/dist/{export-EGA5M5PB.js → export-EVMP7GWY.js} +3 -3
- package/dist/{extension-WZ4SUPJB.js → extension-LR7EW3JF.js} +5 -6
- package/dist/{extensions-ECO4RPFQ.js → extensions-NGEJI7JH.js} +7 -7
- package/dist/{files-4VEJDASH.js → files-3SM7V33S.js} +5 -5
- package/dist/{history-EJMMLXDO.js → history-PQD3LXEP.js} +4 -4
- package/dist/{import-YCGPMBSI.js → import-PR2OCGQJ.js} +3 -3
- package/dist/{join-2GBJKZEN.js → join-R4EN5CWQ.js} +1 -1
- package/dist/{list-Q6O7FGAN.js → list-B4XNUOFO.js} +4 -4
- package/dist/{login-RL6AU2SM.js → login-62JVY6A2.js} +4 -4
- package/dist/{login-RET5WESK.js → login-URWP6S2N.js} +2 -2
- package/dist/{logout-CGAGJN3L.js → logout-NXJQJDLI.js} +2 -2
- package/dist/{logout-JRPBEMMR.js → logout-ZK2N62T3.js} +2 -2
- package/dist/message-delivery-UJHCLVU4.js +30 -0
- package/dist/{mind-LUWRQUQ5.js → mind-E2ZV2WRX.js} +17 -17
- package/dist/{mind-activity-tracker-VYN2ZZ2M.js → mind-activity-tracker-ASNZBMLC.js} +3 -3
- package/dist/{mind-list-V5WW5DUA.js → mind-list-BEI7E5WY.js} +2 -2
- package/dist/mind-manager-IPA6DZXD.js +26 -0
- package/dist/{mind-sleep-R6PTNNW4.js → mind-sleep-CANABWJI.js} +4 -4
- package/dist/{mind-status-I4ISFJ6I.js → mind-status-6WKZVUOP.js} +2 -2
- package/dist/{mind-wake-67ZQEWAV.js → mind-wake-RZKLH2IN.js} +4 -4
- package/dist/{package-OYUD4ZJ4.js → package-NU4CA7OU.js} +3 -3
- package/dist/{pages-watcher-Z3PKNROC.js → pages-watcher-72OVPRMH.js} +4 -3
- package/dist/read-THL362EI.js +74 -0
- package/dist/{register-NZDSTLP3.js → register-QAQELAS6.js} +4 -4
- package/dist/{registry-ODSALQQL.js → registry-ASXCQCNH.js} +1 -1
- package/dist/{reject-2HZOJEIJ.js → reject-AYPBNPNL.js} +4 -4
- package/dist/{restart-QHS3NT64.js → restart-6SKPV3T2.js} +4 -4
- package/dist/{sandbox-O5FUSF43.js → sandbox-6ZEWQDVU.js} +3 -3
- package/dist/{seed-WUQMPLDM.js → seed-OWX2AW75.js} +36 -12
- package/dist/{send-OAN3RYYY.js → send-ZO4BTWXK.js} +5 -5
- package/dist/{setup-QMDK5RZX.js → setup-7CFITEQN.js} +2 -4
- package/dist/{setup-XJH3E7YM.js → setup-ZXBXG7E4.js} +6 -8
- package/dist/{skill-FZIN4W4Q.js → skill-YFXP67A2.js} +4 -4
- package/dist/skills/dreaming/references/INSTALL.md +2 -3
- package/dist/skills/dreaming/scripts/dream.ts +3 -25
- package/dist/skills/volute-mind/SKILL.md +1 -1
- package/dist/sleep-manager-TPS6OGCA.js +30 -0
- package/dist/{split-EXYGGGQN.js → split-MI62KJUU.js} +1 -1
- package/dist/{sprout-AXQ6H5DB.js → sprout-FDVI2CGN.js} +5 -6
- package/dist/{start-MTOVL6SY.js → start-D64BRKPH.js} +4 -4
- package/dist/{status-ZRO37MWR.js → status-ZZWBYFGE.js} +4 -5
- package/dist/{stop-OK5WEPVC.js → stop-OP2CTXCO.js} +4 -4
- package/dist/system-chat-B43GIXQU.js +30 -0
- package/dist/{systems-W3BBMSOZ.js → systems-EQPPT4B7.js} +5 -5
- package/dist/{tailscale-BM72RXCJ.js → tailscale-6DJKUMNF.js} +1 -1
- package/dist/up-TDXEP3VA.js +16 -0
- package/dist/{update-PLPHMMZ2.js → update-KUJXATRS.js} +4 -5
- package/dist/{update-check-CVCN7MF6.js → update-check-5WVSU37T.js} +2 -2
- package/dist/{upgrade-I6NPCYUU.js → upgrade-KBHCWX6T.js} +1 -1
- package/dist/{version-notify-2NTWVEHL.js → version-notify-75ELVKPV.js} +17 -21
- package/dist/web-assets/assets/index-BM1cTzBg.js +72 -0
- package/dist/web-assets/assets/index-BfJkKTPF.css +1 -0
- package/dist/web-assets/ext-theme.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0000_baseline.sql +152 -0
- package/drizzle/0001_add_conversation_private.sql +1 -0
- package/drizzle/0002_turns.sql +21 -0
- package/drizzle/0003_turn_feed_links.sql +11 -0
- package/drizzle/meta/0000_snapshot.json +3 -223
- package/drizzle/meta/0001_snapshot.json +3 -294
- package/drizzle/meta/0002_snapshot.json +3 -335
- package/drizzle/meta/0003_snapshot.json +3 -413
- package/drizzle/meta/_journal.json +8 -106
- package/package.json +3 -3
- package/packages/extensions/notes/dist/ui/assets/{index-DgawVO5g.css → index-B8GdTnXs.css} +1 -1
- package/packages/extensions/notes/dist/ui/assets/index-CDpGTCWb.js +2 -0
- package/packages/extensions/notes/dist/ui/index.html +2 -2
- package/packages/extensions/notes/skills/notes/SKILL.md +8 -8
- package/packages/extensions/pages/skills/pages/SKILL.md +7 -4
- package/packages/extensions/pages/skills/pages/scripts/pages.mjs +58 -0
- package/templates/_base/.init/.config/bin/volute +27 -0
- package/templates/_base/src/lib/auto-commit.ts +82 -43
- package/templates/_base/src/lib/daemon-client.ts +19 -23
- package/templates/_base/src/lib/router.ts +17 -1
- package/templates/_base/src/lib/startup.ts +6 -8
- package/templates/_base/src/lib/volute-server.ts +2 -5
- package/templates/claude/src/agent.ts +2 -1
- package/templates/claude/src/lib/hooks/auto-commit.ts +7 -3
- package/templates/claude/src/server.ts +0 -9
- package/templates/pi/package.json.tmpl +1 -1
- package/templates/pi/src/agent.ts +1 -1
- package/templates/pi/src/lib/event-handler.ts +5 -3
- package/dist/chunk-7D47T4RB.js +0 -84
- package/dist/chunk-CVH6Y2YG.js +0 -59
- package/dist/chunk-LIRWLNAK.js +0 -729
- package/dist/daemon-client-BCTFGVCZ.js +0 -9
- package/dist/down-NGBMGORS.js +0 -14
- package/dist/message-delivery-6YMVNOEC.js +0 -28
- package/dist/migrate-registry-to-db-FK35IPEH.js +0 -110
- package/dist/mind-manager-YFCOIAAX.js +0 -18
- package/dist/read-WQMPTSN2.js +0 -46
- package/dist/sleep-manager-O7YQFCV5.js +0 -30
- package/dist/up-BXUAIDXB.js +0 -17
- package/dist/web-assets/assets/index--kREqKl9.js +0 -72
- package/dist/web-assets/assets/index-BXYTG0nJ.css +0 -1
- package/drizzle/0000_flaky_mariko_yashida.sql +0 -34
- package/drizzle/0001_careless_warpath.sql +0 -12
- package/drizzle/0002_wealthy_the_call.sql +0 -6
- package/drizzle/0003_clean_ego.sql +0 -12
- package/drizzle/0004_magical_silverclaw.sql +0 -1
- package/drizzle/0005_rename_agents_to_minds.sql +0 -11
- package/drizzle/0006_mind_history.sql +0 -20
- package/drizzle/0007_system_prompts.sql +0 -5
- package/drizzle/0008_volute_channels.sql +0 -24
- package/drizzle/0009_shared_skills.sql +0 -9
- package/drizzle/0010_delivery_queue.sql +0 -12
- package/drizzle/0011_rename_human_to_brain.sql +0 -1
- package/drizzle/0012_activity.sql +0 -11
- package/drizzle/0013_user_profiles.sql +0 -3
- package/drizzle/0014_conversation_reads.sql +0 -7
- package/drizzle/0015_notes.sql +0 -23
- package/drizzle/0016_note_reactions_and_replies.sql +0 -15
- package/drizzle/0017_minds.sql +0 -16
- package/drizzle/meta/0004_snapshot.json +0 -410
- package/drizzle/meta/0005_snapshot.json +0 -410
- package/drizzle/meta/0006_snapshot.json +0 -7
- package/drizzle/meta/0007_snapshot.json +0 -7
- package/drizzle/meta/0008_snapshot.json +0 -7
- package/drizzle/meta/0009_snapshot.json +0 -7
- package/drizzle/meta/0010_snapshot.json +0 -7
- package/drizzle/meta/0011_snapshot.json +0 -7
- package/drizzle/meta/0012_snapshot.json +0 -7
- package/drizzle/meta/0013_snapshot.json +0 -7
- package/packages/extensions/notes/dist/ui/assets/index-qUWoeC4c.js +0 -2
- package/packages/extensions/notes/skills/notes/scripts/notes.mjs +0 -185
- package/templates/_base/home/public/.gitkeep +0 -0
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
getUser,
|
|
4
|
+
getUserByUsername
|
|
5
|
+
} from "./chunk-R5QJBZZG.js";
|
|
6
|
+
import {
|
|
4
7
|
publish
|
|
5
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-EKDWA7E4.js";
|
|
6
9
|
import {
|
|
7
10
|
hashSkillDir,
|
|
8
11
|
importSkillFromDir,
|
|
9
12
|
sharedSkillsDir
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-S2TZLSDH.js";
|
|
11
14
|
import {
|
|
12
15
|
logger_default
|
|
13
16
|
} from "./chunk-YUIHSKR6.js";
|
|
14
17
|
import {
|
|
15
18
|
getDb,
|
|
16
19
|
mindDir,
|
|
17
|
-
|
|
20
|
+
turns,
|
|
18
21
|
voluteHome,
|
|
19
|
-
voluteSystemDir
|
|
20
|
-
|
|
21
|
-
} from "./chunk-HHTXM4JT.js";
|
|
22
|
+
voluteSystemDir
|
|
23
|
+
} from "./chunk-X62AXPR7.js";
|
|
22
24
|
|
|
23
25
|
// src/lib/extensions.ts
|
|
24
26
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
@@ -40,47 +42,6 @@ function createExtension(manifest) {
|
|
|
40
42
|
return manifest;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
// packages/extensions/notes/src/db.ts
|
|
44
|
-
function initDb(db) {
|
|
45
|
-
db.exec(`
|
|
46
|
-
CREATE TABLE IF NOT EXISTS notes (
|
|
47
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
|
-
author_id INTEGER NOT NULL,
|
|
49
|
-
title TEXT NOT NULL,
|
|
50
|
-
slug TEXT NOT NULL,
|
|
51
|
-
content TEXT NOT NULL,
|
|
52
|
-
reply_to_id INTEGER,
|
|
53
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
54
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
55
|
-
);
|
|
56
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
|
|
58
|
-
CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
|
|
59
|
-
|
|
60
|
-
CREATE TABLE IF NOT EXISTS note_comments (
|
|
61
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
|
-
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
63
|
-
author_id INTEGER NOT NULL,
|
|
64
|
-
content TEXT NOT NULL,
|
|
65
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
66
|
-
);
|
|
67
|
-
CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
|
|
68
|
-
|
|
69
|
-
CREATE TABLE IF NOT EXISTS note_reactions (
|
|
70
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
71
|
-
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
72
|
-
user_id INTEGER NOT NULL,
|
|
73
|
-
emoji TEXT NOT NULL,
|
|
74
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
75
|
-
);
|
|
76
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
|
|
77
|
-
CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
|
|
78
|
-
`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// packages/extensions/notes/src/routes.ts
|
|
82
|
-
import { Hono } from "hono";
|
|
83
|
-
|
|
84
45
|
// packages/extensions/notes/src/notes.ts
|
|
85
46
|
function slugify(text) {
|
|
86
47
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -319,7 +280,199 @@ async function resolveNoteId(db, getUserByUsername2, authorSlug) {
|
|
|
319
280
|
return row?.id ?? null;
|
|
320
281
|
}
|
|
321
282
|
|
|
283
|
+
// packages/extensions/notes/src/commands.ts
|
|
284
|
+
function getFlag(args, flag) {
|
|
285
|
+
const idx = args.indexOf(flag);
|
|
286
|
+
if (idx !== -1 && args[idx + 1]) return args[idx + 1];
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
function createCommands() {
|
|
290
|
+
return {
|
|
291
|
+
write: {
|
|
292
|
+
description: "Write a new note",
|
|
293
|
+
usage: 'volute notes write "title" "content" [--reply-to author/slug]',
|
|
294
|
+
handler: async (args, ctx) => {
|
|
295
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
296
|
+
const mindName = ctx.mindName;
|
|
297
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
298
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
299
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
300
|
+
const title = args[0];
|
|
301
|
+
const content = args[1];
|
|
302
|
+
if (!title || !content)
|
|
303
|
+
return { error: 'Usage: volute notes write "title" "content" [--reply-to author/slug]' };
|
|
304
|
+
let replyToId;
|
|
305
|
+
const replyTo = getFlag(args, "--reply-to");
|
|
306
|
+
if (replyTo) {
|
|
307
|
+
const id = await resolveNoteId(ctx.db, ctx.getUserByUsername, replyTo);
|
|
308
|
+
if (id === null) return { error: `Reply target not found: ${replyTo}` };
|
|
309
|
+
replyToId = id;
|
|
310
|
+
}
|
|
311
|
+
const note = await createNote(ctx.db, ctx.getUser, user.id, title, content, replyToId);
|
|
312
|
+
ctx.publishActivity({
|
|
313
|
+
type: "note_created",
|
|
314
|
+
mind: user.username,
|
|
315
|
+
summary: `${user.username} wrote "${title}"`,
|
|
316
|
+
metadata: { author: user.username, slug: note.slug, bodyHtml: content.slice(0, 500) }
|
|
317
|
+
});
|
|
318
|
+
return { output: `Published: ${note.author_username}/${note.slug}` };
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
list: {
|
|
322
|
+
description: "List notes",
|
|
323
|
+
usage: "volute notes list [--author name] [--limit N]",
|
|
324
|
+
handler: async (args, ctx) => {
|
|
325
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
326
|
+
const author = getFlag(args, "--author");
|
|
327
|
+
const limit = parseInt(getFlag(args, "--limit") ?? "10", 10);
|
|
328
|
+
const notes = await listNotes(ctx.db, ctx.getUser, ctx.getUserByUsername, {
|
|
329
|
+
authorUsername: author,
|
|
330
|
+
limit
|
|
331
|
+
});
|
|
332
|
+
if (notes.length === 0) return { output: "No notes found." };
|
|
333
|
+
const lines = notes.map((n) => {
|
|
334
|
+
const date = new Date(n.created_at).toLocaleDateString();
|
|
335
|
+
return ` ${n.author_username}/${n.slug} "${n.title}" (${date})`;
|
|
336
|
+
});
|
|
337
|
+
return { output: lines.join("\n") };
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
read: {
|
|
341
|
+
description: "Read a note",
|
|
342
|
+
usage: "volute notes read <author/slug>",
|
|
343
|
+
handler: async (args, ctx) => {
|
|
344
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
345
|
+
const ref = args[0];
|
|
346
|
+
if (!ref || !ref.includes("/")) return { error: "Usage: volute notes read <author/slug>" };
|
|
347
|
+
const [author, slug] = ref.split("/", 2);
|
|
348
|
+
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
349
|
+
if (!note) return { error: "Note not found" };
|
|
350
|
+
const lines = [
|
|
351
|
+
`# ${note.title}
|
|
352
|
+
`,
|
|
353
|
+
`By ${note.author_username} \u2014 ${new Date(note.created_at).toLocaleString()}
|
|
354
|
+
`,
|
|
355
|
+
note.content
|
|
356
|
+
];
|
|
357
|
+
if (note.reactions?.length) {
|
|
358
|
+
lines.push(
|
|
359
|
+
`
|
|
360
|
+
Reactions: ${note.reactions.map((r) => `${r.emoji} (${r.count})`).join(" ")}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (note.comments?.length) {
|
|
364
|
+
lines.push(`
|
|
365
|
+
Comments (${note.comments.length}):`);
|
|
366
|
+
for (const c of note.comments) {
|
|
367
|
+
lines.push(` ${c.author_username}: ${c.content}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { output: lines.join("\n") };
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
comment: {
|
|
374
|
+
description: "Comment on a note",
|
|
375
|
+
usage: 'volute notes comment <author/slug> "content"',
|
|
376
|
+
handler: async (args, ctx) => {
|
|
377
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
378
|
+
const mindName = ctx.mindName;
|
|
379
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
380
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
381
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
382
|
+
const ref = args[0];
|
|
383
|
+
const content = args[1];
|
|
384
|
+
if (!ref || !ref.includes("/") || !content) {
|
|
385
|
+
return { error: 'Usage: volute notes comment <author/slug> "content"' };
|
|
386
|
+
}
|
|
387
|
+
const [author, slug] = ref.split("/", 2);
|
|
388
|
+
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
389
|
+
if (!note) return { error: "Note not found" };
|
|
390
|
+
await addComment(ctx.db, ctx.getUser, note.id, user.id, content);
|
|
391
|
+
return { output: "Comment added." };
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
react: {
|
|
395
|
+
description: "React to a note",
|
|
396
|
+
usage: 'volute notes react <author/slug> "emoji"',
|
|
397
|
+
handler: async (args, ctx) => {
|
|
398
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
399
|
+
const mindName = ctx.mindName;
|
|
400
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
401
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
402
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
403
|
+
const ref = args[0];
|
|
404
|
+
const emoji = args[1];
|
|
405
|
+
if (!ref || !ref.includes("/") || !emoji) {
|
|
406
|
+
return { error: 'Usage: volute notes react <author/slug> "emoji"' };
|
|
407
|
+
}
|
|
408
|
+
const [author, slug] = ref.split("/", 2);
|
|
409
|
+
const note = await getNote(ctx.db, ctx.getUser, ctx.getUserByUsername, author, slug);
|
|
410
|
+
if (!note) return { error: "Note not found" };
|
|
411
|
+
const result = toggleReaction(ctx.db, note.id, user.id, emoji);
|
|
412
|
+
return { output: result.added ? "Reaction added." : "Reaction removed." };
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
delete: {
|
|
416
|
+
description: "Delete your own note",
|
|
417
|
+
usage: "volute notes delete <author/slug>",
|
|
418
|
+
handler: async (args, ctx) => {
|
|
419
|
+
if (!ctx.db) return { error: "Notes extension requires a database" };
|
|
420
|
+
const mindName = ctx.mindName;
|
|
421
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
422
|
+
const user = await ctx.getUserByUsername(mindName);
|
|
423
|
+
if (!user) return { error: `Unknown mind: ${mindName}` };
|
|
424
|
+
const ref = args[0];
|
|
425
|
+
if (!ref || !ref.includes("/"))
|
|
426
|
+
return { error: "Usage: volute notes delete <author/slug>" };
|
|
427
|
+
const [author, slug] = ref.split("/", 2);
|
|
428
|
+
const deleted = await deleteNote(ctx.db, ctx.getUserByUsername, author, slug, user.id);
|
|
429
|
+
if (!deleted) return { error: "Note not found or not authorized" };
|
|
430
|
+
return { output: "Note deleted." };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// packages/extensions/notes/src/db.ts
|
|
437
|
+
function initDb(db) {
|
|
438
|
+
db.exec(`
|
|
439
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
440
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
441
|
+
author_id INTEGER NOT NULL,
|
|
442
|
+
title TEXT NOT NULL,
|
|
443
|
+
slug TEXT NOT NULL,
|
|
444
|
+
content TEXT NOT NULL,
|
|
445
|
+
reply_to_id INTEGER,
|
|
446
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
447
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
448
|
+
);
|
|
449
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_author_slug ON notes(author_id, slug);
|
|
450
|
+
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at);
|
|
451
|
+
CREATE INDEX IF NOT EXISTS idx_notes_reply_to ON notes(reply_to_id);
|
|
452
|
+
|
|
453
|
+
CREATE TABLE IF NOT EXISTS note_comments (
|
|
454
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
455
|
+
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
456
|
+
author_id INTEGER NOT NULL,
|
|
457
|
+
content TEXT NOT NULL,
|
|
458
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
459
|
+
);
|
|
460
|
+
CREATE INDEX IF NOT EXISTS idx_note_comments_note_id ON note_comments(note_id);
|
|
461
|
+
|
|
462
|
+
CREATE TABLE IF NOT EXISTS note_reactions (
|
|
463
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
464
|
+
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
|
465
|
+
user_id INTEGER NOT NULL,
|
|
466
|
+
emoji TEXT NOT NULL,
|
|
467
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
468
|
+
);
|
|
469
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_note_reactions_unique ON note_reactions(note_id, user_id, emoji);
|
|
470
|
+
CREATE INDEX IF NOT EXISTS idx_note_reactions_note_id ON note_reactions(note_id);
|
|
471
|
+
`);
|
|
472
|
+
}
|
|
473
|
+
|
|
322
474
|
// packages/extensions/notes/src/routes.ts
|
|
475
|
+
import { Hono } from "hono";
|
|
323
476
|
async function parseJson(c) {
|
|
324
477
|
try {
|
|
325
478
|
return await c.req.json();
|
|
@@ -366,12 +519,19 @@ function createRoutes(ctx) {
|
|
|
366
519
|
replyToId = id;
|
|
367
520
|
}
|
|
368
521
|
const note = await createNote(db, getUser2, actor.id, body.title, body.content, replyToId);
|
|
369
|
-
ctx.publishActivity(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
522
|
+
ctx.publishActivity(
|
|
523
|
+
{
|
|
524
|
+
type: "note_created",
|
|
525
|
+
mind: actor.username,
|
|
526
|
+
summary: `${actor.username} wrote "${body.title}"`,
|
|
527
|
+
metadata: {
|
|
528
|
+
author: actor.username,
|
|
529
|
+
slug: note.slug,
|
|
530
|
+
bodyHtml: body.content.slice(0, 500)
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
c
|
|
534
|
+
);
|
|
375
535
|
return c.json(note, 201);
|
|
376
536
|
}).get("/:author/:slug", async (c) => {
|
|
377
537
|
const { author, slug } = c.req.param();
|
|
@@ -460,13 +620,20 @@ var src_default = createExtension({
|
|
|
460
620
|
version: "0.1.0",
|
|
461
621
|
description: "Public notes for sharing thoughts, reflections, and ideas",
|
|
462
622
|
routes: (ctx) => createRoutes(ctx),
|
|
623
|
+
commands: createCommands(),
|
|
463
624
|
initDb,
|
|
464
625
|
skillsDir,
|
|
465
626
|
standardSkill: true,
|
|
466
627
|
ui: {
|
|
467
628
|
assetsDir,
|
|
468
629
|
systemSection: { id: "notes", label: "Notes", urlPatterns: ["/notes", "/notes/:author/:slug"] },
|
|
469
|
-
mindSections: [
|
|
630
|
+
mindSections: [
|
|
631
|
+
{
|
|
632
|
+
id: "notes",
|
|
633
|
+
label: "Notes",
|
|
634
|
+
icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2h6l4 4v8H4V2z"/><path d="M10 2v4h4"/><path d="M6 9h6M6 12h4"/></svg>'
|
|
635
|
+
}
|
|
636
|
+
],
|
|
470
637
|
feedSource: {
|
|
471
638
|
endpoint: "/api/ext/notes/feed"
|
|
472
639
|
}
|
|
@@ -499,7 +666,7 @@ var MIME_TYPES = {
|
|
|
499
666
|
var _pagesWatcher = null;
|
|
500
667
|
async function getPagesWatcher() {
|
|
501
668
|
if (_pagesWatcher) return _pagesWatcher;
|
|
502
|
-
const mod = await import("./pages-watcher-
|
|
669
|
+
const mod = await import("./pages-watcher-72OVPRMH.js");
|
|
503
670
|
_pagesWatcher = mod;
|
|
504
671
|
return _pagesWatcher;
|
|
505
672
|
}
|
|
@@ -553,6 +720,31 @@ function createRoutes2(ctx) {
|
|
|
553
720
|
} catch (err) {
|
|
554
721
|
return c.json({ error: `Connection failed: ${err.message}` }, 502);
|
|
555
722
|
}
|
|
723
|
+
}).post("/notify", async (c) => {
|
|
724
|
+
const user = ctx.resolveUser(c);
|
|
725
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
726
|
+
let body;
|
|
727
|
+
try {
|
|
728
|
+
body = await c.req.json();
|
|
729
|
+
} catch {
|
|
730
|
+
body = {};
|
|
731
|
+
}
|
|
732
|
+
const file = body.file ?? "page";
|
|
733
|
+
ctx.publishActivity(
|
|
734
|
+
{
|
|
735
|
+
type: "page_updated",
|
|
736
|
+
mind: user.username,
|
|
737
|
+
summary: `${user.username} updated ${file}`,
|
|
738
|
+
metadata: { file, iframeUrl: `/ext/pages/public/${user.username}/${file}` }
|
|
739
|
+
},
|
|
740
|
+
c
|
|
741
|
+
);
|
|
742
|
+
try {
|
|
743
|
+
const mod = await import("./pages-watcher-72OVPRMH.js");
|
|
744
|
+
mod.invalidateCache();
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
return c.json({ ok: true });
|
|
556
748
|
}).get("/status/:name", async (c) => {
|
|
557
749
|
const user = ctx.resolveUser(c);
|
|
558
750
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
@@ -576,7 +768,7 @@ function createRoutes2(ctx) {
|
|
|
576
768
|
var _voluteHome = null;
|
|
577
769
|
async function getVoluteHome() {
|
|
578
770
|
if (_voluteHome) return _voluteHome();
|
|
579
|
-
const mod = await import("./registry-
|
|
771
|
+
const mod = await import("./registry-ASXCQCNH.js");
|
|
580
772
|
_voluteHome = mod.voluteHome;
|
|
581
773
|
return _voluteHome();
|
|
582
774
|
}
|
|
@@ -624,9 +816,26 @@ var skillsDir2 = resolve3(import.meta.dirname, "../skills");
|
|
|
624
816
|
var _watcher = null;
|
|
625
817
|
async function getWatcher() {
|
|
626
818
|
if (_watcher) return _watcher;
|
|
627
|
-
_watcher = await import("./pages-watcher-
|
|
819
|
+
_watcher = await import("./pages-watcher-72OVPRMH.js");
|
|
628
820
|
return _watcher;
|
|
629
821
|
}
|
|
822
|
+
var notifyHandler = async (args, ctx) => {
|
|
823
|
+
const mindName = ctx.mindName;
|
|
824
|
+
if (!mindName) return { error: "No mind specified (use --mind or VOLUTE_MIND)" };
|
|
825
|
+
const file = args[0] || "page";
|
|
826
|
+
ctx.publishActivity({
|
|
827
|
+
type: "page_updated",
|
|
828
|
+
mind: mindName,
|
|
829
|
+
summary: `${mindName} updated ${file}`,
|
|
830
|
+
metadata: { file, iframeUrl: `/ext/pages/public/${mindName}/${file}` }
|
|
831
|
+
});
|
|
832
|
+
try {
|
|
833
|
+
const mod = await import("./pages-watcher-72OVPRMH.js");
|
|
834
|
+
mod.invalidateCache();
|
|
835
|
+
} catch {
|
|
836
|
+
}
|
|
837
|
+
return { output: `Notified: ${file}` };
|
|
838
|
+
};
|
|
630
839
|
var src_default2 = createExtension({
|
|
631
840
|
id: "pages",
|
|
632
841
|
name: "Pages",
|
|
@@ -634,6 +843,13 @@ var src_default2 = createExtension({
|
|
|
634
843
|
description: "Publish and serve web pages from mind directories",
|
|
635
844
|
routes: (ctx) => createRoutes2(ctx),
|
|
636
845
|
publicRoutes: (ctx) => createPublicRoutes(ctx),
|
|
846
|
+
commands: {
|
|
847
|
+
notify: {
|
|
848
|
+
description: "Notify that a page was created or updated",
|
|
849
|
+
usage: "volute pages notify [filename]",
|
|
850
|
+
handler: notifyHandler
|
|
851
|
+
}
|
|
852
|
+
},
|
|
637
853
|
skillsDir: skillsDir2,
|
|
638
854
|
standardSkill: true,
|
|
639
855
|
ui: {
|
|
@@ -643,7 +859,13 @@ var src_default2 = createExtension({
|
|
|
643
859
|
label: "Pages",
|
|
644
860
|
urlPatterns: ["/pages", "/pages/:site", "/pages/:site/:path"]
|
|
645
861
|
},
|
|
646
|
-
mindSections: [
|
|
862
|
+
mindSections: [
|
|
863
|
+
{
|
|
864
|
+
id: "pages",
|
|
865
|
+
label: "Pages",
|
|
866
|
+
icon: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M2 8h12M8 2c-2 2-2 10 0 12M8 2c2 2 2 10 0 12"/></svg>'
|
|
867
|
+
}
|
|
868
|
+
],
|
|
647
869
|
feedSource: {
|
|
648
870
|
endpoint: "/api/ext/pages/feed"
|
|
649
871
|
}
|
|
@@ -668,170 +890,110 @@ var src_default2 = createExtension({
|
|
|
668
890
|
}
|
|
669
891
|
});
|
|
670
892
|
|
|
671
|
-
// src/lib/
|
|
672
|
-
import {
|
|
673
|
-
import {
|
|
674
|
-
var
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
async function createUser(username, password) {
|
|
685
|
-
const db = await getDb();
|
|
686
|
-
const hash = hashSync(password, 10);
|
|
687
|
-
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
|
|
688
|
-
const role = value === 0 ? "admin" : "pending";
|
|
689
|
-
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning(userSelectFields);
|
|
690
|
-
return result;
|
|
691
|
-
}
|
|
692
|
-
async function verifyUser(username, password) {
|
|
693
|
-
const db = await getDb();
|
|
694
|
-
const row = await db.select().from(users).where(eq(users.username, username)).get();
|
|
695
|
-
if (!row) return null;
|
|
696
|
-
if (row.user_type === "mind") return null;
|
|
697
|
-
if (!compareSync(password, row.password_hash)) return null;
|
|
698
|
-
const { password_hash: _, ...user } = row;
|
|
699
|
-
return user;
|
|
700
|
-
}
|
|
701
|
-
async function getUser(id) {
|
|
702
|
-
const db = await getDb();
|
|
703
|
-
const row = await db.select(userSelectFields).from(users).where(eq(users.id, id)).get();
|
|
704
|
-
return row ?? null;
|
|
705
|
-
}
|
|
706
|
-
async function getUserByUsername(username) {
|
|
707
|
-
const db = await getDb();
|
|
708
|
-
const row = await db.select(userSelectFields).from(users).where(eq(users.username, username)).get();
|
|
709
|
-
return row ?? null;
|
|
710
|
-
}
|
|
711
|
-
async function listUsers() {
|
|
712
|
-
const db = await getDb();
|
|
713
|
-
return db.select(userSelectFields).from(users).orderBy(users.created_at).all();
|
|
714
|
-
}
|
|
715
|
-
async function listPendingUsers() {
|
|
716
|
-
const db = await getDb();
|
|
717
|
-
return db.select(userSelectFields).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
718
|
-
}
|
|
719
|
-
async function listUsersByType(userType) {
|
|
720
|
-
const db = await getDb();
|
|
721
|
-
return db.select(userSelectFields).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
|
|
722
|
-
}
|
|
723
|
-
async function getOrCreateMindUser(mindName) {
|
|
724
|
-
const db = await getDb();
|
|
725
|
-
const existing = await db.select(userSelectFields).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
726
|
-
if (existing) return existing;
|
|
893
|
+
// src/lib/daemon/turn-tracker.ts
|
|
894
|
+
import { randomUUID } from "crypto";
|
|
895
|
+
import { eq } from "drizzle-orm";
|
|
896
|
+
var tlog = logger_default.child("turn-tracker");
|
|
897
|
+
var activeTurns = /* @__PURE__ */ new Map();
|
|
898
|
+
function key(mind, session) {
|
|
899
|
+
return `${mind}:${session ?? "*"}`;
|
|
900
|
+
}
|
|
901
|
+
async function createTurn(mind) {
|
|
902
|
+
const k = key(mind);
|
|
903
|
+
const existing = activeTurns.get(k);
|
|
904
|
+
if (existing) return existing.turnId;
|
|
905
|
+
const turnId = randomUUID();
|
|
727
906
|
try {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
password_hash: "!mind",
|
|
731
|
-
role: "user",
|
|
732
|
-
user_type: "mind"
|
|
733
|
-
}).returning(userSelectFields);
|
|
734
|
-
return result;
|
|
907
|
+
const db = await getDb();
|
|
908
|
+
await db.insert(turns).values({ id: turnId, mind, status: "active" });
|
|
735
909
|
} catch (err) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (retried) return retried;
|
|
739
|
-
}
|
|
740
|
-
throw err;
|
|
910
|
+
tlog.error(`failed to create turn for ${mind}`, logger_default.errorData(err));
|
|
911
|
+
return void 0;
|
|
741
912
|
}
|
|
913
|
+
activeTurns.set(k, { turnId, lastToolUseEventId: void 0 });
|
|
914
|
+
return turnId;
|
|
742
915
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
|
|
746
|
-
}
|
|
747
|
-
async function changePassword(userId, currentPassword, newPassword) {
|
|
748
|
-
const db = await getDb();
|
|
749
|
-
const row = await db.select().from(users).where(eq(users.id, userId)).get();
|
|
750
|
-
if (!row) return false;
|
|
751
|
-
if (!compareSync(currentPassword, row.password_hash)) return false;
|
|
752
|
-
const hash = hashSync(newPassword, 10);
|
|
753
|
-
await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
async function approveUser(id) {
|
|
757
|
-
const db = await getDb();
|
|
758
|
-
await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
759
|
-
}
|
|
760
|
-
async function countAdmins() {
|
|
761
|
-
const db = await getDb();
|
|
762
|
-
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.role, "admin"));
|
|
763
|
-
return value;
|
|
916
|
+
function getActiveTurnId(mind, session) {
|
|
917
|
+
return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
|
|
764
918
|
}
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
if (!target) throw new Error("User not found");
|
|
769
|
-
await db.update(users).set({ role }).where(eq(users.id, id));
|
|
919
|
+
function trackToolUse(mind, session, eventId) {
|
|
920
|
+
const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
|
|
921
|
+
if (entry) entry.lastToolUseEventId = eventId;
|
|
770
922
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const target = await db.select({ id: users.id }).from(users).where(and(eq(users.id, id), eq(users.user_type, "brain"))).get();
|
|
774
|
-
if (!target) throw new Error("User not found");
|
|
775
|
-
await db.delete(users).where(and(eq(users.id, id), eq(users.user_type, "brain")));
|
|
923
|
+
function getLastToolUseEventId(mind, session) {
|
|
924
|
+
return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
|
|
776
925
|
}
|
|
777
|
-
async function
|
|
778
|
-
const
|
|
779
|
-
const
|
|
780
|
-
if (!
|
|
781
|
-
|
|
926
|
+
async function assignSession(mind, turnId, session) {
|
|
927
|
+
const wildcardKey = key(mind);
|
|
928
|
+
const entry = activeTurns.get(wildcardKey);
|
|
929
|
+
if (!entry || entry.turnId !== turnId) {
|
|
930
|
+
tlog.warn(`assignSession: no matching turn for ${mind} (turnId=${turnId}, session=${session})`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
try {
|
|
934
|
+
const db = await getDb();
|
|
935
|
+
await db.update(turns).set({ session }).where(eq(turns.id, turnId));
|
|
936
|
+
} catch (err) {
|
|
937
|
+
tlog.error(`failed to assign session to turn ${turnId}`, logger_default.errorData(err));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
activeTurns.delete(wildcardKey);
|
|
941
|
+
activeTurns.set(key(mind, session), entry);
|
|
942
|
+
}
|
|
943
|
+
async function completeTurn(mind, session) {
|
|
944
|
+
const k = key(mind, session);
|
|
945
|
+
const wildcardKey = key(mind);
|
|
946
|
+
const entry = activeTurns.get(k) ?? activeTurns.get(wildcardKey);
|
|
947
|
+
if (!entry) return void 0;
|
|
948
|
+
try {
|
|
949
|
+
const db = await getDb();
|
|
950
|
+
await db.update(turns).set({ status: "complete" }).where(eq(turns.id, entry.turnId));
|
|
951
|
+
} catch (err) {
|
|
952
|
+
tlog.error(`failed to complete turn ${entry.turnId}`, logger_default.errorData(err));
|
|
953
|
+
return void 0;
|
|
954
|
+
}
|
|
955
|
+
activeTurns.delete(k);
|
|
956
|
+
activeTurns.delete(wildcardKey);
|
|
957
|
+
return entry.turnId;
|
|
782
958
|
}
|
|
783
|
-
async function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
const changed = user.display_name !== newProfile.display_name || user.description !== newProfile.description || user.avatar !== newProfile.avatar;
|
|
791
|
-
if (!changed) return;
|
|
792
|
-
const db = await getDb();
|
|
793
|
-
await db.update(users).set(newProfile).where(eq(users.id, user.id));
|
|
794
|
-
broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
|
|
959
|
+
async function setSummaryEventId(turnId, summaryEventId) {
|
|
960
|
+
try {
|
|
961
|
+
const db = await getDb();
|
|
962
|
+
await db.update(turns).set({ summary_event_id: summaryEventId }).where(eq(turns.id, turnId));
|
|
963
|
+
} catch (err) {
|
|
964
|
+
tlog.error(`failed to set summary event for turn ${turnId}`, logger_default.errorData(err));
|
|
965
|
+
}
|
|
795
966
|
}
|
|
796
|
-
async function
|
|
797
|
-
const
|
|
798
|
-
|
|
967
|
+
async function clearMind(mind) {
|
|
968
|
+
const toDelete = [];
|
|
969
|
+
const turnIds = [];
|
|
970
|
+
for (const [k, entry] of activeTurns.entries()) {
|
|
971
|
+
if (k.startsWith(`${mind}:`)) {
|
|
972
|
+
turnIds.push(entry.turnId);
|
|
973
|
+
toDelete.push(k);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
for (const k of toDelete) activeTurns.delete(k);
|
|
977
|
+
if (turnIds.length > 0) {
|
|
978
|
+
try {
|
|
979
|
+
const db = await getDb();
|
|
980
|
+
for (const id of turnIds) {
|
|
981
|
+
await db.update(turns).set({ status: "complete" }).where(eq(turns.id, id));
|
|
982
|
+
}
|
|
983
|
+
} catch (err) {
|
|
984
|
+
tlog.error(`failed to complete orphaned turns for ${mind}`, logger_default.errorData(err));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
799
987
|
}
|
|
800
988
|
|
|
801
989
|
// src/lib/systems-config.ts
|
|
802
|
-
import {
|
|
803
|
-
existsSync,
|
|
804
|
-
mkdirSync,
|
|
805
|
-
readFileSync,
|
|
806
|
-
renameSync,
|
|
807
|
-
unlinkSync,
|
|
808
|
-
writeFileSync
|
|
809
|
-
} from "fs";
|
|
990
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
810
991
|
import { resolve as resolve4 } from "path";
|
|
811
992
|
var DEFAULT_API_URL = "https://volute.systems";
|
|
812
993
|
function configPath() {
|
|
813
994
|
return resolve4(voluteSystemDir(), "systems.json");
|
|
814
995
|
}
|
|
815
|
-
function migrateIfNeeded() {
|
|
816
|
-
const target = configPath();
|
|
817
|
-
if (existsSync(target)) return;
|
|
818
|
-
const oldPaths = [
|
|
819
|
-
resolve4(voluteUserHome(), "systems.json"),
|
|
820
|
-
resolve4(voluteHome(), "systems.json")
|
|
821
|
-
];
|
|
822
|
-
for (const old of oldPaths) {
|
|
823
|
-
if (old !== target && existsSync(old)) {
|
|
824
|
-
try {
|
|
825
|
-
mkdirSync(voluteSystemDir(), { recursive: true });
|
|
826
|
-
renameSync(old, target);
|
|
827
|
-
} catch {
|
|
828
|
-
}
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
996
|
function readSystemsConfig() {
|
|
834
|
-
migrateIfNeeded();
|
|
835
997
|
const path = configPath();
|
|
836
998
|
if (!existsSync(path)) return null;
|
|
837
999
|
const raw = readFileSync(path, "utf-8");
|
|
@@ -904,69 +1066,6 @@ async function openExtensionDb(_id, dataDir) {
|
|
|
904
1066
|
const Database = await getLibsqlDatabase();
|
|
905
1067
|
return new Database(dbPath);
|
|
906
1068
|
}
|
|
907
|
-
async function migrateNotesFromCoreDb(extDb) {
|
|
908
|
-
const coreDbPath = process.env.VOLUTE_DB_PATH || resolve5(voluteSystemDir(), "volute.db");
|
|
909
|
-
if (!existsSync2(coreDbPath)) return;
|
|
910
|
-
const existing = extDb.prepare("SELECT COUNT(*) as c FROM notes").get();
|
|
911
|
-
if (existing.c > 0) return;
|
|
912
|
-
const Database = await getLibsqlDatabase();
|
|
913
|
-
const coreDb = new Database(coreDbPath);
|
|
914
|
-
try {
|
|
915
|
-
const tableExists = coreDb.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'").get();
|
|
916
|
-
if (!tableExists) return;
|
|
917
|
-
const coreNotes = coreDb.prepare(
|
|
918
|
-
"SELECT id, author_id, title, slug, content, reply_to_id, created_at, updated_at FROM notes ORDER BY id"
|
|
919
|
-
).all();
|
|
920
|
-
if (coreNotes.length === 0) return;
|
|
921
|
-
logger_default.info(`migrating ${coreNotes.length} notes from core DB to extension DB`);
|
|
922
|
-
const coreComments = coreDb.prepare("SELECT id, note_id, author_id, content, created_at FROM note_comments ORDER BY id").all();
|
|
923
|
-
const coreReactions = coreDb.prepare("SELECT id, note_id, user_id, emoji, created_at FROM note_reactions ORDER BY id").all();
|
|
924
|
-
extDb.exec("BEGIN TRANSACTION");
|
|
925
|
-
try {
|
|
926
|
-
for (const note of coreNotes) {
|
|
927
|
-
extDb.prepare(
|
|
928
|
-
"INSERT OR IGNORE INTO notes (id, author_id, title, slug, content, reply_to_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
929
|
-
).run(
|
|
930
|
-
note.id,
|
|
931
|
-
note.author_id,
|
|
932
|
-
note.title,
|
|
933
|
-
note.slug,
|
|
934
|
-
note.content,
|
|
935
|
-
note.reply_to_id,
|
|
936
|
-
note.created_at,
|
|
937
|
-
note.updated_at
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
for (const comment of coreComments) {
|
|
941
|
-
extDb.prepare(
|
|
942
|
-
"INSERT OR IGNORE INTO note_comments (id, note_id, author_id, content, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
943
|
-
).run(comment.id, comment.note_id, comment.author_id, comment.content, comment.created_at);
|
|
944
|
-
}
|
|
945
|
-
for (const reaction of coreReactions) {
|
|
946
|
-
extDb.prepare(
|
|
947
|
-
"INSERT OR IGNORE INTO note_reactions (id, note_id, user_id, emoji, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
948
|
-
).run(
|
|
949
|
-
reaction.id,
|
|
950
|
-
reaction.note_id,
|
|
951
|
-
reaction.user_id,
|
|
952
|
-
reaction.emoji,
|
|
953
|
-
reaction.created_at
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
extDb.exec("COMMIT");
|
|
957
|
-
} catch (txErr) {
|
|
958
|
-
extDb.exec("ROLLBACK");
|
|
959
|
-
throw txErr;
|
|
960
|
-
}
|
|
961
|
-
logger_default.info(
|
|
962
|
-
`migrated ${coreNotes.length} notes, ${coreComments.length} comments, ${coreReactions.length} reactions`
|
|
963
|
-
);
|
|
964
|
-
} catch (err) {
|
|
965
|
-
logger_default.error("failed to migrate notes from core DB", logger_default.errorData(err));
|
|
966
|
-
} finally {
|
|
967
|
-
coreDb.close();
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
1069
|
async function buildContext(manifest, dataDir, authMw) {
|
|
971
1070
|
let db = null;
|
|
972
1071
|
if (manifest.initDb) {
|
|
@@ -977,9 +1076,6 @@ async function buildContext(manifest, dataDir, authMw) {
|
|
|
977
1076
|
realDb.close();
|
|
978
1077
|
throw new Error(`initDb failed for extension ${manifest.id}: ${err.message}`);
|
|
979
1078
|
}
|
|
980
|
-
if (manifest.id === "notes") {
|
|
981
|
-
await migrateNotesFromCoreDb(realDb);
|
|
982
|
-
}
|
|
983
1079
|
db = realDb;
|
|
984
1080
|
}
|
|
985
1081
|
return {
|
|
@@ -992,8 +1088,15 @@ async function buildContext(manifest, dataDir, authMw) {
|
|
|
992
1088
|
},
|
|
993
1089
|
getUser: async (id) => getUser(id),
|
|
994
1090
|
getUserByUsername: async (username) => getUserByUsername(username),
|
|
995
|
-
publishActivity: (event) => {
|
|
996
|
-
|
|
1091
|
+
publishActivity: (event, sessionOrContext) => {
|
|
1092
|
+
const session = typeof sessionOrContext === "string" ? sessionOrContext : sessionOrContext?.get("mindSession");
|
|
1093
|
+
const turnId = getActiveTurnId(event.mind, session);
|
|
1094
|
+
const sourceEventId = getLastToolUseEventId(event.mind, session);
|
|
1095
|
+
publish({
|
|
1096
|
+
...event,
|
|
1097
|
+
turn_id: turnId,
|
|
1098
|
+
source_event_id: sourceEventId
|
|
1099
|
+
}).catch(
|
|
997
1100
|
(err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
|
|
998
1101
|
);
|
|
999
1102
|
},
|
|
@@ -1030,6 +1133,35 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1030
1133
|
const publicApp = manifest.publicRoutes(context);
|
|
1031
1134
|
app.route(`/ext/${manifest.id}/public`, publicApp);
|
|
1032
1135
|
}
|
|
1136
|
+
if (manifest.commands) {
|
|
1137
|
+
for (const [cmdName, cmd] of Object.entries(manifest.commands)) {
|
|
1138
|
+
app.post(`${extApiPath}/commands/${cmdName}`, async (c) => {
|
|
1139
|
+
let body;
|
|
1140
|
+
try {
|
|
1141
|
+
body = await c.req.json();
|
|
1142
|
+
} catch {
|
|
1143
|
+
return c.json({ error: "Invalid JSON in request body" }, 400);
|
|
1144
|
+
}
|
|
1145
|
+
const user = c.get("user");
|
|
1146
|
+
const mindName = body.mind || user?.username;
|
|
1147
|
+
const session = c.get("mindSession");
|
|
1148
|
+
try {
|
|
1149
|
+
const result = await cmd.handler(body.args ?? [], {
|
|
1150
|
+
...context,
|
|
1151
|
+
// Bind publishActivity to the session so command handlers
|
|
1152
|
+
// don't need to pass it explicitly
|
|
1153
|
+
publishActivity: (event, sc) => context.publishActivity(event, sc ?? session),
|
|
1154
|
+
mindName,
|
|
1155
|
+
session
|
|
1156
|
+
});
|
|
1157
|
+
return c.json(result);
|
|
1158
|
+
} catch (err) {
|
|
1159
|
+
logger_default.error(`extension command ${manifest.id}/${cmdName} failed`, logger_default.errorData(err));
|
|
1160
|
+
return c.json({ error: err.message }, 500);
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1033
1165
|
let resolvedAssetsDir = manifest.ui?.assetsDir ?? "";
|
|
1034
1166
|
if (resolvedAssetsDir && !existsSync2(resolvedAssetsDir)) {
|
|
1035
1167
|
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
@@ -1228,17 +1360,42 @@ async function loadAllExtensions(app, authMw) {
|
|
|
1228
1360
|
logger_default.error(`failed to load extension: ${manifest.id}`, logger_default.errorData(err));
|
|
1229
1361
|
}
|
|
1230
1362
|
}
|
|
1363
|
+
app.get("/api/extensions/commands", (c) => {
|
|
1364
|
+
const result = {};
|
|
1365
|
+
for (const { manifest } of loaded) {
|
|
1366
|
+
if (!manifest.commands) continue;
|
|
1367
|
+
const cmds = {};
|
|
1368
|
+
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1369
|
+
cmds[name] = { description: cmd.description, ...cmd.usage ? { usage: cmd.usage } : {} };
|
|
1370
|
+
}
|
|
1371
|
+
result[manifest.id] = { commands: cmds };
|
|
1372
|
+
}
|
|
1373
|
+
return c.json(result);
|
|
1374
|
+
});
|
|
1231
1375
|
}
|
|
1232
1376
|
function getLoadedExtensions() {
|
|
1233
|
-
return loaded.map(({ manifest }) =>
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1377
|
+
return loaded.map(({ manifest }) => {
|
|
1378
|
+
let commands;
|
|
1379
|
+
if (manifest.commands) {
|
|
1380
|
+
commands = {};
|
|
1381
|
+
for (const [name, cmd] of Object.entries(manifest.commands)) {
|
|
1382
|
+
commands[name] = {
|
|
1383
|
+
description: cmd.description,
|
|
1384
|
+
...cmd.usage ? { usage: cmd.usage } : {}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
id: manifest.id,
|
|
1390
|
+
name: manifest.name,
|
|
1391
|
+
version: manifest.version,
|
|
1392
|
+
description: manifest.description,
|
|
1393
|
+
systemSection: manifest.ui?.systemSection,
|
|
1394
|
+
mindSections: manifest.ui?.mindSections,
|
|
1395
|
+
feedSource: manifest.ui?.feedSource,
|
|
1396
|
+
commands
|
|
1397
|
+
};
|
|
1398
|
+
});
|
|
1242
1399
|
}
|
|
1243
1400
|
function getExtensionStandardSkills() {
|
|
1244
1401
|
const skills = [];
|
|
@@ -1300,23 +1457,14 @@ function notifyExtensionsMindStop(mindName) {
|
|
|
1300
1457
|
}
|
|
1301
1458
|
|
|
1302
1459
|
export {
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
deleteMindUser,
|
|
1312
|
-
changePassword,
|
|
1313
|
-
approveUser,
|
|
1314
|
-
countAdmins,
|
|
1315
|
-
setUserRole,
|
|
1316
|
-
deleteUser,
|
|
1317
|
-
updateUserProfile,
|
|
1318
|
-
syncMindProfile,
|
|
1319
|
-
migrateMindRoles,
|
|
1460
|
+
createTurn,
|
|
1461
|
+
getActiveTurnId,
|
|
1462
|
+
trackToolUse,
|
|
1463
|
+
getLastToolUseEventId,
|
|
1464
|
+
assignSession,
|
|
1465
|
+
completeTurn,
|
|
1466
|
+
setSummaryEventId,
|
|
1467
|
+
clearMind,
|
|
1320
1468
|
readSystemsConfig,
|
|
1321
1469
|
writeSystemsConfig,
|
|
1322
1470
|
deleteSystemsConfig,
|